# Tecs > Typed entity component system and game engine for Lua. --- ## Tecs CLI # Tecs CLI The `tecs` executable carries the engine, Teal toolchain, project template, and native services. It builds games without LuaRocks or a compiler on the player's machine. ## Commands | Command | What it does | | ------------------------------- | --------------------------------------------------------------------- | | `tecs new ` | Scaffold a working project; `--force` permits replacement | | `tecs check [paths]` | Type-check against the engine's installed Teal types | | `tecs format [--check] [paths]` | Format, or report files that are not formatted | | `tecs test` | Compile and run the project's specs | | `tecs build` | Compile sources and stage assets in the project's build directory | | `tecs run [entry] [-- args...]` | Build, then replace the CLI process with the selected game entry | | `tecs clean` | Remove the project's build directory | | `tecs info` | Print versions, pinned revisions, project details and package targets | | `tecs docs [query]` | Browse or search the offline reference carried with the tool | | `tecs mcp` | Connect an MCP client on stdio to a running game's HTTP endpoint | | `tecs completions ` | Print a Bash, Zsh, or Fish completion script | Run `tecs help`, `tecs --help`, or ` --help` for the complete command reference. `tecs --version` prints the CLI version. A project is a directory containing `tecs.lua`. Project commands search upward for it, so they work from any directory inside the project. ## Entries and arguments With no operand, `tecs run` uses the `entry` configured in `tecs.lua`. A project-relative `.tl` or `.lua` path selects another application entry for that invocation: ```bash tecs run tecs run src/editor.tl tecs run tools/asset-preview.lua tecs run src/main.tl -- --debug "save slot 2" ``` The build type-checks and compiles a Teal entry. A Lua entry runs after the project build. Both receive the `tecs` global, can require compiled project modules, and must return `tecs.newApplication(...)`. Only operands after `--` become game arguments. The application receives them in `arg[1]` through `arg[#arg]`. ## Shell completion `tecs completions bash|zsh|fish` prints a completion script generated from the command's own parser. Install it for the shell that runs `tecs`: ```sh # Bash: add this to ~/.bashrc. eval "$(tecs completions bash)" # Zsh: write _tecs into a directory on fpath. tecs completions zsh > "${fpath[1]}/_tecs" # Fish tecs completions fish > ~/.config/fish/completions/tecs.fish ``` ## Offline reference `tecs docs` prints an index of API pages and guides. A short page name resolves the page, while a fully-qualified public name resolves its generated reference: ```bash tecs docs tecs docs physics tecs docs tecs.physics.attach ``` The single-file executable embeds this site's Markdown. `tecs docs` works outside a project and never uses the network. An exact or unique match exits zero. A missing query exits one. An ambiguous query lists its matches and exits two. ## MCP bridge Configure the agent to start the single-file CLI as an MCP stdio server: ```json { "mcpServers": { "tecs": { "command": "tecs", "args": ["mcp"] } } } ``` `tecs mcp` keeps stdout for MCP, discovers a running game's Streamable HTTP endpoint at `/mcp`, and proxies the game's tools to the client. It writes diagnostics to stderr. Discovery checks three loopback ports beginning at `TECS_MCP_PORT`, or `19999` by default. It retries after one second while the game starts. After a restart or failed connection, the next operation scans the ports and reconnects. ## Module contents ### Submodules | Submodule | Description | | --- | --- | | [`Projects`](/cli/projects/) | The tecs.lua project manifest, its fields, and entry-file discovery | | [`Scope`](/cli/scope/) | The boundary between project commands and the repository build, plus the offline documentation source | --- ## Projects # Projects A project contains a `tecs.lua` manifest. Project commands search upward from the working directory, so they also work from a subdirectory. ```lua return { name = "hello", identifier = "com.example.hello", entry = "src/main.tl", assets = "assets", window = { title = "Hello", width = 1280, height = 960 }, } ``` The Lua manifest supports comments and uses the same toolchain as the game. Every field except `name` has a default. `tlconfig.lua` is still written by `tecs new`, because `tl` needs it, but it is not the project marker. The entry file compiles to `build/main.lua` and must return an application. See [getting started](/getting-started#entry-file). --- ## Scope # Scope ## Project and engine builds Cargo builds the engine and the CLI. The CLI builds games. `cargo xtask example ui-demo` runs this repository's showcase; `tecs run` builds and runs a project. The CLI builds for its host platform. It does not cross-compile or package releases. ## Offline documentation This site and `tecs docs` read the same Markdown. Page descriptions label the offline index. Product staging copies the pages into the CLI payload, and `cargo xtask docs-check` validates them. --- ## Cooperative I/O # Cooperative I/O Tecs uses one style for finite work: call the operation and use its result. A game does not choose between a callback, Future, Task, async suffix, or await keyword. A direct system call returns inline when ready or parks the logical update while mio, SDL AsyncIO, Tokio, or a bounded CPU lane makes progress; both paths continue the same system before later systems run and the phase commits once ```teal world:addSystem({ name = "game.LoadShips", phase = tecs.ecs.phases.Update, run = function() for entity, request in pendingShips:iter() do -- A cache hit returns inline. A miss resumes on this line after -- file acquisition and image decoding finish off the main thread. local image = tecs.assets.loadImage(request.sprite) local sprite = app.renderer.sprites:registerImage( image ) world:set(entity, sprite) world:remove(entity, LoadShip) end end, }) ``` The call has the same signature outside a system: ```teal local image = tecs.assets.loadImage("sprites/player.png") ``` The context changes how Tecs waits, not what the API returns. An unresolved operation parks the world's reusable logical-update coroutine when called by a normal system. Startup, shutdown, and headless code block their caller while driving the same private producer. Completion-backed calls use their declared finite wait budgets and report failure at the direct call when a budget is exhausted. Socket reads wait for input or closure, and readiness methods use the timeout supplied by their caller. ## A suspended system keeps its place Systems still run in declared order. The first unresolved call parks the whole logical update. SDL continues processing events and completion queues, but a later system does not overtake the parked one and extraction never sees a half-finished phase. ```mermaid flowchart TB A["Scheduler calls a system"] --> B["System calls a finite API"] B --> C{"Result ready?"} C -- "Yes" --> D["Return inline"] C -- "No" --> E["Register private completion"] E --> F["Park reusable world coroutine"] F --> G["SDL keeps pumping input and I/O"] G --> H["Queue completion on the main thread"] H --> I["Resume at the original call"] I --> J["Run remaining systems in order"] J --> K["Commit the completed phase once"] ``` Deferred-only mutation is what makes this coherent. Archetypes do not publish structural changes while a query is suspended, and only the scheduler commits at phase boundaries. ## Overlapping waits at one call site Because one wait parks the whole update, five calls in a row cost the sum of their five waits. `tecs.batch` runs its callbacks at the same time and returns their results in the order they were given, so the same five cost about the longest one: ```teal local answers = tecs.batch({ function(): any return client:send({url = manifest}) end, function(): any return tecs.io.files.read("save.json") end, function(): any return tecs.assets.loadImage("sprites/player.png") end, }) ``` Each callback is its own cooperative task, so one that waits releases the others. The call suspends the system until every callback settles. The first callback that raises, in the order the callbacks were given, cancels the ones still running, waits for them to unwind their scopes, and raises that failure from `batch`. Each callback reports once: a later callback that failed earlier in time raises when `batch` reaches it. Callbacks return values for the caller to apply. Staging a spawn inside a callback hands out an entity identifier at the moment that callback runs, which makes identifiers, and the snapshots that carry them, depend on which wait finished first. Reading and computing inside callbacks and mutating after `batch` returns keeps that order fixed. ## Coroutines wait; threads and reactors do work Coroutines do not make a blocking decoder or disk syscall asynchronous. Tecs routes work according to what it needs: | Work | Execution place | | ---------------------------------------------------------------- | ------------------------------ | | Cache hits, memory Readers, URI parsing, ECS and GPU publication | Main thread | | TCP, UDP, timers, and pollable handles | Native `mio` readiness reactor | | Bulk regular-file reads and writes | One bounded SDL AsyncIO queue | | File-backed HTTP request bodies | Tokio HTTP file stream | | Opens, metadata, directories, and uncovered platform calls | Bounded blocking-I/O lane | | Image decode and other expensive transformations | Separate bounded CPU lane | | Game-supplied computation in its own Lua state | A `tecs.workers` worker thread | An asset miss therefore reads through SDL AsyncIO, decodes in the CPU lane, publishes the result on the main thread, and resumes the system. A cache hit does none of that work and touches no coroutine completion. A worker is the lane a game writes itself. `Worker:receive` follows the same rule as everything above it: a ready result returns inline, a wait suspends the system, and outside a system the call blocks its own caller. The pump takes results once per frame, so a suspended receive can resume up to one frame after the worker sent its answer. A caller that cannot spend that frame polls with `worker:receive(0)` and does its own work in the meantime. ```teal world:addSystem({ name = "game.HashLevel", phase = tecs.ecs.phases.Update, run = function() hasher:send({name = "level1", bytes = level}) -- Other systems, rendering, and input continue while the worker runs. local answer = hasher:receive(-1) world:setResource(LevelHash, answer.hash) end, }) ``` `Worker:call` is the same wait with the request attached, for a worker that answers rather than streams. It sends a request and returns the reply to that request, so the two lines above become one: ```teal local answer = hasher:call({name = "level1", bytes = level}) ``` A channel is a stream, so the pairing is not free: each call carries an identifier, takes only the reply that carries the same one, and leaves everything else queued for `receive`. A canceled call drops its identifier, and the reply that arrives for it afterwards is discarded rather than handed to the next caller. The worker answers from `Self:serve`, which reads a request, runs a handler, and sends the result back under that identifier; a handler that raises fails its own call rather than the worker. The waiting rule is unchanged, and so is its cost. A suspended call resumes up to one frame after the worker replied, and outside a system the call blocks its own caller and pays none of that frame. Both halves cross as serialized bytes, so a request and a reply carry numbers, strings, booleans, and tables of those, and never a live handle, a socket, or cdata. Two calls written in a row cost the sum of both waits; `tecs.batch` overlaps them and returns their results in argument order whatever order the replies arrive in. One worker still serves its own requests one at a time, so overlapping the waits is worth it when the calls go to different workers. ## Streams remain ordinary Readers and Writers A Reader may be memory-backed, a process pipe, a socket, or a progressive HTTP body. Its ordinary read call returns immediately when bytes are ready and waits appropriately when they are not. Each HTTP response body has an independent bounded queue, so an unread body slows only its own transfer while headers and other bodies continue. ```teal local client = tecs.io.http.newClient() local response = client:send({ url = assert(tecs.io.URI.new("https://example.com/levels/one")), }) -- send returns when status and headers exist. Body storage is bounded, so a -- slow consumer applies transport backpressure instead of buffering it all. local scratch = tecs.io.newBuffer(64 * 1024) local reader = assert(response.body:newReader()) while true do local count = assert(reader:readInto(scratch, 0, 64 * 1024)) if count == 0 then break end consume(scratch, count) end reader:close() client:close() ``` Request bodies compose the same way. The client reads an arbitrary streaming body inside client-owned cooperative work, so a socket, process pipe, transform, or another HTTP body may wait without blocking SDL. On the SDL storage backend, a file stream takes a more direct internal route: Tokio opens the path and feeds Reqwest in bounded chunks without retaining the complete file in Lua. Both paths use the same call: ```teal local source = assert( download.body:withMetadata("application/octet-stream") ) local uploaded = client:send({ url = assert(tecs.io.URI.new("https://example.com/uploads/one")), method = "PUT", body = source, }) ``` The upload work belongs to its client rather than to the system that started it. A generic Reader uses a client-owned task; a native file body stays under the client's Tokio request. This matters because `send` returns at response headers while the bounded upload may still be applying transport backpressure. Closing the client cancels and drains that work; application shutdown closes any client that was not closed earlier. `files.read`, `files.write`, file streams, socket operations, process pipes, process waits, native dialogs, asset loads, worker receives, worker calls, and HTTP use this contextual wait rule. There is no public process pump to remember. ## Continuous input is a service, not a forever wait A finite read can complete, fail, time out, or be canceled. A file watcher or platform event feed may continue forever, so the Application ingests those sources into bounded queues and publishes their already received values during `Ingress`. They do not park a world waiting for the next item. Raw listeners, datagram sockets, process output, and worker channels remain owned endpoints. One `accept`, `receive`, or `read` is a finite call: it suspends when used in a system and blocks its caller elsewhere. A plugin that wants one of those endpoints to run continuously owns its lifetime and turns received values into bounded ECS-visible state in an `Ingress` system. Tecs does not silently create an unbounded background inbox. SDL platform events use the same logical boundary. The host seals one retained event batch when a logical update starts. Input is latched once, observers run inside `Ingress`, and events arriving while an observer is suspended belong to the next update. The active batch is released only after the update completes or is canceled. This preserves system and mutation order. External arrival time is still not a deterministic simulation input, so rollback code records or supplies immutable tick input and keeps unresolved I/O outside deterministic phases. --- ## Getting started # Getting started Tecs combines a typed entity component system with a game engine for Teal and LuaJIT. Game state, rendering, audio, physics, and tools share the same world. ## Install the CLI The [Tecs CLI](/cli/) carries the engine, project toolchain, template, and offline reference: ```bash tecs new my-game cd my-game tecs run ``` The generated `tecs.lua` marks the project root and names its entry file. Project commands work from any directory below it. ## Build this repository Contributors build the engine through Cargo: ```bash git clone https://github.com/tecs-dev/tecs.git cd tecs cargo xtask deps cargo xtask build cargo xtask test cargo xtask example ui-demo cargo xtask example scene3d cargo xtask example gltf3d cargo xtask example morph3d ``` `cargo xtask deps` installs system development dependencies. The repository pins Teal, the formatter, and tealdoc revisions for every checkout. `--preset` selects a target. Development presets use system libraries. Package presets build pinned dependencies from source: ```bash cargo xtask presets cargo xtask test # run Rust, ABI, and Lua/Teal tests cargo xtask package --preset macos-arm64 cargo xtask check-package out/package ``` ## Entry file The host owns the loop. The entry file returns an application: ```teal return tecs.newApplication({ window = { title = "Hello", width = 1280, height = 960, }, plugin = function(world: tecs.World, app: tecs.Application) -- Register the game here. end, }) ``` The host loads `tecs` before the entry file, so game code uses it as a global. A headless script or spec loads the same table explicitly: ```teal local tecs = require("tecs") ``` See [`tecs.Application`](/modules/Application) for lifecycle and configuration. ## First plugin The entry plugin registers systems, observers, resources, and entities: ```teal local Transform2D = tecs.Transform2D return tecs.newApplication({ plugin = function(world: tecs.World, app: tecs.Application) local movers = world:newQuery({ include = {Transform2D}, }) world:addSystem({ name = "game.Spin", phase = tecs.ecs.phases.Update, run = function(dt: number) for archetype, length in movers:iter() do local transforms = archetype:getMut( Transform2D ) for row = 1, length do transforms[row].rotation = transforms[row].rotation + dt end end end, }) world:spawn(Transform2D(100, 100)) end, }) ``` Keep three rules visible when writing systems: - Create persistent queries during plugin setup. - Read columns with `get` and mark written columns with `getMut`. - Break or return from `query:iter()` freely; iteration owns no transaction scope or resource that needs cleanup. The [mutation model](/modules/ecs/mutation-model) covers deferred changes and dirty tracking. ## Optional transparent meshes Opaque mesh rendering keeps its original three-pass cull chain. Enable the separate transparent resources only when a game needs glTF `BLEND` materials or registers an `ALPHA_BLEND` material itself: ```teal return tecs.newApplication({ sprites = false, meshes = { transparency = true, }, plugin = function(world: tecs.World, app: tecs.Application) local loaded = tecs.assets.loadGLTF( tecs.io.files.assetPath("models/glass.gltf") ) -- Decoded scalar material factors remain caller-writable until the -- model is registered. This is useful for legacy converted assets. loaded.materials[1].roughness = 0.35 local instance = app.renderer.meshes:registerModel( loaded ):newInstance() for _, primitive in ipairs(instance.primitives) do world:spawn( primitive.transform, primitive.mesh, primitive.bounds, primitive.material, tecs.gfx.Tint(), tecs.gfx.Renderable3D() ) end end, }) ``` The mesh domain frustum-culls and depth-sorts complete indexed commands on the GPU. It draws transparent meshes before the sprite forward lane, so sprites retain deterministic overlay ordering in a renderer that enables both domains. The domain exposes registration methods, configuration, and residency counts; its GPU buffers belong to the internal backend and are not part of the game API. ## Vertex colors, fog, SSAO, bloom, and a 2D HUD Enable each expensive lane explicitly and keep the base 2D and rigid-mesh paths unchanged: ```teal return tecs.newApplication({ bloom = { scale = 0.5, threshold = 0.75, knee = 0.1, intensity = 0.65, }, meshes = { vertexColors = true, fog = { start = 20, finish = 100, r = 0.12, g = 0.16, b = 0.24, }, ssao = { scale = 0.5, radius = 0.9, bias = 0.025, intensity = 1.0, power = 1.5, }, }, plugin = function(world: tecs.World, app: tecs.Application) tecs.gfx.layers.configure( 16, { sort = "z", screenSpace = true, unlit = true, overlay = true, } ) -- The overlay flag selects the sprite forward lane. It runs after -- opaque and transparent meshes and after bloom composition, even -- when this tint is fully opaque. world:spawn( tecs.Transform2D(160, 40, 0, 16, 0, 280, 56), tecs.gfx.Tint(0.02, 0.04, 0.08, 1.0), tecs.gfx.Renderable2D() ) end, }) ``` `assets.newMesh` accepts linear RGBA through `colors`; `assets.loadGLTF` decodes normalized integer or float `COLOR_0` values in VEC3 or VEC4 form. The color multiplies `Tint`, the material base-color factor, and the sampled base-color texture. Its alpha therefore participates in `ALPHA_MASK` and `ALPHA_BLEND` material policy. A colored mesh requires `meshes.vertexColors = true`; omitting the option preserves the 48-byte base vertex stream and creates no color buffer or shader variant. Fog is linear camera-distance fog and applies after both metallic-roughness and unlit material dispatch, in deferred and transparent mesh passes. Its runtime fields are available on `app.renderer.meshes.fog`. Omitting `meshes.fog` keeps the fog-free shaders and uniform path. SSAO reconstructs opaque mesh positions from depth and samples the surrounding world-space hemisphere. `scale` selects the two R8 target sizes and defaults to 0.5. `radius` and `bias` are world units; `intensity` and `power` control the amount and contrast. Two edge-aware blur passes preserve depth and normal boundaries, and linear upsampling avoids block-sized transitions at the default half resolution. The result multiplies the material's authored occlusion before lighting, so it affects ambient and environment light but not direct light. Change the four runtime fields through `app.renderer.meshes.ssao`; changing `scale` requires recreating the renderer. Omitting `meshes.ssao` allocates no AO targets, sampler, uniforms, or pipelines. Transparent meshes and sprites remain outside this opaque G-buffer effect. Run `cargo xtask example animated3d` and press O to compare it on a CC0 animated character and floor. Bloom preserves resolved opaque highlights in packed HDR, extracts them into two scaled blur targets, and adds them before transparent meshes and sprites. The packed lighting and blur formats retain values above white without using more bytes per pixel than RGBA8. `threshold` and `knee` are non-negative HDR brightness values. Omitting `bloom` declares no bloom targets or passes. A light component has no visible geometry of its own, so it blooms the bright opaque surfaces it illuminates rather than drawing a halo at its position. Transparent lamps and UI remain outside this branch. Run `cargo xtask example scene3d` for the complete mixed-domain setup. ## Multiple cameras Set `maxViews` when creating the application, then spawn ordered `View` components. A view may draw either domain or both. Coordinates are fractions of the frame, so this is a two-player split with a 2D HUD composed last: ```teal local View = tecs.gfx.View local left = tecs.gfx.newCamera3D({z = 8}) local right = tecs.gfx.newCamera3D({x = 4, z = 8}) world:spawn(View.new({camera3D = left, width = 0.5, order = 0})) world:spawn(View.new({ camera3D = right, x = 0.5, width = 0.5, order = 1 })) world:spawn(View.new({ camera2D = app.renderer.sprites.camera, order = 2 })) ``` ```teal return tecs.newApplication({ maxViews = 3, meshes = {}, plugin = game, }) ``` The renderer extracts scene instances once. Each view then reuses the same G-buffer, visible lists, light tiles, and transparent intermediate in strict sequence: cull, shade, composite, then overwrite for the next view. The original path remains in use when `maxViews` is omitted, so a one-camera game allocates no multi-camera target and records no extra composition pass. Opaque and transparent metallic-roughness meshes use the same Cook-Torrance direct-light function. Opaque meshes reconstruct world position from the geometry depth target; transparent meshes already carry it from the vertex stage. Roughness uses GGX, visibility uses Smith-Schlick, and Fresnel uses the Schlick approximation. Sprite materials keep Lambert diffuse lighting: their shape normals and one scalar parameter do not define a metallic-roughness PBR surface, and the cheaper term stays isolated from every mesh shader variant. ## Local 3D lights and imported textures Point and spot lights are another independently allocated mesh lane: ```teal return tecs.newApplication({ sprites = false, meshes = { lights = { capacity = 256, shadows = {capacity = 8, size = 256}, }, doubleSided = true, packTextures = false, mipmaps = true, }, plugin = function(world: tecs.World) world:spawn( tecs.Transform3D.new({x = 2, y = 4, z = 1}), tecs.gfx.PointLight3D.new({ radius = 12, r = 1.0, g = 0.6, b = 0.25, intensity = 18, flags = tecs.gfx.LIGHT_CASTS_SHADOWS, }) ) world:spawn( tecs.Transform3D.new({x = 0, y = 8, z = 2}), tecs.gfx.SpotLight3D.new({ radius = 20, innerAngle = math.rad(18), outerAngle = math.rad(32), r = 0.7, g = 0.85, b = 1.0, intensity = 28, flags = tecs.gfx.LIGHT_CASTS_SHADOWS, }) ) end, }) ``` `PointLight3D` uses its transform position. `SpotLight3D` also rotates local negative Z through the transform quaternion. Radius, color, intensity, and cone angles are fixed-layout FFI fields. The renderer bins enabled lights into a 32-by-32 screen grid for every 3D view. Omitting `meshes.lights` creates no queries, buffers, binning dispatch, bindings, or local-light shader variants. Local shadows are a second opt-in under `meshes.lights`. The renderer chooses the first flagged lights in stable extraction order, up to the configured shadow capacity. Each selected point light renders six square cells in one R16 atlas row; a spot light uses the first cell of its row. One conservative GPU compaction per selected light removes instances outside its reach before the point light's six faces or the spot cone rasterize. `bias` defaults to 0.002, and `softness` defaults to a 3-by-3 PCF radius of one texel. Omitting `lights.shadows` allocates no atlas, matrices, indirect commands, sampler, or shadowed local-light shader variants. Directional cascades remain the separate `meshes.shadows` option. An ambient-cube probe adds diffuse environment light without a texture sample: ```teal meshes = { probe = { positiveX = {0.10, 0.12, 0.16}, negativeX = {0.07, 0.08, 0.11}, positiveY = {0.24, 0.30, 0.42}, -- sky negativeY = {0.04, 0.03, 0.02}, -- ground bounce positiveZ = {0.13, 0.10, 0.08}, negativeZ = {0.07, 0.09, 0.12}, intensity = 0.85, }, } ``` The six RGB faces are world-space irradiance and may exceed one. The shader weights them by the squared components of each mesh normal, adds the result to `ambientLight`, and applies ambient occlusion and the material's diffuse-metal split. This is diffuse probe lighting. It remains useful as the lower-cost option when glossy image-based reflections are unnecessary. Assign through `app.renderer.meshes.probe` to change it at runtime. Omitting `meshes.probe` adds no uniform data or fragment work and selects no probe pipeline in a 2D or ordinary 3D application. Prebuilt releases currently carry those optional variants in the shared shader pack. Enable a sampled specular environment independently, then register its six decoded RGBA8 faces. Each direct load waits appropriately for its context: ```teal return tecs.newApplication({ sprites = false, meshes = { environment = { size = 256, intensity = 1.4, skyboxIntensity = 1.0, rotation = 0.0, }, }, plugin = function(_world: tecs.World, app: tecs.Application) local names : {string} = { "positive-x.png", "negative-x.png", "positive-y.png", "negative-y.png", "positive-z.png", "negative-z.png", } local face: {tecs.assets.Image} = {} for index, name in ipairs(names) do face[index] = tecs.assets.loadImage( tecs.io.files.assetPath("environment/studio/" .. name) ) end app.renderer.meshes:registerEnvironment({ positiveX = face[1], negativeX = face[2], positiveY = face[3], negativeY = face[4], positiveZ = face[5], negativeZ = face[6], }) end, }) ``` Every face must be square, exactly `environment.size` pixels, and decoded as RGBA8. Registration validates the complete set before replacing the six GPU layers, consumes the images, and generates their mip chain. Material roughness selects among those mips; the skybox always samples the sharpest level. `app.renderer.meshes.environment` keeps `intensity`, `skyboxIntensity`, and `rotation` caller-writable. Set `skyboxIntensity` to zero for reflections over another background. Omitting `meshes.environment` creates no environment texture, sampler, upload staging, or sampled binding. Run `cargo xtask example ibl3d` to compare five roughness values on metallic and dielectric spheres under six repository-owned CC0 faces. Ordinary glTF images decode to RGBA8. Unpacked mipmapped arrays accept smaller images by repeating their edge through the rest of the fixed layer before GPU mip generation, so neighboring texels never bleed into the sampled UV region. For a large imported scene, select an offline-compressed array instead: ```teal return tecs.newApplication({ sprites = false, meshes = { textureWidth = 1024, textureHeight = 1024, textureLayers = 72, textureFormat = tecs.assets.IMAGE_BC3, packTextures = false, mipmaps = true, }, plugin = game, }) ``` `cargo xtask fetch sponza` downloads a pinned Khronos scene, retains its upstream notice, and writes `Sponza.tecs.gltf` plus complete BC3 mip chains in standard KTX2 containers under the ignored `assets/external` cache. BC3 uses one quarter of RGBA8 texture memory including equivalent mip chains. Creation raises if the selected GPU cannot sample BC3 arrays; it never silently decodes into a larger fallback. The format option uses an integer constant and affects only the mesh array. Every authored glTF primitive becomes an independent bounded culling command. The worker additionally splits any primitive above 65,536 triangles, remaps only the vertices that each chunk references, and preserves color, skin, and morph streams. This keeps a million-triangle source primitive from becoming one all-or-nothing frustum test. Meshoptimizer then improves vertex-cache and vertex-fetch locality inside each command, remapping every optional vertex stream together. Alpha-blended commands retain authored triangle order and use only the lossless fetch remap. Run `cargo xtask example sponza3d` for double-sided materials, compressed mipmaps, point and spot lights, shadows, fog, and bloom together. The example command reports the required fetch command before opening a window when its ignored scene cache is absent. Every 3D example installs `tecs.gfx.FlyCamera3D` as an ordinary Update-phase system. Click to enter relative mouse mode, move with WASD, change height with Q and E, hold Shift to sprint, press Tab to release the pointer, and press Escape to quit. The split-screen `scene3d` example moves its primary left camera and leaves its secondary right camera fixed for comparison. This is noclip movement with no collision or gravity, which keeps scene navigation independent from Rapier. Sponza and Bistro additionally set `showFps`, which refreshes a rolling FPS reading in the window title twice per second without enabling the sprite domain. They default to immediate presentation and accept `TECS_PRESENT=vsync` as an override. `cargo xtask fetch bistro` downloads and verifies the pinned CC BY 4.0 Amazon Lumberyard exterior, decodes its older Draco stream with the reference codec, reconstructs two-channel normal maps, downsamples textures into 512px BC3 KTX2 mip chains, and removes the 986 MB source after producing a roughly 227 MB ignored cache. Run `cargo xtask example bistro3d` to exercise 2.9 million vertices, 8.5 million indices, 1,593 independently culled chunks, the ambient probe, local lights, shadows, fog, and bloom together. The Bistro example starts near late afternoon. Scroll the mouse wheel up toward day or down toward night. The control continuously blends ambient and probe light, the directional sun or moon, fog, and the four lamp lights without rebuilding renderer resources. ## Optional mesh shadows Enable the mesh domain's directional light and shadow resources at renderer creation: ```teal return tecs.newApplication({ ambientLight = {0.12, 0.13, 0.16}, sprites = false, meshes = { shadows = { scale = 1, distance = 40, splitLambda = 0.7, splitBlend = 0.1, depthPadding = 20, directionX = -0.45, directionY = -1, directionZ = -0.3, intensity = 1.4, strength = 0.85, softness = 1.5, }, }, plugin = function(_world: tecs.World, app: tecs.Application) -- Everything except scale remains mutable after creation. app.renderer.meshes.shadow.bias = 0.002 end, }) ``` `scale` fixes all three map sizes and is creation-only. The other fields are copied to `app.renderer.meshes.shadow` and may change between frames. `distance` is the maximum camera depth that receives directional shadows. `splitLambda` distributes resolution between near detail and even depth coverage, `splitBlend` cross-fades boundaries, and `depthPadding` retains casters beyond each receiver slice along the light direction. Meshes outside each stabilized cascade volume are removed by the same ordered GPU mark, scan, and compact shape used for camera culling. Each light-space center snaps to its map's texel grid, so translating the camera does not slide shadow samples across stationary receivers. Culling rejects complete mesh instances; one surviving mesh still draws its full resident index range. Opaque and masked materials cast and receive. Blended materials receive but do not cast. Omitting `meshes.shadows` preserves the shadow-free mesh shaders and allocates no maps, shadow command buffers, cull pipeline, or graphics pipeline. The 2D `shadows` application option remains a separate occluder and drop-shadow system. ## Optional GPU skinning Enable skeletal deformation separately from rigid mesh rendering: ```teal local IDENTITY : {number} = { 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, } return tecs.newApplication({ sprites = false, meshes = { skinning = {jointCapacity = 4096}, }, plugin = function(_world: tecs.World, app: tecs.Application) local skin = app.renderer.meshes:registerSkin( "models/hero#pose", IDENTITY ) -- Spawn `skin` beside Mesh, Bounds3D, MeshMaterial, Tint, and -- Renderable3D. Call updateSkin with the same matrix count later. end, }) ``` `assets.newMesh` accepts four joint indices and four weights per vertex through its separate `joints` and `weights` arrays. `assets.loadGLTF` decodes `JOINTS_0`, `WEIGHTS_0`, skins, and inverse bind matrices. `registerModel` returns shared residency, and each `newInstance` registers independent palettes. Spawn `primitive.skin` beside the rest of a skinned primitive bundle. `updateSkin` stages complete column-major palettes into the next frame rather than submitting a GPU command buffer per call. Joint matrices deform positions, normals, tangents, and shadow casters. Culling still uses the entity's `Bounds3D`, so animated content must supply a sphere large enough for every pose it can reach. Omitting `meshes.skinning` preserves the rigid vertex and instance layouts and allocates no skin attributes, palette offsets, joint matrices, or skinned shader variants. Run `cargo xtask example skinning3d` for a two-joint example and `cargo xtask bench meshskinning` with `BENCH_MESH_SKINNING=0` and `=1` to measure the isolated lane. ## Optional GPU morphing Enable morph deformation independently from rigid meshes and skinning: ```teal return tecs.newApplication({ sprites = false, meshes = { morphing = { vertexCapacity = 1048576, weightCapacity = 65536, }, }, plugin = function(_world: tecs.World, app: tecs.Application) local morph = app.renderer.meshes:registerMorph( "models/face#expression", {0, 0, 0} ) -- Spawn `morph` beside a mesh with three registered targets. Later, -- update the same three weights with `updateMorph`. end, }) ``` `assets.newMesh` accepts target-order position deltas and optional normal and tangent deltas. `assets.loadGLTF` decodes the equivalent glTF targets, mesh and node default weights, and linear, step, or cubic-spline weight animation. `registerModel` gives every `newInstance` private reusable weight vectors; spawn `primitive.morph` beside each morphed primitive. When a glTF primitive has texture coordinates but no authored tangents, the importer generates MikkTSpace tangents. It splits vertices at tangent discontinuities and remaps color, skin, and morph data with them, so normal-map seams remain correct without requiring an offline repair step. Morph deltas are immutable GPU residency. A five-float record locates each instance's geometry and weights, and complete weight vectors are staged only when registered or updated. Morphing runs before skinning when both options are enabled. A domain with both options appends the skin offset to that record, allowing vertex colors, morphing, and skinning to coexist within the backend's eight vertex-storage-buffer limit. `newMesh` and the glTF decoder conservatively enlarge bounds for weights from zero through one; negative or extrapolated weights require a larger caller-supplied `Bounds3D`. Omitting `meshes.morphing` preserves the rigid and skin-only layouts and allocates no target, locator, weight, or morph-shader resources. Run `cargo xtask example morph3d` for the worker-to-GPU path. The example cycles an indexed cube between tall tapered and low twisted targets under a directional light, so its silhouette, highlights, and shadow all expose the deformation. Compare `BENCH_MESH_MORPHING=0` with `=1` under `cargo xtask bench meshmorphing` to measure the isolated lane. ## Animated glTF instances `loadGLTF` decodes translation, rotation, scale, and morph-weight channels using core glTF linear, step, and cubic-spline interpolation. A resident `Model3D` shares that immutable clip data while each instance keeps its own reusable pose, joint palettes, and morph vectors: ```teal local loaded = tecs.assets.loadGLTF("models/hero.gltf") local model = app.renderer.meshes:registerModel(loaded) local instance = model:newInstance() for index, primitive in ipairs(instance.primitives) do local entity = world:spawn( primitive.transform, primitive.mesh, primitive.bounds, primitive.material, primitive.skin, primitive.morph, tecs.gfx.Tint(), tecs.gfx.Renderable3D() ) instance:bind(world, index, entity) end instance:play("Walk") ``` Call `instance:update(dt)` from a system to advance playback, or `instance:sample("Walk", time)` for deterministic explicit sampling. Sampling reuses its tables, composes a caller-writable `instance.transform` after the authored hierarchy, updates bound `Transform3D` components, and stages complete joint palettes without submitting a command buffer. Assign a transform to place, turn, or scale the complete instance without changing its shared model or clip; its default nil value preserves the authored placement and skips the composition. The `Bounds3D` component is still caller-owned and must enclose every pose. Run `cargo xtask example animated3d` to see a CC0 character cycle skeletal clips facing the camera beside an independently placed, six-color morph animation under a shadowed Cook-Torrance directional light, and run `cargo xtask bench modelanimation` for CPU sampling cost and heap growth. ## Game modules Split a game into plugins and install them from the entry plugin: ```teal local enemies = require("game.enemies") local movement = require("game.movement") return tecs.newApplication({ plugin = function(world: tecs.World, app: tecs.Application) world:addPlugin(movement.plugin) world:addPlugin(enemies.plugin) end, }) ``` The host adds the project content root to `package.path`, so `require("game.enemies")` loads `game/enemies.lua`. ## Next pages - [tecs.ecs](/modules/ecs/) introduces worlds, components, queries, and systems. - [Modules](/modules/) lists every public engine module. - [Tecs CLI](/cli/) covers project commands and the offline reference. --- ## Tecs ## Install The `tecs` command carries the compiler, engine, type definitions, and project template. It needs no separate Lua, LuaRocks, Teal, or C compiler installation. ::: code-group ```bash [macOS] brew install tecs-dev/tap/tecs ``` ```powershell [Windows] scoop bucket add tecs https://github.com/tecs-dev/scoop-bucket scoop install tecs ``` ```bash [Linux] brew install tecs-dev/tap/tecs ``` ::: Create a game and run it: ```bash tecs new my-game && cd my-game && tecs run ``` The [Tecs CLI](/cli/) carries the project toolchain in one file. Contributors can build this repository through Cargo; [getting started](/getting-started) covers that workflow. ## Entities are the interface A drawn quad, a light, a sound, a physics body: each one is an entity carrying components, and the subsystem that cares for it finds it by query. A game holds no draw list, no voice handle and no body pointer, so what works on one of them works on all of them. A [snapshot](/modules/ecs/save-games) saves the scene, the [profiler](/modules/ecs/profiling) reports where the frame went, and the [debug server](/modules/io/mcp) inspects and edits any of it while the game runs. The host owns the loop. An entry file returns an application, and the plugin it carries registers work instead of driving frames. ```teal local Transform2D = tecs.Transform2D local gfx = tecs.gfx return tecs.newApplication({ window = {title = "Spin", width = 1280, height = 720}, ambientLight = {0.05, 0.05, 0.08}, plugin = function(world: tecs.World, app: tecs.Application) -- Something on screen is an entity. Nothing issues a draw call for it. world:spawn( Transform2D.new({ x = 640, y = 360, scaleX = 64, scaleY = 64 }), gfx.Tint(0.85, 0.4, 0.3, 1.0), gfx.Renderable2D() ) -- So is the light falling on it, placed by the same Transform2D. world:spawn( Transform2D.new({x = 520, y = 300}), gfx.PointLight2D(120, 600, 1.0, 0.9, 0.7, 3.0) ) local movers = world:newQuery({ include = {Transform2D, gfx.Renderable2D}, }) world:addSystem({ name = "game.Spin", phase = tecs.ecs.phases.Update, run = function(dt: number) for archetype, length in movers:iter() do local transforms = archetype:getMut( Transform2D ) for row = 1, length do transforms[row].rotation = transforms[row].rotation + dt end end end, }) end, }) ``` ## ECS examples ::: code-group ```teal [Components] local world = tecs.ecs.newWorld() -- Define typed components with Teal local record Position is tecs.ecs.Component x: number y: number metamethod __call: function(self, x?: number, y?: number): Position end tecs.ecs.newFFIComponent({ name = "Position", container = Position, fields = {{"x", "float"}, {"y", "float"}}, }) local record Velocity is tecs.ecs.Component x: number y: number metamethod __call: function(self, x?: number, y?: number): Velocity end tecs.ecs.newFFIComponent({ name = "Velocity", container = Velocity, fields = {{"x", "float"}, {"y", "float"}}, }) -- Query and update entities local query = world:newQuery({include = {Position, Velocity}}) world:addSystem({ phase = tecs.ecs.phases.Update, run = function(dt: number, _world: tecs.World) for archetype, len in query:iter() do local positions = archetype:getMut(Position) local velocities = archetype:get(Velocity) for row = 1, len do positions[row].x = positions[row].x + velocities[row].x * dt end end end, }) world:spawn(Position(100, 100), Velocity(10, 0)) ``` ```teal [Relationships] local world = tecs.ecs.newWorld() local ChildOf = tecs.ecs.ChildOf -- Create a parent entity local parent: integer = world:spawn( tecs.ecs.Name("parent"), tecs.Transform2D(100, 100) ) -- ChildOf has cascadeDelete; despawning parent despawns children too. local child1: integer = world:spawn( ChildOf(parent), tecs.ecs.RelativeTransform2D(20, 0) ) local child2: integer = world:spawn( ChildOf(parent), tecs.ecs.RelativeTransform2D(-20, 0) ) -- Walk children of a specific parent world:targets( parent, ChildOf, function(childId: integer) print("child:", childId) end ) -- Despawning parent cascades to child1 and child2 world:despawn(parent) ``` ```teal [Events] local world = tecs.ecs.newWorld() -- Define a custom event local record DamageEvent is tecs.events.Event target: integer amount: number metamethod __call: function( self, target: integer, amount: number ): DamageEvent end -- Wire up the event init hook (it mutates a pre-allocated instance) DamageEvent.init = function( e: DamageEvent, target: integer, amount: number ) e.target = target e.amount = amount end tecs.events.newEvent(DamageEvent) -- Observe events anywhere in your game (0 = world-level) world:observe( 0, DamageEvent, function(e: DamageEvent) local health: Health = world:get(e.target, Health) if health then health.current = health.current - e.amount if health.current <= 0 then world:despawn(e.target) end end end ) -- Emit events from systems world:emit(0, DamageEvent, enemyId, 25) ``` ::: ## Reference The host loads `tecs` before the entry file. A game can use these names without a `require`: ::: module-columns - [`tecs.assets`](/modules/assets) - loading bytes, images and sounds off the main thread - [`tecs.audio`](/modules/audio) - voices, groups, keyed limits, fades, pitch, loop points, streaming, devices - [`tecs.data`](/modules/data) - typed stores, JSON, byte encodings, DEFLATE, transcoding, UTF-8, UUIDs, hashes and checksums - [`tecs.debug`](/modules/debug) - debugger commands, and the debug-server tools they project to - [`tecs.ecs`](/modules/ecs/) - worlds, components, queries, systems, events and resources - [`tecs.events`](/modules/events) - typed events and address-based message buses - [`tecs.gfx`](/modules/gfx/) - the camera, the components, the renderer, text, and the vocabularies below - [`tecs.input`](/modules/input) - gameplay input, gamepads and standalone sensors - [`tecs.io`](/modules/io/) - binary I/O, cooperative sockets, HTTP, and external tools - [`tecs.log`](/modules/log) - named, leveled platform logging - [`tecs.math`](/modules/math) - angle math and two-dimensional geometry - [`tecs.physics`](/modules/physics) - Rapier 2D, solved across a shared thread pool - [`tecs.platform`](/modules/platform/) - platform events, operating-system services, time, and windows - [`tecs.regex`](/modules/regex) - compiled regular expressions over Lua byte strings - [`tecs.sequence`](/modules/sequence) - timelines with the tween runtime inside them - [`tecs.ui`](/modules/ui) - retained layout, scrolling, clipping, and interaction over existing drawing components - [`tecs.workers`](/modules/workers) - typed background jobs Inside one of those, one level and no deeper: - [`tecs.data.utf8`](/modules/data/utf8) - UTF-8 codepoint decoding, encoding, validation and truncation - [`tecs.ecs.random`](/modules/ecs/random) - seeded named streams and standalone generators - [`tecs.gfx.animation`](/modules/gfx/animation) - sprite sheets, and the playback that reads them - [`tecs.gfx.layers`](/modules/gfx/layers) - z-ordering and per-layer behavior - [`tecs.gfx.materials`](/modules/gfx/materials) - one fragment shader, compiled from the material set - [`tecs.gfx.particles`](/modules/gfx/particles) - emitters - [`tecs.io.files`](/modules/io/files) - where a game may read and write, and what to do with a path - [`tecs.io.http`](/modules/io/http) - fetching over HTTP without stopping the frame - [`tecs.io.mcp`](/modules/io/mcp) - the debug server agents and humans drive a running game through - [`tecs.io.Path`](/modules/io/Path) - immutable UTF-8 paths with platform-native component rules - [`tecs.io.Process`](/modules/io/Process) - streaming child processes with backpressured standard I/O - [`tecs.io.URI`](/modules/io/URI) - immutable general-purpose URIs with component-aware modification - [`tecs.io.watcher`](/modules/io/watcher) - watching files for change - [`tecs.math.noise`](/modules/math/noise) - native procedural scalar fields and bulk grids - [`tecs.math.vec2`](/modules/math/vec2) - allocation-free two-dimensional vector and point math - [`tecs.platform.events`](/modules/platform/events) - typed platform events routed through the world - [`tecs.platform.os`](/modules/platform/os) - capabilities, process signals, the clipboard, and desktop services - [`tecs.platform.time`](/modules/platform/time) - clocks, calendar time, delays and frame timing - [`tecs.platform.window`](/modules/platform/window) - the window, its size, its display and its mode On `tecs` itself, because no one module owns them: - [`tecs.Application`](/modules/Application) - the object an entry file returns, and what the host drives - [`tecs.batch`](/modules/#tecs.batch) - runs several waits at one call site and returns their results in order - [`tecs.newApplication`](/modules/Application) - builds the application an entry file returns - [`tecs.scoped`](/modules/#tecs.scoped) - names, profiles, and closes one lexical resource lifetime - [`tecs.Transform2D`](/modules/ecs/builtins#transform) - where an entity is, and the one component every subsystem moves - [`tecs.Transform3D`](/modules/ecs/#tecs.ecs.Transform3D) - a right-handed 3D position, orientation, and scale - [`tecs.version`](/modules/) - the version of this build, as a string ::: ## tecs.ecs Worlds, components, queries, systems, events and resources. One table with two ways in: a game reads it off `tecs` like any other module, and an engine module writes `require("tecs.ecs")`, because `tecs` is the aggregator that pulls every engine module in and a module `tecs` exports cannot also depend on `tecs`. These are the concepts behind the names. ::: module-columns - [Overview](/modules/ecs/) - the model, in one page - [Archetypes](/modules/ecs/archetype) - cache-friendly storage for millions of entities - [Builtins](/modules/ecs/builtins) - names, transforms, hierarchy, TTL, pause, disable, state events - [Bundles](/modules/ecs/components/bundles) - reusable entity templates and batch spawning - [Components](/modules/ecs/components/) - table, tag, scalar and FFI data containers - [Dirty tracking](/modules/ecs/components/dirty-tracking) - change-gated systems and GPU synchronization - [Events](/modules/ecs/events) - type-safe pub/sub and entity lifecycle events - [Mutation model](/modules/ecs/mutation-model) - the normative rules for reads, writes and dirty bits - [Phases](/modules/ecs/phases) - ordered phase scheduling - [Plugins](/modules/ecs/plugins) - modular, shareable game mechanics - [Profiling](/modules/ecs/profiling) - where a frame went - [Queries](/modules/ecs/queries/) - reusable filters with archetype iteration, callbacks and grouping - [Relationships](/modules/ecs/relationships/) - links, hierarchies, relative transforms, cascade deletion - [Save games](/modules/ecs/save-games) - snapshots, component codecs, migrations, resource handlers - [States](/modules/ecs/states) - stack-based game states with transition events - [Systems](/modules/ecs/systems) - phase scheduling, dependencies and run conditions - [World](/modules/ecs/world) - entities, resources and the state stack ::: --- ## tecs.Application # tecs.Application The application that an entry chunk returns. The host owns the loop and calls the application to initialize, process events, iterate, and shut down. Game code registers systems and observers instead of calling a blocking `run` function: ```teal return tecs.newApplication({ window = { title = "Game", width = 1280, height = 720, }, plugin = function(world: tecs.World, app: tecs.Application) local transforms = world:newQuery({ include = {tecs.Transform2D}, }) world:addSystem({ name = "game.Move", phase = tecs.ecs.phases.Update, run = function(dt: number) for archetype, length in transforms:iter() do local column = archetype:getMut( tecs.Transform2D ) for row = 1, length do column[row].x = column[row].x + 120 * dt end end end, }) world:spawn(tecs.Transform2D(100, 100)) end, }) ``` ## Lifecycle The entry plugin receives the world and application after engine subsystem installation and before startup phases. It registers the game. The world then owns the scheduled lifecycle: | Stage | Game API | | --- | --- | | Startup | `PreStartup`, `Startup`, and `PostStartup` | | Event | `world:observe(0, tecs.platform.events.on., handler)` | | Frame | The phases driven by `world:update(dt)` | | Shutdown | `PreShutdown`, `Shutdown`, and `PostShutdown` | Each host iteration drains bounded native progress, then starts or resumes one logical world update. File, socket, process, HTTP, dialog, and asset operations may park that update without blocking SDL. A parked update resumes at the original call site; later systems do not overtake it, and the scheduler commits each completed phase once. Platform events belong to that same logical update. The host seals one retained batch, input folds it once, and the first engine-owned `Ingress` system emits its events in sequence order. An observer may suspend like any other system. Events arriving meanwhile stay pending for the next update, so input and event state cannot change underneath the suspended observer. The process completion pump, audio cleanup, and debug control plane continue while gameplay is parked or crashed. Rendering uses only the latest completed world state and never extracts a half-finished phase. ## Checkpoints and crashes Mobile platforms may suspend a process before another frame runs. Prepare a snapshot during an ordinary frame and stage its bytes before that deadline: ```teal local save = world:saveSnapshot().buffer app:stageCheckpoint(tostring(save)) ``` The application writes the latest staged bytes when the platform backgrounds or terminates it. The next entry plugin can call `readCheckpoint` and load the result. The application catches errors from game systems, observers, plugins, and startup phases. It records the first traceback while the host continues to serve platform events and the debug connection. The guard restores engine-owned scopes, not partially changed game state. Use `clearCrash` only as a development reload aid. Methods whose names begin with an underscore belong to the host contract. ## Module contents ### Constructors | Constructor | Description | | --- | --- | | [`newApplication`](/modules/Application/#tecs.Application.newApplication) | Builds an application. | ### Types | Type | Kind | Description | | --- | --- | --- | | [`Config`](/modules/Application/#tecs.Application.Config) | record | Caller-writable. Sets the window to open, as Options describes it. | | [`EntryPlugin`](/modules/Application/#tecs.Application.EntryPlugin) | type | Configures the world owned by an application. | ### Functions | Function | Kind | Description | | --- | --- | --- | | [`checkpointPath`](/modules/Application/#tecs.Application.checkpointPath) | Instance | Returns the checkpoint path, or nil when the config names none. | | [`clearCrash`](/modules/Application/#tecs.Application.clearCrash) | Instance | Clears a recoverable gameplay crash so development can render another frame. | | [`crashed`](/modules/Application/#tecs.Application.crashed) | Instance | Returns the gameplay traceback, or nil while the game is healthy. | | [`readCheckpoint`](/modules/Application/#tecs.Application.readCheckpoint) | Instance | Reads the bytes the last run left, or returns nil when there are none. | | [`stageCheckpoint`](/modules/Application/#tecs.Application.stageCheckpoint) | Instance | Hands the engine the bytes to write when the platform backgrounds us. | ### Values | Value | Type | Description | | --- | --- | --- | | [`audio`](/modules/Application/#tecs.Application.audio) | [`Audio`](/modules/audio/#tecs.audio.Audio) | Read-only. Exposes the audio mixer. | | [`device`](/modules/Application/#tecs.Application.device) | `Device` | Read-only. Exposes the GPU device. | | [`elapsed`](/modules/Application/#tecs.Application.elapsed) | `number` | Engine-owned. Reports seconds of simulated time. | | [`frame`](/modules/Application/#tecs.Application.frame) | `integer` | Engine-owned. Reports completed application iterations. | | [`input`](/modules/Application/#tecs.Application.input) | [`Input`](/modules/input/#tecs.input.Input) | Read-only. Exposes current input state. | | [`mcp`](/modules/Application/#tecs.Application.mcp) | [`Server`](/modules/io/mcp/#tecs.io.mcp.Server) | Read-only. Exposes the debug server when configuration requested one. | | [`quitRequested`](/modules/Application/#tecs.Application.quitRequested) | `boolean` | Caller-writable. Set to true to leave the loop at the end of the current iteration. | | [`renderer`](/modules/Application/#tecs.Application.renderer) | [`Renderer`](/modules/gfx/#tecs.gfx.Renderer) | Read-only. Exposes the renderer that extracts this world. | | [`suspended`](/modules/Application/#tecs.Application.suspended) | `boolean` | Engine-owned. Reports true while the platform has the application in the background. | | [`window`](/modules/Application/#tecs.Application.window) | [`Window`](/modules/platform/window/#tecs.platform.window.Window) | Read-only. Exposes the application window. | | [`world`](/modules/Application/#tecs.Application.world) | [`World`](/modules/ecs/#tecs.World) | Read-only. Exposes the world driven by this application. | ## Constructors ### tecs.Application.newApplication Static Builds an application. Return the result from the entry chunk. Reached as `tecs.newApplication`, at the root rather than under a module, because an application is not a subsystem: it owns the window, the device, the input, the renderer and the audio. Tecs hands it to the entry plugin beside the world. ```teal function tecs.Application.newApplication( config: Application.Config ): Application ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `config` | [`Application.Config`](/modules/Application/#tecs.Application.Config) | Tecs reads this table here and ignores later changes. This call opens nothing. | #### Returns | Type | Description | | --- | --- | | [`Application`](/modules/Application/) | The application, at rest until the host's first callback. | ## Types ### tecs.Application.Config record ```teal record tecs.Application.Config window: Window.Options debug: boolean showFps: boolean framesInFlight: integer presentMode: string ambientLight: {number} audio: Audio.Config logFile: string logLevel: integer checkpoint: string mcpPort: integer watch: watcher.Config capacity: integer sprites: boolean meshes: Renderer.MeshOptions bloom: Renderer.BloomOptions maxViews: integer timestep: number fixedMaxSteps: integer fixedOverload: ecs.FixedOverload maxEntities: integer reserveRuns: boolean partialRewrites: boolean packImages: boolean shadows: Deferred.ShadowOptions debugMaxFrames: integer quitOnEscape: boolean plugin: EntryPlugin end ``` #### tecs.Application.Config.window field Caller-writable. Sets the window to open, as [`Options`](/modules/platform/window/#tecs.platform.window.Options) describes it. The application passes the table through whole. ```teal tecs.Application.Config.window: Window.Options ``` #### tecs.Application.Config.debug field Caller-writable. Marks this run as a development run. Tecs creates the device with validation layers enabled and writes the log file below by default. This setting also joins the three settings `clearCrash` looks at, beside `mcpPort` and `watch`, to decide whether resuming after a gameplay crash is something this build does at all. ```teal tecs.Application.Config.debug: boolean ``` #### tecs.Application.Config.showFps field Caller-writable. Appends a rolling frames-per-second reading to the window title. Defaults to false. Enabled applications own the title after startup and refresh it twice per second. ```teal tecs.Application.Config.showFps: boolean ``` #### tecs.Application.Config.framesInFlight field Caller-writable. Sets how many frames the CPU may run ahead of the GPU. The device defaults to two. ```teal tecs.Application.Config.framesInFlight: integer ``` #### tecs.Application.Config.presentMode field Caller-writable. Selects `"vsync"`, `"immediate"` or `"mailbox"` presentation. The device defaults to `"vsync"`. ```teal tecs.Application.Config.presentMode: string ``` #### tecs.Application.Config.ambientLight field Caller-writable. Sets the light every surface receives before any light entity contributes, as red, green and blue. Defaults to white. White rather than a dim gray, because a scene that has placed no lights is the first thing anyone builds and it should look like what it is: sprites at their own color, with lights adding on top of them. A game doing its own lighting turns this down to the level it wants the unlit parts of the scene to sit at. ```teal tecs.Application.Config.ambientLight: {number} ``` #### tecs.Application.Config.audio field Caller-writable. Configures sound output. Omitted takes the defaults, which open the platform's default device. ```teal tecs.Application.Config.audio: Audio.Config ``` #### tecs.Application.Config.logFile field Caller-writable. Sets a JSON Lines log file under the writable root. Omitted creates one when `mcpPort` or `debug` is set and otherwise keeps logging at the platform destination. ```teal tecs.Application.Config.logFile: string ``` #### tecs.Application.Config.logLevel field Caller-writable. Sets the lowest priority that reaches the log, as one of `log.TRACE`, `log.VERBOSE`, `log.DEBUG`, `log.INFO`, `log.WARN`, `log.ERROR` or `log.CRITICAL`. Omitted leaves the platform default. The level applies to every category and does not change whether `logFile` stores the accepted messages. ```teal tecs.Application.Config.logLevel: integer ``` #### tecs.Application.Config.checkpoint field Caller-writable. Sets the file name under the writable root that receives a checkpoint when the platform backgrounds or terminates the application. Omitted means no checkpoint, and `stageCheckpoint` says so. See `Application:stageCheckpoint` for what a game has to do, which is the part that matters. ```teal tecs.Application.Config.checkpoint: string ``` #### tecs.Application.Config.mcpPort field Caller-writable. Sets the MCP server port. After the server starts, the application appends `[MCP :port]` to the window title. Omitted means no server, since a game should not open a socket nobody asked for. ```teal tecs.Application.Config.mcpPort: integer ``` #### tecs.Application.Config.watch field Caller-writable. Watches loaded content files and reloads them when they change. Omitted means no watcher, on the same footing as the server above: a poll of the filesystem is not something to start because a build happened to be able to. A release refuses it. ```teal tecs.Application.Config.watch: watcher.Config ``` #### tecs.Application.Config.capacity field Caller-writable. Sets the renderer buffer capacity in instances. Defaults to 65536. An instance is a row the GPU draws: every renderable entity is one, and so is every glyph of every text, which an instance producer writes into a run of its own rather than spawning an entity per glyph. This is a ceiling rather than a hint. Rows past it are dropped rather than growing a buffer mid-frame, and `renderer.sprites.dropped` counts them. Sized independently of `maxEntities` beside it, because the two count different things: a world full of entities that carry no [`Renderable2D`](/modules/gfx/#tecs.gfx.Renderable2D) needs no instances at all, and one text entity needs one instance per character. ```teal tecs.Application.Config.capacity: integer ``` #### tecs.Application.Config.sprites field Caller-writable. Enables the 2D sprite domain unless set to false. Defaults to true. Disabling it creates no sprite image array, packet, buffers, or world queries, and makes `renderer.sprites` nil. ```teal tecs.Application.Config.sprites: boolean ``` #### tecs.Application.Config.meshes field Caller-writable. Enables and configures the 3D mesh domain. Nil, the default, loads no mesh renderer and allocates no mesh buffers. ```teal tecs.Application.Config.meshes: Renderer.MeshOptions ``` #### tecs.Application.Config.bloom field Caller-writable. Enables optional bloom after opaque lighting and before transparent meshes and 2D UI. Nil omits its GPU resources. ```teal tecs.Application.Config.bloom: Renderer.BloomOptions ``` #### tecs.Application.Config.maxViews field Caller-writable. Enables [`View`](/modules/gfx/#tecs.gfx.View) entities and sets the maximum enabled views composed in one frame. Omit for the zero-overhead synthesized full-frame view. ```teal tecs.Application.Config.maxViews: integer ``` #### tecs.Application.Config.timestep field Caller-writable. Sets the seconds one fixed step covers. Must be greater than zero. Defaults to 1/60. ```teal tecs.Application.Config.timestep: number ``` #### tecs.Application.Config.fixedMaxSteps field Caller-writable. Sets the most fixed steps one update runs before the overload policy applies. Must be a positive integer. Defaults to 10. ```teal tecs.Application.Config.fixedMaxSteps: integer ``` #### tecs.Application.Config.fixedOverload field Caller-writable. Selects what becomes of catch-up that did not fit. Defaults to "drop", which bounds a frame's work and reports the loss through `world:getStats`. "accumulate" keeps every second instead. ```teal tecs.Application.Config.fixedOverload: ecs.FixedOverload ``` #### tecs.Application.Config.maxEntities field Caller-writable. Sets the world's entity slot capacity. Defaults to 2^20, and 2^22 - 1 is the ceiling the packed id format allows. Concurrent slots rather than lifetime spawns: a slot is given back when the world despawns the entity, then the next entity reuses it. The world preallocates this many slots, so it is a memory decision as much as a limit, and a game that knows its population sets it rather than paying for a million slots it will not use. ```teal tecs.Application.Config.maxEntities: integer ``` #### tecs.Application.Config.reserveRuns field Caller-writable. Gives each archetype a run with room to grow rather than packing the runs end to end. Defaults to false, which packs. Packed, a run cannot grow without shifting every run after it, so a spawn anywhere rewrites every instance in the scene. Reserved, it grows into its own slack and the archetypes around it are left alone. The cull also dispatches over the slack as well as over the rows, and that reserving needs headroom: a `capacity` sized to exactly the population leaves nothing to reserve out of and the setting does nothing. Worth it for a scene that spawns into one archetype while most of the world sits in others, which is what a game with a level and projectiles in it looks like. Worth nothing for a scene held in a single archetype, where there is no other run to leave alone. Not named for debugging, unlike `debugMaxFrames` below, because it is not a debugging setting: it is a tuning one, measured at 15.4x on a mixed-archetype spawner scene. A name with `debug` in it would warn off exactly the shipped games that should turn it on. ```teal tecs.Application.Config.reserveRuns: boolean ``` #### tecs.Application.Config.partialRewrites field Caller-writable. Rewrites the rows a spawn or a despawn wrote rather than the archetype's whole run. Defaults to false, which rewrites the run. A row moving into an archetype has every column newly written at that row, so a structural change dirties every column of the archetype it lands on and the run is resynced whole. The rest of the run did not change, and the ECS records which rows did, so this makes one spawn cost the rows it wrote rather than the archetype's population. It composes with `reserveRuns` above rather than replacing it: reserving keeps a spawn from moving the archetypes around it, and this keeps it from rewriting the one it landed in. Two things are deliberately left out of it. A value write through `archetype:getMut` names a column and not a row, so it still rewrites the run; that includes a `batchSpawn` fill callback, which declares intent on the whole column it writes new rows into. And an archetype carrying [`PreviousTransform2D`](/modules/gfx/#tecs.gfx.PreviousTransform2D) is interpolated, so its drawn positions move on every frame that falls at a new point in the fixed step whatever the world did. ```teal tecs.Application.Config.partialRewrites: boolean ``` #### tecs.Application.Config.packImages field Caller-writable. Fits many images into each layer of the renderer's image array rather than one. Defaults to false, where the ceiling on distinct images is the array's layer count and a small image costs a whole cell. On, the ceiling is the array's area instead. ```teal tecs.Application.Config.packImages: boolean ``` #### tecs.Application.Config.shadows field Caller-writable. Enables entity shadows and configures their cost. Nil, the default, means an [`Occluder2D`](/modules/gfx/#tecs.gfx.Occluder2D) or a [`DropShadow2D`](/modules/gfx/#tecs.gfx.DropShadow2D) on an entity draws the entity and casts nothing, because the targets that would hold a shadow are never built. An empty table turns them on with every default; see `Deferred.ShadowOptions` for the seven numbers. ```teal tecs.Application.Config.shadows: Deferred.ShadowOptions ``` #### tecs.Application.Config.debugMaxFrames field Caller-writable. Stops after this many iterations. This lets an automated run drive a real window to completion without a human closing it. ```teal tecs.Application.Config.debugMaxFrames: integer ``` #### tecs.Application.Config.quitOnEscape field Caller-writable. Requests a clean exit when the platform delivers an Escape key press. Defaults to false. The check is independent of gameplay input layers and remains available after a gameplay crash. ```teal tecs.Application.Config.quitOnEscape: boolean ``` #### tecs.Application.Config.plugin field Caller-writable. Sets the game plugin that receives the world and this application. Called at the end of initialization, after every engine subsystem is installed and before the startup phases run, so it can register a `Startup` system and have it run this initialization rather than the next one. One entry point rather than a list, because composing plugins is something the world already does: `world:addPlugin` takes an `tecs.Plugin` and is how the engine installs its own, so a game with several calls it from in here and needs no second mechanism. The world comes first because every plugin the world takes is `function(world)`, so this reads as that shape with one more thing and code moves between here and a delegated plugin without a silent argument swap. ```teal tecs.Application.Config.plugin: EntryPlugin ``` ### tecs.Application.EntryPlugin type Configures the world owned by an application. ```teal type tecs.Application.EntryPlugin = function(types.World, Application) ``` ## Functions ### tecs.Application:checkpointPath Instance Returns the checkpoint path, or nil when the config names none. ```teal function tecs.Application.checkpointPath(self): string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Application` | | #### Returns | Type | Description | | --- | --- | | `string` | | ### tecs.Application:clearCrash Instance Clears a recoverable gameplay crash so development can render another frame. Reload the world before trusting it. The method works only when `debug`, `mcpPort` or `watch` marks the run for development. It refuses to continue after incomplete graphics recovery. ```teal function tecs.Application.clearCrash(self): boolean, string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Application` | | #### Returns | Type | Description | | --- | --- | | `boolean` | Returns whether the loop resumed. | | `string` | Returns the reason when the loop could not resume. | ### tecs.Application:crashed Instance Returns the gameplay traceback, or nil while the game is healthy. ```teal function tecs.Application.crashed(self): string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Application` | | #### Returns | Type | Description | | --- | --- | | `string` | | ### tecs.Application:readCheckpoint Instance Reads the bytes the last run left, or returns nil when there are none. Read it while building the world, which is what the plugin and the startup phases are for. Nil covers every reason there is nothing to resume from: a first run, a game that never staged anything, a file the player deleted. None of those is an error, so none of them raises. ```teal function tecs.Application.readCheckpoint(self): string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Application` | | #### Returns | Type | Description | | --- | --- | | `string` | | ### tecs.Application:stageCheckpoint Instance Hands the engine the bytes to write when the platform backgrounds us. Call this from an ordinary system, on an ordinary frame, whenever the state worth keeping has changed. Nothing is written here; the bytes are held, and `_willEnterBackground` writes them at the one moment the platform gives. What this takes is bytes rather than a function that produces them, and that is the whole design rather than an inconvenience. iOS allows roughly five seconds from the backgrounding callback returning and Android rather less, and at this project's scale a world is not serializable inside any of it: the platform could kill a callback part way through four million entities through and leave nothing behind. A function would let a game postpone the serializing into exactly that callback while looking like it had prepared something. A string cannot: by the time it exists, the expensive half has already happened on a frame that could afford it. The host times the hook and says so past 250 ms, which is the check on the rest. Staging again replaces the previous bytes; there is one checkpoint, not a queue. Staging and then backgrounding twice writes once, because a write no changes only gives the platform another chance to interrupt the write for nothing. The counterpart is `readCheckpoint`, which is how the next run gets it back. Raises when the config names no `checkpoint`, because silently holding bytes that will never be written is the one failure a game would not notice. ```teal function tecs.Application.stageCheckpoint(self, bytes: string) ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Application` | | | `bytes` | `string` | | #### Returns None. ## Values ### tecs.Application.audio variable Read-only. Exposes the audio mixer. ```teal tecs.Application.audio: Audio ``` ### tecs.Application.device variable Read-only. Exposes the GPU device. ```teal tecs.Application.device: Device ``` ### tecs.Application.elapsed variable Engine-owned. Reports seconds of simulated time. The frame delta advances it so a replay reproduces the value exactly. ```teal tecs.Application.elapsed: number ``` ### tecs.Application.frame variable Engine-owned. Reports completed application iterations. ```teal tecs.Application.frame: integer ``` ### tecs.Application.input variable Read-only. Exposes current input state. ```teal tecs.Application.input: Input ``` ### tecs.Application.mcp variable Read-only. Exposes the debug server when configuration requested one. ```teal tecs.Application.mcp: mcp.Server ``` ### tecs.Application.quitRequested variable Caller-writable. Set to true to leave the loop at the end of the current iteration. ```teal tecs.Application.quitRequested: boolean ``` ### tecs.Application.renderer variable Read-only. Exposes the renderer that extracts this world. ```teal tecs.Application.renderer: Renderer ``` ### tecs.Application.suspended variable Engine-owned. Reports true while the platform has the application in the background. The loop still runs, but simulation and rendering do not. ```teal tecs.Application.suspended: boolean ``` ### tecs.Application.window variable Read-only. Exposes the application window. ```teal tecs.Application.window: Window ``` ### tecs.Application.world variable Read-only. Exposes the world driven by this application. ```teal tecs.Application.world: types.World ``` --- ## tecs.assets # tecs.assets Acquires and decodes assets without blocking the SDL host. Loads return their values directly. Inside a frame system dispatched by `world:update`, a worker decode transparently suspends the logical update: ```teal return tecs.newApplication({ plugin = function(world: tecs.World, app: tecs.Application) local loaded = false world:addSystem({ name = "game.SpawnHero", phase = tecs.ecs.phases.PreUpdate, run = function() if loaded then return end local image = tecs.assets.loadImage( tecs.io.files.assetPath("sprites/hero.png") ) local sprite = app.renderer.sprites:registerImage(image) world:spawn( tecs.Transform2D(100, 100), sprite, tecs.gfx.Renderable2D() ) loaded = true end, }) end, }) ``` The loader decodes pixels but creates no GPU resource. The renderer decides texture residency. Audio follows the same split between decoded clips and voices. Image cache misses read through SDL AsyncIO, decode in the bounded native CPU lane, and publish on the main thread. `newMesh` constructs immutable procedural geometry synchronously. `loadGLTF` decodes glTF 2.0 and GLB scenes through the maintained Rust glTF importer on a warmed isolated worker, including images, metallic-roughness materials, vertex colors, alpha masks, `BLEND` modes, skins, and node animation clips including morph weights. One opaque native allocation retains the import; the main thread borrows its geometry through flat views instead of serializing large strings through the worker channel. Both produce CPU-owned data that the mesh domain consumes when it becomes resident. A model with a `BLEND` material requires a mesh domain created with `transparency = true`. Primitives without authored tangents use MikkTSpace tangent generation. Tangent discontinuities split vertices while color, skin, and morph streams follow the split, preserving normal-map seams without changing authored tangents. One imported primitive becomes one independently bounded GPU-culling command. The decoder splits a primitive above 65,536 triangles into bounded commands, remapping the vertices and optional color, skin, and morph streams in each chunk. Meshoptimizer then reorders opaque and masked triangles for the vertex cache and remaps every vertex stream into first-use order. Alpha-blended triangles retain their authored order because order affects their result, but their vertex streams still receive the lossless fetch remap. The repository's large-scene fetch command can replace source images with complete BC3 mip chains in KTX2 containers. The maintained Rust parser validates the container before exposing its blocks. Those images remain compressed through upload and require a mesh domain created with `textureFormat = tecs.assets.IMAGE_BC3`, `mipmaps = true`, and `packTextures = false`. PNG, JPEG, SVG, and ordinary glTF images remain decoded RGBA8. ## Lifetime [`Application`](/modules/Application/) installs and shuts down the asset lanes. Headless code calls `install` before model or sound loading and `shutdown` afterwards. A load outside a world update blocks and drives its private producer until the value or failure arrives; a system suspends at the same direct call. Release a payload after its consumer takes ownership. The final release frees the decoded memory. Overlapping image loads for one path share a decode and receive holds on the same image. A later load after settlement decodes again. Sound loads never share. `loadString` reads a complete file into a binary-safe string without interpreting it. Image loads accept PNG, JPEG, static SVG, and linear BC3 KTX2 mip chains. SVG renders at its intrinsic dimensions. It uses the bundled JetBrains Mono for all text and ignores external image references, so installed fonts and the worker's current directory cannot change its pixels. `tecs.audio.decoders()` reports the sound formats linked into the current build. Sound mode `"resident"` decodes the complete clip, `"stream"` opens a source for each voice, and `"auto"` chooses from the duration threshold. Release each payload after its consumer takes ownership. ## Module contents ### Constructors | Constructor | Description | | --- | --- | | [`newMesh`](/modules/assets/#tecs.assets.newMesh) | Builds procedural mesh data in the renderer's fixed vertex layout. | ### Types | Type | Kind | Description | | --- | --- | --- | | [`Image`](/modules/assets/#tecs.assets.Image) | record | Represents decoded pixels or an imported compressed mip chain and the caller's hold on them. | | [`Mesh`](/modules/assets/#tecs.assets.Mesh) | record | Describes one immutable indexed triangle mesh before GPU registration. | | [`MeshMorphTarget`](/modules/assets/#tecs.assets.MeshMorphTarget) | record | Supplies one procedural morph target to newMesh. | | [`MeshOptions`](/modules/assets/#tecs.assets.MeshOptions) | record | Supplies procedural geometry to newMesh. | | [`Model`](/modules/assets/#tecs.assets.Model) | record | Represents one decoded glTF 2.0 scene and its node animations. | | [`ModelAnimation`](/modules/assets/#tecs.assets.ModelAnimation) | record | Describes one decoded glTF node-animation clip. | | [`ModelAnimationChannel`](/modules/assets/#tecs.assets.ModelAnimationChannel) | record | Describes one decoded glTF animation channel. | | [`ModelDraw`](/modules/assets/#tecs.assets.ModelDraw) | record | Describes one static primitive instance in a decoded glTF scene. | | [`ModelMaterial`](/modules/assets/#tecs.assets.ModelMaterial) | record | Describes one decoded glTF material before GPU registration. | | [`ModelNode`](/modules/assets/#tecs.assets.ModelNode) | record | Describes one node in a decoded glTF transform hierarchy. | | [`ModelSkin`](/modules/assets/#tecs.assets.ModelSkin) | record | Describes one initial joint palette decoded from a glTF skin instance. | | [`Sound`](/modules/assets/#tecs.assets.Sound) | record | Represents a loaded clip and the caller's hold on it. | ### Functions | Function | Kind | Description | | --- | --- | --- | | [`install`](/modules/assets/#tecs.assets.install) | Static | Starts the loading worker. | | [`installed`](/modules/assets/#tecs.assets.installed) | Static | Reports whether the loading worker is running. | | [`loadGLTF`](/modules/assets/#tecs.assets.loadGLTF) | Static | Queues glTF 2.0 or GLB decoding on the asset worker. | | [`loadImage`](/modules/assets/#tecs.assets.loadImage) | Static | Loads an image and returns its decoded pixels. | | [`loadSound`](/modules/assets/#tecs.assets.loadSound) | Static | Loads a sound and returns its decoded or streaming payload. | | [`loadString`](/modules/assets/#tecs.assets.loadString) | Static | Reads a complete file and returns its bytes. | | [`pending`](/modules/assets/#tecs.assets.pending) | Static | Returns the number of asset loads still in flight. | | [`shutdown`](/modules/assets/#tecs.assets.shutdown) | Static | Stops the loading worker. | | [`waitAll`](/modules/assets/#tecs.assets.waitAll) | Static | Blocks until every queued load has finished. | ### Values | Value | Type | Description | | --- | --- | --- | | [`ANIMATION_CUBIC`](/modules/assets/#tecs.assets.ANIMATION_CUBIC) | `integer` | Read-only. Selects cubic-spline interpolation. | | [`ANIMATION_LINEAR`](/modules/assets/#tecs.assets.ANIMATION_LINEAR) | `integer` | Read-only. Selects linear interpolation. | | [`ANIMATION_ROTATION`](/modules/assets/#tecs.assets.ANIMATION_ROTATION) | `integer` | Read-only. Selects a rotation animation channel. | | [`ANIMATION_SCALE`](/modules/assets/#tecs.assets.ANIMATION_SCALE) | `integer` | Read-only. Selects a scale animation channel. | | [`ANIMATION_STEP`](/modules/assets/#tecs.assets.ANIMATION_STEP) | `integer` | Read-only. Selects held-key interpolation. | | [`ANIMATION_TRANSLATION`](/modules/assets/#tecs.assets.ANIMATION_TRANSLATION) | `integer` | Read-only. Selects a translation animation channel. | | [`ANIMATION_WEIGHTS`](/modules/assets/#tecs.assets.ANIMATION_WEIGHTS) | `integer` | Read-only. Selects a morph-weight animation channel. | | [`IMAGE_BC3`](/modules/assets/#tecs.assets.IMAGE_BC3) | `integer` | Read-only. Selects a complete imported BC3 image mip chain. | | [`IMAGE_RGBA8`](/modules/assets/#tecs.assets.IMAGE_RGBA8) | `integer` | Read-only. Selects decoded, uncompressed RGBA8 image storage. | ## Constructors ### tecs.assets.newMesh Static Builds procedural mesh data in the renderer's fixed vertex layout. ```teal function tecs.assets.newMesh(options: MeshOptions): Mesh ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `options` | [`MeshOptions`](/modules/assets/#tecs.assets.MeshOptions) | The caller supplies a name, interleaved vertices, and zero-based triangle indices. | #### Returns | Type | Description | | --- | --- | | [`Mesh`](/modules/assets/#tecs.assets.Mesh) | Returns CPU geometry that `renderer.meshes:registerMesh` consumes or the caller releases. | ## Types ### tecs.assets.Image record Represents decoded pixels or an imported compressed mip chain and the caller's hold on them. What `loadImage` returns. Reference counts govern the pixels rather than owning them outright, because two overlapping loads share a decode: each caller holds the same `Image`, and the last caller to release it one that frees. Reading `pixels` after that is reading freed memory, which is why a released image reads nil there rather than looking uploadable. Read-only. Exposes the decoded image type. ```teal record tecs.assets.Image path: string pixels: loader.CValue width: integer height: integer pitch: integer format: integer storageWidth: integer storageHeight: integer levels: integer byteCount: integer release: function(self) end ``` #### tecs.assets.Image.path field Read-only. Contains the requested path unchanged. The loader does not resolve it, so it is whatever the caller passed. ```teal tecs.assets.Image.path: string ``` #### tecs.assets.Image.pixels field Read-only. Contains decoded RGBA pixels or BC3 blocks until the last `release`, then becomes nil. ```teal tecs.assets.Image.pixels: loader.CValue ``` #### tecs.assets.Image.width field Read-only. Reports the width in pixels. ```teal tecs.assets.Image.width: integer ``` #### tecs.assets.Image.height field Read-only. Reports the height in pixel rows. ```teal tecs.assets.Image.height: integer ``` #### tecs.assets.Image.pitch field Read-only. Reports the row stride in bytes, which a decoder may pad beyond `width * 4`. ```teal tecs.assets.Image.pitch: integer ``` #### tecs.assets.Image.format field Read-only. Selects decoded RGBA8 pixels or an imported BC3 mip chain with an `assets.IMAGE_*` integer constant. ```teal tecs.assets.Image.format: integer ``` #### tecs.assets.Image.storageWidth field Read-only. Reports the texture width represented by the uploaded mip chain. It equals `width` for decoded RGBA8 images. ```teal tecs.assets.Image.storageWidth: integer ``` #### tecs.assets.Image.storageHeight field Read-only. Reports the texture height represented by the uploaded mip chain. It equals `height` for decoded RGBA8 images. ```teal tecs.assets.Image.storageHeight: integer ``` #### tecs.assets.Image.levels field Read-only. Reports the number of consecutive mip levels in `pixels`. ```teal tecs.assets.Image.levels: integer ``` #### tecs.assets.Image.byteCount field Read-only. Reports bytes available from `pixels`. Decoded RGBA8 images report `pitch * height`. ```teal tecs.assets.Image.byteCount: integer ``` #### tecs.assets.Image:release Instance Gives up this caller's hold on the pixels, and frees at the last. Called once whatever needed them has taken a copy, which for an image means after the caller uploads it. Where several loads shared one decode, this releases one of them. Releasing an image already down to nothing does nothing, so a shutdown path need not know whether something else got there first. ```teal function tecs.assets.Image.release(self) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Image` | | ##### Returns None. ### tecs.assets.Mesh record Describes one immutable indexed triangle mesh before GPU registration. Vertices use one fixed interleaved layout: position xyz, normal xyz, tangent xyzw, and texture uv. Indices are zero-based unsigned 32-bit values. A mesh may therefore contain far more than 65,535 vertices, and a primitive is one indexed range rather than one triangle. `renderer.meshes:registerMesh` copies both arrays into device-local storage and releases this object. The Lua-number constructor is intended for procedural and example geometry. A file decoder can build the same record directly without expanding a large mesh into Lua tables. Read-only. Exposes the immutable CPU mesh type. ```teal record tecs.assets.Mesh name: string vertices: loader.CArray colorVertices: loader.CArray indices: loader.CArray skinVertices: loader.CArray morphVertices: loader.CArray morphTargetCount: integer morphWeights: {number} vertexCount: integer indexCount: integer centerX: number centerY: number centerZ: number radius: number release: function(self) end ``` #### tecs.assets.Mesh.name field Read-only. Contains the stable name used by `meshId` and snapshots. ```teal tecs.assets.Mesh.name: string ``` #### tecs.assets.Mesh.vertices field Read-only. Contains interleaved vertex floats until `release` runs. ```teal tecs.assets.Mesh.vertices: loader.CArray ``` #### tecs.assets.Mesh.colorVertices field Read-only. Contains optional linear RGBA vertex colors until `release` runs. Nil means every vertex is white. ```teal tecs.assets.Mesh.colorVertices: loader.CArray ``` #### tecs.assets.Mesh.indices field Read-only. Contains zero-based unsigned 32-bit indices until `release` runs. ```teal tecs.assets.Mesh.indices: loader.CArray ``` #### tecs.assets.Mesh.skinVertices field Read-only. Contains optional joint indices and weights as eight floats per vertex until `release` runs. Nil means rigid geometry. ```teal tecs.assets.Mesh.skinVertices: loader.CArray ``` #### tecs.assets.Mesh.morphVertices field Read-only. Contains optional position, normal, and tangent deltas as nine floats per target vertex until `release` runs. Nil means the geometry has no morph targets. ```teal tecs.assets.Mesh.morphVertices: loader.CArray ``` #### tecs.assets.Mesh.morphTargetCount field Read-only. Reports the number of consecutive morph targets. ```teal tecs.assets.Mesh.morphTargetCount: integer ``` #### tecs.assets.Mesh.morphWeights field Read-only. Contains one default weight per morph target. ```teal tecs.assets.Mesh.morphWeights: {number} ``` #### tecs.assets.Mesh.vertexCount field Read-only. Reports the number of vertices, not floats. ```teal tecs.assets.Mesh.vertexCount: integer ``` #### tecs.assets.Mesh.indexCount field Read-only. Reports the number of indices. It is always a multiple of three. ```teal tecs.assets.Mesh.indexCount: integer ``` #### tecs.assets.Mesh.centerX field Read-only. Reports the local-space bounding-sphere center x. ```teal tecs.assets.Mesh.centerX: number ``` #### tecs.assets.Mesh.centerY field Read-only. Reports the local-space bounding-sphere center y. ```teal tecs.assets.Mesh.centerY: number ``` #### tecs.assets.Mesh.centerZ field Read-only. Reports the local-space bounding-sphere center z. ```teal tecs.assets.Mesh.centerZ: number ``` #### tecs.assets.Mesh.radius field Read-only. Reports the non-negative local-space bounding-sphere radius. ```teal tecs.assets.Mesh.radius: number ``` #### tecs.assets.Mesh:release Instance Gives up the CPU geometry. Calling it again does nothing. ```teal function tecs.assets.Mesh.release(self) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Mesh` | | ##### Returns None. ### tecs.assets.MeshMorphTarget record Supplies one procedural morph target to `newMesh`. Read-only. Exposes one procedural morph-target description. ```teal record tecs.assets.MeshMorphTarget positions: {number} normals: {number} tangents: {number} end ``` #### tecs.assets.MeshMorphTarget.positions field Caller-writable. Supplies three position deltas per vertex. ```teal tecs.assets.MeshMorphTarget.positions: {number} ``` #### tecs.assets.MeshMorphTarget.normals field Caller-writable. Supplies three normal deltas per vertex, or nil for zero deltas. ```teal tecs.assets.MeshMorphTarget.normals: {number} ``` #### tecs.assets.MeshMorphTarget.tangents field Caller-writable. Supplies three tangent deltas per vertex, or nil for zero deltas. Tangent handedness is unchanged. ```teal tecs.assets.MeshMorphTarget.tangents: {number} ``` ### tecs.assets.MeshOptions record Supplies procedural geometry to `newMesh`. ```teal global record tecs.assets.MeshOptions name: string vertices: {number} colors: {number} indices: {integer} joints: {integer} weights: {number} morphTargets: {MeshMorphTarget} morphWeights: {number} end ``` #### tecs.assets.MeshOptions.name field Caller-writable. Supplies the stable non-empty mesh name. ```teal tecs.assets.MeshOptions.name: string ``` #### tecs.assets.MeshOptions.vertices field Caller-writable. Supplies interleaved position xyz, normal xyz, tangent xyzw, and texture uv floats. ```teal tecs.assets.MeshOptions.vertices: {number} ``` #### tecs.assets.MeshOptions.colors field Caller-writable. Supplies optional linear RGBA colors, four per vertex. Nil makes every vertex white without allocating a color array. ```teal tecs.assets.MeshOptions.colors: {number} ``` #### tecs.assets.MeshOptions.indices field Caller-writable. Supplies zero-based triangle indices. ```teal tecs.assets.MeshOptions.indices: {integer} ``` #### tecs.assets.MeshOptions.joints field Caller-writable. Supplies four zero-based joint indices per vertex. Nil requires `weights` to be nil and builds rigid geometry. ```teal tecs.assets.MeshOptions.joints: {integer} ``` #### tecs.assets.MeshOptions.weights field Caller-writable. Supplies four non-negative joint weights per vertex. Each group is normalized by `newMesh` and requires `joints`. ```teal tecs.assets.MeshOptions.weights: {number} ``` #### tecs.assets.MeshOptions.morphTargets field Caller-writable. Supplies morph targets in file order. Each target carries vertex-count-matched position deltas and optional normal and tangent deltas. ```teal tecs.assets.MeshOptions.morphTargets: {MeshMorphTarget} ``` #### tecs.assets.MeshOptions.morphWeights field Caller-writable. Supplies one finite default weight per morph target. Omitted weights default to zero. ```teal tecs.assets.MeshOptions.morphWeights: {number} ``` ### tecs.assets.Model record Represents one decoded glTF 2.0 scene and its node animations. Meshes use the same fixed vertex layout as `newMesh`. Images remain decoded CPU pixels. `renderer.meshes:registerModel` consumes all of them and returns shared residency that creates independently posed instances. Skinning includes the initial joint pose. Morph data includes immutable deltas and default weights. Animation clips retain allocation-stable CPU sampling data for node transforms and morph weights. Read-only. Exposes the decoded glTF scene type. ```teal record tecs.assets.Model path: string mipmaps: boolean meshes: {Mesh} images: {Image} materials: {ModelMaterial} skins: {ModelSkin} nodes: {ModelNode} animations: {ModelAnimation} draws: {ModelDraw} release: function(self) end ``` #### tecs.assets.Model.path field Read-only. Contains the requested `.gltf` or `.glb` path unchanged. ```teal tecs.assets.Model.path: string ``` #### tecs.assets.Model.mipmaps field Read-only. Reports whether the source sampler requires a complete linearly filtered mip chain. ```teal tecs.assets.Model.mipmaps: boolean ``` #### tecs.assets.Model.meshes field Read-only. Contains unique decoded primitive geometry. ```teal tecs.assets.Model.meshes: {Mesh} ``` #### tecs.assets.Model.images field Read-only. Contains unique decoded source images. ```teal tecs.assets.Model.images: {Image} ``` #### tecs.assets.Model.materials field Read-only. Contains decoded metallic-roughness material descriptions. ```teal tecs.assets.Model.materials: {ModelMaterial} ``` #### tecs.assets.Model.skins field Read-only. Contains initial joint palettes referenced by model draws. ```teal tecs.assets.Model.skins: {ModelSkin} ``` #### tecs.assets.Model.nodes field Read-only. Contains every node in source-index order. ```teal tecs.assets.Model.nodes: {ModelNode} ``` #### tecs.assets.Model.animations field Read-only. Contains decoded translation, rotation, and scale clips. ```teal tecs.assets.Model.animations: {ModelAnimation} ``` #### tecs.assets.Model.draws field Read-only. Contains the selected scene's flattened static draws. ```teal tecs.assets.Model.draws: {ModelDraw} ``` #### tecs.assets.Model:release Instance Releases every CPU mesh and image not already consumed. Calling it again does nothing. ```teal function tecs.assets.Model.release(self) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Model` | | ##### Returns None. ### tecs.assets.ModelAnimation record Describes one decoded glTF node-animation clip. Read-only. Exposes one decoded node-animation clip. ```teal record tecs.assets.ModelAnimation name: string duration: number channels: {ModelAnimationChannel} end ``` #### tecs.assets.ModelAnimation.name field Read-only. Contains the authored clip name or a stable generated name. ```teal tecs.assets.ModelAnimation.name: string ``` #### tecs.assets.ModelAnimation.duration field Read-only. Reports the last key time in seconds. ```teal tecs.assets.ModelAnimation.duration: number ``` #### tecs.assets.ModelAnimation.channels field Read-only. Contains translation, rotation, scale, and morph-weight channels. ```teal tecs.assets.ModelAnimation.channels: {ModelAnimationChannel} ``` ### tecs.assets.ModelAnimationChannel record Describes one decoded glTF animation channel. Read-only. Exposes one decoded animation channel. ```teal record tecs.assets.ModelAnimationChannel node: integer path: integer interpolation: integer width: integer times: {number} values: {number} end ``` #### tecs.assets.ModelAnimationChannel.node field Read-only. Selects a one-based target node. ```teal tecs.assets.ModelAnimationChannel.node: integer ``` #### tecs.assets.ModelAnimationChannel.path field Read-only. Selects `ANIMATION_TRANSLATION`, `ANIMATION_ROTATION`, `ANIMATION_SCALE`, or `ANIMATION_WEIGHTS`. ```teal tecs.assets.ModelAnimationChannel.path: integer ``` #### tecs.assets.ModelAnimationChannel.interpolation field Read-only. Selects `ANIMATION_LINEAR`, `ANIMATION_STEP`, or `ANIMATION_CUBIC`. ```teal tecs.assets.ModelAnimationChannel.interpolation: integer ``` #### tecs.assets.ModelAnimationChannel.width field Read-only. Reports packed values per key. It is three for translation and scale, four for rotation, and the target count for morph weights. ```teal tecs.assets.ModelAnimationChannel.width: integer ``` #### tecs.assets.ModelAnimationChannel.times field Read-only. Contains strictly increasing key times in seconds. ```teal tecs.assets.ModelAnimationChannel.times: {number} ``` #### tecs.assets.ModelAnimationChannel.values field Read-only. Contains packed key values. Cubic keys contain incoming tangent, value, and outgoing tangent rows. ```teal tecs.assets.ModelAnimationChannel.values: {number} ``` ### tecs.assets.ModelDraw record Describes one static primitive instance in a decoded glTF scene. Read-only. Exposes one flattened glTF primitive instance. ```teal record tecs.assets.ModelDraw mesh: integer material: integer skin: integer node: integer weights: {number} x: number y: number z: number rotationX: number rotationY: number rotationZ: number rotationW: number scaleX: number scaleY: number scaleZ: number end ``` #### tecs.assets.ModelDraw.mesh field Read-only. Selects a one-based entry in `Model.meshes`. ```teal tecs.assets.ModelDraw.mesh: integer ``` #### tecs.assets.ModelDraw.material field Read-only. Selects a one-based entry in `Model.materials`, or zero for the neutral material. ```teal tecs.assets.ModelDraw.material: integer ``` #### tecs.assets.ModelDraw.skin field Read-only. Selects a one-based entry in `Model.skins`, or zero for a rigid primitive. ```teal tecs.assets.ModelDraw.skin: integer ``` #### tecs.assets.ModelDraw.node field Read-only. Selects the one-based scene node this primitive follows. ```teal tecs.assets.ModelDraw.node: integer ``` #### tecs.assets.ModelDraw.weights field Read-only. Contains this node's initial morph weights in target order. ```teal tecs.assets.ModelDraw.weights: {number} ``` #### tecs.assets.ModelDraw.x field Read-only. Reports world translation x from the selected glTF scene. ```teal tecs.assets.ModelDraw.x: number ``` #### tecs.assets.ModelDraw.y field Read-only. Reports world translation y. ```teal tecs.assets.ModelDraw.y: number ``` #### tecs.assets.ModelDraw.z field Read-only. Reports world translation z. ```teal tecs.assets.ModelDraw.z: number ``` #### tecs.assets.ModelDraw.rotationX field Read-only. Reports world quaternion x. ```teal tecs.assets.ModelDraw.rotationX: number ``` #### tecs.assets.ModelDraw.rotationY field Read-only. Reports world quaternion y. ```teal tecs.assets.ModelDraw.rotationY: number ``` #### tecs.assets.ModelDraw.rotationZ field Read-only. Reports world quaternion z. ```teal tecs.assets.ModelDraw.rotationZ: number ``` #### tecs.assets.ModelDraw.rotationW field Read-only. Reports world quaternion w. ```teal tecs.assets.ModelDraw.rotationW: number ``` #### tecs.assets.ModelDraw.scaleX field Read-only. Reports world scale x. ```teal tecs.assets.ModelDraw.scaleX: number ``` #### tecs.assets.ModelDraw.scaleY field Read-only. Reports world scale y. ```teal tecs.assets.ModelDraw.scaleY: number ``` #### tecs.assets.ModelDraw.scaleZ field Read-only. Reports world scale z. ```teal tecs.assets.ModelDraw.scaleZ: number ``` ### tecs.assets.ModelMaterial record Describes one decoded glTF material before GPU registration. Read-only. Exposes one glTF material description. ```teal record tecs.assets.ModelMaterial name: string model: integer alphaMode: integer baseColorImage: integer normalImage: integer metallicRoughnessImage: integer occlusionImage: integer emissiveImage: integer alphaCutoff: number baseR: number baseG: number baseB: number baseA: number emissiveR: number emissiveG: number emissiveB: number metallic: number roughness: number normalScale: number occlusionStrength: number doubleSided: boolean end ``` #### tecs.assets.ModelMaterial.name field Read-only. Contains the stable material name. ```teal tecs.assets.ModelMaterial.name: string ``` #### tecs.assets.ModelMaterial.model field Caller-writable. Until model registration, selects metallic-roughness PBR at zero, unlit at one, or Lambert diffuse at two. This defaults to the decoded glTF model. ```teal tecs.assets.ModelMaterial.model: integer ``` #### tecs.assets.ModelMaterial.alphaMode field Read-only. Selects opaque, masked, or blended rendering with a `MeshDomain.ALPHA_*` integer constant. ```teal tecs.assets.ModelMaterial.alphaMode: integer ``` #### tecs.assets.ModelMaterial.baseColorImage field Read-only. Selects a one-based entry in `Model.images`, or zero. ```teal tecs.assets.ModelMaterial.baseColorImage: integer ``` #### tecs.assets.ModelMaterial.normalImage field Read-only. Selects a tangent-space normal image, or zero. ```teal tecs.assets.ModelMaterial.normalImage: integer ``` #### tecs.assets.ModelMaterial.metallicRoughnessImage field Read-only. Selects a glTF metallic-roughness image, or zero. ```teal tecs.assets.ModelMaterial.metallicRoughnessImage: integer ``` #### tecs.assets.ModelMaterial.occlusionImage field Read-only. Selects an occlusion image, or zero. ```teal tecs.assets.ModelMaterial.occlusionImage: integer ``` #### tecs.assets.ModelMaterial.emissiveImage field Read-only. Selects an emissive image, or zero. ```teal tecs.assets.ModelMaterial.emissiveImage: integer ``` #### tecs.assets.ModelMaterial.alphaCutoff field Read-only. Reports the alpha-mask cutoff. Zero keeps every fragment. ```teal tecs.assets.ModelMaterial.alphaCutoff: number ``` #### tecs.assets.ModelMaterial.baseR field Read-only. Reports the base-color red factor. ```teal tecs.assets.ModelMaterial.baseR: number ``` #### tecs.assets.ModelMaterial.baseG field Read-only. Reports the base-color green factor. ```teal tecs.assets.ModelMaterial.baseG: number ``` #### tecs.assets.ModelMaterial.baseB field Read-only. Reports the base-color blue factor. ```teal tecs.assets.ModelMaterial.baseB: number ``` #### tecs.assets.ModelMaterial.baseA field Read-only. Reports the base-color alpha factor. ```teal tecs.assets.ModelMaterial.baseA: number ``` #### tecs.assets.ModelMaterial.emissiveR field Read-only. Reports the emissive red factor. ```teal tecs.assets.ModelMaterial.emissiveR: number ``` #### tecs.assets.ModelMaterial.emissiveG field Read-only. Reports the emissive green factor. ```teal tecs.assets.ModelMaterial.emissiveG: number ``` #### tecs.assets.ModelMaterial.emissiveB field Read-only. Reports the emissive blue factor. ```teal tecs.assets.ModelMaterial.emissiveB: number ``` #### tecs.assets.ModelMaterial.metallic field Caller-writable. Until model registration, this multiplies sampled metallic and defaults to the decoded glTF factor. ```teal tecs.assets.ModelMaterial.metallic: number ``` #### tecs.assets.ModelMaterial.roughness field Caller-writable. Until model registration, this multiplies sampled roughness and defaults to the decoded glTF factor. ```teal tecs.assets.ModelMaterial.roughness: number ``` #### tecs.assets.ModelMaterial.normalScale field Read-only. Reports tangent-space normal strength. ```teal tecs.assets.ModelMaterial.normalScale: number ``` #### tecs.assets.ModelMaterial.occlusionStrength field Read-only. Reports sampled occlusion strength. ```teal tecs.assets.ModelMaterial.occlusionStrength: number ``` #### tecs.assets.ModelMaterial.doubleSided field Read-only. Reports whether both triangle faces must render. ```teal tecs.assets.ModelMaterial.doubleSided: boolean ``` ### tecs.assets.ModelNode record Describes one node in a decoded glTF transform hierarchy. Read-only. Exposes one decoded model node. ```teal record tecs.assets.ModelNode parent: integer x: number y: number z: number rotationX: number rotationY: number rotationZ: number rotationW: number scaleX: number scaleY: number scaleZ: number matrix: {number} end ``` #### tecs.assets.ModelNode.parent field Read-only. Selects the one-based parent node, or zero for a root. ```teal tecs.assets.ModelNode.parent: integer ``` #### tecs.assets.ModelNode.x field Read-only. Contains the base translation x. ```teal tecs.assets.ModelNode.x: number ``` #### tecs.assets.ModelNode.y field Read-only. Contains the base translation y. ```teal tecs.assets.ModelNode.y: number ``` #### tecs.assets.ModelNode.z field Read-only. Contains the base translation z. ```teal tecs.assets.ModelNode.z: number ``` #### tecs.assets.ModelNode.rotationX field Read-only. Contains the base quaternion x. ```teal tecs.assets.ModelNode.rotationX: number ``` #### tecs.assets.ModelNode.rotationY field Read-only. Contains the base quaternion y. ```teal tecs.assets.ModelNode.rotationY: number ``` #### tecs.assets.ModelNode.rotationZ field Read-only. Contains the base quaternion z. ```teal tecs.assets.ModelNode.rotationZ: number ``` #### tecs.assets.ModelNode.rotationW field Read-only. Contains the base quaternion scalar component. ```teal tecs.assets.ModelNode.rotationW: number ``` #### tecs.assets.ModelNode.scaleX field Read-only. Contains the base x scale. ```teal tecs.assets.ModelNode.scaleX: number ``` #### tecs.assets.ModelNode.scaleY field Read-only. Contains the base y scale. ```teal tecs.assets.ModelNode.scaleY: number ``` #### tecs.assets.ModelNode.scaleZ field Read-only. Contains the base z scale. ```teal tecs.assets.ModelNode.scaleZ: number ``` #### tecs.assets.ModelNode.matrix field Read-only. Contains a fixed column-major local matrix when the source node used `matrix`, or nil when its local matrix is composed from TRS. ```teal tecs.assets.ModelNode.matrix: {number} ``` ### tecs.assets.ModelSkin record Describes one initial joint palette decoded from a glTF skin instance. Read-only. Exposes one decoded initial joint palette. ```teal record tecs.assets.ModelSkin name: string matrices: {number} node: integer joints: {integer} inverseBindMatrices: {number} end ``` #### tecs.assets.ModelSkin.name field Read-only. Contains the stable palette name used during registration. ```teal tecs.assets.ModelSkin.name: string ``` #### tecs.assets.ModelSkin.matrices field Read-only. Contains column-major joint matrices, sixteen floats per joint, in the skinned mesh's local coordinate space. ```teal tecs.assets.ModelSkin.matrices: {number} ``` #### tecs.assets.ModelSkin.node field Read-only. Selects the one-based mesh node whose local space contains this palette. ```teal tecs.assets.ModelSkin.node: integer ``` #### tecs.assets.ModelSkin.joints field Read-only. Contains one-based joint-node indices in palette order. ```teal tecs.assets.ModelSkin.joints: {integer} ``` #### tecs.assets.ModelSkin.inverseBindMatrices field Read-only. Contains column-major inverse bind matrices, sixteen floats per joint. ```teal tecs.assets.ModelSkin.inverseBindMatrices: {number} ``` ### tecs.assets.Sound record Represents a loaded clip and the caller's hold on it. What `loadSound` returns. Two overlapping loads of one path do not share, unlike images, so a `Sound` normally has one holder; the count is here so that releasing twice frees once rather than twice. Read-only. Exposes the loaded sound type. ```teal record tecs.assets.Sound path: string audio: loader.CValue resident: boolean durationMs: integer release: function(self) end ``` #### tecs.assets.Sound.path field Read-only. Contains the requested path unchanged. ```teal tecs.assets.Sound.path: string ``` #### tecs.assets.Sound.audio field Read-only. Contains the loaded clip until the last `release`. It is nil for one that streams, which holds nothing, and nil once released. ```teal tecs.assets.Sound.audio: loader.CValue ``` #### tecs.assets.Sound.resident field Read-only. Reports whether memory holds the decoded audio. It is false for a clip each voice reads from the file for itself. ```teal tecs.assets.Sound.resident: boolean ``` #### tecs.assets.Sound.durationMs field Read-only. Reports the length in milliseconds, or -1 when the container cannot say. ```teal tecs.assets.Sound.durationMs: integer ``` #### tecs.assets.Sound:release Instance Gives up this caller's hold on the clip, and frees at the last. A voice reads a clip where it lies, so release a clip when nothing will play it again. A track holds its own reference, so releasing one that is still sounding is safe: the mixer drops it when the last track using it does. Releasing a clip already down to nothing does nothing. ```teal function tecs.assets.Sound.release(self) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Sound` | | ##### Returns None. ## Functions ### tecs.assets.install Static Starts the loading worker. Installing twice is installing once. Spawning unconditionally would leave the first thread running with both its channels and nothing reading them, and every queued decode would answer into an abandoned channel, so a load in flight across the second call would never resolve. ```teal function tecs.assets.install(luaPath: string) ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `luaPath` | `string` | `package.path` for the worker's own state, which shares no loaded modules with this one. Defaults to this state's, which is what makes the worker resolve the same modules the game does. | #### Returns None. ### tecs.assets.installed Static Reports whether the loading worker is running. For a subsystem that loads an asset of its own and has no way of knowing whether the game has started the worker yet. ```teal function tecs.assets.installed(): boolean ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | `boolean` | Whether the worker exists, which is a fact about the process rather than about any world. False means a load raises rather than queueing, so this is the guard rather than an optimization. | ### tecs.assets.loadGLTF Static Queues glTF 2.0 or GLB decoding on the asset worker. External buffers and images resolve relative to the model path. Data URIs and embedded GLB resources are supported. Triangle primitives, static node transforms, metallic-roughness PBR, normal, occlusion, emissive, alpha-mask, alpha-blended, and unlit materials are decoded. JOINTS_0, WEIGHTS_0, skins, inverse bind matrices, sparse accessors, morph targets, mesh and node weights, and node animation clips are decoded. Missing tangents are generated with MikkTSpace, splitting vertices at tangent discontinuities and remapping every optional vertex stream. Non-triangle input and texture-coordinate transforms raise at the direct load call instead of loading incompletely. A primitive above 65,536 triangles is split into independently bounded and culled mesh records. Opaque and masked records are optimized for vertex-cache and vertex-fetch locality. Alpha-blended records preserve authored triangle order and receive only the lossless vertex-fetch remap. Registering a model containing alpha blending requires a mesh domain created with `transparency = true`. ```teal function tecs.assets.loadGLTF(path: string): Model ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `path` | `string` | The caller supplies a `.gltf` or `.glb` asset path. | #### Returns | Type | Description | | --- | --- | | [`Model`](/modules/assets/#tecs.assets.Model) | Returns the decoded model. The call suspends its system while the worker runs, or blocks when called outside a world update. | ### tecs.assets.loadImage Static Loads an image and returns its decoded pixels. Two loads of one path that overlap share a decode, because decoding the same PNG twice at once duplicates work without producing another result. They share the [`Image`](/modules/assets/#tecs.assets.Image), so the last caller to release it frees the pixels. A load that starts after the first has settled decodes again: nothing here is a cache. ```teal function tecs.assets.loadImage(path: string): Image ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `path` | `string` | The caller supplies a PNG, JPEG, static SVG, or linear BC3 KTX2 path. KTX2 input must contain one complete mip chain for one image. The loader renders SVG at its intrinsic width and height, uses bundled JetBrains Mono for text, and ignores external image references. Another format fails during decoding. | #### Returns | Type | Description | | --- | --- | | [`Image`](/modules/assets/#tecs.assets.Image) | Returns the decoded image. Inside a system, the call suspends only while its decode is pending. A missing file or decode failure raises. | ### tecs.assets.loadSound Static Loads a sound and returns its decoded or streaming payload. Whatever the mixer's decoders can read loads, so the format is the file's business rather than the caller's. `mode` is "resident", "stream", or "auto", and auto keeps anything shorter than `streamMs` resident. This call initializes the library instead of the worker because `MIX_Init` does not support concurrent calls. Initialization before sending the task puts it in order ahead of every decode without a lock. Two overlapping loads of one path do not share, unlike images: each gets its own clip, because that is what `MIX_LoadAudio` produces. The returned [`Sound`](/modules/assets/#tecs.assets.Sound) has exactly one holder. ```teal function tecs.assets.loadSound( path: string, mode: string, streamMs: integer ): Sound ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `path` | `string` | | | `mode` | `string` | "resident", "stream" or "auto". Any other value selects "stream". | | `streamMs` | `integer` | The boundary "auto" decides on, in milliseconds. Read only under "auto", and a file whose length the container cannot state streams whatever it says. | #### Returns | Type | Description | | --- | --- | | [`Sound`](/modules/assets/#tecs.assets.Sound) | Returns the loaded sound. Mixer initialization or decode failure raises. | ### tecs.assets.loadString Static Reads a complete file and returns its bytes. The returned string preserves embedded NUL bytes. The operation uses the common file lane; callers that need retained reuse store the returned immutable string in their own asset resource. ```teal function tecs.assets.loadString(path: string, kind: string): string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `path` | `string` | The caller supplies an absolute path or one from [`assetPath`](/modules/io/files/#tecs.io.files.assetPath). | | `kind` | `string` | The caller supplies the content kind used by file watching, or omits it to record a document. | #### Returns | Type | Description | | --- | --- | | `string` | Returns the complete bytes. A missing or unreadable file raises with the platform's own reason behind the path it could not read. | ### tecs.assets.pending Static Returns the number of asset loads still in flight. ```teal function tecs.assets.pending(): integer ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | `integer` | Every image decode, model load, and sound load owned by this module, not one caller's. `loadString` uses the shared file lane directly and is not counted. A model or sound load every caller has canceled remains counted until the worker answer is taken and destroyed. | ### tecs.assets.shutdown Static Stops the loading worker. Blocks until the thread exits. This call cancels loads still in flight, so drain with `waitAll` first when their results matter. Releasing an [`Image`](/modules/assets/#tecs.assets.Image) or a [`Sound`](/modules/assets/#tecs.assets.Sound) that already settled still works afterwards; this frees nothing. ```teal function tecs.assets.shutdown() ``` #### Arguments None. #### Returns None. ### tecs.assets.waitAll Static Blocks until every queued load has finished. This global barrier is for startup, shutdown, and tests outside a system. Applications use the process runtime during frames. It waits on every load in this process, including work a subsystem started, rather than only loads one caller initiated. ```teal function tecs.assets.waitAll(timeoutMs: number) ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `timeoutMs` | `number` | Wall-clock milliseconds, defaulting to 5000. Running out is not an error and is not reported, so read `assets.pending` afterwards to tell the two endings apart. | #### Returns None. ## Values ### tecs.assets.ANIMATION_CUBIC variable Read-only. Selects cubic-spline interpolation. ```teal tecs.assets.ANIMATION_CUBIC: integer ``` ### tecs.assets.ANIMATION_LINEAR variable Read-only. Selects linear interpolation. ```teal tecs.assets.ANIMATION_LINEAR: integer ``` ### tecs.assets.ANIMATION_ROTATION variable Read-only. Selects a rotation animation channel. ```teal tecs.assets.ANIMATION_ROTATION: integer ``` ### tecs.assets.ANIMATION_SCALE variable Read-only. Selects a scale animation channel. ```teal tecs.assets.ANIMATION_SCALE: integer ``` ### tecs.assets.ANIMATION_STEP variable Read-only. Selects held-key interpolation. ```teal tecs.assets.ANIMATION_STEP: integer ``` ### tecs.assets.ANIMATION_TRANSLATION variable Read-only. Selects a translation animation channel. ```teal tecs.assets.ANIMATION_TRANSLATION: integer ``` ### tecs.assets.ANIMATION_WEIGHTS variable Read-only. Selects a morph-weight animation channel. ```teal tecs.assets.ANIMATION_WEIGHTS: integer ``` ### tecs.assets.IMAGE_BC3 variable Read-only. Selects a complete imported BC3 image mip chain. ```teal tecs.assets.IMAGE_BC3: integer ``` ### tecs.assets.IMAGE_RGBA8 variable Read-only. Selects decoded, uncompressed RGBA8 image storage. ```teal tecs.assets.IMAGE_RGBA8: integer ``` --- ## tecs.audio # tecs.audio Clips, voices, groups, limits, and entity-owned sound. [`Application`](/modules/Application/) exposes one [`Audio`](/modules/audio/#tecs.audio.Audio) as `app.audio`. A platform without an output device still provides the object, but `available` reports false and playback returns handle zero. ## Playback `load` returns one shared clip per path. Inside a system it suspends the logical world update while the asset worker reads. Outside an update it blocks. ```teal local step = app.audio:load("assets/sfx/step.ogg") app.audio:setLimit( "footstep", { voices = 3, cooldown = 0.05, } ) local voice = app.audio:play( step, { gain = 0.7, group = "sfx", key = "footstep", pitchVariance = 0.1, } ) if voice ~= 0 then app.audio:stop(voice, 0.2) end ``` Clips shorter than `streamSeconds` stay resident and share decoded samples. Longer clips stream separately for each voice. `LoadOptions.stream` overrides that choice. Use `Audio.decoders()` when a game needs to report the formats available in the current build. A stale voice handle never controls a later voice that reuses its slot. Commands ignore stale handles and queries return false or nil. ## Groups and limits A group controls gain, mute, pause, resume, and stop. Its settings also apply to voices that join later. A key controls admission through a concurrent voice limit and cooldown. One voice may use both. Snapshots retain master and group settings. Pitch variance uses the world's snapshotted `tecs.audio` random stream. ## Placement `setPosition` uses right-handed coordinates with a listener fixed at the origin. The game chooses the listener, subtracts its position, and converts world units to an audio scale. `setStereo` assigns explicit left and right gains. The latest placement call replaces the earlier mode. ## Entity-owned sound An entity can carry [`Sound`](/modules/audio/#tecs.audio.Sound) instead of keeping a voice handle. The audio system starts it, follows writable playback fields, and stops it when the component or entity disappears. Write through `world:getMut`. A direct FFI write needs `world:markComponentDirty`, and `batchSpawn` must initialize every field. ## Module contents ### Constructors | Constructor | Description | | --- | --- | | [`newAudio`](/modules/audio/#tecs.audio.newAudio) | Builds the mixer a game plays sound through, opening the platform's default output. | ### Types | Type | Kind | Description | | --- | --- | --- | | [`Audio`](/modules/audio/#tecs.audio.Audio) | record | Represents one audio output and the voices sounding on it. | | [`Clip`](/modules/audio/#tecs.audio.Clip) | type | A loaded clip, or one still loading. | | [`Config`](/modules/audio/#tecs.audio.Config) | type | Configures newAudio. | | [`Device`](/modules/audio/#tecs.audio.Device) | record | Describes a physical device before the game opens it. | | [`Limit`](/modules/audio/#tecs.audio.Limit) | type | Defines how many voices a key allows and how soon it may repeat. | | [`LoadOptions`](/modules/audio/#tecs.audio.LoadOptions) | type | Configures Audio:load. | | [`Microphone`](/modules/audio/#tecs.audio.Microphone) | record | An open recording device, read by polling. | | [`MicrophoneConfig`](/modules/audio/#tecs.audio.MicrophoneConfig) | record | Configures openMicrophone. | | [`PlayOptions`](/modules/audio/#tecs.audio.PlayOptions) | type | Configures Audio:play. | | [`Sound`](/modules/audio/#tecs.audio.Sound) | record | Represents a sound attached to an entity. | | [`VoiceInfo`](/modules/audio/#tecs.audio.VoiceInfo) | type | Describes one voice returned by Audio:voices. | ### Functions | Function | Kind | Description | | --- | --- | --- | | [`openMicrophone`](/modules/audio/#tecs.audio.openMicrophone) | Static | Opens a microphone as interleaved native-endian 32-bit float samples. | | [`playbackDevices`](/modules/audio/#tecs.audio.playbackDevices) | Static | Returns the physical playback devices attached now. | | [`recordingDevices`](/modules/audio/#tecs.audio.recordingDevices) | Static | Returns the physical recording devices attached now. | ## Constructors ### tecs.audio.newAudio Static Builds the mixer a game plays sound through, opening the platform's default output. Never raises for want of hardware. A machine with no sound card gets an object whose calls all succeed and produce nothing, because few games require an audio device while many test machines lack one. ```teal function tecs.audio.newAudio(config: Audio.Config): Audio ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `config` | [`Audio.Config`](/modules/audio/#tecs.audio.Audio.Config) | Omitted takes the defaults. `maxVoices` outside 1 to 65535 raises, which is the one field here that does; every other one out of range is the platform's to refuse. | #### Returns | Type | Description | | --- | --- | | [`Audio`](/modules/audio/#tecs.audio.Audio) | The mixer, which the caller has to `destroy`. Check `available` for whether an output actually opened; it is false on a machine with no sound, and every call still succeeds and is silent. | ## Types ### tecs.audio.Audio record Represents one audio output and the voices sounding on it. Units, in one place, because none of them is in a signature: * **Time is seconds** everywhere on this surface: a fade, a seek, a loop point, a cooldown, a clip's duration. Milliseconds appear only below the platform seam. Every fade is a duration to run for, never a moment to finish at, so calling one twice restarts it rather than moving a deadline. * **Gain is linear amplitude, not decibels.** 0 is silence and 1 is the sound as recorded. Above 1 is louder and may clip. The level a voice reaches the hardware at is its own gain times its group's gain times the master gain, and a mute at either level composes as a zero without discarding the gain underneath it. * **Pitch is a playback rate, not an interval.** 1 preserves the original rate, 2 is an octave up and half as long, 0.5 an octave down and twice as long. It resamples, so it changes duration; there is no time-stretch here. * **Position is the mixer's frame, not the world's.** The mixer fixes the listener at the origin. x is positive to the right, y is positive **up**, and z is positive behind. World Y runs down, so a caller feeding world coordinates in has to flip Y as well as subtract whatever it decided is listening. See [`Sound`](/modules/audio/#tecs.audio.Sound). A voice handle is a generation and a slot packed into one integer. It stays meaningful for exactly as long as the voice sounds. When a voice ends naturally or either `stop` or `update` ends it, every call taking the handle becomes a no-op, and it never starts naming the next voice to take that slot. Nothing requires release, and holding a stale handle is not an error. Nothing here is thread safe, and nothing here calls back. Everything runs on the thread that calls it. ```teal record tecs.audio.Audio record Config frequency: integer channels: integer maxVoices: integer streamSeconds: number backend: Backend device: number end record Clip path: string id: integer status: string error: string duration: number resident: boolean end record LoadOptions stream: boolean end record PlayOptions gain: number loop: boolean loopStart: number start: number fadeIn: number pitch: number pitchVariance: number group: string key: string spatial: boolean x: number y: number z: number stereo: boolean left: number right: number end record Limit voices: integer cooldown: number end record VoiceInfo handle: integer clip: string gain: number applied: number pitch: number group: string key: string paused: boolean stopping: boolean owned: boolean loop: boolean spatial: boolean x: number y: number z: number stereo: boolean left: number right: number end record Sound is Component clip: number playing: number gain: number loop: number pitch: number spatial: number x: number y: number z: number group: number voice: number end available: boolean clipId: function(path: string): integer clipPath: function(id: integer): string decoders: function(): {string} groupId: function(name: string): integer groupName: function(id: integer): string of: function(world: types.World): Audio clearSpatial: function(self, handle: integer) clip: function(self, id: integer): Audio.Clip clips: function(self): {Audio.Clip} destroy: function(self) groupGain: function(self, name: string): number groupMuted: function(self, name: string): boolean groupPaused: function(self, name: string): boolean groups: function(self): {string} install: function(self, world: types.World) keyCount: function(self, key: string): integer keys: function(self): {string} limit: function(self, key: string): Audio.Limit load: function( self, path: string, options: Audio.LoadOptions ): Audio.Clip looping: function(self, handle: integer): boolean masterGain: function(self): number maxVoices: function(self): integer muted: function(self): boolean pause: function(self, handle: integer) paused: function(self, handle: integer): boolean pauseGroup: function(self, name: string) play: function( self, clip: Audio.Clip, options: Audio.PlayOptions ): integer playing: function(self, handle: integer): boolean reload: function(self, path: string): boolean, string resume: function(self, handle: integer) resumeGroup: function(self, name: string) seek: function(self, handle: integer, seconds: number): boolean setGain: function(self, handle: integer, gain: number) setGroupGain: function(self, name: string, gain: number) setGroupMuted: function(self, name: string, muted: boolean) setLimit: function(self, key: string, limit: Audio.Limit) setLoop: function(self, handle: integer, loop: boolean) setMasterGain: function(self, gain: number) setMuted: function(self, muted: boolean) setPitch: function(self, handle: integer, ratio: number) setPosition: function( self, handle: integer, x: number, y: number, z: number ) setStereo: function( self, handle: integer, left: number, right: number ) sounding: function(self): integer stop: function(self, handle: integer, fadeOut: number) stopAll: function(self, fadeOut: number) stopGroup: function(self, name: string, fadeOut: number) tell: function(self, handle: integer): number update: function(self, dt: number): integer voices: function(self): {Audio.VoiceInfo} end ``` #### tecs.audio.Audio.Config record Configures `newAudio` through optional fields. ```teal record tecs.audio.Audio.Config frequency: integer channels: integer maxVoices: integer streamSeconds: number backend: Backend device: number end ``` ##### tecs.audio.Audio.Config.frequency field Caller-writable. Sets the sample frequency in frames per second and defaults to 48000. ```teal tecs.audio.Audio.Config.frequency: integer ``` ##### tecs.audio.Audio.Config.channels field Caller-writable. Sets the number of output channels and defaults to two. ```teal tecs.audio.Audio.Config.channels: integer ``` ##### tecs.audio.Audio.Config.maxVoices field Caller-writable. Sets how many voices may sound at once and defaults to 32. ```teal tecs.audio.Audio.Config.maxVoices: integer ``` ##### tecs.audio.Audio.Config.streamSeconds field Caller-writable. Sets the duration in seconds at which a clip streams instead of remaining resident. Defaults to 10. ```teal tecs.audio.Audio.Config.streamSeconds: number ``` ##### tecs.audio.Audio.Config.backend field Caller-writable. Selects the output backend and defaults to the installed platform backend. ```teal tecs.audio.Audio.Config.backend: Backend ``` ##### tecs.audio.Audio.Config.device field Caller-writable. Selects a physical output device by an id from `tecs.audio.playbackDevices`. Omitted follows the operating-system default as it changes. ```teal tecs.audio.Audio.Config.device: number ``` #### tecs.audio.Audio.Clip record Represents one shared playable sound after its load. `load` returns this only after reading succeeds. `clip` also exposes the cached failure state, which is why the status and error remain visible. Every load for the same path shares one instance. ```teal record tecs.audio.Audio.Clip path: string id: integer status: string error: string duration: number resident: boolean end ``` ##### tecs.audio.Audio.Clip.path field Read-only. Reports the path used to load the clip, exactly as given. This is a clip's identity, so two spellings of one file are two clips. ```teal tecs.audio.Audio.Clip.path: string ``` ##### tecs.audio.Audio.Clip.id field Read-only. Reports the `path` index carried by a [`Sound`](/modules/audio/#tecs.audio.Sound) component. ```teal tecs.audio.Audio.Clip.id: integer ``` ##### tecs.audio.Audio.Clip.status field Read-only. Reports `"pending"` while the loader reads the file, then `"ready"` or `"failed"`, and `"released"` once the owning [`Audio`](/modules/audio/#tecs.audio.Audio) is destroyed. These four words reach an agent over JSON-RPC, through the `audio` debug tool's clip list, so they are a compatibility surface rather than identifiers this tree renames in one commit. `"released"` means the load succeeded and the owning audio instance later returned its samples. ```teal tecs.audio.Audio.Clip.status: string ``` ##### tecs.audio.Audio.Clip.error field Read-only. Reports the error when loading fails. ```teal tecs.audio.Audio.Clip.error: string ``` ##### tecs.audio.Audio.Clip.duration field Read-only. Reports the audio duration in seconds, or zero when the file cannot provide one. ```teal tecs.audio.Audio.Clip.duration: number ``` ##### tecs.audio.Audio.Clip.resident field Read-only. Reports whether decoded audio remains in memory. False identifies a clip each voice reads from the file for itself. ```teal tecs.audio.Audio.Clip.resident: boolean ``` #### tecs.audio.Audio.LoadOptions record Configures `load` through one optional field. ```teal record tecs.audio.Audio.LoadOptions stream: boolean end ``` ##### tecs.audio.Audio.LoadOptions.stream field Caller-writable. Forces streaming when true and residency when false. Left unset, the clip's duration decides against `streamSeconds`. ```teal tecs.audio.Audio.LoadOptions.stream: boolean ``` #### tecs.audio.Audio.PlayOptions record Configures `play` through optional fields. The whole table may be omitted. Read once, when the voice starts. Changing the table afterwards reaches nothing: use `setGain`, `setPitch`, `setLoop`, `setPosition` and `setStereo` on the handle instead. The table is not retained, so one a caller may fill and reuse one table for every play. ```teal record tecs.audio.Audio.PlayOptions gain: number loop: boolean loopStart: number start: number fadeIn: number pitch: number pitchVariance: number group: string key: string spatial: boolean x: number y: number z: number stereo: boolean left: number right: number end ``` ##### tecs.audio.Audio.PlayOptions.gain field Caller-writable. Sets linear gain from zero to one before group and master gains. Defaults to one. ```teal tecs.audio.Audio.PlayOptions.gain: number ``` ##### tecs.audio.Audio.PlayOptions.loop field Caller-writable. Repeats playback until stopped and defaults to false. ```teal tecs.audio.Audio.PlayOptions.loop: boolean ``` ##### tecs.audio.Audio.PlayOptions.loopStart field Caller-writable. Sets the position in seconds to which a repeat returns, so an intro can play once and the rest of it loop. Defaults to 0. ```teal tecs.audio.Audio.PlayOptions.loopStart: number ``` ##### tecs.audio.Audio.PlayOptions.start field Caller-writable. Sets where the first pass begins in seconds and defaults to zero. ```teal tecs.audio.Audio.PlayOptions.start: number ``` ##### tecs.audio.Audio.PlayOptions.fadeIn field Caller-writable. Sets the fade-in duration in seconds and defaults to zero. ```teal tecs.audio.Audio.PlayOptions.fadeIn: number ``` ##### tecs.audio.Audio.PlayOptions.pitch field Caller-writable. Sets playback rate and defaults to one. ```teal tecs.audio.Audio.PlayOptions.pitch: number ``` ##### tecs.audio.Audio.PlayOptions.pitchVariance field Caller-writable. Sets the fraction of `pitch` varied for each new voice. A value of 0.1 spreads voices over plus or minus a tenth. Defaults to 0. ```teal tecs.audio.Audio.PlayOptions.pitchVariance: number ``` ##### tecs.audio.Audio.PlayOptions.group field Caller-writable. Selects the group this voice joins and defaults to none. ```teal tecs.audio.Audio.PlayOptions.group: string ``` ##### tecs.audio.Audio.PlayOptions.key field Caller-writable. Selects the limit bucket counted by this voice and defaults to none. ```teal tecs.audio.Audio.PlayOptions.key: string ``` ##### tecs.audio.Audio.PlayOptions.spatial field Caller-writable. Enables spatial positioning. See [`Sound`](/modules/audio/#tecs.audio.Sound) for the coordinate system. ```teal tecs.audio.Audio.PlayOptions.spatial: boolean ``` ##### tecs.audio.Audio.PlayOptions.x field Caller-writable. Sets the position right of the listener and defaults to zero. ```teal tecs.audio.Audio.PlayOptions.x: number ``` ##### tecs.audio.Audio.PlayOptions.y field Caller-writable. Sets the position above the listener, which uses the opposite sign from world Y. Defaults to 0. ```teal tecs.audio.Audio.PlayOptions.y: number ``` ##### tecs.audio.Audio.PlayOptions.z field Caller-writable. Sets the position behind the listener and defaults to zero. ```teal tecs.audio.Audio.PlayOptions.z: number ``` ##### tecs.audio.Audio.PlayOptions.stereo field Caller-writable. Pins the voice to the front pair of speakers at `left` and `right`, which is a pan rather than a position. Ignored when `spatial` is set, because the mixer holds one placement per track. Both gains default to 1. ```teal tecs.audio.Audio.PlayOptions.stereo: boolean ``` ##### tecs.audio.Audio.PlayOptions.left field Caller-writable. Sets left-speaker gain on the same linear scale as `gain`. Negative reads as silence and above 1 is louder. Defaults to 1. ```teal tecs.audio.Audio.PlayOptions.left: number ``` ##### tecs.audio.Audio.PlayOptions.right field Caller-writable. Sets right-speaker gain on the same terms as `left`. ```teal tecs.audio.Audio.PlayOptions.right: number ``` #### tecs.audio.Audio.Limit record Defines what a key allows. ```teal record tecs.audio.Audio.Limit voices: integer cooldown: number end ``` ##### tecs.audio.Audio.Limit.voices field Caller-writable. Sets how many voices this key may hold at once. Zero or absent removes the ceiling. ```teal tecs.audio.Audio.Limit.voices: integer ``` ##### tecs.audio.Audio.Limit.cooldown field Caller-writable. Sets the cooldown in seconds after a voice starts. Zero disables the cooldown. ```teal tecs.audio.Audio.Limit.cooldown: number ``` #### tecs.audio.Audio.VoiceInfo record Describes one sounding voice for inspection. ```teal record tecs.audio.Audio.VoiceInfo handle: integer clip: string gain: number applied: number pitch: number group: string key: string paused: boolean stopping: boolean owned: boolean loop: boolean spatial: boolean x: number y: number z: number stereo: boolean left: number right: number end ``` ##### tecs.audio.Audio.VoiceInfo.handle field Read-only. Reports the handle accepted by `playing` and `stop`. ```teal tecs.audio.Audio.VoiceInfo.handle: integer ``` ##### tecs.audio.Audio.VoiceInfo.clip field Read-only. Reports the clip path, or nil after release. ```teal tecs.audio.Audio.VoiceInfo.clip: string ``` ##### tecs.audio.Audio.VoiceInfo.gain field Read-only. Reports the gain requested by the voice before group and master gain. ```teal tecs.audio.Audio.VoiceInfo.gain: number ``` ##### tecs.audio.Audio.VoiceInfo.applied field Read-only. Reports the gain applied to the track, including group gain and mute. The master gain is not in it: that is the mixer's own number and is not multiplied per voice. ```teal tecs.audio.Audio.VoiceInfo.applied: number ``` ##### tecs.audio.Audio.VoiceInfo.pitch field Read-only. Reports the playback rate carried by the track, so the pitch variance a value already includes any variance that `play` drew rather than only the requested rate. ```teal tecs.audio.Audio.VoiceInfo.pitch: number ``` ##### tecs.audio.Audio.VoiceInfo.group field Read-only. Reports the group tag, or nil for no group. ```teal tecs.audio.Audio.VoiceInfo.group: string ``` ##### tecs.audio.Audio.VoiceInfo.key field Read-only. Reports the limit bucket, or nil for no bucket. ```teal tecs.audio.Audio.VoiceInfo.key: string ``` ##### tecs.audio.Audio.VoiceInfo.paused field Read-only. Reports whether its own pause or its group holds it. ```teal tecs.audio.Audio.VoiceInfo.paused: boolean ``` ##### tecs.audio.Audio.VoiceInfo.stopping field Read-only. Reports whether a fade-out is stopping the voice. ```teal tecs.audio.Audio.VoiceInfo.stopping: boolean ``` ##### tecs.audio.Audio.VoiceInfo.owned field Read-only. Reports whether a [`Sound`](/modules/audio/#tecs.audio.Sound) component started the voice instead of `play`. ```teal tecs.audio.Audio.VoiceInfo.owned: boolean ``` ##### tecs.audio.Audio.VoiceInfo.loop field Read-only. Reports whether playback repeats at the end. ```teal tecs.audio.Audio.VoiceInfo.loop: boolean ``` ##### tecs.audio.Audio.VoiceInfo.spatial field Read-only. Reports whether the voice has a spatial position. This remains false when `stereo`: the mixer holds one placement per track. ```teal tecs.audio.Audio.VoiceInfo.spatial: boolean ``` ##### tecs.audio.Audio.VoiceInfo.x field Read-only. Reports the last horizontal position pushed in the mixer's coordinate system, positive right. Meaningless unless `spatial`. ```teal tecs.audio.Audio.VoiceInfo.x: number ``` ##### tecs.audio.Audio.VoiceInfo.y field Read-only. Reports the last vertical position pushed, positive up. It has no meaning unless `spatial`. ```teal tecs.audio.Audio.VoiceInfo.y: number ``` ##### tecs.audio.Audio.VoiceInfo.z field Read-only. Reports the last depth position pushed, positive behind. It has no meaning unless `spatial`. ```teal tecs.audio.Audio.VoiceInfo.z: number ``` ##### tecs.audio.Audio.VoiceInfo.stereo field Read-only. Reports whether the voice remains pinned to the front speaker pair. ```teal tecs.audio.Audio.VoiceInfo.stereo: boolean ``` ##### tecs.audio.Audio.VoiceInfo.left field Read-only. Reports the last left-speaker gain pushed. It has no meaning unless `stereo`. ```teal tecs.audio.Audio.VoiceInfo.left: number ``` ##### tecs.audio.Audio.VoiceInfo.right field Read-only. Reports the last right-speaker gain pushed. It has no meaning unless `stereo`. ```teal tecs.audio.Audio.VoiceInfo.right: number ``` #### tecs.audio.Audio.Sound record Represents a sound attached to an entity. Presence is the instruction: an entity carrying this with a loaded clip starts sounding on the next audio pass, and stops when the component or the entity goes away. That is what makes sound an entity rather than a handle a game has to remember to release, and it is why despawning something mid-sound does the obvious thing. The audio pass sends position to the mixer and does nothing else with it. `spatial` switches it on, and the mixer reads `x`, `y` and `z` in a right-handed system. The mixer fixes its listener at the origin, with x positive to the right, y positive up and z positive behind. So a caller with a camera and a world has three jobs this component does not do for it, and doing any of them here would fix answers that belong to a game: 1. Subtract the listener. There is no listener component and no rule that the camera is one, so whatever a game decides is listening is what it subtracts before writing these fields. 2. Choose a scale. The mixer attenuates with distance on its own, and how loud a sound a hundred world units away should be is a question about a game's units, not about audio. 3. Decide what a screen-space sound means. A sound on a layer that does not move with the camera has no world position to convert, and the answer is to leave `spatial` at zero. A name identifies a group, just as a path identifies a clip. An FFI component holds numbers, so what `group` carries is an interned index from `Audio.groupId` rather than the mixer's tag itself, and `Audio.groupName` reads it back. The index is this run's and no other, which is why `serialize` writes the name: an integer in a save file would name a different group the next time a build interned its names in a different order. The audio pass follows `playing`, `gain`, `loop`, `pitch` and the position for as long as the voice sounds, so writing any of them is enough and nothing has to restart the voice to apply a change. Each of those costs one read and compare per sounding row per frame, which is the price of the component being live and is why `clip` and `group` are not on the list: both take an untag, a retag and a fresh input to change, neither is something a game writes often, and following them would put that cost on every row that never does. Moving a sound into another group is setting `group` and clearing `voice`, which is the same thing changing its clip takes. Disabling an entity silences its sound and re-enabling starts it again. Queries exclude [`Disabled`](/modules/ecs/#tecs.ecs.Disabled) unless they name it, so a disabled row is not followed and its voice is taken back; the audio pass clears the handle at the same moment, which is what lets the sound return with the entity rather than reading as a one-shot that had finished. Read-only. Exposes the [`Sound`](/modules/audio/#tecs.audio.Sound) component for queries and writes. See the record's own documentation for what its fields mean. ```teal record tecs.audio.Audio.Sound is Component clip: number playing: number gain: number loop: number pitch: number spatial: number x: number y: number z: number group: number voice: number end ``` ##### Interfaces | Interface | | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | ##### tecs.audio.Audio.Sound.clip field Caller-writable. Selects a clip by its `Audio.clipId` index. Zero plays nothing. ```teal tecs.audio.Audio.Sound.clip: number ``` ##### tecs.audio.Audio.Sound.playing field Caller-writable. Uses nonzero to request playback and zero to stop. This field gives an instruction rather than a report: clearing it stops the voice and setting it again starts one, including on a one-shot that has already run out. ```teal tecs.audio.Audio.Sound.playing: number ``` ##### tecs.audio.Audio.Sound.gain field Caller-writable. Sets linear gain from zero to one before group and master gain. ```teal tecs.audio.Audio.Sound.gain: number ``` ##### tecs.audio.Audio.Sound.loop field Caller-writable. Uses nonzero to repeat. Clearing it part way through lets the voice play out to its end rather than cutting it. ```teal tecs.audio.Audio.Sound.loop: number ``` ##### tecs.audio.Audio.Sound.pitch field Caller-writable. Sets playback rate. One leaves it unchanged, while two plays an octave higher in half the time. ```teal tecs.audio.Audio.Sound.pitch: number ``` ##### tecs.audio.Audio.Sound.spatial field Caller-writable. Uses nonzero to read `x`, `y`, and `z`; zero mixes without a position. ```teal tecs.audio.Audio.Sound.spatial: number ``` ##### tecs.audio.Audio.Sound.x field Caller-writable. Sets the position right of the listener. ```teal tecs.audio.Audio.Sound.x: number ``` ##### tecs.audio.Audio.Sound.y field Caller-writable. Sets the position above the listener. ```teal tecs.audio.Audio.Sound.y: number ``` ##### tecs.audio.Audio.Sound.z field Caller-writable. Sets the position behind the listener. ```teal tecs.audio.Audio.Sound.z: number ``` ##### tecs.audio.Audio.Sound.group field Caller-writable. Selects a group by its `Audio.groupId` index. Zero joins no group. ```teal tecs.audio.Audio.Sound.group: number ``` ##### tecs.audio.Audio.Sound.voice field Engine-owned. Reports the voice assigned by the audio pass: zero before it starts and negative once a one-shot has finished. Written by the engine; setting it back to zero is how a game asks for the sound again. ```teal tecs.audio.Audio.Sound.voice: number ``` #### tecs.audio.Audio.available field Read-only. Reports whether an output opened. False identifies a machine with no sound, where every call here still works and nothing is heard. ```teal tecs.audio.Audio.available: boolean ``` #### tecs.audio.Audio.clipId Static Returns the index of a clip path and assigns one on first use. ```teal function tecs.audio.Audio.clipId(path: string): integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `path` | `string` | Must be non-empty; an empty one raises. Not checked against the filesystem, so this hands out an index for a path that does not exist. | ##### Returns | Type | Description | | --- | --- | | `integer` | An index from 1 up, shared by every [`Audio`](/modules/audio/#tecs.audio.Audio) in the process. It belongs only to this run and means nothing in a file, which is why [`Sound`](/modules/audio/#tecs.audio.Sound) serializes the path instead. | #### tecs.audio.Audio.clipPath Static Returns the path represented by a clip index, or nil. ```teal function tecs.audio.Audio.clipPath(id: integer): string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `id` | `integer` | Zero is the index of no clip and answers nil, as does any index never handed out. | ##### Returns | Type | Description | | --- | --- | | `string` | | #### tecs.audio.Audio.decoders Static Returns the decoders linked into this build in mixer order. What a build asked for and what it got are different questions: a decoder The mixer silently omits a decoder whose dependency the build could not find. This function answers the second question. ```teal function tecs.audio.Audio.decoders(): {string} ``` ##### Arguments None. ##### Returns | Type | Description | | --- | --- | | `{string}` | Decoder names such as `"WAV"` and `"OGG"`, in the mixer's own order rather than sorted, and empty when the mixer will not start at all. A fresh table each call. | #### tecs.audio.Audio.groupId Static Returns the index of a group name and assigns one on first use. What a [`Sound`](/modules/audio/#tecs.audio.Sound) carries in `group`. The index means nothing outside the run that handed it out, so it belongs in a component and never in a file. ```teal function tecs.audio.Audio.groupId(name: string): integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `name` | `string` | Must be non-empty; an empty one raises. A group needs no declaring: naming one here is all it takes to exist. | ##### Returns | Type | Description | | --- | --- | | `integer` | An index from 1 up, shared by every [`Audio`](/modules/audio/#tecs.audio.Audio) in the process. | #### tecs.audio.Audio.groupName Static Returns the name represented by a group index, or nil. ```teal function tecs.audio.Audio.groupName(id: integer): string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `id` | `integer` | Zero is the index of no group and answers nil, which is what a [`Sound`](/modules/audio/#tecs.audio.Sound) in no group carries. | ##### Returns | Type | Description | | --- | --- | | `string` | | #### tecs.audio.Audio.of Static Returns the audio installed into a world, or nil. What lets something holding only the world reach the mixer, which is what the debug tools have and what a game writing its own systems often has too. ```teal function tecs.audio.Audio.of(world: types.World): Audio ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`types.World`](/modules/ecs/#tecs.World) | | ##### Returns | Type | Description | | --- | --- | | [`Audio`](/modules/audio/#tecs.audio.Audio) | nil before `install` and again after `destroy`, which removes the instance from every world. | #### tecs.audio.Audio:clearSpatial Instance Returns a voice to unpositioned mixing, out of either placement. One call answers for both, because clearing either mode in the mixer clears the other. A no-op for a handle that names nothing and for a voice that was never placed. ```teal function tecs.audio.Audio.clearSpatial(self, handle: integer) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `handle` | `integer` | | ##### Returns None. #### tecs.audio.Audio:clip Instance Returns the clip represented by an index, or nil when this instance has not loaded it. ```teal function tecs.audio.Audio.clip(self, id: integer): Audio.Clip ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `id` | `integer` | From `Audio.clipId`, or from a `Sound.clip` field. Clip indices are every instance shares clip indices but not clips, so an index another instance loaded answers nil here. | ##### Returns | Type | Description | | --- | --- | | [`Audio.Clip`](/modules/audio/#tecs.audio.Audio.Clip) | | #### tecs.audio.Audio:clips Instance Returns every loaded clip in request order. For introspection: it builds a list per call, so nothing on a frame's path should read it. ```teal function tecs.audio.Audio.clips(self): {Audio.Clip} ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | ##### Returns | Type | Description | | --- | --- | | `{`[`Audio.Clip`](/modules/audio/#tecs.audio.Audio.Clip)`}` | A fresh list each call, holding the live clip records rather than copies, so their `status` moves under a caller that keeps one. | #### tecs.audio.Audio:destroy Instance Stops everything and closes the output. Stops every voice without a fade, moves every clip to `"released"`, and removes this mixer from every world that installed it. There is no reopening: make a new instance instead. ```teal function tecs.audio.Audio.destroy(self) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | ##### Returns None. #### tecs.audio.Audio:groupGain Instance Returns a group's gain, or one when none has been set. The level, not what is audible: a muted group still answers with the gain an unmute would put back. ```teal function tecs.audio.Audio.groupGain(self, name: string): number ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `name` | `string` | | ##### Returns | Type | Description | | --- | --- | | `number` | 1 for a group nothing has set, which is indistinguishable from one explicitly set to 1. | #### tecs.audio.Audio:groupMuted Instance Reports whether mute applies to a group. ```teal function tecs.audio.Audio.groupMuted(self, name: string): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `name` | `string` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | | #### tecs.audio.Audio:groupPaused Instance Returns whether a group holds its current and future voices. ```teal function tecs.audio.Audio.groupPaused(self, name: string): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `name` | `string` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | | #### tecs.audio.Audio:groups Instance Returns every group known to this instance, including any whose gain, mute, or pause has been set, and one a sounding voice is in. Sorted, so a caller reading it twice reads it the same way. ```teal function tecs.audio.Audio.groups(self): {string} ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | ##### Returns | Type | Description | | --- | --- | | `{string}` | A fresh table each call, which the caller owns. A group whose only voice has ended and which nothing was set on drops out of it, so this is not a stable list of a game's groups. | #### tecs.audio.Audio:install Instance Adds the system that plays [`Sound`](/modules/audio/#tecs.audio.Sound) components, and the snapshot handler that carries the mixer. `update` is not added here. Reaping voices is not world work: it has to continue during a world pause, and an application drives it from the iteration instead. ```teal function tecs.audio.Audio.install(self, world: types.World) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `world` | [`types.World`](/modules/ecs/#tecs.World) | Repoints this instance's pitch variance at the world's `tecs.audio` random stream, so installing into a second world moves the variance to that one's. This interface expects one [`Audio`](/modules/audio/#tecs.audio.Audio) per world. | ##### Returns None. #### tecs.audio.Audio:keyCount Instance Returns how many voices a key holds now. ```teal function tecs.audio.Audio.keyCount(self, key: string): integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `key` | `string` | | ##### Returns | Type | Description | | --- | --- | | `integer` | Voices counted against the key, paused and fading ones included, since a slot is not given back until the voice ends. 0 for a key that has never held one. | #### tecs.audio.Audio:keys Instance Returns every key with a limit or counted voice, sorted. ```teal function tecs.audio.Audio.keys(self): {string} ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | ##### Returns | Type | Description | | --- | --- | | `{string}` | A fresh table each call, which the caller owns. A key keeps its bucket once one exists, so a key that has played and stopped is still listed. | #### tecs.audio.Audio:limit Instance Returns the limit assigned to a key, or nil. ```teal function tecs.audio.Audio.limit(self, key: string): Audio.Limit ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `key` | `string` | | ##### Returns | Type | Description | | --- | --- | | [`Audio.Limit`](/modules/audio/#tecs.audio.Audio.Limit) | The record that was set, not a copy, so writing through it changes the limit in force. | #### tecs.audio.Audio:load Instance Loads a sound and returns its cached clip. Loading the same path twice returns the same clip. A clip is the file, and playing it twice is two voices reading one clip. `options.stream` overrides the duration threshold that otherwise decides whether it stays in memory. ```teal function tecs.audio.Audio.load( self, path: string, options: Audio.LoadOptions ): Audio.Clip ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `path` | `string` | Must be non-empty. A missing or unreadable file raises after the asset worker reports it. | | `options` | [`Audio.LoadOptions`](/modules/audio/#tecs.audio.Audio.LoadOptions) | | ##### Returns | Type | Description | | --- | --- | | [`Audio.Clip`](/modules/audio/#tecs.audio.Audio.Clip) | Returns the cached clip. Only the first call reads `options`. | #### tecs.audio.Audio:looping Instance Returns whether a voice repeats at the end. ```teal function tecs.audio.Audio.looping(self, handle: integer): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `handle` | `integer` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | What this layer last told the mixer, not a reading from it. false for a handle that names nothing. | #### tecs.audio.Audio:masterGain Instance Returns master gain, whether or not mute holds it down. ```teal function tecs.audio.Audio.masterGain(self): number ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | ##### Returns | Type | Description | | --- | --- | | `number` | | #### tecs.audio.Audio:maxVoices Instance Returns the configured maximum number of simultaneous voices. ```teal function tecs.audio.Audio.maxVoices(self): integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | ##### Returns | Type | Description | | --- | --- | | `integer` | The ceiling `play` refuses at, fixed for the instance's life. Not a hardware limit: it is what `newAudio` was given, defaulting to 32. | #### tecs.audio.Audio:muted Instance Returns whether master mute holds the output down. ```teal function tecs.audio.Audio.muted(self): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | | #### tecs.audio.Audio:pause Instance Holds a voice where it is. It keeps its slot until something resumes or stops it, because a paused voice has not finished. Neither this nor `resume` takes a fade, and that is a decision rather than an omission. The mixer ramps on a play and on a stop and nowhere else, so a faded pause has to be a ramp run from here: a list of voices in flight, a `setGain` per voice per frame, and the pause itself issued only once the ramp reaches zero, which makes this a command that takes effect later and needs its own answer for what a stop, a group gain or a second pause during the ramp means. Each step also lands on a frame boundary, so a quarter-second fade at 60 frames a second is fifteen steps on a stream running at 48000, and its length follows the frame rate rather than the clock. Drift and cost of exactly that kind are what handing fades to the mixer avoids, and one voice held for a menu is not worth giving it up. The mixer does offer the pieces to fade out and take a sound back where it left off: `tell` reads the position, `stop` fades out, and `play` returns with `start` and `fadeIn`. That re-reads the input rather than holding it, so it is a different thing from a pause, and which of the two a moment wants is a game's answer rather than this layer's. ```teal function tecs.audio.Audio.pause(self, handle: integer) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `handle` | `integer` | A no-op for a handle that names nothing, one already paused, and one fading out, since a paused fade would never finish. Not holder counted: one `resume` undoes any number of `pause` calls. | ##### Returns None. #### tecs.audio.Audio:paused Instance Returns whether a handle names a paused voice. ```teal function tecs.audio.Audio.paused(self, handle: integer): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `handle` | `integer` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | true whether the hold came from `pause` on this handle or from `pauseGroup` on its group; the two are not told apart. false for a handle that names nothing. | #### tecs.audio.Audio:pauseGroup Instance Holds every voice in a group where it is, and every voice that joins it later. The audio object records the pause as well as sending it, because the mixer's tag pause reaches only voices sounding at that moment. Without the record, a sound started into a paused group would be the one thing still heard. ```teal function tecs.audio.Audio.pauseGroup(self, name: string) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `name` | `string` | Not holder counted: one `resumeGroup` undoes any number of these. A voice `resume`d out of a paused group stays going, and the group's hold still applies to whatever joins next. | ##### Returns None. #### tecs.audio.Audio:play Instance Plays `clip`, returning a handle, or zero when it did not start. Zero means the clip is not loaded, loading failed, a key's limit or cooldown declined it, every voice is busy, or the machine has no output. None of those is worth raising over: a sound that does not play is not a reason for a frame to stop. A key's limit **drops the new voice**; it never steals an older one. A sound that stops halfway through because something else started is harder to explain than one that never started, and the same reasoning is why a full voice pool declines rather than stealing. ```teal function tecs.audio.Audio.play( self, clip: Audio.Clip, options: Audio.PlayOptions ): integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `clip` | [`Audio.Clip`](/modules/audio/#tecs.audio.Audio.Clip) | Accepts nil and returns zero, so callers may pass a `load` result directly without a guard. | | `options` | [`Audio.PlayOptions`](/modules/audio/#tecs.audio.Audio.PlayOptions) | The method reads this once and follows no later changes. | ##### Returns | Type | Description | | --- | --- | | `integer` | A handle for `stop`, `playing`, `setGain` and the rest, or zero when nothing started. It stays valid while the voice sounds and goes inert for good once it does not; it is never reissued for a later voice, and requires no release. | #### tecs.audio.Audio:playing Instance Returns whether a handle still names a sounding voice. A paused or fading voice still counts: it has not finished. ```teal function tecs.audio.Audio.playing(self, handle: integer): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `handle` | `integer` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | false for a handle whose voice has ended, for zero, and for one from another [`Audio`](/modules/audio/#tecs.audio.Audio). A voice that ended on its own reads true until the next `update` reaps it, since nothing here is told the instant it finished. | #### tecs.audio.Audio:reload Instance Re-reads a clip's file over the clip already loaded from it. The counterpart to `renderer.sprites:replaceImage`, and it keeps identity on the same terms. A clip's index is its path's, so an edited file comes back under the index every [`Sound`](/modules/audio/#tecs.audio.Sound) row already carries and nothing in the world is touched. A streamed clip holds nothing to replace. Each voice opens the file for itself, so the next one to start reads what is on disk now and this has only to say so. Voices already sounding read the stream they opened, which is what happens to a file edited under a running voice with or without a reload. The loader decodes a resident clip again and swaps it in. It stays resident whatever the new file's length would have chosen: rows already pointing at it were started against held samples, and turning it into a stream under them would change what a voice is, not what it sounds like. The clip it replaces is destroyed here, which is safe with voices still on it: the mixer counts a reference per track and frees at the last one, so a sound playing across the swap finishes on the samples it started with. Blocking, like every other reload: it is a debug operation, and answering before the file has been read would report a success that had not happened yet. ```teal function tecs.audio.Audio.reload(self, path: string): boolean, string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `path` | `string` | The path originally used to load the clip. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Whether the clip was re-read, and a reason when it was not. | | `string` | | #### tecs.audio.Audio:resume Instance Lets a paused voice carry on. ```teal function tecs.audio.Audio.resume(self, handle: integer) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `handle` | `integer` | A no-op for a handle that names nothing and for one that is not paused. Resumes a voice its group paused, and the group's hold does not put it back; a voice starting into that group afterwards is still held. | ##### Returns None. #### tecs.audio.Audio:resumeGroup Instance Lets a paused group carry on, and lets later joiners start sounding. ```teal function tecs.audio.Audio.resumeGroup(self, name: string) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `name` | `string` | | ##### Returns None. #### tecs.audio.Audio:seek Instance Moves a voice's read position, in seconds from the start of its clip. False when the handle names nothing, or when the input cannot seek: a the decoder does not support seeking, or can only reach a time rather than an exact sample. ```teal function tecs.audio.Audio.seek( self, handle: integer, seconds: number ): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `handle` | `integer` | | | `seconds` | `number` | From the start of the clip, not from where the voice is now, and not from the loop point. Converted through the track's own rate, so it lands on a sample frame rather than on a mixer tick. | ##### Returns | Type | Description | | --- | --- | | `boolean` | false when the handle names nothing or the input refused the seek. A true does not promise the exact sample: some decoders reach only the nearest point they can. | #### tecs.audio.Audio:setGain Instance Sets a voice's gain, before its group and the master. ```teal function tecs.audio.Audio.setGain(self, handle: integer, gain: number) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `handle` | `integer` | A no-op for one that names nothing. | | `gain` | `number` | Linear amplitude, not decibels: 0 is silence, 1 is the clip as recorded, above 1 is louder and may clip. Takes effect on the next buffer the mixer fills rather than ramping, so a large jump on a sounding voice can be audible as a step. | ##### Returns None. #### tecs.audio.Audio:setGroupGain Instance Scales every voice in a group, and every voice that joins it later. Composed here rather than through the mixer's per-tag gain, which writes each tagged track's own gain and would overwrite what the voice asked for. ```teal function tecs.audio.Audio.setGroupGain(self, name: string, gain: number) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `name` | `string` | Needs no declaring, and setting a gain on a name nothing is in is how a game configures a group before anything joins it. Recorded against the name, so it outlives every voice that was in it. | | `gain` | `number` | Linear amplitude, multiplied with each voice's own gain. There is no removing it: set 1 to return the group to unscaled. | ##### Returns None. #### tecs.audio.Audio:setGroupMuted Instance Silences a group without discarding the level it was set to. Bookkeeping over the gains that already exist rather than anything the mixer receives: every voice in the group contributes zero while this holds, and an unmute puts each one back at its own gain times `groupGain(name)`. ```teal function tecs.audio.Audio.setGroupMuted( self, name: string, muted: boolean ) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `name` | `string` | | | `muted` | `boolean` | | ##### Returns None. #### tecs.audio.Audio:setLimit Instance Caps how many voices a key may hold and how often it may start one. A key is not a group. A group says where a sound's gain comes from and what a pause reaches; a key says how many of one sound the mix will carry. The two are set independently on `play`, so "at most three footsteps at once, all of them in the effects group" is the ordinary case and neither name has to know about the other. Passing nil removes the limit. Voices already sounding are left alone. When a key reaches its limit, `play` **drops the new voice**: it returns zero and leaves every sounding voice alone. ```teal function tecs.audio.Audio.setLimit( self, key: string, limit: Audio.Limit ) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `key` | `string` | | | `limit` | [`Audio.Limit`](/modules/audio/#tecs.audio.Audio.Limit) | nil removes the limit. Lowering `voices` below what the key already holds does not stop any of them; it only refuses the next until enough have ended. The audio object measures `cooldown` from when a voice last started, against the time passed to `update`, so a game that never calls `update` never advances a cooldown. | ##### Returns None. #### tecs.audio.Audio:setLoop Instance Changes whether a voice repeats, part way through. The mixer replaces its starting repeat count, so clearing this on a looping piece of music lets it play out to its end rather than cutting it, and setting it on a one-shot keeps it going. It reaches a sounding voice only: a stopped one takes its count from the next play. ```teal function tecs.audio.Audio.setLoop(self, handle: integer, loop: boolean) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `handle` | `integer` | | | `loop` | `boolean` | true repeats forever; there is no finite repeat count here. Setting it does not move the loop point. The voice keeps the `loopStart` from `play`. | ##### Returns None. #### tecs.audio.Audio:setMasterGain Instance Scales everything. One number on the mixer, so this costs the same whether one voice is sounding or every voice is. Setting this while muted changes the level a later unmute returns to and nothing that is audible now, which is what a volume slider moved with the sound off should do. ```teal function tecs.audio.Audio.setMasterGain(self, gain: number) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `gain` | `number` | Linear amplitude, not decibels, on the same scale as a voice's: 0 silences, 1 leaves the mix as mixed, above 1 is louder and may clip. | ##### Returns None. #### tecs.audio.Audio:setMuted Instance Silences everything without discarding the master gain. The mixer's own number again, so it costs one call however many voices are sounding. It does not fan out to the groups: `groupMuted` answers "is this group silenced", and writing every group's bit here would overwrite the answers an unmute has to put back. That is the same loss as setting a gain to zero, which is what a mute exists instead of. ```teal function tecs.audio.Audio.setMuted(self, muted: boolean) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `muted` | `boolean` | | ##### Returns None. #### tecs.audio.Audio:setPitch Instance Sets a voice's playback rate. A value of 1 preserves the original rate. ```teal function tecs.audio.Audio.setPitch(self, handle: integer, ratio: number) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `handle` | `integer` | | | `ratio` | `number` | A resampling rate, not an interval: 2 is an octave up and half as long, 0.5 an octave down and twice as long. It changes how long the clip takes, so a looped voice's period moves with it. | ##### Returns None. #### tecs.audio.Audio:setPosition Instance Places a voice in space. See [`Sound`](/modules/audio/#tecs.audio.Sound) for what the numbers mean. Replaces a pan set by `setStereo` rather than combining with it. Puts the voice in the mixer's 3D mode, which folds its input down to mono before placing it, so a stereo clip loses its stereo image when positioned. ```teal function tecs.audio.Audio.setPosition( self, handle: integer, x: number, y: number, z: number ) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `handle` | `integer` | | | `x` | `number` | Positive to the right of the listener. The mixer fixes the listener at the origin. | | `y` | `number` | Positive **above** the listener. World Y runs down, so world coordinates need their Y negated on the way in. | | `z` | `number` | Positive behind the listener. | ##### Returns None. #### tecs.audio.Audio:setStereo Instance Pins a voice to the front pair of speakers at explicit gains. A pan rather than a position, and usually what a game laid out on a plane wants: there is no listener to subtract and no distance model to argue with, only "how much of this comes out of each side". `left` at 0.8 and `right` at 0.2 is a sound over to the left, whatever the speakers are. Negative reads as silence and above 1 is louder, on the same terms as a gain. Replaces a position set by `setPosition` rather than combining with it: the mixer holds one placement per track, and each call writes it. ```teal function tecs.audio.Audio.setStereo( self, handle: integer, left: number, right: number ) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `handle` | `integer` | | | `left` | `number` | Gain out of the left speaker, on the same linear scale as `setGain`. It multiplies with the voice, group and master gains rather than replacing any of them, so a pan of 1 and 1 is not silence. | | `right` | `number` | Gain out of the right speaker. There is no normalization between the two: 1 and 1 is the sound at full out of both sides, not half out of each. | ##### Returns None. #### tecs.audio.Audio:sounding Instance Returns the number of voices sounding now, including paused and fading voices. ```teal function tecs.audio.Audio.sounding(self): integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | ##### Returns | Type | Description | | --- | --- | | `integer` | Slots in use, so it drops only when `update` reaps a voice, not the instant one ends. `play` declines once it reaches `maxVoices`. | #### tecs.audio.Audio:stop Instance Stops a voice. A handle to one that already ended does nothing. `fadeOut` seconds keeps the voice sounding while it fades, so `playing` stays true until `update` sees the mixer finish it. ```teal function tecs.audio.Audio.stop( self, handle: integer, fadeOut: number ) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `handle` | `integer` | A stale one is a no-op, as is one already fading out: a second stop does not shorten a fade in progress. | | `fadeOut` | `number` | Seconds to ramp down over, a duration and not a moment to finish at. Omitted or zero stops at once and frees the slot within this call. The ramp is the mixer's, so it follows the audio clock rather than the frame rate. | ##### Returns None. #### tecs.audio.Audio:stopAll Instance Stops everything, over `fadeOut` seconds if that is given. A faded stop leaves each voice sounding until the mixer finishes it, so `update` reaps them instead of this call. The mixer owns the ramp, on the same terms as `stop` and `stopGroup`. Reaches every voice, whichever group it is in and whether a [`Sound`](/modules/audio/#tecs.audio.Sound) component started it or `play` did. A row still asking to sound starts a fresh voice on the next audio pass, so this silences a world rather than keeping it silent. ```teal function tecs.audio.Audio.stopAll(self, fadeOut: number) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `fadeOut` | `number` | Seconds, a duration. Omitted or zero frees every slot within this call. A positive one unpauses the voices it fades, because a paused voice does not advance and its fade would never finish. | ##### Returns None. #### tecs.audio.Audio:stopGroup Instance Ends every voice in a group, over `fadeOut` seconds if that is given. A faded stop leaves the voices sounding until they finish, so they are reaped by `update` rather than here. Stops the voices, not the group: a gain, mute or pause set on the name survives, and anything that joins afterwards starts under them. ```teal function tecs.audio.Audio.stopGroup(self, name: string, fadeOut: number) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `name` | `string` | | | `fadeOut` | `number` | Seconds, a duration. Omitted or zero frees the slots within this call. A positive one unpauses the voices it fades, since a paused voice's fade would never finish. | ##### Returns None. #### tecs.audio.Audio:tell Instance Returns the voice position in seconds, or nil. Nil when the handle names nothing or the mixer cannot say. A paused voice reports where it stopped, which with `seek` and `fadeIn` is what taking a sound back where it left off is built from. ```teal function tecs.audio.Audio.tell(self, handle: integer): number ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `handle` | `integer` | | ##### Returns | Type | Description | | --- | --- | | `number` | Seconds from the start of the clip, so a looped voice's reading falls back at each repeat rather than accumulating. nil when the handle names nothing and nil when the mixer will not say, which are not told apart here. | #### tecs.audio.Audio:update Instance Takes finished loads and reaps the voices the mixer has finished with. Call once per frame, with the frame's step. The step advances cooldowns. Nothing else here needs time: a fade is the mixer's to run, and a voice is over when the mixer says so rather than when a clock here says it should be. Not a world system, and `install` deliberately does not add one: reaping has to continue during a world pause, so an application drives this from the iteration instead. ```teal function tecs.audio.Audio.update(self, dt: number): integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `dt` | `number` | Seconds since the last call. The audio pass adds it to the clock that governs cooldowns. measured against. Omitting it advances nothing, which is what a test stepping voices without time wants. | ##### Returns | Type | Description | | --- | --- | | `integer` | Voices still sounding after the reap, the same number `sounding` answers. | #### tecs.audio.Audio:voices Instance Returns every sounding voice, including paused and fading voices. For introspection, on the same terms as `clips`. The handles it reports are the ones `playing` and `stop` take, so callers can act on these results. ```teal function tecs.audio.Audio.voices(self): {Audio.VoiceInfo} ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | ##### Returns | Type | Description | | --- | --- | | `{`[`Audio.VoiceInfo`](/modules/audio/#tecs.audio.Audio.VoiceInfo)`}` | A fresh list of fresh records each call, ordered by voice slot, which is not the order the voices started in. It is a snapshot: nothing in it follows the voice afterwards. | ### tecs.audio.Clip type A loaded clip, or one still loading. ```teal type tecs.audio.Clip = Audio.Clip ``` ### tecs.audio.Config type Configures `newAudio`. ```teal type tecs.audio.Config = Audio.Config ``` ### tecs.audio.Device record Describes a physical device before the game opens it. ```teal record tecs.audio.Device id: number name: string frequency: integer channels: integer end ``` #### tecs.audio.Device.id field Read-only. Reports the platform's numeric device identifier. ```teal tecs.audio.Device.id: number ``` #### tecs.audio.Device.name field Read-only. Reports the platform's display name for the device. ```teal tecs.audio.Device.name: string ``` #### tecs.audio.Device.frequency field Read-only. Reports the device's preferred samples per second. ```teal tecs.audio.Device.frequency: integer ``` #### tecs.audio.Device.channels field Read-only. Reports the device's preferred channels per frame. ```teal tecs.audio.Device.channels: integer ``` ### tecs.audio.Limit type Defines how many voices a key allows and how soon it may repeat. ```teal type tecs.audio.Limit = Audio.Limit ``` ### tecs.audio.LoadOptions type Configures `Audio:load`. ```teal type tecs.audio.LoadOptions = Audio.LoadOptions ``` ### tecs.audio.Microphone record An open recording device, read by polling. ```teal record tecs.audio.Microphone frequency: integer channels: integer availableFrames: function(self): integer destroy: function(self) pause: function(self): boolean, string read: function(self, maxFrames: integer): string, string resume: function(self): boolean, string end ``` #### tecs.audio.Microphone.frequency field Read-only. Reports samples per second after SDL converts from whatever the device actually runs at. This is the requested value, not the device's own. ```teal tecs.audio.Microphone.frequency: integer ``` #### tecs.audio.Microphone.channels field Read-only. Reports interleaved channels per frame after conversion. ```teal tecs.audio.Microphone.channels: integer ``` #### tecs.audio.Microphone:availableFrames Instance Complete sample frames ready to read without blocking. ```teal function tecs.audio.Microphone.availableFrames(self): integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Microphone` | | ##### Returns | Type | Description | | --- | --- | | `integer` | Whole frames only, so a partly arrived frame is not counted and reads as zero until the rest of it lands. Zero once destroyed, rather than an error. | #### tecs.audio.Microphone:destroy Instance Stops capture and closes the recording device. Safe more than once. Whatever was captured and not yet read is discarded with the stream, so a last `read` belongs before this rather than after. ```teal function tecs.audio.Microphone.destroy(self) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Microphone` | | ##### Returns None. #### tecs.audio.Microphone:pause Instance Stops the device filling the stream, keeping whatever is already in it. ```teal function tecs.audio.Microphone.pause(self): boolean, string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Microphone` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | False with a reason on failure, including for a microphone already destroyed. Pausing one already paused succeeds. | | `string` | The reason, when the first return is false. | #### tecs.audio.Microphone:read Instance Pulls up to `maxFrames` complete sample frames. ```teal function tecs.audio.Microphone.read( self, maxFrames: integer ): string, string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Microphone` | | | `maxFrames` | `integer` | Omit to take everything ready now. Zero is allowed and takes nothing. A limit above what is ready takes what is ready rather than waiting for the rest. | ##### Returns | Type | Description | | --- | --- | | `string` | Interleaved native-endian float32 samples, as bytes in a string, `channels * 4` per frame. An empty string means nothing was ready and is not a failure. Nil on one, with the reason beside it: a destroyed microphone, a `maxFrames` that is not a non-negative integer, or SDL's own error. | | `string` | The reason, when the first return is nil. | #### tecs.audio.Microphone:resume Instance Starts the device filling the stream again after `pause`. ```teal function tecs.audio.Microphone.resume(self): boolean, string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Microphone` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | False with a reason on failure, including for a microphone already destroyed. | | `string` | The reason, when the first return is false. | ### tecs.audio.MicrophoneConfig record Configures `openMicrophone`. ```teal record tecs.audio.MicrophoneConfig device: number frequency: integer channels: integer end ``` #### tecs.audio.MicrophoneConfig.device field Caller-writable. Selects a physical device id from `recordingDevices`. Omitted uses the current system default. ```teal tecs.audio.MicrophoneConfig.device: number ``` #### tecs.audio.MicrophoneConfig.frequency field Caller-writable. Sets samples per second after SDL converts the device input. Defaults to 48000. ```teal tecs.audio.MicrophoneConfig.frequency: integer ``` #### tecs.audio.MicrophoneConfig.channels field Caller-writable. Sets interleaved channels. Defaults to one. ```teal tecs.audio.MicrophoneConfig.channels: integer ``` ### tecs.audio.PlayOptions type Configures `Audio:play`. ```teal type tecs.audio.PlayOptions = Audio.PlayOptions ``` ### tecs.audio.Sound record Represents a sound attached to an entity. Presence is the instruction: an entity carrying this with a loaded clip starts sounding on the next audio pass, and stops when the component or the entity goes away. That is what makes sound an entity rather than a handle a game has to remember to release, and it is why despawning something mid-sound does the obvious thing. The audio pass sends position to the mixer and does nothing else with it. `spatial` switches it on, and the mixer reads `x`, `y` and `z` in a right-handed system. The mixer fixes its listener at the origin, with x positive to the right, y positive up and z positive behind. So a caller with a camera and a world has three jobs this component does not do for it, and doing any of them here would fix answers that belong to a game: 1. Subtract the listener. There is no listener component and no rule that the camera is one, so whatever a game decides is listening is what it subtracts before writing these fields. 2. Choose a scale. The mixer attenuates with distance on its own, and how loud a sound a hundred world units away should be is a question about a game's units, not about audio. 3. Decide what a screen-space sound means. A sound on a layer that does not move with the camera has no world position to convert, and the answer is to leave `spatial` at zero. A name identifies a group, just as a path identifies a clip. An FFI component holds numbers, so what `group` carries is an interned index from `Audio.groupId` rather than the mixer's tag itself, and `Audio.groupName` reads it back. The index is this run's and no other, which is why `serialize` writes the name: an integer in a save file would name a different group the next time a build interned its names in a different order. The audio pass follows `playing`, `gain`, `loop`, `pitch` and the position for as long as the voice sounds, so writing any of them is enough and nothing has to restart the voice to apply a change. Each of those costs one read and compare per sounding row per frame, which is the price of the component being live and is why `clip` and `group` are not on the list: both take an untag, a retag and a fresh input to change, neither is something a game writes often, and following them would put that cost on every row that never does. Moving a sound into another group is setting `group` and clearing `voice`, which is the same thing changing its clip takes. Disabling an entity silences its sound and re-enabling starts it again. Queries exclude [`Disabled`](/modules/ecs/#tecs.ecs.Disabled) unless they name it, so a disabled row is not followed and its voice is taken back; the audio pass clears the handle at the same moment, which is what lets the sound return with the entity rather than reading as a one-shot that had finished. Read-only. Exposes the `Sound` component that plays a clip from an entity. ```teal record tecs.audio.Sound is Component clip: number playing: number gain: number loop: number pitch: number spatial: number x: number y: number z: number group: number voice: number end ``` #### Interfaces | Interface | | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | #### tecs.audio.Sound.clip field Caller-writable. Selects a clip by its `Audio.clipId` index. Zero plays nothing. ```teal tecs.audio.Sound.clip: number ``` #### tecs.audio.Sound.playing field Caller-writable. Uses nonzero to request playback and zero to stop. This field gives an instruction rather than a report: clearing it stops the voice and setting it again starts one, including on a one-shot that has already run out. ```teal tecs.audio.Sound.playing: number ``` #### tecs.audio.Sound.gain field Caller-writable. Sets linear gain from zero to one before group and master gain. ```teal tecs.audio.Sound.gain: number ``` #### tecs.audio.Sound.loop field Caller-writable. Uses nonzero to repeat. Clearing it part way through lets the voice play out to its end rather than cutting it. ```teal tecs.audio.Sound.loop: number ``` #### tecs.audio.Sound.pitch field Caller-writable. Sets playback rate. One leaves it unchanged, while two plays an octave higher in half the time. ```teal tecs.audio.Sound.pitch: number ``` #### tecs.audio.Sound.spatial field Caller-writable. Uses nonzero to read `x`, `y`, and `z`; zero mixes without a position. ```teal tecs.audio.Sound.spatial: number ``` #### tecs.audio.Sound.x field Caller-writable. Sets the position right of the listener. ```teal tecs.audio.Sound.x: number ``` #### tecs.audio.Sound.y field Caller-writable. Sets the position above the listener. ```teal tecs.audio.Sound.y: number ``` #### tecs.audio.Sound.z field Caller-writable. Sets the position behind the listener. ```teal tecs.audio.Sound.z: number ``` #### tecs.audio.Sound.group field Caller-writable. Selects a group by its `Audio.groupId` index. Zero joins no group. ```teal tecs.audio.Sound.group: number ``` #### tecs.audio.Sound.voice field Engine-owned. Reports the voice assigned by the audio pass: zero before it starts and negative once a one-shot has finished. Written by the engine; setting it back to zero is how a game asks for the sound again. ```teal tecs.audio.Sound.voice: number ``` ### tecs.audio.VoiceInfo type Describes one voice returned by `Audio:voices`. ```teal type tecs.audio.VoiceInfo = Audio.VoiceInfo ``` ## Functions ### tecs.audio.openMicrophone Static Opens a microphone as interleaved native-endian 32-bit float samples. The API installs no callback. The audio thread fills its own stream and the game pulls completed bytes from the main thread with `read`. ```teal function tecs.audio.openMicrophone( config: platformaudio.MicrophoneConfig ): platformaudio.Microphone, string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `config` | [`platformaudio.MicrophoneConfig`](/modules/audio/#tecs.audio.MicrophoneConfig) | Omitted opens the default recording device at its own frequency and channel count. | #### Returns | Type | Description | | --- | --- | | [`platformaudio.Microphone`](/modules/audio/#tecs.audio.Microphone) | The open microphone, whose closing is the caller's through `destroy`. Nil on failure, and nothing is left open in that case. | | `string` | The reason, when the first return is nil. | ### tecs.audio.playbackDevices Static Returns the physical playback devices attached now. ```teal function tecs.audio.playbackDevices(): {platformaudio.Device}, string ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | `{`[`platformaudio.Device`](/modules/audio/#tecs.audio.Device)`}` | The devices, listed afresh each call and the caller's to keep. Empty rather than nil when the subsystem cannot start. | | `string` | SDL's reason, when something went wrong. | ### tecs.audio.recordingDevices Static Returns the physical recording devices attached now. ```teal function tecs.audio.recordingDevices(): {platformaudio.Device}, string ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | `{`[`platformaudio.Device`](/modules/audio/#tecs.audio.Device)`}` | The devices, listed afresh each call and the caller's to keep. Empty rather than nil when the subsystem cannot start. | | `string` | SDL's reason, when something went wrong. | --- ## tecs.data # tecs.data Typed stores, JSON, UTF-8, UUIDs, hashes, and checksums. ## JSON Encode and decode complete documents: ```teal local text = tecs.data.encodeJSON({ jsonrpc = "2.0", id = 1, result = {entities = {12, 24, 36}}, }) local value = tecs.data.decodeJSON(text) ``` Both functions raise on invalid input. Protect untrusted documents with `pcall`. Lua needs sentinels for two JSON values: ```teal local value = tecs.data.decodeJSON('{"name":null,"rows":[]}') assert(value.name == tecs.data.null) local rows = setmetatable({}, tecs.data.array_mt) return tecs.data.encodeJSON({rows = rows}) ``` `null` preserves a present key whose JSON value equals null. An empty Lua table normally encodes as an object; `empty_array` and `array_mt` express an empty list. `newJSON` creates an independent configuration for protocols that must not share process-wide settings. The sentinel and option names retain lua-cjson spelling because they belong to that library's compatibility surface. ## UUIDs Generate RFC 9562 identifiers without a central allocator: ```teal local objectId = tecs.data.uuid4() local eventId = tecs.data.uuid7() ``` `uuid4` draws its random bits from the operating system. `uuid7` starts with the current Unix timestamp in milliseconds and orders calls made by this process, which makes it suitable for identifiers stored in an ordered index. Both return the canonical lowercase 8-4-4-4-12 representation. UUIDs identify values; they do not authenticate a player or make an unguessable credential. ## UTF-8 Codepoint operations live under `tecs.data.utf8`: ```teal local utf8 = tecs.data.utf8 local codepoint, nextOffset = utf8.decodeAt("A€", 2) assert(codepoint == 0x20ac) assert(nextOffset == 5) ``` The child works over explicit byte lengths in strings and ByteViews, so embedded NUL is U+0000 rather than a terminator. Malformed bytes decode as U+FFFD and remain traversable. These are codepoint operations, not grapheme segmentation. ## Hashes and checksums `fnv1a64` supplies fast process and build identity as 16 lowercase hexadecimal digits. `sha256` supplies a cryptographic digest as 64 lowercase hexadecimal digits. `adler32` and `crc32` implement checksums required by formats that name them. Pass the checksum from one call as the second argument to continue over the next chunk without joining the chunks into one string: ```teal local checksum = tecs.data.crc32(header) checksum = tecs.data.crc32(body, checksum) ``` SHA-256 authenticates nothing by itself. Verify downloaded artifacts against a digest obtained through a trusted channel or a signed manifest. ## Typed stores Create typed keys once and use them with independent stores: ```teal local SPEED : tecs.data.Key = tecs.data.Store.newKey( "game.speed" ) local store = tecs.data.newStore() store[SPEED] = 120 ``` `world.resources` is a `Store`. Stores are in-memory and independent; a named key is the process-wide identity that lets hot reload and tooling find the same value again. `Store.listKeys` reports the named keys registered by the process. ## Module contents ### Submodules | Submodule | Description | | --- | --- | | [`tecs.data.utf8`](/modules/data/utf8/) | UTF-8 codepoint decoding, encoding, validation, and truncation | ### Constructors | Constructor | Description | | --- | --- | | [`newStore`](/modules/data/#tecs.data.newStore) | Creates an independent typed store. | ### Types | Type | Kind | Description | | --- | --- | --- | | [`ByteInput`](/modules/data/#tecs.data.ByteInput) | type | ByteInput accepts a Lua byte string or an open Buffer or ByteView. | | [`Key`](/modules/data/#tecs.data.Key) | interface | Key identifies one typed store value across every store in the process. | | [`Store`](/modules/data/#tecs.data.Store) | record | Store holds typed values under process-wide keys and exposes their process-wide registry. | ### Functions | Function | Kind | Description | | --- | --- | --- | | [`adler32`](/modules/data/#tecs.data.adler32) | Static | Computes Adler-32 over the bytes of text as a number in [0, 2^32). | | [`crc32`](/modules/data/#tecs.data.crc32) | Static | Computes CRC-32 over the bytes of text as a number in [0, 2^32). | | [`fnv1a64`](/modules/data/#tecs.data.fnv1a64) | Static | FNV-1a over the bytes of text, 64-bit, as sixteen lowercase hex digits. | | [`sha256`](/modules/data/#tecs.data.sha256) | Static | Computes SHA-256 over the bytes of text. | | [`uuid4`](/modules/data/#tecs.data.uuid4) | Static | Generates a random RFC 9562 UUID version 4. | | [`uuid7`](/modules/data/#tecs.data.uuid7) | Static | Generates a time-ordered RFC 9562 UUID version 7. | ## Constructors ### tecs.data.newStore Static Creates an independent typed store. ```teal function tecs.data.newStore(): Store ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | [`Store`](/modules/data/#tecs.data.Store) | Returns an empty store. You own it and its values. | ## Types ### tecs.data.ByteInput type `ByteInput` accepts a Lua byte string or an open Buffer or ByteView. ```teal type tecs.data.ByteInput = string | ByteView ``` ### tecs.data.Key interface `Key` identifies one typed store value across every store in the process. ```teal interface tecs.data.Key end ``` #### Type Parameters | Name | Constraint | Description | | --- | --- | --- | | `T` | | | ### tecs.data.Store record `Store` holds typed values under process-wide keys and exposes their process-wide registry. ```teal record tecs.data.Store findKey: function(name: string): Key | nil listKeys: function(): {string: integer} newKey: function(name: string, forType: T): Key metamethod __index: function(self, key: Key): T metamethod __newindex: function(self, key: Key, value: T) end ``` #### tecs.data.Store.findKey Static Returns a named key created by `newKey`. ```teal function tecs.data.Store.findKey(name: string): Key | nil ``` ##### Type Parameters | Name | Constraint | Description | | --- | --- | --- | | `T` | | | ##### Arguments | Name | Type | Description | | --- | --- | --- | | `name` | `string` | The name used during key creation. | ##### Returns | Type | Description | | --- | --- | | [`Key`](/modules/data/#tecs.data.Key)<T> | nil | Returns the existing key, or nil before any module creates it. | #### tecs.data.Store.listKeys Static Returns all named keys as name-to-id entries. ```teal function tecs.data.Store.listKeys(): {string: integer} ``` ##### Arguments None. ##### Returns | Type | Description | | --- | --- | | `{string : integer}` | Returns a fresh process-wide table. | #### tecs.data.Store.newKey Static Creates a typed key. Always pass a name. Named keys are discoverable through `findKey` and `listKeys`, and repeating a name returns the same key so values remain reachable when a module reloads. ```teal function tecs.data.Store.newKey(name: string, forType: T): Key ``` ##### Type Parameters | Name | Constraint | Description | | --- | --- | --- | | `T` | | | ##### Arguments | Name | Type | Description | | --- | --- | --- | | `name` | `string` | The namespaced identity, such as `"game.state"`. Omitting it creates an anonymous key and warns. | | `forType` | `T` | Pass `nil as T` when Teal needs the value type carried explicitly. The function does not read it. | ##### Returns | Type | Description | | --- | --- | | [`Key`](/modules/data/#tecs.data.Key)`` | Returns the stable key for a name or a fresh anonymous key. | #### tecs.data.Store:__index metamethod Returns the value associated with a typed key. An unwritten key returns nil. ```teal local SPEED : tecs.data.Key = tecs.data.Store.newKey( "game.speed" ) local store = tecs.data.newStore() local speed = store[SPEED] ``` ```teal metamethod tecs.data.Store.$meta.__index(self, key: Key): T ``` ##### Type Parameters | Name | Constraint | Description | | --- | --- | --- | | `T` | | | ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | [`Store`](/modules/data/#tecs.data.Store) | The Store to index. | | `key` | [`Key`](/modules/data/#tecs.data.Key)`` | The Key used to retrieve its value. | ##### Returns | Type | Description | | --- | --- | | `T` | Returns the value for `key`, or nil when no value has been assigned. | #### tecs.data.Store:__newindex metamethod Associates a value with a typed key. Assigning nil removes the value. ```teal local SPEED : tecs.data.Key = tecs.data.Store.newKey( "game.speed" ) local store = tecs.data.newStore() store[SPEED] = 120 ``` ```teal metamethod tecs.data.Store.$meta.__newindex( self, key: Key, value: T ) ``` ##### Type Parameters | Name | Constraint | Description | | --- | --- | --- | | `T` | | | ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | [`Store`](/modules/data/#tecs.data.Store) | The Store to index. | | `key` | [`Key`](/modules/data/#tecs.data.Key)`` | The Key used to associate the value. | | `value` | `T` | The value to associate with `key`. Passing nil removes the association. | ##### Returns None. ## Functions ### tecs.data.adler32 Static Computes Adler-32 over the bytes of `text` as a number in [0, 2^32). The checksum RFC 1950 puts in a zlib stream's trailer, which is the only reason it is here. It is a poor content hash and is not offered as one: it is a sum of a sum, it barely mixes on short inputs, and two files that differ by reordering equal-length runs collide outright. Use `fnv1a64` for identity and this only for the format that specifies it. zlib computes it. The seed is 1, which is what RFC 1950 starts from and what zlib's own documentation says to pass for the first call. Pass the return value back as `previous` to continue the checksum over another chunk. An empty chunk leaves `previous` unchanged. ```teal function tecs.data.adler32( text: ByteInput, previous: integer ): integer ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `text` | [`ByteInput`](/modules/data/#tecs.data.ByteInput) | A byte string or open [`ByteView`](/modules/io/#tecs.io.ByteView). NUL and bytes above 127 contribute like any other. | | `previous` | `integer` | The checksum returned after all preceding bytes. Omit it for the first chunk. Raises unless it is an unsigned 32-bit integer. | #### Returns | Type | Description | | --- | --- | | `integer` | The checksum after `text`, as an exact Lua number in [0, 2^32). | ### tecs.data.crc32 Static Computes CRC-32 over the bytes of `text` as a number in [0, 2^32). The checksum PNG, gzip and ZIP put beside compressed bytes. It detects accidental corruption and is not a cryptographic digest; use it only where a format specifies CRC-32 or where an error-detecting checksum is enough. Pass the return value back as `previous` to continue the checksum over another chunk. An empty chunk leaves `previous` unchanged. ```teal function tecs.data.crc32( text: ByteInput, previous: integer ): integer ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `text` | [`ByteInput`](/modules/data/#tecs.data.ByteInput) | A byte string or open [`ByteView`](/modules/io/#tecs.io.ByteView). NUL and bytes above 127 contribute like any other. | | `previous` | `integer` | The checksum returned after all preceding bytes. Omit it for the first chunk. Raises unless it is an unsigned 32-bit integer. | #### Returns | Type | Description | | --- | --- | | `integer` | The checksum after `text`, as an exact Lua number in [0, 2^32). | ### tecs.data.fnv1a64 Static FNV-1a over the bytes of `text`, 64-bit, as sixteen lowercase hex digits. ```teal function tecs.data.fnv1a64(text: ByteInput): string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `text` | [`ByteInput`](/modules/data/#tecs.data.ByteInput) | A byte string or open [`ByteView`](/modules/io/#tecs.io.ByteView). NUL and bytes above 127 hash like any other. | #### Returns | Type | Description | | --- | --- | | `string` | Sixteen hex digits, high half first, so hashes sort as their values do and two of them compare as strings. | ### tecs.data.sha256 Static Computes SHA-256 over the bytes of `text`. SHA-256 detects whether bytes match a trusted digest. It does not prove who supplied either value, so artifact verification obtains the expected digest through a trusted channel or a signed manifest. ```teal function tecs.data.sha256(text: ByteInput): string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `text` | [`ByteInput`](/modules/data/#tecs.data.ByteInput) | A byte string or open [`ByteView`](/modules/io/#tecs.io.ByteView). NUL and bytes above 127 hash like any other. | #### Returns | Type | Description | | --- | --- | | `string` | Sixty-four lowercase hexadecimal digits, high byte first. | ### tecs.data.uuid4 Static Generates a random RFC 9562 UUID version 4. ```teal function tecs.data.uuid4(): string ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | `string` | Returns a fresh UUID in canonical lowercase 8-4-4-4-12 form. | ### tecs.data.uuid7 Static Generates a time-ordered RFC 9562 UUID version 7. Calls made by this process remain ordered even when more than one falls in the same millisecond. The timestamp makes the result unsuitable for hiding when an identifier was created. ```teal function tecs.data.uuid7(): string ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | `string` | Returns a fresh UUID in canonical lowercase 8-4-4-4-12 form. | --- ## tecs.data.utf8 # tecs.data.utf8 UTF-8 codepoint decoding, encoding, validation, and byte-safe truncation. Offsets are one-based byte cursor positions, matching Lua string indices. `decodeAt` reads the codepoint beginning at an offset and returns the cursor after it. `decodeBefore` reads the codepoint before a cursor and returns its starting offset: ```teal local utf8 = tecs.data.utf8 local text = "A€" local codepoint, nextOffset = utf8.decodeAt(text, 2) assert(codepoint == 0x20ac) assert(nextOffset == #text + 1) codepoint, nextOffset = utf8.decodeBefore(text, nextOffset) assert(codepoint == 0x20ac) assert(nextOffset == 2) ``` The end cursor is the byte length plus one. `decodeAt` returns nil there, and `decodeBefore` returns nil at cursor 1. An offset inside a multibyte sequence is not moved to a boundary automatically. SDL reports the malformed fragment at that exact position as U+FFFD. Every inspection function accepts a Lua string or an open [`ByteView`](/modules/io/#tecs.io.ByteView). A view is read through its retained pointer without first becoming a Lua string. `validPrefixLength` answers the byte count a caller can use to retain a zero-copy view at a UTF-8 boundary. Malformed UTF-8 is recoverable rather than exceptional. Forward decoding returns U+FFFD and consumes one malformed byte, so callers can keep scanning. A valid encoded U+FFFD remains indistinguishable by value but consumes its three bytes. Embedded NUL is the valid U+0000 codepoint and does not terminate a Lua string. These functions operate on Unicode codepoints, not user-perceived characters. A combining mark, variation selector, or member of an emoji sequence is a separate result. Text editing that needs cursor movement by grapheme cluster requires a Unicode segmentation implementation above this module. ## Module contents ### Functions | Function | Kind | Description | | --- | --- | --- | | [`decodeAt`](/modules/data/utf8/#tecs.data.utf8.decodeAt) | Static | Decodes the codepoint beginning at a byte offset. | | [`decodeBefore`](/modules/data/utf8/#tecs.data.utf8.decodeBefore) | Static | Decodes the codepoint before a byte cursor. | | [`encode`](/modules/data/utf8/#tecs.data.utf8.encode) | Static | Encodes one Unicode scalar value as UTF-8. | | [`isValid`](/modules/data/utf8/#tecs.data.utf8.isValid) | Static | Reports whether every byte forms canonical UTF-8. | | [`length`](/modules/data/utf8/#tecs.data.utf8.length) | Static | Counts Unicode codepoints in bytes. | | [`truncate`](/modules/data/utf8/#tecs.data.utf8.truncate) | Static | Truncates a string without splitting a valid UTF-8 sequence. | | [`validPrefixLength`](/modules/data/utf8/#tecs.data.utf8.validPrefixLength) | Static | Returns the longest prefix ending at a complete UTF-8 decoding unit. | ## Functions ### tecs.data.utf8.decodeAt Static Decodes the codepoint beginning at a byte offset. The function returns U+FFFD and advances one byte when the offset begins malformed or truncated UTF-8. It does not search for a nearby codepoint boundary when the caller supplies an offset inside a multibyte sequence. ```teal function tecs.data.utf8.decodeAt( bytes: ByteInput, byteOffset: integer ): integer | nil, integer ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `bytes` | `ByteInput` | The caller supplies a complete byte string or open [`ByteView`](/modules/io/#tecs.io.ByteView). Embedded NUL is U+0000. | | `byteOffset` | `integer` | A one-based byte cursor from 1 through the byte length plus one. The final value is the end cursor. Other values raise. | #### Returns | Type | Description | | --- | --- | | integer | nil | The Unicode codepoint, or nil at the end cursor. | | `integer` | The one-based cursor immediately after the result, or the unchanged end cursor when no codepoint remains. | #### Examples Reads forward from a one-based byte offset. ```teal local utf8 = require("tecs.data.utf8") local codepoint, nextOffset = utf8.decodeAt("A€", 2) assert(codepoint == 0x20ac) assert(nextOffset == 5) ``` ### tecs.data.utf8.decodeBefore Static Decodes the codepoint before a byte cursor. The decoder searches backward for a possible sequence leader. When that candidate does not form one scalar ending at the cursor, it reports the final byte as U+FFFD instead. Forward and reverse scans therefore recover from malformed input one byte at a time. ```teal function tecs.data.utf8.decodeBefore( bytes: ByteInput, byteOffset: integer ): integer | nil, integer ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `bytes` | `ByteInput` | The caller supplies a complete byte string or open [`ByteView`](/modules/io/#tecs.io.ByteView). Embedded NUL is U+0000. | | `byteOffset` | `integer` | A one-based byte cursor from 1 through the byte length plus one. It points after the codepoint to read. Other values raise. | #### Returns | Type | Description | | --- | --- | | integer | nil | The preceding Unicode codepoint, or nil at cursor 1. | | `integer` | The one-based starting offset of the result, or 1 when no codepoint precedes the cursor. | #### Examples Reads backward from the end cursor. ```teal local utf8 = require("tecs.data.utf8") local codepoint, startOffset = utf8.decodeBefore("A€", 5) assert(codepoint == 0x20ac) assert(startOffset == 2) ``` ### tecs.data.utf8.encode Static Encodes one Unicode scalar value as UTF-8. ```teal function tecs.data.utf8.encode(codepoint: integer): string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `codepoint` | `integer` | An integer from U+0000 through U+10FFFF, excluding the UTF-16 surrogate range U+D800 through U+DFFF. Other values raise. | #### Returns | Type | Description | | --- | --- | | `string` | One through four bytes. U+0000 returns a one-byte Lua string containing NUL. | #### Examples Encodes a Unicode scalar value. ```teal local utf8 = require("tecs.data.utf8") assert(utf8.encode(0x20ac) == "€") ``` ### tecs.data.utf8.isValid Static Reports whether every byte forms canonical UTF-8. RFC 3629 rules reject overlong encodings, UTF-16 surrogate values, codepoints above U+10FFFF, stray continuation bytes, and truncated sequences. Embedded NUL and an encoded U+FFFD are valid. ```teal function tecs.data.utf8.isValid(bytes: ByteInput): boolean ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `bytes` | `ByteInput` | The caller supplies a complete byte string or open [`ByteView`](/modules/io/#tecs.io.ByteView), including embedded NUL. | #### Returns | Type | Description | | --- | --- | | `boolean` | True only when forward decoding never needs a replacement for a malformed byte. | #### Examples Rejects malformed UTF-8 bytes. ```teal local utf8 = require("tecs.data.utf8") assert(utf8.isValid("café")) assert(not utf8.isValid("\xff")) ``` ### tecs.data.utf8.length Static Counts Unicode codepoints in bytes. This is not a byte count or a grapheme count. Each malformed byte contributes one U+FFFD result, while an embedded NUL contributes the valid U+0000 codepoint and does not end the input. ```teal function tecs.data.utf8.length(bytes: ByteInput): integer ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `bytes` | `ByteInput` | The caller supplies a complete byte string or open [`ByteView`](/modules/io/#tecs.io.ByteView), including embedded NUL. | #### Returns | Type | Description | | --- | --- | | `integer` | The number of forward decoding steps. | #### Examples Counts Unicode codepoints rather than bytes. ```teal local utf8 = require("tecs.data.utf8") assert(utf8.length("A€") == 2) ``` ### tecs.data.utf8.truncate Static Truncates a string without splitting a valid UTF-8 sequence. The result is a byte prefix, not a normalization. Malformed bytes are one-byte decoding units and remain unchanged when they fit. The function does not combine marks or emoji into grapheme clusters. ```teal function tecs.data.utf8.truncate( text: string, maxBytes: integer ): string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `text` | `string` | The complete byte string. | | `maxBytes` | `integer` | A non-negative integer byte ceiling. Other values raise. | #### Returns | Type | Description | | --- | --- | | `string` | The longest prefix no larger than `maxBytes` whose final decoding unit is complete. Returns `text` itself when it fits. | #### Examples Leaves a multibyte codepoint intact at the byte limit. ```teal local utf8 = require("tecs.data.utf8") assert(utf8.truncate("A€B", 4) == "A€") ``` ### tecs.data.utf8.validPrefixLength Static Returns the longest prefix ending at a complete UTF-8 decoding unit. The function returns a byte count and never copies the input. A caller can pass the result to `ByteView:view` to retain the prefix. Malformed bytes remain one-byte units and therefore fit whenever their byte does. ```teal function tecs.data.utf8.validPrefixLength( bytes: ByteInput, maxBytes: integer ): integer ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `bytes` | `ByteInput` | The caller supplies a complete byte string or open [`ByteView`](/modules/io/#tecs.io.ByteView). | | `maxBytes` | `integer` | A non-negative integer byte ceiling. Other values raise. | #### Returns | Type | Description | | --- | --- | | `integer` | Returns the largest safe prefix length no greater than `maxBytes`. | #### Examples Retains a zero-copy prefix at a complete codepoint boundary. ```teal local tecs = require("tecs") local bytes = tecs.io.newBuffer("A€B") local all = bytes:view() local count = tecs.data.utf8.validPrefixLength(all, 3) local prefix = all:view(0, count) assert(prefix:getString() == "A") prefix:close() all:close() bytes:close() ``` --- ## tecs.debug # tecs.debug Debugger commands, and the tools they project to. A command is declared once. Its argument schema types both a typed command line and a JSON tool call, its action returns one structured result, and the debug server advertises it with an input schema, an output schema and safety hints that were all derived from that single declaration. Nothing about a command is written twice, so the two surfaces cannot disagree. ## Getting the commands `tecs.io.mcp` does not install them. An application with `mcpPort` set calls `ensure` for its own world, so every session that has a debug server has the whole command surface. A headless tool calls `ensure` itself. ## Adding one ```teal local debugapi = require("tecs.debug") debugapi.ensure(world) debugapi.register( world, { name = "wave", section = "Custom", shortHelp = "report the wave the game is on", agentHelp = "Reports the current wave number and how many enemies are left in it. " .. "Call it to check progress before spawning or despawning anything.", readOnly = true, outputSchema = { ["type"] = "object", properties = { wave = {["type"] = "integer"}, remaining = {["type"] = "integer"}, }, required = {"wave", "remaining"}, }, run = function(_values: {string: any}): debugapi.Result return { message = "wave " .. tostring(state.wave), data = { wave = state.wave, remaining = state.remaining }, } end, } ) ``` That registers the tool `wave`. A command with `subcommands` registers one tool per verb, named `_`, and registers `` as well when it carries a `run` of its own. ## Results and failures `Result.data` is the tool call's structured content, and `Result.message` travels beside it under the key `message`. A result with `ok = false` raises its `code` and `message` through the tool call, so an agent reads a failure the way it reads any other tool error rather than by inspecting a field. Declare `outputSchema` on anything a game ships. It is what lets an agent know the shape of an answer before it makes the call, and it is what the reference renders. ## Command names are a compatibility surface A tool name is generated from a command name and a verb name, so renaming either renames a tool that an agent already calls. Choose both once. Requiring this module as `debug` shadows Lua's own `debug` library. A module that needs both binds this one as `debugapi`. ## Module contents ### Types | Type | Kind | Description | | --- | --- | --- | | [`ArgSpec`](/modules/debug/#tecs.debug.ArgSpec) | record | Declares one argument. | | [`Command`](/modules/debug/#tecs.debug.Command) | type | Declares one command. | | [`Registry`](/modules/debug/#tecs.debug.Registry) | type | Holds the commands one world has declared. | | [`Result`](/modules/debug/#tecs.debug.Result) | record | Reports what one command did. | | [`Schema`](/modules/debug/#tecs.debug.Schema) | record | Declares a command's arguments. | | [`Subcommand`](/modules/debug/#tecs.debug.Subcommand) | record | Declares one verb under a command. | ### Functions | Function | Kind | Description | | --- | --- | --- | | [`bindRenderer`](/modules/debug/#tecs.debug.bindRenderer) | Static | Tells the commands which renderer draws this world. | | [`ensure`](/modules/debug/#tecs.debug.ensure) | Static | Builds the world's command registry, or returns the one it already has. | | [`of`](/modules/debug/#tecs.debug.of) | Static | Returns the registry installed in world. | | [`register`](/modules/debug/#tecs.debug.register) | Static | Registers a game's own command and projects it onto the debug server. | ### Values | Value | Type | Description | | --- | --- | --- | | [`SECTIONS`](/modules/debug/#tecs.debug.SECTIONS) | `{string}` | Read-only. Groups commands in help and in the generated reference, in the order they are presented. | ## Types ### tecs.debug.ArgSpec record Declares one argument. ```teal record tecs.debug.ArgSpec required: boolean default: any help: string forward: string synthetic: {string} kind: string enum: {string} min: number max: number rest: boolean end ``` #### tecs.debug.ArgSpec.required field Caller-writable. Requires the argument to hold a value once parsing finishes. ```teal tecs.debug.ArgSpec.required: boolean ``` #### tecs.debug.ArgSpec.default field Caller-writable. Supplies the value used when the argument is absent, and infers the value type when `kind` is omitted. A forwarding argument never receives its default, which only types it. ```teal tecs.debug.ArgSpec.default: any ``` #### tecs.debug.ArgSpec.help field Caller-writable. Describes the argument in generated help and in the MCP tool schema. ```teal tecs.debug.ArgSpec.help: string ``` #### tecs.debug.ArgSpec.forward field Caller-writable. Names the synthetic argument this argument feeds its value into. ```teal tecs.debug.ArgSpec.forward: string ``` #### tecs.debug.ArgSpec.synthetic field Caller-writable. Lists the source argument names this synthetic argument accepts, and its presence marks the argument synthetic, so users neither name it nor see it. ```teal tecs.debug.ArgSpec.synthetic: {string} ``` #### tecs.debug.ArgSpec.kind field Caller-writable. States the value type explicitly, as `"number"`, `"boolean"`, `"string"`, `"list"`, `"table"` or `"rows"`. Omitting it infers the type from `default`, so set it for an optional typed argument that has no default, such as a number that leaves the current value unchanged when absent. A `"list"` is a comma-separated string on the command line and a JSON string array over MCP. A `"table"` is a brace-balanced Lua expression on the command line and a JSON object over MCP. A `"rows"` argument is that same expression on the command line and a JSON array of objects over MCP, so an action receives either a string or a table and handles both. ```teal tecs.debug.ArgSpec.kind: string ``` #### tecs.debug.ArgSpec.enum field Caller-writable. Restricts a string argument to these values, and projects to a JSON Schema `enum`. ```teal tecs.debug.ArgSpec.enum: {string} ``` #### tecs.debug.ArgSpec.min field Caller-writable. Bounds a number argument from below, inclusively. ```teal tecs.debug.ArgSpec.min: number ``` #### tecs.debug.ArgSpec.max field Caller-writable. Bounds a number argument from above, inclusively. ```teal tecs.debug.ArgSpec.max: number ``` #### tecs.debug.ArgSpec.rest field Caller-writable. Makes the slot consume every remaining token as free text, joined with single spaces, where a quoted span keeps its inner spacing. The argument must be the last positional slot and must be string-, table- or rows-typed. ```teal tecs.debug.ArgSpec.rest: boolean ``` ### tecs.debug.Command type Declares one command. ```teal type tecs.debug.Command = debugtypes.Command ``` ### tecs.debug.Registry type Holds the commands one world has declared. ```teal type tecs.debug.Registry = registry.Registry ``` ### tecs.debug.Result record Reports what one command did. ```teal record tecs.debug.Result ok: boolean code: string message: string data: {string: any} end ``` #### tecs.debug.Result.ok field Caller-writable. Reports whether the command did what it was asked. Omitted counts as success. False raises the command's message through the tool call rather than answering with it. ```teal tecs.debug.Result.ok: boolean ``` #### tecs.debug.Result.code field Caller-writable. Names the failure in a word an agent can match on, such as `no_match` or `invalid_ref`. Required when `ok` is false and ignored otherwise. ```teal tecs.debug.Result.code: string ``` #### tecs.debug.Result.message field Caller-writable. Summarizes the outcome for a person in one line. A successful command carries it beside its data under the key `message`. ```teal tecs.debug.Result.message: string ``` #### tecs.debug.Result.data field Caller-writable. Carries the structured answer, which is what the tool call returns and what `outputSchema` describes. Omitted answers with the message alone. ```teal tecs.debug.Result.data: {string: any} ``` ### tecs.debug.Schema record Declares a command's arguments. ```teal record tecs.debug.Schema args: {string: ArgSpec} positional: {string} end ``` #### tecs.debug.Schema.args field Caller-writable. Maps each argument name to the spec that types it. ```teal tecs.debug.Schema.args: {string: ArgSpec} ``` #### tecs.debug.Schema.positional field Caller-writable. Lists the argument names bound by position, in order. A slot may name a synthetic argument. ```teal tecs.debug.Schema.positional: {string} ``` ### tecs.debug.Subcommand record Declares one verb under a command. ```teal record tecs.debug.Subcommand name: string aliases: {string} shortHelp: string agentHelp: string schema: cmdargs.Schema examples: {string} outputSchema: {string: any} readOnly: boolean destructive: boolean run: function({string: any}): Result end ``` #### tecs.debug.Subcommand.name field Caller-writable. Names the verb. The projected tool is `_`, so this string is part of an externally typed surface once it ships. Required. ```teal tecs.debug.Subcommand.name: string ``` #### tecs.debug.Subcommand.aliases field Caller-writable. Names other spellings that dispatch to this verb on a typed command line. Aliases are never projected as tools. ```teal tecs.debug.Subcommand.aliases: {string} ``` #### tecs.debug.Subcommand.shortHelp field Caller-writable. Says what the verb does in one line, lowercase and without a trailing period. Required. ```teal tecs.debug.Subcommand.shortHelp: string ``` #### tecs.debug.Subcommand.agentHelp field Caller-writable. Says what an agent needs: when to call this, what it changes, what it answers with, and what to call next. The projected tool description falls back to `shortHelp`, which is written to fit one line rather than to brief an agent. ```teal tecs.debug.Subcommand.agentHelp: string ``` #### tecs.debug.Subcommand.schema field Caller-writable. Declares the arguments. Omitted means the verb takes none. ```teal tecs.debug.Subcommand.schema: cmdargs.Schema ``` #### tecs.debug.Subcommand.examples field Caller-writable. Shows complete command lines that work, for the generated reference and the usage output. ```teal tecs.debug.Subcommand.examples: {string} ``` #### tecs.debug.Subcommand.outputSchema field Caller-writable. Describes `Result.data` as a JSON Schema. The projection advertises it as the tool's `outputSchema`, so an agent knows the shape of the answer before making the call. ```teal tecs.debug.Subcommand.outputSchema: {string: any} ``` #### tecs.debug.Subcommand.readOnly field Caller-writable. Declares that the verb only reads. Omitted derives the answer from the verb: `list`, `info`, `status` and `get` read. ```teal tecs.debug.Subcommand.readOnly: boolean ``` #### tecs.debug.Subcommand.destructive field Caller-writable. Declares that the verb destroys something a caller cannot get back. Omitted derives the answer from the verb. ```teal tecs.debug.Subcommand.destructive: boolean ``` #### tecs.debug.Subcommand.run Static Caller-writable. Runs the verb. Required. ```teal function tecs.debug.Subcommand.run({string: any}): Result ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `#1` | `{string : any}` | | ##### Returns | Type | Description | | --- | --- | | `Result` | | ## Functions ### tecs.debug.bindRenderer Static Tells the commands which renderer draws this world. Commands that read or draw through the renderer answer with a failure until something calls this, since a world alone does not name one. ```teal function tecs.debug.bindRenderer( world: types.World, renderer: Renderer ) ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`types.World`](/modules/ecs/#tecs.World) | The caller supplies the world the renderer draws. | | `renderer` | [`Renderer`](/modules/gfx/#tecs.gfx.Renderer) | The caller supplies the renderer, or nil to unbind. | #### Returns None. ### tecs.debug.ensure Static Builds the world's command registry, or returns the one it already has. Registering the engine's own commands projects each of them onto the debug server, so a session that calls this once has the whole surface. Calling it again answers with the same registry and registers nothing further. ```teal function tecs.debug.ensure(world: types.World): registry.Registry ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`types.World`](/modules/ecs/#tecs.World) | The caller supplies the world the commands read and write. | #### Returns | Type | Description | | --- | --- | | [`registry.Registry`](/modules/debug/#tecs.debug.Registry) | The world's registry. | ### tecs.debug.of Static Returns the registry installed in `world`. ```teal function tecs.debug.of(world: types.World): registry.Registry ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`types.World`](/modules/ecs/#tecs.World) | The caller supplies a world with or without a registry. | #### Returns | Type | Description | | --- | --- | | [`registry.Registry`](/modules/debug/#tecs.debug.Registry) | The world's registry, or nil before `ensure` has run. | ### tecs.debug.register Static Registers a game's own command and projects it onto the debug server. Raises when the world has no registry, and raises on a malformed declaration: a command is written once and called forever, so a mistake in one belongs at the call that made it. ```teal function tecs.debug.register( world: types.World, command: debugtypes.Command ) ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`types.World`](/modules/ecs/#tecs.World) | The caller supplies a world `ensure` has already run on. | | `command` | [`debugtypes.Command`](/modules/debug/#tecs.debug.Command) | The caller supplies a complete declaration. | #### Returns None. ## Values ### tecs.debug.SECTIONS variable Read-only. Groups commands in help and in the generated reference, in the order they are presented. A command that names no section groups as `"Custom"`. ```teal tecs.debug.SECTIONS: {string} ``` --- ## Archetypes # Archetypes An archetype stores entities with one component signature. Adding or removing a component moves an entity to another archetype. Most game code reaches these storage groups through a [query](/modules/ecs/queries/). ## Rows and columns One row index selects an entity ID and every component value for that entity: ```teal for archetype, length, entities in movers:iter() do local transforms = archetype:getMut(tecs.Transform2D) local velocities = archetype:get(Velocity) for row = 1, length do local transform = transforms[row] local velocity = velocities[row] transform.x = transform.x + velocity.x * dt transform.y = transform.y + velocity.y * dt print(entities[row]) end end ``` Rows start at 1. `archetype.entities[0]` contains the current length. Treat the entity column and `componentList` as read-only. A row identifies a position in current storage, not an entity. Despawn and archetype transitions use swap-pop movement, so never retain a row across structural changes. Retain the entity ID instead. ## Read and write intent `archetype:get(Component)` returns a column without marking it dirty. `archetype:getMut(Component)` returns the same storage and marks the component dirty for that archetype. Use `getMut` only when the loop will write. A speculative call dirties every row in the column and forces dirty-gated consumers to process unchanged data. For conditional writes, read through `get`, perform the write only when needed, then call `markComponentDirty`. `archetype:set(row, value)` replaces a component already present in the signature and marks it dirty. It cannot add a component. Use `world:set` when the entity may need an archetype transition. ## Relationship storage Dense relationship instances occupy archetype columns. Use `forEachRelationship` or `getFirstRelationship` when code already has an archetype and row. Sparse relationships, including `ChildOf`, keep targets in a world-owned store. Resolve them through `world:getFirstRelationship`, `world:targets`, `world:traverse`, or `world:walkUp`. See [Relationships](/modules/ecs/relationships/). ## Dirty consumers Incremental consumers can test one component, test the whole archetype, or iterate its dirty components. The world clears dirty bits after each `world:update`, once the pipeline has consumed them. Spawn placement, archetype movement, swap-pop, `getMut`, and `set` maintain dirty state automatically. Call explicit markers only after a write through a path Tecs cannot observe, such as direct FFI cdata obtained through `get`. [Dirty tracking](/modules/ecs/components/dirty-tracking) covers the complete write contract. ## Lifecycle reactions An archetype observer can react to contiguous additions, removals, row moves, activation, deactivation, and destruction. It remains attached for that archetype's lifetime and cannot unsubscribe. Prefer [query callbacks](/modules/ecs/queries/callbacks) when the reaction belongs to a component filter. Queries discover current and future matching archetypes and attach the necessary observers. Use a direct archetype observer only when the storage object itself matters. --- ## Builtins # Builtins Every world registers the same core components, relationship, events, and systems. Games use them directly from `tecs.ecs`, except for `tecs.Transform2D`, which sits at the root because every subsystem moves it. The durable entity-key component uses the public name `EntityKey` and the externally typed registered name `"Key"`. `tecs.data.Key` names the typed store-key type. ## Name {#name} `Name` stores a non-unique string label: ```teal local entity = world:spawn(tecs.ecs.Name("Phreddy")) print(world:get(entity, tecs.ecs.Name)) world:set(entity, tecs.ecs.Name, "Greg") ``` The scalar column stores the raw string. Callers may replace it through `world:set`; Tecs owns the column and scalar wrapper. Use `Name` for display and debugging. Use [`EntityKey`](#entitykey) when code must rediscover an entity. ## EntityKey {#entitykey} `EntityKey` adds a durable, developer-chosen string to the world's unique index: ```teal local player = world:spawn( tecs.ecs.EntityKey("player"), tecs.ecs.Name("Player ship") ) assert(world:byKey("player") == player) assert(world:requireKey("player") == player) ``` Callers choose and may replace the key through `world:set`. Tecs owns the index, rejects duplicate live keys, releases a key on removal or despawn, and rebuilds the index after snapshot load. ## ChildOf {#childof} `ChildOf` links one child to one parent. Its registration enables exclusive targets, sparse storage, a reverse index, and cascade delete. ```teal local parent = world:spawn() local child = world:spawn(tecs.ecs.ChildOf(parent)) world:targets( parent, tecs.ecs.ChildOf, function(childId: integer) print(childId) end ) world:despawn(parent) -- also despawns child ``` Tecs owns the relationship target field; callers treat it as read-only and replace the edge through `world:set`. [Relationships](/modules/ecs/relationships/) covers storage and traversal. ## Transform2D {#transform} `Transform2D` holds world position, layer, rotation, and scale in one FFI component. Positions use world units, which map to pixels with the origin at the top left. Rotation uses radians. Layer starts at 1 and rejects values below 1. The positional constructor orders values as `x`, `y`, `z`, `layer`, `rotation`, `scaleX`, and `scaleY`: ```teal local entity = world:spawn(tecs.Transform2D(10, 11, 1, 2)) local transform = world:getMut(entity, tecs.Transform2D) transform.rotation = math.pi / 4 transform.scaleX = 2 transform.scaleY = 2 ``` Callers may write transform fields through `getMut`. Tecs owns storage and dirty marks. A write through `world:get` changes the cdata but marks nothing, so that path requires `world:markComponentDirty(entity, tecs.Transform2D)`. The hierarchy, sequencer, physics, and renderer share this component. Rendering additionally requires `Tint` and `Renderable2D`. ## RelativeTransform2D {#relativetransform} `RelativeTransform2D` expresses a child pose relative to its `ChildOf` parent: ```teal local parent = world:spawn(tecs.Transform2D(100, 100)) local child = world:spawn( tecs.ecs.ChildOf(parent), tecs.ecs.RelativeTransform2D(50, 30) ) ``` The component requires `Transform2D`, so both enter the same archetype transition. Callers own and may mutate the relative fields through `getMut`. The builtin hierarchy system owns the resulting world `Transform2D` while the entity carries both `ChildOf` and `RelativeTransform2D`; a later composition overwrites direct edits to that derived transform. Composition rotates and scales the offset by the parent, adds rotations, multiplies scales, and copies the parent's layer. Origin fields express a fraction of size, with `0`, `0.5`, and `1` marking the near edge, center, and far edge. ## TTL {#ttl} `TTL` despawns an entity when its remaining fixed-clock time reaches zero: ```teal world:spawn(tecs.ecs.TTL(10)) ``` `TTL(remaining)` uses the same value for the starting time. `TTL(remaining, startingTime)` starts partway through and requires `startingTime >= remaining > 0`. Callers set the starting values and may adjust them through `getMut`. The builtin `ttl` system owns the per-step decrement of `remaining`. `percentComplete()` reports progress from zero to one. ## Disabled {#disabled} `Disabled` removes an entity from every query unless the descriptor explicitly includes the tag. Renderer queries follow the same rule, so a disabled entity does not draw. ```teal world:set(entity, tecs.ecs.Disabled) world:remove(entity, tecs.ecs.Disabled) ``` ## Paused {#paused} `Paused` keeps presentation visible while stopping logic queries. A query with `type = "logic"` excludes the tag. Render queries and untyped queries continue to match it unless they list `Paused` under `exclude`. The [state stack](/modules/ecs/states) manages this tag for a state whose `onBlur` policy equals `"pause"`. Games may also add or remove it directly. ## Events {#events} Observers should treat every event payload as read-only. Tecs owns the payload for the duration of dispatch and may reuse its backing storage afterwards. ### OnSpawn {#onspawn-event} `OnSpawn` carries the entity ID at address `0`. `world:spawn` emits it while the entity remains staged, before archetype placement. An observer may stage follow-up mutations against the ID. `batchSpawn` and `batchSpawnAt` do not emit `OnSpawn`; use their fill callback or a query's `onEntitiesAdded`. ### OnDespawn {#ondespawn-event} `OnDespawn` carries the entity ID first at the entity address, then at address `0`. The entity remains alive and readable during both dispatches. Tecs clears every observer at the entity address after dispatch, then removes the row at commit. ### ArchetypeCreated {#archetypecreated-event} `ArchetypeCreated` carries a newly created archetype at address `0`. Tecs owns the `archetype` field and callers treat it as read-only. Queries consume this event internally; game code should use [query callbacks](/modules/ecs/queries/callbacks) for match-set changes. ### State transition events {#state-transition-events} The state stack emits `StateEnter`, `StateExit`, `StateBlur`, and `StateFocus` at address `0`. Their engine-owned string fields identify the state and, for blur or focus, the pushed or popped state. ### Snapshot events {#snapshot-events} `OnSnapshotSave`, `StartSnapshotLoad`, and `FinishSnapshotLoad` bracket snapshot work at address `0`. Their engine-owned payloads expose functions for adding data, excluding derived entities, and subscribing to keyed load data. Callers may invoke those functions but must not replace them. [Save games](/modules/ecs/save-games) covers the lifecycle and recommends named snapshot handlers for ordinary subsystem state. ## Builtin plugin {#builtin-plugin} World construction installs three systems: | System | Phase | Work | | ------------------------------- | ------------- | -------------------------------------------------- | | `ttl` | `FixedUpdate` | Decrement `TTL.remaining` and despawn at zero | | `RelativeTransform2D` | `PostUpdate` | Compose child world transforms | | `RelativeTransformDirtySampler` | `RenderLast` | Carry late hierarchy dirtiness into the next frame | The `ttl` query uses `type = "logic"`, so paused entities keep their remaining time. Hierarchy composition runs before `RenderFirst` extraction and only writes a child `Transform2D` when the composed values differ. --- ## Component bundles # Component bundles A bundle names one component set and compiles its spawn path once: ```teal local playerBundle = world:newBundle( "Player", { required = {tecs.Transform2D, Health}, with = { [tecs.gfx.Tint] = function() return tecs.gfx.Tint(1, 1, 1, 1) end, [tecs.gfx.Renderable2D] = true, }, } ) local player = playerBundle:spawn( tecs.Transform2D(100, 200), Health(100) ) ``` `required` sets the positional arguments to `spawn`. `with` creates the rest for every entity. ## Required components Declaration order controls spawn order: ```teal local enemyBundle = world:newBundle( "Enemy", { required = {tecs.Transform2D, Health, Damage}, } ) local enemy = enemyBundle:spawn( tecs.Transform2D(100, 200), Health(50), Damage(10) ) ``` Every argument must match its declared component. Move any value that varies per spawn into `required`. ## Bundle defaults Each `with` value must hold a factory or `true`. A factory runs once per spawn and returns a fresh instance: ```teal local bulletBundle = world:newBundle( "Bullet", { required = {tecs.Transform2D}, with = { [Velocity] = function() return Velocity(100, 0) end, [Damage] = function() return Damage(25) end, }, } ) ``` `true` calls the component with no arguments. It suits tags and components whose declared defaults already have the right value: ```teal local propBundle = world:newBundle( "Prop", { required = {tecs.Transform2D}, with = { [tecs.gfx.Renderable2D] = true, [Static] = true, }, } ) ``` A spawn cannot override a component from `with`. Put that component in `required` when callers need to supply it. The definition may name each component once across `required` and `with`. Registration rejects duplicates, invalid `with` values, and duplicate bundle names. ## Staged spawning The bundle object and the world registry call the same compiled spawn path: ```teal local first = playerBundle:spawn( tecs.Transform2D(0, 0), Health(100) ) local second = world:spawnBundle( "Player", tecs.Transform2D(20, 0), Health(100) ) ``` Bundle spawns follow `world:spawn` timing. They reserve an ID immediately and stage placement until the next pipeline barrier. The returned ID works immediately for later staged operations: ```teal for _archetype, _length in query:iter() do local id = playerBundle:spawn( tecs.Transform2D(0, 0), Health(100) ) world:set(id, Selected) end ``` ## Registry lookup `world:getBundle(name)` returns one bundle or `nil`. `world:getBundles()` returns a fresh name-to-bundle map, so changing the map does not change the registry. --- ## Component construction # Component construction Structured components share one construction model: - `Component(...)` maps positional arguments to declared fields. - `Component.new({...})` provides the named form. - `defaults` fill omitted positional values. - `init` validates or derives values after field mapping. - A custom configuration `__call` replaces positional mapping. Storage changes the allocated value, not these rules. ## Positional fields Table components list field names. FFI components list name and C type pairs: ```teal local record Health is tecs.ecs.Component current: integer maximum: integer metamethod __call: function( self, current?: integer, maximum?: integer ): Health end tecs.ecs.newFFIComponent({ name = "Health", container = Health, fields = { {"current", "int32_t"}, {"maximum", "int32_t"}, }, defaults = {100, 100}, }) local full = Health() local damaged = Health(80, 120) ``` Defaults line up with fields. A nil slot means no declared default, while false remains a valid default. FFI allocation supplies zero values for fields that positional arguments and defaults omit. Relationship targets always occupy the first positional argument and do not appear in the public field list. ## Named construction {#table-construction} The named form routes fields through the positional constructor: ```teal local damaged = Health.new({ current = 80, maximum = 120, }) ``` Tecs reads declared fields in order and calls `Health(80, 120)`. The initializer receives positional values, not the input table. Provide a custom `new` only when named construction cannot map to the positional form. ## Validation and derived values `init(instance, ...)` runs after allocation, field assignment, and defaults: ```teal tecs.ecs.newFFIComponent({ name = "Health", container = Health, fields = { {"current", "int32_t"}, {"maximum", "int32_t"}, }, defaults = {100, 100}, init = function(instance: Health) if instance.current < 0 then error("Health.current must not be negative") end if instance.maximum < instance.current then error("Health.maximum must cover current health") end end, }) ``` Use defaults for static values. Use `init` for validation, normalization, and derived state. An initializer requires declared fields or a custom `new`, so the named path stays defined. ## Custom call shapes Supply configuration `__call(instance, ...)` when public constructor arguments do not match stored fields: ```teal tecs.ecs.newComponent({ name = "ParticleEmitter", container = ParticleEmitter, requires = {tecs.Transform2D}, __call = function( instance: ParticleEmitter, options: EmitterOptions ) initEmitter(instance, options) end, new = function(data: {string: any}): ParticleEmitter local instance = {} as ParticleEmitter initEmitter(instance, data as EmitterOptions) return instance end, }) ``` Tecs allocates the base value and applies defaults before the custom call. It does not invoke `init` afterwards. Call shared initialization explicitly when both paths need it. --- ## Dirty tracking # Dirty tracking Tecs tracks dirty state per archetype and component. One mark means that some row in the column may have changed. It does not track individual rows. Rendering and other incremental consumers use that signal to skip unchanged columns. A write that Tecs cannot see leaves those consumers with stale data. ## Declare write intent Read through `get` and write through `getMut`: ```teal for archetype, length in movers:iter() do local transforms = archetype:getMut(tecs.Transform2D) local velocities = archetype:get(Velocity) for row = 1, length do local transform = transforms[row] local velocity = velocities[row] transform.x = transform.x + velocity.x * dt transform.y = transform.y + velocity.y * dt end end ``` `getMut` marks the entire component column dirty before returning it. Never call it speculatively in a loop that might not write. For a conditional write, read first and mark only when the condition succeeds: ```teal local transforms = archetype:get(tecs.Transform2D) local changed = false for row = 1, length do if needsCorrection(transforms[row]) then correct(transforms[row]) changed = true end end if changed then archetype:markComponentDirty(tecs.Transform2D) end ``` LuaJIT cannot enforce read-only cdata. A field assignment through `world:get` or `archetype:get` changes memory without marking it. Prefer `getMut`; otherwise call `world:markComponentDirty` or the archetype marker after the write. ## Automatic marks These paths maintain dirty state: - `getMut`, `world:set`, and `archetype:set` - spawn placement - movement into another archetype - swap-pop after removal Spawn and structural movement mark every component on the archetype, because a row moving in has every column newly written at that row. Tecs records those marks as one archetype-wide structural flag rather than as every column's bit, and every reader above composes the flag in. `getMut`, `markComponentDirty` and `set` still set the bit for the one column they name. The distinction is not observable through these readers, and it is what lets the renderer's `partialRewrites` option rewrite the rows a spawn wrote instead of the archetype's whole run: a value write names a column and says nothing about which rows changed, while a structural change carries its rows. `world:update` clears marks after the pipeline runs. A consumer can iterate dirty archetypes, test one component, test any component, or iterate dirty components. Do not structurally mutate the world while walking `dirtyArchetypes`. ## Batch initialization `batchSpawn` reserves rows and calls the fill callback without running component constructors. FFI defaults do not run. Initialize every field the batch will use: ```teal world:batchSpawn( 1000, {Position}, function(archetype, firstRow, lastRow) local positions = archetype:getMut(Position) for row = firstRow, lastRow do positions[row].x = 0 positions[row].y = 0 end end ) ``` Placement already marks the destination columns. The callback still uses `getMut` to state its write intent clearly. The [mutation model](/modules/ecs/mutation-model) defines the normative marking rules. --- ## FFI components # FFI components An FFI component stores each value as a fixed C struct. Its archetype column forms one contiguous memory block instead of an array of Lua table references. Use FFI storage for numeric, boolean, pointer, and fixed-array data that hot systems or native-facing code process in bulk. Use a [table component](/modules/ecs/components/table-components) for strings, nested Lua objects, or flexible shape. ## Struct declarations Pair each public field with a LuaJIT C type: ```teal local record Velocity is tecs.ecs.Component x: number y: number metamethod __call: function(self, x?: number, y?: number): Velocity end tecs.ecs.newFFIComponent({ name = "Velocity", container = Velocity, fields = { {"x", "float"}, {"y", "float"}, }, }) local velocity = Velocity(10, 20) ``` Field names must form unique C identifiers. Common choices include: | Data | C types | | ----------------- | --------------------------------------------- | | Signed integers | `int8_t`, `int16_t`, `int32_t`, `int64_t` | | Unsigned integers | `uint8_t`, `uint16_t`, `uint32_t`, `uint64_t` | | Floating point | `float`, `double` | | Boolean | `bool` | | Pointer | `void*`, `const char*`, `float*` | | Fixed array | `float[4]`, `uint8_t[256]`, `char[64]` | The type string enters LuaJIT's C parser. Fixed arrays live inside each struct. Pointers do not own their targets; game code must guarantee the pointed memory outlives every component value that refers to it. ## Defaults and validation FFI allocation initializes numbers to zero, booleans to false, and pointers to nil. Declarative defaults override those values: ```teal local record Color is tecs.ecs.Component r: number g: number b: number a: number end tecs.ecs.newFFIComponent({ name = "Color", container = Color, fields = { {"r", "float"}, {"g", "float"}, {"b", "float"}, {"a", "float"}, }, defaults = {1, 1, 1, 1}, }) ``` The shared [construction model](/modules/ecs/components/construction) provides positional calls, named `new`, defaults, validation through `init`, and custom constructor shapes. `batchSpawn` bypasses per-instance construction and defaults. Its callback must assign every field that later code reads. ## Mutation and dirty state FFI field access looks like ordinary record access, but a cdata reference does not notify Tecs when code writes through it: ```teal local velocity = world:getMut(entity, Velocity) velocity.x = velocity.x + acceleration * dt ``` Use `world:getMut` or `archetype:getMut` for writes. A write through `get` requires an explicit dirty mark. See [Dirty tracking](/modules/ecs/components/dirty-tracking). ## Durable representations Raw fields work well when their bits mean the same thing in every run. Process-local indices, native handles, pointers, and resolved GPU slots do not. Give those components a custom durable serializer or mark runtime-only state transient. Binary snapshots copy matching FFI columns in bulk. Schema changes can migrate same-named fields. See [Component serialization](/modules/ecs/components/serialization). --- ## Components # Components Components hold entity data. A system binds component columns from each matching archetype, reads through `get`, and takes writable columns through `getMut`: ```teal local Transform2D = tecs.Transform2D local movers = world:newQuery({ include = {Transform2D, Velocity}, }) world:addSystem({ name = "game.Move", phase = tecs.ecs.phases.Update, run = function(dt: number) for archetype, length in movers:iter() do local transforms = archetype:getMut(Transform2D) local velocities = archetype:get(Velocity) for row = 1, length do transforms[row].x = transforms[row].x + velocities[row].x * dt transforms[row].y = transforms[row].y + velocities[row].y * dt end end end, }) ``` `getMut` marks the `Transform2D` column dirty. The renderer and other incremental consumers use that mark to skip unchanged columns. Component values belong to callers. Callers may replace them with `world:set` or mutate their fields through `getMut`; Tecs owns the storage and dirty marks around those values. ## Storage choices | Kind | Use | | --------------------------------------------------- | ------------------------------------------------------------ | | [Table](/modules/ecs/components/table-components) | Strings, nested tables, opaque handles, and other Lua values | | [FFI](/modules/ecs/components/ffi) | Fixed-size numeric fields in contiguous C structs | | [Scalar](/modules/ecs/components/scalar-components) | One `number`, `boolean`, or `string` per entity | | [Tag](/modules/ecs/components/tag-components) | Presence with no per-entity value | The engine uses the same factories. `Transform2D`, `Sprite`, `Tint`, `Material`, `Clip`, and `PointLight2D` use FFI storage; `Renderable2D` uses table-component registration as a presence marker. ## Entity access `world:get(entity, Component)` returns the component or `nil`. A scalar component returns its raw value: ```teal local transform = world:get(entity, tecs.Transform2D) local name = world:get(entity, tecs.ecs.Name) ``` Call `world:getMut` before an in-place write: ```teal local transform = world:getMut(entity, tecs.Transform2D) if transform then transform.x = transform.x + 10 end ``` Do not call `getMut` at a site that might only read. It declares mutation intent and defeats dirty-gated work even when no value changes. An FFI reference obtained through `world:get` remains writable because LuaJIT cannot make cdata const. If code writes through that reference, it must call `world:markComponentDirty(entity, Component)` explicitly. A spawn reserves an ID but does not place the entity until the next pipeline barrier. `world:get` and `world:getMut` return `nil` for that staged entity. Pass its initial values to `world:spawn` instead. ## Adding and removing components Pass instances to `world:spawn` and `world:set`: ```teal local entity = world:spawn( tecs.ecs.Name("Frank"), tecs.Transform2D(100, 200) ) world:set(entity, tecs.ecs.Name("Grace")) world:remove(entity, tecs.ecs.Name) ``` `world:has(entity, Component)` tests presence. Relationship containers and instances add any-target and specific-target checks; see [Relationships](/modules/ecs/relationships/). Adding or removing a component changes the entity's archetype, so those calls always stage until a pipeline barrier. [Structural transactions](/modules/ecs/world#structural-transactions) covers the visibility rules. ## Component dependencies {#auto-dependencies-with-requires} `requires` declares components that must accompany another component. Tecs adds the full transitive closure in one archetype transition: ```teal local record Velocity is tecs.ecs.Component x: number y: number metamethod __call: function(self, x?: number, y?: number): Velocity end tecs.ecs.newFFIComponent({ name = "Velocity", container = Velocity, fields = { {"x", "float"}, {"y", "float"}, }, defaults = {0, 0}, requires = {tecs.Transform2D}, }) local entity = world:spawn(Velocity(10, 20)) assert(world:has(entity, tecs.Transform2D)) ``` A `requires` entry may hold a component type, which Tecs calls with no arguments, or a component instance shared by every automatic addition. `newComponent`, `newFFIComponent`, `newScalarComponent`, `newTagComponent`, and both relationship factories accept the option. `tecs.ecs.RelativeTransform2D` requires `tecs.Transform2D`, so a relative transform and the world transform it feeds enter the same archetype together. Use [query callbacks](/modules/ecs/queries/callbacks) for work that must run when a signature starts or stops matching. ## Transient state Set `transient = true` on components and relationships that hold runtime projections such as native handles or caches. Snapshots omit those columns but keep their entities. Rebuild the omitted values from durable components after load. `transient` and a custom `serialize` function conflict, so registration rejects that combination. [Component serialization](/modules/ecs/components/serialization) covers codecs and migrations. ## Module contents ### Submodules | Submodule | Description | | --- | --- | | [`Component bundles`](/modules/ecs/components/bundles/) | Reusable entity templates with world:newBundle and spawnBundle | | [`Component construction`](/modules/ecs/components/construction/) | Shared component construction model covering __call, new, fields, defaults, and the init hook | | [`Component serialization`](/modules/ecs/components/serialization/) | Component serialize and deserialize hooks, transient, and automatic FFI schema fingerprint migration | | [`Dirty tracking`](/modules/ecs/components/dirty-tracking/) | Per-archetype per-component dirty bits set by getMut and set, and the extractor that reads them | | [`FFI components`](/modules/ecs/components/ffi/) | FFI struct-backed components via newFFIComponent with C field types and defaults | | [`Scalar components`](/modules/ecs/components/scalar-components/) | Single-value number, boolean, or string components via newScalarComponent with fast SoA columns | | [`Table components`](/modules/ecs/components/table-components/) | Lua-table-backed components via newComponent with fields, init, custom __call, and new | | [`Tag components`](/modules/ecs/components/tag-components/) | Dataless presence tags via newTagComponent for flags, markers, and query filtering | --- ## Scalar components # Scalar components A scalar component stores one number, boolean, or string directly in an archetype column: ```teal local Health = tecs.ecs.newScalarComponent({ name = "Health", kind = "number", default = 100, }) local entity = world:spawn(Health(75)) world:set(entity, Health, 50) print(world:get(entity, Health)) -- 50 ``` Use this storage when the component means exactly one primitive value. Use a tag for presence alone and a table or FFI component for a structured value. ## Values and constructor tokens `world:get` and archetype columns return the raw primitive. `Health(75)` instead returns a small component token for spawn and the two-argument `world:set` form: ```teal world:set(entity, Health(25)) assert(world:get(entity, Health) == 25) ``` The token does not compare equal to its primitive value. Treat it as an input to component APIs, not as stored data. Calling `world:set(entity, Health)` writes the registered default. When no default exists, the kind supplies `0`, false, or an empty string. ## Column updates Scalar columns follow ordinary query and dirty rules: ```teal local living = world:newQuery({ include = {Health}, type = "logic", }) for archetype, length in living:iter() do local health = archetype:getMut(Health) for row = 1, length do health[row] = math.max(0, health[row] - 1) end end ``` Do not split one coherent value into many scalar components only to pursue column density. A position normally belongs in one structured component rather than separate X and Y components. ## Typed module exports Scalar registration has no container record to carry its value type. State the type on a module export: ```teal local record combat Health: tecs.ecs.ScalarComponent end combat.Health = tecs.ecs.newScalarComponent({ name = "Health", kind = "number", default = 100, }) return combat ``` Snapshots store the raw value. A transient scalar stays out of snapshots. --- ## Component serialization # Component serialization Snapshots serialize ordinary components automatically. Add custom codecs only when runtime fields do not carry durable meaning. ## Automatic paths Table components save their user fields and load through the named constructor: ```teal tecs.ecs.newComponent({ name = "Health", container = Health, fields = {"current", "maximum"}, }) ``` FFI components save every declared field. A binary snapshot copies a matching column as raw bytes. Scalar components save their raw value, and tags save their presence: a tag holds no per-row state, so it writes an empty payload and loads back from any payload a snapshot carries for it. The named constructor must remain defined for automatic table deserialization. See [Named construction](/modules/ecs/components/construction#table-construction). ## Durable identity Use codecs when a component stores a process-local index, native handle, pointer, resolved GPU slot, circular reference, or derived value. Save a durable name or authored ID, then rebuild the runtime representation: ```teal tecs.ecs.newFFIComponent({ name = "Sprite", container = Sprite, fields = { {"image", "int32_t"}, {"u0", "float"}, {"v0", "float"}, {"u1", "float"}, {"v1", "float"}, {"slot", "int32_t"}, }, serialize = function(sprite: Sprite): {string: any} return { image = imageNames[sprite.image as integer], u0 = sprite.u0, v0 = sprite.v0, u1 = sprite.u1, v1 = sprite.v1, } end, deserialize = function( _world: tecs.World, data: {string: any} ): Sprite local image = imageId(data.image as string) return Sprite( image, data.u0 as number, data.v0 as number, data.u1 as number, data.v1 as number ) end, }) ``` The save carries the image name instead of the intern index. It omits the resolved slot and derives it again after load. The deserializer uses the same registration path as a fresh component. Custom hooks opt an FFI component out of bulk byte copying. ## Runtime-only columns {#skipping-a-component-from-snapshots} Set `transient = true` when the entity belongs in the save but one component contains only rebuildable process state: ```teal tecs.ecs.newComponent({ name = "PhysicsBodyHandle", container = PhysicsBodyHandle, transient = true, }) ``` The snapshot omits the column but retains the entity. Recreate transient handles, caches, and slots from durable components after load. Registration rejects a component that combines `transient` with a custom serializer. ## FFI schema migration Binary snapshots store an FFI schema fingerprint. Matching schemas use the bulk path. A mismatch maps saved fields into a new current instance by name. | Schema change | Result for saved data | | ------------------------------- | --------------------------------------------------------------- | | Add a field | The field receives its current default. | | Remove a field | Load drops the saved field. | | Reorder fields | Values continue by name. | | Change a numeric type | LuaJIT converts the value. Narrowing may truncate. | | Rename a field | The saved value is ignored and the new field takes its default. | | Change array or aggregate shape | Load raises. | For a rename, include both fields and copy the saved value after load, or provide a custom codec that accepts the saved durable shape. Table-format snapshots and non-FFI components always use their structured codec. They tolerate add, remove, and reorder changes by field name, with the same rename caveat. ## Codec cost | Path | Work | | ----------------------------- | ----------------------------------------- | | Matching binary FFI schema | One bulk copy per column. | | Changed FFI schema | Per-entity same-name migration. | | Custom or table codec | Per-entity structured conversion. | | Sparse relationship archetype | Row-major conversion with presence masks. | Choose a custom codec for correctness first. The bulk path applies only when raw bytes already form the durable representation. --- ## Table components # Table components Table components hold values that do not fit a fixed C struct: strings, nested tables, opaque handles, and data that needs Lua reference semantics. ```teal local record Health is tecs.ecs.Component value: number max: number metamethod __call: function( self, value?: number, max?: number ): Health end tecs.ecs.newComponent({ name = "Health", container = Health, fields = {"value", "max"}, defaults = {100, 100}, }) local full = Health() local hurt = Health(40, 100) local named = Health.new({value = 40, max = 100}) ``` Each instance owns a Lua table. Its metatable resolves `componentType`, methods, and other container fields without copying them onto every instance. Callers own the instance fields and may mutate them through `getMut`. Tecs owns the metatable and component metadata; callers should treat those as read-only. Use [FFI components](/modules/ecs/components/ffi) for fixed-size primitive fields that hot loops or native code read from contiguous memory. ## Field construction `fields` controls positional order and generates the table-form `.new`. `defaults` fills omitted values in the same order; `nil` leaves a field without a default. Registration requires only `name` and `container`. A table component may act as a presence marker with no instance fields: ```teal local record Renderable2D is tecs.ecs.Component end tecs.ecs.newComponent({ name = "Renderable2D", container = Renderable2D, }) ``` For field-by-field checking on `.new`, declare a config record and narrow the inherited signature: ```teal local record Health is tecs.ecs.Component value: number max: number record Config value: number max: number end metamethod __call: function( self, value?: number, max?: number ): Health new: function(config: Config): Health end ``` The narrower declaration changes Teal checking only. The generated `.new` still maps the table through `fields`. ## Validation and derived fields Add `init` when direct field mapping needs validation or refinement: ```teal local record Inventory is tecs.ecs.Component slots: {string} capacity: integer metamethod __call: function( self, slots: {string}, capacity?: integer ): Inventory end tecs.ecs.newComponent({ name = "Inventory", container = Inventory, fields = {"slots", "capacity"}, defaults = {nil, 10}, init = function(inventory: Inventory) if inventory.slots == nil then error("Inventory requires slots") end if #inventory.slots > inventory.capacity then error("Inventory exceeds capacity") end end, }) local inventory = Inventory({"sword"}) ``` Tecs fills fields and defaults before `init` runs. Both the positional constructor and generated `.new` run the hook. An `init` hook requires `fields` or an explicit `new`. Without one of those, Tecs cannot map the table form to positional arguments. ## Semantic constructors Use a custom `__call` when arguments describe an operation rather than a field list. Tecs allocates the instance and applies defaults before it invokes the hook. A custom `__call` replaces the generated path and does not invoke `init`; call shared initialization explicitly. Pair it with a custom `new` when the table form needs its own mapping: ```teal tecs.ecs.newComponent({ name = "ParticleEmitter", container = ParticleEmitter, requires = {tecs.Transform2D}, __call = function(emitter: ParticleEmitter, options: EmitterOptions) initEmitter(emitter, options) end, new = function(data: {string: any}): ParticleEmitter local emitter = {} as ParticleEmitter initEmitter(emitter, data as EmitterOptions) return emitter end, }) local sparks = ParticleEmitter({effect = "sparks"}) ``` Tecs applies the component metatable to the table returned by `new`. [Component construction](/modules/ecs/components/construction) covers the rules shared with FFI components and relationships. ## Lifecycle reactions Table components support in-place writes, so a value change does not pass through a setter. Use [dirty tracking](/modules/ecs/components/dirty-tracking) when a consumer needs to find changed columns. Use [query callbacks](/modules/ecs/queries/callbacks) when code must react to a component entering or leaving a matching signature. Query callbacks operate on contiguous row ranges and can match several components at once. Snapshots serialize declared fields by default. Use custom `serialize`/`deserialize` functions for process-local values, or set `transient = true` for state that should not enter a snapshot. See [Component serialization](/modules/ecs/components/serialization). --- ## Tag components # Tag components A tag has no per-entity value. Presence supplies the entire signal: ```teal local Selected = tecs.ecs.newTagComponent({name = "Selected"}) local Stunned = tecs.ecs.newTagComponent({name = "Stunned"}) world:set(entity, Selected) assert(world:has(entity, Selected)) world:remove(entity, Selected) ``` Use tags for flags, markers, and classifications. Use a scalar or structured component when each entity needs a value. `world:get` on a tag returns the tag container rather than row data. Prefer `world:has` for a presence check. ## Query membership Tags belong in query filters, not column loops: ```teal local activeEnemies = world:newQuery({ include = {Enemy, Selected}, exclude = {Stunned}, type = "logic", }) for archetype, length, entities in activeEnemies:iter() do local positions = archetype:get(Position) for row = 1, length do updateSelection(entities[row], positions[row]) end end ``` The matching archetype signature already guarantees the tag's presence. ## Structural cost Adding or removing a tag moves the entity between archetypes, just like any component membership change. Use batch operations for large groups: ```teal local targets = world:newQuery({ include = {Enemy, InBlastRadius}, temp = true, }) world:batchSet(targets, Stunned) local stunnedEnemies = world:newQuery({ include = {Enemy, Stunned}, temp = true, }) world:batchRemove(stunnedEnemies, Stunned) ``` `Disabled` causes every ordinary query to exclude the entity unless the query explicitly includes that tag. `Paused` remains visible to render work; `type = "logic"` excludes it. The [state stack](/modules/ecs/states) creates a tag for each named state and adds the current top state's tag to new entities. --- ## Events # Events An observer subscribes to one event type at one integer address. Address `0` belongs to the world; an entity ID addresses that entity. The entry plugin below watches every despawn at the world address: ```teal local Transform2D = tecs.Transform2D return tecs.newApplication({ plugin = function(world: tecs.World) world:observe( 0, tecs.ecs.OnDespawn, function(event: tecs.ecs.OnDespawn) -- OnDespawn runs before commit removes the row. local transform = world:get( event.entity, Transform2D ) if transform then spawnDebrisAt(world, transform.x, transform.y) end end ) end, }) ``` The platform event stream uses the same bus. The host emits each platform kind at address `0`; [`tecs.platform.events`](/modules/platform/events) defines those event types. ## World and entity addresses Use address `0` for messages that belong to the world: ```teal world:observe(0, GamePaused, onGamePaused) world:emit(0, GamePaused) ``` Use an entity ID for a subscription tied to that entity: ```teal world:observe( player, DamageReceived, function(event: DamageReceived) applyDamage(player, event.amount) end ) world:emit(player, DamageReceived, 15) ``` When an entity despawns, the world clears every observer at that address before the slot can belong to another entity. World-address observers remain. ## Observer timing `world:emit` invokes matching observers before it returns. The observer runs in the emitter's phase and joins that phase's structural transaction. Platform events arrive before `world:update`, so their observers run outside the phase tree. They do not receive fixed-step timing, phase order, or state gating. Fold an event into state when a reaction needs those properties. [`Input`](/modules/input) follows that pattern for keyboard, pointer, and gamepad events. Observers suit immediate notification. Systems suit ordered frame work. ## Subscription lifetime `world:observe` accepts an optional string ID. Remove a subscription with its callback or ID: ```teal world:observe(0, GamePaused, onGamePaused, "pause-ui") world:stopObserving(0, GamePaused, onGamePaused) world:stopObserving(0, GamePaused, "pause-ui") ``` Passing a function removes every matching registration of that function. Passing an ID removes the first matching registration. When an observer unsubscribes during dispatch, the bus waits until the current dispatch unwinds before changing its list. `world:clearObservers(address)` clears an address that game code manages. Entity despawn handles entity addresses automatically. `world:hasObservers` matters when building the payload itself costs work: ```teal if world:hasObservers(enemy, PathChanged) then world:emit(enemy, PathChanged, buildPathSnapshot(enemy)) end ``` For ordinary constructor arguments, call `world:emit(address, EventType, ...)` directly. The world checks for observers before it constructs an event. ## Table events Define a record, give it an in-place initializer, then register it: ```teal local record PlayerDamaged is tecs.events.Event amount: number source: string metamethod __call: function( self, amount: number, source: string ): PlayerDamaged end PlayerDamaged.init = function( event: PlayerDamaged, amount: number, source: string ) event.amount = amount event.source = source end tecs.events.newEvent(PlayerDamaged) world:emit(player, PlayerDamaged, 10, "fire") ``` Registration assigns the event type its ID. Register each type once. `PlayerDamaged(10, "fire")` allocates an independent instance. Use that form when code must retain the value or send it through a standalone `MessageBus`. `world:emit(player, PlayerDamaged, 10, "fire")` leases pooled backing storage and returns it after dispatch. Do not retain the instance passed to an observer. The emitter owns the payload during dispatch, so observers should treat its fields as read-only and copy any values that must outlive the callback. ## FFI events `newFFIEvent` stores fixed-size fields in a C struct: ```teal local record DamageEvent is tecs.events.Event amount: number entity: integer metamethod __call: function( self, amount: number, entity: integer ): DamageEvent end tecs.events.newFFIEvent( DamageEvent, { {"amount", "float"}, {"entity", "double"}, }, "Game_DamageEvent" ) world:emit(0, DamageEvent, 15.5, enemy) ``` Without a custom `init`, the generated initializer follows field order. Field names must form unique C identifiers. `eventId` and `typeId` belong to Tecs and cannot appear in `fields`. Use `double` for an entity ID. The packed slot and generation do not fit in a 32-bit integer. FFI events cannot carry Lua strings, tables, functions, or userdata; table events can. `OnSpawn` and `OnDespawn` use FFI storage with a `double entity` field. ## Standalone message buses Each world owns a `MessageBus`. `tecs.events.newMessageBus()` creates the same address router without a world. A standalone bus dispatches an event instance that the caller constructs. It also exposes `observeOnce`, per-address clearing, entity-address clearing, and a full reset. World methods add lazy construction, pooled emission, and automatic cleanup when entities die. --- ## tecs.ecs # tecs.ecs The ECS shared by game code and engine systems. Build queries during setup, reuse them from systems, and put all game state on entities: ```teal local moving: tecs.Query local function game(world: tecs.World) moving = world:newQuery({include = {tecs.Transform2D}}) world:addSystem({ name = "game.Drift", phase = tecs.ecs.phases.Update, run = function(delta: number) for archetype, length in moving:iter() do local transform = archetype:getMut( tecs.Transform2D ) for row = 1, length do transform[row].x = transform[row].x + 60 * delta end end end, }) world:spawn(tecs.Transform2D(0, 0)) end ``` Read columns through `archetype:get`. Write through `getMut` so dirty-gated systems can find the change. See [Worlds](/modules/ecs/world), [Components](/modules/ecs/components/), [Queries](/modules/ecs/queries/), and [Systems](/modules/ecs/systems) for the complete ECS model. ## Module contents ### Submodules | Submodule | Description | | --- | --- | | [`Archetypes`](/modules/ecs/archetype/) | Archetype storage, column access, relationship lookups, dirty tracking, and lifecycle observers | | [`Builtins`](/modules/ecs/builtins/) | The components, relationship, events and systems every world registers automatically, all on tecs.ecs | | [`Components`](/modules/ecs/components/) | Component overview with world get, getMut, set, remove, has, requires, and transient | | [`Events`](/modules/ecs/events/) | Address-based ECS events: observe, emit, hasObservers, newEvent, newFFIEvent, and the MessageBus router | | [`Mutation model`](/modules/ecs/mutation-model/) | Structural transactions, publication barriers, value writes, and visibility guarantees | | [`Phases`](/modules/ecs/phases/) | Game-loop phase groups and the world methods that control them | | [`Plugins`](/modules/ecs/plugins/) | Plugins provide the one way into a world: entry arguments, composition, and patterns that scale | | [`Profiling`](/modules/ecs/profiling/) | LuaJIT sampling profiler and trace-abort tracker through tecs.utils.profile | | [`Queries`](/modules/ecs/queries/) | Creating and iterating queries with include, exclude, includeAny, temp, and deferred mutations | | [`Relationships`](/modules/ecs/relationships/) | Directed entity relationships, storage, deletion, and traversal | | [`Save games`](/modules/ecs/save-games/) | Snapshot saving, loading, transient components, handlers, filtering, and binary format | | [`State stack`](/modules/ecs/states/) | The world state stack, lifecycle policies, automatic tags, and transition events | | [`Systems`](/modules/ecs/systems/) | System configuration, ordering, removal, and tecs.ecs.runif predicates | | [`World`](/modules/ecs/world/) | World entities, structural transactions, batches, resources, plugins, phases, and stats | | [`tecs.ecs.random`](/modules/ecs/random/) | Seeded named streams, standalone generators, and snapshot restoration | ### Constructors | Constructor | Description | | --- | --- | | [`newComponent`](/modules/ecs/#tecs.ecs.newComponent) | Creates and registers a new table component. | | [`newFFIComponent`](/modules/ecs/#tecs.ecs.newFFIComponent) | Creates and registers an FFI-based component. | | [`newFFIRelationship`](/modules/ecs/#tecs.ecs.newFFIRelationship) | Creates an FFI-backed relationship with data fields. | | [`newRelationship`](/modules/ecs/#tecs.ecs.newRelationship) | Creates and registers a relationship component. | | [`newScalarComponent`](/modules/ecs/#tecs.ecs.newScalarComponent) | Creates and registers a new scalar component. | | [`newTagComponent`](/modules/ecs/#tecs.ecs.newTagComponent) | Creates and registers a tag component. | | [`newWorld`](/modules/ecs/#tecs.ecs.newWorld) | Creates a new World. | ### Types | Type | Kind | Description | | --- | --- | --- | | [`Archetype`](/modules/ecs/#tecs.ecs.Archetype) | interface | Archetype stores contiguous component columns. | | [`ArchetypeCreated`](/modules/ecs/#tecs.ecs.ArchetypeCreated) | record | An event emitted at entity 0 (world) when a new archetype is created. | | [`ArchetypeEntityObserver`](/modules/ecs/#tecs.ecs.ArchetypeEntityObserver) | interface | ArchetypeEntityObserver receives entity changes within an archetype. | | [`Bundle`](/modules/ecs/#tecs.ecs.Bundle) | interface | Bundle names a reusable component collection. | | [`BundleDef`](/modules/ecs/#tecs.ecs.BundleDef) | interface | BundleDef configures a bundle. | | [`Component`](/modules/ecs/#tecs.ecs.Component) | interface | Component names any component definition. | | [`ComponentOptions`](/modules/ecs/#tecs.ecs.ComponentOptions) | interface | ComponentOptions configures a table component. | | [`ContainerComponentOptions`](/modules/ecs/#tecs.ecs.ContainerComponentOptions) | interface | Shared options for creating different components. | | [`DoubleArray`](/modules/ecs/#tecs.ecs.DoubleArray) | interface | DoubleArray names an FFI array of doubles. | | [`FFIComponentOptions`](/modules/ecs/#tecs.ecs.FFIComponentOptions) | interface | FFIComponentOptions configures an FFI component. | | [`FFIRelationshipOptions`](/modules/ecs/#tecs.ecs.FFIRelationshipOptions) | interface | FFIRelationshipOptions configures an FFI relationship. | | [`FinishSnapshotLoad`](/modules/ecs/#tecs.ecs.FinishSnapshotLoad) | record | An event emitted on entity 0 once tecs.loadSnapshot finishes -- after all entities are restored AND every data... | | [`FixedOverload`](/modules/ecs/#tecs.ecs.FixedOverload) | enum | FixedOverload selects fixed-step overload behavior. | | [`OnDespawn`](/modules/ecs/#tecs.ecs.OnDespawn) | record | An event emitted when a specific entity is despawned. | | [`OnSnapshotSave`](/modules/ecs/#tecs.ecs.OnSnapshotSave) | record | An event emitted on entity 0 at the start of tecs.saveSnapshot, BEFORE archetype data is written. | | [`OnSpawn`](/modules/ecs/#tecs.ecs.OnSpawn) | record | An event emitted when a specific entity is spawned. | | [`Phase`](/modules/ecs/#tecs.ecs.Phase) | interface | Phase identifies one system phase. | | [`phases`](/modules/ecs/#tecs.ecs.phases) | record | Defines the predefined ECS phases, or lifecycle states, of the game and event loop. | | [`Pipeline`](/modules/ecs/#tecs.ecs.Pipeline) | interface | Pipeline identifies a world update pipeline. | | [`Plugin`](/modules/ecs/#tecs.Plugin) | type | A plugin configures a world with systems, resources, observers, and initial entities. | | [`Query`](/modules/ecs/#tecs.Query) | interface | A reusable filter over the archetypes in a world. | | [`QueryDescriptor`](/modules/ecs/#tecs.ecs.QueryDescriptor) | type | QueryDescriptor configures a query. | | [`Relationship`](/modules/ecs/#tecs.ecs.Relationship) | interface | Relationship names any relationship definition. | | [`RelationshipOptions`](/modules/ecs/#tecs.ecs.RelationshipOptions) | interface | RelationshipOptions configures a relationship. | | [`RelativeTransform2D`](/modules/ecs/#tecs.ecs.RelativeTransform2D) | record | A component that defines an entity's transform relative to its parent (ChildOf). | | [`ScalarComponent`](/modules/ecs/#tecs.ecs.ScalarComponent) | interface | ScalarComponent names a single-value component. | | [`ScalarComponentOptions`](/modules/ecs/#tecs.ecs.ScalarComponentOptions) | interface | ScalarComponentOptions configures a scalar component. | | [`Snapshot`](/modules/ecs/#tecs.ecs.Snapshot) | type | Snapshot names serialized world state. | | [`SnapshotComponentTableEntry`](/modules/ecs/#tecs.ecs.SnapshotComponentTableEntry) | type | SnapshotComponentTableEntry identifies one component in a snapshot. | | [`SnapshotHandler`](/modules/ecs/#tecs.ecs.SnapshotHandler) | type | SnapshotHandler saves and restores plugin data. | | [`SnapshotOptions`](/modules/ecs/#tecs.ecs.SnapshotOptions) | type | SnapshotOptions controls snapshot serialization. | | [`SnapshotOutput`](/modules/ecs/#tecs.ecs.SnapshotOutput) | type | SnapshotOutput collects serialized data. | | [`SnapshotPrelude`](/modules/ecs/#tecs.ecs.SnapshotPrelude) | type | SnapshotPrelude names snapshot metadata. | | [`StartSnapshotLoad`](/modules/ecs/#tecs.ecs.StartSnapshotLoad) | record | An event emitted on entity 0 at the start of tecs.loadSnapshot, AFTER the world has been fully restored and BEFORE... | | [`StateBlur`](/modules/ecs/#tecs.ecs.StateBlur) | record | An event emitted when a state loses focus (another state pushed on top). | | [`StateEnter`](/modules/ecs/#tecs.ecs.StateEnter) | record | An event emitted when a state is pushed onto the stack. | | [`StateExit`](/modules/ecs/#tecs.ecs.StateExit) | record | An event emitted when a state is popped from the stack. | | [`StateFocus`](/modules/ecs/#tecs.ecs.StateFocus) | record | An event emitted when a state regains focus (state above popped). | | [`StatePolicy`](/modules/ecs/#tecs.ecs.StatePolicy) | record | StatePolicy controls state-stack participation. | | [`Stats`](/modules/ecs/#tecs.ecs.Stats) | type | Stats reports live world counts. | | [`System`](/modules/ecs/#tecs.System) | type | A system function runs once when its phase dispatches. | | [`SystemConfig`](/modules/ecs/#tecs.ecs.SystemConfig) | interface | SystemConfig configures a system. | | [`SystemInfo`](/modules/ecs/#tecs.ecs.SystemInfo) | interface | SystemInfo reports one registered system. | | [`TagComponentOptions`](/modules/ecs/#tecs.ecs.TagComponentOptions) | interface | TagComponentOptions configures a tag component. | | [`Transform2D`](/modules/ecs/#tecs.ecs.Transform2D) | record | Provides the coordinates and transform of an entity. | | [`Transform3D`](/modules/ecs/#tecs.ecs.Transform3D) | record | Places an entity in a right-handed three-dimensional world. | | [`TTL`](/modules/ecs/#tecs.ecs.TTL) | record | Despawns an entity when the TTL reaches zero. | | [`World`](/modules/ecs/#tecs.World) | interface | A world owns entities, components, queries, systems, resources, events, snapshots, and the state stack. | ### Functions | Function | Kind | Description | | --- | --- | --- | | [`declaredComponents`](/modules/ecs/#tecs.ecs.declaredComponents) | Static | Returns every component declared in this process. | | [`findComponentById`](/modules/ecs/#tecs.ecs.findComponentById) | Static | Returns a registered component by numeric id. | | [`findComponentByName`](/modules/ecs/#tecs.ecs.findComponentByName) | Static | Returns the component registered under name. | ### Values | Value | Type | Description | | --- | --- | --- | | [`ChildOf`](/modules/ecs/#tecs.ecs.ChildOf) | [`Relationship`](/modules/ecs/#tecs.ecs.Relationship) | Read-only. ChildOf links a parent and child. | | [`DEFAULT_MAX_ENTITIES`](/modules/ecs/#tecs.ecs.DEFAULT_MAX_ENTITIES) | `integer` | Read-only. DEFAULT_MAX_ENTITIES supplies 2^20 when world configuration omits maxEntities. | | [`Disabled`](/modules/ecs/#tecs.ecs.Disabled) | [`Component`](/modules/ecs/#tecs.ecs.Component) | Read-only. Disabled excludes an entity from queries that do not ask for it. | | [`EntityKey`](/modules/ecs/#tecs.ecs.EntityKey) | [`ScalarComponent`](/modules/ecs/#tecs.ecs.ScalarComponent) | Read-only. EntityKey stores a durable unique lookup key for world:byKey. | | [`MAX_ENTITIES`](/modules/ecs/#tecs.ecs.MAX_ENTITIES) | `integer` | Read-only. MAX_ENTITIES sets the absolute World.Config.maxEntities ceiling (2^22 - 1 usable slots; the entity-id... | | [`Name`](/modules/ecs/#tecs.ecs.Name) | [`ScalarComponent`](/modules/ecs/#tecs.ecs.ScalarComponent) | Read-only. Name stores an entity label as a raw string. | | [`Paused`](/modules/ecs/#tecs.ecs.Paused) | [`Component`](/modules/ecs/#tecs.ecs.Component) | Read-only. Paused excludes an entity from logic queries while keeping it visible. | | [`runif`](/modules/ecs/#tecs.ecs.runif) | `runIfHelpers` | Read-only. runif contains composable system run conditions. | ## Constructors ### tecs.ecs.newComponent Static Creates and registers a new table component. See `ecs.ComponentOptions` and the component docs for the shared `fields` / `defaults` / `init` / `.new` model. ```teal function tecs.ecs.newComponent( options: ComponentOptions ): C ``` #### Type Parameters | Name | Constraint | Description | | --- | --- | --- | | `C` | [`Component`](/modules/ecs/#tecs.ecs.Component) | | #### Arguments | Name | Type | Description | | --- | --- | --- | | `options` | [`ComponentOptions`](/modules/ecs/#tecs.ecs.ComponentOptions)`` | The caller supplies a process-unique name and the table component definition. | #### Returns | Type | Description | | --- | --- | | `C` | Returns the supplied container after permanent process-wide registration makes it callable. | ### tecs.ecs.newFFIComponent Static Creates and registers an FFI-based component. Same constructor model as `newComponent`, but the base instance is an FFI struct. ```teal function tecs.ecs.newFFIComponent( options: FFIComponentOptions ): C ``` #### Type Parameters | Name | Constraint | Description | | --- | --- | --- | | `C` | [`Component`](/modules/ecs/#tecs.ecs.Component) | | #### Arguments | Name | Type | Description | | --- | --- | --- | | `options` | [`FFIComponentOptions`](/modules/ecs/#tecs.ecs.FFIComponentOptions)`` | The caller supplies fields that map to a C struct: numbers, booleans and fixed-size arrays. | #### Returns | Type | Description | | --- | --- | | `C` | Returns the registered component with contiguous cdata columns. Direct writes through `world:get` require `world:markComponentDirty`. | ### tecs.ecs.newFFIRelationship Static Creates an FFI-backed relationship with data fields. ```teal function tecs.ecs.newFFIRelationship( config: FFIRelationshipOptions ): R ``` #### Type Parameters | Name | Constraint | Description | | --- | --- | --- | | `R` | [`Relationship`](/modules/ecs/#tecs.ecs.Relationship) | | #### Arguments | Name | Type | Description | | --- | --- | --- | | `config` | [`FFIRelationshipOptions`](/modules/ecs/#tecs.ecs.FFIRelationshipOptions)`` | The caller supplies relationship behavior and C-compatible edge fields. | #### Returns | Type | Description | | --- | --- | | `R` | Returns the registered relationship. | ### tecs.ecs.newRelationship Static Creates and registers a relationship component. A target-only relationship needs a name and flags. Add `container` and `fields` when each edge carries data. ```teal function tecs.ecs.newRelationship( config: RelationshipOptions ): R ``` #### Type Parameters | Name | Constraint | Description | | --- | --- | --- | | `R` | [`Relationship`](/modules/ecs/#tecs.ecs.Relationship) | | #### Arguments | Name | Type | Description | | --- | --- | --- | | `config` | [`RelationshipOptions`](/modules/ecs/#tecs.ecs.RelationshipOptions)`` | The caller supplies target, exclusivity, sparsity, reverse index and cascade behavior. | #### Returns | Type | Description | | --- | --- | | `R` | Returns the registered relationship, callable with a target to create an instance. | ### tecs.ecs.newScalarComponent Static Creates and registers a new scalar component. ```teal function tecs.ecs.newScalarComponent( options: ScalarComponentOptions ): ScalarComponent ``` #### Type Parameters | Name | Constraint | Description | | --- | --- | --- | | `T` | | | #### Arguments | Name | Type | Description | | --- | --- | --- | | `options` | [`ScalarComponentOptions`](/modules/ecs/#tecs.ecs.ScalarComponentOptions)`` | The caller supplies the scalar type, name and defaults. | #### Returns | Type | Description | | --- | --- | | [`ScalarComponent`](/modules/ecs/#tecs.ecs.ScalarComponent)`` | Returns the registered component whose rows hold bare values. | ### tecs.ecs.newTagComponent Static Creates and registers a tag component. A tag holds no per-row state, so a snapshot saves its presence and writes no payload for it. ```teal function tecs.ecs.newTagComponent( options: TagComponentOptions ): Component ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `options` | [`TagComponentOptions`](/modules/ecs/#tecs.ecs.TagComponentOptions) | The caller supplies the tag name. | #### Returns | Type | Description | | --- | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | Returns the registered bitset-backed tag. | ### tecs.ecs.newWorld Static Creates a new World. ```teal function tecs.ecs.newWorld(config: types.World.Config): World ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `config` | [`types.World.Config`](/modules/ecs/#tecs.World.Config) | The caller supplies world settings or omits them for `DEFAULT_MAX_ENTITIES` and the default pipeline. The entity ceiling cannot grow after creation. | #### Returns | Type | Description | | --- | --- | | [`World`](/modules/ecs/#tecs.World) | Returns an independent world with builtins registered and no systems. | ## Types ### tecs.ecs.Archetype interface `Archetype` stores contiguous component columns. ```teal interface tecs.ecs.Archetype id: integer entities: DoubleArray componentList: {components.Component} addEntityObserver: function(self, ArchetypeEntityObserver) anyComponentDirty: function(self): boolean dirtyComponents: function(self): function(): components.Component forEachRelationship: function( self, row: T, callback: integer, function(T) ) get: function(self, T): {T} getFirstRelationship: function( self, row: T, integer ): T getMut: function(self, T): {T} isComponentDirty: function( self, component: components.Component ): boolean markAllComponentsDirty: function(self) markComponentDirty: function(self, components.Component) set: function( self, row: integer, value: C ) end ``` #### tecs.ecs.Archetype.id field Read-only. The unique identifier of the archetype in the ECS container. ```teal tecs.ecs.Archetype.id: integer ``` #### tecs.ecs.Archetype.entities field Read-only. Entity IDs that belong to this archetype. Length is `entities[0]`; rows are 1-based (`entities[1]` is the first entity). ```teal tecs.ecs.Archetype.entities: DoubleArray ``` #### tecs.ecs.Archetype.componentList field Read-only. Components in this archetype, in the order they were passed at construction. Finalized at creation -- archetypes never add or remove components. Iterate with `#componentList` / `ipairs` to walk the archetype's signature. ```teal tecs.ecs.Archetype.componentList: {components.Component} ``` #### tecs.ecs.Archetype:addEntityObserver Instance Register an observer for lifecycle changes on this archetype. The observer remains attached for the archetype's lifetime. Registration applies only to this archetype; it does not discover other archetypes with the same components. See `ArchetypeEntityObserver`. ```teal function tecs.ecs.Archetype.addEntityObserver( self, ArchetypeEntityObserver ) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Archetype` | | | `#2` | `ArchetypeEntityObserver` | | ##### Returns None. #### tecs.ecs.Archetype:anyComponentDirty Instance True if any component on this archetype is currently dirty. Useful for bulk re-sync paths that don't track dirty granularity below the archetype level. ```teal function tecs.ecs.Archetype.anyComponentDirty(self): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Archetype` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | | #### tecs.ecs.Archetype:dirtyComponents Instance Iterate components currently marked dirty on this archetype. ```teal function tecs.ecs.Archetype.dirtyComponents( self ): function(): components.Component ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Archetype` | | ##### Returns | Type | Description | | --- | --- | | `function(): `[`components.Component`](/modules/ecs/#tecs.ecs.Component) | | #### tecs.ecs.Archetype:forEachRelationship Instance Iterate all relationship instances of the given container for the entity at `row`. ```teal function tecs.ecs.Archetype.forEachRelationship( self, row: T, callback: integer, function(T) ) ``` ##### Type Parameters | Name | Constraint | Description | | --- | --- | --- | | `T` | [`components.Relationship`](/modules/ecs/#tecs.ecs.Relationship) | | ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Archetype` | | | `row` | `T` | 1-based row position. | | `callback` | `integer` | Called once per matching relationship instance, in no defined order. Adding or removing relationships on this entity from inside it is not safe. | | `#4` | `function(T)` | | ##### Returns None. #### tecs.ecs.Archetype:get Instance Read-only column access. Returns the row-indexed column for the given component type, or nil if the archetype doesn't carry it. Does NOT mark the component dirty. ```teal function tecs.ecs.Archetype.get( self, T ): {T} ``` ##### Type Parameters | Name | Constraint | Description | | --- | --- | --- | | `T` | [`components.Component`](/modules/ecs/#tecs.ecs.Component) | | ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Archetype` | | | `#2` | `T` | | ##### Returns | Type | Description | | --- | --- | | `{T}` | The archetype's live column, indexed from one and not a copy, so it is valid only until something moves an entity in or out of this archetype. Nil only when this archetype does not carry the component at all. A sparse relationship answers with a row-indexed proxy rather than a stored column, which reads the same and is not writable. Writing through this leaves the column clean, and on an FFI component that means the GPU never re-syncs. | #### tecs.ecs.Archetype:getFirstRelationship Instance Get the first relationship instance of the given container for the entity at `row`, or nil if none exists. ```teal function tecs.ecs.Archetype.getFirstRelationship( self, row: T, integer ): T ``` ##### Type Parameters | Name | Constraint | Description | | --- | --- | --- | | `T` | [`components.Relationship`](/modules/ecs/#tecs.ecs.Relationship) | | ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Archetype` | | | `row` | `T` | 1-based row position. | | `#3` | `integer` | | ##### Returns | Type | Description | | --- | --- | | `T` | The one instance for an exclusive relationship, and an arbitrary one of several for a relationship that is not: "first" is storage order rather than the order they were added. Nil when the entity has none. | #### tecs.ecs.Archetype:getMut Instance Mutable column access. Returns the row-indexed column AND marks the component dirty on this archetype. Use this at every site where you intend to write into the column. ```teal function tecs.ecs.Archetype.getMut( self, T ): {T} ``` ##### Type Parameters | Name | Constraint | Description | | --- | --- | --- | | `T` | [`components.Component`](/modules/ecs/#tecs.ecs.Component) | | ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Archetype` | | | `#2` | `T` | | ##### Returns | Type | Description | | --- | --- | | `{T}` | The same column `get` answers with, dirty-marked before it is handed over. Marked whether or not anything is then written, so calling this in a loop that might not write defeats every dirty-gated consumer; take it on the first row that actually changes instead. | #### tecs.ecs.Archetype:isComponentDirty Instance True if the given component's column on this archetype is currently marked dirty. ```teal function tecs.ecs.Archetype.isComponentDirty( self, component: components.Component ): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Archetype` | | | `component` | [`components.Component`](/modules/ecs/#tecs.ecs.Component) | False for a component this archetype does not carry, so a consumer may name one without testing membership. | ##### Returns | Type | Description | | --- | --- | | `boolean` | True for a value write to this column, and true for any structural change to the archetype: a row moving in has every column newly written at that row. | #### tecs.ecs.Archetype:markAllComponentsDirty Instance Mark every component on this archetype dirty. Says that every column changed and says nothing about where, so a consumer that can resync single rows resyncs the whole archetype after it. Prefer `markComponentDirty` for a column you can name. ```teal function tecs.ecs.Archetype.markAllComponentsDirty(self) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Archetype` | | ##### Returns None. #### tecs.ecs.Archetype:markComponentDirty Instance Mark a single component dirty on this archetype. Idempotent. ```teal function tecs.ecs.Archetype.markComponentDirty( self, components.Component ) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Archetype` | | | `#2` | [`components.Component`](/modules/ecs/#tecs.ecs.Component) | | ##### Returns None. #### tecs.ecs.Archetype:set Instance Set a component value at a row and mark it dirty. Use for in-place updates that don't change the entity's archetype. ```teal function tecs.ecs.Archetype.set( self, row: integer, value: C ) ``` ##### Type Parameters | Name | Constraint | Description | | --- | --- | --- | | `C` | [`components.Component`](/modules/ecs/#tecs.ecs.Component) | | ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Archetype` | | | `row` | `integer` | 1-based row position. | | `value` | `C` | New component value (must carry a `componentType`). Stored as given rather than copied field by field, so a table component holds the caller's table afterwards. | ##### Returns None. ### tecs.ecs.ArchetypeCreated record An event emitted at entity 0 (world) when a new archetype is created. ```teal record tecs.ecs.ArchetypeCreated is types.events.Event archetype: types.Archetype metamethod __call: function(self, types.Archetype): ArchetypeCreated end ``` #### Interfaces | Interface | | --- | | [`types.events.Event`](/modules/events/#tecs.events.Event) | #### tecs.ecs.ArchetypeCreated.archetype field Engine-owned. Reports this public value. ```teal tecs.ecs.ArchetypeCreated.archetype: types.Archetype ``` #### tecs.ecs.ArchetypeCreated:__call metamethod Create a new ArchetypeCreated event. ```teal metamethod tecs.ecs.ArchetypeCreated.$meta.__call( self, types.Archetype ): ArchetypeCreated ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `ArchetypeCreated` | | | `#2` | [`types.Archetype`](/modules/ecs/#tecs.ecs.Archetype) | | ##### Returns | Type | Description | | --- | --- | | [`ArchetypeCreated`](/modules/ecs/#tecs.ecs.ArchetypeCreated) | | ### tecs.ecs.ArchetypeEntityObserver interface `ArchetypeEntityObserver` receives entity changes within an archetype. ```teal interface tecs.ecs.ArchetypeEntityObserver onActivated: function(self, archetype: Archetype) onArchetypeDestroyed: function(self, Archetype) onDeactivated: function(self, archetype: Archetype) onEntitiesAdded: function( self, archetype: Archetype, firstRow: integer, lastRow: integer, count: integer, sourceArchetype: Archetype ) onEntitiesRemoved: function( self, archetype: Archetype, firstRow: integer, lastRow: integer, count: integer, destArchetype: Archetype ) onEntityMove: function( self, archetype: Archetype, entity: integer, fromRow: integer, toRow: integer ) end ``` #### tecs.ecs.ArchetypeEntityObserver:onActivated Instance Called when archetype transitions from empty to non-empty (0 -> 1 entities). ```teal function tecs.ecs.ArchetypeEntityObserver.onActivated( self, archetype: Archetype ) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `ArchetypeEntityObserver` | | | `archetype` | [`Archetype`](/modules/ecs/#tecs.ecs.Archetype) | The archetype that became active. | ##### Returns None. #### tecs.ecs.ArchetypeEntityObserver:onArchetypeDestroyed Instance Called when an archetype is being permanently destroyed (during `world:compact()`). Observers must remove all references to the archetype. ```teal function tecs.ecs.ArchetypeEntityObserver.onArchetypeDestroyed( self, Archetype ) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `ArchetypeEntityObserver` | | | `#2` | [`Archetype`](/modules/ecs/#tecs.ecs.Archetype) | | ##### Returns None. #### tecs.ecs.ArchetypeEntityObserver:onDeactivated Instance Called when archetype transitions from non-empty to empty (1 -> 0 entities). ```teal function tecs.ecs.ArchetypeEntityObserver.onDeactivated( self, archetype: Archetype ) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `ArchetypeEntityObserver` | | | `archetype` | [`Archetype`](/modules/ecs/#tecs.ecs.Archetype) | The archetype that became inactive. | ##### Returns None. #### tecs.ecs.ArchetypeEntityObserver:onEntitiesAdded Instance Called once per contiguous range of entities added to the archetype. ```teal function tecs.ecs.ArchetypeEntityObserver.onEntitiesAdded( self, archetype: Archetype, firstRow: integer, lastRow: integer, count: integer, sourceArchetype: Archetype ) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `ArchetypeEntityObserver` | | | `archetype` | [`Archetype`](/modules/ecs/#tecs.ecs.Archetype) | The archetype the range was placed in. | | `firstRow` | `integer` | The 1-based row of the first new entity. | | `lastRow` | `integer` | The 1-based row of the last new entity (inclusive). | | `count` | `integer` | Number of entities in the range (lastRow - firstRow + 1). | | `sourceArchetype` | [`Archetype`](/modules/ecs/#tecs.ecs.Archetype) | The archetype the range originated from (nil for spawns). | ##### Returns None. #### tecs.ecs.ArchetypeEntityObserver:onEntitiesRemoved Instance Called once per contiguous range of entities removed from the archetype. ```teal function tecs.ecs.ArchetypeEntityObserver.onEntitiesRemoved( self, archetype: Archetype, firstRow: integer, lastRow: integer, count: integer, destArchetype: Archetype ) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `ArchetypeEntityObserver` | | | `archetype` | [`Archetype`](/modules/ecs/#tecs.ecs.Archetype) | The archetype the range is being removed from. | | `firstRow` | `integer` | The 1-based row of the first removed entity. | | `lastRow` | `integer` | The 1-based row of the last removed entity (inclusive). | | `count` | `integer` | Number of entities in the range (lastRow - firstRow + 1). | | `destArchetype` | [`Archetype`](/modules/ecs/#tecs.ecs.Archetype) | The archetype the range is moving to (nil for despawns). | ##### Returns None. #### tecs.ecs.ArchetypeEntityObserver:onEntityMove Instance Called when an entity is swap-popped into a different row. Implementing `onEntitiesAdded` is not required. ```teal function tecs.ecs.ArchetypeEntityObserver.onEntityMove( self, archetype: Archetype, entity: integer, fromRow: integer, toRow: integer ) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `ArchetypeEntityObserver` | | | `archetype` | [`Archetype`](/modules/ecs/#tecs.ecs.Archetype) | The archetype. | | `entity` | `integer` | The entity ID that moved. | | `fromRow` | `integer` | The original row position (0-based). | | `toRow` | `integer` | The new row position (0-based). | ##### Returns None. ### tecs.ecs.Bundle interface `Bundle` names a reusable component collection. ```teal interface tecs.ecs.Bundle interface Definition required: {components.Component} with: {components.Component: boolean | function(): components.Component} end name: string required: {string} defaulted: {string} spawn: function(self, ...: components.Component): integer end ``` #### tecs.ecs.Bundle.Definition interface Declarative bundle definition. ```teal interface tecs.ecs.Bundle.Definition required: {components.Component} with: {components.Component: boolean | function(): components.Component} end ``` ##### tecs.ecs.Bundle.Definition.required field Caller-writable. Components that must be provided as positional args to spawn(). ```teal tecs.ecs.Bundle.Definition.required: {components.Component} ``` ##### tecs.ecs.Bundle.Definition.with field Caller-writable. Components with defaults, keyed by component type. Value is either a factory function returning a component instance, or `true` to call the component's default constructor. ```teal tecs.ecs.Bundle.Definition.with: {components.Component: boolean | function(): components.Component} ``` #### tecs.ecs.Bundle.name field Read-only. The name of the bundle. ```teal tecs.ecs.Bundle.name: string ``` #### tecs.ecs.Bundle.required field Read-only. Component names that must be provided when spawning. ```teal tecs.ecs.Bundle.required: {string} ``` #### tecs.ecs.Bundle.defaulted field Read-only. Component names with default factories. ```teal tecs.ecs.Bundle.defaulted: {string} ``` #### tecs.ecs.Bundle:spawn Instance Reserve an entity using this bundle and stage its placement for the next pipeline barrier. ```teal function tecs.ecs.Bundle.spawn( self, ...: components.Component ): integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Bundle` | | | `...` | [`components.Component`](/modules/ecs/#tecs.ecs.Component) | Required component instances in declaration order. | ##### Returns | Type | Description | | --- | --- | | `integer` | The entity ID. | ### tecs.ecs.BundleDef interface `BundleDef` configures a bundle. ```teal interface tecs.ecs.BundleDef required: {components.Component} with: {components.Component: boolean | function(): components.Component} end ``` #### tecs.ecs.BundleDef.required field Caller-writable. Components that must be provided as positional args to spawn(). ```teal tecs.ecs.BundleDef.required: {components.Component} ``` #### tecs.ecs.BundleDef.with field Caller-writable. Components with defaults, keyed by component type. Value is either a factory function returning a component instance, or `true` to call the component's default constructor. ```teal tecs.ecs.BundleDef.with: {components.Component: boolean | function(): components.Component} ``` ### tecs.ecs.Component interface `Component` names any component definition. ```teal interface tecs.ecs.Component componentType: self componentName: string componentId: integer relationshipType: Component wildcardContainer: Component target: integer transient: boolean deserialize: function(world: World, data: {string: any}): self init: function(self, any) new: function({string: any}): self serialize: function(instance: self): {string: any} metamethod __call: function(self, ...: any): self end ``` #### tecs.ecs.Component.componentType field Read-only. The container type of the component, available on instances and containers. ```teal tecs.ecs.Component.componentType: self ``` #### tecs.ecs.Component.componentName field Read-only. The name of the component. ```teal tecs.ecs.Component.componentName: string ``` #### tecs.ecs.Component.componentId field Read-only. The auto-incrementing ID of the component container. ```teal tecs.ecs.Component.componentId: integer ``` #### tecs.ecs.Component.relationshipType field Read-only. Relationship components only: the relationship container type (nil for non-relationships). For containers, this equals self. For instances, it points to the container. ```teal tecs.ecs.Component.relationshipType: Component ``` #### tecs.ecs.Component.wildcardContainer field Read-only. Relationship instance wildcard container (nil for non-relationships and containers). For `Rel(target)` dense instances, this points back to `Rel`. ```teal tecs.ecs.Component.wildcardContainer: Component ``` #### tecs.ecs.Component.target field Read-only. Relationship components only: The target entity ID (for relationship instances only). ```teal tecs.ecs.Component.target: integer ``` #### tecs.ecs.Component.transient field Read-only. True if this component is runtime-only and omitted from snapshots. ```teal tecs.ecs.Component.transient: boolean ``` #### tecs.ecs.Component.deserialize Static Deserialize a component instance from a plain table. ```teal function tecs.ecs.Component.deserialize( world: World, data: {string: any} ): self ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | `World` | The world context for loading resources. | | `data` | `{string : any}` | The plain table data to deserialize. | ##### Returns | Type | Description | | --- | --- | | `self` | A new component instance. | #### tecs.ecs.Component.init Static Optional post-allocation positional initializer used by the built-in factories. Runs after the base instance has been allocated and any declarative field population has occurred. ```teal function tecs.ecs.Component.init(self, any) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `#1` | `self` | | | `#2` | `any` | | ##### Returns None. #### tecs.ecs.Component.new Static Build a component instance from a table of named fields. The positional `__call` form is the hot path; `new` is the ergonomic/snapshot form. ```teal function tecs.ecs.Component.new({string: any}): self ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `#1` | `{string : any}` | | ##### Returns | Type | Description | | --- | --- | | `self` | | #### tecs.ecs.Component.serialize Static Serialize a component instance to a plain table. ```teal function tecs.ecs.Component.serialize(instance: self): {string: any} ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `instance` | `self` | The component instance to serialize. | ##### Returns | Type | Description | | --- | --- | | `{string : any}` | A plain table representation of the component data. | #### tecs.ecs.Component:__call metamethod Creates an instance of the component from the container using positional arguments. ```teal metamethod tecs.ecs.Component.$meta.__call(self, ...: any): self ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Component` | | | `...` | `any` | Field values in declaration order. One left nil takes the field's registered default, so trailing arguments may be omitted. | ##### Returns | Type | Description | | --- | --- | | `self` | A new instance, not a shared one, so two calls with the same arguments answer two values. | ### tecs.ecs.ComponentOptions interface `ComponentOptions` configures a table component. ```teal interface tecs.ecs.ComponentOptions is ContainerComponentOptions fields: {string} defaults: {any} __call: function(C, any) deserialize: function(World, {string: any}): C init: function(C, any) new: function({string: any}): C serialize: function(C): {string: any} end ``` #### Interfaces | Interface | | --- | | [`ContainerComponentOptions`](/modules/ecs/#tecs.ecs.ContainerComponentOptions)`` | #### Type Parameters | Name | Constraint | Description | | --- | --- | --- | | `C` | [`Component`](/modules/ecs/#tecs.ecs.Component) | | #### tecs.ecs.ComponentOptions.fields field Caller-writable. Ordered field names for positional construction and `.new(data)`. Example: `fields = {"x", "y"}` means `Component(1, 2)` maps to `{x = 1, y = 2}` and `Component.new({x = 1, y = 2})` unpacks by the same order. The generated constructor is `load`ed so LuaJIT sees literal field assignments. ```teal tecs.ecs.ComponentOptions.fields: {string} ``` #### tecs.ecs.ComponentOptions.defaults field Caller-writable. Positional defaults, in the same order as `fields`. Use `nil` for fields that have no default. Requires `fields`. ```teal tecs.ecs.ComponentOptions.defaults: {any} ``` #### tecs.ecs.ComponentOptions.__call Static Optional custom constructor hook for the container's `__call`. When present, Tecs allocates a base instance, applies declarative `defaults`, then invokes this hook as `__call(instance, ...)`. On this path, `init` is NOT auto-run; call `Component.init(...)` explicitly from `__call` if desired. ```teal function tecs.ecs.ComponentOptions.__call(C, any) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `#1` | `C` | | | `#2` | `any` | | ##### Returns None. #### tecs.ecs.ComponentOptions.deserialize Static Custom deserialization function (optional). Reconstructs a component instance from a plain table. ```teal function tecs.ecs.ComponentOptions.deserialize(World, {string: any}): C ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `#1` | `World` | | | `#2` | `{string : any}` | | ##### Returns | Type | Description | | --- | --- | | `C` | | #### tecs.ecs.ComponentOptions.init Static Optional post-allocation positional initializer. Runs after the base instance has been allocated and any declarative `fields` / `defaults` have been applied. Use for validation, normalization, or derived state. If provided without `fields` or `new`, registration errors because Tecs would have no clear way to implement `.new(data)`. ```teal function tecs.ecs.ComponentOptions.init(C, any) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `#1` | `C` | | | `#2` | `any` | | ##### Returns None. #### tecs.ecs.ComponentOptions.new Static Optional table-form constructor, called as `Component.new(data)`. When `fields` are present, Tecs generates this automatically by unpacking named fields and routing through `__call`. ```teal function tecs.ecs.ComponentOptions.new({string: any}): C ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `#1` | `{string : any}` | | ##### Returns | Type | Description | | --- | --- | | `C` | | #### tecs.ecs.ComponentOptions.serialize Static Custom serialization function (optional). Converts a component instance to a plain table for JSON serialization. ```teal function tecs.ecs.ComponentOptions.serialize(C): {string: any} ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `#1` | `C` | | ##### Returns | Type | Description | | --- | --- | | `{string : any}` | | ### tecs.ecs.ContainerComponentOptions interface Shared options for creating different components. ```teal interface tecs.ecs.ContainerComponentOptions is BasicComponentOptions container: C end ``` #### Interfaces | Interface | | --- | | `BasicComponentOptions` | #### Type Parameters | Name | Constraint | Description | | --- | --- | --- | | `C` | [`Component`](/modules/ecs/#tecs.ecs.Component) | | #### tecs.ecs.ContainerComponentOptions.container field Caller-writable. The component container/type to use (required). ```teal tecs.ecs.ContainerComponentOptions.container: C ``` ### tecs.ecs.DoubleArray interface `DoubleArray` names an FFI array of doubles. ```teal interface tecs.ecs.DoubleArray is {integer} metamethod __len: function(self) end ``` #### Interfaces | Interface | | --- | | `{integer}` | #### tecs.ecs.DoubleArray:__len metamethod Reads the count out of slot 0 rather than probing for a border, so `#array` is exact even though slot 0 is occupied and would otherwise make the array look empty to Lua's own length operator. A macro, so it costs one load at the call site. ```teal metamethod tecs.ecs.DoubleArray.$meta.__len(self) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `DoubleArray` | | ##### Returns None. ### tecs.ecs.FFIComponentOptions interface `FFIComponentOptions` configures an FFI component. ```teal interface tecs.ecs.FFIComponentOptions is ContainerComponentOptions fields: {{string, string}} defaults: {any} metatable: {any: any} __call: function(C, any) deserialize: function(World, {string: any}): C init: function(C, any) new: function({string: any}): C serialize: function(C): {string: any} end ``` #### Interfaces | Interface | | --- | | [`ContainerComponentOptions`](/modules/ecs/#tecs.ecs.ContainerComponentOptions)`` | #### Type Parameters | Name | Constraint | Description | | --- | --- | --- | | `C` | [`Component`](/modules/ecs/#tecs.ecs.Component) | | #### tecs.ecs.FFIComponentOptions.fields field Caller-writable. FFI field definitions for the backing struct. ```teal tecs.ecs.FFIComponentOptions.fields: {{string, string}} ``` #### tecs.ecs.FFIComponentOptions.defaults field Caller-writable. Positional defaults, in the same order as `fields`. ```teal tecs.ecs.FFIComponentOptions.defaults: {any} ``` #### tecs.ecs.FFIComponentOptions.metatable field Caller-writable. Optional metatable to apply to FFI instances (for instance methods). ```teal tecs.ecs.FFIComponentOptions.metatable: {any: any} ``` #### tecs.ecs.FFIComponentOptions.__call Static Optional custom constructor hook. Same semantics as `ComponentOptions.__call`, but the base instance is an FFI struct. ```teal function tecs.ecs.FFIComponentOptions.__call(C, any) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `#1` | `C` | | | `#2` | `any` | | ##### Returns None. #### tecs.ecs.FFIComponentOptions.deserialize Static Custom deserialization function (optional). Reconstructs a component instance from a plain table. Typically reruns the same construction path a fresh spawn would take so the restored instance gets valid runtime state. ```teal function tecs.ecs.FFIComponentOptions.deserialize( World, {string: any} ): C ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `#1` | `World` | | | `#2` | `{string : any}` | | ##### Returns | Type | Description | | --- | --- | | `C` | | #### tecs.ecs.FFIComponentOptions.init Static Optional post-allocation positional initializer. Same semantics as `ComponentOptions.init`, but the base instance is an FFI struct. ```teal function tecs.ecs.FFIComponentOptions.init(C, any) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `#1` | `C` | | | `#2` | `any` | | ##### Returns None. #### tecs.ecs.FFIComponentOptions.new Static Optional table-form constructor. Defaults to unpacking `fields` by name into positional args and routing through `__call`. ```teal function tecs.ecs.FFIComponentOptions.new({string: any}): C ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `#1` | `{string : any}` | | ##### Returns | Type | Description | | --- | --- | | `C` | | #### tecs.ecs.FFIComponentOptions.serialize Static Custom serialization function (optional). Converts a component instance to a plain table for JSON serialization. Return `nil` to omit the component from snapshots. ```teal function tecs.ecs.FFIComponentOptions.serialize(C): {string: any} ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `#1` | `C` | | ##### Returns | Type | Description | | --- | --- | | `{string : any}` | | ### tecs.ecs.FFIRelationshipOptions interface `FFIRelationshipOptions` configures an FFI relationship. ```teal interface tecs.ecs.FFIRelationshipOptions is BaseRelationshipOptions, FFIComponentOptions end ``` #### Interfaces | Interface | | --- | | `BaseRelationshipOptions` | | [`FFIComponentOptions`](/modules/ecs/#tecs.ecs.FFIComponentOptions)`` | #### Type Parameters | Name | Constraint | Description | | --- | --- | --- | | `R` | [`Relationship`](/modules/ecs/#tecs.ecs.Relationship) | | ### tecs.ecs.FinishSnapshotLoad record An event emitted on entity 0 once `tecs.loadSnapshot` finishes -- after all entities are restored AND every data callback has run. Useful for cleanup or "load is complete" callbacks. ```teal record tecs.ecs.FinishSnapshotLoad is types.events.Event prelude: any metamethod __call: function(self): FinishSnapshotLoad end ``` #### Interfaces | Interface | | --- | | [`types.events.Event`](/modules/events/#tecs.events.Event) | #### tecs.ecs.FinishSnapshotLoad.prelude field Engine-owned. The snapshot prelude (version, counts). ```teal tecs.ecs.FinishSnapshotLoad.prelude: any ``` #### tecs.ecs.FinishSnapshotLoad:__call metamethod ```teal metamethod tecs.ecs.FinishSnapshotLoad.$meta.__call( self ): FinishSnapshotLoad ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `FinishSnapshotLoad` | | ##### Returns | Type | Description | | --- | --- | | [`FinishSnapshotLoad`](/modules/ecs/#tecs.ecs.FinishSnapshotLoad) | | ### tecs.ecs.FixedOverload enum `FixedOverload` selects fixed-step overload behavior. ```teal enum tecs.ecs.FixedOverload "accumulate" "drop" end ``` ### tecs.ecs.OnDespawn record An event emitted when a specific entity is despawned. ```teal record tecs.ecs.OnDespawn is types.events.Event entity: integer metamethod __call: function(self, integer): OnDespawn end ``` #### Interfaces | Interface | | --- | | [`types.events.Event`](/modules/events/#tecs.events.Event) | #### tecs.ecs.OnDespawn.entity field Engine-owned. Reports this public value. ```teal tecs.ecs.OnDespawn.entity: integer ``` #### tecs.ecs.OnDespawn:__call metamethod Create a new OnDespawn event. ```teal metamethod tecs.ecs.OnDespawn.$meta.__call(self, integer): OnDespawn ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `OnDespawn` | | | `#2` | `integer` | | ##### Returns | Type | Description | | --- | --- | | [`OnDespawn`](/modules/ecs/#tecs.ecs.OnDespawn) | | ### tecs.ecs.OnSnapshotSave record An event emitted on entity 0 at the start of `tecs.saveSnapshot`, BEFORE archetype data is written. Listeners can: - Attach arbitrary keyed metadata via `ev:addData(key, value)` (queued; flushed into the data section after archetypes). - Mark plugin-derived entities for omission via `ev:exclude(component)`. Entities carrying any excluded component are skipped entirely; on load the plugin re-derives them from durable source-of-truth entities that ARE saved. ```teal record tecs.ecs.OnSnapshotSave is types.events.Event addData: function(self, string, any) exclude: function(self, types.components.Component) metamethod __call: function(self): OnSnapshotSave end ``` #### Interfaces | Interface | | --- | | [`types.events.Event`](/modules/events/#tecs.events.Event) | #### tecs.ecs.OnSnapshotSave:addData Instance Encode a (key, value) pair into the snapshot's data section. Keys MUST be strings; values must be `string.buffer`-encodable. Plugins typically use namespaced keys (e.g. `"tecs.physics"`, `"mygame.scoreboard"`) to avoid collisions. ```teal function tecs.ecs.OnSnapshotSave.addData(self, string, any) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `OnSnapshotSave` | | | `#2` | `string` | | | `#3` | `any` | | ##### Returns None. #### tecs.ecs.OnSnapshotSave:exclude Instance Exclude every entity that carries `component` from the snapshot. Use this when a plugin owns derived state (e.g. TileChunk projection of a Tilemap, physics body backing a RigidBody marker) and can re-spawn those entities on load from a durable source component that IS saved. ```teal function tecs.ecs.OnSnapshotSave.exclude( self, types.components.Component ) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `OnSnapshotSave` | | | `#2` | [`types.components.Component`](/modules/ecs/#tecs.ecs.Component) | | ##### Returns None. #### tecs.ecs.OnSnapshotSave:__call metamethod ```teal metamethod tecs.ecs.OnSnapshotSave.$meta.__call(self): OnSnapshotSave ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `OnSnapshotSave` | | ##### Returns | Type | Description | | --- | --- | | [`OnSnapshotSave`](/modules/ecs/#tecs.ecs.OnSnapshotSave) | | ### tecs.ecs.OnSpawn record An event emitted when a specific entity is spawned. ```teal record tecs.ecs.OnSpawn is types.events.Event entity: integer metamethod __call: function(self, integer): OnSpawn end ``` #### Interfaces | Interface | | --- | | [`types.events.Event`](/modules/events/#tecs.events.Event) | #### tecs.ecs.OnSpawn.entity field Engine-owned. Reports this public value. ```teal tecs.ecs.OnSpawn.entity: integer ``` #### tecs.ecs.OnSpawn:__call metamethod Create a new OnSpawn event. ```teal metamethod tecs.ecs.OnSpawn.$meta.__call(self, integer): OnSpawn ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `OnSpawn` | | | `#2` | `integer` | | ##### Returns | Type | Description | | --- | --- | | [`OnSpawn`](/modules/ecs/#tecs.ecs.OnSpawn) | | ### tecs.ecs.Phase interface `Phase` identifies one system phase. ```teal interface tecs.ecs.Phase name: string position: integer children: {Phase} end ``` #### tecs.ecs.Phase.name field Read-only. The name of the phase. ```teal tecs.ecs.Phase.name: string ``` #### tecs.ecs.Phase.position field Read-only. The public pipeline position for this phase. ```teal tecs.ecs.Phase.position: integer ``` #### tecs.ecs.Phase.children field Read-only. Child phases of this phase. ```teal tecs.ecs.Phase.children: {Phase} ``` ### tecs.ecs.phases record Defines the predefined ECS phases, or lifecycle states, of the game and event loop. Tecs provides these standard phases but also supports custom pipelines via World.Config.pipelineFactory. Read-only. `phases` contains the phases a system may join. ```teal record tecs.ecs.phases record AllGroups is Phase end record MainGroup is Phase end record PreStartup is Phase end record Startup is Phase end record PostStartup is Phase end record StartupGroup is Phase end record Ingress is Phase end record First is Phase end record PreUpdate is Phase end record FixedFirst is Phase end record FixedPreUpdate is Phase end record FixedUpdate is Phase end record FixedPostUpdate is Phase end record FixedLast is Phase end record FixedUpdateGroup is Phase end record Update is Phase end record PostUpdate is Phase end record RenderFirst is Phase end record PreRender is Phase end record Render is Phase end record PostRender is Phase end record RenderLast is Phase end record RenderGroup is Phase end record Last is Phase end record PreShutdown is Phase end record Shutdown is Phase end record PostShutdown is Phase end record ShutdownGroup is Phase end index: {Phase} end ``` #### tecs.ecs.phases.AllGroups record Contains all phases that make up the ECS lifecycle. ```teal record tecs.ecs.phases.AllGroups is Phase end ``` ##### Interfaces | Interface | | --- | | [`Phase`](/modules/ecs/#tecs.ecs.Phase) | #### tecs.ecs.phases.MainGroup record A meta-phase for the main game loop. ```teal record tecs.ecs.phases.MainGroup is Phase end ``` ##### Interfaces | Interface | | --- | | [`Phase`](/modules/ecs/#tecs.ecs.Phase) | #### tecs.ecs.phases.PreStartup record Runs once at application start for critical initialization (e.g., logging, core systems). ```teal record tecs.ecs.phases.PreStartup is Phase end ``` ##### Interfaces | Interface | | --- | | [`Phase`](/modules/ecs/#tecs.ecs.Phase) | #### tecs.ecs.phases.Startup record Runs once at application start for general initialization (e.g., loading assets, creating entities). ```teal record tecs.ecs.phases.Startup is Phase end ``` ##### Interfaces | Interface | | --- | | [`Phase`](/modules/ecs/#tecs.ecs.Phase) | #### tecs.ecs.phases.PostStartup record Runs once after startup for final setup (e.g., starting gameplay, enabling systems). ```teal record tecs.ecs.phases.PostStartup is Phase end ``` ##### Interfaces | Interface | | --- | | [`Phase`](/modules/ecs/#tecs.ecs.Phase) | #### tecs.ecs.phases.StartupGroup record ```teal record tecs.ecs.phases.StartupGroup is Phase end ``` ##### Interfaces | Interface | | --- | | [`Phase`](/modules/ecs/#tecs.ecs.Phase) | #### tecs.ecs.phases.Ingress record Runs once at the start of a logical update for sealed platform and external-service input. Observers run here in event sequence order. ```teal record tecs.ecs.phases.Ingress is Phase end ``` ##### Interfaces | Interface | | --- | | [`Phase`](/modules/ecs/#tecs.ecs.Phase) | #### tecs.ecs.phases.First record Runs at the very start of each frame (e.g., time updates, input polling). ```teal record tecs.ecs.phases.First is Phase end ``` ##### Interfaces | Interface | | --- | | [`Phase`](/modules/ecs/#tecs.ecs.Phase) | #### tecs.ecs.phases.PreUpdate record Runs before the main update (e.g., physics preparation, event processing). ```teal record tecs.ecs.phases.PreUpdate is Phase end ``` ##### Interfaces | Interface | | --- | | [`Phase`](/modules/ecs/#tecs.ecs.Phase) | #### tecs.ecs.phases.FixedFirst record Runs at the start of each fixed timestep iteration (e.g., fixed timer updates). ```teal record tecs.ecs.phases.FixedFirst is Phase end ``` ##### Interfaces | Interface | | --- | | [`Phase`](/modules/ecs/#tecs.ecs.Phase) | #### tecs.ecs.phases.FixedPreUpdate record Runs before fixed update (e.g., collision detection preparation). ```teal record tecs.ecs.phases.FixedPreUpdate is Phase end ``` ##### Interfaces | Interface | | --- | | [`Phase`](/modules/ecs/#tecs.ecs.Phase) | #### tecs.ecs.phases.FixedUpdate record Runs at fixed timestep intervals for deterministic updates (e.g., physics simulation, gameplay logic). ```teal record tecs.ecs.phases.FixedUpdate is Phase end ``` ##### Interfaces | Interface | | --- | | [`Phase`](/modules/ecs/#tecs.ecs.Phase) | #### tecs.ecs.phases.FixedPostUpdate record Runs after fixed update (e.g., collision response, constraint solving). ```teal record tecs.ecs.phases.FixedPostUpdate is Phase end ``` ##### Interfaces | Interface | | --- | | [`Phase`](/modules/ecs/#tecs.ecs.Phase) | #### tecs.ecs.phases.FixedLast record Runs at the end of each fixed timestep iteration (e.g., state synchronization). ```teal record tecs.ecs.phases.FixedLast is Phase end ``` ##### Interfaces | Interface | | --- | | [`Phase`](/modules/ecs/#tecs.ecs.Phase) | #### tecs.ecs.phases.FixedUpdateGroup record ```teal record tecs.ecs.phases.FixedUpdateGroup is Phase end ``` ##### Interfaces | Interface | | --- | | [`Phase`](/modules/ecs/#tecs.ecs.Phase) | #### tecs.ecs.phases.Update record Main update phase, runs once per frame for gameplay logic and non-physics updates. ```teal record tecs.ecs.phases.Update is Phase end ``` ##### Interfaces | Interface | | --- | | [`Phase`](/modules/ecs/#tecs.ecs.Phase) | #### tecs.ecs.phases.PostUpdate record Runs after update and before rendering (e.g., animation, transform updates, camera updates). ```teal record tecs.ecs.phases.PostUpdate is Phase end ``` ##### Interfaces | Interface | | --- | | [`Phase`](/modules/ecs/#tecs.ecs.Phase) | #### tecs.ecs.phases.RenderFirst record Runs at the start of rendering (e.g., clearing buffers, setting up render state). ```teal record tecs.ecs.phases.RenderFirst is Phase end ``` ##### Interfaces | Interface | | --- | | [`Phase`](/modules/ecs/#tecs.ecs.Phase) | #### tecs.ecs.phases.PreRender record Runs just before rendering (e.g., culling, sorting, batching). ```teal record tecs.ecs.phases.PreRender is Phase end ``` ##### Interfaces | Interface | | --- | | [`Phase`](/modules/ecs/#tecs.ecs.Phase) | #### tecs.ecs.phases.Render record Main rendering phase (e.g., drawing sprites, meshes, UI). ```teal record tecs.ecs.phases.Render is Phase end ``` ##### Interfaces | Interface | | --- | | [`Phase`](/modules/ecs/#tecs.ecs.Phase) | #### tecs.ecs.phases.PostRender record Runs just after rendering (e.g., post-processing effects, screen transitions). ```teal record tecs.ecs.phases.PostRender is Phase end ``` ##### Interfaces | Interface | | --- | | [`Phase`](/modules/ecs/#tecs.ecs.Phase) | #### tecs.ecs.phases.RenderLast record Runs at the end of rendering (e.g., presenting frame, GPU synchronization). ```teal record tecs.ecs.phases.RenderLast is Phase end ``` ##### Interfaces | Interface | | --- | | [`Phase`](/modules/ecs/#tecs.ecs.Phase) | #### tecs.ecs.phases.RenderGroup record ```teal record tecs.ecs.phases.RenderGroup is Phase end ``` ##### Interfaces | Interface | | --- | | [`Phase`](/modules/ecs/#tecs.ecs.Phase) | #### tecs.ecs.phases.Last record Runs at the very end of each frame (e.g., cleanup, metrics collection, frame timing). ```teal record tecs.ecs.phases.Last is Phase end ``` ##### Interfaces | Interface | | --- | | [`Phase`](/modules/ecs/#tecs.ecs.Phase) | #### tecs.ecs.phases.PreShutdown record Runs just before shutdown (e.g., saving game state, closing connections). ```teal record tecs.ecs.phases.PreShutdown is Phase end ``` ##### Interfaces | Interface | | --- | | [`Phase`](/modules/ecs/#tecs.ecs.Phase) | #### tecs.ecs.phases.Shutdown record Main shutdown phase (e.g., releasing resources, destroying entities). ```teal record tecs.ecs.phases.Shutdown is Phase end ``` ##### Interfaces | Interface | | --- | | [`Phase`](/modules/ecs/#tecs.ecs.Phase) | #### tecs.ecs.phases.PostShutdown record Runs after shutdown for final cleanup (e.g., logging shutdown metrics, final cleanup). ```teal record tecs.ecs.phases.PostShutdown is Phase end ``` ##### Interfaces | Interface | | --- | | [`Phase`](/modules/ecs/#tecs.ecs.Phase) | #### tecs.ecs.phases.ShutdownGroup record ```teal record tecs.ecs.phases.ShutdownGroup is Phase end ``` ##### Interfaces | Interface | | --- | | [`Phase`](/modules/ecs/#tecs.ecs.Phase) | #### tecs.ecs.phases.index field Read-only. Maps each lifecycle position to its phase definition. ```teal tecs.ecs.phases.index: {Phase} ``` ### tecs.ecs.Pipeline interface `Pipeline` identifies a world update pipeline. ```teal interface tecs.ecs.Pipeline count: integer fixedTimestep: number fixedAccumulator: number fixedStepCount: integer fixedMaxSteps: integer fixedOverload: FixedOverload fixedTimeDropped: number fixedStepsDropped: integer addSystem: function(self, config: SystemConfig) disablePhase: function(self, phase: Phase) enablePhase: function(self, phase: Phase) listSystems: function(self): {SystemInfo} registerPhase: function(self, phase: Phase) removeSystem: function(self, systemName: string) run: function(self, phase: Phase, dt: number, world: World) setSystemEnabled: function( self, systemName: string, enabled: boolean ): boolean, string update: function(self, dt: number, world: World) end ``` #### tecs.ecs.Pipeline.count field Read-only. The number of systems in the pipeline. ```teal tecs.ecs.Pipeline.count: integer ``` #### tecs.ecs.Pipeline.fixedTimestep field Read-only. The fixed timestep interval in seconds (e.g., 1/60 for 60Hz physics). ```teal tecs.ecs.Pipeline.fixedTimestep: number ``` #### tecs.ecs.Pipeline.fixedAccumulator field Read-only. Accumulated time not yet consumed by fixed updates. Use with fixedTimestep to compute interpolation alpha: accumulator / timestep. ```teal tecs.ecs.Pipeline.fixedAccumulator: number ``` #### tecs.ecs.Pipeline.fixedStepCount field Read-only. Fixed steps run since the pipeline was made. ```teal tecs.ecs.Pipeline.fixedStepCount: integer ``` #### tecs.ecs.Pipeline.fixedMaxSteps field Read-only. The most fixed steps one `update` will run before the overload policy decides what happens to the rest. Defaults to 10. ```teal tecs.ecs.Pipeline.fixedMaxSteps: integer ``` #### tecs.ecs.Pipeline.fixedOverload field Read-only. What happens to the time those steps would have consumed. Defaults to `"drop"`. ```teal tecs.ecs.Pipeline.fixedOverload: FixedOverload ``` #### tecs.ecs.Pipeline.fixedTimeDropped field Read-only. Simulated seconds abandoned by the `"drop"` policy since the pipeline was made. Stays at zero under `"accumulate"`, which abandons nothing, and stays at zero on a machine that keeps up. Read it. A simulation, a replay or anything networked is wrong by exactly this much, and nothing else in the world says so. ```teal tecs.ecs.Pipeline.fixedTimeDropped: number ``` #### tecs.ecs.Pipeline.fixedStepsDropped field Read-only. Fixed steps that were owed and never ran, on the same terms. Whole steps only: `fixedTimeDropped` is this many timesteps, because a drop leaves the sub-step remainder alone rather than resetting the interpolation alpha along with it. ```teal tecs.ecs.Pipeline.fixedStepsDropped: integer ``` #### tecs.ecs.Pipeline:addSystem Instance Add a system to the pipeline. ```teal function tecs.ecs.Pipeline.addSystem(self, config: SystemConfig) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Pipeline` | | | `config` | `SystemConfig` | The system configuration. | ##### Returns None. #### tecs.ecs.Pipeline:disablePhase Instance Disable a phase. ```teal function tecs.ecs.Pipeline.disablePhase(self, phase: Phase) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Pipeline` | | | `phase` | [`Phase`](/modules/ecs/#tecs.ecs.Phase) | The phase to disable. | ##### Returns None. #### tecs.ecs.Pipeline:enablePhase Instance Enable a phase. ```teal function tecs.ecs.Pipeline.enablePhase(self, phase: Phase) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Pipeline` | | | `phase` | [`Phase`](/modules/ecs/#tecs.ecs.Phase) | The phase to enable. | ##### Returns None. #### tecs.ecs.Pipeline:listSystems Instance Reports every system the pipeline holds. ```teal function tecs.ecs.Pipeline.listSystems(self): {SystemInfo} ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Pipeline` | | ##### Returns | Type | Description | | --- | --- | | `{SystemInfo}` | A fresh list the caller owns, ordered by phase in the order the lifecycle reaches each phase, and within a phase in the order the systems run. | #### tecs.ecs.Pipeline:registerPhase Instance Register a custom phase with the pipeline. ```teal function tecs.ecs.Pipeline.registerPhase(self, phase: Phase) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Pipeline` | | | `phase` | [`Phase`](/modules/ecs/#tecs.ecs.Phase) | The phase to register. | ##### Returns None. #### tecs.ecs.Pipeline:removeSystem Instance Remove a system from the pipeline. ```teal function tecs.ecs.Pipeline.removeSystem(self, systemName: string) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Pipeline` | | | `systemName` | `string` | The name of the system to remove. | ##### Returns None. #### tecs.ecs.Pipeline:run Instance Run a specific phase. ```teal function tecs.ecs.Pipeline.run( self, phase: Phase, dt: number, world: World ) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Pipeline` | | | `phase` | [`Phase`](/modules/ecs/#tecs.ecs.Phase) | The phase to run. | | `dt` | `number` | The delta time since the last update. | | `world` | `World` | The world to run the phase on. | ##### Returns None. #### tecs.ecs.Pipeline:setSystemEnabled Instance Enables or disables one system, leaving it registered either way. ```teal function tecs.ecs.Pipeline.setSystemEnabled( self, systemName: string, enabled: boolean ): boolean, string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Pipeline` | | | `systemName` | `string` | The name the system is registered under, which `listSystems` reports. | | `enabled` | `boolean` | True lets the system run again, and false skips it from the next update until something enables it. | ##### Returns | Type | Description | | --- | --- | | `boolean` | True once the system carries that state, including when it already did. | | `string` | The reason no system carries that name, when the first return is false. | #### tecs.ecs.Pipeline:update Instance Update the pipeline. ```teal function tecs.ecs.Pipeline.update(self, dt: number, world: World) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Pipeline` | | | `dt` | `number` | The delta time since the last update. | | `world` | `World` | The world to update. | ##### Returns None. ### tecs.Plugin type A plugin configures a world with systems, resources, observers, and initial entities. ```teal type tecs.Plugin = function(World) ``` ### tecs.Query interface A reusable filter over the archetypes in a world. ```teal interface tecs.Query enum Kind "logic" "render" end interface Descriptor name: string type: Kind include: {components.Component} includeAny: {components.Component} exclude: {components.Component} temp: boolean groupBy: function(archetype: Archetype): integer onEntitiesAdded: function( archetype: Archetype, firstRow: integer, lastRow: integer, count: integer ) onEntitiesRemoved: function( archetype: Archetype, firstRow: integer, lastRow: integer, count: integer ) end type IterFn = function(Query, any): (Archetype, integer, {integer}) type GroupsIterFn = function(Query, any): integer type GroupIterFn = function( Query, any ): (Archetype, integer, {integer}) descriptor: Descriptor count: function(self): integer getGroup: function(self, archetype: Archetype): integer getGroupCount: function(self, groupId: integer): integer group: function(self, groupId: integer): GroupIterFn, Query, any groups: function(self): GroupsIterFn, Query, any iter: function(self): IterFn, Query, any end ``` #### tecs.Query.Kind enum Whether a query drives simulation or presentation. ```teal enum tecs.Query.Kind "logic" "render" end ``` #### tecs.Query.Descriptor interface The options passed to `world:newQuery`. ```teal interface tecs.Query.Descriptor name: string type: Kind include: {components.Component} includeAny: {components.Component} exclude: {components.Component} temp: boolean groupBy: function(archetype: Archetype): integer onEntitiesAdded: function( archetype: Archetype, firstRow: integer, lastRow: integer, count: integer ) onEntitiesRemoved: function( archetype: Archetype, firstRow: integer, lastRow: integer, count: integer ) end ``` ##### tecs.Query.Descriptor.name field Caller-writable. Names the query in logs, profiles, and debug tools. ```teal tecs.Query.Descriptor.name: string ``` ##### tecs.Query.Descriptor.type field Caller-writable. Selects simulation or presentation behavior. `"logic"` excludes `Paused` entities, so pausing a state stops the systems that move, damage, or think. `"render"` records the opposite intent: paused entities keep drawing. Omitting the field leaves the query unfiltered, which matches every query written before this option existed. ```teal tecs.Query.Descriptor.type: Kind ``` ##### tecs.Query.Descriptor.include field Caller-writable. Lists every component a matching archetype must contain. ```teal tecs.Query.Descriptor.include: {components.Component} ``` ##### tecs.Query.Descriptor.includeAny field Caller-writable. Lists components of which a matching archetype must contain at least one. ```teal tecs.Query.Descriptor.includeAny: {components.Component} ``` ##### tecs.Query.Descriptor.exclude field Caller-writable. Lists every component a matching archetype must omit. ```teal tecs.Query.Descriptor.exclude: {components.Component} ``` ##### tecs.Query.Descriptor.temp field Caller-writable. Set true to match only the archetypes that exist when the query is created. ```teal tecs.Query.Descriptor.temp: boolean ``` ##### tecs.Query.Descriptor.groupBy Static Caller-writable. Assigns an integer group to each archetype. Archetypes with the same group are iterated contiguously. Use with groups() and group(id) for efficient grouped iteration. ```teal function tecs.Query.Descriptor.groupBy(archetype: Archetype): integer ``` ###### Arguments | Name | Type | Description | | --- | --- | --- | | `archetype` | [`Archetype`](/modules/ecs/#tecs.ecs.Archetype) | The archetype to classify. | ###### Returns | Type | Description | | --- | --- | | `integer` | The group identifier. | ##### tecs.Query.Descriptor.onEntitiesAdded Static Caller-writable. Receives each contiguous range of entities that enters the query. ```teal function tecs.Query.Descriptor.onEntitiesAdded( archetype: Archetype, firstRow: integer, lastRow: integer, count: integer ) ``` ###### Arguments | Name | Type | Description | | --- | --- | --- | | `archetype` | [`Archetype`](/modules/ecs/#tecs.ecs.Archetype) | The archetype that contains the range. | | `firstRow` | `integer` | The first row in the range. | | `lastRow` | `integer` | The last row in the range. | | `count` | `integer` | The number of rows in the range. | ###### Returns None. ##### tecs.Query.Descriptor.onEntitiesRemoved Static Caller-writable. Receives each contiguous range of entities that leaves the query. ```teal function tecs.Query.Descriptor.onEntitiesRemoved( archetype: Archetype, firstRow: integer, lastRow: integer, count: integer ) ``` ###### Arguments | Name | Type | Description | | --- | --- | --- | | `archetype` | [`Archetype`](/modules/ecs/#tecs.ecs.Archetype) | The archetype that contains the range. | | `firstRow` | `integer` | The first row in the range. | | `lastRow` | `integer` | The last row in the range. | | `count` | `integer` | The number of rows in the range. | ###### Returns None. #### tecs.Query.IterFn type The step function `query:iter()` returns. Yields the archetype, its live row count, and its entity id column; the entity column is the archetype's own, not a copy, and is only valid until the world reaches its next publication barrier. ```teal type tecs.Query.IterFn = function( Query, any ): (Archetype, integer, {integer}) ``` #### tecs.Query.GroupsIterFn type The step function `query:groups()` returns. Yields group ids in ascending order. ```teal type tecs.Query.GroupsIterFn = function(Query, any): integer ``` #### tecs.Query.GroupIterFn type The step function `query:group(id)` returns, with the same three values as `IterFn` narrowed to one group. ```teal type tecs.Query.GroupIterFn = function( Query, any ): (Archetype, integer, {integer}) ``` #### tecs.Query.descriptor field Read-only. Returns the descriptor used to create the query. Exposed for inspection. Mutating it after query construction does not rebuild component masks, subscriptions, or grouping state. ```teal tecs.Query.descriptor: Descriptor ``` #### tecs.Query:count Instance Total number of entities currently matched by this query. One pass over matching archetypes, not entities. ```teal function tecs.Query.count(self): integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Query` | | ##### Returns | Type | Description | | --- | --- | | `integer` | | #### tecs.Query:getGroup Instance Returns the cached group identifier for an archetype. Only available when `groupBy` is specified. Returns nil when the archetype does not match this query. ```teal function tecs.Query.getGroup(self, archetype: Archetype): integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Query` | | | `archetype` | [`Archetype`](/modules/ecs/#tecs.ecs.Archetype) | The archetype to inspect. | ##### Returns | Type | Description | | --- | --- | | `integer` | Its cached group identifier, or nil when it has none. | #### tecs.Query:getGroupCount Instance Returns the total entity count for a group. ```teal function tecs.Query.getGroupCount(self, groupId: integer): integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Query` | | | `groupId` | `integer` | The group to count. | ##### Returns | Type | Description | | --- | --- | | `integer` | Its entity count, or zero when the group does not exist. | #### tecs.Query:group Instance Iterate over archetypes in a specific group. Only available when groupBy is specified. Usage: `for archetype, len, entities in query:group(id) do ... end`. Yields nothing if the group has no archetypes. ```teal function tecs.Query.group( self, groupId: integer ): GroupIterFn, Query, any ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Query` | | | `groupId` | `integer` | The group to traverse. | ##### Returns | Type | Description | | --- | --- | | [`GroupIterFn`](/modules/ecs/#tecs.Query.GroupIterFn) | The query's grouped step function. | | [`Query`](/modules/ecs/#tecs.Query) | This query. | | `any` | The iterator's initial control value. | #### tecs.Query:groups Instance Iterate over active group IDs in sorted order. Only available when groupBy is specified. Usage: `for blendId in query:groups() do ... end`. ```teal function tecs.Query.groups(self): GroupsIterFn, Query, any ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Query` | | ##### Returns | Type | Description | | --- | --- | | [`GroupsIterFn`](/modules/ecs/#tecs.Query.GroupsIterFn) | | | [`Query`](/modules/ecs/#tecs.Query) | | | `any` | | #### tecs.Query:iter Instance Explicit iterator protocol form. Returns (archetype, length, entities) for each matching archetype. Use `archetype:get(Component)` or `archetype:getMut(Component)` to retrieve component data. Usage: `for archetype, length, entities in query:iter() do ... end` ```teal function tecs.Query.iter(self): IterFn, Query, any ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Query` | | ##### Returns | Type | Description | | --- | --- | | [`IterFn`](/modules/ecs/#tecs.Query.IterFn) | The three values a generic `for` takes. The `entities` table and columns are the archetype's own and remain valid only until the next barrier that changes that archetype. | | [`Query`](/modules/ecs/#tecs.Query) | | | `any` | | ### tecs.ecs.QueryDescriptor type `QueryDescriptor` configures a query. ```teal type tecs.ecs.QueryDescriptor = types.Query.Descriptor ``` ### tecs.ecs.Relationship interface `Relationship` names any relationship definition. ```teal interface tecs.ecs.Relationship is Component exclusiveRelationship: boolean targeting: function(self, integer): self end ``` #### Interfaces | Interface | | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | #### tecs.ecs.Relationship.exclusiveRelationship field Read-only. Component container property indicating that the relationship is exclusive (e.g., has-one). ```teal tecs.ecs.Relationship.exclusiveRelationship: boolean ``` #### tecs.ecs.Relationship:targeting Instance Returns the component type for a specific target. ```teal function tecs.ecs.Relationship.targeting(self, integer): self ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Relationship` | | | `#2` | `integer` | | ##### Returns | Type | Description | | --- | --- | | `self` | | ### tecs.ecs.RelationshipOptions interface `RelationshipOptions` configures a relationship. ```teal interface tecs.ecs.RelationshipOptions is BaseRelationshipOptions, ComponentOptions end ``` #### Interfaces | Interface | | --- | | `BaseRelationshipOptions` | | [`ComponentOptions`](/modules/ecs/#tecs.ecs.ComponentOptions)`` | #### Type Parameters | Name | Constraint | Description | | --- | --- | --- | | `R` | [`Relationship`](/modules/ecs/#tecs.ecs.Relationship) | | ### tecs.ecs.RelativeTransform2D record A component that defines an entity's transform relative to its parent (ChildOf). The relative transform system computes the world-space Transform2D by composing the parent's Transform2D with this RelativeTransform2D's offset data. Note: Requires ChildOf to be present. The parent is determined by ChildOf.target. ```teal record tecs.ecs.RelativeTransform2D is Component x: number y: number z: number rotation: number scaleX: number scaleY: number originX: number originY: number new: function({string: any}): RelativeTransform2D metamethod __call: function( self, x: number, y: number, z: number, rotation: number, scaleX: number, scaleY: number, originX: number, originY: number ): RelativeTransform2D end ``` #### Interfaces | Interface | | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | #### tecs.ecs.RelativeTransform2D.x field Caller-writable. The x offset from the parent. ```teal tecs.ecs.RelativeTransform2D.x: number ``` #### tecs.ecs.RelativeTransform2D.y field Caller-writable. The y offset from the parent. ```teal tecs.ecs.RelativeTransform2D.y: number ``` #### tecs.ecs.RelativeTransform2D.z field Caller-writable. The z offset from the parent. ```teal tecs.ecs.RelativeTransform2D.z: number ``` #### tecs.ecs.RelativeTransform2D.rotation field Caller-writable. The rotation offset in radians from the parent. ```teal tecs.ecs.RelativeTransform2D.rotation: number ``` #### tecs.ecs.RelativeTransform2D.scaleX field Caller-writable. The x scale multiplier relative to the parent. ```teal tecs.ecs.RelativeTransform2D.scaleX: number ``` #### tecs.ecs.RelativeTransform2D.scaleY field Caller-writable. The y scale multiplier relative to the parent. ```teal tecs.ecs.RelativeTransform2D.scaleY: number ``` #### tecs.ecs.RelativeTransform2D.originX field Caller-writable. The origin X as a percentage (0-1) of the entity's width. 0 = left edge, 0.5 = center, 1 = right edge. Defaults to 0 (left edge). ```teal tecs.ecs.RelativeTransform2D.originX: number ``` #### tecs.ecs.RelativeTransform2D.originY field Caller-writable. The origin Y as a percentage (0-1) of the entity's height. 0 = top edge, 0.5 = center, 1 = bottom edge. Defaults to 0 (top edge). ```teal tecs.ecs.RelativeTransform2D.originY: number ``` #### tecs.ecs.RelativeTransform2D.new Static Table-form constructor. ```teal function tecs.ecs.RelativeTransform2D.new( {string: any} ): RelativeTransform2D ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `#1` | `{string : any}` | | ##### Returns | Type | Description | | --- | --- | | [`RelativeTransform2D`](/modules/ecs/#tecs.ecs.RelativeTransform2D) | | #### tecs.ecs.RelativeTransform2D:__call metamethod Creates a new RelativeTransform2D component from positional args. For the table form, use `RelativeTransform2D.new({x=…, y=…})`. ```teal metamethod tecs.ecs.RelativeTransform2D.$meta.__call( self, x: number, y: number, z: number, rotation: number, scaleX: number, scaleY: number, originX: number, originY: number ): RelativeTransform2D ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `RelativeTransform2D` | | | `x` | `number` | The x offset from the parent. | | `y` | `number` | The y offset (defaults to 0). | | `z` | `number` | The z offset (defaults to 0). | | `rotation` | `number` | The rotation offset in radians (defaults to 0). | | `scaleX` | `number` | The x scale multiplier (defaults to 1). | | `scaleY` | `number` | The y scale multiplier (defaults to 1). | | `originX` | `number` | The origin X as a percentage (defaults to 0). | | `originY` | `number` | The origin Y as a percentage (defaults to 0). | ##### Returns | Type | Description | | --- | --- | | [`RelativeTransform2D`](/modules/ecs/#tecs.ecs.RelativeTransform2D) | the created RelativeTransform2D component. | ### tecs.ecs.ScalarComponent interface `ScalarComponent` names a single-value component. ```teal interface tecs.ecs.ScalarComponent is Component scalarKind: ScalarKind scalarDefault: T enum ScalarKind "boolean" "number" "string" end end ``` #### Interfaces | Interface | | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | #### Type Parameters | Name | Constraint | Description | | --- | --- | --- | | `T` | | | #### tecs.ecs.ScalarComponent.scalarKind field Read-only. Which primitive the column holds. Fixed at registration and never widened afterwards. ```teal tecs.ecs.ScalarComponent.scalarKind: ScalarKind ``` #### tecs.ecs.ScalarComponent.scalarDefault field Read-only. The value a row takes when the component is added without one. Never nil: `newScalarComponent` fills an omitted `default` with the zero of `kind` (`0`, `false` or `""`). ```teal tecs.ecs.ScalarComponent.scalarDefault: T ``` #### tecs.ecs.ScalarComponent.ScalarKind enum The primitives a scalar column may hold. Anything else, tables and cdata included, is a table or FFI component instead. ```teal enum tecs.ecs.ScalarComponent.ScalarKind "boolean" "number" "string" end ``` ### tecs.ecs.ScalarComponentOptions interface `ScalarComponentOptions` configures a scalar component. ```teal interface tecs.ecs.ScalarComponentOptions is BasicComponentOptions> kind: string default: T end ``` #### Interfaces | Interface | | --- | | `BasicComponentOptions<`[`ScalarComponent`](/modules/ecs/#tecs.ecs.ScalarComponent)`>` | #### Type Parameters | Name | Constraint | Description | | --- | --- | --- | | `T` | | | #### tecs.ecs.ScalarComponentOptions.kind field Caller-writable. Required. One of `"number"`, `"boolean"` or `"string"`; anything else errors at registration rather than at first use. ```teal tecs.ecs.ScalarComponentOptions.kind: string ``` #### tecs.ecs.ScalarComponentOptions.default field Caller-writable. Optional. Omitting it takes the zero of `kind` (`0`, `false` or `""`), so a scalar column never reads back nil. ```teal tecs.ecs.ScalarComponentOptions.default: T ``` ### tecs.ecs.Snapshot type `Snapshot` names serialized world state. ```teal type tecs.ecs.Snapshot = types.World.Snapshot ``` ### tecs.ecs.SnapshotComponentTableEntry type `SnapshotComponentTableEntry` identifies one component in a snapshot. ```teal type tecs.ecs.SnapshotComponentTableEntry = types.World.SnapshotComponentTableEntry ``` ### tecs.ecs.SnapshotHandler type `SnapshotHandler` saves and restores plugin data. ```teal type tecs.ecs.SnapshotHandler = types.World.SnapshotHandler ``` ### tecs.ecs.SnapshotOptions type `SnapshotOptions` controls snapshot serialization. ```teal type tecs.ecs.SnapshotOptions = types.World.SnapshotOptions ``` ### tecs.ecs.SnapshotOutput type `SnapshotOutput` collects serialized data. ```teal type tecs.ecs.SnapshotOutput = types.World.SnapshotOutput ``` ### tecs.ecs.SnapshotPrelude type `SnapshotPrelude` names snapshot metadata. ```teal type tecs.ecs.SnapshotPrelude = types.World.SnapshotPrelude ``` ### tecs.ecs.StartSnapshotLoad record An event emitted on entity 0 at the start of `tecs.loadSnapshot`, AFTER the world has been fully restored and BEFORE the data section is dispatched. Listeners register per-key callbacks via `ev:onData(key, callback)`; each callback fires once per matching data entry written by `OnSnapshotSave`. ```teal record tecs.ecs.StartSnapshotLoad is types.events.Event onData: function(self, string, function(any)) metamethod __call: function(self): StartSnapshotLoad end ``` #### Interfaces | Interface | | --- | | [`types.events.Event`](/modules/events/#tecs.events.Event) | #### tecs.ecs.StartSnapshotLoad:onData Instance Register a callback for a data entry keyed by `key`. The callback receives the decoded value. Multiple listeners may register the same key; all fire in registration order. ```teal function tecs.ecs.StartSnapshotLoad.onData(self, string, function(any)) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `StartSnapshotLoad` | | | `#2` | `string` | | | `#3` | `function(any)` | | ##### Returns None. #### tecs.ecs.StartSnapshotLoad:__call metamethod ```teal metamethod tecs.ecs.StartSnapshotLoad.$meta.__call( self ): StartSnapshotLoad ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `StartSnapshotLoad` | | ##### Returns | Type | Description | | --- | --- | | [`StartSnapshotLoad`](/modules/ecs/#tecs.ecs.StartSnapshotLoad) | | ### tecs.ecs.StateBlur record An event emitted when a state loses focus (another state pushed on top). ```teal record tecs.ecs.StateBlur is types.events.Event state: string pushed: string metamethod __call: function(self, string, string): StateBlur end ``` #### Interfaces | Interface | | --- | | [`types.events.Event`](/modules/events/#tecs.events.Event) | #### tecs.ecs.StateBlur.state field Engine-owned. The state losing focus. ```teal tecs.ecs.StateBlur.state: string ``` #### tecs.ecs.StateBlur.pushed field Engine-owned. The state being pushed on top. ```teal tecs.ecs.StateBlur.pushed: string ``` #### tecs.ecs.StateBlur:__call metamethod ```teal metamethod tecs.ecs.StateBlur.$meta.__call( self, string, string ): StateBlur ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `StateBlur` | | | `#2` | `string` | | | `#3` | `string` | | ##### Returns | Type | Description | | --- | --- | | [`StateBlur`](/modules/ecs/#tecs.ecs.StateBlur) | | ### tecs.ecs.StateEnter record An event emitted when a state is pushed onto the stack. ```teal record tecs.ecs.StateEnter is types.events.Event state: string metamethod __call: function(self, string): StateEnter end ``` #### Interfaces | Interface | | --- | | [`types.events.Event`](/modules/events/#tecs.events.Event) | #### tecs.ecs.StateEnter.state field Engine-owned. The state name being entered. ```teal tecs.ecs.StateEnter.state: string ``` #### tecs.ecs.StateEnter:__call metamethod ```teal metamethod tecs.ecs.StateEnter.$meta.__call(self, string): StateEnter ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `StateEnter` | | | `#2` | `string` | | ##### Returns | Type | Description | | --- | --- | | [`StateEnter`](/modules/ecs/#tecs.ecs.StateEnter) | | ### tecs.ecs.StateExit record An event emitted when a state is popped from the stack. ```teal record tecs.ecs.StateExit is types.events.Event state: string metamethod __call: function(self, string): StateExit end ``` #### Interfaces | Interface | | --- | | [`types.events.Event`](/modules/events/#tecs.events.Event) | #### tecs.ecs.StateExit.state field Engine-owned. The state name being exited. ```teal tecs.ecs.StateExit.state: string ``` #### tecs.ecs.StateExit:__call metamethod ```teal metamethod tecs.ecs.StateExit.$meta.__call(self, string): StateExit ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `StateExit` | | | `#2` | `string` | | ##### Returns | Type | Description | | --- | --- | | [`StateExit`](/modules/ecs/#tecs.ecs.StateExit) | | ### tecs.ecs.StateFocus record An event emitted when a state regains focus (state above popped). ```teal record tecs.ecs.StateFocus is types.events.Event state: string popped: string metamethod __call: function(self, string, string): StateFocus end ``` #### Interfaces | Interface | | --- | | [`types.events.Event`](/modules/events/#tecs.events.Event) | #### tecs.ecs.StateFocus.state field Engine-owned. The state regaining focus. ```teal tecs.ecs.StateFocus.state: string ``` #### tecs.ecs.StateFocus.popped field Engine-owned. The state that was popped. ```teal tecs.ecs.StateFocus.popped: string ``` #### tecs.ecs.StateFocus:__call metamethod ```teal metamethod tecs.ecs.StateFocus.$meta.__call( self, string, string ): StateFocus ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `StateFocus` | | | `#2` | `string` | | | `#3` | `string` | | ##### Returns | Type | Description | | --- | --- | | [`StateFocus`](/modules/ecs/#tecs.ecs.StateFocus) | | ### tecs.ecs.StatePolicy record `StatePolicy` controls state-stack participation. ```teal record tecs.ecs.StatePolicy record Action apply: string call: function(World) end onBlur: string | Action | function(World) onFocus: string | Action | function(World) onExit: string | Action | function(World) onEnter: function(World) end ``` #### tecs.ecs.StatePolicy.Action record Action for a state lifecycle hook. A string action ("pause", "resume", "despawn", "disable"), a plugin function, or a policy with both. ```teal record tecs.ecs.StatePolicy.Action apply: string call: function(World) end ``` ##### tecs.ecs.StatePolicy.Action.apply field Caller-writable. Built-in action: "pause", "resume", "despawn", "disable" ```teal tecs.ecs.StatePolicy.Action.apply: string ``` ##### tecs.ecs.StatePolicy.Action.call Static Custom function called with the world ```teal function tecs.ecs.StatePolicy.Action.call(World) ``` ###### Arguments | Name | Type | Description | | --- | --- | --- | | `#1` | [`World`](/modules/ecs/#tecs.World) | | ###### Returns None. #### tecs.ecs.StatePolicy.onBlur field Caller-writable. Fires when this state is no longer top (another state pushed on top). ```teal tecs.ecs.StatePolicy.onBlur: string | Action | function(World) ``` #### tecs.ecs.StatePolicy.onFocus field Caller-writable. Fires when this state becomes top again (state above popped). ```teal tecs.ecs.StatePolicy.onFocus: string | Action | function(World) ``` #### tecs.ecs.StatePolicy.onExit field Caller-writable. Fires when this state is popped (default: "despawn"). ```teal tecs.ecs.StatePolicy.onExit: string | Action | function(World) ``` #### tecs.ecs.StatePolicy.onEnter Static Fires when this state is first pushed. Only accepts a plugin function. ```teal function tecs.ecs.StatePolicy.onEnter(World) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `#1` | [`World`](/modules/ecs/#tecs.World) | | ##### Returns None. ### tecs.ecs.Stats type `Stats` reports live world counts. ```teal type tecs.ecs.Stats = types.World.Stats ``` ### tecs.System type A system function runs once when its phase dispatches. ```teal type tecs.System = function(number, World) ``` ### tecs.ecs.SystemConfig interface `SystemConfig` configures a system. ```teal interface tecs.ecs.SystemConfig name: string phase: Phase run: System before: {string} after: {string} commitBefore: boolean commitAfter: boolean runIf: function(number, World, string): boolean end ``` #### tecs.ecs.SystemConfig.name field Caller-writable. Optional, and unique across the whole pipeline: registering a second system under a name already taken errors. Omitting it assigns a synthetic `_anonymousSystemN`, so `removeSystem` always has a handle, but the debugger, the MCP tools and profiles then show that instead of anything readable. Name every persistent system. ```teal tecs.ecs.SystemConfig.name: string ``` #### tecs.ecs.SystemConfig.phase field Caller-writable. Required, and must already be registered with the pipeline; `addSystem` errors on an unregistered phase rather than creating one. ```teal tecs.ecs.SystemConfig.phase: Phase ``` #### tecs.ecs.SystemConfig.run field Caller-writable. Required. Receives `(dt, world)`, where `dt` is seconds and, in a fixed phase, is the fixed timestep rather than frame time. When `world:update` dispatches the system, an asynchronous engine call returns inline when ready and transparently suspends that logical update when it must wait. ```teal tecs.ecs.SystemConfig.run: System ``` #### tecs.ecs.SystemConfig.before field Caller-writable. Names of systems this one must run before. A name that no system in the SAME phase carries is ignored silently: ordering is solved per phase, so a constraint naming a system in another phase does nothing. Mutual constraints across a group error as a cycle. ```teal tecs.ecs.SystemConfig.before: {string} ``` #### tecs.ecs.SystemConfig.after field Caller-writable. Names of systems this one must run after, on the same terms as `before`. Systems with no constraint between them keep registration order. ```teal tecs.ecs.SystemConfig.after: {string} ``` #### tecs.ecs.SystemConfig.commitBefore field Caller-writable. Requests a structural publication barrier before this system's `runIf` and `run` dispatch. Defaults to false. Use it only when this system must observe structural mutations staged by an earlier system in the same phase. ```teal tecs.ecs.SystemConfig.commitBefore: boolean ``` #### tecs.ecs.SystemConfig.commitAfter field Caller-writable. Requests a structural publication barrier after this system's dispatch. Defaults to false. Use it only when a later system in the same phase must observe this system's structural mutations. Every phase already commits at its end. Use `world:enqueueCommit()` from `run` instead when the dependency is conditional at runtime. ```teal tecs.ecs.SystemConfig.commitAfter: boolean ``` #### tecs.ecs.SystemConfig.runIf Static Caller-writable. Optional gate evaluated immediately before each dispatch, so a false answer skips this frame only and does not unregister anything. Receives the same `dt` the system would have, plus the system's name, which is what a self-removing predicate passes back to `removeSystem`. ```teal function tecs.ecs.SystemConfig.runIf(number, World, string): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `#1` | `number` | | | `#2` | `World` | | | `#3` | `string` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | | ### tecs.ecs.SystemInfo interface `SystemInfo` reports one registered system. ```teal interface tecs.ecs.SystemInfo name: string phase: string position: integer enabled: boolean hasRunIf: boolean end ``` #### tecs.ecs.SystemInfo.name field Read-only. Names the system, which is the `name` its [`SystemConfig`](/modules/ecs/#tecs.ecs.SystemConfig) carried, or the synthetic `_anonymousSystemN` the pipeline assigns an unnamed one. Either name selects the system in `world:setSystemEnabled`, and a synthetic one changes whenever registration order changes. ```teal tecs.ecs.SystemInfo.name: string ``` #### tecs.ecs.SystemInfo.phase field Read-only. Names the [`Phase`](/modules/ecs/#tecs.ecs.Phase) the system runs in, and reads `"Unknown"` for a custom phase registered without a name. ```teal tecs.ecs.SystemInfo.phase: string ``` #### tecs.ecs.SystemInfo.position field Read-only. Counts from one, and reports where the system sits among the systems of its own phase in the order they run, which is registration order after `before` and `after` are applied. ```teal tecs.ecs.SystemInfo.position: integer ``` #### tecs.ecs.SystemInfo.enabled field Read-only. Reports whether the system runs at all. A disabled system stays registered and keeps its name, its position and its ordering constraints, and runs nothing until `world:setSystemEnabled` enables it again. ```teal tecs.ecs.SystemInfo.enabled: boolean ``` #### tecs.ecs.SystemInfo.hasRunIf field Read-only. Reports whether the system declares a `runIf` of its own. That gate is evaluated per dispatch and this listing does not call it, so an enabled system with one may still skip any given frame. ```teal tecs.ecs.SystemInfo.hasRunIf: boolean ``` ### tecs.ecs.TagComponentOptions interface `TagComponentOptions` configures a tag component. ```teal interface tecs.ecs.TagComponentOptions is BasicComponentOptions container: Component end ``` #### Interfaces | Interface | | --- | | `BasicComponentOptions<`[`Component`](/modules/ecs/#tecs.ecs.Component)`>` | #### tecs.ecs.TagComponentOptions.container field Caller-writable. Optional container to use for the tag component. ```teal tecs.ecs.TagComponentOptions.container: Component ``` ### tecs.ecs.Transform2D record Provides the coordinates and transform of an entity. ```teal record tecs.ecs.Transform2D is Component x: number y: number z: number layer: integer rotation: number scaleX: number scaleY: number new: function({string: any}): Transform2D metamethod __call: function( self, x: number, y: number, z: number, layer: integer, rotation: number, scaleX: number, scaleY: number ): Transform2D end ``` #### Interfaces | Interface | | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | #### tecs.ecs.Transform2D.x field Caller-writable. The x coordinate of the entity. ```teal tecs.ecs.Transform2D.x: number ``` #### tecs.ecs.Transform2D.y field Caller-writable. The y coordinate of the entity. ```teal tecs.ecs.Transform2D.y: number ``` #### tecs.ecs.Transform2D.z field Caller-writable. The z coordinate of the entity. ```teal tecs.ecs.Transform2D.z: number ``` #### tecs.ecs.Transform2D.layer field Caller-writable. The layer of the entity ```teal tecs.ecs.Transform2D.layer: integer ``` #### tecs.ecs.Transform2D.rotation field Caller-writable. The rotation in radians (defaults to 0). ```teal tecs.ecs.Transform2D.rotation: number ``` #### tecs.ecs.Transform2D.scaleX field Caller-writable. The x scale (defaults to 1). ```teal tecs.ecs.Transform2D.scaleX: number ``` #### tecs.ecs.Transform2D.scaleY field Caller-writable. The y scale (defaults to 1). ```teal tecs.ecs.Transform2D.scaleY: number ``` #### tecs.ecs.Transform2D.new Static Table-form constructor. `data` is a partial `{x, y, z, layer, rotation, scaleX, scaleY}`. ```teal function tecs.ecs.Transform2D.new({string: any}): Transform2D ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `#1` | `{string : any}` | | ##### Returns | Type | Description | | --- | --- | | [`Transform2D`](/modules/ecs/#tecs.ecs.Transform2D) | | #### tecs.ecs.Transform2D:__call metamethod Creates a new Transform2D component from positional args. For the table form, use `Transform2D.new({x=…, y=…})`. ```teal metamethod tecs.ecs.Transform2D.$meta.__call( self, x: number, y: number, z: number, layer: integer, rotation: number, scaleX: number, scaleY: number ): Transform2D ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Transform2D` | | | `x` | `number` | The x position. | | `y` | `number` | The y position. | | `z` | `number` | The z position. | | `layer` | `integer` | The layer (defaults to 1) | | `rotation` | `number` | The rotation in radians (defaults to 0). | | `scaleX` | `number` | The x scale (defaults to 1). | | `scaleY` | `number` | The y scale (defaults to 1). | ##### Returns | Type | Description | | --- | --- | | [`Transform2D`](/modules/ecs/#tecs.ecs.Transform2D) | the created transform component. | ### tecs.ecs.Transform3D record Places an entity in a right-handed three-dimensional world. Position uses world units. Rotation is a normalized quaternion in `(x, y, z, w)` order that turns local coordinates into world coordinates. Scale is local and may be non-uniform. The identity transform is at the origin with quaternion `(0, 0, 0, 1)` and unit scale. ```teal record tecs.ecs.Transform3D is Component x: number y: number z: number rotationX: number rotationY: number rotationZ: number rotationW: number scaleX: number scaleY: number scaleZ: number new: function(data: {string: any}): Transform3D metamethod __call: function( self, x: number, y: number, z: number, rotationX: number, rotationY: number, rotationZ: number, rotationW: number, scaleX: number, scaleY: number, scaleZ: number ): Transform3D end ``` #### Interfaces | Interface | | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | #### tecs.ecs.Transform3D.x field Caller-writable. Caller-writable. Sets the world-space x coordinate. ```teal tecs.ecs.Transform3D.x: number ``` #### tecs.ecs.Transform3D.y field Caller-writable. Caller-writable. Sets the world-space y coordinate. ```teal tecs.ecs.Transform3D.y: number ``` #### tecs.ecs.Transform3D.z field Caller-writable. Caller-writable. Sets the world-space z coordinate. ```teal tecs.ecs.Transform3D.z: number ``` #### tecs.ecs.Transform3D.rotationX field Caller-writable. Caller-writable. Sets the quaternion x component. ```teal tecs.ecs.Transform3D.rotationX: number ``` #### tecs.ecs.Transform3D.rotationY field Caller-writable. Caller-writable. Sets the quaternion y component. ```teal tecs.ecs.Transform3D.rotationY: number ``` #### tecs.ecs.Transform3D.rotationZ field Caller-writable. Caller-writable. Sets the quaternion z component. ```teal tecs.ecs.Transform3D.rotationZ: number ``` #### tecs.ecs.Transform3D.rotationW field Caller-writable. Caller-writable. Sets the quaternion scalar component. ```teal tecs.ecs.Transform3D.rotationW: number ``` #### tecs.ecs.Transform3D.scaleX field Caller-writable. Caller-writable. Sets the local x scale. ```teal tecs.ecs.Transform3D.scaleX: number ``` #### tecs.ecs.Transform3D.scaleY field Caller-writable. Caller-writable. Sets the local y scale. ```teal tecs.ecs.Transform3D.scaleY: number ``` #### tecs.ecs.Transform3D.scaleZ field Caller-writable. Caller-writable. Sets the local z scale. ```teal tecs.ecs.Transform3D.scaleZ: number ``` #### tecs.ecs.Transform3D.new Static Creates a three-dimensional transform from named values. ```teal function tecs.ecs.Transform3D.new(data: {string: any}): Transform3D ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `data` | `{string : any}` | A partial transform table whose omitted fields use the identity defaults. | ##### Returns | Type | Description | | --- | --- | | [`Transform3D`](/modules/ecs/#tecs.ecs.Transform3D) | The created transform component. | #### tecs.ecs.Transform3D:__call metamethod Creates a three-dimensional transform from positional values. Prefer `Transform3D.new` when setting only some fields because the positional quaternion follows the three position values. ```teal metamethod tecs.ecs.Transform3D.$meta.__call( self, x: number, y: number, z: number, rotationX: number, rotationY: number, rotationZ: number, rotationW: number, scaleX: number, scaleY: number, scaleZ: number ): Transform3D ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Transform3D` | | | `x` | `number` | The world-space x coordinate, which defaults to zero. | | `y` | `number` | The world-space y coordinate, which defaults to zero. | | `z` | `number` | The world-space z coordinate, which defaults to zero. | | `rotationX` | `number` | The quaternion x component, which defaults to zero. | | `rotationY` | `number` | The quaternion y component, which defaults to zero. | | `rotationZ` | `number` | The quaternion z component, which defaults to zero. | | `rotationW` | `number` | The quaternion scalar component, which defaults to one. | | `scaleX` | `number` | The local x scale, which defaults to one. | | `scaleY` | `number` | The local y scale, which defaults to one. | | `scaleZ` | `number` | The local z scale, which defaults to one. | ##### Returns | Type | Description | | --- | --- | | [`Transform3D`](/modules/ecs/#tecs.ecs.Transform3D) | The created transform component. | ### tecs.ecs.TTL record Despawns an entity when the TTL reaches zero. ```teal record tecs.ecs.TTL is Component startingTime: number remaining: number percentComplete: function(self): number metamethod __call: function(self, remaining: number): TTL end ``` #### Interfaces | Interface | | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | #### tecs.ecs.TTL.startingTime field Engine-owned. The total amount of time the entity had to live. ```teal tecs.ecs.TTL.startingTime: number ``` #### tecs.ecs.TTL.remaining field Engine-owned. The remaining time the entity has to live. ```teal tecs.ecs.TTL.remaining: number ``` #### tecs.ecs.TTL:percentComplete Instance Compute the percentage of completion as a number between 0 and 1. ```teal function tecs.ecs.TTL.percentComplete(self): number ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `TTL` | | ##### Returns | Type | Description | | --- | --- | | `number` | | #### tecs.ecs.TTL:__call metamethod Create a new TTL component. ```teal metamethod tecs.ecs.TTL.$meta.__call(self, remaining: number): TTL ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `TTL` | | | `remaining` | `number` | The amount of time the entity has to live. | ##### Returns | Type | Description | | --- | --- | | [`TTL`](/modules/ecs/#tecs.ecs.TTL) | the created TTL component. | ### tecs.World interface A world owns entities, components, queries, systems, resources, events, snapshots, and the state stack. ```teal interface tecs.World interface Config timestep: number fixedMaxSteps: integer fixedOverload: FixedOverload maxEntities: integer pipelineFactory: function( timestep: number, fixedMaxSteps: integer, fixedOverload: FixedOverload ): Pipeline end interface SnapshotComponentTableEntry name: string fingerprint: string | nil end interface SnapshotPrelude version: integer nextEntityId: integer entityCount: integer archetypeCount: integer componentTable: {SnapshotComponentTableEntry} end interface SnapshotArchetypeEntry columnIndices: {integer} entities: {{any}} end interface SnapshotDataEntry key: string value: any end interface Snapshot version: integer nextEntityId: integer componentTable: {SnapshotComponentTableEntry} archetypes: {SnapshotArchetypeEntry} data: {SnapshotDataEntry} end enum SnapshotFormat "binary" "table" end interface SnapshotOptions format: SnapshotFormat buffer: StringBuffer path: string filterQuery: Query.Descriptor layers: {integer} customData: {string: any} end interface SnapshotOutput format: SnapshotFormat buffer: StringBuffer | nil snapshot: Snapshot | nil end interface SnapshotHandler name: string load: function(World, any) | nil finish: function(World, SnapshotPrelude) | nil save: function(world: World): any | nil end resources: Store interface Stats entities: integer archetypes: integer components: integer systems: integer fixedTimeDropped: number fixedStepsDropped: integer end addPlugin: function(self, plugin: Plugin) addSnapshotHandler: function(self, handler: SnapshotHandler) addSystem: function(self, config: SystemConfig) batchDespawn: function(self, query: Query) batchRemove: function( self, query: Query, componentType: components.Component ) batchSet: function( self, query: Query, componentOrInstance: components.Component, callback: function(Archetype, integer, integer, integer) ) batchSpawn: function( self, count: integer, componentTypes: {components.Component}, callback: function(Archetype, integer, integer, integer) ): integer | nil, {integer} | nil batchSpawnAt: function( self, ids: {integer}, componentTypes: {components.Component}, callback: function(Archetype, integer, integer, integer) ) byKey: function(self, key: string): integer | nil clearEntities: function(self) clearObservers: function(self, address: integer) compact: function(self): integer, integer createState: function( self, name: string, policy: StatePolicy ): components.Component despawn: function(self, entity: integer) dirtyArchetypes: function(self): function(): Archetype disablePhase: function(self, phase: Phase) emit: function( self, address: integer, eventOrType: events.Event, ...: any ) enablePhase: function(self, phase: Phase) enqueueCommit: function(self) findArchetypes: function( self, component: components.Component ): function(): (Archetype, integer, DoubleArray) fixedStepCount: function(self): integer forEachArchetype: function(self, callback: function(Archetype)) get: function( self, entity: integer, component: T ): T getBundle: function(self, name: string): Bundle | nil getBundles: function(self): {string: Bundle} getFirstRelationship: function( self, entity: integer, relationship: T ): T getFixedTiming: function(self): number, number, number getMut: function( self, entity: integer, component: T ): T getStats: function(self, fill: World.Stats): World.Stats has: function( self, entity: integer, component: components.Component ): boolean hasObservers: function( self, address: integer, event: T ): boolean isAlive: function(self, entity: integer): boolean listStates: function(self): {string} listSystems: function(self): {SystemInfo} loadSnapshot: function(self, source: any): SnapshotPrelude markComponentDirty: function( self, entity: integer, component: components.Component ) newBundle: function( self, name: string, def: Bundle.Definition ): Bundle newQuery: function(self, descriptor: Query.Descriptor): Query observe: function( self, address: integer, event: T, callback: function(T), id: string ) peekState: function(self): string popState: function(self) pushState: function(self, name: string) registerPhase: function(self, phase: Phase) remove: function( self, entity: integer, component: components.Component ) removeSystem: function(self, systemName: string) requireKey: function(self, key: string): integer runPhase: function(self, phase: Phase, dt: number) saveSnapshot: function(self, opts: SnapshotOptions): SnapshotOutput set: function( self, entity: integer, component: components.Component, value: any ) setSystemEnabled: function( self, systemName: string, enabled: boolean ): boolean, string shutdown: function(self) spawn: function(self, ...: components.Component): integer spawnAt: function(self, id: integer, ...: components.Component) spawnBundle: function( self, name: string, ...: components.Component ): integer startup: function(self) stopObserving: function( self, address: integer, event: T, observer: function(T) | string ) targets: function( self, entity: integer, relationship: components.Relationship, callback: function(integer, T), context: T ) traverse: function( self, root: integer, relationship: components.Relationship ): function(): (integer, integer) update: function(self, dt: number): boolean walkUp: function( self, entity: integer, relationship: components.Relationship, callback: function(integer, integer, T): boolean, context: T, maxDepth: number ) end ``` #### tecs.World.Config interface The options passed to `tecs.ecs.newWorld`. ```teal interface tecs.World.Config timestep: number fixedMaxSteps: integer fixedOverload: FixedOverload maxEntities: integer pipelineFactory: function( timestep: number, fixedMaxSteps: integer, fixedOverload: FixedOverload ): Pipeline end ``` ##### tecs.World.Config.timestep field Caller-writable. Sets the positive duration of one fixed step in seconds. Defaults to 1/60. ```teal tecs.World.Config.timestep: number ``` ##### tecs.World.Config.fixedMaxSteps field Caller-writable. Sets the positive number of fixed steps one update may run before the overload policy applies. Defaults to 10. ```teal tecs.World.Config.fixedMaxSteps: integer ``` ##### tecs.World.Config.fixedOverload field Caller-writable. Selects what happens to catch-up steps that exceed `fixedMaxSteps`. Defaults to `"drop"`. ```teal tecs.World.Config.fixedOverload: FixedOverload ``` ##### tecs.World.Config.maxEntities field Caller-writable. Sets the positive number of concurrent entity slots, up to 2^22 - 1. Defaults to 2^20. ```teal tecs.World.Config.maxEntities: integer ``` ##### tecs.World.Config.pipelineFactory Static Caller-writable. Builds a custom system pipeline once during world construction. Most callers omit this field. ```teal function tecs.World.Config.pipelineFactory( timestep: number, fixedMaxSteps: integer, fixedOverload: FixedOverload ): Pipeline ``` ###### Arguments | Name | Type | Description | | --- | --- | --- | | `timestep` | `number` | The configured fixed timestep. | | `fixedMaxSteps` | `integer` | The configured fixed-step limit. | | `fixedOverload` | [`FixedOverload`](/modules/ecs/#tecs.ecs.FixedOverload) | The configured overload policy. | ###### Returns | Type | Description | | --- | --- | | [`Pipeline`](/modules/ecs/#tecs.ecs.Pipeline) | The pipeline this world will run. | #### tecs.World.SnapshotComponentTableEntry interface One component the snapshot's archetypes refer to. Archetype frames name components by 1-based index into the component table rather than repeating the name per archetype. ```teal interface tecs.World.SnapshotComponentTableEntry name: string fingerprint: string | nil end ``` ##### tecs.World.SnapshotComponentTableEntry.name field Caller-writable. Names the registered component a load resolves this entry against. ```teal tecs.World.SnapshotComponentTableEntry.name: string ``` ##### tecs.World.SnapshotComponentTableEntry.fingerprint field Caller-writable. Carries the canonical FFI field-layout fingerprint, or nil for a non-FFI component. Binary output embeds it so a load can detect that the saved struct layout differs from the current one and route those rows through per-entity migration. nil for every non-FFI component, and absent entirely from `"table"` output, whose loads go through field-name-keyed `deserialize` and migrate without it. ```teal tecs.World.SnapshotComponentTableEntry.fingerprint: string | nil ``` #### tecs.World.SnapshotPrelude interface The header of a snapshot, and what `loadSnapshot` answers with. ```teal interface tecs.World.SnapshotPrelude version: integer nextEntityId: integer entityCount: integer archetypeCount: integer componentTable: {SnapshotComponentTableEntry} end ``` ##### tecs.World.SnapshotPrelude.version field Read-only. Reports the snapshot format version, not the game version. ```teal tecs.World.SnapshotPrelude.version: integer ``` ##### tecs.World.SnapshotPrelude.nextEntityId field Read-only. Reports where entity slot allocation resumes. A load moves the allocator forward to this when it is ahead of where the allocator already is, and never backwards, so ids handed out after a load cannot collide with restored ones. ```teal tecs.World.SnapshotPrelude.nextEntityId: integer ``` ##### tecs.World.SnapshotPrelude.entityCount field Read-only. Reports the entity count when the writer knew it, or nil for a streaming writer. ```teal tecs.World.SnapshotPrelude.entityCount: integer ``` ##### tecs.World.SnapshotPrelude.archetypeCount field Read-only. Reports the archetype count when the writer knew it, or nil for a streaming writer. ```teal tecs.World.SnapshotPrelude.archetypeCount: integer ``` ##### tecs.World.SnapshotPrelude.componentTable field Read-only. Lists every referenced component in archetype index order. ```teal tecs.World.SnapshotPrelude.componentTable: {SnapshotComponentTableEntry} ``` #### tecs.World.SnapshotArchetypeEntry interface One archetype's worth of entities in a `"table"` snapshot. ```teal interface tecs.World.SnapshotArchetypeEntry columnIndices: {integer} entities: {{any}} end ``` ##### tecs.World.SnapshotArchetypeEntry.columnIndices field Caller-writable. Lists 1-based indices into `Snapshot.componentTable`, in the same order the per-entity payloads appear. ```teal tecs.World.SnapshotArchetypeEntry.columnIndices: {integer} ``` ##### tecs.World.SnapshotArchetypeEntry.entities field Caller-writable. Holds one entry per entity, shaped `{id, data1, ..., dataN}`, where `dataI` is whatever the component at `columnIndices[i]` returned from `serialize`. ```teal tecs.World.SnapshotArchetypeEntry.entities: {{any}} ``` #### tecs.World.SnapshotDataEntry interface One keyed value in a snapshot's data section. ```teal interface tecs.World.SnapshotDataEntry key: string value: any end ``` ##### tecs.World.SnapshotDataEntry.key field Caller-writable. Names the key a `SnapshotHandler` or `customData` entry wrote under. Keys beginning `__tecs.` are the engine's own. ```teal tecs.World.SnapshotDataEntry.key: string ``` ##### tecs.World.SnapshotDataEntry.value field Caller-writable. Holds the value written under `key`. It must survive the chosen format: a `"binary"` snapshot can carry anything the serializer accepts, a `"table"` one anything a plain Lua table can hold. ```teal tecs.World.SnapshotDataEntry.value: any ``` #### tecs.World.Snapshot interface A whole snapshot in plain-table form, which is what `saveSnapshot` returns under `format = "table"`. ```teal interface tecs.World.Snapshot version: integer nextEntityId: integer componentTable: {SnapshotComponentTableEntry} archetypes: {SnapshotArchetypeEntry} data: {SnapshotDataEntry} end ``` ##### tecs.World.Snapshot.version field Caller-writable. Sets the snapshot format version. ```teal tecs.World.Snapshot.version: integer ``` ##### tecs.World.Snapshot.nextEntityId field Caller-writable. Sets the slot where entity allocation resumes. ```teal tecs.World.Snapshot.nextEntityId: integer ``` ##### tecs.World.Snapshot.componentTable field Caller-writable. Lists the components referenced by the archetype entries; `SnapshotArchetypeEntry.columnIndices` points into it. ```teal tecs.World.Snapshot.componentTable: {SnapshotComponentTableEntry} ``` ##### tecs.World.Snapshot.archetypes field Caller-writable. Lists archetypes in save order. ```teal tecs.World.Snapshot.archetypes: {SnapshotArchetypeEntry} ``` ##### tecs.World.Snapshot.data field Caller-writable. Lists keyed values after every archetype, in the order they were added, from `opts.customData` and from `OnSnapshotSave` listeners. ```teal tecs.World.Snapshot.data: {SnapshotDataEntry} ``` #### tecs.World.SnapshotFormat enum Which of the two representations `saveSnapshot` produces. ```teal enum tecs.World.SnapshotFormat "binary" "table" end ``` #### tecs.World.SnapshotOptions interface Options passed to `saveSnapshot`. All fields are optional. ```teal interface tecs.World.SnapshotOptions format: SnapshotFormat buffer: StringBuffer path: string filterQuery: Query.Descriptor layers: {integer} customData: {string: any} end ``` ##### tecs.World.SnapshotOptions.format field Caller-writable. Selects `"binary"` for a LuaJIT `string.buffer` or `"table"` for a plain table. ```teal tecs.World.SnapshotOptions.format: SnapshotFormat ``` ##### tecs.World.SnapshotOptions.buffer field Caller-writable. Supplies a `string.buffer` to reset and reuse for binary output. It is reset before anything is written, so whatever it held is gone. Binary output only: passing one with `format = "table"` errors rather than being ignored. ```teal tecs.World.SnapshotOptions.buffer: StringBuffer ``` ##### tecs.World.SnapshotOptions.path field Caller-writable. Supplies an optional path for binary output. When provided, `saveSnapshot` writes the bytes to disk and still returns the tagged buffer result. Binary output only: passing one with `format = "table"` errors rather than being ignored. ```teal tecs.World.SnapshotOptions.path: string ``` ##### tecs.World.SnapshotOptions.filterQuery field Caller-writable. Selects saved archetypes through a temporary query. ```teal tecs.World.SnapshotOptions.filterQuery: Query.Descriptor ``` ##### tecs.World.SnapshotOptions.layers field Caller-writable. Allows only the listed `Transform2D.layer` values from 0 through 31. ```teal tecs.World.SnapshotOptions.layers: {integer} ``` ##### tecs.World.SnapshotOptions.customData field Caller-writable. Adds keyed metadata to the snapshot. ```teal tecs.World.SnapshotOptions.customData: {string: any} ``` #### tecs.World.SnapshotOutput interface What `saveSnapshot` answers with. Exactly one of `buffer` and `snapshot` is set, and `format` says which; the other is nil. `loadSnapshot` accepts this record whole, so a round trip needs no unpacking. ```teal interface tecs.World.SnapshotOutput format: SnapshotFormat buffer: StringBuffer | nil snapshot: Snapshot | nil end ``` ##### tecs.World.SnapshotOutput.format field Read-only. Reports which representation carries the snapshot. ```teal tecs.World.SnapshotOutput.format: SnapshotFormat ``` ##### tecs.World.SnapshotOutput.buffer field Read-only. Returns the bytes under `format = "binary"`, or nil for table output. This is `opts.buffer` itself when one was passed, so a caller that reuses a buffer gets the same object back and must read it before the next save overwrites it. ```teal tecs.World.SnapshotOutput.buffer: StringBuffer | nil ``` ##### tecs.World.SnapshotOutput.snapshot field Read-only. Returns the snapshot under `format = "table"`, or nil for binary output. The table is freshly built and the caller's to keep. ```teal tecs.World.SnapshotOutput.snapshot: Snapshot | nil ``` #### tecs.World.SnapshotHandler interface Named snapshot participant for custom non-component data. `save` writes one keyed value into the snapshot data section when it returns non-nil. `load` receives that value after the ECS world has been restored. `finish` runs after every data callback has completed. ```teal interface tecs.World.SnapshotHandler name: string load: function(World, any) | nil finish: function(World, SnapshotPrelude) | nil save: function(world: World): any | nil end ``` ##### tecs.World.SnapshotHandler.name field Caller-writable. Sets the non-empty key used to store the value or registration errors. Namespace it with dots (`"myGame.gameState"`); keys beginning `__tecs.` are the engine's own. ```teal tecs.World.SnapshotHandler.name: string ``` ##### tecs.World.SnapshotHandler.load field Caller-writable. Optionally receives saved data. Tecs calls it only when the snapshot carries this handler's key, so a snapshot saved before the handler existed simply does not call it. ```teal tecs.World.SnapshotHandler.load: function(World, any) | nil ``` ##### tecs.World.SnapshotHandler.finish field Caller-writable. Optionally runs after every data callback has completed, which is the place for work that depends on more than one handler's value. ```teal tecs.World.SnapshotHandler.finish: function(World, SnapshotPrelude) | nil ``` ##### tecs.World.SnapshotHandler.save Static Caller-writable. Optionally returns data to save. Returning nil writes no entry, which is how a handler declines rather than storing an empty value. ```teal function tecs.World.SnapshotHandler.save(world: World): any | nil ``` ###### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`World`](/modules/ecs/#tecs.World) | The world being saved. | ###### Returns | Type | Description | | --- | --- | | any | nil | The value to store, or nil to omit this handler. | #### tecs.World.resources field Caller-writable. Stores world resources under typed keys. ```teal tecs.World.resources: Store ``` #### tecs.World.Stats interface Counts and fixed-step loss reported by `world:getStats`. ```teal interface tecs.World.Stats entities: integer archetypes: integer components: integer systems: integer fixedTimeDropped: number fixedStepsDropped: integer end ``` ##### tecs.World.Stats.entities field Read-only. Reports the number of active entities. ```teal tecs.World.Stats.entities: integer ``` ##### tecs.World.Stats.archetypes field Read-only. Reports the number of archetypes. ```teal tecs.World.Stats.archetypes: integer ``` ##### tecs.World.Stats.components field Read-only. Reports the number of registered components. ```teal tecs.World.Stats.components: integer ``` ##### tecs.World.Stats.systems field Read-only. Reports the number of registered systems. ```teal tecs.World.Stats.systems: integer ``` ##### tecs.World.Stats.fixedTimeDropped field Read-only. Reports simulated seconds abandoned by the fixed step overload policy. ```teal tecs.World.Stats.fixedTimeDropped: number ``` ##### tecs.World.Stats.fixedStepsDropped field Read-only. Reports fixed steps abandoned by the overload policy. ```teal tecs.World.Stats.fixedStepsDropped: integer ``` #### tecs.World:addPlugin Instance Add a plugin to the world. ```teal function tecs.World.addPlugin(self, plugin: Plugin) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | | `plugin` | [`Plugin`](/modules/ecs/#tecs.Plugin) | The plugin to add. | ##### Returns None. #### tecs.World:addSnapshotHandler Instance Register named save/load callbacks for custom snapshot data. This is a convenience wrapper over `OnSnapshotSave`, `StartSnapshotLoad`, and `FinishSnapshotLoad`. Use the raw snapshot events when you need lower-level behavior such as excluding derived entities. ```teal function tecs.World.addSnapshotHandler(self, handler: SnapshotHandler) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | | `handler` | [`SnapshotHandler`](/modules/ecs/#tecs.World.SnapshotHandler) | Its `name` is an externally typed snapshot key and must remain stable across builds. | ##### Returns None. #### tecs.World:addSystem Instance Add a system to the world. ```teal function tecs.World.addSystem(self, config: SystemConfig) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | | `config` | [`SystemConfig`](/modules/ecs/#tecs.ecs.SystemConfig) | The system configuration. | ##### Returns None. #### tecs.World:batchDespawn Instance Bulk-despawn every entity matching `query`. The actual teardown is deferred until the next pipeline barrier. `query` must be a `Query` object built via `world:newQuery(...)` and reused across calls -- batch ops do not accept QueryDescriptors or raw component-type arrays. Build the query once outside your hot loop. At publication, events and relationship bookkeeping run as expected: * `OnDespawn` events fire for each despawned entity (global observers and per-entity observers). * Per-entity observer subscriptions are cleared after the event fans out. * Query observers receive a single `onEntitiesRemoved` for the full range, followed by `onDeactivated` when the archetype empties. * Archetypes with dense relationships or entities that are reverse-index targets fall back to per-entity despawn so cascade-delete and reverse-index unlink run correctly. ```teal function tecs.World.batchDespawn(self, query: Query) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | | `query` | [`Query`](/modules/ecs/#tecs.Query) | Query built via `world:newQuery(...)`. | ##### Returns None. #### tecs.World:batchRemove Instance Bulk-remove a component from every entity matching `query` whose archetype currently carries it. Archetypes in the query that lack the component are skipped silently (no-op). Fast path runs when the **target component** is plain (no bulk-incompatible behavior flag, no wildcard container, not sparse): one bulk move to `src:withoutComponent(type)` per matched archetype, no per-entity dispatch. Relationship-bearing or otherwise non-bulk components fall back to per-entity `world:remove` so reverse-index unlink and cascade delete run correctly. Other components in the source archetype do not affect path selection. ```teal function tecs.World.batchRemove( self, query: Query, componentType: components.Component ) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | | `query` | [`Query`](/modules/ecs/#tecs.Query) | Query built via `world:newQuery(...)`. | | `componentType` | [`components.Component`](/modules/ecs/#tecs.ecs.Component) | Component to remove. | ##### Returns None. #### tecs.World:batchSet Instance Bulk-set a component on every entity matching `query`. Two modes: * Constant: `world:batchSet(q, Stunned)` or `world:batchSet(q, Position(0, 0))`. The instance is copied to every matched row. If an archetype lacks the component, entities are bulk-moved to the archetype `src:withComponent(type)` first, then the new column is filled. * Callback: `world:batchSet(q, Position, function(arch, firstRow, lastRow, count) ... end)`. Component is ensured to exist (bulk move if needed), then the callback is invoked once per affected archetype with 1-based inclusive row bounds so the caller can write the column directly. Fast path runs when the **target component** is "plain": it has no bulk-incompatible behavior flag, is not a dense relationship instance (no wildcard container), and is not sparse. Fast path does one move plan per (src, dst) archetype, bulk column copy, and whole-archetype truncate. Relationship-bearing or otherwise non-bulk components fall back to per-entity `world:set` for correctness (const form only; see below). Callback form additionally requires the target component to be plain because the callback is the value-write step. Non-bulk components must use the constant-value form. Sparse relationships always route through the per-entity path since they live in per-world stores, not archetype columns. ```teal function tecs.World.batchSet( self, query: Query, componentOrInstance: components.Component, callback: function(Archetype, integer, integer, integer) ) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | | `query` | [`Query`](/modules/ecs/#tecs.Query) | Query built via `world:newQuery(...)`. | | `componentOrInstance` | [`components.Component`](/modules/ecs/#tecs.ecs.Component) | Component instance (const mode) or component type (callback mode). | | `callback` | `function(`[`Archetype`](/modules/ecs/#tecs.ecs.Archetype)`, integer, integer, integer)` | Optional chunk writer `(arch, firstRow, lastRow, count)`. | ##### Returns None. #### tecs.World:batchSpawn Instance Bulk-spawn `count` entities sharing one component signature. Resolves the target archetype once at call time and defers the actual row placement and `callback` invocation until the next pipeline barrier. Returns `(firstId, nil)` when it can allocate a contiguous ID range. In that case IDs are `firstId`, `firstId + 1`, ..., `firstId + count - 1`. Returns `(nil, ids)` when it falls back to recycled, non-contiguous IDs. In that case iterate the returned `ids` list explicitly. Returned IDs are valid immediately -- you can call `world:set`, `world:remove`, or `world:despawn` on them before that barrier and the mutations are ordered correctly. At publication the target archetype's row range is claimed, then `callback(archetype, firstRow, lastRow, count)` runs so you can write per-entity data via mutable column access (`archetype:getMut(Component)[row] = ...`). Relationship components (dense or sparse) are supported in the signature as either the bare container (e.g. `ChildOf`) or as a specific-target instance (e.g. `ChildOf(parent)`). When an instance is passed, its wildcard container is added to the archetype automatically -- no follow-up `world:set` is needed for queries on the bare container to match. Sparse relationship columns are row-indexed proxies that error on direct writes from the `callback`. For per-entity *varying* target values, call `world:set(spawnedId, SparseRel(target))` either inside the callback or any time before publication. Use `firstId + i` only when the return was contiguous; otherwise use the explicit `ids` list. The staged sparse sets drain alongside the batchSpawn placement. ```teal function tecs.World.batchSpawn( self, count: integer, componentTypes: {components.Component}, callback: function(Archetype, integer, integer, integer) ): integer | nil, {integer} | nil ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | | `count` | `integer` | Number of entities to spawn. | | `componentTypes` | `{`[`components.Component`](/modules/ecs/#tecs.ecs.Component)`}` | Array of component types defining the archetype. | | `callback` | `function(`[`Archetype`](/modules/ecs/#tecs.ecs.Archetype)`, integer, integer, integer)` | Called at publication with `(archetype, firstRow, lastRow, count)`. Iterate rows with `for i = firstRow, lastRow do ... end`. | ##### Returns | Type | Description | | --- | --- | | integer | nil | firstId First entity ID when the allocation is contiguous, otherwise nil. | | {integer} | nil | ids Explicit entity ID list when fallback uses non-contiguous IDs, otherwise nil. | #### tecs.World:batchSpawnAt Instance Like `batchSpawn`, but uses the supplied entity IDs instead of allocating a new contiguous range. Intended for snapshot loads where each restored entity keeps its original ID. The archetype resolution, capacity check, and required-component expansion happen once per call regardless of ID ordering. ```teal function tecs.World.batchSpawnAt( self, ids: {integer}, componentTypes: {components.Component}, callback: function(Archetype, integer, integer, integer) ) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | | `ids` | `{integer}` | Explicit entity IDs to spawn, in order. | | `componentTypes` | `{`[`components.Component`](/modules/ecs/#tecs.ecs.Component)`}` | Array of component types defining the archetype. | | `callback` | `function(`[`Archetype`](/modules/ecs/#tecs.ecs.Archetype)`, integer, integer, integer)` | Called at publication with `(archetype, firstRow, lastRow, count)`. | ##### Returns None. #### tecs.World:byKey Instance Return the live entity currently carrying `tecs.ecs.EntityKey(key)`, or nil if no live entity has that key. ```teal function tecs.World.byKey(self, key: string): integer | nil ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | | `key` | `string` | The durable key to find. | ##### Returns | Type | Description | | --- | --- | | integer | nil | The live entity, or nil when the key has none. | #### tecs.World:clearEntities Instance Wipe all entity data but preserve structural state (pipeline, registered systems, queries, query observers, archetype column capacity). Use this for per-test reuse, benchmark setup, and save/load clear-before-load. Clears: entities, pending transaction, sparse relationship stores and queued messages. Preserves: systems, queries, archetype columns, observers. If you need post-construction state (systems gone, queries gone), just call `tecs.ecs.newWorld()` -- same path, clearer intent. ```teal function tecs.World.clearEntities(self) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | ##### Returns None. #### tecs.World:clearObservers Instance Clear all observers for an address (used on entity despawn). ```teal function tecs.World.clearObservers(self, address: integer) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | | `address` | `integer` | Every event type at this address is cleared, not one. Address zero is the world's own, so clearing it removes every broadcast observer the world has. | ##### Returns None. #### tecs.World:compact Instance Compact the world: prune dead archetypes whose relationship targets have been despawned and shrink overallocated archetype storage. Must be called on a quiet world (no pending mutations). ```teal function tecs.World.compact(self): integer, integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | ##### Returns | Type | Description | | --- | --- | | `integer` | archetypesPruned Number of dead archetypes removed. | | `integer` | archetypesCompacted Number of archetypes whose storage was shrunk. | #### tecs.World:createState Instance Create a named state with an optional lifecycle policy. Returns a tag component that is auto-added to entities spawned while this state is on top of the stack. ```teal function tecs.World.createState( self, name: string, policy: StatePolicy ): components.Component ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | | `name` | `string` | The name of the state. | | `policy` | `StatePolicy` | Optional lifecycle policy for state transitions. | ##### Returns | Type | Description | | --- | --- | | [`components.Component`](/modules/ecs/#tecs.ecs.Component) | The tag component for this state. | #### tecs.World:despawn Instance Despawn an entity. ```teal function tecs.World.despawn(self, entity: integer) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | | `entity` | `integer` | The entity to despawn. | ##### Returns None. #### tecs.World:dirtyArchetypes Instance Return an iterator over archetypes whose rows have been mutated since the last `world:update()`. Used by systems that consume dirty state incrementally (GPU shadow upload, reactive systems, debug tooling). The set is cleared automatically at the end of each `update()` after the pipeline finishes. Same iteration constraint as queries: do not mutate the world during iteration. ```teal function tecs.World.dirtyArchetypes(self): function(): Archetype ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | ##### Returns | Type | Description | | --- | --- | | `function(): `[`Archetype`](/modules/ecs/#tecs.ecs.Archetype) | | #### tecs.World:disablePhase Instance Disable a phase. ```teal function tecs.World.disablePhase(self, phase: Phase) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | | `phase` | [`Phase`](/modules/ecs/#tecs.ecs.Phase) | The phase to disable. | ##### Returns None. #### tecs.World:emit Instance Emit an event to an address. Use 0 for world-level events, entity ID for entity events. ```teal function tecs.World.emit( self, address: integer, eventOrType: events.Event, ...: any ) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | | `address` | `integer` | The address to emit to (0 for world, entity ID for entity). | | `eventOrType` | `events.Event` | The event instance to emit, or an event type plus constructor args. | | `...` | `any` | Constructor arguments when `eventOrType` is an event type. | ##### Returns None. #### tecs.World:enablePhase Instance Enable a phase. ```teal function tecs.World.enablePhase(self, phase: Phase) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | | `phase` | [`Phase`](/modules/ecs/#tecs.ecs.Phase) | The phase to enable. | ##### Returns None. #### tecs.World:enqueueCommit Instance Requests publication of the world's staged structural mutations. Outside system dispatch, this publishes synchronously before the call returns. During a system, the request is coalesced with any other requests from that system and the pipeline publishes after the system returns, before the next system runs. The requesting system therefore does not observe its own structural changes. Do not call this from inside manual query traversal. Outside a phase it may change archetype storage immediately; finish the traversal first, then request publication. ```teal function tecs.World.enqueueCommit(self) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | ##### Returns None. #### tecs.World:findArchetypes Instance Find archetypes that have the given component. ```teal function tecs.World.findArchetypes( self, component: components.Component ): function(): (Archetype, integer, DoubleArray) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | | `component` | [`components.Component`](/modules/ecs/#tecs.ecs.Component) | The component to find. | ##### Returns | Type | Description | | --- | --- | | `function(): (`[`Archetype`](/modules/ecs/#tecs.ecs.Archetype)`, integer, DoubleArray)` | an iterator over the archetypes that have the component. | #### tecs.World:fixedStepCount Instance Fixed steps run since the world was made. The clock for anything that advances on the simulation rather than on frame time. It counts steps rather than summing seconds, so two runs fed the same steps read the same number however many frames either of them drew, and the value stays a whole number. Counted whether or not any system is registered in a fixed phase. ```teal function tecs.World.fixedStepCount(self): integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | ##### Returns | Type | Description | | --- | --- | | `integer` | Steps run, from zero. | #### tecs.World:forEachArchetype Instance Iterate all archetypes in the world. Mainly useful for save game / debugging tools that need to walk entity state without matching a specific component signature. ```teal function tecs.World.forEachArchetype( self, callback: function(Archetype) ) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | | `callback` | `function(`[`Archetype`](/modules/ecs/#tecs.ecs.Archetype)`)` | Called with each archetype. | ##### Returns None. #### tecs.World:get Instance Get a component instance attached to an entity. ```teal function tecs.World.get( self, entity: integer, component: T ): T ``` ##### Type Parameters | Name | Constraint | Description | | --- | --- | --- | | `T` | [`components.Component`](/modules/ecs/#tecs.ecs.Component) | | ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | | `entity` | `integer` | Entity ID. | | `component` | `T` | Component type to get. | ##### Returns | Type | Description | | --- | --- | | `T` | The component instance or nil if not found. | #### tecs.World:getBundle Instance Get a registered bundle by name. ```teal function tecs.World.getBundle(self, name: string): Bundle | nil ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | | `name` | `string` | The bundle name. | ##### Returns | Type | Description | | --- | --- | | Bundle | nil | The bundle, or nil if not found. | #### tecs.World:getBundles Instance Get all registered bundles. ```teal function tecs.World.getBundles(self): {string: Bundle} ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | ##### Returns | Type | Description | | --- | --- | | `{string : Bundle}` | A fresh map of bundle name to bundle. | #### tecs.World:getFirstRelationship Instance Get the first relationship instance for a relationship container on an entity. For exclusive relationships (like ChildOf), this returns the single instance. ```teal function tecs.World.getFirstRelationship( self, entity: integer, relationship: T ): T ``` ##### Type Parameters | Name | Constraint | Description | | --- | --- | --- | | `T` | [`components.Relationship`](/modules/ecs/#tecs.ecs.Relationship) | | ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | | `entity` | `integer` | Entity ID. | | `relationship` | `T` | Relationship container type. | ##### Returns | Type | Description | | --- | --- | | `T` | The relationship instance or nil if not found. | #### tecs.World:getFixedTiming Instance Return fixed-step timing for interpolation consumers. The return values are the fixed timestep, the residual time not consumed by fixed updates, and the residual divided by the timestep clamped to `[0, 1]`. This method does not allocate. The fixed-step clock advances even when fixed phases are disabled. ```teal function tecs.World.getFixedTiming(self): number, number, number ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | ##### Returns | Type | Description | | --- | --- | | `number` | timestep Fixed update interval in seconds. | | `number` | accumulator Residual time in seconds. | | `number` | alpha Clamped interpolation fraction. | #### tecs.World:getMut Instance Mutable counterpart to `get`. Returns the component AND marks it dirty on the entity's archetype so dirty-gated consumers (shadow pipeline, change observers) re-process the row after subsequent cdata writes. Use this whenever you intend to mutate the component through the returned reference. `get` + cdata write silently bypasses dirty tracking and leaves stale state downstream. This access is immediate because it cannot change an archetype. Side effects performed through the returned object therefore also happen immediately. An entity spawned since the last barrier is not live yet, so `getMut` returns nil for it. Spawn with final values or wait for the next declared pipeline barrier. ```teal function tecs.World.getMut( self, entity: integer, component: T ): T ``` ##### Type Parameters | Name | Constraint | Description | | --- | --- | --- | | `T` | [`components.Component`](/modules/ecs/#tecs.ecs.Component) | | ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | | `entity` | `integer` | Entity ID. | | `component` | `T` | Component type to get-and-mark-dirty. | ##### Returns | Type | Description | | --- | --- | | `T` | The component instance or nil if not found. | #### tecs.World:getStats Instance Get stats about the World. ```teal function tecs.World.getStats(self, fill: World.Stats): World.Stats ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | | `fill` | `World.Stats` | Optional stats table to fill instead of allocating a new one. Passing one every frame is how this is read without adding a table per frame to the collector. | ##### Returns | Type | Description | | --- | --- | | `World.Stats` | The same table `fill` named when one was given, so the caller already holds it. A fresh one otherwise, and either way a copy rather than a view: the numbers do not update on their own. | #### tecs.World:has Instance Check whether an entity currently has a component. For sparse relationships: - passing the relationship container checks whether the entity has any target for that relationship - passing a relationship instance checks that specific target ```teal function tecs.World.has( self, entity: integer, component: components.Component ): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | | `entity` | `integer` | Entity ID. | | `component` | [`components.Component`](/modules/ecs/#tecs.ecs.Component) | Component type (or relationship instance) to check. | ##### Returns | Type | Description | | --- | --- | | `boolean` | true if present, false otherwise. | #### tecs.World:hasObservers Instance Check if there are observers for an event at an address. ```teal function tecs.World.hasObservers( self, address: integer, event: T ): boolean ``` ##### Type Parameters | Name | Constraint | Description | | --- | --- | --- | | `T` | `events.Event` | | ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | | `address` | `integer` | The world address or entity identifier. | | `event` | `T` | The event type to inspect. | ##### Returns | Type | Description | | --- | --- | | `boolean` | True when the address has an observer for this event type. | #### tecs.World:isAlive Instance Check if an entity is committed and alive. ```teal function tecs.World.isAlive(self, entity: integer): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | | `entity` | `integer` | The entity to check. | ##### Returns | Type | Description | | --- | --- | | `boolean` | true if the entity is committed and alive, false otherwise. | #### tecs.World:listStates Instance Reports the names on the state stack, bottom-first. The last name is the one `peekState` answers with. Read the whole stack to tell a pause pushed over play from a pause that is all there is, which is what a save prompt, a back button and a debugger each need and what the top name alone cannot say. States the world created and never pushed are not on the stack and are not reported. ```teal function tecs.World.listStates(self): {string} ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | ##### Returns | Type | Description | | --- | --- | | `{string}` | A fresh list of names the caller owns, bottom-first, and empty when nothing is pushed. It is a copy taken at the call rather than a view, so a later push or pop does not reach it. | #### tecs.World:listSystems Instance Reports every system this world runs. Read it to draw a system overlay, to check what a plugin registered, or to find the name to hand `setSystemEnabled`. A system a plugin added without a name reports the synthetic name the pipeline gave it. ```teal function tecs.World.listSystems(self): {SystemInfo} ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | ##### Returns | Type | Description | | --- | --- | | `{`[`SystemInfo`](/modules/ecs/#tecs.ecs.SystemInfo)`}` | A fresh list the caller owns, ordered by phase in the order the lifecycle reaches each phase, and within a phase in the order the systems run. Each row is a copy taken at the call, so it does not follow a later enable or disable. | #### tecs.World:loadSnapshot Instance Restore the world from either binary or table form. `source` may be a Lua string, a LuaJIT `string.buffer`, or a snapshot table previously returned by `saveSnapshot` with `format = "table"` (or parsed from JSON). ```teal function tecs.World.loadSnapshot(self, source: any): SnapshotPrelude ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | | `source` | `any` | One of the three forms above. Every entity already in the world is despawned before the first one is restored, so this replaces a world rather than merging into one. | ##### Returns | Type | Description | | --- | --- | | [`SnapshotPrelude`](/modules/ecs/#tecs.World.SnapshotPrelude) | What the snapshot said about itself. The prelude is read first but handed back last, so a caller that meant to reject an unwanted version reads it after the world has already been replaced. Raises while a logical world update is suspended. | #### tecs.World:markComponentDirty Instance Mark a component dirty on the entity's archetype. Used when code mutates a component's bytes through a path that doesn't go through `archetype:getMut` (e.g. a smart wrapper that holds an entity id and writes through a fetched FFI cdata). ```teal function tecs.World.markComponentDirty( self, entity: integer, component: components.Component ) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | | `entity` | `integer` | Entity ID. | | `component` | [`components.Component`](/modules/ecs/#tecs.ecs.Component) | Component type whose column was mutated. | ##### Returns None. #### tecs.World:newBundle Instance Create and register a bundle with the world. ```teal function tecs.World.newBundle( self, name: string, def: Bundle.Definition ): Bundle ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | | `name` | `string` | The name of the bundle. | | `def` | `Bundle.Definition` | Bundle definition with required and with-default components. | ##### Returns | Type | Description | | --- | --- | | `Bundle` | The registered bundle. | #### tecs.World:newQuery Instance Creates a new query. Iteration does NOT auto-mark anything dirty; mutation intent lives at the access site via `archetype:getMut(Foo)` inside the iter loop, which marks just that component dirty on the archetype. ```teal function tecs.World.newQuery( self, descriptor: Query.Descriptor ): Query ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | | `descriptor` | [`Query.Descriptor`](/modules/ecs/#tecs.Query.Descriptor) | Read here and not retained, so editing it afterwards does not change the query. Give it a `name` for anything that outlives a call: the debugger and the profiles address queries by name. | ##### Returns | Type | Description | | --- | --- | | [`Query`](/modules/ecs/#tecs.Query) | A query that keeps itself current as archetypes appear, so it is built once during plugin setup and reused. Building one inside a system's `run` pays the match cost every frame. | #### tecs.World:observe Instance Observe an event at an address. Use 0 for world-level events, entity ID for entity events. ```teal function tecs.World.observe( self, address: integer, event: T, callback: function(T), id: string ) ``` ##### Type Parameters | Name | Constraint | Description | | --- | --- | --- | | `T` | `events.Event` | | ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | | `address` | `integer` | The address to observe (0 for world, entity ID for entity). | | `event` | `T` | The event type to observe. | | `callback` | `function(T)` | The callback to call when the event is emitted. | | `id` | `string` | Optional ID for the observer. | ##### Returns None. #### tecs.World:peekState Instance Peek at the current top state name. ```teal function tecs.World.peekState(self): string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | ##### Returns | Type | Description | | --- | --- | | `string` | The name of the top state, or nil if the stack is empty. | #### tecs.World:popState Instance Pop the current state from the state stack. Fires the current state's onExit policy and the new top state's onFocus policy. ```teal function tecs.World.popState(self) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | ##### Returns None. #### tecs.World:pushState Instance Push a state onto the state stack. Fires the previous top state's onBlur policy and the new state's onEnter policy. Entities spawned after this call will automatically receive the state's tag component. ```teal function tecs.World.pushState(self, name: string) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | | `name` | `string` | The state name (must have been created with createState). | ##### Returns None. #### tecs.World:registerPhase Instance Register a custom phase with the world's pipeline. This allows external modules to define their own phases. ```teal function tecs.World.registerPhase(self, phase: Phase) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | | `phase` | [`Phase`](/modules/ecs/#tecs.ecs.Phase) | The phase to register. | ##### Returns None. #### tecs.World:remove Instance Remove a component from an entity. The removal is structural and becomes visible at the next pipeline barrier. ```teal function tecs.World.remove( self, entity: integer, component: components.Component ) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | | `entity` | `integer` | Entity ID. | | `component` | [`components.Component`](/modules/ecs/#tecs.ecs.Component) | Component type to remove. | ##### Returns None. #### tecs.World:removeSystem Instance Remove a system from the world. ```teal function tecs.World.removeSystem(self, systemName: string) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | | `systemName` | `string` | The name of the system to remove. | ##### Returns None. #### tecs.World:requireKey Instance Return the live entity currently carrying `tecs.ecs.EntityKey(key)`, or error if no live entity has that key. ```teal function tecs.World.requireKey(self, key: string): integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | | `key` | `string` | The durable key to find. | ##### Returns | Type | Description | | --- | --- | | `integer` | The live entity. | #### tecs.World:runPhase Instance Run every system registered to `phase` (and its enabled descendants) with the given `dt`. Publishes pending structural work before dispatch and after each phase, but unlike `update` does not clear dirty bits. Useful for piecewise phase execution (e.g. custom game loops splitting Update and Render across distinct ticks). Raises when a world update is suspended, because another phase cannot publish its half-staged transaction. ```teal function tecs.World.runPhase(self, phase: Phase, dt: number) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | | `phase` | [`Phase`](/modules/ecs/#tecs.ecs.Phase) | The phase to run. | | `dt` | `number` | The elapsed seconds passed to its systems, or zero when omitted. | ##### Returns None. #### tecs.World:saveSnapshot Instance Save the world to either binary or table form. Binary is the default. Set `opts.format = "table"` for a plain Lua snapshot table. `opts.path` is supported only for binary output. ```teal function tecs.World.saveSnapshot( self, opts: SnapshotOptions ): SnapshotOutput ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | | `opts` | [`SnapshotOptions`](/modules/ecs/#tecs.World.SnapshotOptions) | May be nil, which writes binary and returns it rather than writing a file. Components registered `transient` are left out whatever this says, so a snapshot never carries a live handle. | ##### Returns | Type | Description | | --- | --- | | [`SnapshotOutput`](/modules/ecs/#tecs.World.SnapshotOutput) | The snapshot, in whichever form was asked for. Taken from the world as it stands, so this is a point-in-time copy and later changes do not reach it. Raises while a logical world update is suspended, because its deferred phase is not committed state. | #### tecs.World:set Instance Add or update a component on an entity. Replacing a value the entity already carries is immediate and marks its column dirty. Adding a component is structural and becomes visible at the next pipeline barrier. ```teal function tecs.World.set( self, entity: integer, component: components.Component, value: any ) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | | `entity` | `integer` | Entity ID. | | `component` | [`components.Component`](/modules/ecs/#tecs.ecs.Component) | The component instance or scalar component type. | | `value` | `any` | The scalar value, or nil to use the registered default. | ##### Returns None. #### tecs.World:setSystemEnabled Instance Enables or disables one system, leaving it registered either way. A disabled system keeps its name, its position and its ordering constraints, and stops running from the next `world:update`. Enabling it restores the `runIf` it declared for itself, so a gated system comes back gated rather than unconditional. Disabling a system that is already disabled changes nothing and reports success. Use `removeSystem` instead when the system is never to run again; this is the pause a debugger, a cheat menu or a test wants, and the system is one call away from running again. ```teal function tecs.World.setSystemEnabled( self, systemName: string, enabled: boolean ): boolean, string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | | `systemName` | `string` | The name the system is registered under, which `listSystems` reports. A name no system carries is an operational outcome rather than a programmer error, because the name usually comes from a person or an agent. | | `enabled` | `boolean` | True lets the system run again, and false skips it until something enables it. | ##### Returns | Type | Description | | --- | --- | | `boolean` | True once the system carries that state, including when it already did. | | `string` | The reason no system carries that name, when the first return is false. | #### tecs.World:shutdown Instance Run the shutdown systems. ```teal function tecs.World.shutdown(self) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | ##### Returns None. #### tecs.World:spawn Instance Reserve a new entity and stage its placement with optional variadic components. The ID returns immediately; the entity becomes alive and queryable at the next pipeline barrier. ```teal function tecs.World.spawn( self, ...: components.Component ): integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | | `...` | [`components.Component`](/modules/ecs/#tecs.ecs.Component) | Variable number of components to add to the entity. | ##### Returns | Type | Description | | --- | --- | | `integer` | The ID of the spawned entity. | #### tecs.World:spawnAt Instance Spawn an entity at a specific packed id rather than auto- allocating. The id carries both slot and generation, so relationship targets resolve to the same entity across a save/load cycle. The caller is responsible for ensuring the id's slot is not already live. ```teal function tecs.World.spawnAt( self, id: integer, ...: components.Component ) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | | `id` | `integer` | The packed entity id to reuse. | | `...` | [`components.Component`](/modules/ecs/#tecs.ecs.Component) | Variable number of components to add to the entity. | ##### Returns None. #### tecs.World:spawnBundle Instance Reserve an entity using a registered bundle and stage its placement for the next pipeline barrier. ```teal function tecs.World.spawnBundle( self, name: string, ...: components.Component ): integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | | `name` | `string` | The name of the bundle. | | `...` | [`components.Component`](/modules/ecs/#tecs.ecs.Component) | Component overrides (required components must be provided). | ##### Returns | Type | Description | | --- | --- | | `integer` | The entity ID. | #### tecs.World:startup Instance Run the startup systems. Raises when a world update is suspended, because another phase cannot publish its half-staged transaction. ```teal function tecs.World.startup(self) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | ##### Returns None. #### tecs.World:stopObserving Instance Stop observing an event at an address. ```teal function tecs.World.stopObserving( self, address: integer, event: T, observer: function(T) | string ) ``` ##### Type Parameters | Name | Constraint | Description | | --- | --- | --- | | `T` | `events.Event` | | ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | | `address` | `integer` | The address to stop observing. | | `event` | `T` | The event type to stop observing. | | `observer` | function(T) | string | The observer function or ID. | ##### Returns None. #### tecs.World:targets Instance Get all source entities that target a given entity via a sparse relationship. For ChildOf, this returns the children of the entity. The optional `context` is forwarded to the callback as its second argument so visitors can be hoisted to module scope and read/write state via the context table without per-call closure allocation. Context is appended last so existing single-arg callbacks (`function(srcId)`) keep working -- Lua silently drops extra args. ```teal function tecs.World.targets( self, entity: integer, relationship: components.Relationship, callback: function(integer, T), context: T ) ``` ##### Type Parameters | Name | Constraint | Description | | --- | --- | --- | | `T` | | | ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | | `entity` | `integer` | The target entity ID. | | `relationship` | [`components.Relationship`](/modules/ecs/#tecs.ecs.Relationship) | The sparse relationship container. | | `callback` | `function(integer, T)` | Receives (sourceId, context) for each source. | | `context` | `T` | Optional context value forwarded to the callback. | ##### Returns None. #### tecs.World:traverse Instance Depth-first traversal over a sparse relationship's inverse index. For example, this can be used to walk the ChildOf hierarchy of a node from the top down. ```teal function tecs.World.traverse( self, root: integer, relationship: components.Relationship ): function(): (integer, integer) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | | `root` | `integer` | The root entity ID. | | `relationship` | [`components.Relationship`](/modules/ecs/#tecs.ecs.Relationship) | The sparse relationship container. | ##### Returns | Type | Description | | --- | --- | | `function(): (integer, integer)` | An iterator yielding (depth, entityId) for each descendant. | #### tecs.World:update Instance Updates or resumes one logical world update. An asynchronous call inside a system can suspend the update. The application continues pumping I/O and calls this again until it completes. A headless loop that drives a world directly must do the same; `dt` is ignored while resuming suspended work. ```teal function tecs.World.update(self, dt: number): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | | `dt` | `number` | The time since the last completed update. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns true when the logical update completed, or false when a system is still waiting. | #### tecs.World:walkUp Instance Walk up a relationship chain, calling `callback` for each ancestor. Follows the first target per level (equivalent to repeated `getFirstRelationship`), so semantics match exclusive relationships like `ChildOf` exactly and fall back to "first edge" for non-exclusive ones. Depth starts at 1 (direct parent) and increments per level. The callback may return `false` to stop the walk early; any other return value (including nil / no return) continues. `maxDepth` defaults to 100 and triggers a hard error if exceeded so accidental cycles surface immediately. The optional `context` is passed through to the callback unchanged, so a visitor function can live at module scope and read/write its state via the context table without per-call closure allocation. ```teal function tecs.World.walkUp( self, entity: integer, relationship: components.Relationship, callback: function(integer, integer, T): boolean, context: T, maxDepth: number ) ``` ##### Type Parameters | Name | Constraint | Description | | --- | --- | --- | | `T` | | | ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `World` | | | `entity` | `integer` | The entity to walk up from. | | `relationship` | [`components.Relationship`](/modules/ecs/#tecs.ecs.Relationship) | The relationship to follow. | | `callback` | `function(integer, integer, T): boolean` | Receives (ancestorId, depth, context). Return `false` to stop. | | `context` | `T` | Optional context value forwarded to the callback as its 3rd arg. | | `maxDepth` | `number` | Safety cap, defaults to 100. Errors if exceeded. | ##### Returns None. ## Functions ### tecs.ecs.declaredComponents Static Returns every component declared in this process. A fresh table per call, so the registry itself stays unwritable from outside registration. The result omits dense relationship instances. Registration creates one stamped component per target, and no caller declared those components. ```teal function tecs.ecs.declaredComponents(): {string: Component} ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | `{string : `[`Component`](/modules/ecs/#tecs.ecs.Component)`}` | Returns a fresh caller-owned name-to-component table that omits generated dense relationship instances. | ### tecs.ecs.findComponentById Static Returns a registered component by numeric id. ```teal function tecs.ecs.findComponentById(id: integer): Component ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `id` | `integer` | The caller supplies a process-wide component id. | #### Returns | Type | Description | | --- | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | Returns the component, or nil for an unallocated id. Numeric ids do not remain stable across runs. | ### tecs.ecs.findComponentByName Static Returns the component registered under `name`. The process-wide registry allocates each component id once, and every world agrees on it. This function answers which components have names; a world answers which components it carries. Finds a dense relationship instance by its stamped name as well, which `declaredComponents` leaves out. ```teal function tecs.ecs.findComponentByName(name: string): Component ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `name` | `string` | The caller supplies the process-wide registration name. | #### Returns | Type | Description | | --- | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | Returns the component, or nil when nothing has registered that name. | ## Values ### tecs.ecs.ChildOf variable Read-only. [`ChildOf`](/modules/ecs/#tecs.ecs.ChildOf) links a parent and child. It stores edges sparsely and cascade-deletes children with their parent. ```teal tecs.ecs.ChildOf: Relationship ``` ### tecs.ecs.DEFAULT_MAX_ENTITIES variable Read-only. `DEFAULT_MAX_ENTITIES` supplies 2^20 when world configuration omits `maxEntities`. ```teal tecs.ecs.DEFAULT_MAX_ENTITIES: integer ``` ### tecs.ecs.Disabled variable Read-only. [`Disabled`](/modules/ecs/#tecs.ecs.Disabled) excludes an entity from queries that do not ask for it. ```teal tecs.ecs.Disabled: Component ``` ### tecs.ecs.EntityKey variable Read-only. [`EntityKey`](/modules/ecs/#tecs.ecs.EntityKey) stores a durable unique lookup key for `world:byKey`. It supports hot reload, authored references, tooling, and save-compatible lookup. The registered component name is the externally typed string `"Key"`. [`tecs.data.Key`](/modules/data/#tecs.data.Key) names typed store keys. ```teal tecs.ecs.EntityKey: ScalarComponent ``` ### tecs.ecs.MAX_ENTITIES variable Read-only. `MAX_ENTITIES` sets the absolute `World.Config.maxEntities` ceiling (2^22 - 1 usable slots; the entity-id format reserves slot 0). ```teal tecs.ecs.MAX_ENTITIES: integer ``` ### tecs.ecs.Name variable Read-only. [`Name`](/modules/ecs/#tecs.ecs.Name) stores an entity label as a raw string. It does not enforce uniqueness. Use [`EntityKey`](/modules/ecs/#tecs.ecs.EntityKey) for durable unique lookup. ```teal tecs.ecs.Name: ScalarComponent ``` ### tecs.ecs.Paused variable Read-only. [`Paused`](/modules/ecs/#tecs.ecs.Paused) excludes an entity from logic queries while keeping it visible. [`Disabled`](/modules/ecs/#tecs.ecs.Disabled) excludes both logic and rendering queries. ```teal tecs.ecs.Paused: Component ``` ### tecs.ecs.runif variable Read-only. `runif` contains composable system run conditions. ```teal tecs.ecs.runif: runIfHelpers ``` --- ## Mutation model # Mutation model Tecs has one structural mutation model. Spawns, despawns, component additions, component removals, bundle spawns, and every batch operation stage work into the world's current transaction. They never change archetype rows before returning. The pipeline owns normal publication. It settles the transaction before lifecycle dispatch and after each non-empty phase. A system may declare an unconditional additional barrier with `commitBefore` or `commitAfter`, and `world:enqueueCommit()` requests a conditional safe-point barrier. ## Mutation classes | Class | APIs | Visibility | | -------------- | --------------------------------------------------------------------- | ------------------------------------ | | Structural | `spawn`, `despawn`, structural `set`, `remove`, bundles, and `batch*` | Next pipeline barrier | | Value | `set` for a component already present | Before return | | Mutable access | `world:getMut` and `archetype:getMut` | The returned live value is immediate | | Explicit ID | `spawnAt`, `batchSpawnAt`, and snapshot restore | Next owning internal barrier | | Sparse | Sparse relationship `set` and `remove` | Last step of transaction settle | `getMut` is immediate because it cannot change an entity's archetype. It marks the component column dirty before returning the live value. Any other side effect performed through that value also happens immediately; Tecs does not attempt to roll value writes back when later structural work fails. `set` has the same fast path when the committed entity already owns the component. Once that entity has a pending structural change, later values join its staged destination so the transaction still has one final result. ## Publication ownership The standard lifecycle publishes at these points: 1. `startup`, `update`, `runPhase`, and `shutdown` publish work queued before dispatch begins. 2. Systems in one phase normally run against the same committed structure. 3. The pipeline publishes after each non-empty phase. 4. A system with `commitBefore = true` adds a barrier before its `runIf` and `run`. A system with `commitAfter = true` adds one after its dispatch. 5. `enqueueCommit()` called by a system coalesces into one barrier after that system returns and before the next system runs. The declarations are scheduler metadata for unconditional dependencies. `enqueueCommit()` is the conditional form: it sets one request bit during system dispatch, so calling it more than once still creates only one barrier. The requesting system retains its stable view and cannot observe the structural work it just staged. If it is the last system, the request is honored before the ordinary phase-end barrier, which then has nothing left to publish. Use `commitBefore` when a system consumes archetype membership produced by an earlier system in the same phase. Use `commitAfter` when a later system in that phase must consume this system's structural output. Prefer a later phase when the dependency is part of the frame's normal architecture. Call `enqueueCommit()` inside `run` only when whether the next system needs the output is known at runtime. ```teal world:addSystem({ name = "game.ResolveSpawns", phase = tecs.ecs.phases.Update, commitBefore = true, run = resolveSpawns, }) ``` `runIf` should remain a predicate. A declared pre-barrier runs before it, and a declared post-barrier runs after the dispatch slot even when the predicate skips the system, so the publication schedule does not depend on dynamic code. Outside system dispatch, `enqueueCommit()` publishes synchronously before it returns. Tests use that behavior to inspect a settled world, and the built-in MCP mutation tools use it so their responses describe the mutation they just performed. This is still the one deferred structural model: each operation stages first, and the explicit request only selects its publication boundary. ## Entity transaction states One transaction places each entity ID in exactly one state: | State | Meaning | | -------------- | --------------------------------------------------------------------- | | Committed | The entity occupies an archetype and has no staged structural change. | | Staged spawn | The transaction reserved the ID but has not placed the entity. | | Staged mutate | A committed entity has a staged component addition or removal. | | Staged despawn | The transaction recorded despawn but has not removed the row. | | State | `isAlive` | `get` and `has` | Queries | | -------------- | --------- | ------------------------- | ------------------------------ | | Committed | `true` | See committed values. | Match committed structure. | | Staged spawn | `false` | Return `nil` and `false`. | Do not match. | | Staged mutate | `true` | See committed structure. | Match the committed archetype. | | Staged despawn | `true` | See committed values. | Match until row removal. | Publication clears every staged state. A spawn returns its reserved ID immediately, so later calls in the same transaction may modify or cancel it. Pass final values to `spawn` when code needs them before the next barrier; `getMut` cannot return a value for a staged spawn. ## Operations across states `set` behaves as follows: - A committed value replacement writes immediately and marks its column dirty. - A committed component addition stages an archetype move. - A staged spawn or mutate updates its final staged shape and value. - A staged despawn ignores the call. `remove` stages a move for a committed entity, edits the final shape of a staged spawn or mutate, and ignores a staged despawn. `despawn` stages removal for a committed entity, cancels placement of a staged spawn, replaces a staged move with removal, and ignores a repeated despawn. `spawnAt` requires a non-live ID. It revives a free slot and reconciles allocator state. Passing a live ID violates the caller contract. ## Settle order One publication drains dirty archetypes in fixed-point waves: 1. Despawns remove rows and free capacity. 2. Bundle queues, batch queues, explicit-ID batches, and ordinary spawns place rows. 3. Structural moves relocate rows and write final component values. 4. Batch `set` and `remove` operations run in call order. 5. Sparse relationship writes flush to their stores. Observers and query callbacks may stage more work during settle. That work starts another wave. The drain stops at a fixed point or raises after 64 waves with a likely observer-cascade error. The settle guarantees that despawns precede spawns within a wave, the last staged write wins, net-zero remove/add changes keep their row, despawn cancels other staged work for that entity, and recycled slots cannot inherit sparse writes from their previous entity. Sparse structural moves use a compact list of touched slots when the changed set is small relative to its source archetype. Dense changes retain a linear descending scan. Spawn staging transfers packed value arrays directly and stores no payload for tags. These are settle implementation details; neither changes transaction ordering or visibility. ## Query iteration Query iteration does not own transaction lifetime. `query:iter()` and grouped iteration read committed archetypes while structural calls stage for a later pipeline barrier. Breaking, returning, raising, or suspending a coroutine cannot leave the world in a special mutation mode and requires no iterator cleanup. Columns and entity arrays are live archetype storage. They remain valid until a later publication changes that archetype. Outside a system, `enqueueCommit()` may publish immediately, so finish manual traversal and release retained archetype storage before calling it. Inside a system the request waits until the system returns and is safe in a query loop. ## Dead and stale entity IDs Entity generations reject a handle after slot reuse: | Operation | Dead or stale ID | | ----------------------------------------- | ---------------------------------- | | `get`, `getMut` | Return `nil`. | | `has`, `isAlive` | Return `false`. | | `markComponentDirty`, `remove`, `despawn` | Do nothing. | | `set` | Raise `Entity ID not found: `. | ## Lifecycle events `spawn`, `spawnAt`, and bundle spawn emit `OnSpawn` once per entity during the call, after its final initial shape has been staged. The entity is not alive or queryable yet, but observers may stage more mutations against its ID. `despawn` and `batchDespawn` emit `OnDespawn` once at the entity address and once at address `0` before physical removal. The entity remains alive and readable during dispatch. Tecs clears entity-address observers after fan-out. `batchSpawn` and `batchSpawnAt` emit neither lifecycle event. Use their fill callback or archetype `onEntitiesAdded` notification. Query callbacks run when settle actually adds or removes matching rows. ## Dirty tracking Dirty bits belong to an archetype and component. Mutation paths maintain them; callers still owe these access rules: - Read through `get`; take `getMut` only when code will write. - A direct cdata write through `get` needs `markComponentDirty(entity, Component)`. - `world:update` clears dirty bits after the pipeline, once every consumer has had a chance to observe them. - `batchSpawn` runs no component constructors and applies only `requires` defaults before its callback. The callback must write every field it uses. ## Structural invariants Every membership path honors the same rules: - Adding a component includes its transitive `requires` closure in one final archetype transition. Caller values override required defaults. - A dense relationship instance adds its wildcard container. - Spawn paths add the active state tag; `set` and `remove` do not. - Key claiming rejects duplicate live values before visibility. Bulk spawn paths reject `EntityKey`. - Scalar columns store raw values, and tags store no per-row value. - Table defaults and bundle factories create a fresh table per row. - Sparse relationships route through the world store. Exclusive edges evict the prior target, reverse indexes unlink before linking, and cascade delete recursively stages source despawns. - Value writes dirty component columns; placements and moves dirty archetypes. - Entity-address observers never survive slot reuse. --- ## Phases # Phases A system names the phase that runs it: ```teal world:addSystem({ name = "game.StepEnemies", phase = tecs.ecs.phases.FixedUpdate, run = stepEnemies, }) world:addSystem({ name = "game.FadeTints", phase = tecs.ecs.phases.Update, run = fadeTints, }) ``` `FixedUpdate` runs on the simulation clock. `Update` runs once per frame on the presentation clock. The phase gives game systems an order relative to engine systems. [`Application`](/modules/Application) calls `world:startup()` once, `world:update(dt)` every iteration, and `world:shutdown()` at teardown. Events do not occupy a phase; see [Observer timing](/modules/ecs/events#observer-timing). ## Lifecycle groups The phase tree hangs from `tecs.ecs.phases.AllGroups`: - `startup()` runs `StartupGroup`: `PreStartup`, `Startup`, and `PostStartup`. - `update(dt)` runs `MainGroup`: `First`, `PreUpdate`, `FixedUpdateGroup`, `Update`, `PostUpdate`, `RenderGroup`, and `Last`. - `shutdown()` runs `ShutdownGroup`: `PreShutdown`, `Shutdown`, and `PostShutdown`. `FixedUpdateGroup` contains `FixedFirst`, `FixedPreUpdate`, `FixedUpdate`, `FixedPostUpdate`, and `FixedLast`. `RenderGroup` contains `RenderFirst`, `PreRender`, `Render`, `PostRender`, and `RenderLast`. Application startup runs after the entry plugin registers its systems and entities. Startup work therefore finishes before the first frame and does not inflate that frame's `dt`. ## Engine system order The engine installs its work into the same tree: | Phase | Engine work | | ----------------- | --------------------------------------------------------------------------------------- | | `First` | Advance frame-clock sequences | | `FixedFirst` | Latch fixed input, snapshot transforms, advance fixed-clock sequences | | `FixedUpdate` | Run TTL and physics | | `FixedPostUpdate` | Copy physics poses | | `FixedLast` | Leave fixed-input mode | | `Update` | Advance presentation-clock sequences | | `PostUpdate` | Compose relative transforms, play sounds, encode animation, lay out text, sync emitters | | `RenderFirst` | Extract the world into a frame packet | | `RenderLast` | Sample relative-transform dirtiness | Plugins install optional rows such as physics, animation, text, and particles. Every world installs the builtin rows. Extraction runs in `RenderFirst`. A system that changes what the current frame draws must run before extraction. `PostUpdate` provides the last general phase for that work. A change made after extraction draws one frame late rather than never. Spawning, despawning, and writing a component the renderer draws from all reach the instance buffer on the next frame's extraction, even though the frame's dirty marks are cleared in between. Latency is the whole of the cost, so a system that has to run in `Render`, `PostRender`, `RenderLast`, or `Last` is free to write; one that needs the current frame to show its change still belongs earlier. GPU submission does not run as a system. After `world:update` returns, `Application` acquires a frame, calls `Renderer:render`, and submits it. `Render`, `PostRender`, and `RenderLast` remain available to game systems even though the renderer itself does not submit there. ## Fixed and presentation clocks Fixed phases receive the configured timestep as `dt`. `world:update` consumes accumulated time in whole steps and caps one frame at ten steps, so a long stall cannot create an unbounded catch-up loop. Variable phases receive the frame `dt`. Use them for presentation work that should follow display rate rather than simulation rate. `world:getFixedTiming()` returns the timestep, the unconsumed accumulator, and an interpolation alpha clamped to `[0, 1]`: ```teal local timestep, accumulator, alpha = world:getFixedTiming() ``` `PreviousTransform2D` lets the renderer interpolate an entity between its last two fixed poses. `tecs.SnapshotTransforms` copies the current pose in `FixedFirst` before simulation changes it. `world:fixedStepCount()` counts completed fixed steps. The fixed clock and its count advance even when no fixed system exists or callers disable the fixed group. ## System placement Systems within one phase follow insertion order unless `before` or `after` names another system. The engine table above supplies the names and boundaries that game plugins commonly order around; [Systems](/modules/ecs/systems) covers those constraints. Concrete phases expose `position` for inspection. The world assigns it, and callers must treat it as read-only. Groups expose a read-only `children` tree and have no position. Select phases by object instead of storing numeric positions. A custom phase must enter the world's pipeline before a system can use it: ```teal world:registerPhase(MyPhase) world:addSystem({ name = "game.CustomStep", phase = MyPhase, run = customStep, }) ``` `registerPhase` assigns a missing position and enables the phase. Registration rejects an invalid phase object, and system registration rejects a phase that the pipeline does not know. ## Disabling phases Disabling a group also disables its descendants: ```teal world:disablePhase(tecs.ecs.phases.FixedUpdateGroup) world:enablePhase(tecs.ecs.phases.FixedUpdateGroup) ``` Disabling `FixedUpdateGroup` stops its systems but not the fixed clock. Disabling `RenderGroup` also stops `RenderFirst`, so extraction stops updating the frame packet. GPU submission still draws the last packet because it runs outside the phase tree. To pause gameplay while presentation continues, use the [state stack](/modules/ecs/states) and logic [queries](/modules/ecs/queries/#paused-entities). ## Direct phase execution `world:runPhase(phase, dt)` dispatches one phase or group immediately: ```teal world:runPhase(tecs.ecs.phases.RenderGroup, dt) ``` It honors disabled state, including disabled ancestors. Re-enable a phase before calling it directly. Like `world:update`, `runPhase` publishes pending structural work before dispatch and after each phase it runs. It does not clear dirty bits afterwards; that contract supports custom loops that run parts of the phase tree on separate ticks. Calling it while `world:update` is suspended raises instead of publishing the suspended system's half-staged transaction. --- ## Plugins # Plugins A plugin configures one world. It registers components, queries, systems, resources, states, observers, and initial entities. The application calls its entry plugin after engine subsystem installation and before startup phases: ```teal return tecs.newApplication({ window = { title = "My game", width = 1280, height = 720, }, plugin = function(world: tecs.World, app: tecs.Application) world:addPlugin(spinPlugin(1.5)) world:addPlugin(tecs.gfx.textPlugin({ renderer = app.renderer, })) end, }) ``` Game code captures `app` here when a system needs the renderer, window, input, or audio. Systems and observers provide the per-frame and per-event lifecycle. ## One-time setup Declare component types at module scope. Build queries once in the plugin, then close over them from systems: ```teal local function spinPlugin(speed: number): tecs.Plugin return function(world: tecs.World) local spinning = world:newQuery({ include = {tecs.Transform2D, tecs.gfx.Renderable2D}, type = "logic", }) world:addSystem({ name = "game.Spin", phase = tecs.ecs.phases.Update, run = function(dt: number) for archetype, length in spinning:iter() do local transforms = archetype:getMut( tecs.Transform2D ) for row = 1, length do transforms[row].rotation = transforms[row].rotation + speed * dt end end end, }) end end ``` Never construct a persistent query inside `run`; that rebuilds its match set every frame. Name every system that needs ordering, removal, or useful debug output. ## Composition and dependencies `world:addPlugin` supplies the only composition mechanism. A plugin can install other plugins: ```teal local function gameplay(world: tecs.World) world:addPlugin(healthPlugin) world:addPlugin(inventory.plugin) world:addPlugin(spinPlugin(1.5)) end ``` Pass configuration through a closure, as `spinPlugin` does. This keeps configuration typed and immutable inside the installed systems. A plugin that depends on another should read the required resource during setup and fail immediately: ```teal local SPAWNER : tecs.data.Key = tecs.data.Store.newKey( "game.spawner" ) local function wavePlugin(world: tecs.World) local spawner = world.resources[SPAWNER] if not spawner then error("wavePlugin requires spawnerPlugin") end world:addSystem({ name = "game.Waves", phase = tecs.ecs.phases.Update, runIf = tecs.ecs.runif.every(5, 0.5), run = function() spawner:release() end, }) end ``` Always name resource keys. Re-registering one name returns the same key, which supports hot reload and lets tooling discover the dependency through `tecs.data.Store.listKeys`. Export component and event types beside the plugin function when other modules need them. Keep one purpose per plugin, then group related plugins with another plugin. World construction installs the [builtin plugin](/modules/ecs/builtins#builtin-plugin) automatically. --- ## Profiling # Profiling `tecs.utils.profile` exposes two independent sessions: - Sampling answers where CPU time goes. - Trace tracking reports where LuaJIT abandons compilation. Require this supported utility directly: ```teal local profile = require("tecs.utils.profile") ``` Only one sampling session and one trace session may run at a time. Stopping a session twice raises. ## Sampling a run The sampler writes [collapsed stacks][collapsed] for Speedscope, FlameGraph, Inferno, Pyroscope, and similar tools. Pipeline zones attribute samples to fixed-loop regions, phases, and systems. This system records five seconds and writes the result under the writable root: ```teal local session = profile.sample({ intervalMs = 10, stackDepth = 16, }) world:addSystem({ name = "profile.StopSample", phase = tecs.ecs.phases.First, runIf = tecs.ecs.runif.after(5), run = function() session:stop(tecs.io.files.writablePath("tecs.collapsed")) end, }) ``` Longer intervals reduce overhead. Deeper stacks cost more per sample. A zone prefix can restrict output to one pipeline subtree. Add [`jit.zone`][zones] around expensive regions inside a system: ```teal local zone = require("jit.zone") zone("uploadBuffers") uploadBuffers() zone() ``` Leave useful zone calls in production code. They do almost nothing when no profiling session needs them. Do not push and pop zones inside hot row loops. ## Trace failures Sampling shows slow code but cannot say whether LuaJIT compiled it. `profile.trace` aggregates trace aborts by reason, source location, and active zone: ```teal local session = profile.trace() -- Run the workload. local report = session:stop( tecs.io.files.writablePath("aborts.csv") ) print(report.totalAborts, report.blacklisted) ``` The report and its file use RFC 4180 CSV. Rows sort actionable failures first. | Severity | Meaning | Response | | ----------- | ---------------------------------------------------- | -------------------------------------------- | | `blacklist` | LuaJIT stopped attempting the trace for this run. | Investigate hot sites. | | `warn` | Recording hit unsupported bytecode or a trace limit. | Rewrite only when the site matters. | | `info` | Normal trace-formation events. | Include only during a focused investigation. | Many aborts occur in cold code and cost nothing important. Use the zone and location to decide whether a row belongs to a hot workload. Pause and resume either session to exclude setup or teardown while preserving data from both sides of the pause. ## Measurements outside the profiler Sampling and trace reports cannot answer every performance question. Frame timing reports `extract` once per world update as the sum of every enabled rendering domain. `extractSprites` and `extractMeshes` attribute that same work to the 2D and 3D domains. The aggregate is a frame-level sample, not an interleaving of unrelated domain samples under one name, so its percentiles can be compared directly with `simulate`, `record`, and `submit`. Input latency measures the time from the oldest consumed input event to the submission of the frame that reacted to it. Frame averages cannot substitute for that measurement. A pipelined frame may improve throughput while increasing latency. Allocation measurements need a separate run. `collectgarbage("count")` reports heap size, not allocated bytes, and a collection can erase the delta. Stop the collector during the measurement window. Do not put `collectgarbage` probes inside the frame you want to characterize because the probe itself aborts traces. Run once without frame probes for the total, then run again with coarse phase probes for attribution. Use trace tracking to reject runs in which the probes changed compilation. [zones]: https://luajit.org/ext_profiler.html#jit_zone [collapsed]: https://www.brendangregg.com/flamegraphs.html --- ## Query callbacks # Query callbacks `onEntitiesAdded` and `onEntitiesRemoved` react to changes in a query's match set. Each callback receives one contiguous row range: ```teal local physicsBodies = world:newQuery({ include = {tecs.Transform2D, tecs.physics.RigidBody}, onEntitiesAdded = function( archetype: tecs.ecs.Archetype, firstRow: integer, lastRow: integer, count: integer ) local transforms = archetype:get(tecs.Transform2D) local bodies = archetype:get(tecs.physics.RigidBody) reservePhysicsBodies(count) for row = firstRow, lastRow do attachBody(bodies[row], transforms[row]) end end, onEntitiesRemoved = function( archetype: tecs.ecs.Archetype, firstRow: integer, lastRow: integer, _count: integer ) local bodies = archetype:get(tecs.physics.RigidBody) for row = firstRow, lastRow do detachBody(bodies[row]) end end, }) ``` The inclusive bounds use one-based rows. `count` equals the range length, so a batch path can reserve external storage once. A temporary query cannot define either callback. ## Entities entering the query {#onentitiesadded-callback} `onEntitiesAdded` runs when entities first match the descriptor: - A spawn places matching components. - A component addition satisfies `include`. - A component removal satisfies `exclude`. Moving between two archetypes that both match does not run the callback. The entity never left the query. Use [`requires`](/modules/ecs/components/#auto-dependencies-with-requires) when one component always implies another. Use a callback when matching should trigger work in an external system or allocate a resource. ## Entities leaving the query {#onentitiesremoved-callback} `onEntitiesRemoved` runs before rows leave their archetype, so the callback can still read every included column. These changes remove a match: - Despawn. - Removal of an included component. - Addition of an excluded component. A one-component query reacts to that component alone. A wider descriptor can express a lifecycle boundary such as `{Enemy, Stunned}` or `{Transform2D, RigidBody}` without coordinating separate hooks. ## Transaction settle {#transaction-settle} Query callbacks run while the pipeline settles a transaction. Settle applies changes in waves: despawns first, then spawns, then archetype moves. It applies batch mutations and sparse relationship writes between waves. A callback may stage more work. The next wave applies that work: - Structural calls through `set`, `remove`, `spawn`, `despawn`, and the `batch*` APIs do not change committed structure immediately. - `get`, `has`, and queries see committed structure. They do not see staged additions, removals, spawns, or despawns. - A value-only `set` on a committed entity writes immediately when no staged structural change blocks it, and it marks the component dirty. - The callback may read the supplied archetype and row range until it returns. Read needed values before staging changes to those entities. A finite callback cascade settles through later waves. Tecs stops an unbounded cascade with an error after 64 waves. The [mutation model](/modules/ecs/mutation-model#settle-order) defines the complete ordering and visibility contract. ## Archetype-local observers `archetype:addEntityObserver` attaches lower-level hooks to one archetype, including activation, deactivation, moves, and destruction. It does not find other archetypes with the same signature. Query callbacks follow the query as new archetypes begin to match, so game and subsystem code should normally use them. --- ## Query grouping # Query grouping Grouping keeps matching archetypes with the same integer key together. Use it when each group needs expensive setup that should not run for every entity. ```teal local Kind = { Textured = 1, Shaded = 2, Flat = 3, } local renderables = world:newQuery({ include = {tecs.Transform2D, tecs.gfx.Renderable2D}, groupBy = function(archetype: tecs.ecs.Archetype): integer if archetype:get(tecs.gfx.Sprite) then return Kind.Textured end if archetype:get(tecs.gfx.Material) then return Kind.Shaded end return Kind.Flat end, }) ``` Tecs computes the key when an archetype begins matching and caches it. `groupBy` must therefore depend only on the component signature, never on row values or mutable external state. ## Group traversal `groups()` yields active keys in sorted order. `group(key)` yields the nonempty archetypes for one key: ```teal for kind in renderables:groups() do beginBatch(kind) for archetype, length in renderables:group(kind) do local transforms = archetype:get(tecs.Transform2D) for row = 1, length do drawRow(transforms[row]) end end endBatch() end ``` An empty group disappears from `groups()` until one of its archetypes fills again. `getGroup(archetype)` returns the cached key. `getGroupCount(key)` sums the entities in that group without visiting rows, which supports two-pass buffer layout: ```teal local offset = 0 local offsets: {integer: integer} = {} for key in renderables:groups() do offsets[key] = offset offset = offset + renderables:getGroupCount(key) end ``` Grouped iteration follows the same transaction-independent rules as `query:iter()`. Breaking or returning early needs no cleanup, and nested grouped loops keep independent traversal state. --- ## Queries # Queries A query tracks archetypes whose component signatures match one descriptor: ```teal local Transform2D = tecs.Transform2D local movers = world:newQuery({ name = "game.Movers", include = {Transform2D, Velocity}, exclude = {Frozen}, type = "logic", }) for archetype, length, entities in movers:iter() do local transforms = archetype:getMut(Transform2D) local velocities = archetype:get(Velocity) for row = 1, length do transforms[row].x = transforms[row].x + velocities[row].x * dt print(entities[row]) end end ``` `include` requires every listed component. `exclude` rejects every archetype with a listed component. `includeAny` adds an OR group: ```teal local drawn = world:newQuery({ include = {tecs.Transform2D, tecs.gfx.Renderable2D}, includeAny = {tecs.gfx.Sprite, tecs.gfx.Material}, type = "render", }) ``` A query exposes its descriptor for inspection. Tecs owns the compiled masks, subscriptions, and grouping state; callers must treat the descriptor as read-only after construction. Changing it does not rebuild the query. ## Archetype iteration `query:iter()` yields each non-empty matching archetype, its row count, and its entity-ID column. Tecs owns the entity-ID column; callers treat it as read-only. Bind each component column once per archetype. `archetype:get` gives a read-only access path. `archetype:getMut` gives caller-writable values and marks that component dirty: ```teal for archetype, length in movers:iter() do local transforms = archetype:getMut(Transform2D) local velocities = archetype:get(Velocity) for row = 1, length do local transform = transforms[row] local velocity = velocities[row] transform.x = transform.x + velocity.x * dt transform.y = transform.y + velocity.y * dt end end ``` LuaJIT cannot enforce const cdata, so writing through `get` may change memory without dirtying it. Use `getMut` for unconditional writes. For a conditional write, read through `get` and call `archetype:markComponentDirty(Component)` only when the write occurs. `query:count()` sums archetype lengths without visiting entity rows. Iteration supports nesting, including two loops over the same query. Iterators own traversal state only; they do not control structural transaction lifetime. ## Structural changes Structural calls such as `spawn`, `despawn`, component-adding `set`, `remove`, and batch operations always stage. Iteration continues over the committed rows and the pipeline publishes at its next declared barrier: ```teal local expiring = world:newQuery({ include = {tecs.ecs.TTL}, type = "logic", }) for archetype, length, entities in expiring:iter() do local ttls = archetype:getMut(tecs.ecs.TTL) for row = 1, length do ttls[row].remaining = ttls[row].remaining - dt if ttls[row].remaining <= 0 then world:despawn(entities[row]) end end end ``` Iterator exhaustion does not publish those changes. The [mutation model](/modules/ecs/mutation-model) defines their visibility and ordering. ### Early exit {#breaking-out-early} An early `break` or `return` is safe because iteration owns no transaction scope or resource that needs cleanup: ```teal for archetype, _length, entities in query:iter() do if matchesSelection(archetype) then selected = entities[1] break end end ``` The same rule applies to `groups()` and `group(id)`. Nested and interleaved loops keep independent traversal state, including multiple loops over the same query. ## Persistent and temporary queries Persistent queries subscribe to new archetypes and remain suitable for systems that run every frame. Build them once during plugin setup. `temp = true` takes a one-shot view of the current archetype set without registering observers: ```teal for archetype, length in world:newQuery({ include = {tecs.gfx.PointLight2D}, temp = true, }):iter() do inspectLights(archetype, length) end ``` A temporary query cannot define `onEntitiesAdded` or `onEntitiesRemoved`. [Query callbacks](/modules/ecs/queries/callbacks) cover persistent match-set reactions. [Grouping](/modules/ecs/queries/grouping) sorts matching archetypes under integer keys. ## Disabled entities {#disabled-entities} Every query excludes `tecs.ecs.Disabled` unless `include` explicitly names the tag. Renderer queries follow the same rule. ```teal local disabledRenderables = world:newQuery({ include = { tecs.Transform2D, tecs.gfx.Renderable2D, tecs.ecs.Disabled, }, }) ``` ## Paused entities {#paused-entities} `type = "logic"` excludes `tecs.ecs.Paused`. `type = "render"` records that paused entities should continue to match. An omitted type applies no pause filter. ```teal local movement = world:newQuery({ include = {tecs.Transform2D, Velocity}, type = "logic", }) local sprites = world:newQuery({ include = {tecs.Transform2D, tecs.gfx.Sprite}, type = "render", }) ``` Explicitly including `Paused` overrides the filter. Listing it under `exclude` matches the logic behavior. ## One-component archetype scans `world:findArchetypes(Component)` walks the component-to-archetype index without constructing a query: ```teal for archetype, length, entities in world:findArchetypes( tecs.gfx.PointLight2D ) do local lights = archetype:get(tecs.gfx.PointLight2D) for row = 1, length do print(entities[row], lights[row].radius) end end ``` This iterator reads the live archetype index directly. Do not make structural changes while it runs; call it only where the surrounding scheduler contract keeps publication out of the traversal. ## Module contents ### Submodules | Submodule | Description | | --- | --- | | [`Query callbacks`](/modules/ecs/queries/callbacks/) | Batch onEntitiesAdded and onEntitiesRemoved query hooks with row ranges and deferred-drain semantics | | [`Query grouping`](/modules/ecs/queries/grouping/) | Grouping matching archetypes by integer key with groupBy, groups, group, getGroup, and getGroupCount | --- ## tecs.ecs.random # tecs.ecs.random Seeded generation, in named streams a snapshot carries. ```teal tecs.ecs.random.seed(world, 20260726) local loot = tecs.ecs.random.stream(world, "game.loot") local spawns = tecs.ecs.random.stream(world, "game.spawns") local roll = loot:integer(1, 20) ``` ## Streams The world seed and stream name determine each [`Random`](/modules/ecs/random/#tecs.ecs.random.Random) sequence. Drawing from `loot` never advances `spawns`, so adding a stream leaves existing consumers unchanged. Use namespaced, nonempty names and call `seed` during world setup. Calling it later restarts every stream. Use `newRandom` for tools, tests, and benchmarks that need a generator outside a world. Do not share a generator between threads. ## Snapshots The first `seed` or `stream` call installs the snapshot handler. Make that call before loading a snapshot. A load updates existing generators in place, so systems that captured a stream during setup retain the same object. ## Ranges ```teal local unit = loot:next() -- [0, 1) local d20 = loot:integer(20) -- [1, 20] local offset = loot:integer(-3, 3) -- [-3, 3] local speed = loot:range(2.0, 5.0) -- [2, 5) ``` `integer` includes both ends and rejects an empty range. `range` excludes its upper end. ## Module contents ### Constructors | Constructor | Description | | --- | --- | | [`newRandom`](/modules/ecs/random/#tecs.ecs.random.newRandom) | Creates a generator outside every world and snapshot. | ### Types | Type | Kind | Description | | --- | --- | --- | | [`Random`](/modules/ecs/random/#tecs.ecs.random.Random) | record | Represents a generator through four 32-bit words of xoshiro128 state and the number of draws taken from it. | ### Functions | Function | Kind | Description | | --- | --- | --- | | [`seed`](/modules/ecs/random/#tecs.ecs.random.seed) | Static | Sets the world's seed and restarts every stream in it from that seed. | | [`stream`](/modules/ecs/random/#tecs.ecs.random.stream) | Static | Returns the generator named name in world, creating it on the first call and returning the same object afterwards. | ## Constructors ### tecs.ecs.random.newRandom Static Creates a generator outside every world and snapshot. Tools, benchmarks, and tests use this; games usually use `stream`. A seed is read as a 32-bit word, so 2^31 and -2^31 name the same one. ```teal function tecs.ecs.random.newRandom(seed: integer): Random ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `seed` | `integer` | Omitted takes a fixed constant, not a clock, so an unseeded generator still repeats run to run. | #### Returns | Type | Description | | --- | --- | | [`Random`](/modules/ecs/random/#tecs.ecs.random.Random) | A generator nothing else holds and nothing else knows about: two calls on one seed give two objects that draw the same sequence, each at its own pace. | ## Types ### tecs.ecs.random.Random record Represents a generator through four 32-bit words of xoshiro128** state and the number of draws taken from it. Not thread-safe, and not meant to be: a generator shared across threads would produce an order that depends on which one got there first, which is the whole thing this module exists to avoid. Give each thread a stream of its own. ```teal record tecs.ecs.random.Random integer: function(self, m: integer, n: integer): integer next: function(self): number range: function(self, lo: number, hi: number): number reseed: function(self, seed: integer) setState: function(self, words: {integer}) shuffle: function(self, list: {T}): {T} state: function(self): {integer} end ``` #### tecs.ecs.random.Random:integer Instance Returns the next integer in [1, m], or in [m, n] when both are given. It includes both ends and advances the generator. Errors on an empty range, because a caller asking for a number between 5 and 3 has a bug and a silent 5 hides it. ```teal function tecs.ecs.random.Random.integer( self, m: integer, n: integer ): integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Random` | | | `m` | `integer` | The upper end when it is the only argument, and the lower end when `n` follows it. The one-argument form counts from 1, so `integer(0)` is the empty range [1, 0] and raises. | | `n` | `integer` | The upper end, included. Nil takes the one-argument form. | ##### Returns | Type | Description | | --- | --- | | `integer` | An integer in the range from one draw whatever the range is. The method scales and floors instead of resampling, so a width that does not divide 2^32 has a bias of about one part in 2^32. | #### tecs.ecs.random.Random:next Instance Returns the next value and advances the generator by one round. ```teal function tecs.ecs.random.Random.next(self): number ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Random` | | ##### Returns | Type | Description | | --- | --- | | `number` | A multiple of 2^-32 in [0, 1). Zero is a value it takes; one is not. | #### tecs.ecs.random.Random:range Instance Returns the next value in [lo, hi) and advances the generator. ```teal function tecs.ecs.random.Random.range( self, lo: number, hi: number ): number ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Random` | | | `lo` | `number` | The included end, whether or not it is the smaller of the two. | | `hi` | `number` | The excluded end. A reversed pair is not an error and is not swapped either: the draw is `lo + next() * (hi - lo)` and `next` never reaches one, so `range(7, 3)` draws from (3, 7] rather than [3, 7). | ##### Returns | Type | Description | | --- | --- | | `number` | A value between the two ends, and `lo` exactly when the two are equal. | #### tecs.ecs.random.Random:reseed Instance Restarts the sequence from `seed` in place. The generator retains its identity, so existing holders draw from the restart. ```teal function tecs.ecs.random.Random.reseed(self, seed: integer) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Random` | | | `seed` | `integer` | Read as a 32-bit word, as `newRandom` reads one, so 2^31 and -2^31 restart the same sequence. Zero is a seed like any other; the generator expands it instead of rejecting it. | ##### Returns None. #### tecs.ecs.random.Random:setState Instance Puts four state words back, in place, so anything already holding this generator draws from what was put back. ```teal function tecs.ecs.random.Random.setState(self, words: {integer}) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Random` | | | `words` | `{integer}` | Exactly four, in the order `state` gives them, and each read as a 32-bit word, so a `state` result round-trips unchanged. Any other length raises, as does nil, as does all four being zero. | ##### Returns None. #### tecs.ecs.random.Random:shuffle Instance Shuffles a table in place with Fisher-Yates and returns the same table. It advances the generator once per element past the first. ```teal function tecs.ecs.random.Random.shuffle(self, list: {T}): {T} ``` ##### Type Parameters | Name | Constraint | Description | | --- | --- | --- | | `T` | | | ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | [`Random`](/modules/ecs/random/#tecs.ecs.random.Random) | The generator advances once per element after the first. | | `list` | `{T}` | The method reorders this list over `1..#list`. A list of one or none stays unchanged and draws nothing, so its length determines how far a shared generator advances. | ##### Returns | Type | Description | | --- | --- | | `{T}` | `list` itself rather than a copy, so the caller's own reference is already shuffled and the return is a convenience. | #### tecs.ecs.random.Random:state Instance Returns the four state words as plain integers that a snapshot, log line, or bug report can carry. ```teal function tecs.ecs.random.Random.state(self): {integer} ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Random` | | ##### Returns | Type | Description | | --- | --- | | `{integer}` | A fresh four-element array each call, and a copy: writing to it does not move the generator, and the generator moving does not change it. | ## Functions ### tecs.ecs.random.seed Static Sets the world's seed and restarts every stream in it from that seed. Call it during setup. Calling it mid-run is well defined and rewinds everything, which is rarely what a caller means. ```teal function tecs.ecs.random.seed(world: types.World, seed: integer) ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`types.World`](/modules/ecs/#tecs.World) | The call reseeds each existing stream in place, so a holder keeps the object. It also installs the snapshot handler, which is why games that load snapshots call it during setup. | | `seed` | `integer` | Read as a 32-bit word, as `newRandom` reads one. There is no way to ask a world for its seed back; take it from a snapshot instead. | #### Returns None. ### tecs.ecs.random.stream Static Returns the generator named `name` in `world`, creating it on the first call and returning the same object afterwards. Names use dot namespaces like snapshot data keys. The engine uses `tecs.audio` and `tecs.runif`, so game names should carry their own prefix. ```teal function tecs.ecs.random.stream( world: types.World, name: string ): Random ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`types.World`](/modules/ecs/#tecs.World) | The first ask installs a snapshot handler named `tecs.random` on it. A world that loads a snapshot before anything has asked for a stream has no handler yet and drops the saved seed, which `random.seed` during setup avoids. | | `name` | `string` | Nil or empty raises. The module compares names byte for byte, so two spellings of the same idea create independent streams. | #### Returns | Type | Description | | --- | --- | | [`Random`](/modules/ecs/random/#tecs.ecs.random.Random) | The world's generator for this name, not a copy: two callers asking for one name draw from the same sequence, in the order they happen to ask. | --- ## FFI relationships # FFI relationships `newFFIRelationship` stores an edge and its payload in a LuaJIT FFI struct. Use it for fixed-size numeric data that a hot loop or native API reads: ```teal local record Follows is tecs.ecs.Relationship delay: number maxDistance: number metamethod __call: function( self, target: integer, delay?: number, maxDistance?: number ): Follows end tecs.ecs.newFFIRelationship({ name = "Follows", container = Follows, fields = { {"delay", "float"}, {"maxDistance", "float"}, }, defaults = {0.5, 100}, }) world:set(follower, Follows(leader, 0.25, 50)) world:set( follower, Follows.new({ target = leader, delay = 0.25, maxDistance = 50, }) ) ``` Use [`newRelationship`](/modules/ecs/relationships/) for a target-only edge or a payload that needs strings, tables, functions, or other Lua values. ## Struct layout Tecs prepends this field to the generated struct: ```c double target; ``` Do not declare `target` in `fields`. Tecs owns the field; callers must treat it as read-only and replace the edge through `world:set`. An entity ID packs a 22-bit slot with a generation, so a 32-bit integer would truncate a valid ID. The positional constructor takes the target first; `.new` reads it from the `target` key. Callers may mutate dense payload fields through `getMut`. For sparse storage, replace the edge through `world:set` instead. Each entry in `fields` contains a C identifier and a type string. Tecs passes the type string to LuaJIT's `ffi.cdef`. Common choices include `float`, `double`, the fixed-width integer types, `bool`, and fixed arrays such as `float[4]`. Registration rejects an empty or duplicate field name, an invalid C identifier, and an empty type. Registration requires `name`, `container`, and `fields`. ## Initialization `fields` defines the positional order and lets Tecs generate `.new`. `defaults` fills omitted payload fields in that same order. `init` runs after Tecs writes the target, generated fields, and defaults: ```teal tecs.ecs.newFFIRelationship({ name = "SafeFollows", container = SafeFollows, fields = { {"delay", "float"}, {"maxDistance", "float"}, }, init = function( edge: SafeFollows, _target: integer, delay: number, maxDistance: number ) edge.delay = math.max(0.1, delay) edge.maxDistance = math.max(1, maxDistance) end, }) ``` An `init` hook needs `fields` or an explicit `new`; otherwise Tecs cannot map a table to the hook's positional arguments. A custom `__call` replaces the generated constructor and does not run `init`. Call shared initialization explicitly from that hook. [Component construction](/modules/ecs/components/construction) covers the shared constructor rules. ## Relationship behavior FFI relationships support `exclusive`, `sparse`, `reverseIndex`, and `cascadeDelete` with the same rules as [other relationships](/modules/ecs/relationships/). Storage changes the payload layout, not query or lifecycle behavior. Snapshots write `target` and every declared field, then restore the edge through `.new(data)`. A custom serializer cannot accompany `transient = true`. --- ## Relationships # Relationships A relationship connects one entity to another. Tecs ships [`ChildOf`](/modules/ecs/builtins#childof), an exclusive sparse relationship with a reverse index and cascade delete: ```teal local ChildOf = tecs.ecs.ChildOf local parent = world:spawn(tecs.Transform2D(100, 100)) local child = world:spawn( ChildOf(parent), tecs.ecs.RelativeTransform2D(16, 0) ) local link = world:getFirstRelationship(child, ChildOf) print(link.target) -- parent world:despawn(parent) -- also despawns child ``` Relationships use the component API. Pass an instance to `world:set` or `world:remove`, include the relationship in a query, and read it from an entity or archetype. ## Defining a relationship `newRelationship` creates either a target-only relationship or a relationship with a Lua payload. Use [`newFFIRelationship`](/modules/ecs/relationships/ffi) when the payload belongs in a packed C struct. This target-only relationship lets an entity like several targets: ```teal local Likes: tecs.ecs.Relationship = tecs.ecs.newRelationship({ name = "Likes", }) world:set(alice, Likes(bob)) world:set(alice, Likes(carol)) ``` A relationship with data declares the payload fields after the target: ```teal local record Follows is tecs.ecs.Relationship delay: number maxDistance: number metamethod __call: function( self, target: integer, delay?: number, maxDistance?: number ): Follows end tecs.ecs.newRelationship({ name = "Follows", container = Follows, fields = {"delay", "maxDistance"}, defaults = {0.5, 100}, }) world:set(follower, Follows(leader, 0.25, 50)) world:set( follower, Follows.new({ target = leader, delay = 0.25, maxDistance = 50, }) ) ``` The target always comes first in the positional form and lives under `target` in the table form. Tecs owns `target`; treat it as read-only and replace an edge through `world:set` instead of changing the field. Do not include `"target"` in `fields`. [Component construction](/modules/ecs/components/construction) covers `fields`, `defaults`, `init`, custom `__call`, and `.new`. ## Exclusive relationships A relationship normally allows several targets on one source. Setting the same target again replaces that edge's value. Set `exclusive = true` when each source may name only one target: ```teal local Targets: tecs.ecs.Relationship = tecs.ecs.newRelationship({ name = "Targets", exclusive = true, }) world:set(enemy, Targets(player)) world:set(enemy, Targets(decoy)) -- replaces Targets(player) ``` `world:getFirstRelationship(entity, Targets)` returns the edge. For an exclusive relationship, it returns the only edge. ## Dense and sparse storage A dense relationship creates a component type for each target. Entities that point at different targets occupy different archetypes. That layout supports a target-specific query: ```teal local followersOfLeader = world:newQuery({ include = {Follows:targeting(leader)}, }) ``` Use dense storage when systems often query one target and the target set stays small. `sparse = true` keeps targets in entity-indexed side storage. Every source shares the relationship's wildcard component in its archetype, so a large target set does not fragment the world. `ChildOf` uses this layout. ```teal local children = world:newQuery({ include = {ChildOf, tecs.Transform2D}, }) for archetype, length in children:iter() do local parents = archetype:get(ChildOf) for row = 1, length do print(parents[row].target) end end ``` The sparse column proxy supports row reads only. Tecs owns its target values; change an edge through `world:set`. Sparse relationships do not expose `targeting`; filter the proxy inside the loop or use a reverse index. For either layout: - `world:getFirstRelationship(entity, Relationship)` returns an arbitrary edge, and the only edge for an exclusive relationship. - `world:get(entity, Relationship(target))` selects one target. - `world:has(entity, Relationship)` checks for any target. - `world:has(entity, Relationship(target))` checks one target. - A query that includes the bare relationship matches any target. Callers may mutate a dense payload through `getMut`. Sparse payloads belong to the side store; replace those edges through `world:set`. ## Reverse lookup and traversal Set `reverseIndex = true` when code needs to find the sources that point at a target. Both dense and sparse relationships support the index. ```teal world:targets( parent, ChildOf, function(childId: integer) print("child", childId) end ) for depth, entityId in world:traverse(root, ChildOf) do print(depth, entityId) end ``` `world:targets` accepts a context value and passes it as the callback's second argument. A hoisted callback and reused context avoid a closure allocation: ```teal local countContext = {count = 0} local function countChild(_childId: integer, context: typeof(countContext)) context.count = context.count + 1 end countContext.count = 0 world:targets(parent, ChildOf, countChild, countContext) ``` `world:walkUp` follows forward edges, so it needs an exclusive relationship but not a reverse index: ```teal world:walkUp( entity, ChildOf, function(ancestorId: integer, depth: integer) print(depth, ancestorId) end ) ``` The callback may return `false` to stop. The optional `maxDepth` defaults to 100 and turns a cycle into an error instead of an infinite walk. ## Removal and target lifetime Pass an instance to remove one target: ```teal world:remove(alice, Likes(bob)) ``` Passing a sparse relationship container removes all its targets from the source. Removing an edge never triggers cascade delete, so reparenting can remove or replace `ChildOf` without despawning the child. `cascadeDelete = true` makes target despawn recursively despawn every source. It requires both `exclusive = true` and `reverseIndex = true`. A reverse index also lets target despawn unlink ordinary edges. Without one, Tecs has no inverse to consult; `world:compact()` later prunes unreachable archetypes whose targets have died. `Relationship(target)` interns one weakly held instance per target. Dense storage registers that instance as its target-specific component. Sparse storage uses it as an edge value and keeps the payload in the world's side store. Snapshots write `target` plus every declared payload field. Set `transient = true` for an edge that must not survive a snapshot. ## Module contents ### Submodules | Submodule | Description | | --- | --- | | [`FFI relationships`](/modules/ecs/relationships/ffi/) | FFI struct-backed relationships via newFFIRelationship with packed field types and target semantics | --- ## Save games # Save games Snapshots carry durable world state between processes. Use them for save games, checkpoints, replay buffers, and hot reload. ```teal local save = world:saveSnapshot().buffer world:loadSnapshot(save) ``` ## Binary and table formats `saveSnapshot` writes binary data by default. The binary format uses a LuaJIT [`string.buffer`](https://luajit.org/ext_buffer.html#serialize) and copies dense FFI columns in bulk. Use it for shipped saves. The table format returns plain Lua data. Use it for inspection, migrations, and tools: ```teal local snapshot = world:saveSnapshot({format = "table"}).snapshot snapshot.data[#snapshot.data + 1] = { key = "mygame.migrated", value = true, } world:loadSnapshot(snapshot) ``` Table saves accept the same selection options as binary saves. They reject `buffer` and `path`. Encoding a table as JSON through `tecs.data` costs more allocations and per-component work than the binary format. ## Snapshot contents ### Durable world state A snapshot records: - entity IDs, including their slot and generation - components and relationship targets - the complete [state stack](/modules/ecs/states) - the fixed-step accumulator and per-phase enable flags - custom data Load rebuilds the [`EntityKey`](/modules/ecs/builtins#entitykey) index after it restores the entities. A same-world load therefore preserves entity handles whose saved IDs still exist. Create every named state before loading a snapshot that uses it: ```teal world:createState("menu") world:createState("playing") world:loadSnapshot(save) ``` Load raises when the snapshot names an unregistered state. Save also raises when the world contains a non-exclusive sparse relationship unless its component declares `transient = true`. A non-exclusive sparse store can hold several targets for one source, while a snapshot component row holds one value. ### Runtime state Snapshots omit: - `world.resources` - GPU buffers and device handles - audio voices and playback positions - open files and worker threads - suspended operations and native work in flight - Lua locals, closures, and entity-address observers An active operation is never a component field. Its coroutine, native handle, and completion listener are runtime state rather than snapshot data. Give an entity a transient marker when an engine-owned resolver must remember work outside that entity, which is what [`tecs.io.http`](/modules/io/http) does with `Pending`. A sequence cursor waiting on external work saves the provider name, entity, and key. Load restores the cursor without restoring that operation. `isPending` then returns false, and the cursor resumes on the next fixed step. Reissue and retrack the work when the wait must continue. Load replaces the world in place and despawns nothing, so a subsystem holding work for an entity never hears that the entity is gone. Cancel or reissue from [`FinishSnapshotLoad`](/modules/ecs/builtins#snapshot-events) rather than from `OnDespawn`. Keep durable input in components. Recreate process-local objects from that input after load. ## Component durability Dense FFI components normally cross the binary format as raw columns. Table components use their default serializer. A component can instead provide `serialize` and `deserialize` callbacks. See [Component serialization](/modules/ecs/components/serialization) for the registration API. Custom codecs must turn process-local numbers into durable names: - `tecs.gfx.Sprite` saves the image name instead of its intern index. - `tecs.gfx.animation.Animation` saves sheet and tag names, and the phase in the cycle rather than the frame. Load clears the frame field, so the next update encodes the playback the sprite carries from that phase. - `tecs.audio.Sound` saves the clip path and group name. Load starts a new voice instead of restoring playback progress. - `tecs.gfx.Text` saves authored fields and the font name. Load resolves only fonts that `newTTF` already returned under that name; a missing font produces no layout. - `tecs.physics.RigidBody` saves no component value. The physics snapshot handler stores Rapier's complete versioned state under `"tecs.physics"` and reconnects body and collider handles by entity. An FFI component also stores a schema fingerprint. When the saved and current fingerprints match, load copies the column in bulk. Otherwise load maps fields by name into the current schema. New fields start at zero, removed fields disappear, and LuaJIT converts numeric types. Renaming a field discards the saved value. ### Transient components Declare process-local backing data as transient when the entity itself belongs in the save: ```teal local record PathCache is tecs.ecs.Component nodeCount: integer cursor: integer end local PathCacheComponent = tecs.ecs.newFFIComponent({ name = "PathCache", container = PathCache, fields = { {"nodeCount", "int32_t"}, {"cursor", "int32_t"}, }, transient = true, }) ``` Save omits the transient column. Load applies normal spawn behavior, including `requires` defaults. Component registration rejects the combination of `transient = true` and a custom serializer. ### Derived entities A subsystem can exclude fully derived entities during [`OnSnapshotSave`](/modules/ecs/builtins#snapshot-events): ```teal world:observe( 0, tecs.ecs.OnSnapshotSave, function(ev: tecs.ecs.OnSnapshotSave) ev:exclude(TileInstance) end ) ``` The snapshot omits every entity that carries `TileInstance`. The owning subsystem must recreate those entities from durable input after load. ## Save selection and custom data `saveSnapshot` accepts a reusable binary buffer, a destination path, a query, layer selection, and custom data: ```teal local buffer = require("string.buffer") local replayBuffer = buffer.new() local result = world:saveSnapshot({ buffer = replayBuffer, path = tecs.io.files.writablePath("checkpoint.bin"), filterQuery = {include = {Persist}}, layers = {2, 3}, customData = { build = "v12", checkpoint = {level = "intro", elapsed = 42.5}, }, }) local bytes = result.buffer ``` The world resets a supplied buffer before writing. A path writes the same binary bytes and still returns the tagged result. The world clones `filterQuery`, so repeated saves never mutate the caller's descriptor. `layers` accepts values from 0 through 31. It rejects an entity with `Transform2D` when the component's `layer` falls outside the allowlist. It keeps an entity without `Transform2D`. Custom-data values must support `string.buffer` encoding. Prefix your keys with the game or subsystem name. Tecs reserves keys that begin with `__tecs.` for the state stack and pipeline state. ## Load lifecycle `loadSnapshot` accepts a Lua string, `string.buffer`, snapshot table, or tagged save result. It clears and repopulates the world in place. It does not emit `OnDespawn` for the replaced entities. A format-version mismatch raises. Use `EntityKey` to rediscover important entities: ```teal local player = world:spawn(tecs.ecs.EntityKey("player"), Player()) local save = world:saveSnapshot().buffer world:loadSnapshot(save) player = world:requireKey("player") ``` Global observers registered at address `0` survive a same-world load because the world keeps its systems and event bus. A fresh process must register those observers during setup. Load clears entity-address observers with the discarded entity set. Store durable per-entity behavior as component data. Let a query or global observer interpret that data and install any runtime callbacks. ## Snapshot handlers Register a named handler for durable state that lives outside components: ```teal local player: tecs.ecs.Entity = 0 world:addSnapshotHandler({ name = "mygame.session", save = function(_world: tecs.World): any return { difficulty = session.difficulty, rng = rng:save(), } end, load = function(_world: tecs.World, value: any) session.difficulty = value.difficulty rng:load(value.rng) end, finish = function( loadedWorld: tecs.World, _prelude: tecs.ecs.SnapshotPrelude ) player = loadedWorld:requireKey("player") end, }) ``` The caller supplies any combination of `save`, `load`, and `finish`, plus a nonempty `name`. `save` returns one encodable value for that name. A nil result writes no value. Load restores the ECS first, then calls matching `load` callbacks, then calls every `finish` callback. `addSnapshotHandler` builds on three global events: - `OnSnapshotSave` fires before the archetype walk. Tecs owns `addData` and `exclude`; an observer may call these functions but must not replace them. - `StartSnapshotLoad` fires after ECS restoration and before data dispatch. Tecs owns `onData`; an observer may call it to register callbacks by key. - `FinishSnapshotLoad` fires after every data callback. Tecs owns its `prelude`; observers may read it but must not replace it. The prelude reports the format version and entity, archetype, and component table information. Use handlers unless a subsystem needs the lower-level event ordering. ## Engine subsystem state Engine plugins register their own snapshot behavior: - `tecs.ecs.random` stores the world seed and every named stream under `"tecs.random"`. The first `random.stream` or `random.seed` call installs the handler. Seed during world setup so a load cannot encounter the saved value before the handler exists. - Audio stores master and group gain, mute, and pause settings under `"tecs.audio"`. It does not store keyed limits or voice progress. Configure limits during setup. - The sequence plugin stores its runtime under `"tecs.sequence"`. - The text plugin discards cached glyph runs after load and derives them again. - The HTTP plugin saves the `Request` and not the transient `Pending` marker, so a request that was in flight is sent again after load. It stops the transfers the load replaced, because the entities waiting on them are gone. - Physics stores Rapier's complete state under `"tecs.physics"` and reconnects transient handles. `physics.hasBody` reports whether an entity has a live body after reconnection. ## Saving files `path` provides the shortest binary save: ```teal local path = tecs.io.files.writablePath("save.bin") world:saveSnapshot({path = path}) world:loadSnapshot(tecs.io.files.read(path)) ``` To transform the bytes first, write the returned buffer: ```teal local path = tecs.io.files.writablePath("save.bin") local bytes = tostring(world:saveSnapshot().buffer) tecs.io.files.write(path, bytes) ``` `tecs.io.files.writablePath` resolves a path inside the application's writable directory and works on targets where stdio cannot reach platform storage. ## Binary snapshot layout The binary format uses one component table and column-major storage: - Archetypes refer to component names and schemas by table index. - Dense FFI columns use one raw copy per column. - Entity IDs use raw doubles, which retain the packed 22-bit slot and 31-bit generation. - Custom codecs encode one value per row. - Archetypes with a serializable sparse relationship use row-major data and a presence mask. The wire format follows this sequence: ```text prelude: encode(version) encode(nextEntityId) encode(entityCount) encode(archetypeCount) encode(componentCount) per component: encode(name) encode(fingerprint) # empty for non-FFI components per archetype: encode(columnCount) encode(entityCount) per column: encode(componentTableIndex) encode(mode) # 0 column-major, 1 row-major mode 0: putcdata(entityIds, entityCount * 8) per column: putcdata(column, structSize * entityCount) OR per row: encode(serializedValue) mode 1: per row: encode(entityId) encode(presenceMask) per present column: encode(serializedValue) data: repeat: encode(true); encode(key); encode(value) encode(false) ``` Row-major presence masks use exact double arithmetic and support at most 52 columns. ## Table snapshot layout The table writer produces this general shape: ```teal { version = 1, nextEntityId = 42, componentTable = { {name = "Position"}, {name = "Health"}, }, archetypes = { { columnIndices = {1, 2}, entities = { {1, {x = 10, y = 20}, {hp = 100}}, {2, {x = 30, y = 40}, {hp = 50}}, }, }, }, data = { {key = "build", value = "v12"}, }, } ``` Each entity row aligns its component values with the archetype's `columnIndices`. Table load always calls each component's deserializer, so the table writer omits schema fingerprints. --- ## State stack # State stack Every world owns a stack of named states. Use it for play, pause, menus, cutscenes, and game-over overlays. Each state owns a tag component. A spawn automatically receives the tag of the state currently on top. ```teal local GameState = world:createState( "game", { onBlur = "pause", onFocus = "resume", } ) local PauseState = world:createState("pause") world:pushState("game") world:spawn(Player()) -- receives GameState world:pushState("pause") world:spawn(PauseMenu()) -- receives PauseState world:popState() ``` The default exit action despawns the entities tagged with the popped state. Spawn permanent cameras, services, and HUD entities before the first `pushState` so they receive no state tag. ## Transition order Pushing a state performs these steps: 1. Apply the outgoing state's blur policy. 2. Emit `StateBlur`. 3. Push the new state and select its auto-tag. 4. Apply its enter policy. 5. Emit `StateEnter`. Popping performs these steps: 1. Apply the top state's exit policy. 2. Emit `StateExit`. 3. Remove it from the stack. 4. Select the revealed state's auto-tag. 5. Apply its focus policy and emit `StateFocus`. Popping the last state clears the auto-tag. Popping an empty stack or pushing an unregistered name raises. Each transition stages its entity mutations as one transaction and publishes them together before returning. ## Reading the stack `world:peekState()` answers the top name, and `world:listStates()` answers the whole stack bottom-first: ```teal world:pushState("game") world:pushState("pause") world:listStates() -- {"game", "pause"} world:peekState() -- "pause" ``` Read the whole stack to tell a pause pushed over play from a pause that is all there is, which is what a back button, a save prompt and a debugger each need. The returned list is a fresh copy and holds only the states that were pushed, so a state `createState` registered and nothing pushed does not appear. The debug server reports the same stack as the `states` command. ## Lifecycle policies | Hook | Moment | | --------- | ------------------------------------------------------ | | `onEnter` | The state reaches the top through a push. | | `onBlur` | Another state covers it. | | `onFocus` | A pop reveals it. | | `onExit` | The state leaves through a pop. Defaults to `despawn`. | Blur, focus, and exit hooks accept these built-in actions: | Action | Effect on entities carrying the state tag | | --------- | ----------------------------------------- | | `pause` | Add `Paused`. | | `resume` | Remove `Paused`. | | `disable` | Add `Disabled`. | | `despawn` | Despawn the entities. | A hook may instead call a function. A table with `apply` and `call` runs the built-in action first, then the function. Enter accepts a function. `pause` only affects queries declared with `type = "logic"` or an explicit `Paused` exclusion. Render queries continue to match paused entities. Use `disable` when the entities should leave all ordinary queries and stop drawing. ## State-aware work The tag returned by `createState` works in any query: ```teal local enemies = world:newQuery({ include = {GameState, Enemy}, type = "logic", }) ``` Gate a whole system on the current top state: ```teal world:addSystem({ name = "game.Update", phase = tecs.ecs.phases.Update, runIf = tecs.ecs.runif.inState("game"), run = updateGame, }) ``` Observe transition events at address zero when runtime code needs notification: ```teal world:observe( 0, tecs.ecs.StateEnter, function(event: tecs.ecs.StateEnter) print("entered", event.state) end ) ``` ## Snapshot setup Snapshots carry the stack, state tags, `Paused`, and `Disabled`. Policies are functions and do not enter the save. Create every state with its policy during plugin setup before loading a snapshot. Load raises when the saved stack names a state this world has not registered. After load, use state-tag queries to rebuild group indexes and `EntityKey` for the few individual entities that code must rediscover. See [Save games](/modules/ecs/save-games). --- ## Systems # Systems A system runs one function in one [phase](/modules/ecs/phases). Build its query once inside a plugin, then close over that query: ```teal local Transform2D = tecs.Transform2D local function spinPlugin(world: tecs.World) local spinning = world:newQuery({ include = {Transform2D, Spin}, type = "logic", }) world:addSystem({ name = "game.Spin", phase = tecs.ecs.phases.Update, run = function(dt: number) for archetype, length in spinning:iter() do local transforms = archetype:getMut(Transform2D) local speeds = archetype:get(Spin) for row = 1, length do transforms[row].rotation = transforms[row].rotation + speeds[row] * dt end end end, }) end world:addPlugin(spinPlugin) ``` The pipeline calls `run(dt, world)`. Fixed phases supply the fixed timestep; variable phases supply the frame delta. ## Asynchronous work Every frame system dispatched by `world:update` is resumable. There is no second system kind, explicit hold boundary, callback, or completion handle. Call a cooperative engine function and use its returned value directly: ```teal local record AssetBytes is tecs.ecs.Component bytes: string end tecs.ecs.newComponent({ name = "game.AssetBytes", container = AssetBytes, fields = {"bytes"}, }) local function assetPlugin(): tecs.Plugin return function(world: tecs.World) local missing = world:newQuery({include = {AssetPath}}) world:addSystem({ name = "game.LoadAsset", phase = tecs.ecs.phases.PreUpdate, run = function(_dt: number, runWorld: tecs.World) for archetype, length, entities in missing:iter() do local paths = archetype:get(AssetPath) for row = 1, length do local bytes = tecs.assets.loadString( paths[row].path ) runWorld:set( entities[row], AssetBytes, AssetBytes(bytes) ) end end end, }) end end ``` When the value is already available, the call returns inline and performs no scheduler turn. When it must wait, Tecs parks the world update at that exact Lua stack frame. Events and process-wide I/O continue to pump, and the application may render the last completed frame. The next system and phase do not run early. The coroutine preserves query iterators and locals. Structural mutations stay staged while the system is suspended because the system has not returned, so a spawn after a wait remains ordered and commits at the next declared barrier. The same mechanism works in fixed phases: the fixed step resumes without replaying its earlier systems or advancing its clock twice. Calling the same cooperative API outside a world update, including from startup, shutdown, or `runPhase`, blocks while pumping its producer. A completion-backed operation stops at its documented finite wait budget and reports failure through that call; giving up also releases that caller's hold on the producer, so the work stops rather than running on with nothing left to deliver to. Readiness methods honor their explicit timeouts, while a socket read waits for input or closure. This is useful during initialization and in headless tools. Plugin authors do not choose between synchronous and asynchronous variants. A wait nothing ever completes suspends the update indefinitely, which looks from outside like a hung process. After five seconds of suspended updates the world says what it is parked on through the `tecs.world` logger, at error priority, and repeats about once a minute while the wait lasts. One persistent coroutine belongs to the logical world update, not to every entity. Iterating ten thousand entities does not create ten thousand tasks. Only operations that actually wait enter the scheduler. Cooperation does not make every byte operation asynchronous. The API follows the kind of work: | Work | Behavior inside a system | Native execution | | ---------------------------------------- | ------------------------ | --------------------------- | | Cached asset or ready socket | Returns inline | Immediate lookup or syscall | | DNS resolution or TCP connection | Suspends the update | Bounded Tokio service | | Socket blocked on readiness | Suspends the update | Process-wide `mio` reactor | | HTTP request | Suspends the update | Reqwest and Tokio service | | Asset decode | Suspends the update | Bounded CPU lane | | Regular file transfer | Suspends the update | SDL AsyncIO | | `Process:wait` or native dialog | Suspends the update | Native completion bridge | | Memory Reader, Writer, or transform | Returns inline | Calling Lua thread | | Socket or process-pipe Reader and Writer | Suspends when not ready | Native readiness reactor | CPU-heavy transforms and blocking libraries belong on workers. Users never receive a future, poll a second nonblocking API, or manually manage a coroutine. Socket I/O uses the same direct form. This system does not poll, retain a future, or declare itself asynchronous: ```teal world:addSystem({ name = "game.ReceivePacket", phase = tecs.ecs.phases.PreUpdate, run = function() tecs.scoped( "decode packet", function(scope) local packet = scope:own( assert(inbox:receive()) ) decodePacket(packet.bytes) end ) end, }) ``` The native call runs first. A ready socket returns inline; only `WouldBlock` reaches the scheduler: ```mermaid flowchart TD call["System calls a direct I/O API"] --> ready{"Operation ready?"} ready -->|Yes| value["Return the value inline"] ready -->|No| park["Park the logical world update"] park --> pump["Application pumps events and native readiness"] pump --> resume["Resume the same Lua call"] resume --> ordered["Finish later systems in schedule order"] ordered --> commit["Commit the completed phase once"] ``` ## Frame placement `Application` drives three groups: | Call | Work | | ------------------ | -------------------------------------------------- | | `world:startup()` | Runs startup phases after plugin registration. | | `world:update(dt)` | Runs fixed and variable frame phases. | | `world:shutdown()` | Runs teardown phases before subsystem destruction. | Engine systems share the same schedule. `tecs.SyncRenderState` extracts the world in `RenderFirst`, so a system that must affect the current frame runs no later than `PostUpdate`. `world:update` clears dirty bits after the pipeline. Dirty-gated consumers must run in the same update as the writes they consume. ## Structural barriers Systems in one phase share a structural transaction by default. The pipeline publishes it after the phase, so they normally see the same committed archetypes while the next phase sees their combined changes. Declare `commitBefore = true` when a system must consume structural output from an earlier system in the same phase. Declare `commitAfter = true` when a later system in that phase must consume this system's structural output. These declarations make unconditional dependencies visible in system configuration. Call `world:enqueueCommit()` inside a system for a conditional dependency. The pipeline coalesces repeated requests and publishes after the requesting system returns, before the next system runs. The requesting system keeps its current view. Outside system dispatch, the same call publishes synchronously, which is useful for tests and debug tooling. Prefer moving the consumer to a later phase when that is the natural frame dependency. Additional barriers reduce batching and make more archetype moves observable within one phase. ## System failures Under an application, the crash guard catches a system error, logs its traceback, returns frame resources, and discards structural work staged by the interrupted transaction. A crash never invents an undeclared publication barrier. Simulation stops while the host continues to drain events and serve the debug connection. The pipeline protects one whole non-empty phase at a time rather than wrapping each system separately. If a system raises, that phase guard clears its active commit request before the stack unwinds while retaining the original traceback. The next external `enqueueCommit()` is therefore synchronous as usual. The guard restores engine invariants, not game invariants. A system may have updated only part of a query before it threw. Development code may resume through `app:clearCrash()` after inspection. ## Names and ordering Give every system that participates in ordering or removal an explicit, stable name: ```teal world:addSystem({ name = "game.ResolveDamage", phase = tecs.ecs.phases.PostUpdate, after = {"game.ApplyDamage"}, before = {"tecs.PlaySounds"}, run = resolveDamage, }) ``` Within one phase, the pipeline preserves registration order and then applies `before` and `after` constraints. A missing target name contributes no edge, which lets optional plugins declare ordering without requiring one another. The pipeline rejects cycles and duplicate system names. The pipeline generates a private name for an unnamed system. Treat that name as engine-owned and unstable. `world:removeSystem(name)` requires an existing name, so callers should remove only explicitly named systems. ## Listing and stopping systems `world:listSystems()` reports every system the world runs, ordered by phase and by run order within each phase. Each row names the system and its phase, gives its position in that phase, and says whether the system is enabled and whether it declares a `runIf` of its own: ```teal for _, info in ipairs(world:listSystems()) do print(info.phase, info.position, info.name, info.enabled) end ``` The list is a fresh copy, so a later enable or disable does not reach a list already handed out. `world:setSystemEnabled(name, enabled)` stops one system or starts it again. A disabled system stays registered: it keeps its name, its position and its ordering constraints, contributes to `world:getStats().systems`, and simply does not run from the next `world:update`. Enabling restores the `runIf` it declared for itself, so a gated system comes back gated: ```teal local stopped, reason = world:setSystemEnabled("game.Spin", false) if not stopped then print(reason) end ``` A name no system carries returns `false` and a reason rather than raising, because that name usually comes from a person or a debugger rather than from code. Disabling a system that is already disabled reports success and changes nothing. Use `removeSystem` when the system is never to run again, and `setSystemEnabled` when it is a pause. The debug server exposes the same pair as the `systems` command. ## Conditional execution `runIf(dt, world, systemName)` gates `run`. Any function with that shape may serve as a predicate: ```teal world:addSystem({ name = "game.LowHealthWarning", phase = tecs.ecs.phases.Update, runIf = function(_dt: number, world: tecs.World): boolean return world.resources[PLAYER_HEALTH] < 25 end, run = showLowHealthWarning, }) ``` `tecs.ecs.runif` supplies stateful predicates for common schedules. ### Delayed one-shot {#after} `runif.after(delay)` waits for the named duration, allows one run, then removes the system. The predicate uses the system name passed by the pipeline, so even an unnamed one-shot can clean itself up. ### Repeating interval {#every} `runif.every(interval, jitter?)` repeats on an interval. Jitter chooses the next interval within the requested variance and draws from the world's `"tecs.runif"` random stream. The stream makes schedules deterministic under seeding and snapshots. Clamping keeps a large jitter from producing a zero-length interval. ```teal world:addSystem({ name = "game.SpawnWave", phase = tecs.ecs.phases.Update, runIf = tecs.ecs.runif.every(0.5, 0.1), run = spawnWave, }) ``` ### Immediate cooldown {#cooldown} `runif.cooldown(duration)` allows the first update immediately, then suppresses the system until the duration elapses. ### Active state {#instate} `runif.inState(name)` allows the system only while that state occupies the top of the [state stack](/modules/ecs/states): ```teal runIf = tecs.ecs.runif.inState("game") ``` ### Negation {#negate} `runif.negate(predicate)` inverts one predicate. ### Conjunction {#both} `runif.both(lhs, rhs)` short-circuits like logical AND. Operand order changes stateful timing: - `both(inState("game"), every(2))` pauses the interval outside the state. - `both(every(2), inState("game"))` keeps the interval advancing and spends ticks that land outside the state. Put a gate first when its false state should pause the timer. ### Disjunction {#either} `runif.either(lhs, rhs)` short-circuits like logical OR. The right predicate receives `dt` only when the left predicate returns false, so stateful operands make order part of the schedule. ```teal world:addSystem({ name = "game.AmbientAnimation", phase = tecs.ecs.phases.Update, runIf = tecs.ecs.runif.either( tecs.ecs.runif.inState("game"), tecs.ecs.runif.inState("editor") ), run = animateAmbientScene, }) ``` --- ## World # World A world owns the complete ECS runtime: entities, archetypes, queries, systems, resources, bundles, event observers, snapshot handlers, and state. ```teal local world = tecs.ecs.newWorld({ timestep = 1 / 60, }) ``` The default world supports about one million concurrent entity slots. A configuration may raise `maxEntities` to the packed-ID limit of `2^22 - 1`. Tests and specialized hosts may also supply a pipeline factory. ## Lifecycle `Application` creates and drives its world. After the entry plugin finishes, it calls `startup()` once, `update(dt)` every host iteration, and `shutdown()` at teardown. Tests, tools, and benchmarks may drive those calls directly: ```teal world:startup() world:update(1 / 60) world:shutdown() ``` Before phase dispatch, `update` publishes pending structural work. The pipeline then publishes after each non-empty phase, and `update` clears component dirty bits only after the pipeline returns. Render extraction runs inside that pipeline and consumes the bits first. `getFixedTiming()` returns the timestep, residual accumulator, and clamped interpolation alpha without allocating. `fixedStepCount()` returns the number of fixed steps since world construction. The scheduler advances those values even when callers disable fixed phases. ## Entity IDs Entity IDs pack a slot and generation into an opaque number: ```teal local old = world:spawn() -- `old` becomes live at the next pipeline barrier. ``` Slot reuse changes the generation, so stale handles fail lookups. Do not inspect IDs with LuaJIT bit operations; packed values may exceed 32 bits. Use `EntityKey` for the few authored entities that runtime code must rediscover: ```teal world:spawn(tecs.ecs.EntityKey("player"), tecs.ecs.Name("Player ship")) local player = world:requireKey("player") ``` Callers choose keys. Tecs owns the unique index, releases entries on removal or despawn, and rebuilds it after snapshot load. ## Spawning entities `spawn` accepts initial components and returns an ID immediately: ```teal local player = world:spawn( tecs.Transform2D(100, 100), tecs.gfx.Tint(1, 1, 1, 1), tecs.gfx.Renderable2D, tecs.ecs.Name("Player") ) ``` The ID is reserved immediately, but the entity occupies no archetype until the next pipeline barrier. Later `set`, `remove`, or `despawn` calls in the same transaction may use that ID and edit its final staged result. `spawnAt` and `batchSpawnAt` place caller-chosen packed IDs. Snapshot loading uses them to preserve relationship targets and generations. The caller must ensure that each chosen slot has no live entity. ## Batch mutation `batchSpawn` resolves one component signature and opens one fill callback: ```teal local signature = { tecs.Transform2D, tecs.gfx.Tint, tecs.gfx.Renderable2D, } local firstId, ids = world:batchSpawn( 1000, signature, function(archetype, firstRow, lastRow) local transforms = archetype:getMut(tecs.Transform2D) local tints = archetype:getMut(tecs.gfx.Tint) for row = firstRow, lastRow do local transform = transforms[row] transform.x = row - firstRow transform.y = 0 transform.z = 0 transform.layer = 1 transform.rotation = 0 transform.scaleX = 1 transform.scaleY = 1 local tint = tints[row] tint.r = 1 tint.g = 1 tint.b = 1 tint.a = 1 end end ) ``` The contiguous allocator returns `firstId`; the fallback returns an explicit `ids` list. Both paths reserve IDs before placement. Batch placement follows these rules: - Component constructors do not run. Tecs writes only `requires` defaults before the callback, so the callback must initialize every field it uses. - `EntityKey` cannot participate because each row needs a distinct index claim. - Batch spawn emits no `OnSpawn`; use the fill callback or `onEntitiesAdded`. - Sparse relationship proxies reject writes. Attach each target through `world:set(reservedId, Relationship(target))`. - The callback runs when the pipeline publishes the transaction. `batchSet` adds or replaces one component across a query. Its constant form accepts an instance. Its callback form accepts a bulk-safe component type and opens the destination column for caller writes. Relationships use the constant form; sparse relationships route through their world stores. `batchRemove` removes one component across matching archetypes. `batchDespawn` removes every matched entity. Relationship cleanup, cascade delete, entity observers, and component cleanup force per-entity work where needed; otherwise the batch path clears whole ranges. Every despawn still emits `OnDespawn`. ## Entity clearing and storage maintenance `clearEntities()` removes entity data, pending transactions, sparse stores, keys, queued events, and entity-address observers. It preserves systems, queries, global observers, bundles, component registrations, archetypes, and column capacity. Use a new world when systems and queries must also disappear. `compact()` prunes unreachable empty relationship archetypes and shrinks excess column capacity. Call it only from lifecycle code at a quiet phase boundary; level transitions suit it better than frame loops. `forEachArchetype` and `dirtyArchetypes` expose engine-owned archetype handles for read-only inspection. Do not mutate the world during those iterations. Dirty-archetype iteration resets after each update. `getStats(fill?)` writes current counts into a caller-owned table. Callers may reuse and read that table; Tecs writes its fields on each call. ## Structural transactions {#structural-transactions} Structural mutations always stage. The scheduler publishes at lifecycle and phase boundaries, so systems in one phase normally share a transaction and see the same committed archetype membership. A system that needs an extra boundary declares it in its configuration: ```teal world:addSystem({ name = "game.ResolveDamage", phase = tecs.ecs.phases.Update, commitBefore = true, run = resolveDamage, }) ``` `commitBefore` publishes work from earlier systems in the same phase before this system runs. `commitAfter` publishes this system's work before the next system. Prefer normal phase ordering when it expresses the dependency. `world:enqueueCommit()` requests the conditional form. Inside a system it publishes after that system returns and before the next one runs. Outside system dispatch it publishes synchronously before returning, which lets tests and debug tools intentionally inspect a settled world. Finish any manual query traversal first because synchronous publication may change archetype storage. A value update to an existing committed component writes through immediately, as do writes through `getMut`. Structural additions and removals remain invisible until a scheduler barrier. Query iteration owns no transaction state, so an early `break` is safe. The [mutation model](/modules/ecs/mutation-model) defines the complete contract. ## Plugins and resources A plugin configures one world. Games, engine features, and reusable mechanics all use the same function shape: ```teal local RATE : tecs.data.Key = tecs.data.Store.newKey( "game.spinRate" ) local function spinPlugin(world: tecs.World) world.resources[RATE] = 1.5 -- Build queries and register systems here. end world:addPlugin(spinPlugin) ``` Callers own resource values and may replace them. Tecs owns resource-key identity. Always name keys so hot reload, tooling, `Store.findKey`, and `Store.listKeys` on `tecs.data` can find the same key. Snapshots omit `world.resources`; register a [snapshot handler](/modules/ecs/save-games#snapshot-handlers) for durable resource state. ## World subsystems The world exposes the shared entry points for: - [Components](/modules/ecs/components/) and [relationships](/modules/ecs/relationships/). - [Bundles](/modules/ecs/components/bundles). - [Queries](/modules/ecs/queries/) and hierarchy traversal. - [Systems](/modules/ecs/systems), [phases](/modules/ecs/phases), and [plugins](/modules/ecs/plugins). - [States](/modules/ecs/states). - [Events](/modules/ecs/events). - [Snapshots](/modules/ecs/save-games). Those pages own their interaction rules; generated Teal reference owns individual method signatures and records. --- ## tecs.events # tecs.events Typed events and address-based message buses. Define a record, initialize it in place, and register it once: ```teal local record Damaged is tecs.events.Event amount: number metamethod __call: function(self, amount: number): Damaged end Damaged.init = function(event: Damaged, amount: number) event.amount = amount end tecs.events.newEvent(Damaged) ``` Worlds use the same event definitions and address routing as a standalone [`MessageBus`](/modules/events/#tecs.events.MessageBus). Address `0` conventionally names a whole world, while entity IDs address individual entities. Event delivery is synchronous. The emitter owns an event instance during dispatch, so observers treat its fields as read-only and copy values they need to retain. A table event constructed directly, such as `Damaged(10)`, is an independent value the caller may keep. ## Module contents ### Constructors | Constructor | Description | | --- | --- | | [`newEvent`](/modules/events/#tecs.events.newEvent) | Configures a table event with a callable constructor. | | [`newFFIEvent`](/modules/events/#tecs.events.newFFIEvent) | Configures an FFI event with a callable constructor. | | [`newMessageBus`](/modules/events/#tecs.events.newMessageBus) | Creates an independent address-based event message bus. | ### Types | Type | Kind | Description | | --- | --- | --- | | [`Event`](/modules/events/#tecs.events.Event) | interface | Event names a registered event definition or one of its instances. | | [`EventInit`](/modules/events/#tecs.events.EventInit) | type | EventInit initializes an event instance from constructor arguments. | | [`EventListener`](/modules/events/#tecs.events.EventListener) | type | EventListener synchronously receives an event of its declared type. | | [`MessageBus`](/modules/events/#tecs.events.MessageBus) | interface | MessageBus routes typed events by integer address. | ## Constructors ### tecs.events.newEvent Static Configures a table event with a callable constructor. Registration mutates the supplied definition by assigning its event ID and constructor metatable. Register each definition once. ```teal function tecs.events.newEvent(event: E) ``` #### Type Parameters | Name | Constraint | Description | | --- | --- | --- | | `E` | [`Event`](/modules/events/#tecs.events.Event) | | #### Arguments | Name | Type | Description | | --- | --- | --- | | `event` | `E` | The caller supplies the event record to configure in place. | #### Returns None. ### tecs.events.newFFIEvent Static Configures an FFI event with a callable constructor. Registration mutates the supplied definition. Field names must be unique C identifiers. `eventId` and `typeId` are reserved. Use `double` for entity IDs so their packed slot and generation remain exact. ```teal function tecs.events.newFFIEvent( event: E, fields: {{string, string}}, structName: string ) ``` #### Type Parameters | Name | Constraint | Description | | --- | --- | --- | | `E` | [`Event`](/modules/events/#tecs.events.Event) | | #### Arguments | Name | Type | Description | | --- | --- | --- | | `event` | `E` | The caller supplies the event record to configure in place. | | `fields` | `{{string, string}}` | The caller supplies each field name and C type in constructor order. | | `structName` | `string` | The caller supplies a shared C struct name or omits it to generate one. | #### Returns None. ### tecs.events.newMessageBus Static Creates an independent address-based event message bus. ```teal function tecs.events.newMessageBus(): MessageBus ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | [`MessageBus`](/modules/events/#tecs.events.MessageBus) | Returns an empty message bus with no observers. | ## Types ### tecs.events.Event interface `Event` names a registered event definition or one of its instances. ```teal interface tecs.events.Event eventId: integer end ``` #### tecs.events.Event.eventId field Read-only. The unique ID of the event. ```teal tecs.events.Event.eventId: integer ``` ### tecs.events.EventInit type `EventInit` initializes an event instance from constructor arguments. ```teal type tecs.events.EventInit = function(E, ...: any) ``` ### tecs.events.EventListener type `EventListener` synchronously receives an event of its declared type. ```teal type tecs.events.EventListener = function(E) ``` ### tecs.events.MessageBus interface `MessageBus` routes typed events by integer address. ```teal interface tecs.events.MessageBus emit: function(MessageBus, integer, E) hasObservers: function(MessageBus, integer, E): boolean observeOnce: function( MessageBus, integer, E, function(E) ) stopObserving: function( MessageBus, integer, E, function(E) | string ) clearAddress: function(self, integer) clearEntityObservers: function(self) observe: function( self, address: integer, eventType: E, observer: function(E), id: string ) reset: function(self) end ``` #### tecs.events.MessageBus.emit Static Emit an event to all observers at the specified address. ```teal function tecs.events.MessageBus.emit(MessageBus, integer, E) ``` ##### Type Parameters | Name | Constraint | Description | | --- | --- | --- | | `E` | [`Event`](/modules/events/#tecs.events.Event) | | ##### Arguments | Name | Type | Description | | --- | --- | --- | | `#1` | [`MessageBus`](/modules/events/#tecs.events.MessageBus) | | | `#2` | `integer` | | | `#3` | `E` | | ##### Returns None. #### tecs.events.MessageBus.hasObservers Static Check if any observers exist for an event type at an address. ```teal function tecs.events.MessageBus.hasObservers( MessageBus, integer, E ): boolean ``` ##### Type Parameters | Name | Constraint | Description | | --- | --- | --- | | `E` | [`Event`](/modules/events/#tecs.events.Event) | | ##### Arguments | Name | Type | Description | | --- | --- | --- | | `#1` | [`MessageBus`](/modules/events/#tecs.events.MessageBus) | | | `#2` | `integer` | | | `#3` | `E` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | | #### tecs.events.MessageBus.observeOnce Static Observe an event type at a specific address, at most once. ```teal function tecs.events.MessageBus.observeOnce( MessageBus, integer, E, function(E) ) ``` ##### Type Parameters | Name | Constraint | Description | | --- | --- | --- | | `E` | [`Event`](/modules/events/#tecs.events.Event) | | ##### Arguments | Name | Type | Description | | --- | --- | --- | | `#1` | [`MessageBus`](/modules/events/#tecs.events.MessageBus) | | | `#2` | `integer` | | | `#3` | `E` | | | `#4` | `function(E)` | | ##### Returns None. #### tecs.events.MessageBus.stopObserving Static Stop observing an event type at a specific address. ```teal function tecs.events.MessageBus.stopObserving( MessageBus, integer, E, function(E) | string ) ``` ##### Type Parameters | Name | Constraint | Description | | --- | --- | --- | | `E` | [`Event`](/modules/events/#tecs.events.Event) | | ##### Arguments | Name | Type | Description | | --- | --- | --- | | `#1` | [`MessageBus`](/modules/events/#tecs.events.MessageBus) | | | `#2` | `integer` | | | `#3` | `E` | | | `#4` | function(E) | string | | ##### Returns None. #### tecs.events.MessageBus:clearAddress Instance Clear all observers for an address (used on entity despawn). ```teal function tecs.events.MessageBus.clearAddress(self, integer) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `MessageBus` | | | `#2` | `integer` | | ##### Returns None. #### tecs.events.MessageBus:clearEntityObservers Instance Clear per-entity observers (every address except the global address 0), preserving global subscriptions. Used by world:clearEntities so query infrastructure stays subscribed. ```teal function tecs.events.MessageBus.clearEntityObservers(self) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `MessageBus` | | ##### Returns None. #### tecs.events.MessageBus:observe Instance Observe an event type at a specific address. Address 0 is for world-level events, entity IDs for entity events. ```teal function tecs.events.MessageBus.observe( self, address: integer, eventType: E, observer: function(E), id: string ) ``` ##### Type Parameters | Name | Constraint | Description | | --- | --- | --- | | `E` | [`Event`](/modules/events/#tecs.events.Event) | | ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | [`MessageBus`](/modules/events/#tecs.events.MessageBus) | The message bus that owns the subscription. | | `address` | `integer` | Use 0 for world-level events or an entity ID for entity events. | | `eventType` | `E` | The registered event type to observe. | | `observer` | `function(E)` | Called synchronously with each matching event. | | `id` | `string` | An optional stable name accepted by `stopObserving`. | ##### Returns None. #### tecs.events.MessageBus:reset Instance Reset the entire message bus. ```teal function tecs.events.MessageBus.reset(self) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `MessageBus` | | ##### Returns None. --- ## tecs.gfx.animation # tecs.gfx.animation Sprite sheets, fixed-step playback, Aseprite slices, pivots, and reloads. A [`Sheet`](/modules/gfx/animation/#tecs.gfx.animation.Sheet) divides one image into frames and names frame ranges with tags. An [`Animation`](/modules/gfx/animation/#tecs.gfx.animation.Animation) selects a sheet tag and carries speed, loop, and playback state. ```teal local hero = tecs.gfx.animation.newGridSheet({ name = "hero", imageWidth = 256, imageHeight = 32, frameWidth = 32, frameHeight = 32, tags = { idle = {from = 1, to = 4}, run = {from = 5, to = 8}, }, }) hero:bind(app.renderer.sprites:sprite("hero.png")) world:addPlugin(tecs.gfx.animation.plugin) world:spawn( tecs.Transform2D(64, 64, 0, 1, 0, 32, 32), hero:sprite(), tecs.gfx.animation.of(hero, "run"), tecs.gfx.Renderable2D() ) ``` Playback advances in fixed steps. Machines that replay the same simulation therefore select the same frames. `frameOf` and `timeOf` report playback on the same fixed-step clock. ## Sheet sources Use `newGridSheet` for uniform cells, `newRectSheet` for an explicit frame list, `newSheetBuilder` for a custom sheet, or `newSheetFromAseprite` for an Aseprite JSON export. Frames count from one. Tags name inclusive frame spans and may play forward, reverse, or ping-pong. Bind a sheet to a renderer sprite before drawing it. Every entity playing that sheet shares its frame and timing data. ## Slices and pivots Aseprite slices may move between frames. A [`Pivot`](/modules/gfx/animation/#tecs.gfx.animation.Pivot) bound to a slice follows that movement, which keeps attachments such as hands, muzzles, and feet on the part of the drawing they name. ## Reloads Re-exporting a sheet under the same name can replace its frame, tag, slice, and timing data in place. Existing entities retain the sheet id and continue from their playback state. A replacement must preserve the bound image dimensions. A sprite sheet divides one image into frames, tags, and slices. Aseprite supplies the model: each frame carries its own duration, tags play inclusive spans in a direction, and slices carry rectangles, nine-slice centers, and pivots that may move between frames. `fromAseprite` reads its JSON export into the same interface that `grid`, `rects`, and `build` produce. Frames count from one in sheet order. Tag zero represents the whole sheet playing forward. Bind the finished sheet to a renderer sprite before drawing: ```teal local hero = tecs.gfx.animation.newSheetFromAseprite({ name = "game.hero", json = asepriteExport, }) hero:bind(app.renderer.sprites:sprite("sprites/hero.png")) ``` `sprite(frame)` creates a [`Sprite`](/modules/gfx/#tecs.gfx.Sprite) for one frame. `pivot(name, frame)` creates a [`Pivot`](/modules/gfx/animation/#tecs.gfx.animation.Pivot) from a slice. Animation keeps that pivot on the named slice as the slice moves. A sheet name forms a snapshot compatibility surface. Every entity that plays the sheet shares its registered frame, tag, slice, and timing data. ## Module contents ### Constructors | Constructor | Description | | --- | --- | | [`newGridSheet`](/modules/gfx/animation/#tecs.gfx.animation.newGridSheet) | Creates a sheet whose frames form a uniform grid. | | [`newRectSheet`](/modules/gfx/animation/#tecs.gfx.animation.newRectSheet) | Creates a sheet from explicitly listed frame rectangles. | | [`newSheetBuilder`](/modules/gfx/animation/#tecs.gfx.animation.newSheetBuilder) | Creates a builder for a sheet that the other constructors cannot describe. | | [`newSheetFromAseprite`](/modules/gfx/animation/#tecs.gfx.animation.newSheetFromAseprite) | Creates a sheet from an Aseprite JSON export. | ### Types | Type | Kind | Description | | --- | --- | --- | | [`Animation`](/modules/gfx/animation/#tecs.gfx.animation.Animation) | record | Stores playback state for one entity. | | [`AnimationEvents`](/modules/gfx/animation/#tecs.gfx.animation.AnimationEvents) | record | Requests Completed and Looped events for an entity. | | [`AsepriteOptions`](/modules/gfx/animation/#tecs.gfx.animation.AsepriteOptions) | record | Configures newSheetFromAseprite. | | [`Builder`](/modules/gfx/animation/#tecs.gfx.animation.Builder) | record | Builds custom sheets one frame at a time. | | [`Completed`](/modules/gfx/animation/#tecs.gfx.animation.Completed) | record | Reports when a non-looping tag runs past its last frame. | | [`Direction`](/modules/gfx/animation/#tecs.gfx.animation.Direction) | enum | Selects how a tag walks its span. | | [`GridOptions`](/modules/gfx/animation/#tecs.gfx.animation.GridOptions) | record | Configures newGridSheet. | | [`Looped`](/modules/gfx/animation/#tecs.gfx.animation.Looped) | record | Reports when a looping tag passes its last frame and restarts. | | [`Pivot`](/modules/gfx/animation/#tecs.gfx.animation.Pivot) | record | Read-only. Exposes the pivot component that controls where an entity's quad turns and scales. | | [`PlayOptions`](/modules/gfx/animation/#tecs.gfx.animation.PlayOptions) | record | Configures of and play. | | [`Rect`](/modules/gfx/animation/#tecs.gfx.animation.Rect) | record | One frame, as newRectSheet and the builder take it. | | [`RectsOptions`](/modules/gfx/animation/#tecs.gfx.animation.RectsOptions) | record | Configures newRectSheet. | | [`Sheet`](/modules/gfx/animation/#tecs.gfx.animation.Sheet) | record | Read-only. Exposes an image divided into frames. | | [`Slice`](/modules/gfx/animation/#tecs.gfx.animation.Slice) | record | A named region that moves across the frames. | | [`SliceKey`](/modules/gfx/animation/#tecs.gfx.animation.SliceKey) | record | Defines where a slice sits from one frame onward. | | [`Tag`](/modules/gfx/animation/#tecs.gfx.animation.Tag) | record | A named span of frames, as the constructors take it. | ### Functions | Function | Kind | Description | | --- | --- | --- | | [`build`](/modules/gfx/animation/#tecs.gfx.animation.build) | Static | Creates a builder for a sheet that the other constructors cannot describe. | | [`byId`](/modules/gfx/animation/#tecs.gfx.animation.byId) | Static | Returns the sheet represented by a registration index, or nil. | | [`byName`](/modules/gfx/animation/#tecs.gfx.animation.byName) | Static | Returns the sheet registered under a name, or nil. | | [`findSheetById`](/modules/gfx/animation/#tecs.gfx.animation.findSheetById) | Static | Returns the sheet represented by a process-wide id. | | [`findSheetByName`](/modules/gfx/animation/#tecs.gfx.animation.findSheetByName) | Static | Returns the sheet registered under a name. | | [`frameOf`](/modules/gfx/animation/#tecs.gfx.animation.frameOf) | Static | Returns the sheet frame shown by an entity's animation. | | [`fromAseprite`](/modules/gfx/animation/#tecs.gfx.animation.fromAseprite) | Static | Creates a sheet from an Aseprite JSON export. | | [`grid`](/modules/gfx/animation/#tecs.gfx.animation.grid) | Static | Creates a sheet whose frames form a uniform grid. | | [`of`](/modules/gfx/animation/#tecs.gfx.animation.of) | Static | Creates an Animation that plays a named sheet tag and is ready to spawn. | | [`play`](/modules/gfx/animation/#tecs.gfx.animation.play) | Static | Points a live entity at a tag and restarts it there. | | [`plugin`](/modules/gfx/animation/#tecs.gfx.animation.plugin) | Static | Adds the systems that drive playback. | | [`rects`](/modules/gfx/animation/#tecs.gfx.animation.rects) | Static | Creates a sheet from explicitly listed frame rectangles. | | [`replace`](/modules/gfx/animation/#tecs.gfx.animation.replace) | Static | Folds a re-exported sheet into the one already registered under its name, in place, so an entity playing the old id... | | [`restart`](/modules/gfx/animation/#tecs.gfx.animation.restart) | Static | Plays an entity's animation again from the start of its tag. | | [`revision`](/modules/gfx/animation/#tecs.gfx.animation.revision) | Static | Returns how many times any sheet's frames have changed. | | [`sheetRevision`](/modules/gfx/animation/#tecs.gfx.animation.sheetRevision) | Static | Returns how many times any sheet's frames have changed. | | [`timeOf`](/modules/gfx/animation/#tecs.gfx.animation.timeOf) | Static | Returns an animation's position in its tag cycle, in seconds. | ### Values | Value | Type | Description | | --- | --- | --- | | [`DEFAULT_DURATION`](/modules/gfx/animation/#tecs.gfx.animation.DEFAULT_DURATION) | `number` | Read-only. Reports how many milliseconds a frame remains visible when nothing overrides its duration. | ## Constructors ### tecs.gfx.animation.newGridSheet Static Creates a sheet whose frames form a uniform grid. ```teal function tecs.gfx.animation.newGridSheet( options: sheet.GridOptions ): sheet.Sheet ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `options` | [`sheet.GridOptions`](/modules/gfx/animation/#tecs.gfx.animation.GridOptions) | Raises on a missing name, a non-positive image or frame size, a grid that fits no cells, or a `count` past what the grid holds. Frames come out in row-major order. | #### Returns | Type | Description | | --- | --- | | [`sheet.Sheet`](/modules/gfx/animation/#tecs.gfx.animation.Sheet) | The finished sheet, already registered under its name and carrying an `id`. | ### tecs.gfx.animation.newRectSheet Static Creates a sheet from explicitly listed frame rectangles. ```teal function tecs.gfx.animation.newRectSheet( options: sheet.RectsOptions ): sheet.Sheet ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `options` | [`sheet.RectsOptions`](/modules/gfx/animation/#tecs.gfx.animation.RectsOptions) | Raises on a missing name, a non-positive image size, an empty frame list, or a frame with no positive size. | #### Returns | Type | Description | | --- | --- | | [`sheet.Sheet`](/modules/gfx/animation/#tecs.gfx.animation.Sheet) | The finished sheet, already registered under its name and carrying an `id`. | ### tecs.gfx.animation.newSheetBuilder Static Creates a builder for a sheet that the other constructors cannot describe. ```teal function tecs.gfx.animation.newSheetBuilder( name: string, imageWidth: number, imageHeight: number ): sheet.Builder ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `name` | `string` | Name to register the finished sheet under. | | `imageWidth` | `number` | Size of the image the frames are cut from, in pixels. | | `imageHeight` | `number` | The other size, in pixels. | #### Returns | Type | Description | | --- | --- | | [`sheet.Builder`](/modules/gfx/animation/#tecs.gfx.animation.Builder) | Returns a builder that registers the sheet when `finish` runs. | ### tecs.gfx.animation.newSheetFromAseprite Static Creates a sheet from an Aseprite JSON export. ```teal function tecs.gfx.animation.newSheetFromAseprite( options: sheet.AsepriteOptions ): sheet.Sheet ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `options` | [`sheet.AsepriteOptions`](/modules/gfx/animation/#tecs.gfx.animation.AsepriteOptions) | Raises on a missing name or an export the reader cannot make a sheet out of. | #### Returns | Type | Description | | --- | --- | | [`sheet.Sheet`](/modules/gfx/animation/#tecs.gfx.animation.Sheet) | The finished sheet, already registered. | ## Types ### tecs.gfx.animation.Animation record Stores playback state for one entity. What an entity is playing lives here and where its cycle has got to lives in its [`Sprite`](/modules/gfx/#tecs.gfx.Sprite), which carries the playback the shader resolves. So `frame` is not an index: the encoder writes `ENCODED` to say the [`Sprite`](/modules/gfx/#tecs.gfx.Sprite) carries a live playback, and zero says the entity is to start its cycle again. Which frame is showing is `animation.frameOf`. `time` is the phase the cycle starts or resumes from, in seconds, and stays inside the cycle. Nothing advances it while playback runs, so it neither grows without bound nor loses precision to its own age. Read-only. Exposes the playback-state component for one entity. ```teal record tecs.gfx.animation.Animation is Component sheet: number tag: number speed: number time: number frame: number loop: boolean playing: boolean end ``` #### Interfaces | Interface | | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | #### tecs.gfx.animation.Animation.sheet field Caller-writable. Selects a sheet by its `Sheet.id` registration index. Zero plays nothing. ```teal tecs.gfx.animation.Animation.sheet: number ``` #### tecs.gfx.animation.Animation.tag field Caller-writable. Selects a tag by its `Sheet:tagId` index. Zero plays the whole sheet in order. ```teal tecs.gfx.animation.Animation.tag: number ``` #### tecs.gfx.animation.Animation.speed field Caller-writable. Multiplies the timing the sheet carries. One is the timing as authored, two is twice as fast. Zero or less holds the current frame and stops time advancing, which is a pause that leaves `playing` alone. How long each frame is held is the sheet's answer and not an entity's, because that is where an artist sets it: a hold frame is a frame with a long duration, which no single rate can express. ```teal tecs.gfx.animation.Animation.speed: number ``` #### tecs.gfx.animation.Animation.time field Engine-owned. Carries the phase the cycle starts or resumes from, in seconds. Ordinary game code should read `timeOf`, which reports where the cycle has got to; this field stays where playback was last started from. The cycle duration is the sum of the durations of the frames the tag visits. Changing `speed` leaves the phase where it is, so playback carries on from the frame it was showing rather than jumping. ```teal tecs.gfx.animation.Animation.time: number ``` #### tecs.gfx.animation.Animation.frame field Engine-owned. Says whether the Sprite carries a live playback, and never which frame that playback is showing. Ordinary game code should use `frameOf`. Zero asks for the cycle to start again, which is what `play`, `restart`, `of` and a restored snapshot write. ```teal tecs.gfx.animation.Animation.frame: number ``` #### tecs.gfx.animation.Animation.loop field Caller-writable. Controls whether the tag restarts after its last frame. ```teal tecs.gfx.animation.Animation.loop: boolean ``` #### tecs.gfx.animation.Animation.playing field Caller-writable. Controls whether time advances. ```teal tecs.gfx.animation.Animation.playing: boolean ``` ### tecs.gfx.animation.AnimationEvents record Requests [`Completed`](/modules/gfx/animation/#tecs.gfx.animation.Completed) and [`Looped`](/modules/gfx/animation/#tecs.gfx.animation.Looped) events for an entity. The event query visits only entities carrying this tag. A game can request events for a handful of entities without walking the rest of its animated crowd. Adding the tag costs one archetype move and nothing per step. Read-only. Exposes the tag that requests [`Completed`](/modules/gfx/animation/#tecs.gfx.animation.Completed) and [`Looped`](/modules/gfx/animation/#tecs.gfx.animation.Looped) events for an entity. ```teal record tecs.gfx.animation.AnimationEvents is Component end ``` #### Interfaces | Interface | | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | ### tecs.gfx.animation.AsepriteOptions record Configures `newSheetFromAseprite`. ```teal record tecs.gfx.animation.AsepriteOptions name: string json: any end ``` #### tecs.gfx.animation.AsepriteOptions.name field Caller-writable. Sets the registration name. It defaults to the export's image name. An export without an image name requires this field. ```teal tecs.gfx.animation.AsepriteOptions.name: string ``` #### tecs.gfx.animation.AsepriteOptions.json field Caller-writable. Supplies the export as JSON text or a decoded table. it. A table is what an asset pipeline that decoded once should pass. ```teal tecs.gfx.animation.AsepriteOptions.json: any ``` ### tecs.gfx.animation.Builder record Builds custom sheets one frame at a time. ```teal record tecs.gfx.animation.Builder finish: function(self): Sheet frame: function( self, x: number, y: number, w: number, h: number, duration: number ): Builder slice: function( self, name: string, x: number, y: number, w: number, h: number, pivotX: number, pivotY: number ): Builder sliceKeys: function( self, name: string, data: string, keys: {SliceKey} ): Builder tag: function( self, name: string, from: integer, to: integer, direction: Direction ): Builder end ``` #### tecs.gfx.animation.Builder:finish Instance Registers and returns the finished sheet. Raises on a sheet with no frames, and on a tag whose span falls outside them. ```teal function tecs.gfx.animation.Builder.finish(self): Sheet ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Builder` | | ##### Returns | Type | Description | | --- | --- | | [`Sheet`](/modules/gfx/animation/#tecs.gfx.animation.Sheet) | | #### tecs.gfx.animation.Builder:frame Instance Appends a frame to the sheet under construction. ```teal function tecs.gfx.animation.Builder.frame( self, x: number, y: number, w: number, h: number, duration: number ): Builder ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Builder` | | | `x` | `number` | Left of the frame in the image, in pixels. | | `y` | `number` | Top of the frame. | | `w` | `number` | Width, which must be positive. | | `h` | `number` | Height, which must be positive. | | `duration` | `number` | Milliseconds it is held. Defaults to `sheet.DEFAULT_DURATION`. | ##### Returns | Type | Description | | --- | --- | | [`Builder`](/modules/gfx/animation/#tecs.gfx.animation.Builder) | The builder. | #### tecs.gfx.animation.Builder:slice Instance Adds a fixed slice with a pivot. ```teal function tecs.gfx.animation.Builder.slice( self, name: string, x: number, y: number, w: number, h: number, pivotX: number, pivotY: number ): Builder ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Builder` | | | `name` | `string` | What a sprite names to take its pivot from. | | `x` | `number` | Left of the slice within a frame, in pixels. | | `y` | `number` | Top of the slice. | | `w` | `number` | Width. | | `h` | `number` | Height. | | `pivotX` | `number` | Pivot within the slice, in the slice's own pixels. Omitted, the slice carries no pivot and its middle stands in. | | `pivotY` | `number` | | ##### Returns | Type | Description | | --- | --- | | [`Builder`](/modules/gfx/animation/#tecs.gfx.animation.Builder) | The builder. | #### tecs.gfx.animation.Builder:sliceKeys Instance Adds a slice from explicit keys for importers. ```teal function tecs.gfx.animation.Builder.sliceKeys( self, name: string, data: string, keys: {SliceKey} ): Builder ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Builder` | | | `name` | `string` | The slice's name. | | `data` | `string` | Free text to carry alongside it, or nil for none. | | `keys` | `{`[`SliceKey`](/modules/gfx/animation/#tecs.gfx.animation.SliceKey)`}` | Keys in frame order, at least one. | ##### Returns | Type | Description | | --- | --- | | [`Builder`](/modules/gfx/animation/#tecs.gfx.animation.Builder) | The builder. | #### tecs.gfx.animation.Builder:tag Instance Names an inclusive frame span and its playback direction. ```teal function tecs.gfx.animation.Builder.tag( self, name: string, from: integer, to: integer, direction: Direction ): Builder ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Builder` | | | `name` | `string` | What an animation asks for. | | `from` | `integer` | First frame, counting from one. | | `to` | `integer` | Last frame, inclusive. | | `direction` | [`Direction`](/modules/gfx/animation/#tecs.gfx.animation.Direction) | Defaults to "forward". | ##### Returns | Type | Description | | --- | --- | | [`Builder`](/modules/gfx/animation/#tecs.gfx.animation.Builder) | The builder. | ### tecs.gfx.animation.Completed record Reports when a non-looping tag runs past its last frame. The animation stops and holds that frame, so this fires once per playthrough and never again until something restarts it. Read-only. Exposes the event emitted when a non-looping tag finishes. ```teal record tecs.gfx.animation.Completed is events.Event entity: integer sheet: Sheet tag: string metamethod __call: function( self, entity: integer, sheet: Sheet, tag: string ): Completed end ``` #### Interfaces | Interface | | --- | | [`events.Event`](/modules/events/#tecs.events.Event) | #### tecs.gfx.animation.Completed.entity field Read-only. Identifies the entity whose animation completed. ```teal tecs.gfx.animation.Completed.entity: integer ``` #### tecs.gfx.animation.Completed.sheet field Read-only. Reports the sheet that completed. ```teal tecs.gfx.animation.Completed.sheet: Sheet ``` #### tecs.gfx.animation.Completed.tag field Read-only. Reports the tag that finished, or the empty string for a whole sheet. ```teal tecs.gfx.animation.Completed.tag: string ``` #### tecs.gfx.animation.Completed:__call metamethod Creates a completed event value. ```teal metamethod tecs.gfx.animation.Completed.$meta.__call( self, entity: integer, sheet: Sheet, tag: string ): Completed ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Completed` | The completed event type. | | `entity` | `integer` | The entity whose animation completed. | | `sheet` | [`Sheet`](/modules/gfx/animation/#tecs.gfx.animation.Sheet) | The sheet that completed. | | `tag` | `string` | The tag that completed, or an empty string for the whole sheet. | ##### Returns | Type | Description | | --- | --- | | [`Completed`](/modules/gfx/animation/#tecs.gfx.animation.Completed) | The completed event value. | ### tecs.gfx.animation.Direction enum Selects how a tag walks its span. ```teal enum tecs.gfx.animation.Direction "forward" "pingpong" "reverse" end ``` ### tecs.gfx.animation.GridOptions record Configures `newGridSheet`. ```teal record tecs.gfx.animation.GridOptions name: string imageWidth: number imageHeight: number frameWidth: number frameHeight: number margin: number spacing: number columns: integer rows: integer count: integer duration: number tags: {string: Tag} slices: {Slice} end ``` #### tecs.gfx.animation.GridOptions.name field Caller-writable. Sets the required registration name. ```teal tecs.gfx.animation.GridOptions.name: string ``` #### tecs.gfx.animation.GridOptions.imageWidth field Caller-writable. Sets the required image width in pixels. ```teal tecs.gfx.animation.GridOptions.imageWidth: number ``` #### tecs.gfx.animation.GridOptions.imageHeight field Caller-writable. Sets the required image height in pixels. ```teal tecs.gfx.animation.GridOptions.imageHeight: number ``` #### tecs.gfx.animation.GridOptions.frameWidth field Caller-writable. Sets the required frame width in pixels. ```teal tecs.gfx.animation.GridOptions.frameWidth: number ``` #### tecs.gfx.animation.GridOptions.frameHeight field Caller-writable. Sets the required frame height in pixels. ```teal tecs.gfx.animation.GridOptions.frameHeight: number ``` #### tecs.gfx.animation.GridOptions.margin field Caller-writable. Sets the border between the image edge and first cell. Defaults to zero. ```teal tecs.gfx.animation.GridOptions.margin: number ``` #### tecs.gfx.animation.GridOptions.spacing field Caller-writable. Sets the gap between neighboring cells and defaults to zero. ```teal tecs.gfx.animation.GridOptions.spacing: number ``` #### tecs.gfx.animation.GridOptions.columns field Caller-writable. Sets how many cells fit across. The constructor derives it from the image size when omitted. ```teal tecs.gfx.animation.GridOptions.columns: integer ``` #### tecs.gfx.animation.GridOptions.rows field Caller-writable. Sets how many cells fit down. The constructor derives it from the image size when omitted. ```teal tecs.gfx.animation.GridOptions.rows: integer ``` #### tecs.gfx.animation.GridOptions.count field Caller-writable. Sets how many frames to take across rows. It defaults to every cell, which is wrong only for a sheet whose last row is short. ```teal tecs.gfx.animation.GridOptions.count: integer ``` #### tecs.gfx.animation.GridOptions.duration field Caller-writable. Sets how many milliseconds every cell remains visible. It defaults to `DEFAULT_DURATION`. A grid uses one duration for every frame; retime individual frames with `sheet.build`. ```teal tecs.gfx.animation.GridOptions.duration: number ``` #### tecs.gfx.animation.GridOptions.tags field Caller-writable. Defines optional named tags over the frames. ```teal tecs.gfx.animation.GridOptions.tags: {string: Tag} ``` #### tecs.gfx.animation.GridOptions.slices field Caller-writable. Defines optional named slices. ```teal tecs.gfx.animation.GridOptions.slices: {Slice} ``` ### tecs.gfx.animation.Looped record Reports when a looping tag passes its last frame and restarts. Once per step at most, not once per cycle: a step long enough to cover several cycles wraps the time once and reports one loop, so counting these is not a way to count playthroughs. Read-only. Exposes the event emitted when a looping tag restarts. ```teal record tecs.gfx.animation.Looped is events.Event entity: integer sheet: Sheet tag: string metamethod __call: function( self, entity: integer, sheet: Sheet, tag: string ): Looped end ``` #### Interfaces | Interface | | --- | | [`events.Event`](/modules/events/#tecs.events.Event) | #### tecs.gfx.animation.Looped.entity field Read-only. Identifies the entity whose animation looped. ```teal tecs.gfx.animation.Looped.entity: integer ``` #### tecs.gfx.animation.Looped.sheet field Read-only. Reports the sheet that looped. ```teal tecs.gfx.animation.Looped.sheet: Sheet ``` #### tecs.gfx.animation.Looped.tag field Read-only. Reports the tag that wrapped, or the empty string for a whole sheet. ```teal tecs.gfx.animation.Looped.tag: string ``` #### tecs.gfx.animation.Looped:__call metamethod Creates a looped event value. ```teal metamethod tecs.gfx.animation.Looped.$meta.__call( self, entity: integer, sheet: Sheet, tag: string ): Looped ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Looped` | The looped event type. | | `entity` | `integer` | The entity whose animation looped. | | `sheet` | [`Sheet`](/modules/gfx/animation/#tecs.gfx.animation.Sheet) | The sheet that looped. | | `tag` | `string` | The tag that looped, or an empty string for the whole sheet. | ##### Returns | Type | Description | | --- | --- | | [`Looped`](/modules/gfx/animation/#tecs.gfx.animation.Looped) | The looped event value. | ### tecs.gfx.animation.Pivot record Read-only. Exposes the pivot component that controls where an entity's quad turns and scales. ```teal record tecs.gfx.animation.Pivot is Component x: number y: number sheet: number slice: number halfX: number halfY: number end ``` #### Interfaces | Interface | | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | #### tecs.gfx.animation.Pivot.x field Caller-writable. Sets the pivot as a fraction across the frame from its left edge. One half selects the middle. ```teal tecs.gfx.animation.Pivot.x: number ``` #### tecs.gfx.animation.Pivot.y field Caller-writable. Sets the pivot as a fraction down the frame from its top edge. One half selects the middle. ```teal tecs.gfx.animation.Pivot.y: number ``` #### tecs.gfx.animation.Pivot.sheet field Caller-writable. Selects the sheet that owns the slice by registration index, or zero for a direct pivot. ```teal tecs.gfx.animation.Pivot.sheet: number ``` #### tecs.gfx.animation.Pivot.slice field Caller-writable. Selects the slice within that sheet by its `Sheet:sliceId` index, or zero for a pivot written directly. ```teal tecs.gfx.animation.Pivot.slice: number ``` #### tecs.gfx.animation.Pivot.halfX field Engine-owned. Stores half the horizontal range that the point may travel over the current cycle, as a fraction of the frame. A direct pivot or single-key slice uses zero, keeping the quad's exact cull bound. A moving slice uses `x` and `y` for the travel midpoint and this field for its reach on either side. ```teal tecs.gfx.animation.Pivot.halfX: number ``` #### tecs.gfx.animation.Pivot.halfY field Engine-owned. Stores the pivot's vertical travel for culling. Ordinary game code should ignore this field. ```teal tecs.gfx.animation.Pivot.halfY: number ``` ### tecs.gfx.animation.PlayOptions record Configures `of` and `play`. ```teal record tecs.gfx.animation.PlayOptions speed: number loop: boolean playing: boolean end ``` #### tecs.gfx.animation.PlayOptions.speed field Caller-writable. Multiplies the sheet's own timing and defaults to one. ```teal tecs.gfx.animation.PlayOptions.speed: number ``` #### tecs.gfx.animation.PlayOptions.loop field Caller-writable. Controls whether the tag restarts after its last frame. Defaults to true. ```teal tecs.gfx.animation.PlayOptions.loop: boolean ``` #### tecs.gfx.animation.PlayOptions.playing field Caller-writable. Controls whether playback starts immediately. Defaults to true. ```teal tecs.gfx.animation.PlayOptions.playing: boolean ``` ### tecs.gfx.animation.Rect record One frame, as `newRectSheet` and the builder take it. ```teal record tecs.gfx.animation.Rect x: number y: number w: number h: number duration: number end ``` #### tecs.gfx.animation.Rect.x field Caller-writable. Sets the frame's left edge in image pixels. ```teal tecs.gfx.animation.Rect.x: number ``` #### tecs.gfx.animation.Rect.y field Caller-writable. Sets the frame's top edge in image pixels. ```teal tecs.gfx.animation.Rect.y: number ``` #### tecs.gfx.animation.Rect.w field Caller-writable. Sets the frame width in pixels. ```teal tecs.gfx.animation.Rect.w: number ``` #### tecs.gfx.animation.Rect.h field Caller-writable. Sets the frame height in pixels. ```teal tecs.gfx.animation.Rect.h: number ``` #### tecs.gfx.animation.Rect.duration field Caller-writable. Sets the frame duration in milliseconds and defaults to `sheet.DEFAULT_DURATION`. ```teal tecs.gfx.animation.Rect.duration: number ``` ### tecs.gfx.animation.RectsOptions record Configures `newRectSheet`. ```teal record tecs.gfx.animation.RectsOptions name: string imageWidth: number imageHeight: number frames: {Rect} tags: {string: Tag} slices: {Slice} end ``` #### tecs.gfx.animation.RectsOptions.name field Caller-writable. Sets the required registration name. ```teal tecs.gfx.animation.RectsOptions.name: string ``` #### tecs.gfx.animation.RectsOptions.imageWidth field Caller-writable. Sets the required image width in pixels. ```teal tecs.gfx.animation.RectsOptions.imageWidth: number ``` #### tecs.gfx.animation.RectsOptions.imageHeight field Caller-writable. Sets the required image height in pixels. ```teal tecs.gfx.animation.RectsOptions.imageHeight: number ``` #### tecs.gfx.animation.RectsOptions.frames field Caller-writable. Sets the required frame rects in address order. ```teal tecs.gfx.animation.RectsOptions.frames: {Rect} ``` #### tecs.gfx.animation.RectsOptions.tags field Caller-writable. Defines optional named tags over the frames. ```teal tecs.gfx.animation.RectsOptions.tags: {string: Tag} ``` #### tecs.gfx.animation.RectsOptions.slices field Caller-writable. Defines optional named slices. ```teal tecs.gfx.animation.RectsOptions.slices: {Slice} ``` ### tecs.gfx.animation.Sheet record Read-only. Exposes an image divided into frames. ```teal record tecs.gfx.animation.Sheet name: string id: integer count: integer imageWidth: number imageHeight: number bind: function(self, sprite: Sprite): Sheet cycle: function(self, id: integer): number duration: function(self, frame: integer): number frameAt: function(self, id: integer, time: number): integer hasTag: function(self, name: string): boolean pivot: function(self, name: string, frame: integer): Pivot pivotOf: function(self, id: integer, frame: integer): number, number rect: function(self, frame: integer): number, number, number, number slice: function(self, name: string): Slice sliceId: function(self, name: string): integer sliceKeyAt: function(self, id: integer, frame: integer): SliceKey sliceName: function(self, id: integer): string sprite: function(self, frame: integer): Sprite tag: function(self, name: string): integer, integer, Direction tagId: function(self, name: string): integer tagName: function(self, id: integer): string uv: function(self, frame: integer): number, number, number, number end ``` #### tecs.gfx.animation.Sheet.name field Read-only. Reports the registered name that snapshots store. ```teal tecs.gfx.animation.Sheet.name: string ``` #### tecs.gfx.animation.Sheet.id field Read-only. Reports the registration index carried by an [`Animation`](/modules/gfx/animation/#tecs.gfx.animation.Animation). Construction assigns an id once and never reuses it. ```teal tecs.gfx.animation.Sheet.id: integer ``` #### tecs.gfx.animation.Sheet.count field Read-only. Reports the number of frames and the largest index `rect`, `uv` and `sprite` accept. ```teal tecs.gfx.animation.Sheet.count: integer ``` #### tecs.gfx.animation.Sheet.imageWidth field Read-only. Reports the source image width in pixels. Frame rectangles use this width, so binding the sheet to an image of a different size places frames incorrectly. ```teal tecs.gfx.animation.Sheet.imageWidth: number ``` #### tecs.gfx.animation.Sheet.imageHeight field Read-only. Reports the height in pixels of the image from which frames are cut. ```teal tecs.gfx.animation.Sheet.imageHeight: number ``` #### tecs.gfx.animation.Sheet:bind Instance Resolves the sheet's frames against a registered image. `sprite` is what `renderer.sprites:sprite(name)` returns for a whole image: its `u1` and `v1` are the fractions of the texture-array layer that image occupies. The function scales the frame's image fraction by them before it names a region of the layer. Pass a sub-rect and the frames land inside that sub-rect, which is not what the sheet describes. Binding again rescales from the pixel rectangles instead of the previous result, so re-registering an image does not accumulate scaling error. Entities already carrying regions from an earlier bind keep them: a rebind does not update any existing [`Sprite`](/modules/gfx/#tecs.gfx.Sprite). ```teal function tecs.gfx.animation.Sheet.bind(self, sprite: Sprite): Sheet ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Sheet` | | | `sprite` | [`Sprite`](/modules/gfx/#tecs.gfx.Sprite) | A whole image, from `renderer.sprites:sprite`. | ##### Returns | Type | Description | | --- | --- | | [`Sheet`](/modules/gfx/animation/#tecs.gfx.animation.Sheet) | Returns the sheet so callers can chain `bind`. | #### tecs.gfx.animation.Sheet:cycle Instance Returns the duration of one tag cycle in seconds. ```teal function tecs.gfx.animation.Sheet.cycle(self, id: integer): number ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Sheet` | | | `id` | `integer` | A tag index, or zero for the whole sheet. | ##### Returns | Type | Description | | --- | --- | | `number` | The sum of the durations of the frames the cycle visits, which for a pingpong tag counts the frames it passes twice twice. | #### tecs.gfx.animation.Sheet:duration Instance Returns how long a frame remains visible, in seconds. ```teal function tecs.gfx.animation.Sheet.duration(self, frame: integer): number ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Sheet` | | | `frame` | `integer` | One to `count`. Anything else raises. | ##### Returns | Type | Description | | --- | --- | | `number` | Returns the authored frame duration in seconds. | #### tecs.gfx.animation.Sheet:frameAt Instance Returns the frame shown by a tag at a point in its cycle. ```teal function tecs.gfx.animation.Sheet.frameAt( self, id: integer, time: number ): integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Sheet` | | | `id` | `integer` | A tag index, or zero for the whole sheet. | | `time` | `number` | Seconds into the cycle. Outside it clamps rather than wrapping, since wrapping is the caller's decision about looping. | ##### Returns | Type | Description | | --- | --- | | `integer` | A frame index into the whole sheet, counting from one. | #### tecs.gfx.animation.Sheet:hasTag Instance Returns whether the sheet contains the given tag. ```teal function tecs.gfx.animation.Sheet.hasTag(self, name: string): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Sheet` | | | `name` | `string` | Any string, and nil answers false rather than raising, so this is what to ask before `tag`. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Whether `tag` would answer for that name. | #### tecs.gfx.animation.Sheet:pivot Instance Creates a Pivot from a named slice, ready to spawn. Bound to the slice as well as resolved from it, so playback moves the pivot as the frame changes rather than leaving it where the frame it was built from put it. Fails on a name the sheet does not carry, for the reason `tag` does: the alternative is a typo that silently pivots on the middle. ```teal function tecs.gfx.animation.Sheet.pivot( self, name: string, frame: integer ): Pivot ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Sheet` | | | `name` | `string` | A slice this sheet carries. | | `frame` | `integer` | The frame to resolve it at, defaulting to the first. What an entity shows before its first step, and what a sheet with no Animation keeps. | ##### Returns | Type | Description | | --- | --- | | [`Pivot`](/modules/gfx/animation/#tecs.gfx.animation.Pivot) | A component value, not an entity. | #### tecs.gfx.animation.Sheet:pivotOf Instance Returns a slice pivot as a fraction of its frame. Aseprite writes a pivot in the slice's own pixels, so this adds the slice's origin and divides by the frame, which is the number a quad wants: nothing downstream has to know the sheet's pixel sizes. A slice with a center but no pivot answers the center's middle, and a slice with neither answers the middle of its own rectangle. Zero, a slice the sheet does not carry, or a frame it has no key for all answer the middle of the frame, which is where a quad sits with no pivot at all. ```teal function tecs.gfx.animation.Sheet.pivotOf( self, id: integer, frame: integer ): number, number ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Sheet` | | | `id` | `integer` | A slice index from `sliceId`. | | `frame` | `integer` | A frame index. | ##### Returns | Type | Description | | --- | --- | | `number` | The pivot's x and y as fractions of the frame, from its top left. | | `number` | | #### tecs.gfx.animation.Sheet:rect Instance Returns a frame's pixel rectangle as x, y, width, and height. ```teal function tecs.gfx.animation.Sheet.rect( self, frame: integer ): number, number, number, number ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Sheet` | | | `frame` | `integer` | One to `count`. Anything else raises, since a frame index out of range is a sheet and an animation disagreeing rather than something to paper over. | ##### Returns | Type | Description | | --- | --- | | `number` | The rect's left, top, width and height, in the pixels of the image the sheet was cut from. | | `number` | | | `number` | | | `number` | | #### tecs.gfx.animation.Sheet:slice Instance Returns a slice by name, or nil when the sheet does not contain one. ```teal function tecs.gfx.animation.Sheet.slice(self, name: string): Slice ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Sheet` | | | `name` | `string` | A slice name. | ##### Returns | Type | Description | | --- | --- | | [`Slice`](/modules/gfx/animation/#tecs.gfx.animation.Slice) | The slice, whose keys the caller must not mutate. | #### tecs.gfx.animation.Sheet:sliceId Instance Returns the index represented by a slice name, or zero when absent. ```teal function tecs.gfx.animation.Sheet.sliceId(self, name: string): integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Sheet` | | | `name` | `string` | A slice name, or nil. | ##### Returns | Type | Description | | --- | --- | | `integer` | The index, which is what a component carries in place of the name for the reason a tag id is. | #### tecs.gfx.animation.Sheet:sliceKeyAt Instance Returns the slice key active on a frame, or nil. A slice holds a key until the next one, so this answers the last key at or before the frame rather than only an exact match. ```teal function tecs.gfx.animation.Sheet.sliceKeyAt( self, id: integer, frame: integer ): SliceKey ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Sheet` | | | `id` | `integer` | A slice index from `sliceId`. Zero answers nil. | | `frame` | `integer` | A frame index. | ##### Returns | Type | Description | | --- | --- | | [`SliceKey`](/modules/gfx/animation/#tecs.gfx.animation.SliceKey) | | #### tecs.gfx.animation.Sheet:sliceName Instance Returns the name represented by a slice index, or the empty string. ```teal function tecs.gfx.animation.Sheet.sliceName(self, id: integer): string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Sheet` | | | `id` | `integer` | A slice index from `sliceId`. Zero or an absent index returns the empty string. | ##### Returns | Type | Description | | --- | --- | | `string` | The slice name, or the empty string when the index is absent. | #### tecs.gfx.animation.Sheet:sprite Instance Creates a Sprite showing one frame, ready to spawn. Defaults to the first frame. Meaningful after `bind`, since before it the sheet names no image and the quad has no layer to sample. ```teal function tecs.gfx.animation.Sheet.sprite(self, frame: integer): Sprite ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Sheet` | | | `frame` | `integer` | One to `count`, defaulting to one. Anything else raises. | ##### Returns | Type | Description | | --- | --- | | [`Sprite`](/modules/gfx/#tecs.gfx.Sprite) | Returns a fresh [`Sprite`](/modules/gfx/#tecs.gfx.Sprite) that the caller owns, not a view onto the sheet. | #### tecs.gfx.animation.Sheet:tag Instance Returns the first frame, last frame, and direction of a named tag. Fails on a name the sheet does not carry, because the alternative is an animation silently playing the whole sheet on a typo. ```teal function tecs.gfx.animation.Sheet.tag( self, name: string ): integer, integer, Direction ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Sheet` | | | `name` | `string` | A tag this sheet was built with. | ##### Returns | Type | Description | | --- | --- | | `integer` | The tag's first frame index, its last, and its direction. | | `integer` | | | [`Direction`](/modules/gfx/animation/#tecs.gfx.animation.Direction) | | #### tecs.gfx.animation.Sheet:tagId Instance Returns the index represented by a tag name, or zero when absent. Zero reads as the whole sheet rather than as nothing, so an animation that names no tag plays every frame in order. A name the sheet does not carry is reported at error level under the `tecs.gfx` logger and then treated as the whole sheet. Zero is a plausible wrong answer rather than a visible failure, so a misspelled tag would otherwise animate every frame with nothing said. The report names the sheet, the name asked for, and the tags the sheet does carry, once per sheet and name however often the name is asked. Ask `hasTag` instead when a name's absence is expected and ordinary. ```teal function tecs.gfx.animation.Sheet.tagId(self, name: string): integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Sheet` | | | `name` | `string` | A tag name, or nil or the empty string for the whole sheet. The empty string is what a snapshot stores for tag zero, so neither it nor nil is reported. | ##### Returns | Type | Description | | --- | --- | | `integer` | Returns the index an [`Animation`](/modules/gfx/animation/#tecs.gfx.animation.Animation) carries. The index belongs to this sheet alone: ids follow tag names in sorted order, so the same name in another sheet has another number. | #### tecs.gfx.animation.Sheet:tagName Instance Returns the name represented by a tag index, or the empty string for the whole sheet. ```teal function tecs.gfx.animation.Sheet.tagName(self, id: integer): string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Sheet` | | | `id` | `integer` | An index from `tagId`. Nil, zero and anything the sheet does not carry all answer the empty string. | ##### Returns | Type | Description | | --- | --- | | `string` | The tag's name, which is what a snapshot writes instead of the index. | #### tecs.gfx.animation.Sheet:uv Instance Returns a frame's region as u0, v0, u1, and v1. Fractions of the image before `bind` and of the texture-array layer after it, which is the region a [`Sprite`](/modules/gfx/#tecs.gfx.Sprite) needs. ```teal function tecs.gfx.animation.Sheet.uv( self, frame: integer ): number, number, number, number ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Sheet` | | | `frame` | `integer` | One to `count`. Anything else raises. | ##### Returns | Type | Description | | --- | --- | | `number` | The region's left, top, right and bottom edges. | | `number` | | | `number` | | | `number` | | ### tecs.gfx.animation.Slice record A named region that moves across the frames. ```teal record tecs.gfx.animation.Slice name: string data: string keys: {SliceKey} end ``` #### tecs.gfx.animation.Slice.name field Caller-writable. Sets the name used to find the slice. ```teal tecs.gfx.animation.Slice.name: string ``` #### tecs.gfx.animation.Slice.data field Caller-writable. Stores free text carried by Aseprite. ```teal tecs.gfx.animation.Slice.data: string ``` #### tecs.gfx.animation.Slice.keys field Caller-writable. Sets at least one key in frame order. ```teal tecs.gfx.animation.Slice.keys: {SliceKey} ``` ### tecs.gfx.animation.SliceKey record Defines where a slice sits from one frame onward. ```teal record tecs.gfx.animation.SliceKey frame: integer x: number y: number w: number h: number centerX: number centerY: number centerW: number centerH: number pivotX: number pivotY: number end ``` #### tecs.gfx.animation.SliceKey.frame field Caller-writable. Sets the first frame on which this key takes effect, counting from one. ```teal tecs.gfx.animation.SliceKey.frame: integer ``` #### tecs.gfx.animation.SliceKey.x field Caller-writable. Sets the slice rectangle's left edge in frame pixels. ```teal tecs.gfx.animation.SliceKey.x: number ``` #### tecs.gfx.animation.SliceKey.y field Caller-writable. Sets the slice rectangle's top edge in frame pixels. ```teal tecs.gfx.animation.SliceKey.y: number ``` #### tecs.gfx.animation.SliceKey.w field Caller-writable. Sets the slice rectangle's width in pixels. ```teal tecs.gfx.animation.SliceKey.w: number ``` #### tecs.gfx.animation.SliceKey.h field Caller-writable. Sets the slice rectangle's height in pixels. ```teal tecs.gfx.animation.SliceKey.h: number ``` #### tecs.gfx.animation.SliceKey.centerX field Caller-writable. Sets the nine-slice center's left edge in slice pixels. Nil means the slice has no center. ```teal tecs.gfx.animation.SliceKey.centerX: number ``` #### tecs.gfx.animation.SliceKey.centerY field Caller-writable. Sets the nine-slice center's top edge in slice pixels. ```teal tecs.gfx.animation.SliceKey.centerY: number ``` #### tecs.gfx.animation.SliceKey.centerW field Caller-writable. Sets the nine-slice center's width in pixels. ```teal tecs.gfx.animation.SliceKey.centerW: number ``` #### tecs.gfx.animation.SliceKey.centerH field Caller-writable. Sets the nine-slice center's height in pixels. ```teal tecs.gfx.animation.SliceKey.centerH: number ``` #### tecs.gfx.animation.SliceKey.pivotX field Caller-writable. Sets the horizontal pivot in slice pixels. Nil means the slice has no pivot. ```teal tecs.gfx.animation.SliceKey.pivotX: number ``` #### tecs.gfx.animation.SliceKey.pivotY field Caller-writable. Sets the vertical pivot in slice pixels. Nil means the slice has no pivot. ```teal tecs.gfx.animation.SliceKey.pivotY: number ``` ### tecs.gfx.animation.Tag record A named span of frames, as the constructors take it. ```teal record tecs.gfx.animation.Tag from: integer to: integer direction: Direction end ``` #### tecs.gfx.animation.Tag.from field Caller-writable. Sets the first frame index in the inclusive span. ```teal tecs.gfx.animation.Tag.from: integer ``` #### tecs.gfx.animation.Tag.to field Caller-writable. Sets the last frame index in the inclusive span. ```teal tecs.gfx.animation.Tag.to: integer ``` #### tecs.gfx.animation.Tag.direction field Caller-writable. Sets the playback direction and defaults to `"forward"`. ```teal tecs.gfx.animation.Tag.direction: Direction ``` ## Functions ### tecs.gfx.animation.build Static Creates a builder for a sheet that the other constructors cannot describe. The model is what the builder writes, so an atlas from any tool reaches the same sheet an Aseprite export does. Frames, tags and slices are added in any order. `build` registers the finished sheet. ```teal function tecs.gfx.animation.build( name: string, imageWidth: number, imageHeight: number ): Builder ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `name` | `string` | Name to register the finished sheet under. | | `imageWidth` | `number` | Size of the image the frames are cut from, in pixels. | | `imageHeight` | `number` | | #### Returns | Type | Description | | --- | --- | | [`Builder`](/modules/gfx/animation/#tecs.gfx.animation.Builder) | A builder whose methods chain. | ### tecs.gfx.animation.byId Static Returns the sheet represented by a registration index, or nil. ```teal function tecs.gfx.animation.byId(id: integer): Sheet ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `id` | `integer` | An `id` from a sheet this process built. Construction assigns ids in order, so each id is meaningful only within one run. | #### Returns | Type | Description | | --- | --- | | [`Sheet`](/modules/gfx/animation/#tecs.gfx.animation.Sheet) | The sheet, or nil for an id nothing was built under. | ### tecs.gfx.animation.byName Static Returns the sheet registered under a name, or nil. Building a second sheet under a name already taken replaces what this returns, so a reload points new entities at the new sheet. Entities already carrying the old id keep drawing the old one, which is what stops a reload from pulling a frame out from under them. ```teal function tecs.gfx.animation.byName(name: string): Sheet ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `name` | `string` | The name a sheet was built under. | #### Returns | Type | Description | | --- | --- | | [`Sheet`](/modules/gfx/animation/#tecs.gfx.animation.Sheet) | The sheet most recently registered under that name, or nil. | ### tecs.gfx.animation.findSheetById Static Returns the sheet represented by a process-wide id. ```teal function tecs.gfx.animation.findSheetById(id: integer): sheet.Sheet ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `id` | `integer` | An id a constructor handed out. | #### Returns | Type | Description | | --- | --- | | [`sheet.Sheet`](/modules/gfx/animation/#tecs.gfx.animation.Sheet) | The sheet, or nil when the id names none. | ### tecs.gfx.animation.findSheetByName Static Returns the sheet registered under a name. ```teal function tecs.gfx.animation.findSheetByName(name: string): sheet.Sheet ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `name` | `string` | The name a constructor registered. | #### Returns | Type | Description | | --- | --- | | [`sheet.Sheet`](/modules/gfx/animation/#tecs.gfx.animation.Sheet) | The sheet, or nil when the name names none. | ### tecs.gfx.animation.frameOf Static Returns the sheet frame shown by an entity's animation. The same answer the vertex shader draws, because both are `frameAt` over the same tag: the shader reads a table built from it and this calls it. What a hitbox on frame five, a footstep on frame three or a muzzle on an animated hand asks for. ```teal function tecs.gfx.animation.frameOf( world: World, entity: integer ): integer ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`World`](/modules/ecs/#tecs.World) | The world the entity lives in. | | `entity` | `integer` | A live entity. | #### Returns | Type | Description | | --- | --- | | `integer` | A frame index into the whole sheet, counting from one, or zero for an entity with nothing to play. | ### tecs.gfx.animation.fromAseprite Static Creates a sheet from an Aseprite JSON export. One reader in front of the model rather than a second model: frames, their durations, frame tags with their directions, and slices with their keys all land in the sheet the builder writes. The reader accepts Aseprite's array layout and its object layout, sorting the latter by frame name. It ignores `spriteSourceSize`, so export with trimming off. ```teal function tecs.gfx.animation.fromAseprite( options: AsepriteOptions ): Sheet ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `options` | [`AsepriteOptions`](/modules/gfx/animation/#tecs.gfx.animation.AsepriteOptions) | The export and the name to register it under. | #### Returns | Type | Description | | --- | --- | | [`Sheet`](/modules/gfx/animation/#tecs.gfx.animation.Sheet) | The finished sheet. | ### tecs.gfx.animation.grid Static Creates a sheet whose frames form a uniform grid. Margin surrounds the grid and spacing separates the cells, so a cell's left edge is `margin + column * (frameWidth + spacing)`. Both default to zero, which is an image cut with nothing between its cells. ```teal function tecs.gfx.animation.grid(options: GridOptions): Sheet ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `options` | [`GridOptions`](/modules/gfx/animation/#tecs.gfx.animation.GridOptions) | Raises on a missing name, a non-positive image or frame size, a grid that fits no cells, or a `count` past what the grid holds. Frames come out in row-major order. | #### Returns | Type | Description | | --- | --- | | [`Sheet`](/modules/gfx/animation/#tecs.gfx.animation.Sheet) | The finished sheet, already registered under its name and carrying an `id`. | ### tecs.gfx.animation.of Static Creates an [`Animation`](/modules/gfx/animation/#tecs.gfx.animation.Animation) that plays a named sheet tag and is ready to spawn. Omit the tag to play the whole sheet in order. Fails on a tag the sheet does not carry, since the alternative is a typo that plays every frame. The entity also needs a [`Sprite`](/modules/gfx/#tecs.gfx.Sprite), which the query matches and playback writes what is playing into. An [`Animation`](/modules/gfx/animation/#tecs.gfx.animation.Animation) on its own draws nothing. ```teal function tecs.gfx.animation.of( source: Sheet, tag: string, options: PlayOptions ): Animation ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `source` | [`Sheet`](/modules/gfx/animation/#tecs.gfx.animation.Sheet) | The sheet to play. Nil raises. | | `tag` | `string` | A tag the sheet names, or nil for the whole sheet. A name the sheet does not carry raises here rather than reporting and playing the whole sheet, because an author naming a tag at this call has one in mind and the sheet is already in hand to check against. | | `options` | [`PlayOptions`](/modules/gfx/animation/#tecs.gfx.animation.PlayOptions) | Defaults are the sheet's own timing, looping, and playing. | #### Returns | Type | Description | | --- | --- | | [`Animation`](/modules/gfx/animation/#tecs.gfx.animation.Animation) | A component value, not an entity: pass it to `world:spawn` or `world:set` yourself. | ### tecs.gfx.animation.play Static Points a live entity at a tag and restarts it there. Restarting is the point: time and frame both reset, so the next step writes the tag's first frame whatever the entity was showing. An entity carrying no Animation gets one. ```teal function tecs.gfx.animation.play( world: World, entity: integer, source: Sheet, tag: string, options: PlayOptions ) ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`World`](/modules/ecs/#tecs.World) | The world the entity lives in. | | `entity` | `integer` | A live entity with a [`Sprite`](/modules/gfx/#tecs.gfx.Sprite). | | `source` | [`Sheet`](/modules/gfx/animation/#tecs.gfx.animation.Sheet) | The sheet to play. Nil raises. | | `tag` | `string` | A tag the sheet names, or nil for the whole sheet. A name the sheet does not carry raises, as it does in `of`. | | `options` | [`PlayOptions`](/modules/gfx/animation/#tecs.gfx.animation.PlayOptions) | Defaults are the sheet's own timing, looping, and playing. | #### Returns None. ### tecs.gfx.animation.plugin Static Adds the systems that drive playback. Call this once for each world that plays sprite sheets. A second call on the same world does nothing. It installs three systems, all in `PostUpdate` so they land before extraction whatever order a game added its plugins in. `tecs.EncodeAnimation` writes what each entity is playing into its [`Sprite`](/modules/gfx/#tecs.gfx.Sprite) and writes nothing on a step where nothing changed what is playing. `tecs.ReportAnimation` derives [`Completed`](/modules/gfx/animation/#tecs.gfx.animation.Completed) and [`Looped`](/modules/gfx/animation/#tecs.gfx.animation.Looped) for entities carrying [`AnimationEvents`](/modules/gfx/animation/#tecs.gfx.animation.AnimationEvents). `tecs.RebaseAnimation` moves the playback clock's origin every few hours of uptime and re-anchors every animated row on it, which is the one update in a hundred thousand that writes every playing animation. ```teal function tecs.gfx.animation.plugin(world: World) ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`World`](/modules/ecs/#tecs.World) | The world to add the systems to. | #### Returns None. ### tecs.gfx.animation.rects Static Creates a sheet from explicitly listed frame rectangles. For an image no grid describes: frames of differing sizes, or an atlas whose cells a packing tool placed. ```teal function tecs.gfx.animation.rects(options: RectsOptions): Sheet ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `options` | [`RectsOptions`](/modules/gfx/animation/#tecs.gfx.animation.RectsOptions) | Raises on a missing name, a non-positive image size, an empty frame list, or a frame with no positive size. Rects are not checked against the image, so one that runs off the edge samples whatever the layer holds there. | #### Returns | Type | Description | | --- | --- | | [`Sheet`](/modules/gfx/animation/#tecs.gfx.animation.Sheet) | The finished sheet, already registered under its name and carrying an `id`. | ### tecs.gfx.animation.replace Static Folds a re-exported sheet into the one already registered under its name, in place, so an entity playing the old id shows the new frames. ```teal function tecs.gfx.animation.replace( built: sheet.Sheet ): sheet.Sheet, string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `built` | [`sheet.Sheet`](/modules/gfx/animation/#tecs.gfx.animation.Sheet) | A sheet from any constructor here, registered moments ago under a name something else already holds. | #### Returns | Type | Description | | --- | --- | | [`sheet.Sheet`](/modules/gfx/animation/#tecs.gfx.animation.Sheet) | Returns the live sheet and nil, or nil and the refusal reason. | | `string` | | ### tecs.gfx.animation.restart Static Plays an entity's animation again from the start of its tag. The sheet, tag, speed and loop flag are left as they are; what resets is where in the cycle playback has got to and whether it is running. For replaying a one-shot that has finished, and for rewinding one that has not. ```teal function tecs.gfx.animation.restart( world: World, entity: integer ): boolean ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`World`](/modules/ecs/#tecs.World) | The world the entity lives in. | | `entity` | `integer` | A live entity. | #### Returns | Type | Description | | --- | --- | | `boolean` | Whether there was an Animation to restart. False leaves the entity untouched, since there is nothing to say what it would play. | ### tecs.gfx.animation.revision Static Returns how many times any sheet's frames have changed. Bumped by registration and by `bind`, both of which move where a frame's region points. Anything holding a copy of those regions compares this against what it copied from rather than being told, which keeps the dependency running one way: a sheet knows nothing about who read it. ```teal function tecs.gfx.animation.revision(): integer ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | `integer` | A number that only ever increases. | ### tecs.gfx.animation.sheetRevision Static Returns how many times any sheet's frames have changed. What a cache of anything derived from a sheet compares against, so a sheet replaced or rebound under a running game invalidates it. ```teal function tecs.gfx.animation.sheetRevision(): integer ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | `integer` | A number that only ever increases. | ### tecs.gfx.animation.timeOf Static Returns an animation's position in its tag cycle, in seconds. Recomputed on the call rather than kept in a column, because keeping it means writing every animating entity on every step and that is the cost resolving the frame in the shader exists to remove. A few hundred calls a step is free; it stops being free somewhere in the tens of thousands, which is a game asking a question this is the wrong shape for. ```teal function tecs.gfx.animation.timeOf( world: World, entity: integer ): number ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`World`](/modules/ecs/#tecs.World) | The world the entity lives in. | | `entity` | `integer` | A live entity. | #### Returns | Type | Description | | --- | --- | | `number` | Seconds into the cycle, wrapped for a looping tag and clamped at the end for a one-shot. Zero for an entity carrying no Animation and for one whose sheet this run does not have. | ## Values ### tecs.gfx.animation.DEFAULT_DURATION variable Read-only. Reports how many milliseconds a frame remains visible when nothing overrides its duration. ```teal tecs.gfx.animation.DEFAULT_DURATION: number ``` --- ## tecs.gfx # tecs.gfx Entity components that describe a rendered scene. [`Transform2D`](/modules/ecs/#tecs.ecs.Transform2D) places an entity, graphics components describe its appearance, and [`Renderable2D`](/modules/gfx/#tecs.gfx.Renderable2D) admits it to rendering. [`Transform2D`](/modules/ecs/#tecs.ecs.Transform2D), [`Tint`](/modules/gfx/#tecs.gfx.Tint), and [`Renderable2D`](/modules/gfx/#tecs.gfx.Renderable2D) form the minimum drawable set: ```teal local player = world:spawn( tecs.Transform2D(120, 80, 0, 1, 0, 32, 32), app.renderer.sprites:sprite("player.png"), tecs.gfx.Tint(1.0, 1.0, 1.0, 1.0), tecs.gfx.Renderable2D() ) local tint = world:getMut(player, tecs.gfx.Tint) tint.a = 0.5 ``` [`Sprite`](/modules/gfx/#tecs.gfx.Sprite) selects an image region. [`Material`](/modules/gfx/#tecs.gfx.Material) selects fragment coverage and lighting. [`Clip`](/modules/gfx/#tecs.gfx.Clip) selects a target-pixel clip region. [`PointLight2D`](/modules/gfx/#tecs.gfx.PointLight2D), [`Occluder2D`](/modules/gfx/#tecs.gfx.Occluder2D), and [`DropShadow2D`](/modules/gfx/#tecs.gfx.DropShadow2D) describe deferred lighting and shadows. The 3D mesh contract is separate from the sprite lane. `tecs.Transform3D`, [`Mesh`](/modules/gfx/#tecs.gfx.Mesh), [`Bounds3D`](/modules/gfx/#tecs.gfx.Bounds3D), [`MeshMaterial`](/modules/gfx/#tecs.gfx.MeshMaterial), optional [`MeshSkin`](/modules/gfx/#tecs.gfx.MeshSkin), optional [`MeshMorph`](/modules/gfx/#tecs.gfx.MeshMorph), [`Tint`](/modules/gfx/#tecs.gfx.Tint), and [`Renderable3D`](/modules/gfx/#tecs.gfx.Renderable3D) describe an opaque mesh for the optional mesh domain. [`PointLight3D`](/modules/gfx/#tecs.gfx.PointLight3D) and [`SpotLight3D`](/modules/gfx/#tecs.gfx.SpotLight3D) enter the optional tiled local-light lane. Their pose comes from `tecs.Transform3D`. [`Material`](/modules/gfx/#tecs.gfx.Material) remains the compiled 2D shader identity; mesh material data has its own persisted name and resident slot. [`Transform2D`](/modules/ecs/#tecs.ecs.Transform2D) remains at `tecs.Transform2D` because physics, hierarchy, sequencing, and graphics share it. Other render components live under `tecs.gfx`. Use `world:getMut` for authored changes. A direct cdata write through `world:get` must call `world:markComponentDirty`, or rendering keeps the old value. `batchSpawn` skips FFI defaults, so its callback must initialize every field. [`PreviousTransform2D`](/modules/gfx/#tecs.gfx.PreviousTransform2D) stores the pose before the current fixed step. The renderer interpolates it toward [`Transform2D`](/modules/ecs/#tecs.ecs.Transform2D) for presentation without changing simulation state. A camera maps world coordinates to one viewport. The camera stores the view center in world units, zoom, and rotation. Callers write these fields directly: ```teal local camera = app.renderer.sprites.camera camera.x = player.x camera.y = player.y camera.zoom = 2 camera.rotation = 0 ``` Position names the center rather than a corner. The default view starts at the world origin. World Y and screen Y both increase downward. ## Coordinate conversion Each conversion takes the viewport dimensions because one camera can serve targets of different sizes: ```teal local worldX , worldY = camera:toWorld( mouseX, mouseY, width, height ) local screenX , screenY = camera:toScreen( worldX, worldY, width, height ) ``` Use the same dimensions for conversion and rendering. `toWorld`, `toScreen`, and `matrix` then share one mapping, and points round-trip. `viewBounds` returns a conservative axis-aligned world rectangle when the camera rotates. `matrix` reuses the camera's sixteen-float array, and `viewBounds` reuses its four-element table. Copy either result before retaining it across another call. A perspective camera maps a right-handed 3D world to one viewport. World +X points right, +Y points up, and the default camera looks along -Z. The camera orientation is a quaternion in `(x, y, z, w)` order that turns camera-local coordinates into world coordinates. Position and orientation are caller-writable fields: ```teal local camera = tecs.gfx.newCamera3D({ x = 0, y = 2, z = 6, verticalFov = math.rad(60), }) camera.rotationY = math.sin(math.rad(15) * 0.5) camera.rotationW = math.cos(math.rad(15) * 0.5) ``` `matrix` writes a column-major world-to-clip matrix with depth in `[0, 1]`, and `inverseMatrix` writes its clip-to-world inverse. `matrices` writes both from one camera calculation for code that needs the pair. Each result uses a separate camera-owned sixteen-float array and remains valid until that same result is written again. A view draws one or both rendering domains into a rectangle of the frame. Views are entities. Spawn a `View` component to replace the renderer's synthesized full-frame view, then assign a 2D camera, a 3D camera, or both: ```teal world:spawn(tecs.gfx.View.new({ camera3D = tecs.gfx.newCamera3D({x = 0, y = 3, z = 8}), x = 0, y = 0, width = 0.5, height = 1, order = 0, })) ``` Viewport coordinates are fractions of the frame. Views draw in ascending `order`; equal orders use entity id. A view with both cameras composes meshes then sprites through the normal shared renderer order. A view with only a 2D camera is the direct way to place a full-frame UI above an earlier 3D view. The renderer must be created with `maxViews` large enough for the explicit views. Omitting `maxViews` preserves the original single-view resources and pass sequence. Coordinates rendering domains and owns frame-wide GPU work. An application owns one renderer. The renderer owns the deferred graph, presentation targets, capture, and staging-slot rotation. `sprites` is the 2D domain and `meshes` is the 3D domain. Each owns its own extraction, residency, instance buffers, and backend. A domain disabled at creation is neither loaded nor allocated. This division is the 3D extension seam. A mesh domain can be prepared beside the sprite domain and contribute its own pass bodies without either domain branching per entity on what kind of renderer it belongs to. The public domain values expose cameras, residency operations, extension points, and statistics. Renderer-only lifecycle methods and the extractor, packet, and backend handles stay behind separately typed internal references. When both domains contribute transparent work, meshes draw first and sprites draw second. Their camera spaces have no universal cross-domain depth order, so the fixed order makes sprites deterministic overlays while each domain retains its own back-to-front sort. Setting `maxViews` enables ordered [`View`](/modules/gfx/#tecs.gfx.View) entities. The renderer extracts and uploads each enabled domain once, then reculls, shades, and immediately composites each view through shared GPU work buffers. Omitting `maxViews` keeps the original full-frame path and allocates no multi-camera intermediate. Shaped text as entities in the rendered world. A `Text` names a font and string. `Transform2D` places its top-left corner, `Tint` colors every glyph, and `Clip` clips it like any other drawable. ```teal world:addPlugin(tecs.gfx.textPlugin({ renderer = app.renderer, })) world:spawn( tecs.Transform2D(24, 24), tecs.gfx.Tint(0.92, 0.96, 1.0, 1.0), tecs.gfx.Text.new({ text = "tecs\n1200 entities", font = tecs.gfx.newTTF({ source = "fonts/JetBrainsMono-ExtraBold.ttf", }), size = 28, align = "center", }) ) ``` Write text fields through `world:getMut`. A direct `world:get` write leaves the column clean and the displayed glyphs unchanged. ## Fonts and layout `newTTF` reads source font bytes off the main thread and returns the opened font. It transparently suspends when called by a system. SDL_ttf and HarfBuzz shape and lay out UTF-8. Tecs asks SDL_ttf for glyph images lazily, caches each glyph once per renderer, and keeps its own one-instance-per-glyph producer. The default `"sdf"` raster scales without regenerating glyphs. Choose a loaded size near the largest ordinary on-screen size. A fixed-size UI may instead load an `"alpha"` raster at exactly its displayed size. Alpha glyphs retain the font rasterizer's small-size fitting and automatically snap their origin on a screen-space layer when they are unrotated and unscaled. Text supports explicit newlines and left, center, or right alignment. It does not wrap to a width, anchor outside the top-left corner, or style individual glyphs. The `glyph` material draws text unlit through the forward-blended lane so its distance-field edge retains partial coverage. ## Module contents ### Submodules | Submodule | Description | | --- | --- | | [`tecs.gfx.animation`](/modules/gfx/animation/) | Sprite sheets, fixed-step playback, Aseprite slices, pivots, and reloads | | [`tecs.gfx.layers`](/modules/gfx/layers/) | Layer bands, sorting, coordinate spaces, parallax, lighting, and clipping | | [`tecs.gfx.materials`](/modules/gfx/materials/) | Material selection, shader authoring, built-in materials, and reload rules | | [`tecs.gfx.particles`](/modules/gfx/particles/) | GPU particle effects, emitter playback, pool sizing, and rendering limits | ### Constructors | Constructor | Description | | --- | --- | | [`newTTF`](/modules/gfx/#tecs.gfx.newTTF) | Loads and opens a source font with SDL_ttf. | ### Types | Type | Kind | Description | | --- | --- | --- | | [`Bounds3D`](/modules/gfx/#tecs.gfx.Bounds3D) | record | Defines a mesh's local-space bounding sphere. | | [`Camera2D`](/modules/gfx/#tecs.gfx.Camera2D) | record | Represents a view onto the world. | | [`Camera3D`](/modules/gfx/#tecs.gfx.Camera3D) | record | Represents one perspective view into a right-handed 3D world. | | [`Clip`](/modules/gfx/#tecs.gfx.Clip) | record | Restricts a renderable's fragments to a clip region. | | [`DropShadow2D`](/modules/gfx/#tecs.gfx.DropShadow2D) | record | Casts a stretched copy of the entity along the ground, away from light. | | [`Font`](/modules/gfx/#tecs.gfx.Font) | record | Represents a loaded font named by Text. | | [`FontRaster`](/modules/gfx/#tecs.gfx.FontRaster) | enum | Selects a font's glyph raster representation. | | [`Material`](/modules/gfx/#tecs.gfx.Material) | record | Selects the material that shades renderable geometry. | | [`Mesh`](/modules/gfx/#tecs.gfx.Mesh) | record | Selects immutable geometry for the 3D mesh domain. | | [`MeshMaterial`](/modules/gfx/#tecs.gfx.MeshMaterial) | record | Selects resident PBR data for the 3D mesh domain. | | [`MeshMorph`](/modules/gfx/#tecs.gfx.MeshMorph) | record | Selects one resident morph-weight vector for GPU mesh deformation. | | [`MeshSkin`](/modules/gfx/#tecs.gfx.MeshSkin) | record | Selects one resident joint palette for GPU mesh skinning. | | [`ModelOwner`](/modules/gfx/#tecs.gfx.ModelOwner) | interface | Read-only. Contains the source asset path. | | [`Occluder2D`](/modules/gfx/#tecs.gfx.Occluder2D) | record | Blocks light from reaching what lies behind the entity. | | [`PointLight2D`](/modules/gfx/#tecs.gfx.PointLight2D) | record | Represents a light resolved by the deferred lighting pass. | | [`PointLight3D`](/modules/gfx/#tecs.gfx.PointLight3D) | record | Represents an omnidirectional light in the 3D mesh domain. | | [`PreviousTransform2D`](/modules/gfx/#tecs.gfx.PreviousTransform2D) | record | Stores the transform as it stood before the current fixed step. | | [`Renderable2D`](/modules/gfx/#tecs.gfx.Renderable2D) | record | Marks an entity as contributing geometry. | | [`Renderable3D`](/modules/gfx/#tecs.gfx.Renderable3D) | record | Marks an entity as contributing geometry to the 3D mesh domain. | | [`Renderer`](/modules/gfx/#tecs.gfx.Renderer) | record | Something that draws instances without owning entities. | | [`SpotLight3D`](/modules/gfx/#tecs.gfx.SpotLight3D) | record | Represents a conical light aimed by its entity's Transform3D rotation. | | [`Sprite`](/modules/gfx/#tecs.gfx.Sprite) | record | Samples a texture instead of drawing flat color. | | [`Text`](/modules/gfx/#tecs.gfx.Text) | record | Lays a string out into glyph instances. | | [`TextOptions`](/modules/gfx/#tecs.gfx.TextOptions) | record | Configures textPlugin. | | [`Tint`](/modules/gfx/#tecs.gfx.Tint) | record | Controls base color and how much of the background remains visible. | | [`TTFOptions`](/modules/gfx/#tecs.gfx.TTFOptions) | record | Configures newTTF. | | [`View`](/modules/gfx/#tecs.gfx.View) | record | Describes one ordered viewport and the domain cameras drawn through it. | ### Functions | Function | Kind | Description | | --- | --- | --- | | [`glyphAt`](/modules/gfx/#tecs.gfx.glyphAt) | Static | Returns a glyph's world x, y, width, and height. | | [`imageId`](/modules/gfx/#tecs.gfx.imageId) | Static | Returns the index of an image name and assigns one on first use. | | [`imageName`](/modules/gfx/#tecs.gfx.imageName) | Static | Returns the name represented by an image index. | | [`measureIntrinsic`](/modules/gfx/#tecs.gfx.measureIntrinsic) | Static | Returns the preferred and minimum-content metrics for a text item. | | [`measureText`](/modules/gfx/#tecs.gfx.measureText) | Static | Returns a text item's width and height in world units. | | [`meshId`](/modules/gfx/#tecs.gfx.meshId) | Static | Returns the process-local index of a normalized mesh asset name. | | [`meshMaterialId`](/modules/gfx/#tecs.gfx.meshMaterialId) | Static | Returns the process-local identity of a mesh material name. | | [`meshMaterialName`](/modules/gfx/#tecs.gfx.meshMaterialName) | Static | Returns the name represented by a mesh material index. | | [`meshMorphId`](/modules/gfx/#tecs.gfx.meshMorphId) | Static | Returns the process-local identity of a normalized mesh-morph name. | | [`meshMorphName`](/modules/gfx/#tecs.gfx.meshMorphName) | Static | Returns the normalized name represented by a mesh-morph index. | | [`meshName`](/modules/gfx/#tecs.gfx.meshName) | Static | Returns the normalized asset name represented by a mesh index. | | [`meshSkinId`](/modules/gfx/#tecs.gfx.meshSkinId) | Static | Returns the process-local identity of a normalized mesh-skin name. | | [`meshSkinName`](/modules/gfx/#tecs.gfx.meshSkinName) | Static | Returns the normalized name represented by a mesh-skin index. | | [`textLayouts`](/modules/gfx/#tecs.gfx.textLayouts) | Static | Returns how many texts the world has laid out. | | [`textPlugin`](/modules/gfx/#tecs.gfx.textPlugin) | Static | Creates the plugin that lays out text for a renderer. | ### Values | Value | Type | Description | | --- | --- | --- | | [`LIGHT_CASTS_SHADOWS`](/modules/gfx/#tecs.gfx.LIGHT_CASTS_SHADOWS) | `integer` | Read-only. Marks a 3D point or spot light for the optional local-shadow atlas when included in its flags field. | ## Constructors ### tecs.gfx.newTTF Static Loads and opens a source font with SDL_ttf. File acquisition follows the cooperative asset path. The call suspends its system until SDL_ttf has opened the bytes, selected the raster mode, and verified the requested point size. ```teal function tecs.gfx.newTTF(options: TTFOptions): Font ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `options` | [`TTFOptions`](/modules/gfx/#tecs.gfx.TTFOptions) | The caller must set `source`. `name` defaults to that path, `size` defaults to 48 points, and `raster` defaults to `"sdf"`. | #### Returns | Type | Description | | --- | --- | | [`Font`](/modules/gfx/#tecs.gfx.Font) | Returns an immutable [`Font`](/modules/gfx/#tecs.gfx.Font). | ## Types ### tecs.gfx.Bounds3D record Defines a mesh's local-space bounding sphere. A sphere stays four floats in extraction and transforms conservatively under non-uniform scale by multiplying `radius` by the largest absolute scale axis. Asset loading may supply these values, and callers may override them for generated or animated geometry. Read-only. Exposes a local-space mesh bounding sphere. The center uses mesh-local coordinates and `radius` defaults to one world unit. ```teal record tecs.gfx.Bounds3D is Component centerX: number centerY: number centerZ: number radius: number end ``` #### Interfaces | Interface | | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | #### tecs.gfx.Bounds3D.centerX field Caller-writable. Sets the local-space sphere-center x coordinate. ```teal tecs.gfx.Bounds3D.centerX: number ``` #### tecs.gfx.Bounds3D.centerY field Caller-writable. Sets the local-space sphere-center y coordinate. ```teal tecs.gfx.Bounds3D.centerY: number ``` #### tecs.gfx.Bounds3D.centerZ field Caller-writable. Sets the local-space sphere-center z coordinate. ```teal tecs.gfx.Bounds3D.centerZ: number ``` #### tecs.gfx.Bounds3D.radius field Caller-writable. Sets the non-negative local-space sphere radius. ```teal tecs.gfx.Bounds3D.radius: number ``` ### tecs.gfx.Camera2D record Represents a view onto the world. Assign every field directly, frame by frame. A renderer copies the values it draws from during extraction, so moving a camera afterwards affects the next frame, not the one in flight. ```teal global record tecs.gfx.Camera2D record Options x: number y: number zoom: number rotation: number end x: number y: number zoom: number rotation: number newCamera2D: function(options: Camera2DOptions): Camera2D matrix: function(self, width: number, height: number): loader.CArray toScreen: function( self, worldX: number, worldY: number, width: number, height: number ): number, number toWorld: function( self, screenX: number, screenY: number, width: number, height: number ): number, number viewBounds: function(self, width: number, height: number): {number} end ``` #### tecs.gfx.Camera2D.Options record Names the options accepted by `newCamera2D` so a game can annotate the table it passes without reaching into this file. ```teal record tecs.gfx.Camera2D.Options x: number y: number zoom: number rotation: number end ``` ##### tecs.gfx.Camera2D.Options.x field Caller-writable. Sets the center of the view in world units. Both values default to zero. ```teal tecs.gfx.Camera2D.Options.x: number ``` ##### tecs.gfx.Camera2D.Options.y field Caller-writable. Sets the center of the view in world units. Both values default to zero. ```teal tecs.gfx.Camera2D.Options.y: number ``` ##### tecs.gfx.Camera2D.Options.zoom field Caller-writable. Sets the zoom and defaults to one. Zero or less divides through in every method here and is not rejected. ```teal tecs.gfx.Camera2D.Options.zoom: number ``` ##### tecs.gfx.Camera2D.Options.rotation field Caller-writable. Sets the rotation in radians and defaults to zero. ```teal tecs.gfx.Camera2D.Options.rotation: number ``` #### tecs.gfx.Camera2D.x field Caller-writable. Sets the horizontal center of the view in world units. ```teal tecs.gfx.Camera2D.x: number ``` #### tecs.gfx.Camera2D.y field Caller-writable. Sets the vertical center of the view in world units. Increasing y moves the view towards the bottom of the world. ```teal tecs.gfx.Camera2D.y: number ``` #### tecs.gfx.Camera2D.zoom field Caller-writable. Sets the zoom. Values above one magnify about the center without moving the point under the middle of the window. ```teal tecs.gfx.Camera2D.zoom: number ``` #### tecs.gfx.Camera2D.rotation field Caller-writable. Sets the rotation in radians. Positive values turn the scene counter-clockwise on screen. ```teal tecs.gfx.Camera2D.rotation: number ``` #### tecs.gfx.Camera2D.newCamera2D Static Creates a camera. Everything defaults to an unrotated, unzoomed view at the world origin, which a renderer then recenters if the game never moves it. ```teal function tecs.gfx.Camera2D.newCamera2D( options: Camera2DOptions ): Camera2D ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `options` | [`Camera2DOptions`](/modules/gfx/#tecs.gfx.Camera2D.Options) | Omit for the default view. The constructor stores every value without validation. | ##### Returns | Type | Description | | --- | --- | | [`Camera2D`](/modules/gfx/#tecs.gfx.Camera2D) | Returns a camera whose fields the caller assigns directly. | #### tecs.gfx.Camera2D:matrix Instance Writes the world-to-clip matrix for a viewport of `width` by `height`. Column major, because that is how a GLSL `mat4` reads a uniform: the first four floats are the first column, not the first row. Transposing these is a mistake that renders something plausible rather than nothing, which is how it survives review. ```teal function tecs.gfx.Camera2D.matrix( self, width: number, height: number ): loader.CArray ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Camera2D` | | | `width` | `number` | Viewport width in pixels. | | `height` | `number` | Viewport height in pixels. | ##### Returns | Type | Description | | --- | --- | | `loader.CArray` | The camera's own sixteen-float array, rewritten in place. It is valid until the next call on this camera, so a caller that needs to keep it copies it rather than holding the pointer. | #### tecs.gfx.Camera2D:toScreen Instance Converts a world point to screen space. Exactly the inverse of `toWorld` at the same width and height, and the same mapping the matrix applies, so a point round-trips. ```teal function tecs.gfx.Camera2D.toScreen( self, worldX: number, worldY: number, width: number, height: number ): number, number ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Camera2D` | | | `worldX` | `number` | World x. | | `worldY` | `number` | World y, running down. | | `width` | `number` | Viewport width in pixels, the same one the matrix was built with. | | `height` | `number` | Viewport height in pixels, the same one the matrix was built with. | ##### Returns | Type | Description | | --- | --- | | `number` | Returns screen x from the left and screen y from the top, in pixels. The function does not clamp either value to the viewport. | | `number` | | #### tecs.gfx.Camera2D:toWorld Instance Converts a screen point to world space. The inverse of what the matrix does, written out rather than inverted, so the Y flip appears once here in the same place it appears above. ```teal function tecs.gfx.Camera2D.toWorld( self, screenX: number, screenY: number, width: number, height: number ): number, number ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Camera2D` | | | `screenX` | `number` | Pixels from the left of the viewport. | | `screenY` | `number` | Pixels from the top of the viewport, running down. | | `width` | `number` | Viewport width in pixels, the same one the matrix was built with. | | `height` | `number` | Viewport height in pixels, the same one the matrix was built with. | ##### Returns | Type | Description | | --- | --- | | `number` | The world x, then the world y. Points outside the viewport convert too, and land outside the view rectangle. | | `number` | | #### tecs.gfx.Camera2D:viewBounds Instance Returns the world-space rectangle this camera can see as minX, minY, maxX, and maxY. When rotated, an axis-aligned box encloses the view's corners, so culling keeps a little more than it must. Keeping too much costs a few instances; keeping too little drops geometry that should have drawn, which is why the error goes this way. ```teal function tecs.gfx.Camera2D.viewBounds( self, width: number, height: number ): {number} ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Camera2D` | | | `width` | `number` | Viewport width in pixels. | | `height` | `number` | Viewport height in pixels. | ##### Returns | Type | Description | | --- | --- | | `{number}` | The camera's own four-element table, rewritten in place. It is valid until the next call on this camera, so a caller that needs to keep it copies it rather than holding the table. | ### tecs.gfx.Camera3D record Represents one perspective view into a right-handed 3D world. ```teal global record tecs.gfx.Camera3D record Options x: number y: number z: number rotationX: number rotationY: number rotationZ: number rotationW: number verticalFov: number near: number far: number end x: number y: number z: number rotationX: number rotationY: number rotationZ: number rotationW: number verticalFov: number near: number far: number newCamera3D: function(options: Camera3DOptions): Camera3D inverseMatrix: function( self, width: number, height: number ): loader.CArray matrices: function( self, width: number, height: number ): loader.CArray, loader.CArray matrix: function(self, width: number, height: number): loader.CArray end ``` #### tecs.gfx.Camera3D.Options record Names the options accepted by `newCamera3D`. ```teal record tecs.gfx.Camera3D.Options x: number y: number z: number rotationX: number rotationY: number rotationZ: number rotationW: number verticalFov: number near: number far: number end ``` ##### tecs.gfx.Camera3D.Options.x field Caller-writable. Sets the world-space x coordinate and defaults to zero. ```teal tecs.gfx.Camera3D.Options.x: number ``` ##### tecs.gfx.Camera3D.Options.y field Caller-writable. Sets the world-space y coordinate and defaults to zero. ```teal tecs.gfx.Camera3D.Options.y: number ``` ##### tecs.gfx.Camera3D.Options.z field Caller-writable. Sets the world-space z coordinate and defaults to zero. ```teal tecs.gfx.Camera3D.Options.z: number ``` ##### tecs.gfx.Camera3D.Options.rotationX field Caller-writable. Sets the orientation quaternion x component and defaults to zero. ```teal tecs.gfx.Camera3D.Options.rotationX: number ``` ##### tecs.gfx.Camera3D.Options.rotationY field Caller-writable. Sets the orientation quaternion y component and defaults to zero. ```teal tecs.gfx.Camera3D.Options.rotationY: number ``` ##### tecs.gfx.Camera3D.Options.rotationZ field Caller-writable. Sets the orientation quaternion z component and defaults to zero. ```teal tecs.gfx.Camera3D.Options.rotationZ: number ``` ##### tecs.gfx.Camera3D.Options.rotationW field Caller-writable. Sets the orientation quaternion scalar component and defaults to one. ```teal tecs.gfx.Camera3D.Options.rotationW: number ``` ##### tecs.gfx.Camera3D.Options.verticalFov field Caller-writable. Sets the vertical field of view in radians and defaults to pi divided by three. ```teal tecs.gfx.Camera3D.Options.verticalFov: number ``` ##### tecs.gfx.Camera3D.Options.near field Caller-writable. Sets the positive near-plane distance and defaults to 0.1 world units. ```teal tecs.gfx.Camera3D.Options.near: number ``` ##### tecs.gfx.Camera3D.Options.far field Caller-writable. Sets the far-plane distance and defaults to 1000 world units. ```teal tecs.gfx.Camera3D.Options.far: number ``` #### tecs.gfx.Camera3D.x field Caller-writable. Sets the world-space x coordinate. ```teal tecs.gfx.Camera3D.x: number ``` #### tecs.gfx.Camera3D.y field Caller-writable. Sets the world-space y coordinate. ```teal tecs.gfx.Camera3D.y: number ``` #### tecs.gfx.Camera3D.z field Caller-writable. Sets the world-space z coordinate. ```teal tecs.gfx.Camera3D.z: number ``` #### tecs.gfx.Camera3D.rotationX field Caller-writable. Sets the local-to-world orientation quaternion x component. ```teal tecs.gfx.Camera3D.rotationX: number ``` #### tecs.gfx.Camera3D.rotationY field Caller-writable. Sets the local-to-world orientation quaternion y component. ```teal tecs.gfx.Camera3D.rotationY: number ``` #### tecs.gfx.Camera3D.rotationZ field Caller-writable. Sets the local-to-world orientation quaternion z component. ```teal tecs.gfx.Camera3D.rotationZ: number ``` #### tecs.gfx.Camera3D.rotationW field Caller-writable. Sets the local-to-world orientation quaternion scalar component. ```teal tecs.gfx.Camera3D.rotationW: number ``` #### tecs.gfx.Camera3D.verticalFov field Caller-writable. Sets the vertical field of view in radians between zero and pi. ```teal tecs.gfx.Camera3D.verticalFov: number ``` #### tecs.gfx.Camera3D.near field Caller-writable. Sets the positive near-plane distance. ```teal tecs.gfx.Camera3D.near: number ``` #### tecs.gfx.Camera3D.far field Caller-writable. Sets the far-plane distance, which must exceed `near`. ```teal tecs.gfx.Camera3D.far: number ``` #### tecs.gfx.Camera3D.newCamera3D Static Creates a perspective camera. ```teal function tecs.gfx.Camera3D.newCamera3D( options: Camera3DOptions ): Camera3D ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `options` | [`Camera3DOptions`](/modules/gfx/#tecs.gfx.Camera3D.Options) | Omit for an identity camera at the origin looking along negative z. | ##### Returns | Type | Description | | --- | --- | | [`Camera3D`](/modules/gfx/#tecs.gfx.Camera3D) | A camera whose fields the caller assigns directly. | #### tecs.gfx.Camera3D:inverseMatrix Instance Writes the column-major clip-to-world matrix for a viewport. This is the exact inverse of `matrix` at the same dimensions. Clip x and y range from negative one to one, clip z ranges from zero to one, and the caller divides the resulting xyz by w. ```teal function tecs.gfx.Camera3D.inverseMatrix( self, width: number, height: number ): loader.CArray ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Camera3D` | | | `width` | `number` | The positive viewport width in pixels. | | `height` | `number` | The positive viewport height in pixels. | ##### Returns | Type | Description | | --- | --- | | `loader.CArray` | The camera's own sixteen-float array, valid until the next call to `inverseMatrix` on this camera. | #### tecs.gfx.Camera3D:matrices Instance Writes the world-to-clip matrix and its clip-to-world inverse. This method normalizes the camera quaternion and derives the projection once, then writes both arrays. Use it when both matrices describe the same view. ```teal function tecs.gfx.Camera3D.matrices( self, width: number, height: number ): loader.CArray, loader.CArray ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Camera3D` | | | `width` | `number` | The positive viewport width in pixels. | | `height` | `number` | The positive viewport height in pixels. | ##### Returns | Type | Description | | --- | --- | | `loader.CArray` | The camera's own world-to-clip sixteen-float array, valid until the next call to `matrix` or `matrices` on this camera. | | `loader.CArray` | The camera's own clip-to-world sixteen-float array, valid until the next call to `inverseMatrix` or `matrices` on this camera. | #### tecs.gfx.Camera3D:matrix Instance Writes the column-major world-to-clip matrix for a viewport. The method normalizes the camera quaternion while calculating the view, without modifying the caller's fields. Clip depth maps `near` to zero and `far` to one. ```teal function tecs.gfx.Camera3D.matrix( self, width: number, height: number ): loader.CArray ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Camera3D` | | | `width` | `number` | The positive viewport width in pixels. | | `height` | `number` | The positive viewport height in pixels. | ##### Returns | Type | Description | | --- | --- | | `loader.CArray` | The camera's own sixteen-float array, valid until the next call on this camera. | ### tecs.gfx.Clip record Restricts a renderable's fragments to a clip region. A component rather than a field on something every renderable has, because presence is the opt-in and absence is the common case. Archetypes without this column skip clipping, and their fragments never read the region table. The index names a rectangle set with `renderer.sprites:setClipRegion`. Zero, which is the default, means no clipping. Nesting is the caller's: a region is one rectangle, so a panel inside a panel is set up as the intersection of the two rather than as two regions an instance sits in at once. Read-only. Exposes the clip component, which keeps a renderable's fragments inside one rectangle. `index` names a region set with `renderer.sprites:setClipRegion`, and 0, the default, means no clipping. A region is a single rectangle, so the caller intersects nested regions. ```teal record tecs.gfx.Clip is Component index: number end ``` #### Interfaces | Interface | | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | #### tecs.gfx.Clip.index field Caller-writable. Selects a clip region configured with `renderer.sprites:setClipRegion`. Zero disables clipping. ```teal tecs.gfx.Clip.index: number ``` ### tecs.gfx.DropShadow2D record Casts a stretched copy of the entity along the ground, away from light. This darkens everything a light left, including ambient light, which [`Occluder2D`](/modules/gfx/#tecs.gfx.Occluder2D) cannot do. It blocks no light in return: a crowd of light-blocking silhouettes merges under the mask into one flat mat of darkness, so the thing that wants a contact shadow is exactly the thing that must not be an occluder. An entity carrying both is an occluder, because dropping that half would silently unblock a light. The nearest few lights by weight throw the copy. Adding a distant light does not move an established shadow. Read-only. Exposes the drop-shadow component, which darkens the ground away from each nearby light, including ambient light, without blocking light. `height` is 0 to 1 and controls the copy's travel under the same interpretation as `Occluder2D.height`. ```teal record tecs.gfx.DropShadow2D is Component height: number end ``` #### Interfaces | Interface | | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | #### tecs.gfx.DropShadow2D.height field Caller-writable. Sets how far lights throw the shadow, from zero to one of the world's configured shadow height. ```teal tecs.gfx.DropShadow2D.height: number ``` ### tecs.gfx.Font record Represents a loaded font named by `Text`. ```teal record tecs.gfx.Font name: string source: string size: number raster: FontRaster end ``` #### tecs.gfx.Font.name field Read-only. Reports the caller-selected identity that snapshots store. ```teal tecs.gfx.Font.name: string ``` #### tecs.gfx.Font.source field Read-only. Reports the source path passed to `newTTF`. ```teal tecs.gfx.Font.source: string ``` #### tecs.gfx.Font.size field Read-only. Reports the point size at which SDL_ttf rasterizes glyphs. ```teal tecs.gfx.Font.size: number ``` #### tecs.gfx.Font.raster field Read-only. Reports whether glyph images contain a scalable distance field or direct alpha coverage. ```teal tecs.gfx.Font.raster: FontRaster ``` ### tecs.gfx.FontRaster enum Selects a font's glyph raster representation. ```teal enum tecs.gfx.FontRaster "alpha" "sdf" end ``` ### tecs.gfx.Material record Selects the material that shades renderable geometry. Absent means the default 2D material. It samples the sprite image array and covers the whole quad, so an entity with neither a Sprite nor a Material still draws. Present selects one of the compiled shader materials found under `materials/`. Use `materials.id(name)` instead of writing an id. Sorted material names determine ids, so adding a file may renumber them. Snapshots therefore store only the material name. Meshes use [`MeshMaterial`](/modules/gfx/#tecs.gfx.MeshMaterial), whose resident PBR data is independent from this compiled 2D shader identity. Read-only. Exposes the compiled 2D shader material component. Absent selects the default material. Take `id` from `materials.id(name)` rather than writing a number because sorted material names determine ids and may move when a file appears. Meshes use `MeshMaterial`. ```teal record tecs.gfx.Material is Component id: number param: number end ``` #### Interfaces | Interface | | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | #### tecs.gfx.Material.id field Caller-writable. Selects a material by the id returned from `materials.id`. ```teal tecs.gfx.Material.id: number ``` #### tecs.gfx.Material.param field Caller-writable. Passes a value from zero to one to the material. The material's business; the rounded rectangle reads it as a corner radius. ```teal tecs.gfx.Material.param: number ``` ### tecs.gfx.Mesh record Selects immutable geometry for the 3D mesh domain. `asset` is the process-local index returned by `meshId`. `slot` is engine-owned residency state and starts negative so a mesh domain can resolve it once. Snapshots store only the normalized asset name. Read-only. Exposes the mesh-reference component. `asset` is a process-local `meshId` and zero means no mesh. `slot` is engine-owned residency state and a negative value means unresolved. ```teal record tecs.gfx.Mesh is Component asset: number slot: number end ``` #### Interfaces | Interface | | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | #### tecs.gfx.Mesh.asset field Caller-writable. Selects geometry by its `meshId` index. Zero selects no mesh. ```teal tecs.gfx.Mesh.asset: number ``` #### tecs.gfx.Mesh.slot field Engine-owned. Stores the mesh domain's residency slot. Set it negative when changing `asset`; otherwise ordinary game code should ignore it. ```teal tecs.gfx.Mesh.slot: number ``` ### tecs.gfx.MeshMaterial record Selects resident PBR data for the 3D mesh domain. A compiled 2D [`Material`](/modules/gfx/#tecs.gfx.Material) and a loaded glTF material are different compatibility surfaces. `asset` therefore names a mesh material independently, while `slot` is engine-owned device residency. Zero in both fields selects the neutral built-in material. Read-only. Exposes the 3D material-reference component. `asset` is a process-local `meshMaterialId`; `slot` is engine-owned residency, and zero selects the neutral built-in material. ```teal record tecs.gfx.MeshMaterial is Component asset: number slot: number end ``` #### Interfaces | Interface | | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | #### tecs.gfx.MeshMaterial.asset field Caller-writable. Selects material data by its `meshMaterialId` index. Zero selects the neutral built-in material. ```teal tecs.gfx.MeshMaterial.asset: number ``` #### tecs.gfx.MeshMaterial.slot field Engine-owned. Stores the mesh domain's resident material slot. Set it negative when changing `asset`; otherwise game code should ignore it. ```teal tecs.gfx.MeshMaterial.slot: number ``` ### tecs.gfx.MeshMorph record Selects one resident morph-weight vector for GPU mesh deformation. `asset` is the stable name's process-local identity. `slot` is the first weight in domain residency and starts negative so extraction can resolve it once. The component is optional; a morphed mesh without it uses its undeformed base geometry. Read-only. Exposes the optional morph-weight component. `asset` is a process-local `meshMorphId`; `slot` is engine-owned residency, and a negative value means unresolved. ```teal record tecs.gfx.MeshMorph is Component asset: number slot: number end ``` #### Interfaces | Interface | | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | #### tecs.gfx.MeshMorph.asset field Caller-writable. Selects weights by their `meshMorphId` index. Zero selects no morph weights. ```teal tecs.gfx.MeshMorph.asset: number ``` #### tecs.gfx.MeshMorph.slot field Engine-owned. Stores the first resident weight. Set it negative when changing `asset`; otherwise ordinary game code should ignore it. ```teal tecs.gfx.MeshMorph.slot: number ``` ### tecs.gfx.MeshSkin record Selects one resident joint palette for GPU mesh skinning. `asset` is the stable name's process-local identity. `slot` is the first joint matrix in domain residency and starts negative so extraction can resolve it once. The component is optional; a mesh without it remains rigid even when its domain enables skinning. Read-only. Exposes the optional joint-palette component. `asset` is a process-local `meshSkinId`; `slot` is engine-owned residency, and a negative value means unresolved. ```teal record tecs.gfx.MeshSkin is Component asset: number slot: number end ``` #### Interfaces | Interface | | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | #### tecs.gfx.MeshSkin.asset field Caller-writable. Selects a palette by its `meshSkinId` index. Zero selects no palette. ```teal tecs.gfx.MeshSkin.asset: number ``` #### tecs.gfx.MeshSkin.slot field Engine-owned. Stores the first resident joint matrix. Set it negative when changing `asset`; otherwise ordinary game code should ignore it. ```teal tecs.gfx.MeshSkin.slot: number ``` ### tecs.gfx.ModelOwner interface ```teal global interface tecs.gfx.ModelOwner path: string animations: {assets.ModelAnimation} animationCount: integer animationIndex: function(self, name: string): integer end ``` #### tecs.gfx.ModelOwner.path field Read-only. Contains the source asset path. ```teal tecs.gfx.ModelOwner.path: string ``` #### tecs.gfx.ModelOwner.animations field Read-only. Contains decoded clips in file order. ```teal tecs.gfx.ModelOwner.animations: {assets.ModelAnimation} ``` #### tecs.gfx.ModelOwner.animationCount field Read-only. Reports the number of decoded clips. ```teal tecs.gfx.ModelOwner.animationCount: integer ``` #### tecs.gfx.ModelOwner:animationIndex Instance Returns a clip's one-based index. ```teal function tecs.gfx.ModelOwner.animationIndex(self, name: string): integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `ModelOwner` | | | `name` | `string` | The caller supplies an authored or generated clip name. | ##### Returns | Type | Description | | --- | --- | | `integer` | Returns its index, or nil when absent. Duplicate authored names raise because selecting either by that name would be ambiguous. | ### tecs.gfx.Occluder2D record Blocks light from reaching what lies behind the entity. The renderer adds the silhouette to the occluder mask every light samples, so one entity blocks every light at the cost of one drawing of itself rather than one per light. It blocks each light's contribution, not ambient light. [`DropShadow2D`](/modules/gfx/#tecs.gfx.DropShadow2D) handles ambient darkening. Coverage is the silhouette, so a circle, a rounded box or a glyph casts the shape it draws with no threshold of its own to set. A translucent entity casts nothing: it is drawn forward over the composited image and never reaches the G-buffer, so a hard silhouette of it would be a lie. Read-only. Exposes the occluder component, which blocks every light with the entity's shape. `height` is 0 to 1 of the world height `Deferred.shadowHeight` sets, so 1 is a full wall and 0 blocks nothing. This component requires renderer `shadows`. ```teal record tecs.gfx.Occluder2D is Component height: number end ``` #### Interfaces | Interface | | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | #### tecs.gfx.Occluder2D.height field Caller-writable. Sets the occluder height from zero to one of the world's configured shadow height. ```teal tecs.gfx.Occluder2D.height: number ``` ### tecs.gfx.PointLight2D record Represents a light resolved by the deferred lighting pass. Read-only. Exposes the point-light component resolved by deferred lighting and positioned by the entity's [`Transform2D`](/modules/ecs/#tecs.ecs.Transform2D). `height` is above the surface plane and at 0 the light contributes nothing at all; `radius` is its reach in world units; `r`, `g`, `b` and `intensity` default to white at 1. ```teal record tecs.gfx.PointLight2D is Component height: number radius: number r: number g: number b: number intensity: number end ``` #### Interfaces | Interface | | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | #### tecs.gfx.PointLight2D.height field Caller-writable. Sets the height above the surface plane. At zero, the Lambert term vanishes and the light contributes nothing. ```teal tecs.gfx.PointLight2D.height: number ``` #### tecs.gfx.PointLight2D.radius field Caller-writable. Sets the light's reach in world units. ```teal tecs.gfx.PointLight2D.radius: number ``` #### tecs.gfx.PointLight2D.r field Caller-writable. Sets the red channel from zero to one. ```teal tecs.gfx.PointLight2D.r: number ``` #### tecs.gfx.PointLight2D.g field Caller-writable. Sets the green channel from zero to one. ```teal tecs.gfx.PointLight2D.g: number ``` #### tecs.gfx.PointLight2D.b field Caller-writable. Sets the blue channel from zero to one. ```teal tecs.gfx.PointLight2D.b: number ``` #### tecs.gfx.PointLight2D.intensity field Caller-writable. Scales the light's contribution. ```teal tecs.gfx.PointLight2D.intensity: number ``` ### tecs.gfx.PointLight3D record Represents an omnidirectional light in the 3D mesh domain. Read-only. Exposes an omnidirectional mesh light positioned by the entity's `tecs.Transform3D`. The mesh domain must enable `lights`. ```teal record tecs.gfx.PointLight3D is Component radius: number r: number g: number b: number intensity: number flags: integer end ``` #### Interfaces | Interface | | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | #### tecs.gfx.PointLight3D.radius field Caller-writable. Sets the light's positive reach in world units. ```teal tecs.gfx.PointLight3D.radius: number ``` #### tecs.gfx.PointLight3D.r field Caller-writable. Sets non-negative red radiance. ```teal tecs.gfx.PointLight3D.r: number ``` #### tecs.gfx.PointLight3D.g field Caller-writable. Sets non-negative green radiance. ```teal tecs.gfx.PointLight3D.g: number ``` #### tecs.gfx.PointLight3D.b field Caller-writable. Sets non-negative blue radiance. ```teal tecs.gfx.PointLight3D.b: number ``` #### tecs.gfx.PointLight3D.intensity field Caller-writable. Scales the light's non-negative radiance. ```teal tecs.gfx.PointLight3D.intensity: number ``` #### tecs.gfx.PointLight3D.flags field Caller-writable. Combines `LIGHT_*` integer constants. Zero, the default, keeps the light out of the optional local-shadow atlas. ```teal tecs.gfx.PointLight3D.flags: integer ``` ### tecs.gfx.PreviousTransform2D record Stores the transform as it stood before the current fixed step. Presence is the opt-in: an entity carrying it is drawn somewhere between this and its current transform, according to how far through the step the frame falls. Simulation advances in fixed jumps and frames arrive whenever the display asks for one, so without this an entity moved by physics steps visibly rather than moving. Only the fields that move continuously are here. Scale and layer change by assignment rather than by integration, and a half-applied assignment is not a value anything asked for. Read-only. Exposes the transform component used for frame interpolation. Presence is the opt-in to interpolation: an entity carrying it is drawn between this and its current transform, so physics does not step visibly. Carries `x`, `y` and `rotation` (radians) only, because scale and layer change by assignment rather than by integration. ```teal record tecs.gfx.PreviousTransform2D is Component x: number y: number rotation: number end ``` #### Interfaces | Interface | | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | #### tecs.gfx.PreviousTransform2D.x field Engine-owned. Stores the previous horizontal position for frame interpolation. Ordinary game code should ignore this field. ```teal tecs.gfx.PreviousTransform2D.x: number ``` #### tecs.gfx.PreviousTransform2D.y field Engine-owned. Stores the previous vertical position for frame interpolation. Ordinary game code should ignore this field. ```teal tecs.gfx.PreviousTransform2D.y: number ``` #### tecs.gfx.PreviousTransform2D.rotation field Engine-owned. Stores the previous rotation in radians for frame interpolation. Ordinary game code should ignore this field. ```teal tecs.gfx.PreviousTransform2D.rotation: number ``` ### tecs.gfx.Renderable2D record Marks an entity as contributing geometry. Without it, a [`Transform2D`](/modules/ecs/#tecs.ecs.Transform2D) represents only a position. Read-only. Exposes the renderable tag, which marks an entity as contributing geometry. The tag costs a column of nothing; without it a [`Transform2D`](/modules/ecs/#tecs.ecs.Transform2D) is only a position, which is what most entities in a world are. ```teal record tecs.gfx.Renderable2D is Component end ``` #### Interfaces | Interface | | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | ### tecs.gfx.Renderable3D record Marks an entity as contributing geometry to the 3D mesh domain. Without it, a `tecs.Transform3D` and [`Mesh`](/modules/gfx/#tecs.gfx.Mesh) describe state but enter no render query. Read-only. Exposes the tag that admits an entity carrying `tecs.Transform3D`, [`Mesh`](/modules/gfx/#tecs.gfx.Mesh), and [`Bounds3D`](/modules/gfx/#tecs.gfx.Bounds3D) to the mesh domain. ```teal record tecs.gfx.Renderable3D is Component end ``` #### Interfaces | Interface | | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | ### tecs.gfx.Renderer record ```teal global record tecs.gfx.Renderer record Options ambient: {number} shadows: Deferred.ShadowOptions sprites: SpriteDomainModule.Options | boolean meshes: MeshDomainModule.Options bloom: Deferred.BloomOptions maxViews: integer end interface DomainStats count: integer dropped: integer rewritten: integer extractSeconds: function(self): number end interface SpriteDomain is DomainStats camera: Camera2D capacity: integer images: TextureArray instances: Buffer addComputeStage: function(self, stage: ComputeStage) addProducer: function(self, producer: InstanceProducer) clearClipRegion: function(self, index: integer) regionOf: function(self, path: string): TextureArray.Region registerImage: function(self, decoded: assets.Image): components.Sprite, TextureArray.Region replaceImage: function(self, decoded: assets.Image): components.Sprite, TextureArray.Region reservesRuns: function(self): boolean setClipRegion: function(self, index: integer, region: ClipRegion) sprite: function(self, name: string, u0: number, v0: number, u1: number, v1: number): components.Sprite spriteSize: function(self, sprite: components.Sprite): number, number end record SpriteOptions capacity: integer cell: integer layers: integer reserveRuns: boolean partialRewrites: boolean packImages: boolean end record SpriteInstanceProducer destroy: function(InstanceProducer) | nil blended: function(self): integer casting: function(self): integer count: function(self): integer takeDirty: function(self): {integer} write: function(self, loader.CArray, loader.CArray, integer, integer, integer) end record SpriteClipRegion x: number y: number width: number height: number end record SpriteComputeStage active: function(self): boolean destroy: function(self) record: function(self, Frame, Buffer, Buffer) end interface MeshDomain is DomainStats record Options capacity: integer vertexCapacity: integer indexCapacity: integer materialCapacity: integer textureWidth: integer textureHeight: integer textureLayers: integer packTextures: boolean mipmaps: boolean textureFormat: integer transparency: boolean doubleSided: boolean shadows: MeshShadowOptions skinning: MeshSkinningOptions morphing: MeshMorphingOptions lights: MeshLightOptions vertexColors: boolean fog: MeshFogOptions probe: MeshProbeOptions environment: MeshEnvironmentOptions ssao: Deferred.SSAOOptions end record MaterialOptions name: string model: integer alphaMode: integer doubleSided: boolean baseColorTexture: integer normalTexture: integer metallicRoughnessTexture: integer occlusionTexture: integer emissiveTexture: integer alphaCutoff: number baseR: number baseG: number baseB: number baseA: number emissiveR: number emissiveG: number emissiveB: number metallic: number roughness: number normalScale: number occlusionStrength: number end record ShadowOptions is MeshShadowTuning scale: number end interface Shadow distance: number splitLambda: number splitBlend: number depthPadding: number directionX: number directionY: number directionZ: number r: number g: number b: number intensity: number strength: number bias: number softness: number end record SkinningOptions jointCapacity: integer end record MorphingOptions vertexCapacity: integer weightCapacity: integer end record LightOptions capacity: integer shadows: MeshLocalShadowOptions end record LocalShadowOptions capacity: integer size: integer bias: number softness: number end record FogOptions is MeshFogTuning end interface Fog start: number finish: number r: number g: number b: number end record ProbeOptions is MeshProbeTuning end interface Probe positiveX: {number} negativeX: {number} positiveY: {number} negativeY: {number} positiveZ: {number} negativeZ: {number} intensity: number end record EnvironmentOptions is MeshEnvironmentTuning size: integer end interface Environment intensity: number skyboxIntensity: number rotation: number end record EnvironmentFaces positiveX: assets.Image negativeX: assets.Image positiveY: assets.Image negativeY: assets.Image positiveZ: assets.Image negativeZ: assets.Image end type SSAOOptions = Deferred.SSAOOptions type SSAO = Deferred.SSAO record RegisteredPrimitive transform: ecs.Transform3D mesh: components.Mesh bounds: components.Bounds3D material: components.MeshMaterial skin: components.MeshSkin morph: components.MeshMorph end record Model3D is ModelOwner record Primitive transform: ecs.Transform3D mesh: components.Mesh bounds: components.Bounds3D material: components.MeshMaterial skin: components.MeshSkin morph: components.MeshMorph end record Instance primitives: {Primitive} transform: ecs.Transform3D animation: integer time: number speed: number loop: boolean playing: boolean bind: function(self, world: World, primitive: integer, entity: integer) play: function(self, animation: string | integer, options: PlayOptions) sample: function(self, animation: string | integer, time: number, loop: boolean) unbind: function(self, primitive: integer) update: function(self, dt: number) end record PlayOptions speed: number loop: boolean playing: boolean end animationIndex: function(self, name: string): integer newInstance: function(self): Instance end MATERIAL_METALLIC_ROUGHNESS: integer MATERIAL_UNLIT: integer MATERIAL_LAMBERT: integer ALPHA_OPAQUE: integer ALPHA_MASK: integer ALPHA_BLEND: integer TEXTURE_RGBA8: integer TEXTURE_BC3: integer camera: Camera3D capacity: integer vertexCapacity: integer indexCapacity: integer meshCount: integer vertexCount: integer indexCount: integer materialCount: integer textureCount: integer jointCount: integer morphVertexCount: integer morphWeightCount: integer transparency: boolean doubleSided: boolean mipmaps: boolean textureFormat: integer shadows: boolean skinning: boolean morphing: boolean localLights: boolean localShadows: boolean localShadowCount: integer lightCapacity: integer lightCount: integer vertexColors: boolean fogging: boolean probing: boolean environmentLighting: boolean environmentSize: integer environmentReady: boolean ssao: Deferred.SSAO shadow: MeshShadowTuning fog: MeshFogTuning probe: MeshProbeTuning environment: MeshEnvironmentTuning material: function(self, name: string): components.MeshMaterial mesh: function(self, name: string): components.Mesh, components.Bounds3D registerEnvironment: function(self, faces: MeshEnvironmentFaces) registerMaterial: function(self, options: MeshMaterialOptions): components.MeshMaterial registerMesh: function(self, mesh: assets.Mesh): components.Mesh, components.Bounds3D registerModel: function(self, model: assets.Model): Model3D registerMorph: function(self, name: string, weights: {number}): components.MeshMorph registerSkin: function(self, name: string, matrices: {number}): components.MeshSkin registerTexture: function(self, image: assets.Image): integer updateMorph: function(self, morph: components.MeshMorph, weights: {number}) updateSkin: function(self, skin: components.MeshSkin, matrices: {number}) end record MeshOptions capacity: integer vertexCapacity: integer indexCapacity: integer materialCapacity: integer textureWidth: integer textureHeight: integer textureLayers: integer packTextures: boolean mipmaps: boolean textureFormat: integer transparency: boolean doubleSided: boolean shadows: MeshShadowOptions skinning: MeshSkinningOptions morphing: MeshMorphingOptions lights: MeshLightOptions vertexColors: boolean fog: MeshFogOptions probe: MeshProbeOptions environment: MeshEnvironmentOptions ssao: Deferred.SSAOOptions end record BloomOptions scale: number threshold: number knee: number intensity: number end sprites: SpriteDomain meshes: MeshDomain deferred: Deferred newRenderer: function(device: loader.CPtr, swapchainFormat: integer, options: RendererOptions): Renderer captureTexture: function(self): Texture depthSortCollapse: function(self): number destroy: function(self) device: function(self): loader.CPtr extractSeconds: function(self): number install: function(self, world: types.World) rebuildPipelines: function(self) render: function(self, frame: Frame) saveScreenshot: function(self, path: string): boolean, string screenshot: function(self): string, string end ``` #### tecs.gfx.Renderer.Options record ```teal record tecs.gfx.Renderer.Options ambient: {number} shadows: Deferred.ShadowOptions sprites: SpriteDomainModule.Options | boolean meshes: MeshDomainModule.Options bloom: Deferred.BloomOptions maxViews: integer end ``` ##### tecs.gfx.Renderer.Options.ambient field Caller-writable. Sets ambient red, green, and blue and defaults to white. ```teal tecs.gfx.Renderer.Options.ambient: {number} ``` ##### tecs.gfx.Renderer.Options.shadows field Caller-writable. Enables and configures 2D shadows. Nil disables them. ```teal tecs.gfx.Renderer.Options.shadows: Deferred.ShadowOptions ``` ##### tecs.gfx.Renderer.Options.sprites field Caller-writable. Configures the 2D sprite rendering domain. False disables it; nil enables it with defaults. ```teal tecs.gfx.Renderer.Options.sprites: SpriteDomainModule.Options | boolean ``` ##### tecs.gfx.Renderer.Options.meshes field Caller-writable. Configures and enables the 3D mesh rendering domain. Nil leaves every mesh module and allocation out of this renderer. ```teal tecs.gfx.Renderer.Options.meshes: MeshDomainModule.Options ``` ##### tecs.gfx.Renderer.Options.bloom field Caller-writable. Enables and configures optional bloom. Nil omits its targets, pipelines, and passes. ```teal tecs.gfx.Renderer.Options.bloom: Deferred.BloomOptions ``` ##### tecs.gfx.Renderer.Options.maxViews field Caller-writable. Enables [`View`](/modules/gfx/#tecs.gfx.View) entities and sets their fixed ceiling. Omit to retain the original single full-frame camera path. Setting it allocates one forward intermediate shared by every view. ```teal tecs.gfx.Renderer.Options.maxViews: integer ``` #### tecs.gfx.Renderer.DomainStats interface ```teal interface tecs.gfx.Renderer.DomainStats count: integer dropped: integer rewritten: integer extractSeconds: function(self): number end ``` ##### tecs.gfx.Renderer.DomainStats.count field Read-only. Reports instances resident after the last extraction. ```teal tecs.gfx.Renderer.DomainStats.count: integer ``` ##### tecs.gfx.Renderer.DomainStats.dropped field Read-only. Reports instances the last extraction could not fit. ```teal tecs.gfx.Renderer.DomainStats.dropped: integer ``` ##### tecs.gfx.Renderer.DomainStats.rewritten field Read-only. Reports instances rewritten by the last extraction. ```teal tecs.gfx.Renderer.DomainStats.rewritten: integer ``` ##### tecs.gfx.Renderer.DomainStats:extractSeconds Instance Returns the seconds consumed by the last extraction. ```teal function tecs.gfx.Renderer.DomainStats.extractSeconds(self): number ``` ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `DomainStats` | | ###### Returns | Type | Description | | --- | --- | | `number` | Returns elapsed seconds, or zero while measurement is inactive. | #### tecs.gfx.Renderer.SpriteDomain interface ```teal interface tecs.gfx.Renderer.SpriteDomain is DomainStats camera: Camera2D capacity: integer images: TextureArray instances: Buffer addComputeStage: function(self, stage: ComputeStage) addProducer: function(self, producer: InstanceProducer) clearClipRegion: function(self, index: integer) regionOf: function(self, path: string): TextureArray.Region registerImage: function( self, decoded: assets.Image ): components.Sprite, TextureArray.Region replaceImage: function( self, decoded: assets.Image ): components.Sprite, TextureArray.Region reservesRuns: function(self): boolean setClipRegion: function(self, index: integer, region: ClipRegion) sprite: function( self, name: string, u0: number, v0: number, u1: number, v1: number ): components.Sprite spriteSize: function( self, sprite: components.Sprite ): number, number end ``` ##### Interfaces | Interface | | --- | | [`DomainStats`](/modules/gfx/#tecs.gfx.Renderer.DomainStats) | ##### tecs.gfx.Renderer.SpriteDomain.camera field Caller-writable. Controls the 2D view. The first drawable frame centers it on the viewport. ```teal tecs.gfx.Renderer.SpriteDomain.camera: Camera2D ``` ##### tecs.gfx.Renderer.SpriteDomain.capacity field Read-only. Reports the instance capacity fixed at creation. ```teal tecs.gfx.Renderer.SpriteDomain.capacity: integer ``` ##### tecs.gfx.Renderer.SpriteDomain.images field Read-only. Provides the sprite image array. ```teal tecs.gfx.Renderer.SpriteDomain.images: TextureArray ``` ##### tecs.gfx.Renderer.SpriteDomain.instances field Engine-owned. Exposes sprite instances to custom compute stages. Ordinary game code should ignore it. ```teal tecs.gfx.Renderer.SpriteDomain.instances: Buffer ``` ##### tecs.gfx.Renderer.SpriteDomain:addComputeStage Instance Adds a compute stage between staging flush and sprite culling. ```teal function tecs.gfx.Renderer.SpriteDomain.addComputeStage( self, stage: ComputeStage ) ``` ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `SpriteDomain` | | | `stage` | [`ComputeStage`](/modules/gfx/#tecs.gfx.Renderer.SpriteComputeStage) | The caller supplies a stage retained and destroyed by the domain. | ###### Returns None. ##### tecs.gfx.Renderer.SpriteDomain:addProducer Instance Adds a retained instance producer after archetype instances. ```teal function tecs.gfx.Renderer.SpriteDomain.addProducer( self, producer: InstanceProducer ) ``` ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `SpriteDomain` | | | `producer` | [`InstanceProducer`](/modules/gfx/#tecs.gfx.Renderer.SpriteInstanceProducer) | The caller supplies a producer destroyed with the domain. | ###### Returns None. ##### tecs.gfx.Renderer.SpriteDomain:clearClipRegion Instance Stops one clip-region index from clipping. ```teal function tecs.gfx.Renderer.SpriteDomain.clearClipRegion( self, index: integer ) ``` ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `SpriteDomain` | | | `index` | `integer` | The caller supplies an integer from 1 through 255. | ###### Returns None. ##### tecs.gfx.Renderer.SpriteDomain:regionOf Instance Returns the region occupied by a registered image. ```teal function tecs.gfx.Renderer.SpriteDomain.regionOf( self, path: string ): TextureArray.Region ``` ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `SpriteDomain` | | | `path` | `string` | The caller supplies the path used during registration. | ###### Returns | Type | Description | | --- | --- | | `TextureArray.Region` | Returns the domain-owned live region, or nil when none matches. | ##### tecs.gfx.Renderer.SpriteDomain:registerImage Instance Uploads a decoded image and returns a sprite selecting the whole image. The path identifies the image for this domain's lifetime. Re-registering it returns the existing region. The call releases valid decoded pixels. ```teal function tecs.gfx.Renderer.SpriteDomain.registerImage( self, decoded: assets.Image ): components.Sprite, TextureArray.Region ``` ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `SpriteDomain` | | | `decoded` | [`assets.Image`](/modules/assets/#tecs.assets.Image) | The caller supplies a ready image that still owns its pixels. | ###### Returns | Type | Description | | --- | --- | | [`components.Sprite`](/modules/gfx/#tecs.gfx.Sprite) | Returns a fresh sprite that selects the whole image. | | `TextureArray.Region` | Returns the domain-owned live region that contains the image. | ##### tecs.gfx.Renderer.SpriteDomain:replaceImage Instance Uploads a decoded image over the one registered under its path. The replacement keeps the registered layer and rectangle, so existing sprites keep their identity. Missing pixels, an unknown path, or a size change raises. The call releases valid decoded pixels. ```teal function tecs.gfx.Renderer.SpriteDomain.replaceImage( self, decoded: assets.Image ): components.Sprite, TextureArray.Region ``` ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `SpriteDomain` | | | `decoded` | [`assets.Image`](/modules/assets/#tecs.assets.Image) | The caller supplies an image matching a registered path and size. | ###### Returns | Type | Description | | --- | --- | | [`components.Sprite`](/modules/gfx/#tecs.gfx.Sprite) | Returns a fresh sprite selecting the unchanged region. | | `TextureArray.Region` | Returns the domain-owned live region. | ##### tecs.gfx.Renderer.SpriteDomain:reservesRuns Instance Returns whether archetype runs have room to grow. ```teal function tecs.gfx.Renderer.SpriteDomain.reservesRuns(self): boolean ``` ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `SpriteDomain` | | ###### Returns | Type | Description | | --- | --- | | `boolean` | Returns the fixed creation setting. | ##### tecs.gfx.Renderer.SpriteDomain:setClipRegion Instance Assigns a clipping rectangle in target pixels. ```teal function tecs.gfx.Renderer.SpriteDomain.setClipRegion( self, index: integer, region: ClipRegion ) ``` ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `SpriteDomain` | | | `index` | `integer` | The caller supplies an integer from 1 through 255. | | `region` | [`ClipRegion`](/modules/gfx/#tecs.gfx.Renderer.SpriteClipRegion) | The caller supplies the target-pixel rectangle. | ###### Returns None. ##### tecs.gfx.Renderer.SpriteDomain:sprite Instance Returns a sprite for a registered image. UVs are fractions of the image. Omitted values select the whole image. An unknown name raises. ```teal function tecs.gfx.Renderer.SpriteDomain.sprite( self, name: string, u0: number, v0: number, u1: number, v1: number ): components.Sprite ``` ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `SpriteDomain` | | | `name` | `string` | The caller supplies the registered image path. | | `u0` | `number` | The caller supplies the left fraction or omits it for zero. | | `v0` | `number` | The caller supplies the top fraction or omits it for zero. | | `u1` | `number` | The caller supplies the right fraction or omits it for one. | | `v1` | `number` | The caller supplies the bottom fraction or omits it for one. | ###### Returns | Type | Description | | --- | --- | | [`components.Sprite`](/modules/gfx/#tecs.gfx.Sprite) | Returns a fresh sprite mapped into the image-array region. | ##### tecs.gfx.Renderer.SpriteDomain:spriteSize Instance Returns a sprite's natural size in source-image pixels. ```teal function tecs.gfx.Renderer.SpriteDomain.spriteSize( self, sprite: components.Sprite ): number, number ``` ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `SpriteDomain` | | | `sprite` | [`components.Sprite`](/modules/gfx/#tecs.gfx.Sprite) | The caller supplies a sprite registered on this domain. | ###### Returns | Type | Description | | --- | --- | | `number` | Returns its width, or zero when the image is unresolved. | | `number` | Returns its height, or zero when the image is unresolved. | #### tecs.gfx.Renderer.SpriteOptions record ```teal record tecs.gfx.Renderer.SpriteOptions capacity: integer cell: integer layers: integer reserveRuns: boolean partialRewrites: boolean packImages: boolean end ``` ##### tecs.gfx.Renderer.SpriteOptions.capacity field Caller-writable. Sets the maximum resident sprite-instance count. Rows beyond this fixed ceiling are dropped instead of growing a buffer under a frame that may still read it. ```teal tecs.gfx.Renderer.SpriteOptions.capacity: integer ``` ##### tecs.gfx.Renderer.SpriteOptions.cell field Caller-writable. Sets the image-array cell size in pixels. ```teal tecs.gfx.Renderer.SpriteOptions.cell: integer ``` ##### tecs.gfx.Renderer.SpriteOptions.layers field Caller-writable. Sets the image-array layer count. ```teal tecs.gfx.Renderer.SpriteOptions.layers: integer ``` ##### tecs.gfx.Renderer.SpriteOptions.reserveRuns field Caller-writable. Gives archetype runs room to grow without moving every later run. Defaults to false, which packs runs end to end. ```teal tecs.gfx.Renderer.SpriteOptions.reserveRuns: boolean ``` ##### tecs.gfx.Renderer.SpriteOptions.partialRewrites field Caller-writable. Rewrites only structurally changed rows instead of a whole archetype run. Defaults to false. ```teal tecs.gfx.Renderer.SpriteOptions.partialRewrites: boolean ``` ##### tecs.gfx.Renderer.SpriteOptions.packImages field Caller-writable. Packs many images into each array layer. Defaults to false, which gives each image its own layer. ```teal tecs.gfx.Renderer.SpriteOptions.packImages: boolean ``` #### tecs.gfx.Renderer.SpriteInstanceProducer record Something that draws instances without owning entities. Text is the reason this exists. A glyph is a textured quad like any other, but making each one an entity puts every glyph in the world into one archetype, so editing one string marks that column dirty and rewrites all of them; and spawning or despawning glyphs moves an archetype's length, which forces the whole scene to be laid out again. A producer sidesteps both: it is laid out as its own run, and it says which parts of that run changed. It writes the same sixteen floats and four bounds floats every other instance carries, so culling, depth, layers and materials all apply to it without knowing it is not an entity. ```teal record tecs.gfx.Renderer.SpriteInstanceProducer destroy: function(InstanceProducer) | nil blended: function(self): integer casting: function(self): integer count: function(self): integer takeDirty: function(self): {integer} write: function( self, loader.CArray, loader.CArray, integer, integer, integer ) end ``` ##### tecs.gfx.Renderer.SpriteInstanceProducer.destroy field Caller-writable. Releases work the producer owns. The renderer calls this at most once, before releasing the device resources its instances feed. The producer may omit it when it has nothing to release. ```teal tecs.gfx.Renderer.SpriteInstanceProducer.destroy: function(InstanceProducer) | nil ``` ##### tecs.gfx.Renderer.SpriteInstanceProducer:blended Instance Instances of its run that may reach the forward pass. Asked every sync, because the forward lane is skipped entirely on a frame nothing said was blended and a producer whose run holds blended instances would then not be drawn. A producer that never blends answers zero and costs one call. An upper bound rather than a count, and deliberately: a producer whose instances are written by compute cannot know how many of them a frame actually has. Over-reporting runs a lane that finds less than it was told to expect, which is the safe direction; under-reporting drops the whole lane, which is not. ```teal function tecs.gfx.Renderer.SpriteInstanceProducer.blended(self): integer ``` ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `InstanceProducer` | | ###### Returns | Type | Description | | --- | --- | | `integer` | | ##### tecs.gfx.Renderer.SpriteInstanceProducer:casting Instance Caller-writable. Reports instances of its run that may reach the shadow lane. This has the same upper-bound contract as `blended`: a CPU producer answers exactly, while a compute producer may conservatively report every slot that can carry a caster. Under-reporting would skip the lane and lose shadows; over-reporting only runs an empty compaction. ```teal function tecs.gfx.Renderer.SpriteInstanceProducer.casting(self): integer ``` ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `InstanceProducer` | | ###### Returns | Type | Description | | --- | --- | | `integer` | | ##### tecs.gfx.Renderer.SpriteInstanceProducer:count Instance Instances to reserve. Changing it moves the layout, so a producer that can avoid changing it should. ```teal function tecs.gfx.Renderer.SpriteInstanceProducer.count(self): integer ``` ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `InstanceProducer` | | ###### Returns | Type | Description | | --- | --- | | `integer` | | ##### tecs.gfx.Renderer.SpriteInstanceProducer:takeDirty Instance Sub-ranges written since the last sync, as flat one-based inclusive pairs, and cleared by returning them. Empty means nothing changed and the run is skipped. ```teal function tecs.gfx.Renderer.SpriteInstanceProducer.takeDirty( self ): {integer} ``` ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `InstanceProducer` | | ###### Returns | Type | Description | | --- | --- | | `{integer}` | | ##### tecs.gfx.Renderer.SpriteInstanceProducer:write Instance Writes instances `first` through `last` of its run. `base` is the instance index the run starts at, so instance `first` is written at `base + first - 1`. ```teal function tecs.gfx.Renderer.SpriteInstanceProducer.write( self, loader.CArray, loader.CArray, integer, integer, integer ) ``` ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `InstanceProducer` | | | `#2` | `loader.CArray` | | | `#3` | `loader.CArray` | | | `#4` | `integer` | | | `#5` | `integer` | | | `#6` | `integer` | | ###### Returns None. #### tecs.gfx.Renderer.SpriteClipRegion record A clip rectangle, in target pixels measured from the top left. Screen space rather than world space, because that is what a fragment is tested in and what a scissor means: a panel occupies a part of the window whether its contents are placed by the camera, in screen pixels, or in virtual coordinates, and one rectangle is right for all three. ```teal record tecs.gfx.Renderer.SpriteClipRegion x: number y: number width: number height: number end ``` ##### tecs.gfx.Renderer.SpriteClipRegion.x field Caller-writable. Sets the left edge in target pixels. ```teal tecs.gfx.Renderer.SpriteClipRegion.x: number ``` ##### tecs.gfx.Renderer.SpriteClipRegion.y field Caller-writable. Sets the top edge in target pixels. ```teal tecs.gfx.Renderer.SpriteClipRegion.y: number ``` ##### tecs.gfx.Renderer.SpriteClipRegion.width field Caller-writable. Sets the width in target pixels. ```teal tecs.gfx.Renderer.SpriteClipRegion.width: number ``` ##### tecs.gfx.Renderer.SpriteClipRegion.height field Caller-writable. Sets the height in target pixels. ```teal tecs.gfx.Renderer.SpriteClipRegion.height: number ``` #### tecs.gfx.Renderer.SpriteComputeStage record Something that writes instances with compute before the cull runs. The pool a particle emitter draws from is the caller this exists for. Its contents outlive the frame, so it is not written through staging the way an archetype run is, and what it writes has to land before the mark pass reads the bounds beside it. A stage is recorded on the frame's command buffer between the staging flush and the cull, and SDL orders all three because each declares what it writes. ```teal record tecs.gfx.Renderer.SpriteComputeStage active: function(self): boolean destroy: function(self) record: function(self, Frame, Buffer, Buffer) end ``` ##### tecs.gfx.Renderer.SpriteComputeStage:active Instance Whether this stage has anything to record this frame. Asked every frame rather than derived from a dirty bit, because the thing this exists for dirties nothing: an emitter's whole field moves every frame while the world holding it reports no change at all. There is no bit to read, so the stage has to publish the answer. ```teal function tecs.gfx.Renderer.SpriteComputeStage.active(self): boolean ``` ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `ComputeStage` | | ###### Returns | Type | Description | | --- | --- | | `boolean` | | ##### tecs.gfx.Renderer.SpriteComputeStage:destroy Instance Releases what the stage owns, called when the backend is destroyed. A stage's buffers and pipelines are built on the backend's device, so they cannot outlive it and nothing else is holding them: a world that installed a stage and then dropped its renderer would leak every one of them otherwise. Must be safe to call more than once. ```teal function tecs.gfx.Renderer.SpriteComputeStage.destroy(self) ``` ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `ComputeStage` | | ###### Returns None. ##### tecs.gfx.Renderer.SpriteComputeStage:record Instance Records its dispatches onto `frame`'s command buffer. The instance and bounds buffers are handed over rather than reached for, because writing them is the whole point of being here and a stage that had to find them would be a stage that could find something else. ```teal function tecs.gfx.Renderer.SpriteComputeStage.record( self, Frame, Buffer, Buffer ) ``` ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `ComputeStage` | | | `#2` | `Frame` | | | `#3` | `Buffer` | | | `#4` | `Buffer` | | ###### Returns None. #### tecs.gfx.Renderer.MeshDomain interface ```teal interface tecs.gfx.Renderer.MeshDomain is DomainStats record Options capacity: integer vertexCapacity: integer indexCapacity: integer materialCapacity: integer textureWidth: integer textureHeight: integer textureLayers: integer packTextures: boolean mipmaps: boolean textureFormat: integer transparency: boolean doubleSided: boolean shadows: MeshShadowOptions skinning: MeshSkinningOptions morphing: MeshMorphingOptions lights: MeshLightOptions vertexColors: boolean fog: MeshFogOptions probe: MeshProbeOptions environment: MeshEnvironmentOptions ssao: Deferred.SSAOOptions end record MaterialOptions name: string model: integer alphaMode: integer doubleSided: boolean baseColorTexture: integer normalTexture: integer metallicRoughnessTexture: integer occlusionTexture: integer emissiveTexture: integer alphaCutoff: number baseR: number baseG: number baseB: number baseA: number emissiveR: number emissiveG: number emissiveB: number metallic: number roughness: number normalScale: number occlusionStrength: number end record ShadowOptions is MeshShadowTuning scale: number end interface Shadow distance: number splitLambda: number splitBlend: number depthPadding: number directionX: number directionY: number directionZ: number r: number g: number b: number intensity: number strength: number bias: number softness: number end record SkinningOptions jointCapacity: integer end record MorphingOptions vertexCapacity: integer weightCapacity: integer end record LightOptions capacity: integer shadows: MeshLocalShadowOptions end record LocalShadowOptions capacity: integer size: integer bias: number softness: number end record FogOptions is MeshFogTuning end interface Fog start: number finish: number r: number g: number b: number end record ProbeOptions is MeshProbeTuning end interface Probe positiveX: {number} negativeX: {number} positiveY: {number} negativeY: {number} positiveZ: {number} negativeZ: {number} intensity: number end record EnvironmentOptions is MeshEnvironmentTuning size: integer end interface Environment intensity: number skyboxIntensity: number rotation: number end record EnvironmentFaces positiveX: assets.Image negativeX: assets.Image positiveY: assets.Image negativeY: assets.Image positiveZ: assets.Image negativeZ: assets.Image end type SSAOOptions = Deferred.SSAOOptions type SSAO = Deferred.SSAO record RegisteredPrimitive transform: ecs.Transform3D mesh: components.Mesh bounds: components.Bounds3D material: components.MeshMaterial skin: components.MeshSkin morph: components.MeshMorph end record Model3D is ModelOwner record Primitive transform: ecs.Transform3D mesh: components.Mesh bounds: components.Bounds3D material: components.MeshMaterial skin: components.MeshSkin morph: components.MeshMorph end record Instance primitives: {Primitive} transform: ecs.Transform3D animation: integer time: number speed: number loop: boolean playing: boolean bind: function(self, world: World, primitive: integer, entity: integer) play: function(self, animation: string | integer, options: PlayOptions) sample: function(self, animation: string | integer, time: number, loop: boolean) unbind: function(self, primitive: integer) update: function(self, dt: number) end record PlayOptions speed: number loop: boolean playing: boolean end animationIndex: function(self, name: string): integer newInstance: function(self): Instance end MATERIAL_METALLIC_ROUGHNESS: integer MATERIAL_UNLIT: integer MATERIAL_LAMBERT: integer ALPHA_OPAQUE: integer ALPHA_MASK: integer ALPHA_BLEND: integer TEXTURE_RGBA8: integer TEXTURE_BC3: integer camera: Camera3D capacity: integer vertexCapacity: integer indexCapacity: integer meshCount: integer vertexCount: integer indexCount: integer materialCount: integer textureCount: integer jointCount: integer morphVertexCount: integer morphWeightCount: integer transparency: boolean doubleSided: boolean mipmaps: boolean textureFormat: integer shadows: boolean skinning: boolean morphing: boolean localLights: boolean localShadows: boolean localShadowCount: integer lightCapacity: integer lightCount: integer vertexColors: boolean fogging: boolean probing: boolean environmentLighting: boolean environmentSize: integer environmentReady: boolean ssao: Deferred.SSAO shadow: MeshShadowTuning fog: MeshFogTuning probe: MeshProbeTuning environment: MeshEnvironmentTuning material: function(self, name: string): components.MeshMaterial mesh: function(self, name: string): components.Mesh, components.Bounds3D registerEnvironment: function(self, faces: MeshEnvironmentFaces) registerMaterial: function(self, options: MeshMaterialOptions): components.MeshMaterial registerMesh: function(self, mesh: assets.Mesh): components.Mesh, components.Bounds3D registerModel: function(self, model: assets.Model): Model3D registerMorph: function(self, name: string, weights: {number}): components.MeshMorph registerSkin: function(self, name: string, matrices: {number}): components.MeshSkin registerTexture: function(self, image: assets.Image): integer updateMorph: function(self, morph: components.MeshMorph, weights: {number}) updateSkin: function(self, skin: components.MeshSkin, matrices: {number}) end ``` ##### Interfaces | Interface | | --- | | [`DomainStats`](/modules/gfx/#tecs.gfx.Renderer.DomainStats) | ##### tecs.gfx.Renderer.MeshDomain.Options record ```teal record tecs.gfx.Renderer.MeshDomain.Options capacity: integer vertexCapacity: integer indexCapacity: integer materialCapacity: integer textureWidth: integer textureHeight: integer textureLayers: integer packTextures: boolean mipmaps: boolean textureFormat: integer transparency: boolean doubleSided: boolean shadows: MeshShadowOptions skinning: MeshSkinningOptions morphing: MeshMorphingOptions lights: MeshLightOptions vertexColors: boolean fog: MeshFogOptions probe: MeshProbeOptions environment: MeshEnvironmentOptions ssao: Deferred.SSAOOptions end ``` ###### tecs.gfx.Renderer.MeshDomain.Options.capacity field Caller-writable. Sets the maximum resident mesh-instance count and defaults to 65,536. ```teal tecs.gfx.Renderer.MeshDomain.Options.capacity: integer ``` ###### tecs.gfx.Renderer.MeshDomain.Options.vertexCapacity field Caller-writable. Sets the immutable geometry ceiling in vertices and defaults to 1,048,576. ```teal tecs.gfx.Renderer.MeshDomain.Options.vertexCapacity: integer ``` ###### tecs.gfx.Renderer.MeshDomain.Options.indexCapacity field Caller-writable. Sets the immutable geometry ceiling in 32-bit indices and defaults to 3,145,728. ```teal tecs.gfx.Renderer.MeshDomain.Options.indexCapacity: integer ``` ###### tecs.gfx.Renderer.MeshDomain.Options.materialCapacity field Caller-writable. Sets the material-table ceiling, including the built-in neutral slot, and defaults to 1,024. ```teal tecs.gfx.Renderer.MeshDomain.Options.materialCapacity: integer ``` ###### tecs.gfx.Renderer.MeshDomain.Options.textureWidth field Caller-writable. Sets the width of every mesh texture-array layer and defaults to 1,024 pixels. ```teal tecs.gfx.Renderer.MeshDomain.Options.textureWidth: integer ``` ###### tecs.gfx.Renderer.MeshDomain.Options.textureHeight field Caller-writable. Sets the height of every mesh texture-array layer and defaults to 1,024 pixels. ```teal tecs.gfx.Renderer.MeshDomain.Options.textureHeight: integer ``` ###### tecs.gfx.Renderer.MeshDomain.Options.textureLayers field Caller-writable. Sets the fixed mesh texture-array layer count and defaults to 16. ```teal tecs.gfx.Renderer.MeshDomain.Options.textureLayers: integer ``` ###### tecs.gfx.Renderer.MeshDomain.Options.packTextures field Caller-writable. Packs multiple images into each texture layer and defaults to true. ```teal tecs.gfx.Renderer.MeshDomain.Options.packTextures: boolean ``` ###### tecs.gfx.Renderer.MeshDomain.Options.mipmaps field Caller-writable. Allocates and linearly filters a complete mip chain. This requires `packTextures = false`. Smaller RGBA images repeat their edge through the rest of the cell before mip generation. ```teal tecs.gfx.Renderer.MeshDomain.Options.mipmaps: boolean ``` ###### tecs.gfx.Renderer.MeshDomain.Options.textureFormat field Caller-writable. Selects decoded RGBA8 or imported BC3 storage with a `TEXTURE_*` integer constant and defaults to `TEXTURE_RGBA8`. BC3 requires mipmaps and disables texture packing. ```teal tecs.gfx.Renderer.MeshDomain.Options.textureFormat: integer ``` ###### tecs.gfx.Renderer.MeshDomain.Options.transparency field Caller-writable. Enables the independently allocated transparent mesh lane and defaults to false. ```teal tecs.gfx.Renderer.MeshDomain.Options.transparency: boolean ``` ###### tecs.gfx.Renderer.MeshDomain.Options.doubleSided field Caller-writable. Enables independently allocated double-sided command and pipeline resources and defaults to false. ```teal tecs.gfx.Renderer.MeshDomain.Options.doubleSided: boolean ``` ###### tecs.gfx.Renderer.MeshDomain.Options.shadows field Caller-writable. Enables and configures one independently allocated directional mesh-shadow lane. Nil disables it. ```teal tecs.gfx.Renderer.MeshDomain.Options.shadows: MeshShadowOptions ``` ###### tecs.gfx.Renderer.MeshDomain.Options.skinning field Caller-writable. Enables independently allocated skin attributes, per-instance palette offsets, joint matrices, and shader variants. Nil disables GPU skinning. ```teal tecs.gfx.Renderer.MeshDomain.Options.skinning: MeshSkinningOptions ``` ###### tecs.gfx.Renderer.MeshDomain.Options.morphing field Caller-writable. Enables independently allocated morph deltas, per-instance metadata and weights, and shader variants. Nil disables GPU morphing. ```teal tecs.gfx.Renderer.MeshDomain.Options.morphing: MeshMorphingOptions ``` ###### tecs.gfx.Renderer.MeshDomain.Options.lights field Caller-writable. Enables independently allocated point and spot light extraction, buffers, screen-tile binning, and shader variants. Nil disables local 3D lights. ```teal tecs.gfx.Renderer.MeshDomain.Options.lights: MeshLightOptions ``` ###### tecs.gfx.Renderer.MeshDomain.Options.vertexColors field Caller-writable. Enables a separate immutable linear RGBA vertex-color stream and shader variants. Defaults to false. A colored procedural or glTF mesh requires this option. ```teal tecs.gfx.Renderer.MeshDomain.Options.vertexColors: boolean ``` ###### tecs.gfx.Renderer.MeshDomain.Options.fog field Caller-writable. Enables linear camera-distance fog for meshes. Nil omits its shader variants and runtime work. ```teal tecs.gfx.Renderer.MeshDomain.Options.fog: MeshFogOptions ``` ###### tecs.gfx.Renderer.MeshDomain.Options.probe field Caller-writable. Enables diffuse environment lighting for meshes. Nil omits its shader variants and runtime work. ```teal tecs.gfx.Renderer.MeshDomain.Options.probe: MeshProbeOptions ``` ###### tecs.gfx.Renderer.MeshDomain.Options.environment field Caller-writable. Enables a six-face specular environment and optional skybox. Nil allocates no environment texture or sampler. ```teal tecs.gfx.Renderer.MeshDomain.Options.environment: MeshEnvironmentOptions ``` ###### tecs.gfx.Renderer.MeshDomain.Options.ssao field Caller-writable. Enables half-resolution screen-space ambient occlusion for opaque meshes. Nil allocates no AO targets or pipelines. ```teal tecs.gfx.Renderer.MeshDomain.Options.ssao: Deferred.SSAOOptions ``` ##### tecs.gfx.Renderer.MeshDomain.MaterialOptions record ```teal record tecs.gfx.Renderer.MeshDomain.MaterialOptions name: string model: integer alphaMode: integer doubleSided: boolean baseColorTexture: integer normalTexture: integer metallicRoughnessTexture: integer occlusionTexture: integer emissiveTexture: integer alphaCutoff: number baseR: number baseG: number baseB: number baseA: number emissiveR: number emissiveG: number emissiveB: number metallic: number roughness: number normalScale: number occlusionStrength: number end ``` ###### tecs.gfx.Renderer.MeshDomain.MaterialOptions.name field Caller-writable. Supplies the stable, non-empty material name used by snapshots and duplicate registration. ```teal tecs.gfx.Renderer.MeshDomain.MaterialOptions.name: string ``` ###### tecs.gfx.Renderer.MeshDomain.MaterialOptions.model field Caller-writable. Selects one of the domain's `MATERIAL_*` integer constants and defaults to `MATERIAL_METALLIC_ROUGHNESS`. ```teal tecs.gfx.Renderer.MeshDomain.MaterialOptions.model: integer ``` ###### tecs.gfx.Renderer.MeshDomain.MaterialOptions.alphaMode field Caller-writable. Selects opaque, masked, or blended rendering with an `ALPHA_*` integer constant and defaults to `ALPHA_OPAQUE`. ```teal tecs.gfx.Renderer.MeshDomain.MaterialOptions.alphaMode: integer ``` ###### tecs.gfx.Renderer.MeshDomain.MaterialOptions.doubleSided field Caller-writable. Renders both triangle faces and defaults to false. This requires `meshes.doubleSided = true`. ```teal tecs.gfx.Renderer.MeshDomain.MaterialOptions.doubleSided: boolean ``` ###### tecs.gfx.Renderer.MeshDomain.MaterialOptions.baseColorTexture field Caller-writable. Selects a texture returned by `registerTexture`, or zero for the white fallback. ```teal tecs.gfx.Renderer.MeshDomain.MaterialOptions.baseColorTexture: integer ``` ###### tecs.gfx.Renderer.MeshDomain.MaterialOptions.normalTexture field Caller-writable. Selects a tangent-space normal texture, or zero for a flat normal. ```teal tecs.gfx.Renderer.MeshDomain.MaterialOptions.normalTexture: integer ``` ###### tecs.gfx.Renderer.MeshDomain.MaterialOptions.metallicRoughnessTexture field Caller-writable. Selects a glTF metallic-roughness texture, or zero. ```teal tecs.gfx.Renderer.MeshDomain.MaterialOptions.metallicRoughnessTexture: integer ``` ###### tecs.gfx.Renderer.MeshDomain.MaterialOptions.occlusionTexture field Caller-writable. Selects an occlusion texture, or zero for no occlusion. ```teal tecs.gfx.Renderer.MeshDomain.MaterialOptions.occlusionTexture: integer ``` ###### tecs.gfx.Renderer.MeshDomain.MaterialOptions.emissiveTexture field Caller-writable. Selects an emissive texture, or zero for black. ```teal tecs.gfx.Renderer.MeshDomain.MaterialOptions.emissiveTexture: integer ``` ###### tecs.gfx.Renderer.MeshDomain.MaterialOptions.alphaCutoff field Caller-writable. Discards base alpha below this threshold and defaults to 0.5. ```teal tecs.gfx.Renderer.MeshDomain.MaterialOptions.alphaCutoff: number ``` ###### tecs.gfx.Renderer.MeshDomain.MaterialOptions.baseR field Caller-writable. Multiplies the base-color texture's red channel. ```teal tecs.gfx.Renderer.MeshDomain.MaterialOptions.baseR: number ``` ###### tecs.gfx.Renderer.MeshDomain.MaterialOptions.baseG field Caller-writable. Multiplies the base-color texture's green channel. ```teal tecs.gfx.Renderer.MeshDomain.MaterialOptions.baseG: number ``` ###### tecs.gfx.Renderer.MeshDomain.MaterialOptions.baseB field Caller-writable. Multiplies the base-color texture's blue channel. ```teal tecs.gfx.Renderer.MeshDomain.MaterialOptions.baseB: number ``` ###### tecs.gfx.Renderer.MeshDomain.MaterialOptions.baseA field Caller-writable. Multiplies the base-color texture's alpha channel. ```teal tecs.gfx.Renderer.MeshDomain.MaterialOptions.baseA: number ``` ###### tecs.gfx.Renderer.MeshDomain.MaterialOptions.emissiveR field Caller-writable. Multiplies emissive red. ```teal tecs.gfx.Renderer.MeshDomain.MaterialOptions.emissiveR: number ``` ###### tecs.gfx.Renderer.MeshDomain.MaterialOptions.emissiveG field Caller-writable. Multiplies emissive green. ```teal tecs.gfx.Renderer.MeshDomain.MaterialOptions.emissiveG: number ``` ###### tecs.gfx.Renderer.MeshDomain.MaterialOptions.emissiveB field Caller-writable. Multiplies emissive blue. ```teal tecs.gfx.Renderer.MeshDomain.MaterialOptions.emissiveB: number ``` ###### tecs.gfx.Renderer.MeshDomain.MaterialOptions.metallic field Caller-writable. Multiplies sampled metallic and defaults to 1. ```teal tecs.gfx.Renderer.MeshDomain.MaterialOptions.metallic: number ``` ###### tecs.gfx.Renderer.MeshDomain.MaterialOptions.roughness field Caller-writable. Multiplies sampled roughness and defaults to 1. ```teal tecs.gfx.Renderer.MeshDomain.MaterialOptions.roughness: number ``` ###### tecs.gfx.Renderer.MeshDomain.MaterialOptions.normalScale field Caller-writable. Scales tangent-space normal xy and defaults to 1. ```teal tecs.gfx.Renderer.MeshDomain.MaterialOptions.normalScale: number ``` ###### tecs.gfx.Renderer.MeshDomain.MaterialOptions.occlusionStrength field Caller-writable. Scales sampled occlusion and defaults to 1. ```teal tecs.gfx.Renderer.MeshDomain.MaterialOptions.occlusionStrength: number ``` ##### tecs.gfx.Renderer.MeshDomain.ShadowOptions record Configures one directional light and its three mesh shadow cascades. ```teal record tecs.gfx.Renderer.MeshDomain.ShadowOptions is MeshShadowTuning scale: number end ``` ###### Interfaces | Interface | | --- | | [`MeshShadowTuning`](/modules/gfx/#tecs.gfx.Renderer.MeshDomain.Shadow) | ###### tecs.gfx.Renderer.MeshDomain.ShadowOptions.scale field Caller-writable. Sets every shadow-map size relative to the frame and defaults to one. It must be positive and is fixed at renderer creation. ```teal tecs.gfx.Renderer.MeshDomain.ShadowOptions.scale: number ``` ##### tecs.gfx.Renderer.MeshDomain.Shadow interface Controls one directional light and its three mesh shadow cascades at runtime. The renderer reads assignments on the next frame. Rendering raises when `distance` does not exceed the camera near plane, a light color or `intensity` is negative, `strength` is outside zero through one, `bias`, `softness`, or `depthPadding` is negative, cascade controls are outside their ranges, or the three direction fields form a zero vector. ```teal interface tecs.gfx.Renderer.MeshDomain.Shadow distance: number splitLambda: number splitBlend: number depthPadding: number directionX: number directionY: number directionZ: number r: number g: number b: number intensity: number strength: number bias: number softness: number end ``` ###### tecs.gfx.Renderer.MeshDomain.Shadow.distance field Caller-writable. Sets the maximum camera distance covered by directional shadows in world units, must exceed the camera near plane, and defaults to 100. ```teal tecs.gfx.Renderer.MeshDomain.Shadow.distance: number ``` ###### tecs.gfx.Renderer.MeshDomain.Shadow.splitLambda field Caller-writable. Blends logarithmic and uniform cascade placement from zero through one and defaults to 0.7. Larger values reserve more detail near the camera. ```teal tecs.gfx.Renderer.MeshDomain.Shadow.splitLambda: number ``` ###### tecs.gfx.Renderer.MeshDomain.Shadow.splitBlend field Caller-writable. Cross-fades each cascade boundary over this fraction of the cascade's depth range, ranges from zero through 0.5, and defaults to 0.1. ```teal tecs.gfx.Renderer.MeshDomain.Shadow.splitBlend: number ``` ###### tecs.gfx.Renderer.MeshDomain.Shadow.depthPadding field Caller-writable. Extends each light-space depth range in world units to keep off-frustum casters and receivers, must be non-negative, and defaults to 20. ```teal tecs.gfx.Renderer.MeshDomain.Shadow.depthPadding: number ``` ###### tecs.gfx.Renderer.MeshDomain.Shadow.directionX field Caller-writable. Sets the x direction light rays travel and defaults to negative 0.5. ```teal tecs.gfx.Renderer.MeshDomain.Shadow.directionX: number ``` ###### tecs.gfx.Renderer.MeshDomain.Shadow.directionY field Caller-writable. Sets the y direction light rays travel and defaults to negative one. ```teal tecs.gfx.Renderer.MeshDomain.Shadow.directionY: number ``` ###### tecs.gfx.Renderer.MeshDomain.Shadow.directionZ field Caller-writable. Sets the z direction light rays travel and defaults to negative 0.5. The three direction fields must not all be zero. ```teal tecs.gfx.Renderer.MeshDomain.Shadow.directionZ: number ``` ###### tecs.gfx.Renderer.MeshDomain.Shadow.r field Caller-writable. Sets non-negative directional-light red and defaults to one. ```teal tecs.gfx.Renderer.MeshDomain.Shadow.r: number ``` ###### tecs.gfx.Renderer.MeshDomain.Shadow.g field Caller-writable. Sets non-negative directional-light green and defaults to one. ```teal tecs.gfx.Renderer.MeshDomain.Shadow.g: number ``` ###### tecs.gfx.Renderer.MeshDomain.Shadow.b field Caller-writable. Sets non-negative directional-light blue and defaults to one. ```teal tecs.gfx.Renderer.MeshDomain.Shadow.b: number ``` ###### tecs.gfx.Renderer.MeshDomain.Shadow.intensity field Caller-writable. Scales directional-light contribution, must be non-negative, and defaults to one. ```teal tecs.gfx.Renderer.MeshDomain.Shadow.intensity: number ``` ###### tecs.gfx.Renderer.MeshDomain.Shadow.strength field Caller-writable. Sets shadow occlusion from zero to one and defaults to one. ```teal tecs.gfx.Renderer.MeshDomain.Shadow.strength: number ``` ###### tecs.gfx.Renderer.MeshDomain.Shadow.bias field Caller-writable. Sets non-negative receiver clip-depth bias and defaults to 0.0015. ```teal tecs.gfx.Renderer.MeshDomain.Shadow.bias: number ``` ###### tecs.gfx.Renderer.MeshDomain.Shadow.softness field Caller-writable. Sets the 3x3 PCF radius in texels. Zero selects one sample and the default is one. ```teal tecs.gfx.Renderer.MeshDomain.Shadow.softness: number ``` ##### tecs.gfx.Renderer.MeshDomain.SkinningOptions record Configures independently allocated GPU skeletal-deformation resources. ```teal record tecs.gfx.Renderer.MeshDomain.SkinningOptions jointCapacity: integer end ``` ###### tecs.gfx.Renderer.MeshDomain.SkinningOptions.jointCapacity field Caller-writable. Sets the total resident joint-matrix ceiling and defaults to 4,096. ```teal tecs.gfx.Renderer.MeshDomain.SkinningOptions.jointCapacity: integer ``` ##### tecs.gfx.Renderer.MeshDomain.MorphingOptions record Configures independently allocated GPU morph-deformation resources. ```teal record tecs.gfx.Renderer.MeshDomain.MorphingOptions vertexCapacity: integer weightCapacity: integer end ``` ###### tecs.gfx.Renderer.MeshDomain.MorphingOptions.vertexCapacity field Caller-writable. Sets the resident target-vertex ceiling and defaults to 1,048,576. One target on a 1,000-vertex mesh consumes 1,000. ```teal tecs.gfx.Renderer.MeshDomain.MorphingOptions.vertexCapacity: integer ``` ###### tecs.gfx.Renderer.MeshDomain.MorphingOptions.weightCapacity field Caller-writable. Sets the total per-instance weight ceiling and defaults to 65,536. ```teal tecs.gfx.Renderer.MeshDomain.MorphingOptions.weightCapacity: integer ``` ##### tecs.gfx.Renderer.MeshDomain.LightOptions record Configures independently allocated point and spot light resources. ```teal record tecs.gfx.Renderer.MeshDomain.LightOptions capacity: integer shadows: MeshLocalShadowOptions end ``` ###### tecs.gfx.Renderer.MeshDomain.LightOptions.capacity field Caller-writable. Sets the extracted 3D local-light ceiling and defaults to 256. Lights beyond it are ignored in stable extraction order. ```teal tecs.gfx.Renderer.MeshDomain.LightOptions.capacity: integer ``` ###### tecs.gfx.Renderer.MeshDomain.LightOptions.shadows field Caller-writable. Enables the independently allocated local-shadow atlas. Nil keeps its resources and shader variants absent. ```teal tecs.gfx.Renderer.MeshDomain.LightOptions.shadows: MeshLocalShadowOptions ``` ##### tecs.gfx.Renderer.MeshDomain.LocalShadowOptions record Configures point and spot shadows inside the optional local-light lane. ```teal record tecs.gfx.Renderer.MeshDomain.LocalShadowOptions capacity: integer size: integer bias: number softness: number end ``` ###### tecs.gfx.Renderer.MeshDomain.LocalShadowOptions.capacity field Caller-writable. Sets how many flagged local lights may cast shadows and defaults to four. Selection follows stable light extraction order. ```teal tecs.gfx.Renderer.MeshDomain.LocalShadowOptions.capacity: integer ``` ###### tecs.gfx.Renderer.MeshDomain.LocalShadowOptions.size field Caller-writable. Sets one atlas cell edge in pixels and defaults to 256. A point light consumes six cells and a spot light consumes one. ```teal tecs.gfx.Renderer.MeshDomain.LocalShadowOptions.size: integer ``` ###### tecs.gfx.Renderer.MeshDomain.LocalShadowOptions.bias field Caller-writable. Sets normalized receiver depth bias and defaults to 0.002. ```teal tecs.gfx.Renderer.MeshDomain.LocalShadowOptions.bias: number ``` ###### tecs.gfx.Renderer.MeshDomain.LocalShadowOptions.softness field Caller-writable. Sets the 3x3 PCF radius in texels. Zero selects one sample and the default is one. ```teal tecs.gfx.Renderer.MeshDomain.LocalShadowOptions.softness: number ``` ##### tecs.gfx.Renderer.MeshDomain.FogOptions record Enables the mesh-only fog shader and G-buffer variants. ```teal record tecs.gfx.Renderer.MeshDomain.FogOptions is MeshFogTuning end ``` ###### Interfaces | Interface | | --- | | [`MeshFogTuning`](/modules/gfx/#tecs.gfx.Renderer.MeshDomain.Fog) | ##### tecs.gfx.Renderer.MeshDomain.Fog interface Controls linear camera-distance fog at runtime. ```teal interface tecs.gfx.Renderer.MeshDomain.Fog start: number finish: number r: number g: number b: number end ``` ###### tecs.gfx.Renderer.MeshDomain.Fog.start field Caller-writable. Sets the distance where fog begins and defaults to 20. ```teal tecs.gfx.Renderer.MeshDomain.Fog.start: number ``` ###### tecs.gfx.Renderer.MeshDomain.Fog.finish field Caller-writable. Sets the distance where fog is complete, must exceed `start`, and defaults to 100. ```teal tecs.gfx.Renderer.MeshDomain.Fog.finish: number ``` ###### tecs.gfx.Renderer.MeshDomain.Fog.r field Caller-writable. Sets fog red from zero through one. ```teal tecs.gfx.Renderer.MeshDomain.Fog.r: number ``` ###### tecs.gfx.Renderer.MeshDomain.Fog.g field Caller-writable. Sets fog green from zero through one. ```teal tecs.gfx.Renderer.MeshDomain.Fog.g: number ``` ###### tecs.gfx.Renderer.MeshDomain.Fog.b field Caller-writable. Sets fog blue from zero through one. ```teal tecs.gfx.Renderer.MeshDomain.Fog.b: number ``` ##### tecs.gfx.Renderer.MeshDomain.ProbeOptions record Enables the mesh-only ambient-cube probe shader variants. ```teal record tecs.gfx.Renderer.MeshDomain.ProbeOptions is MeshProbeTuning end ``` ###### Interfaces | Interface | | --- | | [`MeshProbeTuning`](/modules/gfx/#tecs.gfx.Renderer.MeshDomain.Probe) | ##### tecs.gfx.Renderer.MeshDomain.Probe interface Controls one mesh-only ambient-cube light probe at runtime. Each RGB triplet is irradiance arriving from the named world-space axis. The shader blends the six faces by the squared components of the surface normal. Values must be non-negative and may exceed one for HDR lighting. ```teal interface tecs.gfx.Renderer.MeshDomain.Probe positiveX: {number} negativeX: {number} positiveY: {number} negativeY: {number} positiveZ: {number} negativeZ: {number} intensity: number end ``` ###### tecs.gfx.Renderer.MeshDomain.Probe.positiveX field Caller-writable. Sets irradiance arriving from positive world X. ```teal tecs.gfx.Renderer.MeshDomain.Probe.positiveX: {number} ``` ###### tecs.gfx.Renderer.MeshDomain.Probe.negativeX field Caller-writable. Sets irradiance arriving from negative world X. ```teal tecs.gfx.Renderer.MeshDomain.Probe.negativeX: {number} ``` ###### tecs.gfx.Renderer.MeshDomain.Probe.positiveY field Caller-writable. Sets irradiance arriving from positive world Y. ```teal tecs.gfx.Renderer.MeshDomain.Probe.positiveY: {number} ``` ###### tecs.gfx.Renderer.MeshDomain.Probe.negativeY field Caller-writable. Sets irradiance arriving from negative world Y. ```teal tecs.gfx.Renderer.MeshDomain.Probe.negativeY: {number} ``` ###### tecs.gfx.Renderer.MeshDomain.Probe.positiveZ field Caller-writable. Sets irradiance arriving from positive world Z. ```teal tecs.gfx.Renderer.MeshDomain.Probe.positiveZ: {number} ``` ###### tecs.gfx.Renderer.MeshDomain.Probe.negativeZ field Caller-writable. Sets irradiance arriving from negative world Z. ```teal tecs.gfx.Renderer.MeshDomain.Probe.negativeZ: {number} ``` ###### tecs.gfx.Renderer.MeshDomain.Probe.intensity field Caller-writable. Scales all six faces and defaults to one. ```teal tecs.gfx.Renderer.MeshDomain.Probe.intensity: number ``` ##### tecs.gfx.Renderer.MeshDomain.EnvironmentOptions record Enables independently allocated specular-environment resources. ```teal record tecs.gfx.Renderer.MeshDomain.EnvironmentOptions is MeshEnvironmentTuning size: integer end ``` ###### Interfaces | Interface | | --- | | [`MeshEnvironmentTuning`](/modules/gfx/#tecs.gfx.Renderer.MeshDomain.Environment) | ###### tecs.gfx.Renderer.MeshDomain.EnvironmentOptions.size field Caller-writable. Sets every square face's width and height in pixels, defaults to 64, and is fixed at renderer creation. ```teal tecs.gfx.Renderer.MeshDomain.EnvironmentOptions.size: integer ``` ##### tecs.gfx.Renderer.MeshDomain.Environment interface Controls one mesh-only specular environment at runtime. The first implementation selects from the GPU-generated mip chain by material roughness and uses an analytic split-sum BRDF approximation. It deliberately keeps the six-face upload contract separate from ordinary material textures so enabling it changes no material residency limits. ```teal interface tecs.gfx.Renderer.MeshDomain.Environment intensity: number skyboxIntensity: number rotation: number end ``` ###### tecs.gfx.Renderer.MeshDomain.Environment.intensity field Caller-writable. Scales reflected environment light, must be non-negative, and defaults to one. ```teal tecs.gfx.Renderer.MeshDomain.Environment.intensity: number ``` ###### tecs.gfx.Renderer.MeshDomain.Environment.skyboxIntensity field Caller-writable. Scales the environment shown behind geometry, must be non-negative, and defaults to one. Zero hides the sky without removing reflected environment light. ```teal tecs.gfx.Renderer.MeshDomain.Environment.skyboxIntensity: number ``` ###### tecs.gfx.Renderer.MeshDomain.Environment.rotation field Caller-writable. Rotates the environment around world Y in radians and defaults to zero. ```teal tecs.gfx.Renderer.MeshDomain.Environment.rotation: number ``` ##### tecs.gfx.Renderer.MeshDomain.EnvironmentFaces record Supplies the six decoded RGBA8 faces of one specular environment. Faces use the conventional positive X, negative X, positive Y, negative Y, positive Z, negative Z order. `registerEnvironment` consumes every image after validating the complete set. ```teal record tecs.gfx.Renderer.MeshDomain.EnvironmentFaces positiveX: assets.Image negativeX: assets.Image positiveY: assets.Image negativeY: assets.Image positiveZ: assets.Image negativeZ: assets.Image end ``` ###### tecs.gfx.Renderer.MeshDomain.EnvironmentFaces.positiveX field Caller-writable. Supplies the face viewed along positive world X. ```teal tecs.gfx.Renderer.MeshDomain.EnvironmentFaces.positiveX: assets.Image ``` ###### tecs.gfx.Renderer.MeshDomain.EnvironmentFaces.negativeX field Caller-writable. Supplies the face viewed along negative world X. ```teal tecs.gfx.Renderer.MeshDomain.EnvironmentFaces.negativeX: assets.Image ``` ###### tecs.gfx.Renderer.MeshDomain.EnvironmentFaces.positiveY field Caller-writable. Supplies the face viewed along positive world Y. ```teal tecs.gfx.Renderer.MeshDomain.EnvironmentFaces.positiveY: assets.Image ``` ###### tecs.gfx.Renderer.MeshDomain.EnvironmentFaces.negativeY field Caller-writable. Supplies the face viewed along negative world Y. ```teal tecs.gfx.Renderer.MeshDomain.EnvironmentFaces.negativeY: assets.Image ``` ###### tecs.gfx.Renderer.MeshDomain.EnvironmentFaces.positiveZ field Caller-writable. Supplies the face viewed along positive world Z. ```teal tecs.gfx.Renderer.MeshDomain.EnvironmentFaces.positiveZ: assets.Image ``` ###### tecs.gfx.Renderer.MeshDomain.EnvironmentFaces.negativeZ field Caller-writable. Supplies the face viewed along negative world Z. ```teal tecs.gfx.Renderer.MeshDomain.EnvironmentFaces.negativeZ: assets.Image ``` ##### tecs.gfx.Renderer.MeshDomain.SSAOOptions type ```teal type tecs.gfx.Renderer.MeshDomain.SSAOOptions = Deferred.SSAOOptions ``` ##### tecs.gfx.Renderer.MeshDomain.SSAO type ```teal type tecs.gfx.Renderer.MeshDomain.SSAO = Deferred.SSAO ``` ##### tecs.gfx.Renderer.MeshDomain.RegisteredPrimitive record Contains the component bundle for one model primitive instance. ```teal record tecs.gfx.Renderer.MeshDomain.RegisteredPrimitive transform: ecs.Transform3D mesh: components.Mesh bounds: components.Bounds3D material: components.MeshMaterial skin: components.MeshSkin morph: components.MeshMorph end ``` ###### tecs.gfx.Renderer.MeshDomain.RegisteredPrimitive.transform field Caller-writable. Contains the sampled world transform. ```teal tecs.gfx.Renderer.MeshDomain.RegisteredPrimitive.transform: ecs.Transform3D ``` ###### tecs.gfx.Renderer.MeshDomain.RegisteredPrimitive.mesh field Caller-writable. Selects resident geometry. ```teal tecs.gfx.Renderer.MeshDomain.RegisteredPrimitive.mesh: components.Mesh ``` ###### tecs.gfx.Renderer.MeshDomain.RegisteredPrimitive.bounds field Caller-writable. Supplies the local bound, which must enclose every animated pose. ```teal tecs.gfx.Renderer.MeshDomain.RegisteredPrimitive.bounds: components.Bounds3D ``` ###### tecs.gfx.Renderer.MeshDomain.RegisteredPrimitive.material field Caller-writable. Selects resident PBR material data. ```teal tecs.gfx.Renderer.MeshDomain.RegisteredPrimitive.material: components.MeshMaterial ``` ###### tecs.gfx.Renderer.MeshDomain.RegisteredPrimitive.skin field Caller-writable. Selects this instance's joint palette, or nil for rigid geometry. ```teal tecs.gfx.Renderer.MeshDomain.RegisteredPrimitive.skin: components.MeshSkin ``` ###### tecs.gfx.Renderer.MeshDomain.RegisteredPrimitive.morph field Caller-writable. Selects this instance's morph weights, or nil for geometry without morph targets. ```teal tecs.gfx.Renderer.MeshDomain.RegisteredPrimitive.morph: components.MeshMorph ``` ##### tecs.gfx.Renderer.MeshDomain.Model3D record ```teal record tecs.gfx.Renderer.MeshDomain.Model3D is ModelOwner record Primitive transform: ecs.Transform3D mesh: components.Mesh bounds: components.Bounds3D material: components.MeshMaterial skin: components.MeshSkin morph: components.MeshMorph end record Instance primitives: {Primitive} transform: ecs.Transform3D animation: integer time: number speed: number loop: boolean playing: boolean bind: function( self, world: World, primitive: integer, entity: integer ) play: function( self, animation: string | integer, options: PlayOptions ) sample: function( self, animation: string | integer, time: number, loop: boolean ) unbind: function(self, primitive: integer) update: function(self, dt: number) end record PlayOptions speed: number loop: boolean playing: boolean end animationIndex: function(self, name: string): integer newInstance: function(self): Instance end ``` ###### Interfaces | Interface | | --- | | [`ModelOwner`](/modules/gfx/#tecs.gfx.ModelOwner) | ###### tecs.gfx.Renderer.MeshDomain.Model3D.Primitive record Contains the component bundle for one model primitive instance. ```teal record tecs.gfx.Renderer.MeshDomain.Model3D.Primitive transform: ecs.Transform3D mesh: components.Mesh bounds: components.Bounds3D material: components.MeshMaterial skin: components.MeshSkin morph: components.MeshMorph end ``` ###### tecs.gfx.Renderer.MeshDomain.Model3D.Primitive.transform field Caller-writable. Contains the sampled world transform. ```teal tecs.gfx.Renderer.MeshDomain.Model3D.Primitive.transform: ecs.Transform3D ``` ###### tecs.gfx.Renderer.MeshDomain.Model3D.Primitive.mesh field Caller-writable. Selects resident geometry. ```teal tecs.gfx.Renderer.MeshDomain.Model3D.Primitive.mesh: components.Mesh ``` ###### tecs.gfx.Renderer.MeshDomain.Model3D.Primitive.bounds field Caller-writable. Supplies the local bound, which must enclose every animated pose. ```teal tecs.gfx.Renderer.MeshDomain.Model3D.Primitive.bounds: components.Bounds3D ``` ###### tecs.gfx.Renderer.MeshDomain.Model3D.Primitive.material field Caller-writable. Selects resident PBR material data. ```teal tecs.gfx.Renderer.MeshDomain.Model3D.Primitive.material: components.MeshMaterial ``` ###### tecs.gfx.Renderer.MeshDomain.Model3D.Primitive.skin field Caller-writable. Selects this instance's joint palette, or nil for rigid geometry. ```teal tecs.gfx.Renderer.MeshDomain.Model3D.Primitive.skin: components.MeshSkin ``` ###### tecs.gfx.Renderer.MeshDomain.Model3D.Primitive.morph field Caller-writable. Selects this instance's morph weights, or nil for geometry without morph targets. ```teal tecs.gfx.Renderer.MeshDomain.Model3D.Primitive.morph: components.MeshMorph ``` ###### tecs.gfx.Renderer.MeshDomain.Model3D.Instance record Plays one independently posed copy of a resident model. ```teal record tecs.gfx.Renderer.MeshDomain.Model3D.Instance primitives: {Primitive} transform: ecs.Transform3D animation: integer time: number speed: number loop: boolean playing: boolean bind: function( self, world: World, primitive: integer, entity: integer ) play: function( self, animation: string | integer, options: PlayOptions ) sample: function( self, animation: string | integer, time: number, loop: boolean ) unbind: function(self, primitive: integer) update: function(self, dt: number) end ``` ###### tecs.gfx.Renderer.MeshDomain.Model3D.Instance.primitives field Read-only. Contains this instance's spawnable primitive bundles. ```teal tecs.gfx.Renderer.MeshDomain.Model3D.Instance.primitives: {Primitive} ``` ###### tecs.gfx.Renderer.MeshDomain.Model3D.Instance.transform field Caller-writable. Places the complete sampled model in world space, or nil to preserve the file's authored placement without placement work. Sampling composes this transform after the authored node hierarchy. ```teal tecs.gfx.Renderer.MeshDomain.Model3D.Instance.transform: ecs.Transform3D ``` ###### tecs.gfx.Renderer.MeshDomain.Model3D.Instance.animation field Read-only. Reports the selected one-based clip, or zero before `play`. ```teal tecs.gfx.Renderer.MeshDomain.Model3D.Instance.animation: integer ``` ###### tecs.gfx.Renderer.MeshDomain.Model3D.Instance.time field Read-only. Reports the current clip time in seconds. ```teal tecs.gfx.Renderer.MeshDomain.Model3D.Instance.time: number ``` ###### tecs.gfx.Renderer.MeshDomain.Model3D.Instance.speed field Caller-writable. Multiplies elapsed time. Negative values raise on `update`. ```teal tecs.gfx.Renderer.MeshDomain.Model3D.Instance.speed: number ``` ###### tecs.gfx.Renderer.MeshDomain.Model3D.Instance.loop field Caller-writable. Controls whether playback wraps at the duration. ```teal tecs.gfx.Renderer.MeshDomain.Model3D.Instance.loop: boolean ``` ###### tecs.gfx.Renderer.MeshDomain.Model3D.Instance.playing field Caller-writable. Controls whether `update` advances playback. ```teal tecs.gfx.Renderer.MeshDomain.Model3D.Instance.playing: boolean ``` ###### tecs.gfx.Renderer.MeshDomain.Model3D.Instance:bind Instance Binds one primitive to an existing entity's `Transform3D`. ```teal function tecs.gfx.Renderer.MeshDomain.Model3D.Instance.bind( self, world: World, primitive: integer, entity: integer ) ``` ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Instance` | | | `world` | [`World`](/modules/ecs/#tecs.World) | The caller supplies the entity's world. Every binding on one instance must use the same world. | | `primitive` | `integer` | The caller supplies a one-based primitive index. | | `entity` | `integer` | The caller supplies a live entity carrying `Transform3D`. | ###### Returns None. ###### tecs.gfx.Renderer.MeshDomain.Model3D.Instance:play Instance Selects and restarts a clip. ```teal function tecs.gfx.Renderer.MeshDomain.Model3D.Instance.play( self, animation: string | integer, options: PlayOptions ) ``` ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Instance` | | | `animation` | string | integer | The caller supplies a one-based index or clip name. | | `options` | [`PlayOptions`](/modules/gfx/#tecs.gfx.Renderer.MeshDomain.Model3D.PlayOptions) | Omitted fields default to speed one, looping, and immediate playback. | ###### Returns None. ###### tecs.gfx.Renderer.MeshDomain.Model3D.Instance:sample Instance Samples a clip at an explicit time without allocating. ```teal function tecs.gfx.Renderer.MeshDomain.Model3D.Instance.sample( self, animation: string | integer, time: number, loop: boolean ) ``` ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Instance` | | | `animation` | string | integer | The caller supplies a one-based index or clip name. | | `time` | `number` | The caller supplies seconds. Negative values clamp to zero. | | `loop` | `boolean` | Whether time wraps at the duration. Defaults to false. | ###### Returns None. ###### tecs.gfx.Renderer.MeshDomain.Model3D.Instance:unbind Instance Removes one primitive's entity binding. ```teal function tecs.gfx.Renderer.MeshDomain.Model3D.Instance.unbind( self, primitive: integer ) ``` ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Instance` | | | `primitive` | `integer` | The caller supplies a one-based primitive index. | ###### Returns None. ###### tecs.gfx.Renderer.MeshDomain.Model3D.Instance:update Instance Advances and samples the selected clip. ```teal function tecs.gfx.Renderer.MeshDomain.Model3D.Instance.update( self, dt: number ) ``` ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Instance` | | | `dt` | `number` | The caller supplies non-negative elapsed seconds. | ###### Returns None. ###### tecs.gfx.Renderer.MeshDomain.Model3D.PlayOptions record Configures `Instance:play`. ```teal record tecs.gfx.Renderer.MeshDomain.Model3D.PlayOptions speed: number loop: boolean playing: boolean end ``` ###### tecs.gfx.Renderer.MeshDomain.Model3D.PlayOptions.speed field Caller-writable. Multiplies elapsed time and defaults to one. Negative values raise. ```teal tecs.gfx.Renderer.MeshDomain.Model3D.PlayOptions.speed: number ``` ###### tecs.gfx.Renderer.MeshDomain.Model3D.PlayOptions.loop field Caller-writable. Restarts after the clip duration and defaults to true. ```teal tecs.gfx.Renderer.MeshDomain.Model3D.PlayOptions.loop: boolean ``` ###### tecs.gfx.Renderer.MeshDomain.Model3D.PlayOptions.playing field Caller-writable. Starts advancing immediately and defaults to true. ```teal tecs.gfx.Renderer.MeshDomain.Model3D.PlayOptions.playing: boolean ``` ###### tecs.gfx.Renderer.MeshDomain.Model3D:animationIndex Instance ```teal function tecs.gfx.Renderer.MeshDomain.Model3D.animationIndex( self, name: string ): integer ``` ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Model3D` | | | `name` | `string` | | ###### Returns | Type | Description | | --- | --- | | `integer` | | ###### tecs.gfx.Renderer.MeshDomain.Model3D:newInstance Instance Creates an independently animated instance. ```teal function tecs.gfx.Renderer.MeshDomain.Model3D.newInstance( self ): Instance ``` ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Model3D` | | ###### Returns | Type | Description | | --- | --- | | [`Instance`](/modules/gfx/#tecs.gfx.Renderer.MeshDomain.Model3D.Instance) | Returns reusable primitive templates plus private joint palettes and morph-weight vectors. | ##### tecs.gfx.Renderer.MeshDomain.MATERIAL_METALLIC_ROUGHNESS field Read-only. Selects metallic-roughness PBR material dispatch. ```teal tecs.gfx.Renderer.MeshDomain.MATERIAL_METALLIC_ROUGHNESS: integer ``` ##### tecs.gfx.Renderer.MeshDomain.MATERIAL_UNLIT field Read-only. Selects unlit material dispatch. ```teal tecs.gfx.Renderer.MeshDomain.MATERIAL_UNLIT: integer ``` ##### tecs.gfx.Renderer.MeshDomain.MATERIAL_LAMBERT field Read-only. Selects diffuse-only Lambert material dispatch. ```teal tecs.gfx.Renderer.MeshDomain.MATERIAL_LAMBERT: integer ``` ##### tecs.gfx.Renderer.MeshDomain.ALPHA_OPAQUE field Read-only. Selects an opaque material that ignores base alpha. ```teal tecs.gfx.Renderer.MeshDomain.ALPHA_OPAQUE: integer ``` ##### tecs.gfx.Renderer.MeshDomain.ALPHA_MASK field Read-only. Selects an opaque material that discards below `alphaCutoff`. ```teal tecs.gfx.Renderer.MeshDomain.ALPHA_MASK: integer ``` ##### tecs.gfx.Renderer.MeshDomain.ALPHA_BLEND field Read-only. Selects a material drawn in the sorted forward lane. ```teal tecs.gfx.Renderer.MeshDomain.ALPHA_BLEND: integer ``` ##### tecs.gfx.Renderer.MeshDomain.TEXTURE_RGBA8 field Read-only. Selects decoded RGBA8 mesh textures. ```teal tecs.gfx.Renderer.MeshDomain.TEXTURE_RGBA8: integer ``` ##### tecs.gfx.Renderer.MeshDomain.TEXTURE_BC3 field Read-only. Selects imported BC3 mesh textures with complete mip chains. ```teal tecs.gfx.Renderer.MeshDomain.TEXTURE_BC3: integer ``` ##### tecs.gfx.Renderer.MeshDomain.camera field Caller-writable. Controls the perspective view used by this domain. ```teal tecs.gfx.Renderer.MeshDomain.camera: Camera3D ``` ##### tecs.gfx.Renderer.MeshDomain.capacity field Read-only. Reports the mesh-instance capacity fixed at creation. ```teal tecs.gfx.Renderer.MeshDomain.capacity: integer ``` ##### tecs.gfx.Renderer.MeshDomain.vertexCapacity field Read-only. Reports the vertex capacity fixed at creation. ```teal tecs.gfx.Renderer.MeshDomain.vertexCapacity: integer ``` ##### tecs.gfx.Renderer.MeshDomain.indexCapacity field Read-only. Reports the 32-bit index capacity fixed at creation. ```teal tecs.gfx.Renderer.MeshDomain.indexCapacity: integer ``` ##### tecs.gfx.Renderer.MeshDomain.meshCount field Read-only. Reports meshes registered for immutable residency. ```teal tecs.gfx.Renderer.MeshDomain.meshCount: integer ``` ##### tecs.gfx.Renderer.MeshDomain.vertexCount field Read-only. Reports vertices registered for immutable residency. ```teal tecs.gfx.Renderer.MeshDomain.vertexCount: integer ``` ##### tecs.gfx.Renderer.MeshDomain.indexCount field Read-only. Reports indices registered for immutable residency. ```teal tecs.gfx.Renderer.MeshDomain.indexCount: integer ``` ##### tecs.gfx.Renderer.MeshDomain.materialCount field Read-only. Reports resident material slots, including the neutral built-in slot zero. ```teal tecs.gfx.Renderer.MeshDomain.materialCount: integer ``` ##### tecs.gfx.Renderer.MeshDomain.textureCount field Read-only. Reports unique images uploaded to the mesh texture array. ```teal tecs.gfx.Renderer.MeshDomain.textureCount: integer ``` ##### tecs.gfx.Renderer.MeshDomain.jointCount field Read-only. Reports resident joint matrices in the optional skin lane. ```teal tecs.gfx.Renderer.MeshDomain.jointCount: integer ``` ##### tecs.gfx.Renderer.MeshDomain.morphVertexCount field Read-only. Reports resident target vertices in the optional morph lane. ```teal tecs.gfx.Renderer.MeshDomain.morphVertexCount: integer ``` ##### tecs.gfx.Renderer.MeshDomain.morphWeightCount field Read-only. Reports resident per-instance weights in the optional morph lane. ```teal tecs.gfx.Renderer.MeshDomain.morphWeightCount: integer ``` ##### tecs.gfx.Renderer.MeshDomain.transparency field Read-only. Reports whether this domain owns the optional transparent command and pipeline resources. ```teal tecs.gfx.Renderer.MeshDomain.transparency: boolean ``` ##### tecs.gfx.Renderer.MeshDomain.doubleSided field Read-only. Reports whether this domain owns optional double-sided command and pipeline resources. ```teal tecs.gfx.Renderer.MeshDomain.doubleSided: boolean ``` ##### tecs.gfx.Renderer.MeshDomain.mipmaps field Read-only. Reports whether mesh images own complete mip chains. ```teal tecs.gfx.Renderer.MeshDomain.mipmaps: boolean ``` ##### tecs.gfx.Renderer.MeshDomain.textureFormat field Read-only. Reports the selected `TEXTURE_*` storage format. ```teal tecs.gfx.Renderer.MeshDomain.textureFormat: integer ``` ##### tecs.gfx.Renderer.MeshDomain.shadows field Read-only. Reports whether this domain owns mesh-shadow resources. ```teal tecs.gfx.Renderer.MeshDomain.shadows: boolean ``` ##### tecs.gfx.Renderer.MeshDomain.skinning field Read-only. Reports whether this domain owns GPU skinning resources. ```teal tecs.gfx.Renderer.MeshDomain.skinning: boolean ``` ##### tecs.gfx.Renderer.MeshDomain.morphing field Read-only. Reports whether this domain owns GPU morph resources. ```teal tecs.gfx.Renderer.MeshDomain.morphing: boolean ``` ##### tecs.gfx.Renderer.MeshDomain.localLights field Read-only. Reports whether point and spot mesh lights are enabled. ```teal tecs.gfx.Renderer.MeshDomain.localLights: boolean ``` ##### tecs.gfx.Renderer.MeshDomain.localShadows field Read-only. Reports whether the local-light shadow atlas is enabled. ```teal tecs.gfx.Renderer.MeshDomain.localShadows: boolean ``` ##### tecs.gfx.Renderer.MeshDomain.localShadowCount field Read-only. Reports shadowed local lights selected for the last frame. ```teal tecs.gfx.Renderer.MeshDomain.localShadowCount: integer ``` ##### tecs.gfx.Renderer.MeshDomain.lightCapacity field Read-only. Reports the fixed local-light capacity, or zero when the lane is disabled. ```teal tecs.gfx.Renderer.MeshDomain.lightCapacity: integer ``` ##### tecs.gfx.Renderer.MeshDomain.lightCount field Read-only. Reports local lights extracted for the last frame. ```teal tecs.gfx.Renderer.MeshDomain.lightCount: integer ``` ##### tecs.gfx.Renderer.MeshDomain.vertexColors field Read-only. Reports whether this domain owns vertex-color resources. ```teal tecs.gfx.Renderer.MeshDomain.vertexColors: boolean ``` ##### tecs.gfx.Renderer.MeshDomain.fogging field Read-only. Reports whether mesh fog shader variants are enabled. ```teal tecs.gfx.Renderer.MeshDomain.fogging: boolean ``` ##### tecs.gfx.Renderer.MeshDomain.probing field Read-only. Reports whether ambient-cube probe variants are enabled. ```teal tecs.gfx.Renderer.MeshDomain.probing: boolean ``` ##### tecs.gfx.Renderer.MeshDomain.environmentLighting field Read-only. Reports whether specular-environment resources are enabled. ```teal tecs.gfx.Renderer.MeshDomain.environmentLighting: boolean ``` ##### tecs.gfx.Renderer.MeshDomain.environmentSize field Read-only. Reports the fixed square environment face size, or zero while the lane is disabled. ```teal tecs.gfx.Renderer.MeshDomain.environmentSize: integer ``` ##### tecs.gfx.Renderer.MeshDomain.environmentReady field Read-only. Reports whether all six environment faces were uploaded. ```teal tecs.gfx.Renderer.MeshDomain.environmentReady: boolean ``` ##### tecs.gfx.Renderer.MeshDomain.ssao field Caller-writable. Controls enabled screen-space ambient occlusion. Nil while the domain omits `ssao`. ```teal tecs.gfx.Renderer.MeshDomain.ssao: Deferred.SSAO ``` ##### tecs.gfx.Renderer.MeshDomain.shadow field Caller-writable. Controls the enabled directional light and shadow cascades. Nil while shadows are disabled. ```teal tecs.gfx.Renderer.MeshDomain.shadow: MeshShadowTuning ``` ##### tecs.gfx.Renderer.MeshDomain.fog field Caller-writable. Controls enabled mesh fog. Nil while fog is disabled. ```teal tecs.gfx.Renderer.MeshDomain.fog: MeshFogTuning ``` ##### tecs.gfx.Renderer.MeshDomain.probe field Caller-writable. Controls enabled diffuse ambient-cube lighting. Nil while the probe is disabled. ```teal tecs.gfx.Renderer.MeshDomain.probe: MeshProbeTuning ``` ##### tecs.gfx.Renderer.MeshDomain.environment field Caller-writable. Controls enabled specular environment lighting. Nil while the environment lane is disabled. ```teal tecs.gfx.Renderer.MeshDomain.environment: MeshEnvironmentTuning ``` ##### tecs.gfx.Renderer.MeshDomain:material Instance Returns a component for material data already registered by name. ```teal function tecs.gfx.Renderer.MeshDomain.material( self, name: string ): components.MeshMaterial ``` ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `MeshDomain` | | | `name` | `string` | The caller supplies the stable registered name. | ###### Returns | Type | Description | | --- | --- | | [`components.MeshMaterial`](/modules/gfx/#tecs.gfx.MeshMaterial) | Returns a material component carrying the resident slot. | ##### tecs.gfx.Renderer.MeshDomain:mesh Instance Returns components for geometry already registered under a name. ```teal function tecs.gfx.Renderer.MeshDomain.mesh( self, name: string ): components.Mesh, components.Bounds3D ``` ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `MeshDomain` | | | `name` | `string` | The caller supplies the registered mesh name. | ###### Returns | Type | Description | | --- | --- | | [`components.Mesh`](/modules/gfx/#tecs.gfx.Mesh) | Returns a mesh component carrying the resident slot. | | [`components.Bounds3D`](/modules/gfx/#tecs.gfx.Bounds3D) | Returns its asset-derived local bounding sphere. | ##### tecs.gfx.Renderer.MeshDomain:registerEnvironment Instance Replaces all six faces of the enabled specular environment. Every face must be a square RGBA8 image of `environmentSize` pixels. Validation happens before any upload; a successful call consumes all six image holds and regenerates the roughness mip chain. ```teal function tecs.gfx.Renderer.MeshDomain.registerEnvironment( self, faces: MeshEnvironmentFaces ) ``` ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `MeshDomain` | | | `faces` | [`MeshEnvironmentFaces`](/modules/gfx/#tecs.gfx.Renderer.MeshDomain.EnvironmentFaces) | The caller supplies one decoded image per world-space axis. | ###### Returns None. ##### tecs.gfx.Renderer.MeshDomain:registerMaterial Instance Registers one immutable mesh material and returns its ECS reference. Registering the same normalized name again returns the original slot. Texture fields are integer identities returned by `registerTexture`; zero selects the semantic fallback without consuming a texture layer. ```teal function tecs.gfx.Renderer.MeshDomain.registerMaterial( self, options: MeshMaterialOptions ): components.MeshMaterial ``` ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `MeshDomain` | | | `options` | [`MeshMaterialOptions`](/modules/gfx/#tecs.gfx.Renderer.MeshDomain.MaterialOptions) | The caller supplies a stable name and optional PBR inputs. | ###### Returns | Type | Description | | --- | --- | | [`components.MeshMaterial`](/modules/gfx/#tecs.gfx.MeshMaterial) | Returns a material component ready to spawn. | ##### tecs.gfx.Renderer.MeshDomain:registerMesh Instance Registers immutable CPU geometry in this domain. ```teal function tecs.gfx.Renderer.MeshDomain.registerMesh( self, mesh: assets.Mesh ): components.Mesh, components.Bounds3D ``` ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `MeshDomain` | | | `mesh` | [`assets.Mesh`](/modules/assets/#tecs.assets.Mesh) | The caller supplies CPU geometry that this call consumes. | ###### Returns | Type | Description | | --- | --- | | [`components.Mesh`](/modules/gfx/#tecs.gfx.Mesh) | Returns a mesh component ready to spawn. | | [`components.Bounds3D`](/modules/gfx/#tecs.gfx.Bounds3D) | Returns its asset-derived local bounding sphere. | ##### tecs.gfx.Renderer.MeshDomain:registerModel Instance Registers shared residency for one decoded glTF model. ```teal function tecs.gfx.Renderer.MeshDomain.registerModel( self, model: assets.Model ): Model3D ``` ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `MeshDomain` | | | `model` | [`assets.Model`](/modules/assets/#tecs.assets.Model) | The caller supplies model data that this call consumes. | ###### Returns | Type | Description | | --- | --- | | [`Model3D`](/modules/gfx/#tecs.gfx.Renderer.MeshDomain.Model3D) | Returns resident resources that create independent instances. | ##### tecs.gfx.Renderer.MeshDomain:registerMorph Instance Registers one fixed-size morph-weight vector. ```teal function tecs.gfx.Renderer.MeshDomain.registerMorph( self, name: string, weights: {number} ): components.MeshMorph ``` ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `MeshDomain` | | | `name` | `string` | The caller supplies a stable non-empty weight-vector name. | | `weights` | `{number}` | The caller supplies one finite value per target. | ###### Returns | Type | Description | | --- | --- | | [`components.MeshMorph`](/modules/gfx/#tecs.gfx.MeshMorph) | Returns a morph component ready to spawn. | ##### tecs.gfx.Renderer.MeshDomain:registerSkin Instance Registers one fixed-size joint palette for GPU deformation. ```teal function tecs.gfx.Renderer.MeshDomain.registerSkin( self, name: string, matrices: {number} ): components.MeshSkin ``` ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `MeshDomain` | | | `name` | `string` | The caller supplies a stable non-empty palette name. | | `matrices` | `{number}` | The caller supplies sixteen column-major floats per joint. | ###### Returns | Type | Description | | --- | --- | | [`components.MeshSkin`](/modules/gfx/#tecs.gfx.MeshSkin) | Returns a palette component ready to spawn. | ##### tecs.gfx.Renderer.MeshDomain:registerTexture Instance Uploads one decoded image into mesh-domain texture residency. Images share one linearly filtered array. Registration consumes the caller's image hold and returns a compact integer used by material options. Texture count is independent from triangle and instance counts. ```teal function tecs.gfx.Renderer.MeshDomain.registerTexture( self, image: assets.Image ): integer ``` ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `MeshDomain` | | | `image` | [`assets.Image`](/modules/assets/#tecs.assets.Image) | The caller supplies decoded pixels that this call consumes. | ###### Returns | Type | Description | | --- | --- | | `integer` | Returns a positive texture identity local to this domain. | ##### tecs.gfx.Renderer.MeshDomain:updateMorph Instance Replaces every value in a registered morph-weight vector. ```teal function tecs.gfx.Renderer.MeshDomain.updateMorph( self, morph: components.MeshMorph, weights: {number} ) ``` ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `MeshDomain` | | | `morph` | [`components.MeshMorph`](/modules/gfx/#tecs.gfx.MeshMorph) | The caller supplies the component returned by `registerMorph`. | | `weights` | `{number}` | The caller supplies the same count used at registration. | ###### Returns None. ##### tecs.gfx.Renderer.MeshDomain:updateSkin Instance Replaces every matrix in a registered palette before the next frame. ```teal function tecs.gfx.Renderer.MeshDomain.updateSkin( self, skin: components.MeshSkin, matrices: {number} ) ``` ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `MeshDomain` | | | `skin` | [`components.MeshSkin`](/modules/gfx/#tecs.gfx.MeshSkin) | The caller supplies the component returned by `registerSkin`. | | `matrices` | `{number}` | The caller supplies the same matrix count used at registration. | ###### Returns None. #### tecs.gfx.Renderer.MeshOptions record ```teal record tecs.gfx.Renderer.MeshOptions capacity: integer vertexCapacity: integer indexCapacity: integer materialCapacity: integer textureWidth: integer textureHeight: integer textureLayers: integer packTextures: boolean mipmaps: boolean textureFormat: integer transparency: boolean doubleSided: boolean shadows: MeshShadowOptions skinning: MeshSkinningOptions morphing: MeshMorphingOptions lights: MeshLightOptions vertexColors: boolean fog: MeshFogOptions probe: MeshProbeOptions environment: MeshEnvironmentOptions ssao: Deferred.SSAOOptions end ``` ##### tecs.gfx.Renderer.MeshOptions.capacity field Caller-writable. Sets the maximum resident mesh-instance count and defaults to 65,536. ```teal tecs.gfx.Renderer.MeshOptions.capacity: integer ``` ##### tecs.gfx.Renderer.MeshOptions.vertexCapacity field Caller-writable. Sets the immutable geometry ceiling in vertices and defaults to 1,048,576. ```teal tecs.gfx.Renderer.MeshOptions.vertexCapacity: integer ``` ##### tecs.gfx.Renderer.MeshOptions.indexCapacity field Caller-writable. Sets the immutable geometry ceiling in 32-bit indices and defaults to 3,145,728. ```teal tecs.gfx.Renderer.MeshOptions.indexCapacity: integer ``` ##### tecs.gfx.Renderer.MeshOptions.materialCapacity field Caller-writable. Sets the material-table ceiling, including the built-in neutral slot, and defaults to 1,024. ```teal tecs.gfx.Renderer.MeshOptions.materialCapacity: integer ``` ##### tecs.gfx.Renderer.MeshOptions.textureWidth field Caller-writable. Sets the width of every mesh texture-array layer and defaults to 1,024 pixels. ```teal tecs.gfx.Renderer.MeshOptions.textureWidth: integer ``` ##### tecs.gfx.Renderer.MeshOptions.textureHeight field Caller-writable. Sets the height of every mesh texture-array layer and defaults to 1,024 pixels. ```teal tecs.gfx.Renderer.MeshOptions.textureHeight: integer ``` ##### tecs.gfx.Renderer.MeshOptions.textureLayers field Caller-writable. Sets the fixed mesh texture-array layer count and defaults to 16. ```teal tecs.gfx.Renderer.MeshOptions.textureLayers: integer ``` ##### tecs.gfx.Renderer.MeshOptions.packTextures field Caller-writable. Packs multiple images into each texture layer and defaults to true. ```teal tecs.gfx.Renderer.MeshOptions.packTextures: boolean ``` ##### tecs.gfx.Renderer.MeshOptions.mipmaps field Caller-writable. Allocates and linearly filters a complete mip chain. This requires `packTextures = false`. Smaller RGBA images repeat their edge through the rest of the cell before mip generation. ```teal tecs.gfx.Renderer.MeshOptions.mipmaps: boolean ``` ##### tecs.gfx.Renderer.MeshOptions.textureFormat field Caller-writable. Selects decoded RGBA8 or imported BC3 storage with a `TEXTURE_*` integer constant and defaults to `TEXTURE_RGBA8`. BC3 requires mipmaps and disables texture packing. ```teal tecs.gfx.Renderer.MeshOptions.textureFormat: integer ``` ##### tecs.gfx.Renderer.MeshOptions.transparency field Caller-writable. Enables the independently allocated transparent mesh lane and defaults to false. ```teal tecs.gfx.Renderer.MeshOptions.transparency: boolean ``` ##### tecs.gfx.Renderer.MeshOptions.doubleSided field Caller-writable. Enables independently allocated double-sided command and pipeline resources and defaults to false. ```teal tecs.gfx.Renderer.MeshOptions.doubleSided: boolean ``` ##### tecs.gfx.Renderer.MeshOptions.shadows field Caller-writable. Enables and configures one independently allocated directional mesh-shadow lane. Nil disables it. ```teal tecs.gfx.Renderer.MeshOptions.shadows: MeshShadowOptions ``` ##### tecs.gfx.Renderer.MeshOptions.skinning field Caller-writable. Enables independently allocated skin attributes, per-instance palette offsets, joint matrices, and shader variants. Nil disables GPU skinning. ```teal tecs.gfx.Renderer.MeshOptions.skinning: MeshSkinningOptions ``` ##### tecs.gfx.Renderer.MeshOptions.morphing field Caller-writable. Enables independently allocated morph deltas, per-instance metadata and weights, and shader variants. Nil disables GPU morphing. ```teal tecs.gfx.Renderer.MeshOptions.morphing: MeshMorphingOptions ``` ##### tecs.gfx.Renderer.MeshOptions.lights field Caller-writable. Enables independently allocated point and spot light extraction, buffers, screen-tile binning, and shader variants. Nil disables local 3D lights. ```teal tecs.gfx.Renderer.MeshOptions.lights: MeshLightOptions ``` ##### tecs.gfx.Renderer.MeshOptions.vertexColors field Caller-writable. Enables a separate immutable linear RGBA vertex-color stream and shader variants. Defaults to false. A colored procedural or glTF mesh requires this option. ```teal tecs.gfx.Renderer.MeshOptions.vertexColors: boolean ``` ##### tecs.gfx.Renderer.MeshOptions.fog field Caller-writable. Enables linear camera-distance fog for meshes. Nil omits its shader variants and runtime work. ```teal tecs.gfx.Renderer.MeshOptions.fog: MeshFogOptions ``` ##### tecs.gfx.Renderer.MeshOptions.probe field Caller-writable. Enables diffuse environment lighting for meshes. Nil omits its shader variants and runtime work. ```teal tecs.gfx.Renderer.MeshOptions.probe: MeshProbeOptions ``` ##### tecs.gfx.Renderer.MeshOptions.environment field Caller-writable. Enables a six-face specular environment and optional skybox. Nil allocates no environment texture or sampler. ```teal tecs.gfx.Renderer.MeshOptions.environment: MeshEnvironmentOptions ``` ##### tecs.gfx.Renderer.MeshOptions.ssao field Caller-writable. Enables half-resolution screen-space ambient occlusion for opaque meshes. Nil allocates no AO targets or pipelines. ```teal tecs.gfx.Renderer.MeshOptions.ssao: Deferred.SSAOOptions ``` #### tecs.gfx.Renderer.BloomOptions record Configures the optional bloom branch. Enabling bloom keeps the lighting and blur intermediates in packed HDR so values above white reach extraction without increasing bytes per pixel. ```teal record tecs.gfx.Renderer.BloomOptions scale: number threshold: number knee: number intensity: number end ``` ##### tecs.gfx.Renderer.BloomOptions.scale field Caller-writable. Sets both bloom targets relative to the frame and defaults to 0.5. It must be positive. ```teal tecs.gfx.Renderer.BloomOptions.scale: number ``` ##### tecs.gfx.Renderer.BloomOptions.threshold field Caller-writable. Sets the non-negative HDR brightness threshold and defaults to 0.8. ```teal tecs.gfx.Renderer.BloomOptions.threshold: number ``` ##### tecs.gfx.Renderer.BloomOptions.knee field Caller-writable. Sets the non-negative HDR width of the threshold's soft knee and defaults to 0.1. ```teal tecs.gfx.Renderer.BloomOptions.knee: number ``` ##### tecs.gfx.Renderer.BloomOptions.intensity field Caller-writable. Scales the blurred contribution and defaults to 0.7. ```teal tecs.gfx.Renderer.BloomOptions.intensity: number ``` #### tecs.gfx.Renderer.sprites field Read-only. Provides the concrete 2D rendering lane, or nil when creation disabled it. ```teal tecs.gfx.Renderer.sprites: SpriteDomain ``` #### tecs.gfx.Renderer.meshes field Read-only. Provides the concrete 3D rendering lane, or nil when creation omitted mesh options. ```teal tecs.gfx.Renderer.meshes: MeshDomain ``` #### tecs.gfx.Renderer.deferred field Read-only. Provides the renderer-owned deferred graph and targets. ```teal tecs.gfx.Renderer.deferred: Deferred ``` #### tecs.gfx.Renderer.newRenderer Static Builds a renderer and its enabled rendering domains. ```teal function tecs.gfx.Renderer.newRenderer( device: loader.CPtr, swapchainFormat: integer, options: RendererOptions ): Renderer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `device` | `loader.CPtr` | The engine supplies the GPU device that owns renderer resources. | | `swapchainFormat` | `integer` | The engine supplies the presentation texture format. | | `options` | [`RendererOptions`](/modules/gfx/#tecs.gfx.Renderer.Options) | The caller supplies fixed creation options or nil for defaults. | ##### Returns | Type | Description | | --- | --- | | [`Renderer`](/modules/gfx/#tecs.gfx.Renderer) | Returns a caller-owned renderer that `destroy` releases. | #### tecs.gfx.Renderer:captureTexture Instance Returns the composited image from the last frame. ```teal function tecs.gfx.Renderer.captureTexture(self): Texture ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Renderer` | | ##### Returns | Type | Description | | --- | --- | | `Texture` | Returns the renderer-owned scene target. | #### tecs.gfx.Renderer:depthSortCollapse Instance Returns the world units that collapse onto one depth value. ```teal function tecs.gfx.Renderer.depthSortCollapse(self): number ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Renderer` | | ##### Returns | Type | Description | | --- | --- | | `number` | Returns zero when the depth format preserves the full layer sort. | #### tecs.gfx.Renderer:destroy Instance Releases the domains before the graph they record into. ```teal function tecs.gfx.Renderer.destroy(self) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Renderer` | | ##### Returns None. #### tecs.gfx.Renderer:device Instance Returns the GPU device shared by all domains. ```teal function tecs.gfx.Renderer.device(self): loader.CPtr ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Renderer` | | ##### Returns | Type | Description | | --- | --- | | `loader.CPtr` | Returns the engine-owned device. | #### tecs.gfx.Renderer:extractSeconds Instance Returns the seconds consumed by the last domain extraction. ```teal function tecs.gfx.Renderer.extractSeconds(self): number ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Renderer` | | ##### Returns | Type | Description | | --- | --- | | `number` | Returns elapsed seconds, or zero while measurement is inactive. | #### tecs.gfx.Renderer:install Instance Registers every rendering domain on a world. ```teal function tecs.gfx.Renderer.install(self, world: types.World) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Renderer` | | | `world` | [`types.World`](/modules/ecs/#tecs.World) | The caller supplies the world this renderer reads. | ##### Returns None. #### tecs.gfx.Renderer:rebuildPipelines Instance Rebuilds every domain pipeline from current shader sources. ```teal function tecs.gfx.Renderer.rebuildPipelines(self) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Renderer` | | ##### Returns None. #### tecs.gfx.Renderer:render Instance Prepares every domain and executes the renderer-owned frame graph. ```teal function tecs.gfx.Renderer.render(self, frame: Frame) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Renderer` | | | `frame` | `Frame` | The engine supplies an open frame that it submits afterwards. | ##### Returns None. #### tecs.gfx.Renderer:saveScreenshot Instance Writes the composited image from the last frame as a PNG. ```teal function tecs.gfx.Renderer.saveScreenshot( self, path: string ): boolean, string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Renderer` | | | `path` | `string` | The caller supplies the destination path. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns whether capture, encoding, and writing succeeded. | | `string` | Returns the failure reason when the first return is false. | #### tecs.gfx.Renderer:screenshot Instance Encodes the composited image from the last frame as PNG bytes. ```teal function tecs.gfx.Renderer.screenshot(self): string, string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Renderer` | | ##### Returns | Type | Description | | --- | --- | | `string` | Returns PNG bytes, or nil when readback or encoding fails. | | `string` | Returns the failure reason when the first return is nil. | ### tecs.gfx.SpotLight3D record Represents a conical light aimed by its entity's `Transform3D` rotation. Read-only. Exposes a conical mesh light positioned and aimed along local negative z by the entity's `tecs.Transform3D`. The mesh domain must enable `lights`. ```teal record tecs.gfx.SpotLight3D is Component radius: number innerAngle: number outerAngle: number r: number g: number b: number intensity: number flags: integer end ``` #### Interfaces | Interface | | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | #### tecs.gfx.SpotLight3D.radius field Caller-writable. Sets the light's positive reach in world units. ```teal tecs.gfx.SpotLight3D.radius: number ``` #### tecs.gfx.SpotLight3D.innerAngle field Caller-writable. Sets the fully lit half-angle in radians. It must be non-negative and no greater than `outerAngle`. ```teal tecs.gfx.SpotLight3D.innerAngle: number ``` #### tecs.gfx.SpotLight3D.outerAngle field Caller-writable. Sets the cutoff half-angle in radians. It must be positive and less than pi over two. ```teal tecs.gfx.SpotLight3D.outerAngle: number ``` #### tecs.gfx.SpotLight3D.r field Caller-writable. Sets non-negative red radiance. ```teal tecs.gfx.SpotLight3D.r: number ``` #### tecs.gfx.SpotLight3D.g field Caller-writable. Sets non-negative green radiance. ```teal tecs.gfx.SpotLight3D.g: number ``` #### tecs.gfx.SpotLight3D.b field Caller-writable. Sets non-negative blue radiance. ```teal tecs.gfx.SpotLight3D.b: number ``` #### tecs.gfx.SpotLight3D.intensity field Caller-writable. Scales the light's non-negative radiance. ```teal tecs.gfx.SpotLight3D.intensity: number ``` #### tecs.gfx.SpotLight3D.flags field Caller-writable. Combines `LIGHT_*` integer constants. Zero, the default, keeps the light out of the optional local-shadow atlas. ```teal tecs.gfx.SpotLight3D.flags: integer ``` ### tecs.gfx.Sprite record Samples a texture instead of drawing flat color. `image` is an index from `imageId`, which names the image; `slot` is the texture-array layer the renderer resolved that name to. Both are here because extraction reads the slot for every row it writes and wants a field, not a lookup, while a snapshot needs something a slot cannot give it. Snapshots store only the name. A negative slot means unresolved. The renderer fills it in the first time it writes the row, so a Sprite restored from a snapshot or built by hand resolves once rather than once per frame. Pointing a live Sprite at another image therefore means writing a negative slot along with the new `image`, or the row keeps drawing the layer the old name resolved to. The UV rect selects a region, so an atlas is the same thing as a whole image with the rect set to the full range. Read-only. Exposes the sprite component, which samples a texture instead of drawing flat color. `image` is an `imageId` index, and 0 means no image; `u0, v0, u1, v1` select the region, defaulting to the whole of it, so an atlas entry and a whole image are the same thing. `slot` is the texture-array layer the renderer resolved `image` to, and a negative value means unresolved: pointing a live `Sprite` at another image means writing a negative slot along with the new `image`, or the row keeps drawing the old layer. Only the image name survives a snapshot. ```teal record tecs.gfx.Sprite is Component image: number u0: number v0: number u1: number v1: number slot: number end ``` #### Interfaces | Interface | | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | #### tecs.gfx.Sprite.image field Caller-writable. Selects an image by its `imageId` index. Zero selects no image. ```teal tecs.gfx.Sprite.image: number ``` #### tecs.gfx.Sprite.u0 field Caller-writable. Sets the left edge of the sampled UV rectangle. ```teal tecs.gfx.Sprite.u0: number ``` #### tecs.gfx.Sprite.v0 field Caller-writable. Sets the top edge of the sampled UV rectangle. ```teal tecs.gfx.Sprite.v0: number ``` #### tecs.gfx.Sprite.u1 field Caller-writable. Sets the right edge of the sampled UV rectangle. ```teal tecs.gfx.Sprite.u1: number ``` #### tecs.gfx.Sprite.v1 field Caller-writable. Sets the bottom edge of the sampled UV rectangle. ```teal tecs.gfx.Sprite.v1: number ``` #### tecs.gfx.Sprite.slot field Engine-owned. Stores the resolved texture-array slot. Set it to a negative value when changing `image`; otherwise ordinary game code should ignore this field. ```teal tecs.gfx.Sprite.slot: number ``` ### tecs.gfx.Text record Lays a string out into glyph instances. The entity's Transform2D places the top-left corner of the text block and orients and scales the whole block, and its Tint, if it has one, colors every glyph. Both are ordinary components on an ordinary entity, so a text moves, parents, tweens, and layers like anything else, and a `Clip` keeps its glyphs inside a rectangle exactly as it would any other quad. Write through `world:getMut(entity, Text)`. A write through `world:get` leaves the column clean and the glyphs stale. Read-only. Exposes the text component. ```teal record tecs.gfx.Text is Component text: string font: Font size: number align: string wrapWidth: number width: number height: number end ``` #### Interfaces | Interface | | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | #### tecs.gfx.Text.text field Caller-writable. Sets the laid-out string. A newline starts a line and `wrapWidth` may introduce additional line breaks. ```teal tecs.gfx.Text.text: string ``` #### tecs.gfx.Text.font field Caller-writable. Selects the font that supplies the glyphs. Without one, the text draws nothing. ```teal tecs.gfx.Text.font: Font ``` #### tecs.gfx.Text.size field Caller-writable. Sets the em size in world units. ```teal tecs.gfx.Text.size: number ``` #### tecs.gfx.Text.align field Caller-writable. Selects `"left"`, `"center"`, or `"right"` alignment within the block's widest line. ```teal tecs.gfx.Text.align: string ``` #### tecs.gfx.Text.wrapWidth field Caller-writable. Sets the maximum line width in world units. Zero disables wrapping. Retained UI writes this field for a wrapping intrinsic text leaf after Taffy chooses its available width. ```teal tecs.gfx.Text.wrapWidth: number ``` #### tecs.gfx.Text.width field Engine-owned. Reports the width of the last layout in world units. Assigning it has no effect. ```teal tecs.gfx.Text.width: number ``` #### tecs.gfx.Text.height field Engine-owned. Reports the height of the last layout in world units. Assigning it has no effect. ```teal tecs.gfx.Text.height: number ``` ### tecs.gfx.TextOptions record Configures `textPlugin`. ```teal record tecs.gfx.TextOptions renderer: Renderer end ``` #### tecs.gfx.TextOptions.renderer field Caller-writable. Selects the renderer that receives glyph images and instances. ```teal tecs.gfx.TextOptions.renderer: Renderer ``` ### tecs.gfx.Tint record Controls base color and how much of the background remains visible. Alpha selects the drawing pass. At one, the opaque entity enters the G-buffer and participates in deferred lighting. Below one, the forward pass draws it after compositing, blends straight alpha, and lights it. A value rather than a component, because a fade is the thing this is for and a fade through a component would cause a structural change per entity per frame: adding and removing a tag moves a row between archetypes, which is the most expensive thing here. Alpha already changes as a float. One value also prevents conflicting transparency state. What crossing one costs is what the forward pass cannot do. A blended entity writes no depth, so it hides nothing behind it and nothing reading the G-buffer can see it: it casts no shadow, and its material's emission reaches its own color and no pass that reads the emission attachment. The forward list is also shorter than the instance buffer, so a scene with more blended entities than it holds draws the ones earliest in the buffer and drops the rest. An entity meant to be solid should say so with an alpha of exactly one. The opaque mesh domain reads rgb as its untextured base color. It ignores alpha until a mesh forward lane exists, so a mesh remains opaque at every alpha value. Read-only. Exposes the base-color component. Each channel ranges from 0 to 1 and defaults to opaque white. The alpha decides which pass draws the entity: exactly 1 goes through the G-buffer and is lit once for the whole scene, anything below 1 goes through the forward pass instead and writes no depth. When the forward list fills, the renderer drops later entries. Use exactly 1 for solid content. ```teal record tecs.gfx.Tint is Component r: number g: number b: number a: number end ``` #### Interfaces | Interface | | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | #### tecs.gfx.Tint.r field Caller-writable. Sets the red channel from zero to one. ```teal tecs.gfx.Tint.r: number ``` #### tecs.gfx.Tint.g field Caller-writable. Sets the green channel from zero to one. ```teal tecs.gfx.Tint.g: number ``` #### tecs.gfx.Tint.b field Caller-writable. Sets the blue channel from zero to one. ```teal tecs.gfx.Tint.b: number ``` #### tecs.gfx.Tint.a field Caller-writable. Sets sprite coverage from transparent at zero to opaque at one. The opaque mesh lane currently ignores it. ```teal tecs.gfx.Tint.a: number ``` ### tecs.gfx.TTFOptions record Configures `newTTF`. ```teal record tecs.gfx.TTFOptions source: string name: string size: number raster: FontRaster end ``` #### tecs.gfx.TTFOptions.source field Caller-writable. Sets a TrueType or OpenType path. Relative paths are resolved against the asset root. ```teal tecs.gfx.TTFOptions.source: string ``` #### tecs.gfx.TTFOptions.name field Caller-writable. Sets the snapshot identity. It defaults to `source`. ```teal tecs.gfx.TTFOptions.name: string ``` #### tecs.gfx.TTFOptions.size field Caller-writable. Sets the glyph raster point size. It defaults to 48. ```teal tecs.gfx.TTFOptions.size: number ``` #### tecs.gfx.TTFOptions.raster field Caller-writable. Selects `"sdf"` for scalable text or `"alpha"` for crisp text drawn at the loaded size. It defaults to `"sdf"`. ```teal tecs.gfx.TTFOptions.raster: FontRaster ``` ### tecs.gfx.View record Describes one ordered viewport and the domain cameras drawn through it. ```teal global record tecs.gfx.View is types.components.Component camera2D: Camera2D camera3D: Camera3D x: number y: number width: number height: number order: integer enabled: boolean end ``` #### Interfaces | Interface | | --- | | [`types.components.Component`](/modules/ecs/#tecs.ecs.Component) | #### tecs.gfx.View.camera2D field Caller-writable. Selects the 2D camera, or nil to omit sprites. ```teal tecs.gfx.View.camera2D: Camera2D ``` #### tecs.gfx.View.camera3D field Caller-writable. Selects the 3D camera, or nil to omit meshes. ```teal tecs.gfx.View.camera3D: Camera3D ``` #### tecs.gfx.View.x field Caller-writable. Sets the left edge as a frame fraction and defaults to zero. ```teal tecs.gfx.View.x: number ``` #### tecs.gfx.View.y field Caller-writable. Sets the top edge as a frame fraction and defaults to zero. ```teal tecs.gfx.View.y: number ``` #### tecs.gfx.View.width field Caller-writable. Sets the width as a frame fraction and defaults to one. ```teal tecs.gfx.View.width: number ``` #### tecs.gfx.View.height field Caller-writable. Sets the height as a frame fraction and defaults to one. ```teal tecs.gfx.View.height: number ``` #### tecs.gfx.View.order field Caller-writable. Sets composition order and defaults to zero. ```teal tecs.gfx.View.order: integer ``` #### tecs.gfx.View.enabled field Caller-writable. Enables the view and defaults to true. ```teal tecs.gfx.View.enabled: boolean ``` ## Functions ### tecs.gfx.glyphAt Static Returns a glyph's world x, y, width, and height. Reads the instance produced for rendering, so it reports the drawn placement. ```teal function tecs.gfx.glyphAt( world: World, entity: integer, index: integer ): number, number, number, number ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`World`](/modules/ecs/#tecs.World) | The world that contains the text entity. | | `entity` | `integer` | An entity carrying [`Text`](/modules/gfx/#tecs.gfx.Text). | | `index` | `integer` | A one-based produced-glyph index. Layout operations with no drawable rectangle, including spaces and newlines, take no index. | #### Returns | Type | Description | | --- | --- | | `number` | World x of the glyph center, or nil when no glyph exists. | | `number` | World y of the glyph center, or nil with the first return. | | `number` | Glyph width in world units, or nil with the first return. | | `number` | Glyph height in world units, or nil with the first return. | ### tecs.gfx.imageId Static Returns the index of an image name and assigns one on first use. The function normalizes names lexically, so `"a/b.png"` and `"a/./b.png"` identify one image. It preserves case and `..` and does not touch the filesystem. ```teal function tecs.gfx.imageId(name: string): integer ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `name` | `string` | Must be non-empty; an empty or nil name errors rather than interning. | #### Returns | Type | Description | | --- | --- | | `integer` | An index from 1 upwards, stable for the life of the process and meaningless outside it. 0 is never returned and is what a [`Sprite`](/modules/gfx/#tecs.gfx.Sprite) carries until something names an image. | ### tecs.gfx.imageName Static Returns the name represented by an image index. ```teal function tecs.gfx.imageName(id: integer): string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `id` | `integer` | An index previously handed out by `imageId`. | #### Returns | Type | Description | | --- | --- | | `string` | The normalized name, or nil when the index names nothing. | ### tecs.gfx.measureIntrinsic Static Returns the preferred and minimum-content metrics for a text item. The minimum-content width is the widest whitespace-delimited run. The function shapes only while called and does not change authored fields. Retained UI calls it only when the text or its intrinsic settings are dirty. ```teal function tecs.gfx.measureIntrinsic(item: Text): number, number, number ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `item` | [`Text`](/modules/gfx/#tecs.gfx.Text) | A [`Text`](/modules/gfx/#tecs.gfx.Text) value with a font. | #### Returns | Type | Description | | --- | --- | | `number` | Preferred width with wrapping disabled. | | `number` | Preferred height with wrapping disabled. | | `number` | Minimum-content width of the widest unbroken run. | ### tecs.gfx.measureText Static Returns a text item's width and height in world units. Uses the same layout as the plugin without adding or changing an entity. ```teal function tecs.gfx.measureText( item: Text, wrapWidth: number ): number, number ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `item` | [`Text`](/modules/gfx/#tecs.gfx.Text) | A [`Text`](/modules/gfx/#tecs.gfx.Text) value to read. It need not belong to an entity. | | `wrapWidth` | `number` | The caller may override `item.wrapWidth` for this measurement without changing the retained authored value. | #### Returns | Type | Description | | --- | --- | | `number` | Width at `item.size`, before [`Transform2D`](/modules/ecs/#tecs.ecs.Transform2D) scale. Returns zero for a missing item, font, or string. | | `number` | Height on the same terms, counting complete line boxes. | ### tecs.gfx.meshId Static Returns the process-local index of a normalized mesh asset name. ```teal function tecs.gfx.meshId(name: string): integer ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `name` | `string` | A non-empty mesh asset name or path. | #### Returns | Type | Description | | --- | --- | | `integer` | A positive index stable for the life of the process. | ### tecs.gfx.meshMaterialId Static Returns the process-local identity of a mesh material name. ```teal function tecs.gfx.meshMaterialId(name: string): integer ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `name` | `string` | A non-empty stable material name. | #### Returns | Type | Description | | --- | --- | | `integer` | A positive index stable for the life of the process. | ### tecs.gfx.meshMaterialName Static Returns the name represented by a mesh material index. ```teal function tecs.gfx.meshMaterialName(id: integer): string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `id` | `integer` | An index previously returned by `meshMaterialId`. | #### Returns | Type | Description | | --- | --- | | `string` | The normalized name, or nil when the index names nothing. | ### tecs.gfx.meshMorphId Static Returns the process-local identity of a normalized mesh-morph name. ```teal function tecs.gfx.meshMorphId(name: string): integer ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `name` | `string` | A non-empty stable weight-vector name. | #### Returns | Type | Description | | --- | --- | | `integer` | A positive index stable for the life of the process. | ### tecs.gfx.meshMorphName Static Returns the normalized name represented by a mesh-morph index. ```teal function tecs.gfx.meshMorphName(id: integer): string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `id` | `integer` | An index previously returned by `meshMorphId`. | #### Returns | Type | Description | | --- | --- | | `string` | The normalized name, or nil when the index names nothing. | ### tecs.gfx.meshName Static Returns the normalized asset name represented by a mesh index. ```teal function tecs.gfx.meshName(id: integer): string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `id` | `integer` | An index previously returned by `meshId`. | #### Returns | Type | Description | | --- | --- | | `string` | The normalized name, or nil when the index names nothing. | ### tecs.gfx.meshSkinId Static Returns the process-local identity of a normalized mesh-skin name. ```teal function tecs.gfx.meshSkinId(name: string): integer ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `name` | `string` | A non-empty stable palette name. | #### Returns | Type | Description | | --- | --- | | `integer` | A positive index stable for the life of the process. | ### tecs.gfx.meshSkinName Static Returns the normalized name represented by a mesh-skin index. ```teal function tecs.gfx.meshSkinName(id: integer): string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `id` | `integer` | An index previously returned by `meshSkinId`. | #### Returns | Type | Description | | --- | --- | | `string` | The normalized name, or nil when the index names nothing. | ### tecs.gfx.textLayouts Static Returns how many texts the world has laid out. ```teal function tecs.gfx.textLayouts(world: World): integer ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`World`](/modules/ecs/#tecs.World) | A world with or without the text plugin. | #### Returns | Type | Description | | --- | --- | | `integer` | A count that only increases, or zero before plugin installation. The count measures rows laid out rather than frames. | ### tecs.gfx.textPlugin Static Creates the plugin that lays out text for a renderer. ```teal function tecs.gfx.textPlugin(options: TextOptions): function(World) ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `options` | [`TextOptions`](/modules/gfx/#tecs.gfx.TextOptions) | Requires `renderer`. | #### Returns | Type | Description | | --- | --- | | `function(`[`World`](/modules/ecs/#tecs.World)`)` | A world plugin. Each world gets its own producer run while sharing process-wide fonts. | ## Values ### tecs.gfx.LIGHT_CASTS_SHADOWS variable Read-only. Marks a 3D point or spot light for the optional local-shadow atlas when included in its `flags` field. ```teal tecs.gfx.LIGHT_CASTS_SHADOWS: integer ``` --- ## tecs.gfx.layers # tecs.gfx.layers Depth bands, sorting, coordinate spaces, parallax, and lighting. A layer occupies one of sixteen depth bands. Entities sort inside their band and never against another band. Higher layer numbers draw nearer, so a HUD on layer 8 covers a world on layer 1. ```teal tecs.gfx.layers.configure(1, {sort = "topdown", parallax = 0.4}) tecs.gfx.layers.configure( 8, { sort = "z", screenSpace = true, unlit = true, overlay = true, } ) world:spawn( tecs.Transform2D(16, 16, 0, 8, 0, 96, 24), tecs.gfx.Tint(1.0, 1.0, 1.0, 1.0), tecs.gfx.Renderable2D() ) ``` The fourth [`Transform2D`](/modules/ecs/#tecs.ecs.Transform2D) argument selects the layer. ## Sort modes `"topdown"` sorts by Y with Z for height. `"z"` ignores position. `"isometric"` combines X, Y, and Z for a diamond grid. Equal depths retain instance order. `maxZ` and `maxY` define the authored extents that each sort maps into its band. Values past an extent clamp to its edge. They still draw, but clamped entities no longer sort against one another. Reduce an extent when `Renderer:depthSortCollapse` reports that the target depth format merges nearby values. ## Coordinate spaces World coordinates form the default. `screenSpace` uses target pixels and ignores the camera. `virtualCoords` stretches one `virtualWidth` by `virtualHeight` coordinate system to the target. `ignoreZoom` follows camera position while preserving drawn size. `parallax` scales camera movement. One layer cannot combine `screenSpace` and `virtualCoords`. Virtual coordinates preserve the authored coordinate system but do not letterbox or preserve square pixels. `unlit` bypasses scene lighting for UI, debug overlays, and other content that must retain its own color. In a renderer that also enables meshes, put a HUD on the highest screen-space, unlit overlay layer. `overlay` selects the sprite forward lane even for opaque content. That lane runs after opaque and transparent meshes and after bloom. The 2D layer still orders the HUD internally; 2D and 3D do not pretend to share one camera-space depth scale. `configure` replaces the complete layer configuration. Any omitted option returns to its default. ## Module contents ### Types | Type | Kind | Description | | --- | --- | --- | | [`Config`](/modules/gfx/layers/#tecs.gfx.layers.Config) | record | Defines every decision a layer makes about its contents. | | [`Sort`](/modules/gfx/layers/#tecs.gfx.layers.Sort) | enum | Selects how a layer orders its contents within its own band. | ### Functions | Function | Kind | Description | | --- | --- | --- | | [`bandOf`](/modules/gfx/layers/#tecs.gfx.layers.bandOf) | Static | Returns the two terms depthIn takes, resolved once for a layer. | | [`configure`](/modules/gfx/layers/#tecs.gfx.layers.configure) | Static | Sets what a layer does with its contents. | | [`depthIn`](/modules/gfx/layers/#tecs.gfx.layers.depthIn) | Static | Returns one entity's depth on a band bandOf already resolved. | | [`depthOf`](/modules/gfx/layers/#tecs.gfx.layers.depthOf) | Static | Returns one entity's depth from zero to one, with zero nearest. | | [`depthResolution`](/modules/gfx/layers/#tecs.gfx.layers.depthResolution) | Static | Returns the smallest depth difference depthOf produces between two entities one unit apart, taken over every sort mode. | | [`entryOf`](/modules/gfx/layers/#tecs.gfx.layers.entryOf) | Static | Returns the four floats that position and light one layer: its mode, its parallax offset factor, whether it ignores... | | [`isOverlay`](/modules/gfx/layers/#tecs.gfx.layers.isOverlay) | Static | Returns whether a layer always uses the sprite forward lane. | | [`isScreenSpace`](/modules/gfx/layers/#tecs.gfx.layers.isScreenSpace) | Static | Returns whether a layer uses screen-pixel coordinates. | | [`revision`](/modules/gfx/layers/#tecs.gfx.layers.revision) | Static | Returns the layer table's configuration revision. | | [`sortOf`](/modules/gfx/layers/#tecs.gfx.layers.sortOf) | Static | Returns a layer's sort as the identifier accepted by depthOf. | | [`viewCulled`](/modules/gfx/layers/#tecs.gfx.layers.viewCulled) | Static | Returns whether the camera's view rectangle culls this layer. | ### Values | Value | Type | Description | | --- | --- | --- | | [`MAX`](/modules/gfx/layers/#tecs.gfx.layers.MAX) | `integer` | Read-only. Reports how many bands divide the depth range. | | [`maxY`](/modules/gfx/layers/#tecs.gfx.layers.maxY) | `number` | Caller-writable. Sets half the expected world extent used to normalize position into a band. | | [`maxZ`](/modules/gfx/layers/#tecs.gfx.layers.maxZ) | `number` | Caller-writable. Sets the highest z a scene expects to use and the resulting resolution of sorting within a band. | | [`virtualHeight`](/modules/gfx/layers/#tecs.gfx.layers.virtualHeight) | `number` | Caller-writable. Sets the vertical resolution used to author a virtual-coordinate layer. | | [`virtualWidth`](/modules/gfx/layers/#tecs.gfx.layers.virtualWidth) | `number` | Caller-writable. Sets the horizontal resolution used to author a virtual-coordinate layer. | ## Types ### tecs.gfx.layers.Config record Defines every decision a layer makes about its contents. A field left out takes its default, so this says what a layer is rather than amending what it was. ```teal record tecs.gfx.layers.Config sort: Sort screenSpace: boolean ignoreZoom: boolean virtualCoords: boolean unlit: boolean overlay: boolean parallax: number end ``` #### tecs.gfx.layers.Config.sort field Caller-writable. Selects how contents sort within the layer's band. The caller must set this field. ```teal tecs.gfx.layers.Config.sort: Sort ``` #### tecs.gfx.layers.Config.screenSpace field Caller-writable. Positions contents in screen pixels and ignores the camera entirely. What a HUD wants. Defaults to false. ```teal tecs.gfx.layers.Config.screenSpace: boolean ``` #### tecs.gfx.layers.Config.ignoreZoom field Caller-writable. Makes contents follow the camera's position but not its zoom, so they stay a constant size on screen while moving with the world. Defaults to false. ```teal tecs.gfx.layers.Config.ignoreZoom: boolean ``` #### tecs.gfx.layers.Config.virtualCoords field Caller-writable. Positions contents in `virtualWidth` by `virtualHeight`; Tecs scales that coordinate system to fill the target. Defaults to false and cannot combine with `screenSpace`. ```teal tecs.gfx.layers.Config.virtualCoords: boolean ``` #### tecs.gfx.layers.Config.unlit field Caller-writable. Makes contents bypass the lighting pass and appear at their own color. Defaults to false. A material can ask for the same thing, and a fragment is lit only where the material and the layer agree. ```teal tecs.gfx.layers.Config.unlit: boolean ``` #### tecs.gfx.layers.Config.overlay field Caller-writable. Routes the layer through the sorted sprite forward lane after meshes and bloom, including when its tint is fully opaque. Defaults to false. A topmost HUD uses this with `screenSpace` and `unlit`. ```teal tecs.gfx.layers.Config.overlay: boolean ``` #### tecs.gfx.layers.Config.parallax field Caller-writable. Sets how much the camera's position carries the contents. One moves them with the world; a half drifts them at half speed, which is what a background wants. Defaults to one. ```teal tecs.gfx.layers.Config.parallax: number ``` ### tecs.gfx.layers.Sort enum Selects how a layer orders its contents within its own band. ```teal enum tecs.gfx.layers.Sort "isometric" "topdown" "z" end ``` ## Functions ### tecs.gfx.layers.bandOf Static Returns the two terms `depthIn` takes, resolved once for a layer. What `depthOf` works out before it looks at an entity at all, and what a caller writing many depths on one layer should work out once instead of once a row: the band's near edge, and which sort decides where in the band a row lands. ```teal function tecs.gfx.layers.bandOf(layer: integer): number, integer ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `layer` | `integer` | One to `MAX`. Outside that range this resolves the nearest layer whole, exactly as `depthOf` does, so the sort answered here is the clamped layer's and not what `sortOf` answers for the same argument. | #### Returns | Type | Description | | --- | --- | | `number` | The band's near edge, then the sort identifier. | | `integer` | | ### tecs.gfx.layers.configure Static Sets what a layer does with its contents. Replaces the layer's configuration rather than amending it. A [`Config`](/modules/gfx/layers/#tecs.gfx.layers.Config) defines the whole layer, so each omitted field takes its default instead of retaining its previous value. ```teal function tecs.gfx.layers.configure( layer: integer, config: layers.Config ) ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `layer` | `integer` | One to `MAX`. Anything else raises. | | `config` | [`layers.Config`](/modules/gfx/layers/#tecs.gfx.layers.Config) | Raises on a missing or unknown `sort`, and on asking for `screenSpace` and `virtualCoords` together. | #### Returns None. ### tecs.gfx.layers.depthIn Static Returns one entity's depth on a band `bandOf` already resolved. The per-row half of `depthOf`, split out so a caller writing a run of rows on one layer pays the band lookup once. Bit for bit what `depthOf` answers for the layer the two terms came from. ```teal function tecs.gfx.layers.depthIn( base: number, sort: integer, z: number, x: number, y: number ): number ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `base` | `number` | The band's near edge, from `bandOf`. | | `sort` | `integer` | The sort identifier, from `bandOf`. | | `z` | `number` | Height in the sort, against `maxZ`. Read by every sort mode. | | `x` | `number` | World x, read only by the isometric sort, against `maxY`. | | `y` | `number` | World y, read by the topdown and isometric sorts, against `maxY`. Larger is lower on screen and so nearer. | #### Returns | Type | Description | | --- | --- | | `number` | A depth inside the band, held to zero and one the same way `depthOf` holds its own. | ### tecs.gfx.layers.depthOf Static Returns one entity's depth from zero to one, with zero nearest. Exact ties carry no tie-breaker. The renderer draws instances in index order and the depth test lets an equal fragment through, so two entities at the same depth already resolve the same way every frame: the later one wins. Nudging depth by identity would resolve them the other way, since a larger value is farther, and quietly invert the order a scene was built expecting. ```teal function tecs.gfx.layers.depthOf( layer: integer, z: number, x: number, y: number ): number ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `layer` | `integer` | One to `MAX`, which picks the band and, through `sortOf`, what the rest of the arguments mean. Outside that range this takes the nearest layer whole and goes on sorting within it, because a band is the only thing a depth can name. | | `z` | `number` | Height in the sort, against `maxZ`. Read by every sort mode. | | `x` | `number` | World x, read only by the isometric sort, against `maxY`. | | `y` | `number` | World y, read by the topdown and isometric sorts, against `maxY`. Larger is lower on screen and so nearer. | #### Returns | Type | Description | | --- | --- | | `number` | Returns a depth inside the layer's own band. The function normalizes and clamps each argument to zero through one, so a scene beyond the configured extent stops sorting instead of spilling into the next layer. | ### tecs.gfx.layers.depthResolution Static Returns the smallest depth difference `depthOf` produces between two entities one unit apart, taken over every sort mode. A depth buffer must preserve this difference. A format with a larger step keeps the bands, so layer order remains safe, but loses the sort inside them: the two entities land on one depth value and draw in write order rather than their sorted order. The calculation includes every mode because `configure` may assign any sort after the depth target exists. `MAX`, `maxZ`, and `maxY` determine the result, so raising an extent changes it. ```teal function tecs.gfx.layers.depthResolution(): number ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | `number` | A depth difference, in the input that moves depth least. That is the topdown sort's y at the defaults, which spends the smallest share of a band on the widest range. | ### tecs.gfx.layers.entryOf Static Returns the four floats that position and light one layer: its mode, its parallax offset factor, whether it ignores zoom, and whether it is lit. Returns these values together because consumers pack them together when `revision` changes, not every frame. ```teal function tecs.gfx.layers.entryOf( layer: integer ): number, number, number, number ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `layer` | `integer` | One to `MAX`. Outside that every return is nil, since only that range is ever filled in. | #### Returns | Type | Description | | --- | --- | | `number` | The positioning mode, then the camera position's multiplier, then one when the layer ignores zoom, then one when it is lit. | | `number` | | | `number` | | | `number` | | ### tecs.gfx.layers.isOverlay Static Returns whether a layer always uses the sprite forward lane. ```teal function tecs.gfx.layers.isOverlay(layer: integer): boolean ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `layer` | `integer` | One to `MAX`. Outside that this answers false, which is what an unconfigured layer answers too. | #### Returns | Type | Description | | --- | --- | | `boolean` | Whether `configure` selected `overlay` for the layer. | ### tecs.gfx.layers.isScreenSpace Static Returns whether a layer uses screen-pixel coordinates. ```teal function tecs.gfx.layers.isScreenSpace(layer: integer): boolean ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `layer` | `integer` | One to `MAX`. Outside that this answers false, which is what an unconfigured layer answers too. | #### Returns | Type | Description | | --- | --- | | `boolean` | Whether `configure` selected `screenSpace` for the layer. | ### tecs.gfx.layers.revision Static Returns the layer table's configuration revision. ```teal function tecs.gfx.layers.revision(): integer ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | `integer` | A count that starts at zero and only rises. A consumer compares it against the value it packed at, so the number itself means nothing beyond having changed. | ### tecs.gfx.layers.sortOf Static Returns a layer's sort as the identifier accepted by `depthOf`. ```teal function tecs.gfx.layers.sortOf(layer: integer): integer ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `layer` | `integer` | One to `MAX`. Outside that this answers topdown rather than raising, since it is the default every layer starts on. | #### Returns | Type | Description | | --- | --- | | `integer` | The sort identifier, which is an internal number and not one of the `Sort` strings. | ### tecs.gfx.layers.viewCulled Static Returns whether the camera's view rectangle culls this layer. False on a layer the camera does not place where its world bound says: screen-space and virtual-coordinate contents have no world position for the view to test, and parallax and ignore-zoom draw them somewhere the bound does not describe. Extraction reads this per row and gives those contents a bound that every view contains, so they draw regardless of camera position. Layers using none of these modes retain exact culling. ```teal function tecs.gfx.layers.viewCulled(layer: integer): boolean ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `layer` | `integer` | One to `MAX`. Outside that this answers true, which is what an unconfigured layer answers too. | #### Returns | Type | Description | | --- | --- | | `boolean` | Whether extraction should write this row's real world bound. | ## Values ### tecs.gfx.layers.MAX variable Read-only. Reports how many bands divide the depth range. Load-bearing at sixteen. A sort mode packs into two bits, this value sizes the vertex shader's table, and the shader recovers a layer by multiplying depth by it. `LAYER_BANDS` in `assets/shaders/instance.vert.glsl` is the same number, and the two only work while they agree. Raising it is not a constant change. ```teal tecs.gfx.layers.MAX: integer ``` ### tecs.gfx.layers.maxY variable Caller-writable. Sets half the expected world extent used to normalize position into a band. A scene larger than this still sorts, with entities beyond the edge compressed against it. ```teal tecs.gfx.layers.maxY: number ``` ### tecs.gfx.layers.maxZ variable Caller-writable. Sets the highest z a scene expects to use and the resulting resolution of sorting within a band. A scene reaching past this still draws, with entities beyond the edge resting on it and no longer sorting against each other. ```teal tecs.gfx.layers.maxZ: number ``` ### tecs.gfx.layers.virtualHeight variable Caller-writable. Sets the vertical resolution used to author a virtual-coordinate layer. Tecs scales it with `virtualWidth` to fill the target. ```teal tecs.gfx.layers.virtualHeight: number ``` ### tecs.gfx.layers.virtualWidth variable Caller-writable. Sets the horizontal resolution used to author a virtual-coordinate layer. Every such layer shares one resolution because a game has one authored layout size. Tecs scales it to fill the target, so the layout keeps its internal proportions at any window size and stretches with the window's aspect. ```teal tecs.gfx.layers.virtualWidth: number ``` --- ## tecs.gfx.materials # tecs.gfx.materials Material selection, shader authoring, built-ins, and reload rules. A material decides a fragment's color, coverage, surface normal, and lighting response. Resolve its id by name when creating a [`Material`](/modules/gfx/#tecs.gfx.Material) component: ```teal tecs.gfx.materials.addRoot("game/materials/") world:spawn( tecs.Transform2D(120, 80, 0, 1, 0, 64, 64), tecs.gfx.Material(tecs.gfx.materials.id("rounded"), 0.25), tecs.gfx.Tint(0.2, 0.6, 1.0, 1.0), tecs.gfx.Renderable2D() ) ``` Add game roots before loading shaders or resolving ids. An entity without a [`Material`](/modules/gfx/#tecs.gfx.Material) uses `textured` at id zero. Remaining ids follow sorted material names. Adding or removing a file may renumber them, so persisted state stores a name and resolves it again. `Material.param` supplies one scalar from zero to one. Built-ins use it as follows: - `ellipse` uses the height fraction. - `ring` uses the inner-radius fraction. - `rounded` uses the corner-radius fraction. - `frame` and `line` use a thickness fraction. - `capsule` uses a height fraction. - `pie` uses a full-turn sweep fraction. - `star` uses valley depth. - `glyph` uses the distance-field range. - `textured`, `circle`, and `triangle` ignore it. ## Shader contract A `.glsl` file under a material root defines one `material` function: ```glsl MaterialOutput material(MaterialInput frag) { MaterialOutput result = materialDefaults(); float radius = mix(0.02, 0.20, frag.param); result.albedo = texture(images, frag.uv) * frag.color; result.coverage = -sdRoundedBox( frag.local, vec2(0.5), radius ); result.lit = 1.0; return result; } ``` `frag.local` runs from -0.5 to 0.5 inside the quad. `frag.uv`, `frag.color`, and `frag.param` carry the image coordinates, tint, and instance parameter. `frag.blended` says whether the fragment reaches a pass that blends it. Start from `materialDefaults`, then set `albedo`, `normal`, `orm`, `lit`, `emission`, and `coverage`. Coverage above zero keeps a fragment; zero or below discards it. The deferred lane does not blend partial coverage. A tint alpha below one routes the instance to the blended lane instead, and so does a particle effect whose `render.blend` asks for it. A material that resolves an edge by discarding should put the edge in alpha where `frag.blended` is set, which is what `textured` does. ## Surface properties `orm` carries ambient occlusion, roughness, and metallic in RGB. Alpha is reserved. `materialDefaults` returns `vec4(1.0, 0.5, 0.0, 1.0)`: fully unoccluded, medium roughness, and non-metallic. Ambient occlusion multiplies ambient lighting only. A point light is a known directional contribution and keeps its own brightness; shadow components control whether that light reaches a fragment. Roughness and metallic are stored in the shared G-buffer. Metallic-roughness mesh pixels consume them through Cook-Torrance. Sprite pixels intentionally keep Lambert diffuse lighting and therefore ignore those two channels. ```glsl MaterialOutput material(MaterialInput frag) { MaterialOutput result = materialDefaults(); result.albedo = texture(images, frag.uv) * frag.color; result.coverage = 1.0; // The instance parameter controls authored ambient occlusion. result.orm = vec4(frag.param, 0.8, 0.0, 1.0); return result; } ``` The ORM attachment is eight bits a channel, so values outside zero to one are clamped when geometry writes them. ## Emission `emission` is light the surface gives off: `rgb` its color and `a` how much of it. The renderer adds `rgb * a` to the resolved pixel after the lighting, so an emissive surface is as bright in total darkness as under a lamp and an occluder's shadow does not dim it. This differs from `lit = 0.0`, which replaces the lighting with the albedo; a surface may take light and emit at the same time, which is what a lit lamp with a glowing filament is. ```glsl MaterialOutput material(MaterialInput frag) { MaterialOutput result = materialDefaults(); result.albedo = texture(images, frag.uv) * frag.color; result.coverage = 1.0; // A warm glow at the strength the instance asked for. result.emission = vec4(1.0, 0.6, 0.2, frag.param); return result; } ``` Every built-in material emits nothing, so a scene glows only where a material says it does. Per-entity strength and color come from `frag.param` and `frag.color`, which a material reads as it chooses. The emission attachment is eight bits a channel, so a value above one is clamped to one. Keep the color in range and vary the strength. An entity in the blended lane adds its own emission to its own color and reaches the emission attachment not at all, because the forward pass runs after the G-buffer has been resolved. It therefore glows, and it does not reach a later pass that reads the attachment. ## Reloads `reload` accepts edits to existing material bodies. It refuses additions, removals, and renames because those changes can renumber live components. A refusal restores the previous set. Packaged builds cannot compile changed material source at runtime. ## Module contents ### Types | Type | Kind | Description | | --- | --- | --- | | [`Material`](/modules/gfx/materials/#tecs.gfx.materials.Material) | record | Describes one material by name, build-local id, and source. | ### Functions | Function | Kind | Description | | --- | --- | --- | | [`addRoot`](/modules/gfx/materials/#tecs.gfx.materials.addRoot) | Static | Adds a directory to search before the ones already known, so a game's materials are found alongside the engine's. | | [`define`](/modules/gfx/materials/#tecs.gfx.materials.define) | Static | Supplies a material from memory rather than a file. | | [`find`](/modules/gfx/materials/#tecs.gfx.materials.find) | Static | Returns a material id, or nil when no material has that name. | | [`id`](/modules/gfx/materials/#tecs.gfx.materials.id) | Static | Returns a material id or raises an error that names the available materials. | | [`install`](/modules/gfx/materials/#tecs.gfx.materials.install) | Static | Reads the materials and publishes the dispatch. | | [`name`](/modules/gfx/materials/#tecs.gfx.materials.name) | Static | Returns the material name represented by an id, or nil. | | [`names`](/modules/gfx/materials/#tecs.gfx.materials.names) | Static | Returns every material name in id order. | | [`reload`](/modules/gfx/materials/#tecs.gfx.materials.reload) | Static | Re-reads every material and republishes the dispatch. | | [`reset`](/modules/gfx/materials/#tecs.gfx.materials.reset) | Static | Forgets everything read, so a spec can start from the files again. | ### Values | Value | Type | Description | | --- | --- | --- | | [`defaultName`](/modules/gfx/materials/#tecs.gfx.materials.defaultName) | `string` | Read-only. Reports the material name used by entities without a Material component. | ## Types ### tecs.gfx.materials.Material record Describes one material by name, build-local id, and source. ```teal record tecs.gfx.materials.Material name: string id: integer source: string end ``` #### tecs.gfx.materials.Material.name field Read-only. Reports the file name without its extension, which a game uses to request the material. ```teal tecs.gfx.materials.Material.name: string ``` #### tecs.gfx.materials.Material.id field Read-only. Reports the id an instance carries to select this material. Assigned by `install` from sorted order, so it holds only for the set of files that were present when the dispatch was built. ```teal tecs.gfx.materials.Material.id: integer ``` #### tecs.gfx.materials.Material.source field Read-only. Contains the material's GLSL as read. Renamed on the way into the dispatch rather than here, so this is still the text the file holds. ```teal tecs.gfx.materials.Material.source: string ``` ## Functions ### tecs.gfx.materials.addRoot Static Adds a directory to search before the ones already known, so a game's materials are found alongside the engine's. ```teal function tecs.gfx.materials.addRoot(path: string) ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `path` | `string` | The function searches this directory before every existing root, so a game's file of a given name beats the engine's. It adds a trailing slash when needed. This call does not read the directory; it schedules the next `install`. | #### Returns None. ### tecs.gfx.materials.define Static Supplies a material from memory rather than a file. For a spec, and for a game that generates one at build time. Beats a file of the same name in any root, and renumbers the set on the next `install` as adding a file would. ```teal function tecs.gfx.materials.define(name: string, source: string) ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `name` | `string` | The name a game asks for, matching a file stem. | | `source` | `string` | The material's GLSL, taken as given and renamed only on the way into the dispatch. | #### Returns None. ### tecs.gfx.materials.find Static Returns a material id, or nil when no material has that name. What `id` is built on, for a caller with somewhere better to put the refusal than an error raised from here. Reading a [`Material`](/modules/gfx/#tecs.gfx.Material) back out of a snapshot is that caller: the name it holds is a fact about the file rather than about the call site, and the message says so. ```teal function tecs.gfx.materials.find(name: string): integer ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `name` | `string` | The material's file name without its extension. | #### Returns | Type | Description | | --- | --- | | `integer` | Returns the id, or nil for an unknown name. The function installs first and resolves against the complete on-disk set. | ### tecs.gfx.materials.id Static Returns a material id or raises an error that names the available materials. Resolve by name because the file set determines numbering. Adding an earlier material may change later ids. ```teal function tecs.gfx.materials.id(name: string): integer ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `name` | `string` | The material's file name without its extension. | #### Returns | Type | Description | | --- | --- | | `integer` | The id an instance carries to select it. Raises on a name nothing has. The error lists available names because a misspelled material indicates a build mistake, not a runtime condition. | ### tecs.gfx.materials.install Static Reads the materials and publishes the dispatch. Idempotent. Called by shader loading rather than by a game, so a fragment shader cannot be built before the materials it dispatches to are known. Adding a root or defining a material puts this back on, and the next load rebuilds. ```teal function tecs.gfx.materials.install() ``` #### Arguments None. #### Returns None. ### tecs.gfx.materials.name Static Returns the material name represented by an id, or nil. The reverse of `id`, and what a snapshot writes in place of the number. Nothing is kept in step to answer it: `order` already holds the names at their ids, counting from one, so this is the lookup the numbering was assigned from rather than a second copy of it. ```teal function tecs.gfx.materials.name(id: integer): string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `id` | `integer` | An id as an instance carries it. Nil answers nil rather than raising, so a component read back with no material needs no guard. | #### Returns | Type | Description | | --- | --- | | `string` | The material's name, or nil when nothing has that id. | ### tecs.gfx.materials.names Static Returns every material name in id order. The default occupies id zero, followed by the other names alphabetically. The module fixes the default's position because zero on an instance must select it. ```teal function tecs.gfx.materials.names(): {string} ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | `{string}` | A fresh list the caller owns, indexed from one while the ids it describes count from zero. | ### tecs.gfx.materials.reload Static Re-reads every material and republishes the dispatch. This function refuses a changed material set. Ids come from sorted name order, so a file appearing or disappearing renumbers every later material. A live [`Material`](/modules/gfx/#tecs.gfx.Material) component still holds the number resolved before that change. Editing a body reloads; adding or removing one requires a restart. The module retains materials supplied through `define` because they came from memory rather than a root. ```teal function tecs.gfx.materials.reload(): boolean, string ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | `boolean` | Returns true when the function rebuilds the dispatch. | | `string` | Returns the refusal reason, or nil after a rebuild. | ### tecs.gfx.materials.reset Static Forgets everything read, so a spec can start from the files again. ```teal function tecs.gfx.materials.reset() ``` #### Arguments None. #### Returns None. ## Values ### tecs.gfx.materials.defaultName variable Read-only. Reports the material name used by entities without a Material component. ```teal tecs.gfx.materials.defaultName: string ``` --- ## tecs.gfx.particles # tecs.gfx.particles GPU particle effects, emitter playback, pool sizing, and rendering limits. An entity represents an emitter. The GPU owns individual particles, so game code controls an effect and its emitter rather than inspecting particles one at a time. ```teal local sparks = tecs.gfx.particles.newEffect({ name = "game.sparks", capacity = 256, schedule = {rate = 120, duration = 0.3}, spawn = {shape = "disc", width = 4, outward = true}, initial = { lifetime = {min = 0.2, max = 0.5}, speed = {min = 40, max = 120}, size = 3, }, update = { drag = 2.0, size = tecs.gfx.particles.newCurve({ {0.0, 1.0}, {1.0, 0.0}, }), }, }) world:addPlugin(tecs.gfx.particles.plugin({ renderer = app.renderer, capacity = 4096, })) world:spawn( tecs.Transform2D(120, 80, 0, 1, 0, 1, 1), tecs.gfx.particles.ParticleEmitter({effect = sparks}) ) ``` An immutable [`Effect`](/modules/gfx/particles/#tecs.gfx.particles.Effect) describes schedule, spawn, initial state, updates, and rendering. A [`ParticleEmitter`](/modules/gfx/particles/#tecs.gfx.particles.ParticleEmitter) names the effect and carries playback state. Effect names form a snapshot compatibility surface. ## Emission and motion Schedules combine rate, delay, duration, looping, and timed bursts. `duration = 0` keeps a cycle open. An emission that finds no free effect slot either drops the new particle or replaces the oldest, according to `overflow`. Spawn shapes include point, line, rectangle, rectangle edge, disc, ring, and cone. World-space particles detach from later emitter movement. Local-space particles follow the emitter for their full life. `inheritVelocity` applies only in world space. Curves and gradients sample normalized age from zero to one. Numeric properties accept a constant or a `{min, max}` range. ## Playback `play` starts or resumes emission. `stop` resets the schedule while live particles drain. `pause` holds the schedule and live field. `clear` kills the field without changing emission. `restart` resets schedule and randomness. `burst` queues a count for the next step. `finished` derives its answer from the schedule and the longest lifetime. `estimatedCount` integrates the schedule over the mean lifetime, and may differ after overflow or a truncated burst. Game code cannot inspect, move, kill, or count individual particles. ## Pool and snapshots The plugin fixes world capacity and maximum emitters at installation. Each emitter reserves its effect capacity. An emitter that cannot fit draws nothing and logs the refusal. Snapshots store effect name, seed, configuration, and playback state. They do not store individual particles, so a restored emitter starts empty and refills. ## Blending `render.blend` selects `"alpha"`, `"additive"`, or `"opaque"`, and defaults to `"alpha"`. The first two draw in the forward pass over the composited image, depth tested and not written, so alpha means what it says: a gradient that ends transparent fades out, and `"additive"` adds light instead of covering, which is what fire, glow, and sparks want. `"opaque"` keeps the effect in the G-buffer. A blended effect is not in the G-buffer, so it casts no shadow and no light's occluder mask sees it, and it is not shadowed by one either. It also draws after compositing, so all of one effect's particles composite as one group in pool-slot order rather than being sorted against each other: an effect names one layer and its particles take one depth within it. Sorting within a pool is a separate feature. One blended emitter puts the frame's forward lane to work, which is five compute passes and a draw a world with nothing blended in it does not pay. A world of `"opaque"` effects pays none of it, which is the other reason that name exists. ## Module contents ### Constructors | Constructor | Description | | --- | --- | | [`newCurve`](/modules/gfx/particles/#tecs.gfx.particles.newCurve) | Compiles keyframes into a curve over normalized age. | | [`newEffect`](/modules/gfx/particles/#tecs.gfx.particles.newEffect) | Registers an immutable effect under options.name and shares it among every emitter using that name. | | [`newGradient`](/modules/gfx/particles/#tecs.gfx.particles.newGradient) | Compiles keyframes into a color gradient over normalized age. | ### Types | Type | Kind | Description | | --- | --- | --- | | [`Color`](/modules/gfx/particles/#tecs.gfx.particles.Color) | type | Represents a color accepted by particle authoring fields. | | [`Curve`](/modules/gfx/particles/#tecs.gfx.particles.Curve) | record | Stores a compiled scalar curve. | | [`Draining`](/modules/gfx/particles/#tecs.gfx.particles.Draining) | record | A slot range whose emitter is gone, held until its last particle can no longer be alive. | | [`Effect`](/modules/gfx/particles/#tecs.gfx.particles.Effect) | record | The handle newEffect returns and a ParticleEmitter names. | | [`EffectOptions`](/modules/gfx/particles/#tecs.gfx.particles.EffectOptions) | record | Configures newEffect. | | [`EmitterOptions`](/modules/gfx/particles/#tecs.gfx.particles.EmitterOptions) | record | Configures a ParticleEmitter. | | [`EmitterState`](/modules/gfx/particles/#tecs.gfx.particles.EmitterState) | enum | The three values an emitter's state takes. | | [`Gradient`](/modules/gfx/particles/#tecs.gfx.particles.Gradient) | record | Stores a compiled color gradient. | | [`Holding`](/modules/gfx/particles/#tecs.gfx.particles.Holding) | record | One emitter's place in the pool, as the pool remembers it. | | [`InitialOptions`](/modules/gfx/particles/#tecs.gfx.particles.InitialOptions) | record | Configures a particle's initial properties. | | [`ParticleEmitter`](/modules/gfx/particles/#tecs.gfx.particles.ParticleEmitter) | record | Controls an effect's playback state and per-instance scales. | | [`Pool`](/modules/gfx/particles/#tecs.gfx.particles.Pool) | record | Represents a world's installed particle pool. | | [`PoolOptions`](/modules/gfx/particles/#tecs.gfx.particles.PoolOptions) | record | Configures plugin. | | [`Range`](/modules/gfx/particles/#tecs.gfx.particles.Range) | record | The two-bound form of a Value. | | [`RenderOptions`](/modules/gfx/particles/#tecs.gfx.particles.RenderOptions) | record | Configures how particles draw. | | [`ScheduleOptions`](/modules/gfx/particles/#tecs.gfx.particles.ScheduleOptions) | record | Configures when particles emit. | | [`SpawnOptions`](/modules/gfx/particles/#tecs.gfx.particles.SpawnOptions) | record | Configures where particles spawn. | | [`UpdateOptions`](/modules/gfx/particles/#tecs.gfx.particles.UpdateOptions) | record | Configures how a particle changes over its normalized lifetime. | | [`Value`](/modules/gfx/particles/#tecs.gfx.particles.Value) | type | Accepted wherever a property may vary per particle. | ### Functions | Function | Kind | Description | | --- | --- | --- | | [`find`](/modules/gfx/particles/#tecs.gfx.particles.find) | Static | Returns the effect registered under name, or nil when none exists. | | [`names`](/modules/gfx/particles/#tecs.gfx.particles.names) | Static | Returns every registered effect name in registration order. | | [`plugin`](/modules/gfx/particles/#tecs.gfx.particles.plugin) | Static | Installs the pool on a world. | | [`poolOf`](/modules/gfx/particles/#tecs.gfx.particles.poolOf) | Static | Returns the pool installed on a world, or nil. | | [`reset`](/modules/gfx/particles/#tecs.gfx.particles.reset) | Static | Forgets every registered effect, so a spec can register a set again. | ### Values | Value | Type | Description | | --- | --- | --- | | [`CURVE_SAMPLES`](/modules/gfx/particles/#tecs.gfx.particles.CURVE_SAMPLES) | `integer` | Read-only. Reports how many samples one compiled curve or gradient holds. | | [`EFFECT`](/modules/gfx/particles/#tecs.gfx.particles.EFFECT) | `{string : integer}` | Read-only. Reports each field's offset within an effect record. | | [`EFFECT_FLOATS`](/modules/gfx/particles/#tecs.gfx.particles.EFFECT_FLOATS) | `integer` | Read-only. Reports how many floats each effect record contains. | | [`EMITTER`](/modules/gfx/particles/#tecs.gfx.particles.EMITTER) | `{string : integer}` | Read-only. Reports each field's float offset within an emitter record, counting from zero. | | [`EMITTER_FLOATS`](/modules/gfx/particles/#tecs.gfx.particles.EMITTER_FLOATS) | `integer` | Read-only. Reports how many floats each emitter record contains. | | [`STATE_FLOATS`](/modules/gfx/particles/#tecs.gfx.particles.STATE_FLOATS) | `integer` | Read-only. Reports how many GPU state floats one particle carries. | ## Constructors ### tecs.gfx.particles.newCurve Static Compiles keyframes into a curve over normalized age. ```teal function tecs.gfx.particles.newCurve(keys: {CurveKey}): Curve ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `keys` | `{CurveKey}` | Supplies `{age, value}` pairs with age from zero through one, in any order. Empty or nil raises, and one key defines a constant. | #### Returns | Type | Description | | --- | --- | | [`Curve`](/modules/gfx/particles/#tecs.gfx.particles.Curve) | Returns the piecewise-linear curve resampled at `CURVE_SAMPLES` points. Ages outside zero through one read as the nearest endpoint. | ### tecs.gfx.particles.newEffect Static Registers an immutable effect under `options.name` and shares it among every emitter using that name. The function requires a unique name. ```teal function tecs.gfx.particles.newEffect(options: EffectOptions): Effect ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `options` | [`EffectOptions`](/modules/gfx/particles/#tecs.gfx.particles.EffectOptions) | Every section but `name` is optional and defaults to something drawable. | #### Returns | Type | Description | | --- | --- | | [`Effect`](/modules/gfx/particles/#tecs.gfx.particles.Effect) | Returns a handle that stays valid until `reset`. The process-wide registry makes it reachable from every world. | ### tecs.gfx.particles.newGradient Static Compiles keyframes into a color gradient over normalized age. ```teal function tecs.gfx.particles.newGradient(keys: {CurveKey}): Gradient ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `keys` | `{CurveKey}` | Supplies `{age, color}` pairs under the same rules as `newCurve`. Alpha fades a blended effect and is ignored by an opaque one. | #### Returns | Type | Description | | --- | --- | | [`Gradient`](/modules/gfx/particles/#tecs.gfx.particles.Gradient) | A gradient resampled like a curve, interpolating each channel separately, so two colors blend through whatever lies between them componentwise rather than around a color wheel. | ## Types ### tecs.gfx.particles.Color type Represents a color accepted by particle authoring fields. ```teal type tecs.gfx.particles.Color = Color ``` ### tecs.gfx.particles.Curve record Stores a compiled scalar curve. ```teal record tecs.gfx.particles.Curve samples: {number} end ``` #### tecs.gfx.particles.Curve.samples field Read-only. Provides evenly spaced samples over normalized age from zero to one. ```teal tecs.gfx.particles.Curve.samples: {number} ``` ### tecs.gfx.particles.Draining record A slot range whose emitter is gone, held until its last particle can no longer be alive. ```teal global record tecs.gfx.particles.Draining base: integer count: integer releaseAt: number blended: boolean end ``` #### tecs.gfx.particles.Draining.base field Engine-owned. Stores the first particle slot awaiting release. ```teal tecs.gfx.particles.Draining.base: integer ``` #### tecs.gfx.particles.Draining.count field Engine-owned. Stores the number of particle slots awaiting release. ```teal tecs.gfx.particles.Draining.count: integer ``` #### tecs.gfx.particles.Draining.releaseAt field Engine-owned. Stores the time when the slots become reusable. ```teal tecs.gfx.particles.Draining.releaseAt: number ``` #### tecs.gfx.particles.Draining.blended field Engine-owned. Reports whether the effect that held this range blends. The pool carries this value because the emitter and its effect are gone while the range's particles are still being drawn. ```teal tecs.gfx.particles.Draining.blended: boolean ``` ### tecs.gfx.particles.Effect record The handle `newEffect` returns and a [`ParticleEmitter`](/modules/gfx/particles/#tecs.gfx.particles.ParticleEmitter) names. ```teal record tecs.gfx.particles.Effect name: string index: integer capacity: integer blend: string maxLifetime: number meanLifetime: number emitFor: number rate: number delay: number duration: number looping: boolean bursts: {number} end ``` #### tecs.gfx.particles.Effect.name field Read-only. Reports the effect name and the only property that outlives the process. Everything a save, a tool or a person identifies an effect by is this. ```teal tecs.gfx.particles.Effect.name: string ``` #### tecs.gfx.particles.Effect.index field Read-only. Reports where its record sits in the assembled effect table, counting from one. A position in registration order, so it means nothing outside the process that assigned it and nothing stores one. ```teal tecs.gfx.particles.Effect.index: integer ``` #### tecs.gfx.particles.Effect.capacity field Read-only. Reports how many live particles one emitter can hold. ```teal tecs.gfx.particles.Effect.capacity: integer ``` #### tecs.gfx.particles.Effect.blend field Read-only. Reports the blend mode the effect resolved to, which is one of the three `render.blend` names and never nil. The pool reads it to say how many of its slots may reach the forward pass. ```teal tecs.gfx.particles.Effect.blend: string ``` #### tecs.gfx.particles.Effect.maxLifetime field Read-only. Reports the longest particle lifetime in seconds, which decides when an emitter that has stopped emitting has finished. ```teal tecs.gfx.particles.Effect.maxLifetime: number ``` #### tecs.gfx.particles.Effect.meanLifetime field Read-only. Reports the mean particle lifetime in seconds, halfway between the bounds `initial.lifetime` was authored with. This is the window `estimatedCount` integrates the schedule over, and it is the steady-state live count divided by the emission rate. ```teal tecs.gfx.particles.Effect.meanLifetime: number ``` #### tecs.gfx.particles.Effect.emitFor field Read-only. Reports seconds from `play` to the last emission, or -1 for an effect that never stops emitting. ```teal tecs.gfx.particles.Effect.emitFor: number ``` #### tecs.gfx.particles.Effect.rate field Read-only. Reports the continuous emission rate used by `finished` and `estimatedCount`. ```teal tecs.gfx.particles.Effect.rate: number ``` #### tecs.gfx.particles.Effect.delay field Read-only. Reports the emission delay in seconds. ```teal tecs.gfx.particles.Effect.delay: number ``` #### tecs.gfx.particles.Effect.duration field Read-only. Reports the emission duration in seconds. ```teal tecs.gfx.particles.Effect.duration: number ``` #### tecs.gfx.particles.Effect.looping field Read-only. Reports whether the emission cycle repeats. ```teal tecs.gfx.particles.Effect.looping: boolean ``` #### tecs.gfx.particles.Effect.bursts field Read-only. Provides the compiled burst schedule. ```teal tecs.gfx.particles.Effect.bursts: {number} ``` ### tecs.gfx.particles.EffectOptions record Configures `newEffect`. ```teal record tecs.gfx.particles.EffectOptions name: string capacity: integer overflow: string schedule: ScheduleOptions spawn: SpawnOptions initial: InitialOptions update: UpdateOptions render: RenderOptions end ``` #### tecs.gfx.particles.EffectOptions.name field Caller-writable. Sets the required process-wide unique effect name. Snapshots use this name to identify the effect. ```teal tecs.gfx.particles.EffectOptions.name: string ``` #### tecs.gfx.particles.EffectOptions.capacity field Caller-writable. Sets how many live particles one emitter can hold. Defaults to 256, and under one raises. A steady-state emitter needs at least `rate * maxLifetime`; a smaller capacity registers, drops the emissions that find no slot, and logs a warning saying so, because a spray thinner than the one authored is otherwise found by eye. ```teal tecs.gfx.particles.EffectOptions.capacity: integer ``` #### tecs.gfx.particles.EffectOptions.overflow field Caller-writable. Selects `"drop"` or `"replace"` when an emission arrives with no free slot: give way, or take the oldest live particle's. Defaults to `"replace"`, and anything else raises. ```teal tecs.gfx.particles.EffectOptions.overflow: string ``` #### tecs.gfx.particles.EffectOptions.schedule field Caller-writable. Configures when the effect emits particles. An absent section takes every default below it. ```teal tecs.gfx.particles.EffectOptions.schedule: ScheduleOptions ``` #### tecs.gfx.particles.EffectOptions.spawn field Caller-writable. Configures where the effect spawns particles. An absent section takes every default below it. ```teal tecs.gfx.particles.EffectOptions.spawn: SpawnOptions ``` #### tecs.gfx.particles.EffectOptions.initial field Caller-writable. Configures each particle's initial properties. An absent section takes every default below it. ```teal tecs.gfx.particles.EffectOptions.initial: InitialOptions ``` #### tecs.gfx.particles.EffectOptions.update field Caller-writable. Configures how particles change over their lifetime. An absent section leaves a particle on its launch velocity. ```teal tecs.gfx.particles.EffectOptions.update: UpdateOptions ``` #### tecs.gfx.particles.EffectOptions.render field Caller-writable. Configures how particles draw. An absent section draws white quads on layer one. ```teal tecs.gfx.particles.EffectOptions.render: RenderOptions ``` ### tecs.gfx.particles.EmitterOptions record Configures a [`ParticleEmitter`](/modules/gfx/particles/#tecs.gfx.particles.ParticleEmitter). ```teal record tecs.gfx.particles.EmitterOptions effect: Effect state: EmitterState seed: number rateScale: number sizeScale: number timeScale: number tint: Color end ``` #### tecs.gfx.particles.EmitterOptions.effect field Caller-writable. Selects the required effect. An absent one raises. ```teal tecs.gfx.particles.EmitterOptions.effect: Effect ``` #### tecs.gfx.particles.EmitterOptions.state field Caller-writable. Sets the initial playback state. Defaults to `"playing"`, and anything outside the three raises. ```teal tecs.gfx.particles.EmitterOptions.state: EmitterState ``` #### tecs.gfx.particles.EmitterOptions.seed field Caller-writable. Sets the random seed. Defaults to one. ```teal tecs.gfx.particles.EmitterOptions.seed: number ``` #### tecs.gfx.particles.EmitterOptions.rateScale field Caller-writable. Multiplies the effect's emission rate. Defaults to one. ```teal tecs.gfx.particles.EmitterOptions.rateScale: number ``` #### tecs.gfx.particles.EmitterOptions.sizeScale field Caller-writable. Multiplies every particle's size. Defaults to one. ```teal tecs.gfx.particles.EmitterOptions.sizeScale: number ``` #### tecs.gfx.particles.EmitterOptions.timeScale field Caller-writable. Multiplies the speed of the effect clock. Defaults to one. ```teal tecs.gfx.particles.EmitterOptions.timeScale: number ``` #### tecs.gfx.particles.EmitterOptions.tint field Caller-writable. Multiplies every particle's color. Defaults to none, which multiplies by opaque white. ```teal tecs.gfx.particles.EmitterOptions.tint: Color ``` ### tecs.gfx.particles.EmitterState enum The three values an emitter's `state` takes. ```teal global enum tecs.gfx.particles.EmitterState "paused" "playing" "stopped" end ``` ### tecs.gfx.particles.Gradient record Stores a compiled color gradient. ```teal record tecs.gfx.particles.Gradient samples: {number} end ``` #### tecs.gfx.particles.Gradient.samples field Read-only. Provides evenly spaced RGBA samples over normalized age, with four floats per sample. ```teal tecs.gfx.particles.Gradient.samples: {number} ``` ### tecs.gfx.particles.Holding record One emitter's place in the pool, as the pool remembers it. ```teal global record tecs.gfx.particles.Holding item: ParticleEmitter index: integer base: integer count: integer tint: Color tintR: number tintG: number tintB: number tintA: number end ``` #### tecs.gfx.particles.Holding.item field Engine-owned. Stores the emitter assigned to this allocation. ```teal tecs.gfx.particles.Holding.item: ParticleEmitter ``` #### tecs.gfx.particles.Holding.index field Engine-owned. Stores the reusable emitter index. ```teal tecs.gfx.particles.Holding.index: integer ``` #### tecs.gfx.particles.Holding.base field Engine-owned. Stores the first particle slot. ```teal tecs.gfx.particles.Holding.base: integer ``` #### tecs.gfx.particles.Holding.count field Engine-owned. Stores the number of particle slots. ```teal tecs.gfx.particles.Holding.count: integer ``` #### tecs.gfx.particles.Holding.tint field Engine-owned. Stores the tint as it was last read and what it parsed to. Parsing a color string per emitter per frame would be the one allocation in a loop that otherwise has none, and a tint changes rarely. ```teal tecs.gfx.particles.Holding.tint: Color ``` #### tecs.gfx.particles.Holding.tintR field Engine-owned. Stores the parsed red tint channel. ```teal tecs.gfx.particles.Holding.tintR: number ``` #### tecs.gfx.particles.Holding.tintG field Engine-owned. Stores the parsed green tint channel. ```teal tecs.gfx.particles.Holding.tintG: number ``` #### tecs.gfx.particles.Holding.tintB field Engine-owned. Stores the parsed blue tint channel. ```teal tecs.gfx.particles.Holding.tintB: number ``` #### tecs.gfx.particles.Holding.tintA field Engine-owned. Stores the parsed alpha tint channel. ```teal tecs.gfx.particles.Holding.tintA: number ``` ### tecs.gfx.particles.InitialOptions record Configures a particle's initial properties. ```teal global record tecs.gfx.particles.InitialOptions lifetime: Value speed: Value size: Value rotation: Value angularVelocity: Value accelerationX: Value accelerationY: Value color: Color end ``` #### tecs.gfx.particles.InitialOptions.lifetime field Caller-writable. Sets particle lifetime in seconds. Defaults to one, and its upper bound is the effect's `maxLifetime`. ```teal tecs.gfx.particles.InitialOptions.lifetime: Value ``` #### tecs.gfx.particles.InitialOptions.speed field Caller-writable. Sets initial speed in world units per second. Defaults to zero. ```teal tecs.gfx.particles.InitialOptions.speed: Value ``` #### tecs.gfx.particles.InitialOptions.size field Caller-writable. Sets initial size in world units. Defaults to one. ```teal tecs.gfx.particles.InitialOptions.size: Value ``` #### tecs.gfx.particles.InitialOptions.rotation field Caller-writable. Sets initial rotation in radians. Defaults to zero. ```teal tecs.gfx.particles.InitialOptions.rotation: Value ``` #### tecs.gfx.particles.InitialOptions.angularVelocity field Caller-writable. Sets initial angular velocity in radians per second. Defaults to zero. ```teal tecs.gfx.particles.InitialOptions.angularVelocity: Value ``` #### tecs.gfx.particles.InitialOptions.accelerationX field Caller-writable. Sets constant horizontal acceleration. Independent axes make a plume drift apart instead of translating as a block. Defaults to zero. ```teal tecs.gfx.particles.InitialOptions.accelerationX: Value ``` #### tecs.gfx.particles.InitialOptions.accelerationY field Caller-writable. Sets constant vertical acceleration. Defaults to zero. ```teal tecs.gfx.particles.InitialOptions.accelerationY: Value ``` #### tecs.gfx.particles.InitialOptions.color field Caller-writable. Sets the color every particle starts with, multiplied by the gradient and by the emitter's tint. Defaults to opaque white. ```teal tecs.gfx.particles.InitialOptions.color: Color ``` ### tecs.gfx.particles.ParticleEmitter record Controls an effect's playback state and per-instance scales. Deliberately small. If an instance could override every effect field then effects would stop being reusable GPU data and every emitter would grow to hold a copy. The effect owns capacity because it determines the pool slot range when the emitter starts. Read-only. Exposes the particle-emitter component. ```teal record tecs.gfx.particles.ParticleEmitter is ecs.Component effect: Effect state: EmitterState seed: number rateScale: number sizeScale: number timeScale: number tint: Color burst: function(self, count: number) clear: function(self) estimatedCount: function(self): integer finished: function(self): boolean pause: function(self) play: function(self) restart: function(self) stop: function(self) end ``` #### Interfaces | Interface | | --- | | [`ecs.Component`](/modules/ecs/#tecs.ecs.Component) | #### tecs.gfx.particles.ParticleEmitter.effect field Caller-writable. Selects the effect before spawning the emitter. It cannot change after spawn because the slot range is the effect's capacity, and changing it would mean reallocating. Despawn and spawn again, which is cheap. ```teal tecs.gfx.particles.ParticleEmitter.effect: Effect ``` #### tecs.gfx.particles.ParticleEmitter.state field Caller-writable. Reports whether the emitter plays, pauses, or stops. Set it through `play`, `pause` and `stop` rather than by assignment: those move the cycle's start step and the random generation with it, and a bare write leaves both where the previous state left them, so an emitter written back to `"playing"` resumes part way through its schedule instead of starting it again. ```teal tecs.gfx.particles.ParticleEmitter.state: EmitterState ``` #### tecs.gfx.particles.ParticleEmitter.seed field Caller-writable. Selects the random sequence. Two emitters with one seed produce one field. ```teal tecs.gfx.particles.ParticleEmitter.seed: number ``` #### tecs.gfx.particles.ParticleEmitter.rateScale field Caller-writable. Multiplies the effect's emission rate. ```teal tecs.gfx.particles.ParticleEmitter.rateScale: number ``` #### tecs.gfx.particles.ParticleEmitter.sizeScale field Caller-writable. Multiplies every particle's size. ```teal tecs.gfx.particles.ParticleEmitter.sizeScale: number ``` #### tecs.gfx.particles.ParticleEmitter.timeScale field Caller-writable. Multiplies the speed of the effect clock. ```teal tecs.gfx.particles.ParticleEmitter.timeScale: number ``` #### tecs.gfx.particles.ParticleEmitter.tint field Caller-writable. Multiplies every particle's color, alpha included, so a tint is how one emitter of a blended effect fades against another. An effect whose `render.blend` is `"opaque"` ignores the alpha. ```teal tecs.gfx.particles.ParticleEmitter.tint: Color ``` #### tecs.gfx.particles.ParticleEmitter:burst Instance Emits `count` particles at the next step boundary. A stopped emitter ignores the call. The emitter truncates a count beyond its free capacity to the number that fits. ```teal function tecs.gfx.particles.ParticleEmitter.burst(self, count: number) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `ParticleEmitter` | | | `count` | `number` | | ##### Returns None. #### tecs.gfx.particles.ParticleEmitter:clear Instance Kills every live particle immediately. Use this for teardown and state transitions. It leaves emission unchanged, so a playing emitter starts filling again on the next step. ```teal function tecs.gfx.particles.ParticleEmitter.clear(self) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `ParticleEmitter` | | ##### Returns None. #### tecs.gfx.particles.ParticleEmitter:estimatedCount Instance Returns an estimate of how many particles remain alive. Integrates the schedule over the effect's `meanLifetime` and treats every emission older than that as expired. An effect whose `initial.lifetime` is one number has a mean equal to that lifetime, so the answer is exact but for the step the emitter is part way through; a lifetime range makes it an expectation, because the host does not know which particles drew the short lifetimes. A truncated burst or an overflow replacement moves it either way. Reading the exact count back from the GPU would stall the pipeline. ```teal function tecs.gfx.particles.ParticleEmitter.estimatedCount( self ): integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `ParticleEmitter` | | ##### Returns | Type | Description | | --- | --- | | `integer` | | #### tecs.gfx.particles.ParticleEmitter:finished Instance Returns whether this emitter has stopped emitting and its last particle has died. Exact, and it costs nothing: it is the schedule plus the longest lifetime against the emitter's own clock, all of which the host holds. This is what almost every "has the explosion finished" question is actually asking, and unlike a count it needs nothing read back from the GPU. ```teal function tecs.gfx.particles.ParticleEmitter.finished(self): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `ParticleEmitter` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | | #### tecs.gfx.particles.ParticleEmitter:pause Instance Stops emission and holds. The schedule and the particles both keep their state, and neither ages while this is on. ```teal function tecs.gfx.particles.ParticleEmitter.pause(self) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `ParticleEmitter` | | ##### Returns None. #### tecs.gfx.particles.ParticleEmitter:play Instance Starts or resumes emission. From stopped this begins the cycle again; from paused it carries on from where it was, with the steps spent paused not counted against the schedule. ```teal function tecs.gfx.particles.ParticleEmitter.play(self) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `ParticleEmitter` | | ##### Returns None. #### tecs.gfx.particles.ParticleEmitter:restart Instance Resets the schedule and the random sequence, and starts playing. Reproducible: a restart of an emitter with the same seed produces the same field, because every particle's draws are a pure function of the seed, the generation, the serial and the property, and all four start over. ```teal function tecs.gfx.particles.ParticleEmitter.restart(self) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `ParticleEmitter` | | ##### Returns None. #### tecs.gfx.particles.ParticleEmitter:stop Instance Stops emission and resets the schedule. Live particles drain. The next `play` starts the cycle from its beginning with a fresh random sequence, which is what makes it reproducible. Killing the field instead is `clear`, and the two are deliberately separate. ```teal function tecs.gfx.particles.ParticleEmitter.stop(self) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `ParticleEmitter` | | ##### Returns None. ### tecs.gfx.particles.Pool record Represents a world's installed particle pool. ```teal record tecs.gfx.particles.Pool capacity: integer active: function(self): boolean blended: function(self): integer casting: function(self): integer count: function(self): integer destroy: function(self) record: function( self, frame: Frame, instances: Buffer, bounds: Buffer ) takeDirty: function(self): {integer} write: function( self, floats: loader.CArray, bounds: loader.CArray, base: integer, first: integer, last: integer ) end ``` #### tecs.gfx.particles.Pool.capacity field Read-only. Reports how many live particles the pool can hold. ```teal tecs.gfx.particles.Pool.capacity: integer ``` #### tecs.gfx.particles.Pool:active Instance Whether there is anything to dispatch over. Published rather than derived, and this is the hazard the design named: an emitter dirties nothing, ever, while its whole field moves every frame. There is no archetype bit to read and no component write to observe, so a gate written as "nothing is dirty" would conclude wrongly every frame an emitter was running. The pool counts its own live emitters instead, and holds the gate open one frame past the last of them so the pass that hides their slots is the one that actually runs. ```teal function tecs.gfx.particles.Pool.active(self): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Pool` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | | #### tecs.gfx.particles.Pool:blended Instance Slots the forward pass may find a particle in. Reserved slots rather than live particles, and it has to be: what is alive lives in a buffer the CPU never reads, so the honest answer here is the only one that cannot be too small. Too small is the failure that matters, because the backend skips the whole forward lane on a frame nothing said was blended and every particle of every blended effect would then go missing. A slot with no live particle in it writes the hidden bound, so over-reporting costs the lane five compute passes over slots the mark pass has already rejected, and never a draw. ```teal function tecs.gfx.particles.Pool.blended(self): integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Pool` | | ##### Returns | Type | Description | | --- | --- | | `integer` | | #### tecs.gfx.particles.Pool:casting Instance Particles never write caster lane signs into their bounds. ```teal function tecs.gfx.particles.Pool.casting(self): integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Pool` | | ##### Returns | Type | Description | | --- | --- | | `integer` | | #### tecs.gfx.particles.Pool:count Instance ```teal function tecs.gfx.particles.Pool.count(self): integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Pool` | | ##### Returns | Type | Description | | --- | --- | | `integer` | | #### tecs.gfx.particles.Pool:destroy Instance Releases everything the pool owns. Safe to call more than once. ```teal function tecs.gfx.particles.Pool.destroy(self) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Pool` | | ##### Returns None. #### tecs.gfx.particles.Pool:record Instance ```teal function tecs.gfx.particles.Pool.record( self, frame: Frame, instances: Buffer, bounds: Buffer ) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Pool` | | | `frame` | `Frame` | | | `instances` | `Buffer` | | | `bounds` | `Buffer` | | ##### Returns None. #### tecs.gfx.particles.Pool:takeDirty Instance ```teal function tecs.gfx.particles.Pool.takeDirty(self): {integer} ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Pool` | | ##### Returns | Type | Description | | --- | --- | | `{integer}` | | #### tecs.gfx.particles.Pool:write Instance Hides the run and remembers where it landed. Only ever called on a frame the layout moved, since nothing else is ever dirty. What it writes is the hidden slot every reserved run writes, and the simulate pass overwrites the live ones later on the same frame: the flush is recorded before the dispatch, so a relayout cannot lose a particle. ```teal function tecs.gfx.particles.Pool.write( self, floats: loader.CArray, bounds: loader.CArray, base: integer, first: integer, last: integer ) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Pool` | | | `floats` | `loader.CArray` | | | `bounds` | `loader.CArray` | | | `base` | `integer` | | | `first` | `integer` | | | `last` | `integer` | | ##### Returns None. ### tecs.gfx.particles.PoolOptions record Configures `plugin`. ```teal record tecs.gfx.particles.PoolOptions renderer: Renderer capacity: integer maxEmitters: integer end ``` #### tecs.gfx.particles.PoolOptions.renderer field Caller-writable. Selects the renderer whose instance buffer the pool uses and whose device its buffers and pipelines are built on. Required, and an absent one raises. ```teal tecs.gfx.particles.PoolOptions.renderer: Renderer ``` #### tecs.gfx.particles.PoolOptions.capacity field Caller-writable. Sets how many live particles the pool holds across the world. Defaults to 16384. The value remains fixed at install: changing it would move the run, and moving the run lays out the whole scene again. ```teal tecs.gfx.particles.PoolOptions.capacity: integer ``` #### tecs.gfx.particles.PoolOptions.maxEmitters field Caller-writable. Sets how many emitters the pool holds at once. Defaults to 256. An emitter that arrives past the limit draws nothing and logs the refusal. ```teal tecs.gfx.particles.PoolOptions.maxEmitters: integer ``` ### tecs.gfx.particles.Range record The two-bound form of a `Value`. ```teal record tecs.gfx.particles.Range min: number max: number end ``` #### tecs.gfx.particles.Range.min field Caller-writable. Sets the inclusive lower bound. ```teal tecs.gfx.particles.Range.min: number ``` #### tecs.gfx.particles.Range.max field Caller-writable. Sets the inclusive upper bound. ```teal tecs.gfx.particles.Range.max: number ``` ### tecs.gfx.particles.RenderOptions record Configures how particles draw. ```teal global record tecs.gfx.particles.RenderOptions material: string materialParam: number sprite: Sprite sheet: Sheet tag: string layer: integer pivotX: number pivotY: number alignment: string stretch: number blend: string clip: integer end ``` #### tecs.gfx.particles.RenderOptions.material field Caller-writable. Selects the material that shades the quad by name. An absent value samples the image array and covers the whole quad. ```teal tecs.gfx.particles.RenderOptions.material: string ``` #### tecs.gfx.particles.RenderOptions.materialParam field Caller-writable. Passes one number to the material, clamped to zero through `0.999` and ignored unless `material` names one. The instance carries the material as `id + param` in one float, and the vertex shader reads the integer part as the selector and the fraction as the parameter, so `1.0` would arrive as the next material's id with a parameter of zero. Defaults to zero. ```teal tecs.gfx.particles.RenderOptions.materialParam: number ``` #### tecs.gfx.particles.RenderOptions.sprite field Caller-writable. Selects a registered image from `renderer.sprites:sprite`. An absent value draws a white quad. ```teal tecs.gfx.particles.RenderOptions.sprite: Sprite ``` #### tecs.gfx.particles.RenderOptions.sheet field Caller-writable. Selects a sheet to animate instead of a still region. The cycle is played exactly once over each particle's own life and clamps at the end, so a randomized lifetime randomizes the playback speed. Uses the same frame table an animated entity does, so per-frame durations, reverse and pingpong all arrive already spent. ```teal tecs.gfx.particles.RenderOptions.sheet: Sheet ``` #### tecs.gfx.particles.RenderOptions.tag field Caller-writable. Selects the sheet tag to animate. An absent value animates the whole sheet. A name the sheet does not carry animates the whole sheet too, and the sheet reports it at error level under the `tecs.gfx` logger, naming the tags it does carry. Defining the effect still succeeds, so a sheet re-exported without a tag keeps drawing. ```teal tecs.gfx.particles.RenderOptions.tag: string ``` #### tecs.gfx.particles.RenderOptions.layer field Caller-writable. Selects the render layer from one to `layers.MAX`. Defaults to one, and anything outside the range raises. ```teal tecs.gfx.particles.RenderOptions.layer: integer ``` #### tecs.gfx.particles.RenderOptions.pivotX field Caller-writable. Sets the horizontal pivot as a fraction of the frame from its top left. Defaults to `0.5`, the middle. ```teal tecs.gfx.particles.RenderOptions.pivotX: number ``` #### tecs.gfx.particles.RenderOptions.pivotY field Caller-writable. Sets the vertical pivot as a fraction of the frame from its top edge. Defaults to `0.5`, the middle. ```teal tecs.gfx.particles.RenderOptions.pivotY: number ``` #### tecs.gfx.particles.RenderOptions.alignment field Caller-writable. Selects `"fixed"` or `"velocity"` alignment. Velocity alignment adds the path's angle to the particle's own rotation, so a spinning spark aligned to its path still spins. Defaults to `"fixed"`, and anything else raises. ```teal tecs.gfx.particles.RenderOptions.alignment: string ``` #### tecs.gfx.particles.RenderOptions.stretch field Caller-writable. Multiplies the along-path axis when aligned to velocity. Defaults to one. ```teal tecs.gfx.particles.RenderOptions.stretch: number ``` #### tecs.gfx.particles.RenderOptions.blend field Caller-writable. Selects `"alpha"`, `"additive"`, or `"opaque"`. Defaults to `"alpha"`, and anything else raises. `"alpha"` and `"additive"` draw in the forward pass over the composited image, depth tested and not written, so a particle is still hidden by geometry in front of it. `"alpha"` keeps that much less of what is behind it and `"additive"` adds to it, so a dark additive particle changes nothing and overlapping ones brighten. `"opaque"` draws into the G-buffer instead. What it gives up is alpha, which the G-buffer is written with replace and has nowhere to put: a fade to transparent writes opaque over what was behind it, and the only fade left is a size curve reaching zero. What it buys is everything that reads the G-buffer, which is the occluder mask and being shadowed by one. ```teal tecs.gfx.particles.RenderOptions.blend: string ``` #### tecs.gfx.particles.RenderOptions.clip field Caller-writable. Selects the clip region that contains the fragments. Defaults to zero, which is no clipping. ```teal tecs.gfx.particles.RenderOptions.clip: integer ``` ### tecs.gfx.particles.ScheduleOptions record Configures when particles emit. ```teal global record tecs.gfx.particles.ScheduleOptions rate: number duration: number looping: boolean delay: number bursts: {ScheduleBurst} end ``` #### tecs.gfx.particles.ScheduleOptions.rate field Caller-writable. Sets the continuous emission rate in particles per second. Defaults to zero, which emits only what `bursts` asks for. ```teal tecs.gfx.particles.ScheduleOptions.rate: number ``` #### tecs.gfx.particles.ScheduleOptions.duration field Caller-writable. Sets the emission cycle duration in seconds. Defaults to zero, which leaves the cycle open. ```teal tecs.gfx.particles.ScheduleOptions.duration: number ``` #### tecs.gfx.particles.ScheduleOptions.looping field Caller-writable. Controls whether the emission cycle repeats. Defaults to false. ```teal tecs.gfx.particles.ScheduleOptions.looping: boolean ``` #### tecs.gfx.particles.ScheduleOptions.delay field Caller-writable. Sets the delay before emission starts in seconds. Defaults to zero. ```teal tecs.gfx.particles.ScheduleOptions.delay: number ``` #### tecs.gfx.particles.ScheduleOptions.bursts field Caller-writable. Defines at most four timed burst counts measured from the start of the cycle. A muzzle flash is one burst at time zero. Defaults to none, and a fifth raises. ```teal tecs.gfx.particles.ScheduleOptions.bursts: {ScheduleBurst} ``` ### tecs.gfx.particles.SpawnOptions record Configures where particles spawn. ```teal global record tecs.gfx.particles.SpawnOptions shape: string width: number height: number arc: number rotation: number distribution: string direction: number spread: number outward: boolean space: string inheritVelocity: number end ``` #### tecs.gfx.particles.SpawnOptions.shape field Caller-writable. Selects `"point"`, `"line"`, `"rectangle"`, `"rectangleEdge"`, `"disc"`, `"ring"`, or `"cone"`. Defaults to `"point"`, and anything else raises. ```teal tecs.gfx.particles.SpawnOptions.shape: string ``` #### tecs.gfx.particles.SpawnOptions.width field Caller-writable. Sets width for a line or rectangle and radius for a disc, ring, or cone. Defaults to zero. ```teal tecs.gfx.particles.SpawnOptions.width: number ``` #### tecs.gfx.particles.SpawnOptions.height field Caller-writable. Sets height for a rectangle. Every other shape ignores it. Defaults to zero. ```teal tecs.gfx.particles.SpawnOptions.height: number ``` #### tecs.gfx.particles.SpawnOptions.arc field Caller-writable. Sets the sampled sector of a disc, ring, or cone in radians. Defaults to a full turn. ```teal tecs.gfx.particles.SpawnOptions.arc: number ``` #### tecs.gfx.particles.SpawnOptions.rotation field Caller-writable. Rotates the sampled area in radians, independently of launch direction. Defaults to zero. ```teal tecs.gfx.particles.SpawnOptions.rotation: number ``` #### tecs.gfx.particles.SpawnOptions.distribution field Caller-writable. Selects `"uniform"` or `"normal"` sampling. A gaussian spread makes a column read as a column rather than as a slab. Defaults to `"uniform"`, and anything else raises. ```teal tecs.gfx.particles.SpawnOptions.distribution: string ``` #### tecs.gfx.particles.SpawnOptions.direction field Caller-writable. Sets the launch direction in radians and centers the full cone on it, so a particle leaves within plus or minus half of `spread`. Half-angle is the other plausible reading of `spread` and a mismatch is silent, so it is said here. Defaults to zero. ```teal tecs.gfx.particles.SpawnOptions.direction: number ``` #### tecs.gfx.particles.SpawnOptions.spread field Caller-writable. Sets the full cone width in radians. Defaults to zero, which launches every particle along `direction`. ```teal tecs.gfx.particles.SpawnOptions.spread: number ``` #### tecs.gfx.particles.SpawnOptions.outward field Caller-writable. Launches along the shape's outward normal rather than along `direction`. Meaningful for the shapes that have one: disc, ring, cone and a rectangle's edge. Defaults to false. ```teal tecs.gfx.particles.SpawnOptions.outward: boolean ``` #### tecs.gfx.particles.SpawnOptions.space field Caller-writable. Selects `"local"` or `"world"` space. Local particles follow the emitter's transform for their whole life; world particles read it once, at spawn, and then move independently. Neither is a good universal default, so it is explicit and defaults to `"world"`. Anything else raises. ```teal tecs.gfx.particles.SpawnOptions.space: string ``` #### tecs.gfx.particles.SpawnOptions.inheritVelocity field Caller-writable. Sets how much emitter velocity a particle inherits, from zero to one. Defaults to zero, and world space is the only space it means anything in: a local particle is already carried by the emitter, so inheriting would carry it twice. ```teal tecs.gfx.particles.SpawnOptions.inheritVelocity: number ``` ### tecs.gfx.particles.UpdateOptions record Configures how a particle changes over its normalized lifetime. ```teal global record tecs.gfx.particles.UpdateOptions drag: number radialAcceleration: Value tangentialAcceleration: Value size: Curve color: Gradient end ``` #### tecs.gfx.particles.UpdateOptions.drag field Caller-writable. Sets damping in units of one over seconds, applied as `velocity = velocity / (1 + drag * dt)`, so it composes with any step. Defaults to zero. ```teal tecs.gfx.particles.UpdateOptions.drag: number ``` #### tecs.gfx.particles.UpdateOptions.radialAcceleration field Caller-writable. Sets acceleration away from the emitter. This and `tangentialAcceleration` are the difference between a fountain and a vortex. Defaults to zero. ```teal tecs.gfx.particles.UpdateOptions.radialAcceleration: Value ``` #### tecs.gfx.particles.UpdateOptions.tangentialAcceleration field Caller-writable. Sets acceleration at right angles to the emitter. Defaults to zero. ```teal tecs.gfx.particles.UpdateOptions.tangentialAcceleration: Value ``` #### tecs.gfx.particles.UpdateOptions.size field Caller-writable. Multiplies each particle's size over its life. An absent curve multiplies by one, so leaving it out costs nothing. ```teal tecs.gfx.particles.UpdateOptions.size: Curve ``` #### tecs.gfx.particles.UpdateOptions.color field Caller-writable. Multiplies each particle's color over its life. An absent gradient multiplies by white, so leaving it out costs nothing. Its alpha fades a blended effect and is ignored by an effect whose `render.blend` is `"opaque"`. ```teal tecs.gfx.particles.UpdateOptions.color: Gradient ``` ### tecs.gfx.particles.Value type Accepted wherever a property may vary per particle. ```teal type tecs.gfx.particles.Value = Value ``` ## Functions ### tecs.gfx.particles.find Static Returns the effect registered under `name`, or nil when none exists. ```teal function tecs.gfx.particles.find(name: string): Effect ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `name` | `string` | Nil answers nil rather than raising, so a restore holding no name takes the same path as one holding an unknown name. | #### Returns | Type | Description | | --- | --- | | [`Effect`](/modules/gfx/particles/#tecs.gfx.particles.Effect) | The registered effect itself, not a copy, so two callers finding one name share it. Nil when nothing has that name. | ### tecs.gfx.particles.names Static Returns every registered effect name in registration order. ```teal function tecs.gfx.particles.names(): {string} ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | `{string}` | A fresh table each call, the caller's to keep and to modify. | ### tecs.gfx.particles.plugin Static Installs the pool on a world. Without it, Tecs allocates and dispatches no particle work. ```teal function tecs.gfx.particles.plugin(options: PoolOptions): function( World ) ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `options` | [`PoolOptions`](/modules/gfx/particles/#tecs.gfx.particles.PoolOptions) | Requires `renderer`; both capacities have defaults. | #### Returns | Type | Description | | --- | --- | | `function(`[`World`](/modules/ecs/#tecs.World)`)` | Returns the plugin to install on a world. Installing it on a world that already has a pool does nothing, and installing it on two worlds gives each its own run of the one renderer's buffer. | ### tecs.gfx.particles.poolOf Static Returns the pool installed on a world, or nil. Tests and tooling use this function. ```teal function tecs.gfx.particles.poolOf(world: World): Pool ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`World`](/modules/ecs/#tecs.World) | Accepts a world with or without the plugin. | #### Returns | Type | Description | | --- | --- | | [`Pool`](/modules/gfx/particles/#tecs.gfx.particles.Pool) | Returns the world's live pool, or nil when the plugin was never installed. The function does not copy the pool. | ### tecs.gfx.particles.reset Static Forgets every registered effect, so a spec can register a set again. Every handle already handed out goes stale with it. ```teal function tecs.gfx.particles.reset() ``` #### Arguments None. #### Returns None. ## Values ### tecs.gfx.particles.CURVE_SAMPLES variable Read-only. Reports how many samples one compiled curve or gradient holds. ```teal tecs.gfx.particles.CURVE_SAMPLES: integer ``` ### tecs.gfx.particles.EFFECT variable Read-only. Reports each field's offset within an effect record. Published because the shaders state the same offsets and a disagreement between the two is silent: it draws something, just not the right thing. `spec/particles_spec.lua` reads both and holds them to each other, which is the only reason this is on the surface. ```teal tecs.gfx.particles.EFFECT: {string: integer} ``` ### tecs.gfx.particles.EFFECT_FLOATS variable Read-only. Reports how many floats each effect record contains. ```teal tecs.gfx.particles.EFFECT_FLOATS: integer ``` ### tecs.gfx.particles.EMITTER variable Read-only. Reports each field's float offset within an emitter record, counting from zero. ```teal tecs.gfx.particles.EMITTER: {string: integer} ``` ### tecs.gfx.particles.EMITTER_FLOATS variable Read-only. Reports how many floats each emitter record contains. An emitter record is much the smaller of the two. ```teal tecs.gfx.particles.EMITTER_FLOATS: integer ``` ### tecs.gfx.particles.STATE_FLOATS variable Read-only. Reports how many GPU state floats one particle carries. ```teal tecs.gfx.particles.STATE_FLOATS: integer ``` --- ## tecs # tecs ## Module contents ### Submodules | Submodule | Description | | --- | --- | | [`tecs.Application`](/modules/Application/) | The application lifecycle, entry plugin, frame, checkpoints, and crash state | | [`tecs.assets`](/modules/assets/) | Coroutine-native byte, image, sound, and animated glTF loading with skins and morph targets | | [`tecs.audio`](/modules/audio/) | Clips, voices, groups, limits, entity sounds, snapshots, and audio devices | | [`tecs.data`](/modules/data/) | Typed stores, JSON, byte encodings, DEFLATE, text transcoding, UTF-8, UUIDs, hashes, and checksums | | [`tecs.debug`](/modules/debug/) | Debugger commands declared once and projected as debug-server tools with input and output schemas | | [`tecs.ecs`](/modules/ecs/) | Worlds, entities, components, systems, plugins, queries, archetypes, phases, and resources | | [`tecs.events`](/modules/events/) | Typed event definitions and address-based message buses | | [`tecs.gfx`](/modules/gfx/) | 2D and 3D cameras, render components, meshes, the GPU renderer, images, shadows, and text | | [`tecs.input`](/modules/input/) | Gameplay input in three tiers behind a layer stack, with gamepads and standalone sensors | | [`tecs.io`](/modules/io/) | Synchronous binary streams, cooperative sockets, HTTP, files, and external tools | | [`tecs.log`](/modules/log/) | Named loggers, priority filtering, platform output, and JSON Lines files | | [`tecs.math`](/modules/math/) | Angle wrapping and shortest turns, with two-dimensional geometry beneath them. | | [`tecs.physics`](/modules/physics/) | Entity-first Rapier 2D rigid bodies, colliders, queries, contacts, sensors, and snapshots | | [`tecs.platform`](/modules/platform/) | Platform events, operating-system services, time, and windows | | [`tecs.regex`](/modules/regex/) | Compiled Rust regular expressions over Lua byte strings | | [`tecs.sequence`](/modules/sequence/) | Snapshot-safe programs, waits, ownership, actions, and tween timelines | | [`tecs.ui`](/modules/ui/) | Retained layout, clipping, scrolling, and interaction for composed UI entities | | [`tecs.workers`](/modules/workers/) | Separate Lua states joined by serialized channels, whose results and call replies a system waits for without blocking the frame | ### Constructors | Constructor | Description | | --- | --- | | [`newApplication`](/modules/#tecs.newApplication) | Builds the application an entry file returns. | ### Types | Type | Kind | Description | | --- | --- | --- | | [`Closeable`](/modules/#tecs.Closeable) | interface | An owned value that releases its active lifetime explicitly. | | [`Scope`](/modules/#tecs.Scope) | interface | Scope owns resources while preserving each concrete type. | ### Functions | Function | Kind | Description | | --- | --- | --- | | [`batch`](/modules/#tecs.batch) | Static | Runs callbacks at the same time and returns their results in order. | | [`scoped`](/modules/#tecs.scoped) | Static | Runs a callback inside a fresh named resource scope and JIT zone. | ### Values | Value | Type | Description | | --- | --- | --- | | [`version`](/modules/#tecs.version) | `string` | Read-only. Version of this build. | ## Constructors ### tecs.newApplication Static Builds the application an entry file returns. Not a function that runs until done: an entry file ends with `return tecs.newApplication(config)` and the host drives it from there, because a platform that never hands control back has no loop to block in. ```teal function tecs.newApplication( config: Application.Config ): Application ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `config` | [`Application.Config`](/modules/Application/#tecs.Application.Config) | Read here, so a field changed on the table afterwards is not seen. Nothing is opened yet: the window, the GPU device and the audio mixer are the host's to bring up on the first callback, which is what lets an entry file be loaded by a tool that never runs one. | #### Returns | Type | Description | | --- | --- | | [`Application`](/modules/Application/) | The application the host drives through its init, event, iterate and quit callbacks. Returning it is the whole contract; a game that calls into it itself is doing the host's job. | ## Types ### tecs.Closeable interface An owned value that releases its active lifetime explicitly. `close` may report a delayed finalization failure. Every implementation returns true on success. Its own contract defines whether repeated calls are safe. ```teal interface tecs.Closeable close: function(self): boolean, string end ``` #### tecs.Closeable:close Instance Releases the value's active lifetime. ```teal function tecs.Closeable.close(self): boolean, string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Closeable` | The value to close. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns true when finalization succeeds. | | `string` | Returns a finalization reason when the first return is false. | #### Examples ```teal local destination = tecs.io.newFileStream("save.bin") local writer = assert(destination:newWriter()) assert(writer:write("complete")) assert(writer:close()) destination:close() ``` ### tecs.Scope interface `Scope` owns resources while preserving each concrete type. ```teal interface tecs.Scope name: string own: function(self, value: T, reason: string): T end ``` #### tecs.Scope.name field Read-only. Reports the diagnostic name supplied to `scoped`. ```teal tecs.Scope.name: string ``` #### tecs.Scope:own Instance Registers one owned value for cleanup and returns it unchanged. The scope is active only inside the callback passed to `scoped`. Calling a captured scope after that callback returns, raises, or begins cleanup raises instead of creating an ownership obligation nobody will run. Passing nil raises with `reason` when one is supplied, which lets a constructor's `value, reason` result pass directly to `own`. Without a supplied reason, or with a value that has no `close` method, `own` raises its own argument error. ```teal function tecs.Scope.own( self, value: T, reason: string ): T ``` ##### Type Parameters | Name | Constraint | Description | | --- | --- | --- | | `T` | [`Closeable`](/modules/#tecs.Closeable) | | ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Scope` | The active scope that will own the value. | | `value` | `T` | An open [`Closeable`](/modules/#tecs.Closeable) or Lua file. Each call adds one cleanup obligation, including repeated calls with the same value. | | `reason` | `string` | The operational failure returned beside a nil value. `own` raises this string at the call site. | ##### Returns | Type | Description | | --- | --- | | `T` | The same value with its concrete type unchanged. | ## Functions ### tecs.batch Static Runs callbacks at the same time and returns their results in order. Inside a system the call suspends until every callback settles, and outside one it blocks while driving the producers they wait on. The first callback that raises, in the order the callbacks were given, cancels the rest, waits for them to unwind, and raises that failure. Each callback reports once. Callbacks return values for the caller to apply; staging world mutations inside them makes entity identifiers depend on completion order. ```teal function tecs.batch(bodies: {function(): any}): {any} ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `bodies` | `{function(): any}` | The caller supplies an array of functions that take no arguments. An empty array returns an empty array without waiting. | #### Returns | Type | Description | | --- | --- | | `{any}` | Returns one result per callback, at the callback's index, and that result is the callback's first return value. A batch mixes types freely, so the caller casts each result to what its own callback returned. | ### tecs.scoped Static Runs a callback inside a fresh named resource scope and JIT zone. The callback may suspend through a cooperative engine call inside a system. Every value it registers closes in reverse order before this function returns or propagates a failure. World shutdown also cancels and unwinds a suspended system. Cleanup begins by making the scope inactive, and one failing close does not prevent the remaining closes. ```teal function tecs.scoped(name: string, body: function(Scope)) ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `name` | `string` | The non-empty diagnostic and profiler-zone name. | | `body` | `function(`[`Scope`](/modules/#tecs.Scope)`)` | The callback that receives the only active [`Scope`](/modules/#tecs.Scope) for this invocation. Returned values are ignored, and registered resources must not be used after the callback ends. | #### Returns None. ## Values ### tecs.version variable Read-only. Version of this build. ```teal tecs.version: string ``` --- ## tecs.input # tecs.input ## Gameplay input `tecs.input` folds typed platform events into gameplay input. The application creates one [`Input`](/modules/input/#tecs.input.Input) as `app.input`; ordinary game code does not construct or feed it. The module owns replay, layers, edge detection and fixed-step latching. ### Three input tiers Live state answers whether a key or button remains held. Frame edges answer whether it changed this frame. Latched edges answer that question for the next fixed step. The same `keyPressed`, `mousePressed` and `Gamepad:buttonPressed` methods select frame or latched edges from the phase. The engine consumes a latched edge in the first fixed step after it arrived. ### Layer capture A blocking layer hides every device from lower layers. Calls that omit a layer read the base layer, so gameplay becomes quiet while a menu owns capture. Moving capture clears pending edges, text, wheel movement and mouse deltas. ```teal local pause = app.input:pushLayer("pause") if app.input:keyPressed("Escape", pause) then app.input:popLayer() end -- The base layer cannot read while pause owns capture. if app.input:keyDown("W") then movePlayer() end ``` Pass false to `pushLayer` for an observing overlay. Because capture does not move, the overlay does not clear edges. ### Focus and text input Focus loss releases held keys, mouse buttons, gamepad buttons, fingers, modifiers and pen pressure. The resulting release edges let a game finish the active action. Bind text input to the layer that owns the field. Popping the layer stops the input method and clears composition. ```teal local field = app.input:pushLayer("playerName") app.input:startTextInput( field, { area = { x = 40, y = 200, width = 320, height = 24, cursor = 80 }, } ) ``` Append `input.text` once per frame. Draw `input.composition` until the input method commits it. Update the text area when the field scrolls or its caret moves. ### Gamepads and sensors `Input:gamepads` returns a live connection-order list. Hold a [`Gamepad`](/modules/input/#tecs.input.Gamepad) object or match its `guid` to follow hardware because list positions move and instance ids do not survive a reconnect. Gamepad queries use the same layers and fixed-step tiers as keyboard and mouse queries. Standalone sensors do not belong to a gamepad. Enumerate them with `tecs.input.sensors`, open one by instance id, poll it, and close it explicitly. ## Gamepads Each [`Gamepad`](/modules/input/#tecs.input.Gamepad) owns one device's identity, event state, capabilities, and outputs. A retained reference remains safe after disconnection: queries return neutral values, outputs return false, and `connected` stays false. A reconnect creates a new object rather than reviving the old one. Buttons use positional names. `"south"` names the button nearest the player on every pad. `label("south")` returns the hardware label, such as `"a"` or `"cross"`, for a prompt. ```teal local pad = app.input:gamepad(1) if pad ~= nil and pad.connected then local moveX = pad:axis("leftX") if pad:buttonPressed("south") then jump() end end ``` Axes use a 0.15 deadzone by default. Output methods return false when the device disconnects or lacks the requested hardware, so optional rumble and lights need no capability branch. Gamepad sensors remain off until `enableSensor` starts their event stream. Their readings pass through the same layer stack as buttons and replay from recorded events. ## Standalone sensors Standalone accelerometers, gyroscopes and platform-specific sensors do not belong to a gamepad. Enumerate a snapshot, open the selected instance id, poll it, and close it explicitly. ```teal for _, device in ipairs(tecs.input.sensors()) do local sensor , reason = tecs.input.openSensor( device.id ) if sensor ~= nil then local values , readError = sensor:read() if values ~= nil then print(sensor.name, values[1], values[2], values[3]) else print(readError) end sensor:destroy() else print(reason) end end ``` `Sensor:read` returns three values by default and accepts one to sixteen for a platform-specific sensor. Acceleration uses meters per second squared and rotation uses radians per second. ## Module contents ### Constructors | Constructor | Description | | --- | --- | | [`newInput`](/modules/input/#tecs.input.newInput) | Builds the input state a game reads, with a single base layer. | ### Types | Type | Kind | Description | | --- | --- | --- | | [`Device`](/modules/input/#tecs.input.Device) | record | Describes one enumerated sensor before open. | | [`Gamepad`](/modules/input/#tecs.input.Gamepad) | record | A Gamepad owns one attached gamepad and folds its event state. | | [`Input`](/modules/input/#tecs.input.Input) | record | An Input folds platform events once a frame and exposes live state, frame edges and fixed-step latched edges. | | [`Layer`](/modules/input/#tecs.input.Layer) | record | Identifies a position from pushLayer. | | [`Options`](/modules/input/#tecs.input.Options) | record | Describes framework construction options for Input. | | [`Sensor`](/modules/input/#tecs.input.Sensor) | record | Describes an opened standalone sensor. | | [`SensorDevice`](/modules/input/#tecs.input.SensorDevice) | record | Describes an enumerated standalone sensor before open. | | [`TextArea`](/modules/input/#tecs.input.TextArea) | record | Describes a caller-writable text rectangle. | | [`TextOptions`](/modules/input/#tecs.input.TextOptions) | record | Describes caller-writable text-input options. | | [`Touch`](/modules/input/#tecs.input.Touch) | record | Describes a finger from touches. | | [`TouchpadFinger`](/modules/input/#tecs.input.TouchpadFinger) | type | Describes a gamepad touch. | ### Functions | Function | Kind | Description | | --- | --- | --- | | [`openSensor`](/modules/input/#tecs.input.openSensor) | Static | Opens an attached standalone sensor by instance id. | | [`sensors`](/modules/input/#tecs.input.sensors) | Static | Returns the standalone sensors attached now in platform order. | ## Constructors ### tecs.input.newInput Static Builds the input state a game reads, with a single base layer. Holds no devices yet. `refreshDevices` opens the pads already attached, and everything after that arrives as events. ```teal function tecs.input.newInput(options: InputOptions): Input ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `options` | [`InputOptions`](/modules/input/#tecs.input.Options) | The engine omits this record for a headless state. Keys and pads still fold while window commands report failure. | #### Returns | Type | Description | | --- | --- | | [`Input`](/modules/input/#tecs.input.Input) | Returns the state. The application owns one; a test may build another. | ## Types ### tecs.input.Device record Describes one enumerated sensor before open. ```teal record tecs.input.Device id: number name: string kind: string platformType: integer end ``` #### tecs.input.Device.id field Read-only. The platform sets `id` to an instance id that remains valid while the device stays attached. ```teal tecs.input.Device.id: number ``` #### tecs.input.Device.name field Read-only. The platform sets `name` to the device display name. ```teal tecs.input.Device.name: string ``` #### tecs.input.Device.kind field Read-only. The platform sets `kind` to `"accelerometer"`, `"gyroscope"`, a left or right variant, or `"unknown"`. ```teal tecs.input.Device.kind: string ``` #### tecs.input.Device.platformType field Read-only. The platform sets `platformType` to its private type number, or zero when none exists. Ordinary game code ignores this field. ```teal tecs.input.Device.platformType: integer ``` ### tecs.input.Gamepad record A `Gamepad` owns one attached gamepad and folds its event state. Input owns the object. Callers use its methods and treat public fields as read-only snapshots that change on connection and remap events. Read-only. `Gamepad` names one attached device. Ordinary game code gets objects from `Input:gamepads` and does not replace this module field. ```teal record tecs.input.Gamepad record TouchpadFinger touchpad: integer finger: integer x: number y: number pressure: number down: boolean end id: number connected: boolean name: string kind: string guid: string path: string playerIndex: integer touchpads: integer openGamepad: function( backend: Backend, gate: Gate, id: number ): Gamepad axis: function( self, axis: string | integer, deadzone: number, layer: Layer ): number buttonDown: function( self, button: string | integer, layer: Layer ): boolean buttonPressed: function( self, button: string | integer, layer: Layer ): boolean buttonReleased: function( self, button: string | integer, layer: Layer ): boolean enableSensor: function( self, sensor: string | integer, enabled: boolean ): boolean hasAxis: function(self, axis: string | integer): boolean hasButton: function(self, button: string | integer): boolean hasSensor: function(self, sensor: string | integer): boolean label: function(self, button: string | integer): string power: function(self): string, integer rumble: function( self, low: number, high: number, seconds: number ): boolean rumbleTriggers: function( self, left: number, right: number, seconds: number ): boolean sensor: function( self, sensor: string | integer, layer: Layer ): number, number, number sensorEnabled: function(self, sensor: string | integer): boolean setLED: function( self, red: number, green: number, blue: number ): boolean setPlayerIndex: function(self, index: integer): boolean touchpadFingers: function( self, touchpad: integer, layer: Layer ): {TouchpadFinger} end ``` #### tecs.input.Gamepad.TouchpadFinger record `TouchpadFinger` reports one finger from the latest touchpad event. Gamepad owns and reuses the record. Callers treat every field as read-only. Read-only. Exposes [`TouchpadFinger`](/modules/input/#tecs.input.TouchpadFinger), whose records `touchpadFingers` returns. Ordinary game code does not replace this field. ```teal record tecs.input.Gamepad.TouchpadFinger touchpad: integer finger: integer x: number y: number pressure: number down: boolean end ``` ##### tecs.input.Gamepad.TouchpadFinger.touchpad field Read-only. Gamepad sets `touchpad` to the zero-based touchpad index when it creates the record. ```teal tecs.input.Gamepad.TouchpadFinger.touchpad: integer ``` ##### tecs.input.Gamepad.TouchpadFinger.finger field Read-only. Gamepad sets `finger` to the zero-based slot on that touchpad. The value identifies a position, not a persistent finger. ```teal tecs.input.Gamepad.TouchpadFinger.finger: integer ``` ##### tecs.input.Gamepad.TouchpadFinger.x field Read-only. Gamepad updates `x` from 0 to 1 across the touchpad. ```teal tecs.input.Gamepad.TouchpadFinger.x: number ``` ##### tecs.input.Gamepad.TouchpadFinger.y field Read-only. Gamepad updates `y` from 0 to 1 down the touchpad. ```teal tecs.input.Gamepad.TouchpadFinger.y: number ``` ##### tecs.input.Gamepad.TouchpadFinger.pressure field Read-only. Gamepad updates `pressure` from 0 to 1 when the device reports it. ```teal tecs.input.Gamepad.TouchpadFinger.pressure: number ``` ##### tecs.input.Gamepad.TouchpadFinger.down field Read-only. Gamepad updates `down` when the finger enters or leaves the pad and keeps the record for reuse. ```teal tecs.input.Gamepad.TouchpadFinger.down: boolean ``` #### tecs.input.Gamepad.id field Read-only. Input sets `id` to the platform instance id when it opens the device and never changes it. ```teal tecs.input.Gamepad.id: number ``` #### tecs.input.Gamepad.connected field Read-only. Input sets `connected` true at open and false permanently when the device disconnects. ```teal tecs.input.Gamepad.connected: boolean ``` #### tecs.input.Gamepad.name field Read-only. Input refreshes `name` at open and on remap. The platform chooses it, so saved bindings use `guid` instead. ```teal tecs.input.Gamepad.name: string ``` #### tecs.input.Gamepad.kind field Read-only. Input refreshes `kind` at open and on remap. It contains a platform device family such as `"xboxOne"`, `"ps5"` or `"standard"`. ```teal tecs.input.Gamepad.kind: string ``` #### tecs.input.Gamepad.guid field Read-only. Input refreshes `guid` at open and on remap. Game code uses it to match saved bindings. ```teal tecs.input.Gamepad.guid: string ``` #### tecs.input.Gamepad.path field Read-only. Input refreshes `path` at open and on remap. It contains an empty string when the platform declines to report one. ```teal tecs.input.Gamepad.path: string ``` #### tecs.input.Gamepad.playerIndex field Read-only. Input refreshes `playerIndex` on open, remap and a successful `setPlayerIndex`. It contains -1 when the device has no slot. ```teal tecs.input.Gamepad.playerIndex: integer ``` #### tecs.input.Gamepad.touchpads field Read-only. Input refreshes `touchpads` at open and on remap. ```teal tecs.input.Gamepad.touchpads: integer ``` #### tecs.input.Gamepad.openGamepad Static Engine-owned. Opens a device for an Input object. Ordinary game code gets gamepads from `Input:gamepads` and must not call this function. ```teal function tecs.input.Gamepad.openGamepad( backend: Backend, gate: Gate, id: number ): Gamepad ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `backend` | `Backend` | Input supplies the platform backend. | | `gate` | `Gate` | Input supplies its shared layer gate. | | `id` | `number` | Input supplies the platform instance id. | ##### Returns | Type | Description | | --- | --- | | [`Gamepad`](/modules/input/#tecs.input.Gamepad) | Returns the opened gamepad, or nil when the device disappeared. | #### tecs.input.Gamepad:axis Instance Returns an axis value after applying a deadzone. Sticks return -1 to 1 and triggers return 0 to 1. Values outside the deadzone keep their original magnitude. ```teal function tecs.input.Gamepad.axis( self, axis: string | integer, deadzone: number, layer: Layer ): number ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Gamepad` | | | `axis` | string | integer | The caller supplies a name or platform number. An unknown name raises. | | `deadzone` | `number` | The caller supplies the zero threshold or omits it for the hardware-oriented default of 0.15. | | `layer` | [`Layer`](/modules/input/#tecs.input.Layer) | The caller omits this value to read the base layer. | ##### Returns | Type | Description | | --- | --- | | `number` | Returns zero when the layer cannot read or no event has set the axis. | #### tecs.input.Gamepad:buttonDown Instance Returns whether a button is held. ```teal function tecs.input.Gamepad.buttonDown( self, button: string | integer, layer: Layer ): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Gamepad` | | | `button` | string | integer | The caller supplies a positional name or platform number. An unknown name raises. | | `layer` | [`Layer`](/modules/input/#tecs.input.Layer) | The caller omits this value to read the base layer. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns false when the layer cannot read. | #### tecs.input.Gamepad:buttonPressed Instance Returns whether a button went down in the active tier. ```teal function tecs.input.Gamepad.buttonPressed( self, button: string | integer, layer: Layer ): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Gamepad` | | | `button` | string | integer | The caller supplies a positional name or platform number. An unknown name raises. | | `layer` | [`Layer`](/modules/input/#tecs.input.Layer) | The caller omits this value to read the base layer. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns false when the layer cannot read. | #### tecs.input.Gamepad:buttonReleased Instance Returns whether a button came up in the active tier. ```teal function tecs.input.Gamepad.buttonReleased( self, button: string | integer, layer: Layer ): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Gamepad` | | | `button` | string | integer | The caller supplies a positional name or platform number. An unknown name raises. | | `layer` | [`Layer`](/modules/input/#tecs.input.Layer) | The caller omits this value to read the base layer. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns false when the layer cannot read. | #### tecs.input.Gamepad:enableSensor Instance Enables or disables a gamepad sensor event stream. ```teal function tecs.input.Gamepad.enableSensor( self, sensor: string | integer, enabled: boolean ): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Gamepad` | | | `sensor` | string | integer | The caller supplies a sensor name or platform number. | | `enabled` | `boolean` | The caller passes false to disable the stream; nil and true enable it. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns whether the device accepted the change. | #### tecs.input.Gamepad:hasAxis Instance Returns whether the device carries an axis. ```teal function tecs.input.Gamepad.hasAxis( self, axis: string | integer ): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Gamepad` | | | `axis` | string | integer | The caller supplies a name or platform number. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns false after disconnection. | #### tecs.input.Gamepad:hasButton Instance Returns whether the device carries a button. ```teal function tecs.input.Gamepad.hasButton( self, button: string | integer ): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Gamepad` | | | `button` | string | integer | The caller supplies a name or platform number. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns false after disconnection. | #### tecs.input.Gamepad:hasSensor Instance Returns whether the device carries a sensor. ```teal function tecs.input.Gamepad.hasSensor( self, sensor: string | integer ): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Gamepad` | | | `sensor` | string | integer | The caller supplies `"gyro"`, `"accelerometer"` or a platform number. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns false after disconnection. | #### tecs.input.Gamepad:label Instance Returns the hardware label printed on a button. ```teal function tecs.input.Gamepad.label( self, button: string | integer ): string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Gamepad` | | | `button` | string | integer | The caller supplies a positional name or platform number. | ##### Returns | Type | Description | | --- | --- | | `string` | Returns a label such as `"a"` or `"cross"`, or `"unknown"` after disconnection. | #### tecs.input.Gamepad:power Instance Returns the power state and charge. ```teal function tecs.input.Gamepad.power(self): string, integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Gamepad` | | ##### Returns | Type | Description | | --- | --- | | `string` | Returns `"onBattery"`, `"charging"`, `"charged"`, `"noBattery"`, `"unknown"` or `"error"`. | | `integer` | Returns the percentage from 0 to 100, or -1 when unavailable. | #### tecs.input.Gamepad:rumble Instance Rumbles the low- and high-frequency motors. A new call replaces an active effect. ```teal function tecs.input.Gamepad.rumble( self, low: number, high: number, seconds: number ): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Gamepad` | | | `low` | `number` | The caller supplies the low-frequency strength from 0 to 1. | | `high` | `number` | The caller supplies the high-frequency strength from 0 to 1. | | `seconds` | `number` | The caller supplies the duration in seconds. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns false after disconnection or without rumble hardware. | #### tecs.input.Gamepad:rumbleTriggers Instance Rumbles the left and right trigger motors. ```teal function tecs.input.Gamepad.rumbleTriggers( self, left: number, right: number, seconds: number ): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Gamepad` | | | `left` | `number` | The caller supplies the left strength from 0 to 1. | | `right` | `number` | The caller supplies the right strength from 0 to 1. | | `seconds` | `number` | The caller supplies the duration in seconds. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns false after disconnection or without trigger motors. | #### tecs.input.Gamepad:sensor Instance Returns the latest event reading from a gamepad sensor. ```teal function tecs.input.Gamepad.sensor( self, sensor: string | integer, layer: Layer ): number, number, number ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Gamepad` | | | `sensor` | string | integer | The caller supplies a sensor name or platform number. | | `layer` | [`Layer`](/modules/input/#tecs.input.Layer) | The caller omits this value to read the base layer. | ##### Returns | Type | Description | | --- | --- | | `number` | Returns the x component, or zero when unreadable. | | `number` | Returns the y component, or zero when unreadable. | | `number` | Returns the z component, or zero when unreadable. | #### tecs.input.Gamepad:sensorEnabled Instance Returns whether a gamepad sensor is streaming. ```teal function tecs.input.Gamepad.sensorEnabled( self, sensor: string | integer ): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Gamepad` | | | `sensor` | string | integer | The caller supplies a sensor name or platform number. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns live device state and false after disconnection. | #### tecs.input.Gamepad:setLED Instance Sets the gamepad light color. ```teal function tecs.input.Gamepad.setLED( self, red: number, green: number, blue: number ): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Gamepad` | | | `red` | `number` | The caller supplies the red channel from 0 to 1. | | `green` | `number` | The caller supplies the green channel from 0 to 1. | | `blue` | `number` | The caller supplies the blue channel from 0 to 1. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns false after disconnection or without a light. | #### tecs.input.Gamepad:setPlayerIndex Instance Assigns the platform player slot. A successful call updates the read-only `playerIndex` field. ```teal function tecs.input.Gamepad.setPlayerIndex( self, index: integer ): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Gamepad` | | | `index` | `integer` | The caller supplies a zero-based slot or -1 for none. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns whether the device accepted the slot. | #### tecs.input.Gamepad:touchpadFingers Instance Returns the fingers currently on a gamepad touchpad. ```teal function tecs.input.Gamepad.touchpadFingers( self, touchpad: integer, layer: Layer ): {TouchpadFinger} ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Gamepad` | | | `touchpad` | `integer` | The caller supplies a zero-based index or omits it for the first touchpad. | | `layer` | [`Layer`](/modules/input/#tecs.input.Layer) | The caller omits this value to read the base layer. | ##### Returns | Type | Description | | --- | --- | | `{`[`TouchpadFinger`](/modules/input/#tecs.input.Gamepad.TouchpadFinger)`}` | Returns a new list of Gamepad-owned records. Callers copy values they need to retain. | ### tecs.input.Input record An `Input` folds platform events once a frame and exposes live state, frame edges and fixed-step latched edges. ```teal record tecs.input.Input record Layer name: string blocking: boolean index: integer end record Touch device: string finger: string x: number y: number normalX: number normalY: number pressure: number end record TextArea x: integer y: integer width: integer height: integer cursor: integer end record TextOptions area: TextArea end Options: InputOptions mouseX: number mouseY: number mouseDeltaX: number mouseDeltaY: number wheelX: number wheelY: number wheelPreferredX: number wheelPreferredY: number wheelTicksX: integer wheelTicksY: integer mouseWhich: number mouseSynthetic: boolean text: string composition: string compositionStart: integer compositionLength: integer penX: number penY: number penPressure: number penTiltX: number penTiltY: number penRotation: number penTouching: boolean penEraser: boolean penWhich: number beginFrame: function(self) canRead: function(self, layer: Layer): boolean captureMouse: function(self, enabled: boolean): boolean cursorVisible: function(self): boolean destroy: function(self) enterFixedPhase: function(self) exitFixedPhase: function(self) gamepad: function(self, index: integer): Gamepad gamepadById: function(self, id: number): Gamepad gamepads: function(self): {Gamepad} handleEvent: function(self, event: eventStream.Event) keyDown: function( self, key: string | integer, layer: Layer ): boolean keyName: function(self, scancode: integer): string keyPressed: function( self, key: string | integer, layer: Layer ): boolean keyReleased: function( self, key: string | integer, layer: Layer ): boolean modifierDown: function(self, name: string, layer: Layer): boolean modifiers: function(self): integer mouseDown: function( self, button: string | integer, layer: Layer ): boolean mousePressed: function( self, button: string | integer, layer: Layer ): boolean mouseReleased: function( self, button: string | integer, layer: Layer ): boolean popLayer: function(self): Layer pushLayer: function(self, name: string, blocking: boolean): Layer refreshDevices: function(self) relativeMouseMode: function(self): boolean scancode: function(self, name: string): integer screenKeyboardSupported: function(self): boolean setCursor: function(self, name: string): boolean setRelativeMouseMode: function(self, enabled: boolean): boolean setTextInputArea: function(self, area: TextArea): boolean showCursor: function(self, visible: boolean): boolean startTextInput: function( self, layer: Layer, options: TextOptions ): boolean stopTextInput: function(self): boolean textInputActive: function(self): boolean textInputLayer: function(self): Layer topLayer: function(self): Layer touches: function(self, layer: Layer): {Touch} warpMouse: function(self, x: number, y: number) end ``` #### tecs.input.Input.Layer record Read-only. [`Layer`](/modules/input/#tecs.input.Layer) names a position returned by `pushLayer`. ```teal record tecs.input.Input.Layer name: string blocking: boolean index: integer end ``` ##### tecs.input.Input.Layer.name field Read-only. Names what the layer is for in diagnostics. Not an identity: two layers may share a name, and a query is answered from the object rather than from what it is called. ```teal tecs.input.Input.Layer.name: string ``` ##### tecs.input.Input.Layer.blocking field Read-only. Reports whether this layer hides input from the layers beneath it. ```teal tecs.input.Input.Layer.blocking: boolean ``` ##### tecs.input.Input.Layer.index field Read-only. Reports the position in the stack. Input compares it against the topmost blocking layer to decide whether this layer may read. Fixed when the layer is pushed. Nothing renumbers, because the base layer is never removed and only the top one ever is, so an index cannot go stale while its layer is still on the stack. ```teal tecs.input.Input.Layer.index: integer ``` #### tecs.input.Input.Touch record `Touch` reports one finger from the most recent touch event. Input owns and reuses every field. Callers treat the record as read-only and copy values they need to retain. Read-only. [`Touch`](/modules/input/#tecs.input.Touch) names a finger record returned by `touches`. ```teal record tecs.input.Input.Touch device: string finger: string x: number y: number normalX: number normalY: number pressure: number end ``` ##### tecs.input.Input.Touch.device field Read-only. Input sets `device` to the touch device's opaque identity when the finger first appears. ```teal tecs.input.Input.Touch.device: string ``` ##### tecs.input.Input.Touch.finger field Read-only. Input sets `finger` to the finger's opaque 64-bit identity when the finger first appears. ```teal tecs.input.Input.Touch.finger: string ``` ##### tecs.input.Input.Touch.x field Read-only. Input updates `x` in window coordinates on every touch event. It may lag a resize until the application updates the conversion size. ```teal tecs.input.Input.Touch.x: number ``` ##### tecs.input.Input.Touch.y field Read-only. Input updates `y` in window coordinates with `x`. ```teal tecs.input.Input.Touch.y: number ``` ##### tecs.input.Input.Touch.normalX field Read-only. Input updates `normalX` from the platform's 0 to 1 position on every touch event. ```teal tecs.input.Input.Touch.normalX: number ``` ##### tecs.input.Input.Touch.normalY field Read-only. Input updates `normalY` with `normalX`. ```teal tecs.input.Input.Touch.normalY: number ``` ##### tecs.input.Input.Touch.pressure field Read-only. Input updates `pressure` from 0 to 1 when the surface reports pressure. ```teal tecs.input.Input.Touch.pressure: number ``` #### tecs.input.Input.TextArea record Callers write `TextArea` before passing it to a text-input method. Input reads the record immediately and does not retain it. Read-only. Exposes [`TextArea`](/modules/input/#tecs.input.TextArea), whose records callers pass to text-input methods. ```teal record tecs.input.Input.TextArea x: integer y: integer width: integer height: integer cursor: integer end ``` ##### tecs.input.Input.TextArea.x field Caller-writable. The caller sets `x` to the rectangle's left edge in window coordinates. ```teal tecs.input.Input.TextArea.x: integer ``` ##### tecs.input.Input.TextArea.y field Caller-writable. The caller sets `y` to the rectangle's top edge. ```teal tecs.input.Input.TextArea.y: integer ``` ##### tecs.input.Input.TextArea.width field Caller-writable. The caller sets `width` to the text run the platform should keep clear. ```teal tecs.input.Input.TextArea.width: integer ``` ##### tecs.input.Input.TextArea.height field Caller-writable. The caller sets `height` with `width`. ```teal tecs.input.Input.TextArea.height: integer ``` ##### tecs.input.Input.TextArea.cursor field Caller-writable. The caller sets `cursor` to the caret offset from the left edge. It defaults to zero. ```teal tecs.input.Input.TextArea.cursor: integer ``` #### tecs.input.Input.TextOptions record Read-only. Exposes [`TextOptions`](/modules/input/#tecs.input.TextOptions), whose records callers pass to `startTextInput`. ```teal record tecs.input.Input.TextOptions area: TextArea end ``` ##### tecs.input.Input.TextOptions.area field Caller-writable. The caller sets `area` so the platform can keep its candidate window and on-screen keyboard clear of the edited text. ```teal tecs.input.Input.TextOptions.area: TextArea ``` #### tecs.input.Input.Options field Engine-owned. Exposes the framework construction options. Ordinary game code uses the Input that the application creates and ignores this field. ```teal tecs.input.Input.Options: InputOptions ``` #### tecs.input.Input.mouseX field Read-only. Input updates `mouseX` from the most recent mouse event and retains it across frames without motion. ```teal tecs.input.Input.mouseX: number ``` #### tecs.input.Input.mouseY field Read-only. Input updates `mouseY` with `mouseX`. ```teal tecs.input.Input.mouseY: number ``` #### tecs.input.Input.mouseDeltaX field Read-only. Input accumulates `mouseDeltaX` during a frame and clears it in `beginFrame`. ```teal tecs.input.Input.mouseDeltaX: number ``` #### tecs.input.Input.mouseDeltaY field Read-only. Input accumulates `mouseDeltaY` and clears it with `mouseDeltaX`. ```teal tecs.input.Input.mouseDeltaY: number ``` #### tecs.input.Input.wheelX field Read-only. Input accumulates horizontal wheel motion during a frame and clears it in `beginFrame`. Positive `wheelX` scrolls right. ```teal tecs.input.Input.wheelX: number ``` #### tecs.input.Input.wheelY field Read-only. Input accumulates vertical wheel motion under the sign convention above and clears it in `beginFrame`. ```teal tecs.input.Input.wheelY: number ``` #### tecs.input.Input.wheelPreferredX field Read-only. Input accumulates horizontal wheel motion after applying the platform's configured scroll direction. Scrolling interfaces should use this pair; directional gameplay bindings should use `wheelX` and `wheelY` for their stable sign convention. ```teal tecs.input.Input.wheelPreferredX: number ``` #### tecs.input.Input.wheelPreferredY field Read-only. Input accumulates vertical wheel motion under the user's configured scroll direction and clears it with `wheelPreferredX`. ```teal tecs.input.Input.wheelPreferredY: number ``` #### tecs.input.Input.wheelTicksX field Read-only. Input accumulates horizontal whole-notch wheel motion during a frame and clears it in `beginFrame`. ```teal tecs.input.Input.wheelTicksX: integer ``` #### tecs.input.Input.wheelTicksY field Read-only. Input accumulates vertical whole-notch wheel motion and clears it with `wheelTicksX`. ```teal tecs.input.Input.wheelTicksY: integer ``` #### tecs.input.Input.mouseWhich field Read-only. Input sets `mouseWhich` to the device from the most recent mouse button, motion or wheel event. ```teal tecs.input.Input.mouseWhich: number ``` #### tecs.input.Input.mouseSynthetic field Read-only. Input sets `mouseSynthetic` when that mouse event came from the platform's touch or pen translation. ```teal tecs.input.Input.mouseSynthetic: boolean ``` #### tecs.input.Input.text field Read-only. Input appends committed text during a frame and clears it in `beginFrame`. ```teal tecs.input.Input.text: string ``` #### tecs.input.Input.composition field Read-only. Input replaces `composition` when the input method edits uncommitted text and clears it on commit or session stop. ```teal tecs.input.Input.composition: string ``` #### tecs.input.Input.compositionStart field Read-only. Input sets `compositionStart` to the caret offset within `composition`. ```teal tecs.input.Input.compositionStart: integer ``` #### tecs.input.Input.compositionLength field Read-only. Input sets `compositionLength` to the selected length within `composition`. ```teal tecs.input.Input.compositionLength: integer ``` #### tecs.input.Input.penX field Read-only. Input updates `penX` from the most recent pen event. ```teal tecs.input.Input.penX: number ``` #### tecs.input.Input.penY field Read-only. Input updates `penY` with `penX`. ```teal tecs.input.Input.penY: number ``` #### tecs.input.Input.penPressure field Read-only. Input updates `penPressure` from 0 to 1 and clears it when the pen leaves proximity. ```teal tecs.input.Input.penPressure: number ``` #### tecs.input.Input.penTiltX field Read-only. Input updates `penTiltX` in degrees from upright. ```teal tecs.input.Input.penTiltX: number ``` #### tecs.input.Input.penTiltY field Read-only. Input updates `penTiltY` with `penTiltX`. ```teal tecs.input.Input.penTiltY: number ``` #### tecs.input.Input.penRotation field Read-only. Input updates `penRotation` in clockwise degrees. ```teal tecs.input.Input.penRotation: number ``` #### tecs.input.Input.penTouching field Read-only. Input updates `penTouching` on contact and clears it when the pen leaves proximity. ```teal tecs.input.Input.penTouching: boolean ``` #### tecs.input.Input.penEraser field Read-only. Input sets `penEraser` when a stroke begins. ```teal tecs.input.Input.penEraser: boolean ``` #### tecs.input.Input.penWhich field Read-only. Input sets `penWhich` to the most recent pen identity. A second pen replaces the first pen's state. ```teal tecs.input.Input.penWhich: number ``` #### tecs.input.Input:beginFrame Instance Engine-owned. Starts a new frame and clears frame-local values. The application calls this before it folds events. Ordinary game code must not call it. ```teal function tecs.input.Input.beginFrame(self) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Input` | | ##### Returns None. #### tecs.input.Input:canRead Instance Returns whether a layer may read input. ```teal function tecs.input.Input.canRead(self, layer: Layer): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Input` | | | `layer` | [`Layer`](/modules/input/#tecs.input.Input.Layer) | The caller omits this value to test the base layer. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns false when a blocking layer sits above it. | #### tecs.input.Input:captureMouse Instance Enables or disables mouse capture outside the window. ```teal function tecs.input.Input.captureMouse(self, enabled: boolean): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Input` | | | `enabled` | `boolean` | The caller passes false to release capture; nil and true enable it. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns whether the platform accepted the change. | #### tecs.input.Input:cursorVisible Instance Returns whether the platform shows the cursor. ```teal function tecs.input.Input.cursorVisible(self): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Input` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns live platform state. | #### tecs.input.Input:destroy Instance Engine-owned. Closes devices and platform input resources. The application calls this during shutdown. Ordinary game code must not call it. ```teal function tecs.input.Input.destroy(self) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Input` | | ##### Returns None. #### tecs.input.Input:enterFixedPhase Instance Engine-owned. Selects latched edges for a fixed step. The fixed-phase bracket calls this method. Ordinary game code must not call it. ```teal function tecs.input.Input.enterFixedPhase(self) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Input` | | ##### Returns None. #### tecs.input.Input:exitFixedPhase Instance Engine-owned. Clears latched edges after a fixed step. The fixed-phase bracket calls this method. Ordinary game code must not call it. ```teal function tecs.input.Input.exitFixedPhase(self) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Input` | | ##### Returns None. #### tecs.input.Input:gamepad Instance Returns a connected gamepad by connection order. ```teal function tecs.input.Input.gamepad(self, index: integer): Gamepad ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Input` | | | `index` | `integer` | The caller supplies a one-based position, or nil for the first gamepad. | ##### Returns | Type | Description | | --- | --- | | [`Gamepad`](/modules/input/#tecs.input.Gamepad) | Returns the gamepad, or nil when the position is empty. | #### tecs.input.Input:gamepadById Instance Returns a connected gamepad by platform instance id. ```teal function tecs.input.Input.gamepadById(self, id: number): Gamepad ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Input` | | | `id` | `number` | The caller supplies an id from a platform event. | ##### Returns | Type | Description | | --- | --- | | [`Gamepad`](/modules/input/#tecs.input.Gamepad) | Returns the gamepad, or nil after it disconnects. | #### tecs.input.Input:gamepads Instance Returns the live list of connected gamepads. ```teal function tecs.input.Input.gamepads(self): {Gamepad} ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Input` | | ##### Returns | Type | Description | | --- | --- | | `{`[`Gamepad`](/modules/input/#tecs.input.Gamepad)`}` | Returns Input-owned storage. Callers may retain a [`Gamepad`](/modules/input/#tecs.input.Gamepad) from it but must not edit the list. | #### tecs.input.Input:handleEvent Instance Engine-owned. Folds one typed platform event into input state. The application passes every event here. Input copies every value it retains. Ordinary game code must not call this method. ```teal function tecs.input.Input.handleEvent(self, event: eventStream.Event) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Input` | | | `event` | [`eventStream.Event`](/modules/platform/events/#tecs.platform.events.Event) | The engine supplies a borrowed event record. | ##### Returns None. #### tecs.input.Input:keyDown Instance Returns whether a physical key is held. ```teal function tecs.input.Input.keyDown( self, key: string | integer, layer: Layer ): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Input` | | | `key` | string | integer | The caller supplies a key name or scancode. An unknown name raises. | | `layer` | [`Layer`](/modules/input/#tecs.input.Input.Layer) | The caller omits this value to read the base layer. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns false when the layer cannot read. | #### tecs.input.Input:keyName Instance Returns the platform name for a physical key. ```teal function tecs.input.Input.keyName(self, scancode: integer): string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Input` | | | `scancode` | `integer` | The caller supplies a platform scancode. | ##### Returns | Type | Description | | --- | --- | | `string` | Returns the display name for a binding prompt. | #### tecs.input.Input:keyPressed Instance Returns whether a physical key went down in the active tier. Fixed phases read latched edges; other phases read frame edges. ```teal function tecs.input.Input.keyPressed( self, key: string | integer, layer: Layer ): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Input` | | | `key` | string | integer | The caller supplies a key name or scancode. An unknown name raises. | | `layer` | [`Layer`](/modules/input/#tecs.input.Input.Layer) | The caller omits this value to read the base layer. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns false when the layer cannot read. | #### tecs.input.Input:keyReleased Instance Returns whether a physical key came up in the active tier. ```teal function tecs.input.Input.keyReleased( self, key: string | integer, layer: Layer ): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Input` | | | `key` | string | integer | The caller supplies a key name or scancode. An unknown name raises. | | `layer` | [`Layer`](/modules/input/#tecs.input.Input.Layer) | The caller omits this value to read the base layer. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns false when the layer cannot read. | #### tecs.input.Input:modifierDown Instance Returns whether a named modifier is held. ```teal function tecs.input.Input.modifierDown( self, name: string, layer: Layer ): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Input` | | | `name` | `string` | The caller supplies `"shift"`, `"ctrl"`, `"alt"`, `"gui"`, `"capsLock"` or a sided variant. An unknown name raises. | | `layer` | [`Layer`](/modules/input/#tecs.input.Input.Layer) | The caller omits this value to read the base layer. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns false when the layer cannot read. | #### tecs.input.Input:modifiers Instance Returns the modifier mask from the most recent key event. The value ignores layers and resets on focus loss. ```teal function tecs.input.Input.modifiers(self): integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Input` | | ##### Returns | Type | Description | | --- | --- | | `integer` | Returns the platform modifier bits. | #### tecs.input.Input:mouseDown Instance Returns whether a mouse button is held. ```teal function tecs.input.Input.mouseDown( self, button: string | integer, layer: Layer ): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Input` | | | `button` | string | integer | The caller supplies a platform number or `"left"`, `"middle"`, `"right"`, `"x1"` or `"x2"`. | | `layer` | [`Layer`](/modules/input/#tecs.input.Input.Layer) | The caller omits this value to read the base layer. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns false when the layer cannot read. | #### tecs.input.Input:mousePressed Instance Returns whether a mouse button went down in the active tier. ```teal function tecs.input.Input.mousePressed( self, button: string | integer, layer: Layer ): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Input` | | | `button` | string | integer | The caller supplies a button name or number. | | `layer` | [`Layer`](/modules/input/#tecs.input.Input.Layer) | The caller omits this value to read the base layer. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns false when the layer cannot read. | #### tecs.input.Input:mouseReleased Instance Returns whether a mouse button came up in the active tier. ```teal function tecs.input.Input.mouseReleased( self, button: string | integer, layer: Layer ): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Input` | | | `button` | string | integer | The caller supplies a button name or number. | | `layer` | [`Layer`](/modules/input/#tecs.input.Input.Layer) | The caller omits this value to read the base layer. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns false when the layer cannot read. | #### tecs.input.Input:popLayer Instance Removes and returns the top layer. The base layer remains in place. Popping the owner of a text-input session stops that session. ```teal function tecs.input.Input.popLayer(self): Layer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Input` | | ##### Returns | Type | Description | | --- | --- | | [`Layer`](/modules/input/#tecs.input.Input.Layer) | Returns the removed layer, or nil at the base layer. | #### tecs.input.Input:pushLayer Instance Pushes a layer and returns it. A blocking layer moves capture and clears pending edges. A nonblocking layer observes input without hiding lower layers. ```teal function tecs.input.Input.pushLayer( self, name: string, blocking: boolean ): Layer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Input` | | | `name` | `string` | The caller supplies a diagnostic layer name. | | `blocking` | `boolean` | The caller passes false for a nonblocking overlay; nil and true create a blocking layer. | ##### Returns | Type | Description | | --- | --- | | [`Layer`](/modules/input/#tecs.input.Input.Layer) | Returns the new layer, which remains caller-owned until `popLayer` removes it. | #### tecs.input.Input:refreshDevices Instance Engine-owned. Opens gamepads present at startup. The application calls this once before event intake. Ordinary game code observes the resulting `gamepads` list. ```teal function tecs.input.Input.refreshDevices(self) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Input` | | ##### Returns None. #### tecs.input.Input:relativeMouseMode Instance Returns whether relative mouse mode is active. ```teal function tecs.input.Input.relativeMouseMode(self): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Input` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns live platform state rather than the last request. | #### tecs.input.Input:scancode Instance Resolves and caches a physical key name. ```teal function tecs.input.Input.scancode(self, name: string): integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Input` | | | `name` | `string` | The caller supplies the platform key name without regard to case. An unknown name raises. | ##### Returns | Type | Description | | --- | --- | | `integer` | Returns the platform scancode. | #### tecs.input.Input:screenKeyboardSupported Instance Returns whether the platform supplies an on-screen keyboard. ```teal function tecs.input.Input.screenKeyboardSupported(self): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Input` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns live platform capability state. | #### tecs.input.Input:setCursor Instance Selects a standard platform cursor. Input owns the created cursor and releases it on replacement or `destroy`. ```teal function tecs.input.Input.setCursor(self, name: string): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Input` | | | `name` | `string` | The caller omits this value or passes `"default"` to restore the platform cursor. An unknown name raises. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns whether the platform accepted the cursor. | #### tecs.input.Input:setRelativeMouseMode Instance Enables or disables relative mouse mode. Relative mode hides the cursor, freezes its position and reports movement through the delta fields. ```teal function tecs.input.Input.setRelativeMouseMode( self, enabled: boolean ): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Input` | | | `enabled` | `boolean` | The caller passes false to disable the mode; nil and true enable it. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns whether the platform accepted the change. | #### tecs.input.Input:setTextInputArea Instance Moves the area a running text session reports after its field scrolls or its caret moves. ```teal function tecs.input.Input.setTextInputArea( self, area: TextArea ): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Input` | | | `area` | [`TextArea`](/modules/input/#tecs.input.Input.TextArea) | Input reads this record immediately and does not retain it. A later edit has no effect until another call. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns whether the platform accepted the hint. Text input continues when it returns false. | #### tecs.input.Input:showCursor Instance Shows or hides the application cursor. ```teal function tecs.input.Input.showCursor(self, visible: boolean): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Input` | | | `visible` | `boolean` | The caller passes false to hide the cursor; nil and true show it. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns whether the platform accepted the change. | #### tecs.input.Input:startTextInput Instance Starts text input and optionally binds it to a layer. Popping the owner layer stops the session. ```teal function tecs.input.Input.startTextInput( self, layer: Layer, options: TextOptions ): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Input` | | | `layer` | [`Layer`](/modules/input/#tecs.input.Input.Layer) | The caller omits this value for a session it will stop explicitly. | | `options` | [`TextOptions`](/modules/input/#tecs.input.Input.TextOptions) | The caller may supply the edited text area. Input reads the record immediately and does not retain it. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns whether the platform started the session. | #### tecs.input.Input:stopTextInput Instance Stops text input and clears the current composition. The layer stack and shutdown path both call this method, so its declaration sits beside the rest of the text-input contract. ```teal function tecs.input.Input.stopTextInput(self): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Input` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns false when no session was running. The method still clears composition state, so a teardown may call it unconditionally. | #### tecs.input.Input:textInputActive Instance Returns whether this Input started a text-input session. ```teal function tecs.input.Input.textInputActive(self): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Input` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns false for sessions started outside this object. | #### tecs.input.Input:textInputLayer Instance Returns the layer that owns the current text-input session. ```teal function tecs.input.Input.textInputLayer(self): Layer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Input` | | ##### Returns | Type | Description | | --- | --- | | [`Layer`](/modules/input/#tecs.input.Input.Layer) | Returns nil when no layer owns the session. | #### tecs.input.Input:topLayer Instance Returns the layer currently on top. ```teal function tecs.input.Input.topLayer(self): Layer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Input` | | ##### Returns | Type | Description | | --- | --- | | [`Layer`](/modules/input/#tecs.input.Input.Layer) | Returns the live layer object. Callers treat its fields as read-only. | #### tecs.input.Input:touches Instance Returns the fingers currently on the touch surface. ```teal function tecs.input.Input.touches(self, layer: Layer): {Touch} ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Input` | | | `layer` | [`Layer`](/modules/input/#tecs.input.Input.Layer) | The caller omits this value to read the base layer. | ##### Returns | Type | Description | | --- | --- | | `{`[`Touch`](/modules/input/#tecs.input.Input.Touch)`}` | Returns Input-owned list and records that the next call may reuse. Callers copy values they need to retain. | #### tecs.input.Input:warpMouse Instance Moves the cursor within the window. The platform emits a motion event for the warp. ```teal function tecs.input.Input.warpMouse(self, x: number, y: number) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Input` | | | `x` | `number` | The caller supplies the window-coordinate x position. | | `y` | `number` | The caller supplies the window-coordinate y position. | ##### Returns None. ### tecs.input.Layer record Identifies a position from `pushLayer`. ```teal record tecs.input.Layer name: string blocking: boolean index: integer end ``` #### tecs.input.Layer.name field Read-only. Names what the layer is for in diagnostics. Not an identity: two layers may share a name, and a query is answered from the object rather than from what it is called. ```teal tecs.input.Layer.name: string ``` #### tecs.input.Layer.blocking field Read-only. Reports whether this layer hides input from the layers beneath it. ```teal tecs.input.Layer.blocking: boolean ``` #### tecs.input.Layer.index field Read-only. Reports the position in the stack. Input compares it against the topmost blocking layer to decide whether this layer may read. Fixed when the layer is pushed. Nothing renumbers, because the base layer is never removed and only the top one ever is, so an index cannot go stale while its layer is still on the stack. ```teal tecs.input.Layer.index: integer ``` ### tecs.input.Options record Describes framework construction options for `Input`. ```teal record tecs.input.Options window: loader.CPtr backend: Backend end ``` #### tecs.input.Options.window field Engine-owned. The application supplies `window` for text input and cursor modes. Ordinary game code ignores this field; omitting it makes window commands report failure for a headless test. ```teal tecs.input.Options.window: loader.CPtr ``` #### tecs.input.Options.backend field Engine-owned. The application or a platform test supplies `backend`. Ordinary game code ignores it. Input uses the installed backend when the field is nil. ```teal tecs.input.Options.backend: Backend ``` ### tecs.input.Sensor record Describes an opened standalone sensor. ```teal record tecs.input.Sensor id: number name: string kind: string platformType: integer destroy: function(self) read: function(self, count: integer): {number}, string end ``` #### tecs.input.Sensor.id field Read-only. The platform sets `id` from the opened device. ```teal tecs.input.Sensor.id: number ``` #### tecs.input.Sensor.name field Read-only. The platform sets `name` from the opened device. ```teal tecs.input.Sensor.name: string ``` #### tecs.input.Sensor.kind field Read-only. The platform sets `kind` from the same vocabulary as `Device.kind`. ```teal tecs.input.Sensor.kind: string ``` #### tecs.input.Sensor.platformType field Read-only. The platform sets `platformType` to its private type number, or zero when none exists. Ordinary game code ignores this field. ```teal tecs.input.Sensor.platformType: integer ``` #### tecs.input.Sensor:destroy Instance Closes the sensor. Safe to call more than once. ```teal function tecs.input.Sensor.destroy(self) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Sensor` | | ##### Returns None. #### tecs.input.Sensor:read Instance Reads the newest values. Three values is the natural size for accelerometers and gyroscopes. A platform-specific sensor can request up to sixteen. ```teal function tecs.input.Sensor.read( self, count: integer ): {number}, string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Sensor` | | | `count` | `integer` | The caller requests one to sixteen values or omits the count for three. Invalid counts return an error instead of raising. | ##### Returns | Type | Description | | --- | --- | | `{number}` | Returns a fresh caller-owned list. Acceleration uses meters per second squared and rotation uses radians per second. The method returns nil after failure or destruction. | | `string` | Returns the reason when the first return is nil. | ### tecs.input.SensorDevice record Describes an enumerated standalone sensor before open. ```teal record tecs.input.SensorDevice id: number name: string kind: string platformType: integer end ``` #### tecs.input.SensorDevice.id field Read-only. The platform sets `id` to an instance id that remains valid while the device stays attached. ```teal tecs.input.SensorDevice.id: number ``` #### tecs.input.SensorDevice.name field Read-only. The platform sets `name` to the device display name. ```teal tecs.input.SensorDevice.name: string ``` #### tecs.input.SensorDevice.kind field Read-only. The platform sets `kind` to `"accelerometer"`, `"gyroscope"`, a left or right variant, or `"unknown"`. ```teal tecs.input.SensorDevice.kind: string ``` #### tecs.input.SensorDevice.platformType field Read-only. The platform sets `platformType` to its private type number, or zero when none exists. Ordinary game code ignores this field. ```teal tecs.input.SensorDevice.platformType: integer ``` ### tecs.input.TextArea record Describes a caller-writable text rectangle. ```teal record tecs.input.TextArea x: integer y: integer width: integer height: integer cursor: integer end ``` #### tecs.input.TextArea.x field Caller-writable. The caller sets `x` to the rectangle's left edge in window coordinates. ```teal tecs.input.TextArea.x: integer ``` #### tecs.input.TextArea.y field Caller-writable. The caller sets `y` to the rectangle's top edge. ```teal tecs.input.TextArea.y: integer ``` #### tecs.input.TextArea.width field Caller-writable. The caller sets `width` to the text run the platform should keep clear. ```teal tecs.input.TextArea.width: integer ``` #### tecs.input.TextArea.height field Caller-writable. The caller sets `height` with `width`. ```teal tecs.input.TextArea.height: integer ``` #### tecs.input.TextArea.cursor field Caller-writable. The caller sets `cursor` to the caret offset from the left edge. It defaults to zero. ```teal tecs.input.TextArea.cursor: integer ``` ### tecs.input.TextOptions record Describes caller-writable text-input options. ```teal record tecs.input.TextOptions area: TextArea end ``` #### tecs.input.TextOptions.area field Caller-writable. The caller sets `area` so the platform can keep its candidate window and on-screen keyboard clear of the edited text. ```teal tecs.input.TextOptions.area: TextArea ``` ### tecs.input.Touch record Describes a finger from `touches`. ```teal record tecs.input.Touch device: string finger: string x: number y: number normalX: number normalY: number pressure: number end ``` #### tecs.input.Touch.device field Read-only. Input sets `device` to the touch device's opaque identity when the finger first appears. ```teal tecs.input.Touch.device: string ``` #### tecs.input.Touch.finger field Read-only. Input sets `finger` to the finger's opaque 64-bit identity when the finger first appears. ```teal tecs.input.Touch.finger: string ``` #### tecs.input.Touch.x field Read-only. Input updates `x` in window coordinates on every touch event. It may lag a resize until the application updates the conversion size. ```teal tecs.input.Touch.x: number ``` #### tecs.input.Touch.y field Read-only. Input updates `y` in window coordinates with `x`. ```teal tecs.input.Touch.y: number ``` #### tecs.input.Touch.normalX field Read-only. Input updates `normalX` from the platform's 0 to 1 position on every touch event. ```teal tecs.input.Touch.normalX: number ``` #### tecs.input.Touch.normalY field Read-only. Input updates `normalY` with `normalX`. ```teal tecs.input.Touch.normalY: number ``` #### tecs.input.Touch.pressure field Read-only. Input updates `pressure` from 0 to 1 when the surface reports pressure. ```teal tecs.input.Touch.pressure: number ``` ### tecs.input.TouchpadFinger type Describes a gamepad touch. ```teal type tecs.input.TouchpadFinger = Gamepad.TouchpadFinger ``` ## Functions ### tecs.input.openSensor Static Opens an attached standalone sensor by instance id. ```teal function tecs.input.openSensor(id: number): sensors.Sensor, string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `id` | `number` | The caller supplies an instance id from `sensors`. It stops naming the device after unplug. | #### Returns | Type | Description | | --- | --- | | [`sensors.Sensor`](/modules/input/#tecs.input.Sensor) | Returns the open sensor, or nil on failure. The caller closes it with `destroy`. | | `string` | Returns the reason when the first return is nil. | ### tecs.input.sensors Static Returns the standalone sensors attached now in platform order. A snapshot rather than a subscription: a sensor unplugged after this leaves an id `openSensor` refuses. Gamepad sensors are not here, since their identity and event routing live on the pad. ```teal function tecs.input.sensors(): {sensors.Device}, string ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | `{`[`sensors.Device`](/modules/input/#tecs.input.Device)`}` | Returns a fresh caller-owned list. It returns an empty list when no sensors exist or the subsystem cannot start. | | `string` | Returns the platform's reason when enumeration fails. | --- ## tecs.io.Path # tecs.io.Path Immutable, platform-native filesystem paths. `Path` is an immutable platform-native UTF-8 filesystem path. Methods interpret roots, separators, drive prefixes, and UNC paths according to the operating system running Tecs. They return new paths rather than mutating their receiver: ```teal local root = tecs.io.Path.new("assets") local shader = root:join("shaders", "sprite.glsl") print(shader:fileName()) print(shader:withExtension("spv")) ``` `normalize` is lexical. It removes redundant separators and `.` components and resolves `..` without reading the filesystem. That is useful for output paths which do not exist yet, but it can change which object a path denotes when a traversed component is a symbolic link. `canonicalize` reads the filesystem, requires the path to exist, and follows symbolic links: use `resolve` for a normalized absolute spelling of a configured output and `canonicalize` only when an existing object's filesystem identity matters. Paths contain valid UTF-8 and no NUL byte, matching the string contract used by Tecs file and process APIs. Unix filenames containing invalid UTF-8 cannot be represented. A path object owns only its immutable Lua string and needs no `close`. ## Module contents ### Constructors | Constructor | Description | | --- | --- | | [`new`](/modules/io/Path/#tecs.io.Path.new) | Creates an immutable path from platform-native components. | ### Functions | Function | Kind | Description | | --- | --- | --- | | [`currentDirectory`](/modules/io/Path/#tecs.io.Path.currentDirectory) | Static | Returns the process's current working directory. | | [`separator`](/modules/io/Path/#tecs.io.Path.separator) | Static | Returns the platform's preferred path separator. | | [`absolute`](/modules/io/Path/#tecs.io.Path.absolute) | Instance | Returns an absolute spelling without requiring the path to exist. | | [`canonicalize`](/modules/io/Path/#tecs.io.Path.canonicalize) | Instance | Resolves filesystem identity and follows symbolic links. | | [`extension`](/modules/io/Path/#tecs.io.Path.extension) | Instance | Returns the final component's last extension without its dot. | | [`fileName`](/modules/io/Path/#tecs.io.Path.fileName) | Instance | Returns the final path component. | | [`isAbsolute`](/modules/io/Path/#tecs.io.Path.isAbsolute) | Instance | Returns whether this path carries a platform root. | | [`isRelative`](/modules/io/Path/#tecs.io.Path.isRelative) | Instance | Returns whether this path needs a base directory. | | [`join`](/modules/io/Path/#tecs.io.Path.join) | Instance | Appends path components and returns the result. | | [`normalize`](/modules/io/Path/#tecs.io.Path.normalize) | Instance | Returns a lexically normalized path without filesystem access. | | [`parent`](/modules/io/Path/#tecs.io.Path.parent) | Instance | Returns this path without its final component. | | [`relativeTo`](/modules/io/Path/#tecs.io.Path.relativeTo) | Instance | Expresses this path relative to a base path. | | [`resolve`](/modules/io/Path/#tecs.io.Path.resolve) | Instance | Returns a normalized absolute spelling without filesystem access. | | [`stem`](/modules/io/Path/#tecs.io.Path.stem) | Instance | Returns the final component without its last extension. | | [`toString`](/modules/io/Path/#tecs.io.Path.toString) | Instance | Returns this path's platform-native UTF-8 spelling. | | [`withExtension`](/modules/io/Path/#tecs.io.Path.withExtension) | Instance | Replaces or removes the final component's extension. | | [`withFileName`](/modules/io/Path/#tecs.io.Path.withFileName) | Instance | Replaces the final component. | ## Constructors ### tecs.io.Path.new Static Creates an immutable path from platform-native components. Construction joins the components lexically and does not require the path to exist. A later absolute component replaces the accumulated prefix according to the host platform's rules. ```teal function tecs.io.Path.new(first: string, ...: string): Path ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `first` | `string` | The caller supplies the first valid UTF-8 path component or complete path, without a NUL byte. | | `...` | `string` | The caller supplies additional path components in order. | #### Returns | Type | Description | | --- | --- | | [`Path`](/modules/io/Path/) | Returns a new immutable path. | #### Examples ```teal local shader = tecs.io.Path.new( "assets", "shaders", "sprite.glsl" ) assert(shader:fileName() == "sprite.glsl") ``` ## Functions ### tecs.io.Path.currentDirectory Static Returns the process's current working directory. ```teal function tecs.io.Path.currentDirectory(): Path, string ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | [`Path`](/modules/io/Path/) | Returns a new immutable normalized absolute path. | | `string` | Returns the platform reason when the first return is nil. | ### tecs.io.Path.separator Static Returns the platform's preferred path separator. Paths accept the forms supported by the host platform; this value is only for text that must display or emit a separator itself. ```teal function tecs.io.Path.separator(): string ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | `string` | Returns `"\\"` on Windows and `"/"` elsewhere. | ### tecs.io.Path:absolute Instance Returns an absolute spelling without requiring the path to exist. A relative receiver is based on the process's current directory. Unlike `resolve`, this preserves platform-significant parent components where the native absolute operation preserves them. ```teal function tecs.io.Path.absolute(self): Path, string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Path` | The path to make absolute. | #### Returns | Type | Description | | --- | --- | | [`Path`](/modules/io/Path/) | Returns a new immutable absolute path. | | `string` | Returns the platform reason when the current directory cannot be determined. | ### tecs.io.Path:canonicalize Instance Resolves filesystem identity and follows symbolic links. The receiver must exist. The result is absolute and normalized by the operating system. ```teal function tecs.io.Path.canonicalize(self): Path, string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Path` | The existing path to canonicalize. | #### Returns | Type | Description | | --- | --- | | [`Path`](/modules/io/Path/) | Returns a new immutable canonical path. | | `string` | Returns the filesystem reason when the first return is nil. | #### Examples ```teal local executable, reason = tecs.io.Path.new("."):canonicalize() if executable == nil then error(reason) end print(executable) ``` ### tecs.io.Path:extension Instance Returns the final component's last extension without its dot. ```teal function tecs.io.Path.extension(self): string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Path` | The path to inspect without filesystem access. | #### Returns | Type | Description | | --- | --- | | `string` | Returns the extension, or nil when none exists. | ### tecs.io.Path:fileName Instance Returns the final path component. ```teal function tecs.io.Path.fileName(self): string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Path` | The path to inspect without filesystem access. | #### Returns | Type | Description | | --- | --- | | `string` | Returns the final component, or nil for a root or terminal parent component. | ### tecs.io.Path:isAbsolute Instance Returns whether this path carries a platform root. ```teal function tecs.io.Path.isAbsolute(self): boolean ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Path` | The path to inspect without filesystem access. | #### Returns | Type | Description | | --- | --- | | `boolean` | Returns true when the path is absolute on this platform. | ### tecs.io.Path:isRelative Instance Returns whether this path needs a base directory. ```teal function tecs.io.Path.isRelative(self): boolean ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Path` | The path to inspect without filesystem access. | #### Returns | Type | Description | | --- | --- | | `boolean` | Returns true when the path is relative on this platform. | ### tecs.io.Path:join Instance Appends path components and returns the result. A later absolute argument replaces everything accumulated before it, following the host platform's path rules. The result is not normalized and no component needs to exist. ```teal function tecs.io.Path.join(self, ...: string | Path): Path ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Path` | The base path left unchanged. | | `...` | string | [`Path`](/modules/io/Path/) | The caller supplies strings or paths to append in order. | #### Returns | Type | Description | | --- | --- | | [`Path`](/modules/io/Path/) | Returns a new immutable path. | #### Examples ```teal local assets = tecs.io.Path.new("assets") local shader = assets:join("shaders", "sprite.glsl") assert(tostring(assets) == "assets") assert( shader:toString() == tecs.io.Path.new( "assets", "shaders", "sprite.glsl" ):toString() ) ``` ### tecs.io.Path:normalize Instance Returns a lexically normalized path without filesystem access. Redundant separators and `.` are removed and `..` is resolved. A symbolic link can make that lexical result denote a different object. ```teal function tecs.io.Path.normalize(self): Path ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Path` | The path to normalize. | #### Returns | Type | Description | | --- | --- | | [`Path`](/modules/io/Path/) | Returns a new immutable path. | #### Examples ```teal local configured = tecs.io.Path.new( "assets", ".", "shaders", "..", "sprite.glsl" ) local normalized = configured:normalize() assert(normalized == tecs.io.Path.new("assets", "sprite.glsl")) ``` ### tecs.io.Path:parent Instance Returns this path without its final component. ```teal function tecs.io.Path.parent(self): Path ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Path` | The path to inspect without filesystem access. | #### Returns | Type | Description | | --- | --- | | [`Path`](/modules/io/Path/) | Returns a new immutable parent, or nil when the path has none. | ### tecs.io.Path:relativeTo Instance Expresses this path relative to a base path. Neither path is accessed. On Windows, paths on different drives or UNC roots have no relative spelling. ```teal function tecs.io.Path.relativeTo( self, base: string | Path ): Path, string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Path` | The destination path. | | `base` | string | [`Path`](/modules/io/Path/) | The caller supplies the directory from which to describe the destination. | #### Returns | Type | Description | | --- | --- | | [`Path`](/modules/io/Path/) | Returns a new immutable relative path. | | `string` | Returns a reason when the two paths cannot share a coordinate system. | #### Examples ```teal local root = tecs.io.Path.new("assets") local sprite = root:join("sprites", "hero.png") local relative, reason = sprite:relativeTo(root) if relative == nil then error(reason) end assert(relative == tecs.io.Path.new("sprites", "hero.png")) ``` ### tecs.io.Path:resolve Instance Returns a normalized absolute spelling without filesystem access. Arguments are joined first. The process's current directory supplies the base when the joined path remains relative. ```teal function tecs.io.Path.resolve( self, ...: string | Path ): Path, string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Path` | The first path in the resolution. | | `...` | string | [`Path`](/modules/io/Path/) | The caller supplies strings or paths to append in order. | #### Returns | Type | Description | | --- | --- | | [`Path`](/modules/io/Path/) | Returns a new immutable normalized absolute path. | | `string` | Returns the platform reason when the current directory cannot be determined. | #### Examples ```teal local output, reason = tecs.io.Path.new("build", "game.pack"):resolve() if output == nil then error(reason) end assert(output:isAbsolute()) ``` ### tecs.io.Path:stem Instance Returns the final component without its last extension. ```teal function tecs.io.Path.stem(self): string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Path` | The path to inspect without filesystem access. | #### Returns | Type | Description | | --- | --- | | `string` | Returns the stem, or nil when the path has no file name. | ### tecs.io.Path:toString Instance Returns this path's platform-native UTF-8 spelling. `tostring(path)` returns the same string. ```teal function tecs.io.Path.toString(self): string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Path` | The immutable path to render. | #### Returns | Type | Description | | --- | --- | | `string` | Returns the complete path without accessing the filesystem. | ### tecs.io.Path:withExtension Instance Replaces or removes the final component's extension. ```teal function tecs.io.Path.withExtension(self, extension: string): Path ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Path` | The path whose final component changes. | | `extension` | `string` | The caller supplies one component without a leading dot, or an empty string to remove the extension. | #### Returns | Type | Description | | --- | --- | | [`Path`](/modules/io/Path/) | Returns a new immutable path. | #### Examples ```teal local source = tecs.io.Path.new("shaders", "sprite.glsl") local compiled = source:withExtension("spv") assert(source:extension() == "glsl") assert(compiled:fileName() == "sprite.spv") ``` ### tecs.io.Path:withFileName Instance Replaces the final component. ```teal function tecs.io.Path.withFileName(self, name: string): Path ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Path` | The path whose parent is retained. | | `name` | `string` | The caller supplies one non-empty platform path component. | #### Returns | Type | Description | | --- | --- | | [`Path`](/modules/io/Path/) | Returns a new immutable path. | --- ## tecs.io.Process # tecs.io.Process Streaming child processes. `new` starts the child immediately and returns its live standard-stream endpoints. Piped stdin is a [`Writer`](/modules/io/Process/#tecs.io.Process.Writer); piped stdout and stderr are [`Reader`](/modules/io/Process/#tecs.io.Process.Reader) values. The process owns all three endpoints. Closing one endpoint closes only that pipe; closing the process closes every endpoint, terminates a live child, and reaps it. Buffers and byte views passed to an endpoint remain caller-owned. `Process:communicate` is the complete-exchange form for tools and build steps. It feeds stdin while draining stdout and stderr concurrently, so neither output pipe can fill and deadlock the child: ```teal tecs.scoped( "read child output", function(scope: tecs.Scope) local child = scope:own(tecs.io.Process.new({ args = {"git", "status", "--porcelain"}, timeoutMs = 2000, })) local result, communicateReason = child:communicate() if result == nil then error(communicateReason) end if result:succeeded() then print(result.output) else io.stderr:write(result.errorOutput) end end ) ``` Interactive children use the same reader and writer vocabulary as files, buffers, and transforms. Closing stdin sends EOF: ```teal tecs.scoped( "communicate with child", function(scope: tecs.Scope) local child = scope:own(tecs.io.Process.new({ args = {"/bin/cat"} })) local written, writeReason = child.stdin:write("one request\n") if written == nil then error(writeReason) end child.stdin:close() local reply, readReason = child.stdout:read(4096) if reply == nil then error(readReason) end print(reply) end ) ``` `Process:wait`, pipe reads and writes, and `communicate` all use contextual waiting. They suspend a normal system without blocking SDL and block when called outside an update. Buffer and byte-view variants avoid constructing Lua strings. A pipe read waits at most the reader's timeout, thirty seconds by default and `Reader:setTimeout` to change it, and then returns nil and a reason. Reading one pipe at a time deadlocks against a child that writes to both: the pipe the caller ignores fills, the child stops, and the pipe the caller reads never produces another byte. `communicate` drains both and is the form to reach for whenever a child writes standard error at all. Use `"inherit"` for a CLI that should share the terminal, `"null"` to discard a stream, and `stderr = "stdout"` for one ordered transcript. A nonzero child exit still returns an [`Exit`](/modules/io/Process/#tecs.io.Process.Exit); `succeeded` performs the separate exit-code check. Spawn failures return nil and a reason. Deadlines and explicit kills set the exit's `killed` fields. ## Module contents ### Constructors | Constructor | Description | | --- | --- | | [`new`](/modules/io/Process/#tecs.io.Process.new) | Starts a caller-owned child process with streaming standard I/O. | ### Types | Type | Kind | Description | | --- | --- | --- | | [`CommunicateOptions`](/modules/io/Process/#tecs.io.Process.CommunicateOptions) | record | CommunicateOptions controls a complete duplex exchange. | | [`ErrorMode`](/modules/io/Process/#tecs.io.Process.ErrorMode) | enum | ErrorMode selects where a child writes standard error. | | [`Exit`](/modules/io/Process/#tecs.io.Process.Exit) | record | Exit describes how a child stopped. | | [`InputMode`](/modules/io/Process/#tecs.io.Process.InputMode) | enum | InputMode selects where a child reads standard input. | | [`Options`](/modules/io/Process/#tecs.io.Process.Options) | record | Options configures a child and its standard streams. | | [`OutputMode`](/modules/io/Process/#tecs.io.Process.OutputMode) | enum | OutputMode selects where a child writes standard output. | | [`Reader`](/modules/io/Process/#tecs.io.Process.Reader) | interface | Reader reads one live child-process output pipe. | | [`Result`](/modules/io/Process/#tecs.io.Process.Result) | record | Result contains a completed duplex exchange. | | [`Writer`](/modules/io/Process/#tecs.io.Process.Writer) | interface | Writer writes one live child-process input pipe. | ### Functions | Function | Kind | Description | | --- | --- | --- | | [`communicate`](/modules/io/Process/#tecs.io.Process.communicate) | Instance | Feeds stdin and captures stdout and stderr without pipe deadlock. | | [`isRunning`](/modules/io/Process/#tecs.io.Process.isRunning) | Instance | Returns whether the child has not stopped yet. | | [`kill`](/modules/io/Process/#tecs.io.Process.kill) | Instance | Requests that the child terminate. | | [`wait`](/modules/io/Process/#tecs.io.Process.wait) | Instance | Waits for the child to stop and returns its exit description. | ### Values | Value | Type | Description | | --- | --- | --- | | [`pid`](/modules/io/Process/#tecs.io.Process.pid) | `integer` | Read-only. Reports the operating-system process identifier. | | [`stderr`](/modules/io/Process/#tecs.io.Process.stderr) | [`Reader`](/modules/io/Process/#tecs.io.Process.Reader) | Read-only. Provides standard error when its mode is "pipe", or nil when error output is inherited, discarded, or... | | [`stdin`](/modules/io/Process/#tecs.io.Process.stdin) | [`Writer`](/modules/io/Process/#tecs.io.Process.Writer) | Read-only. Provides standard input when its mode is "pipe", or nil when input is inherited or discarded. | | [`stdout`](/modules/io/Process/#tecs.io.Process.stdout) | [`Reader`](/modules/io/Process/#tecs.io.Process.Reader) | Read-only. Provides standard output when its mode is "pipe", or nil when output is inherited or discarded. | ## Constructors ### tecs.io.Process.new Static Starts a caller-owned child process with streaming standard I/O. Piped endpoints use contextual waiting through the ordinary Reader and Writer methods. The process owns them, while buffers and byte views passed to endpoint methods remain caller-owned. ```teal function tecs.io.Process.new(options: Options): Process, string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `options` | [`Options`](/modules/io/Process/#tecs.io.Process.Options) | The caller supplies the program, arguments, environment, working directory, standard-stream modes, and optional deadline. | #### Returns | Type | Description | | --- | --- | | [`Process`](/modules/io/Process/) | Returns a caller-owned process when the platform starts it. | | `string` | Returns the platform reason when the first return is nil. | #### Examples ```teal tecs.scoped( "inspect repository", function(scope: tecs.Scope) local child = scope:own(tecs.io.Process.new({ args = {"git", "status", "--porcelain"}, cwd = tecs.io.Path.new("workspace"), })) local result, communicateReason = child:communicate() assert(result, communicateReason) end ) ``` ```teal tecs.scoped( "read revision", function(scope: tecs.Scope) local child = scope:own(tecs.io.Process.new({ args = {"git", "rev-parse", "HEAD"}, timeoutMs = 2000, })) local result, communicateReason = child:communicate() assert(result, communicateReason) assert(result:succeeded()) print(result.output) end ) ``` ## Types ### tecs.io.Process.CommunicateOptions record `CommunicateOptions` controls a complete duplex exchange. ```teal record tecs.io.Process.CommunicateOptions input: string | ByteView maxOutputBytes: integer end ``` #### tecs.io.Process.CommunicateOptions.input field Caller-writable. Supplies complete standard-input bytes as a string, open [`Buffer`](/modules/io/#tecs.io.Buffer), or open [`ByteView`](/modules/io/#tecs.io.ByteView). Omitted input sends EOF immediately. ```teal tecs.io.Process.CommunicateOptions.input: string | ByteView ``` #### tecs.io.Process.CommunicateOptions.maxOutputBytes field Caller-writable. Limits stdout and stderr together. Defaults to 268,435,456 bytes. Exceeding it forcibly terminates the child and returns a failure. ```teal tecs.io.Process.CommunicateOptions.maxOutputBytes: integer ``` ### tecs.io.Process.ErrorMode enum `ErrorMode` selects where a child writes standard error. ```teal enum tecs.io.Process.ErrorMode "inherit" "null" "pipe" "stdout" end ``` ### tecs.io.Process.Exit record `Exit` describes how a child stopped. ```teal record tecs.io.Process.Exit exitCode: integer killed: boolean timedOut: boolean succeeded: function(self): boolean end ``` #### tecs.io.Process.Exit.exitCode field Read-only. Reports the platform exit code, or the platform's best answer after termination. ```teal tecs.io.Process.Exit.exitCode: integer ``` #### tecs.io.Process.Exit.killed field Read-only. Reports whether Tecs requested termination. ```teal tecs.io.Process.Exit.killed: boolean ``` #### tecs.io.Process.Exit.timedOut field Read-only. Reports whether the configured deadline requested that termination. ```teal tecs.io.Process.Exit.timedOut: boolean ``` #### tecs.io.Process.Exit:succeeded Instance Returns whether the child exited normally with code zero. ```teal function tecs.io.Process.Exit.succeeded(self): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `ProcessExit` | The completed exit to inspect. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns true only for a normal zero exit. | ### tecs.io.Process.InputMode enum `InputMode` selects where a child reads standard input. ```teal enum tecs.io.Process.InputMode "inherit" "null" "pipe" end ``` ### tecs.io.Process.Options record `Options` configures a child and its standard streams. ```teal record tecs.io.Process.Options args: {string} cwd: string | Path env: {string: string} clearEnv: boolean stdin: ProcessInputMode stdout: ProcessOutputMode stderr: ProcessErrorMode timeoutMs: integer end ``` #### tecs.io.Process.Options.args field Caller-writable. Supplies the program in `args[1]` followed by its arguments. A program without a separator resolves through `PATH`. ```teal tecs.io.Process.Options.args: {string} ``` #### tecs.io.Process.Options.cwd field Caller-writable. Selects the child's working directory as a string or [`Path`](/modules/io/Path/), or omits it to inherit the current directory. ```teal tecs.io.Process.Options.cwd: string | Path ``` #### tecs.io.Process.Options.env field Caller-writable. Overlays environment variables on the inherited environment, or supplies the complete environment with `clearEnv`. ```teal tecs.io.Process.Options.env: {string: string} ``` #### tecs.io.Process.Options.clearEnv field Caller-writable. Starts with an empty environment when true. Defaults to false. ```teal tecs.io.Process.Options.clearEnv: boolean ``` #### tecs.io.Process.Options.stdin field Caller-writable. Selects `"pipe"`, `"inherit"`, or `"null"` for standard input. Defaults to `"pipe"`. ```teal tecs.io.Process.Options.stdin: ProcessInputMode ``` #### tecs.io.Process.Options.stdout field Caller-writable. Selects `"pipe"`, `"inherit"`, or `"null"` for standard output. Defaults to `"pipe"`. ```teal tecs.io.Process.Options.stdout: ProcessOutputMode ``` #### tecs.io.Process.Options.stderr field Caller-writable. Selects `"pipe"`, `"inherit"`, `"null"`, or `"stdout"` for standard error. Defaults to `"pipe"`. ```teal tecs.io.Process.Options.stderr: ProcessErrorMode ``` #### tecs.io.Process.Options.timeoutMs field Caller-writable. Forcibly terminates the child after this many milliseconds, or omits the deadline. The deadline begins when the process is created. ```teal tecs.io.Process.Options.timeoutMs: integer ``` ### tecs.io.Process.OutputMode enum `OutputMode` selects where a child writes standard output. ```teal enum tecs.io.Process.OutputMode "inherit" "null" "pipe" end ``` ### tecs.io.Process.Reader interface `Reader` reads one live child-process output pipe. ```teal interface tecs.io.Process.Reader is Reader isClosed: function(self): boolean isEOF: function(self): boolean setTimeout: function(self, timeoutMs: integer) end ``` #### Interfaces | Interface | | --- | | [`Reader`](/modules/io/#tecs.io.Reader) | #### tecs.io.Process.Reader:isClosed Instance Returns whether this endpoint has been closed. ```teal function tecs.io.Process.Reader.isClosed(self): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `ProcessReader` | The process reader to inspect. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns true after `close`. | #### tecs.io.Process.Reader:isEOF Instance Returns whether the child closed its end and every byte was consumed. ```teal function tecs.io.Process.Reader.isEOF(self): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `ProcessReader` | The process reader to inspect. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns true after end of file. | #### tecs.io.Process.Reader:setTimeout Instance Bounds how long `read` and `readInto` wait for the child to send bytes. The bound covers waiting only, so a read that already has bytes returns them however long the reader has been idle. It starts when a call first finds the pipe empty and ends that call with nil and a reason. The default is 30,000 milliseconds, and zero returns as soon as one attempt finds no bytes. The new bound applies to the next call rather than to one already waiting. ```teal function tecs.io.Process.Reader.setTimeout(self, timeoutMs: integer) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `ProcessReader` | The process reader to configure. | | `timeoutMs` | `integer` | The caller supplies 0 through 2147483647 milliseconds. Any other value raises. | ##### Returns None. ### tecs.io.Process.Result record `Result` contains a completed duplex exchange. ```teal record tecs.io.Process.Result exit: ProcessExit output: string errorOutput: string succeeded: function(self): boolean end ``` #### tecs.io.Process.Result.exit field Read-only. Contains the child's exit description. ```teal tecs.io.Process.Result.exit: ProcessExit ``` #### tecs.io.Process.Result.output field Read-only. Contains every captured standard-output byte. It is empty when stdout was inherited or discarded. ```teal tecs.io.Process.Result.output: string ``` #### tecs.io.Process.Result.errorOutput field Read-only. Contains every captured standard-error byte. It is empty when stderr was inherited, discarded, or merged into stdout. ```teal tecs.io.Process.Result.errorOutput: string ``` #### tecs.io.Process.Result:succeeded Instance Returns whether the child exited normally with code zero. ```teal function tecs.io.Process.Result.succeeded(self): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `ProcessResult` | The completed exchange to inspect. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns true only for a normal zero exit. | ### tecs.io.Process.Writer interface `Writer` writes one live child-process input pipe. ```teal interface tecs.io.Process.Writer is Writer isClosed: function(self): boolean setTimeout: function(self, timeoutMs: integer) end ``` #### Interfaces | Interface | | --- | | [`Writer`](/modules/io/#tecs.io.Writer) | #### tecs.io.Process.Writer:isClosed Instance Returns whether this endpoint has sent EOF or been closed. ```teal function tecs.io.Process.Writer.isClosed(self): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `ProcessWriter` | The process writer to inspect. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns true after `close`. | #### tecs.io.Process.Writer:setTimeout Instance Bounds how long `write`, `writeFrom`, and `writeView` wait for the child to take bytes. The bound covers waiting only, so a write the pipe has room for returns however long the writer has been idle. It starts when a call first finds the pipe full and ends that call with its failure and a reason, and the child taking further bytes starts it again, so it bounds one stall rather than a whole long write. The default is 30,000 milliseconds, and zero returns as soon as one attempt finds no room. The new bound applies to the next call rather than to one already waiting. ```teal function tecs.io.Process.Writer.setTimeout(self, timeoutMs: integer) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `ProcessWriter` | The process writer to configure. | | `timeoutMs` | `integer` | The caller supplies 0 through 2147483647 milliseconds. Any other value raises. | ##### Returns None. ## Functions ### tecs.io.Process:communicate Instance Feeds stdin and captures stdout and stderr without pipe deadlock. The call drains both output pipes while feeding input, closes stdin after the final byte, and returns after the child exits and both pipes reach EOF. It suspends a normal system or blocks an ordinary caller. ```teal function tecs.io.Process.communicate( self, options: CommunicateOptions ): Result, string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Process` | The open process whose piped endpoints are consumed. | | `options` | [`CommunicateOptions`](/modules/io/Process/#tecs.io.Process.CommunicateOptions) | The caller supplies complete input and an output limit, or omits both. | #### Returns | Type | Description | | --- | --- | | [`Result`](/modules/io/Process/#tecs.io.Process.Result) | Returns the complete exchange result. | | `string` | Returns a reason when communication fails or exceeds its output limit. | #### Examples ```teal tecs.scoped( "communicate with child", function(scope: tecs.Scope) local child = scope:own(tecs.io.Process.new({ args = {"/bin/cat"} })) local result, communicateReason = child:communicate({ input = "request bytes\n", maxOutputBytes = 1024 * 1024, }) assert(result, communicateReason) assert(result.output == "request bytes\n") end ) ``` ### tecs.io.Process:isRunning Instance Returns whether the child has not stopped yet. ```teal function tecs.io.Process.isRunning(self): boolean ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Process` | The process to inspect. | #### Returns | Type | Description | | --- | --- | | `boolean` | Returns true while the child remains live. | ### tecs.io.Process:kill Instance Requests that the child terminate. ```teal function tecs.io.Process.kill(self, force: boolean): boolean, string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Process` | The process to terminate. | | `force` | `boolean` | The caller requests an unhandleable termination when true or a graceful request when false or omitted. | #### Returns | Type | Description | | --- | --- | | `boolean` | Returns whether the request was delivered or the child had already stopped. | | `string` | Returns the platform reason when the first return is false. | ### tecs.io.Process:wait Instance Waits for the child to stop and returns its exit description. Inside a system, the call suspends the logical world update without blocking the host thread. Outside a world update, it blocks. ```teal function tecs.io.Process.wait(self): Exit ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Process` | The process to wait for. | #### Returns | Type | Description | | --- | --- | | [`Exit`](/modules/io/Process/#tecs.io.Process.Exit) | Returns how the child stopped. | ## Values ### tecs.io.Process.pid variable Read-only. Reports the operating-system process identifier. ```teal tecs.io.Process.pid: integer ``` ### tecs.io.Process.stderr variable Read-only. Provides standard error when its mode is `"pipe"`, or nil when error output is inherited, discarded, or merged into stdout. The process owns it. ```teal tecs.io.Process.stderr: Reader ``` ### tecs.io.Process.stdin variable Read-only. Provides standard input when its mode is `"pipe"`, or nil when input is inherited or discarded. The process owns it. ```teal tecs.io.Process.stdin: Writer ``` ### tecs.io.Process.stdout variable Read-only. Provides standard output when its mode is `"pipe"`, or nil when output is inherited or discarded. The process owns it. ```teal tecs.io.Process.stdout: Reader ``` --- ## tecs.io.URI # tecs.io.URI Immutable absolute URIs for protocols, resources, and application links. `URI` is not tied to HTTP. It accepts every absolute scheme supported by the native URL parser, while an API such as the HTTP client separately restricts the schemes it can use. Components are parsed once and copied into Lua, so accessors do not reparse or allocate: ```teal local endpoint = tecs.io.URI.new("https://api.example.com/v1") local scores = endpoint:concatPath("scores"):withQuery( "limit=20" ) print(scores:scheme(), scores:host(), scores:path()) print(scores) ``` URI values are immutable. Every `with` method and `concatPath` returns a new value and leaves its receiver unchanged. Supplying a component record to `tecs.io.URI.new` constructs the complete initial value without intermediate copies. A modifier clones the retained parsed value and validates only the replacement component; supplying the existing value returns the receiver. `withEndpoint` replaces the scheme, user information, host, and port from another URI, prefixes the receiver's path with the endpoint path, and preserves the receiver's query and fragment. That makes a configured service endpoint composable with a modeled resource URI without rebuilding either from strings. `resolve` applies an RFC-style relative reference to the receiver. The constructor itself requires an absolute URI, which keeps every stored value self-contained and leaves relative text at the operation where its base is known. A URI owns only immutable Lua strings and needs no `close`. ## Module contents ### Constructors | Constructor | Description | | --- | --- | | [`new`](/modules/io/URI/#tecs.io.URI.new) | Parses and normalizes an absolute URI. | ### Types | Type | Kind | Description | | --- | --- | --- | | [`Components`](/modules/io/URI/#tecs.io.URI.Components) | record | Components constructs one URI without intermediate immutable copies. | ### Functions | Function | Kind | Description | | --- | --- | --- | | [`isURI`](/modules/io/URI/#tecs.io.URI.isURI) | Static | Returns whether a value is a Tecs URI. | | [`validate`](/modules/io/URI/#tecs.io.URI.validate) | Static | Validates an absolute URI without retaining a value. | | [`authority`](/modules/io/URI/#tecs.io.URI.authority) | Instance | Returns the encoded authority. | | [`concatPath`](/modules/io/URI/#tecs.io.URI.concatPath) | Instance | Appends a path while preserving one separator at the boundary. | | [`fragment`](/modules/io/URI/#tecs.io.URI.fragment) | Instance | Returns the encoded fragment without its hash. | | [`host`](/modules/io/URI/#tecs.io.URI.host) | Instance | Returns the normalized host. | | [`password`](/modules/io/URI/#tecs.io.URI.password) | Instance | Returns the encoded password. | | [`path`](/modules/io/URI/#tecs.io.URI.path) | Instance | Returns the encoded path. | | [`port`](/modules/io/URI/#tecs.io.URI.port) | Instance | Returns the explicit port. | | [`query`](/modules/io/URI/#tecs.io.URI.query) | Instance | Returns the encoded query without its question mark. | | [`resolve`](/modules/io/URI/#tecs.io.URI.resolve) | Instance | Resolves a relative or absolute URI reference. | | [`scheme`](/modules/io/URI/#tecs.io.URI.scheme) | Instance | Returns the lower-cased scheme without its colon. | | [`toString`](/modules/io/URI/#tecs.io.URI.toString) | Instance | Returns the normalized absolute URI. | | [`userInfo`](/modules/io/URI/#tecs.io.URI.userInfo) | Instance | Returns the encoded user information. | | [`username`](/modules/io/URI/#tecs.io.URI.username) | Instance | Returns the encoded username. | | [`withEndpoint`](/modules/io/URI/#tecs.io.URI.withEndpoint) | Instance | Applies another URI as this resource's endpoint. | | [`withFragment`](/modules/io/URI/#tecs.io.URI.withFragment) | Instance | Replaces or removes the fragment and returns a new URI. | | [`withHost`](/modules/io/URI/#tecs.io.URI.withHost) | Instance | Replaces or removes the host and returns a new URI. | | [`withPath`](/modules/io/URI/#tecs.io.URI.withPath) | Instance | Replaces the path and returns a new URI. | | [`withPort`](/modules/io/URI/#tecs.io.URI.withPort) | Instance | Replaces or removes the explicit port and returns a new URI. | | [`withQuery`](/modules/io/URI/#tecs.io.URI.withQuery) | Instance | Replaces or removes the query and returns a new URI. | | [`withScheme`](/modules/io/URI/#tecs.io.URI.withScheme) | Instance | Replaces the scheme and returns a new URI. | | [`withUserInfo`](/modules/io/URI/#tecs.io.URI.withUserInfo) | Instance | Replaces or removes user information and returns a new URI. | ## Constructors ### tecs.io.URI.new Static Parses and normalizes an absolute URI. A component record performs one construction; it does not allocate an intermediate URI for each component. ```teal function tecs.io.URI.new( value: string | Components ): URI, string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `value` | string | [`Components`](/modules/io/URI/#tecs.io.URI.Components) | The caller supplies absolute UTF-8 text or URI components. | #### Returns | Type | Description | | --- | --- | | [`URI`](/modules/io/URI/) | Returns an immutable URI, or nil when the text is invalid or relative. | | `string` | Returns the parser's reason when the first return is nil. | #### Examples ```teal local endpoint, reason = tecs.io.URI.new( "https://user@example.com:8443/assets/list.json?page=2" ) if endpoint == nil then error(reason) end assert(endpoint:scheme() == "https") assert(endpoint:host() == "example.com") assert(endpoint:port() == 8443) assert(endpoint:path() == "/assets/list.json") assert(endpoint:query() == "page=2") ``` ## Types ### tecs.io.URI.Components record `Components` constructs one URI without intermediate immutable copies. ```teal record tecs.io.URI.Components scheme: string userInfo: string host: string port: integer path: string query: string fragment: string end ``` #### Examples ```teal local endpoint, reason = tecs.io.URI.new({ scheme = "https", userInfo = "builder:secret", host = "api.example.com", port = 8443, path = "/v2/scores", query = "limit=20", }) if endpoint == nil then error(reason) end assert( endpoint:toString( ) == "https://builder:secret@api.example.com:8443/v2/scores?limit=20" ) ``` #### tecs.io.URI.Components.scheme field Caller-writable. Supplies the required scheme without its colon. ```teal tecs.io.URI.Components.scheme: string ``` #### tecs.io.URI.Components.userInfo field Caller-writable. Supplies encoded `username` or `username:password`, or nil for none. ```teal tecs.io.URI.Components.userInfo: string ``` #### tecs.io.URI.Components.host field Caller-writable. Supplies the host, or nil for a scheme without an authority. IPv6 literals may include or omit brackets. ```teal tecs.io.URI.Components.host: string ``` #### tecs.io.URI.Components.port field Caller-writable. Supplies the explicit port, or nil for none. ```teal tecs.io.URI.Components.port: integer ``` #### tecs.io.URI.Components.path field Caller-writable. Supplies the path. Nil means an empty path. ```teal tecs.io.URI.Components.path: string ``` #### tecs.io.URI.Components.query field Caller-writable. Supplies the query without `?`, or nil for none. ```teal tecs.io.URI.Components.query: string ``` #### tecs.io.URI.Components.fragment field Caller-writable. Supplies the fragment without `#`, or nil for none. ```teal tecs.io.URI.Components.fragment: string ``` ## Functions ### tecs.io.URI.isURI Static Returns whether a value is a Tecs URI. ```teal function tecs.io.URI.isURI(value: any): boolean ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `value` | `any` | The caller supplies any Lua value. | #### Returns | Type | Description | | --- | --- | | `boolean` | Returns true only for values created by this module. | ### tecs.io.URI.validate Static Validates an absolute URI without retaining a value. ```teal function tecs.io.URI.validate(text: string): boolean, string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `text` | `string` | The caller supplies the UTF-8 text to validate. | #### Returns | Type | Description | | --- | --- | | `boolean` | Returns true when `new` can parse the text. | | `string` | Returns the parser's reason when the first return is false. | #### Examples ```teal local valid = tecs.io.URI.validate("urn:tecs:asset:sprite") local relative , reason = tecs.io.URI.validate("../sprite.png") assert(valid) assert(not relative) assert(reason ~= nil) ``` ### tecs.io.URI:authority Instance Returns the encoded authority. ```teal function tecs.io.URI.authority(self): string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `URI` | The immutable URI to inspect. | #### Returns | Type | Description | | --- | --- | | `string` | Returns user information, host, and port, or nil when this URI has no authority. | ### tecs.io.URI:concatPath Instance Appends a path while preserving one separator at the boundary. ```teal function tecs.io.URI.concatPath(self, path: string): URI ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `URI` | The URI left unchanged. | | `path` | `string` | The caller supplies the path suffix to append. | #### Returns | Type | Description | | --- | --- | | [`URI`](/modules/io/URI/) | Returns a new immutable URI. | #### Examples ```teal local api, reason = tecs.io.URI.new("https://example.com/v1/") if api == nil then error(reason) end local manifest = api:concatPath("/games/42/manifest.json") assert(manifest:path() == "/v1/games/42/manifest.json") ``` ### tecs.io.URI:fragment Instance Returns the encoded fragment without its hash. ```teal function tecs.io.URI.fragment(self): string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `URI` | The immutable URI to inspect. | #### Returns | Type | Description | | --- | --- | | `string` | Returns the fragment, or nil when the URI omits it. | ### tecs.io.URI:host Instance Returns the normalized host. ```teal function tecs.io.URI.host(self): string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `URI` | The immutable URI to inspect. | #### Returns | Type | Description | | --- | --- | | `string` | Returns the host, or nil when this scheme has none. | ### tecs.io.URI:password Instance Returns the encoded password. ```teal function tecs.io.URI.password(self): string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `URI` | The immutable URI to inspect. | #### Returns | Type | Description | | --- | --- | | `string` | Returns the password, or nil when none was supplied. | ### tecs.io.URI:path Instance Returns the encoded path. ```teal function tecs.io.URI.path(self): string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `URI` | The immutable URI to inspect. | #### Returns | Type | Description | | --- | --- | | `string` | Returns the path, including its leading slash when present. | ### tecs.io.URI:port Instance Returns the explicit port. ```teal function tecs.io.URI.port(self): integer ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `URI` | The immutable URI to inspect. | #### Returns | Type | Description | | --- | --- | | `integer` | Returns the explicit port, or nil when the URI omits it. | ### tecs.io.URI:query Instance Returns the encoded query without its question mark. ```teal function tecs.io.URI.query(self): string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `URI` | The immutable URI to inspect. | #### Returns | Type | Description | | --- | --- | | `string` | Returns the query, or nil when the URI omits it. | ### tecs.io.URI:resolve Instance Resolves a relative or absolute URI reference. ```teal function tecs.io.URI.resolve(self, reference: string): URI, string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `URI` | The absolute base URI left unchanged. | | `reference` | `string` | The caller supplies an RFC-style URI reference. | #### Returns | Type | Description | | --- | --- | | [`URI`](/modules/io/URI/) | Returns the resolved immutable URI. | | `string` | Returns the parser's reason when the reference is invalid. | #### Examples ```teal local page, reason = tecs.io.URI.new( "https://example.com/games/current/" ) if page == nil then error(reason) end local scores, resolveReason = page:resolve("../42/scores") if scores == nil then error(resolveReason) end assert(scores:toString() == "https://example.com/games/42/scores") ``` ### tecs.io.URI:scheme Instance Returns the lower-cased scheme without its colon. ```teal function tecs.io.URI.scheme(self): string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `URI` | The immutable URI to inspect. | #### Returns | Type | Description | | --- | --- | | `string` | Returns the required scheme. | ### tecs.io.URI:toString Instance Returns the normalized absolute URI. `tostring(uri)` returns the same string. ```teal function tecs.io.URI.toString(self): string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `URI` | The immutable URI to render. | #### Returns | Type | Description | | --- | --- | | `string` | Returns the complete normalized URI. | ### tecs.io.URI:userInfo Instance Returns the encoded user information. ```teal function tecs.io.URI.userInfo(self): string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `URI` | The immutable URI to inspect. | #### Returns | Type | Description | | --- | --- | | `string` | Returns `username`, `username:password`, or nil when neither was supplied. | ### tecs.io.URI:username Instance Returns the encoded username. ```teal function tecs.io.URI.username(self): string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `URI` | The immutable URI to inspect. | #### Returns | Type | Description | | --- | --- | | `string` | Returns the username, or an empty string when none was supplied. | ### tecs.io.URI:withEndpoint Instance Applies another URI as this resource's endpoint. The endpoint supplies scheme, user information, host, and port. Its path prefixes this URI's path; this URI retains its query and fragment. ```teal function tecs.io.URI.withEndpoint(self, endpoint: URI): URI ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `URI` | The resource URI left unchanged. | | `endpoint` | [`URI`](/modules/io/URI/) | The caller supplies the service endpoint. | #### Returns | Type | Description | | --- | --- | | [`URI`](/modules/io/URI/) | Returns a new immutable URI. | #### Examples ```teal local resource, resourceReason = tecs.io.URI.new( "smithy:/games/42/scores?limit=10" ) if resource == nil then error(resourceReason) end local endpoint, endpointReason = tecs.io.URI.new( "https://api.example.com/v2" ) if endpoint == nil then error(endpointReason) end local request = resource:withEndpoint(endpoint) assert( request:toString( ) == "https://api.example.com/v2/games/42/scores?limit=10" ) ``` ### tecs.io.URI:withFragment Instance Replaces or removes the fragment and returns a new URI. ```teal function tecs.io.URI.withFragment(self, fragment: string): URI ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `URI` | The URI left unchanged. | | `fragment` | `string` | The caller supplies a fragment without `#`, or nil to remove it. | #### Returns | Type | Description | | --- | --- | | [`URI`](/modules/io/URI/) | Returns a new immutable URI. | ### tecs.io.URI:withHost Instance Replaces or removes the host and returns a new URI. ```teal function tecs.io.URI.withHost(self, host: string): URI ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `URI` | The URI left unchanged. | | `host` | `string` | The caller supplies a host, or nil when the scheme permits no host. | #### Returns | Type | Description | | --- | --- | | [`URI`](/modules/io/URI/) | Returns a new immutable URI. | ### tecs.io.URI:withPath Instance Replaces the path and returns a new URI. ```teal function tecs.io.URI.withPath(self, path: string): URI ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `URI` | The URI left unchanged. | | `path` | `string` | The caller supplies an encoded or unencoded path. | #### Returns | Type | Description | | --- | --- | | [`URI`](/modules/io/URI/) | Returns a new immutable URI with a normalized path. | ### tecs.io.URI:withPort Instance Replaces or removes the explicit port and returns a new URI. ```teal function tecs.io.URI.withPort(self, port: integer): URI ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `URI` | The URI left unchanged. | | `port` | `integer` | The caller supplies a port from zero through 65535, or nil to omit it. | #### Returns | Type | Description | | --- | --- | | [`URI`](/modules/io/URI/) | Returns a new immutable URI. | ### tecs.io.URI:withQuery Instance Replaces or removes the query and returns a new URI. ```teal function tecs.io.URI.withQuery(self, query: string): URI ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `URI` | The URI left unchanged. | | `query` | `string` | The caller supplies a query without `?`, or nil to remove it. | #### Returns | Type | Description | | --- | --- | | [`URI`](/modules/io/URI/) | Returns a new immutable URI. | #### Examples ```teal local scores, reason = tecs.io.URI.new("https://example.com/scores") if scores == nil then error(reason) end local firstPage = scores:withQuery("limit=20&offset=0") assert(scores:query() == nil) assert(firstPage:query() == "limit=20&offset=0") ``` ### tecs.io.URI:withScheme Instance Replaces the scheme and returns a new URI. ```teal function tecs.io.URI.withScheme(self, scheme: string): URI ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `URI` | The URI left unchanged. | | `scheme` | `string` | The caller supplies a scheme without its colon. | #### Returns | Type | Description | | --- | --- | | [`URI`](/modules/io/URI/) | Returns a new immutable URI. | ### tecs.io.URI:withUserInfo Instance Replaces or removes user information and returns a new URI. ```teal function tecs.io.URI.withUserInfo(self, userInfo: string): URI ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `URI` | The URI left unchanged. | | `userInfo` | `string` | The caller supplies `username`, `username:password`, or nil to remove both components. | #### Returns | Type | Description | | --- | --- | | [`URI`](/modules/io/URI/) | Returns a new immutable URI. | --- ## tecs.io.files # tecs.io.files Resolves game paths and performs complete filesystem operations. Use `assetPath` for shipped content, `writablePath` for persistent mutable state, and `cachePath` for data the application can reconstruct. Set the publisher and game names before resolving the first writable path: ```teal local files = tecs.io.files files.setPreferenceIdentity("Ex Nihilo", "Starfarer") local directory = files.writablePath("saves") local ok, reason = files.createDirectory(directory) if not ok then error(reason) end local path = files.writablePath("saves/slot1.json") local bytes = tecs.data.encodeJSON({level = 3, hp = 100}) ok, reason = files.writeAtomic(path, bytes) if not ok then error(reason) end ``` Filesystem outcomes return a status and a platform reason. Invalid arguments raise because they indicate a defect in the calling program. An atomic write inside a system moves its commit to a worker and suspends the system until the result is ready. `glob` exposes a caller-owned pull stream so recursive enumeration does not retain a complete tree. Temporary files, temporary directories, and streams are [`Closeable`](/modules/#tecs.Closeable) and fit directly in `tecs.scoped`. Every operation that accepts a filesystem location accepts either a string or an immutable [`Path`](/modules/io/Path/). String results remain useful at Lua and platform boundaries; wrap one with `tecs.io.Path.new` when the next operation benefits from component-aware manipulation. `tecs.io.watcher` polls paths recorded by `read` and the built-in asset loaders. It does not walk the whole asset tree. `open` uses the standard Lua modes and returns one seekable file cursor. The SDL backend operates on the file directly. A storage backend without random access falls back to its required whole-file operations and retains the bytes until the file closes. ## Module contents ### Types | Type | Kind | Description | | --- | --- | --- | | [`DirectoryEntry`](/modules/io/files/#tecs.io.files.DirectoryEntry) | record | DirectoryEntry describes one streamed child. | | [`DirectoryStream`](/modules/io/files/#tecs.io.files.DirectoryStream) | record | DirectoryStream is a caller-owned pull cursor over directory entries. | | [`File`](/modules/io/files/#tecs.io.files.File) | interface | File is the seekable cursor returned by open. | | [`FileMode`](/modules/io/files/#tecs.io.files.FileMode) | enum | FileMode selects standard read, write, append, or update behavior. | | [`GlobOptions`](/modules/io/files/#tecs.io.files.GlobOptions) | record | GlobOptions controls pattern matching and traversal depth. | | [`Info`](/modules/io/files/#tecs.io.files.Info) | record | Info describes one resolved path. | | [`LineIterator`](/modules/io/files/#tecs.io.files.LineIterator) | type | LineIterator yields each line in a file and returns nil at the end. | | [`PathInput`](/modules/io/files/#tecs.io.files.PathInput) | type | PathInput accepts either a platform path string or an immutable path object. | | [`PathType`](/modules/io/files/#tecs.io.files.PathType) | enum | PathType identifies a file, directory or other platform object after following symbolic links. | | [`SymlinkKind`](/modules/io/files/#tecs.io.files.SymlinkKind) | enum | SymlinkKind selects a file or directory symbolic link. | | [`TemporaryOptions`](/modules/io/files/#tecs.io.files.TemporaryOptions) | record | TemporaryOptions selects generated-name details. | | [`TemporaryPath`](/modules/io/files/#tecs.io.files.TemporaryPath) | record | TemporaryPath owns an automatically removed file or directory. | | [`UserFolder`](/modules/io/files/#tecs.io.files.UserFolder) | enum | UserFolder identifies a well-known platform folder. | ### Functions | Function | Kind | Description | | --- | --- | --- | | [`assetPath`](/modules/io/files/#tecs.io.files.assetPath) | Static | Resolves relative against the asset root. | | [`assetRoot`](/modules/io/files/#tecs.io.files.assetRoot) | Static | Returns the root from which the engine reads content. | | [`basePath`](/modules/io/files/#tecs.io.files.basePath) | Static | Returns the directory that contains the executable. | | [`cachePath`](/modules/io/files/#tecs.io.files.cachePath) | Static | Returns the system cache directory for this application and creates it if absent. | | [`copy`](/modules/io/files/#tecs.io.files.copy) | Static | Copies the file at from to to, replacing whatever was at to. | | [`createDirectory`](/modules/io/files/#tecs.io.files.createDirectory) | Static | Creates path, and any parents it needs. | | [`createSymlink`](/modules/io/files/#tecs.io.files.createSymlink) | Static | Creates a symbolic link to a file or directory. | | [`createTemporaryDirectory`](/modules/io/files/#tecs.io.files.createTemporaryDirectory) | Static | Creates an empty temporary directory owned by the returned resource. | | [`createTemporaryFile`](/modules/io/files/#tecs.io.files.createTemporaryFile) | Static | Creates an empty temporary file owned by the returned resource. | | [`currentDirectory`](/modules/io/files/#tecs.io.files.currentDirectory) | Static | Returns the process working directory. | | [`exists`](/modules/io/files/#tecs.io.files.exists) | Static | Returns whether anything occupies path. | | [`glob`](/modules/io/files/#tecs.io.files.glob) | Static | Opens a pull stream over entries under path that match pattern. | | [`info`](/modules/io/files/#tecs.io.files.info) | Static | Returns information about path, or nil when nothing occupies it. | | [`isDirectory`](/modules/io/files/#tecs.io.files.isDirectory) | Static | Returns whether path resolves to a directory. | | [`isFile`](/modules/io/files/#tecs.io.files.isFile) | Static | Returns whether path resolves to a regular file. | | [`isSymlink`](/modules/io/files/#tecs.io.files.isSymlink) | Static | Returns whether the final object named by path is a symbolic link. | | [`lines`](/modules/io/files/#tecs.io.files.lines) | Static | Iterates over the lines in a whole file. | | [`load`](/modules/io/files/#tecs.io.files.load) | Static | Compiles a Lua file without running it. | | [`open`](/modules/io/files/#tecs.io.files.open) | Static | Opens a file through one seekable cursor. | | [`preferenceIdentity`](/modules/io/files/#tecs.io.files.preferenceIdentity) | Static | Returns the publisher and game names used for writable user data. | | [`preferencePath`](/modules/io/files/#tecs.io.files.preferencePath) | Static | Returns the writable directory for this application and creates it if absent. | | [`read`](/modules/io/files/#tecs.io.files.read) | Static | Reads a whole file and returns its bytes, or nil when there is none there. | | [`readInto`](/modules/io/files/#tecs.io.files.readInto) | Static | Reads a whole file directly into an owned buffer. | | [`readLink`](/modules/io/files/#tecs.io.files.readLink) | Static | Returns the target spelling stored in a symbolic link. | | [`remove`](/modules/io/files/#tecs.io.files.remove) | Static | Removes a file, or an empty directory. | | [`rename`](/modules/io/files/#tecs.io.files.rename) | Static | Moves from to to, replacing whatever was at to. | | [`setAssetRoot`](/modules/io/files/#tecs.io.files.setAssetRoot) | Static | Overrides the asset root, for a test or a tool. | | [`setPreferenceIdentity`](/modules/io/files/#tecs.io.files.setPreferenceIdentity) | Static | Sets the publisher and game names used for writable user data. | | [`setReadOnly`](/modules/io/files/#tecs.io.files.setReadOnly) | Static | Changes the portable read-only state of a path. | | [`userFolder`](/modules/io/files/#tecs.io.files.userFolder) | Static | Returns one of the platform's well-known folders. | | [`writablePath`](/modules/io/files/#tecs.io.files.writablePath) | Static | Resolves relative against the writable root. | | [`write`](/modules/io/files/#tecs.io.files.write) | Static | Writes bytes to a file, replacing its complete contents. | | [`writeAtomic`](/modules/io/files/#tecs.io.files.writeAtomic) | Static | Durably and atomically writes a complete file. | ## Types ### tecs.io.files.DirectoryEntry record `DirectoryEntry` describes one streamed child. ```teal record tecs.io.files.DirectoryEntry path: Path name: string kind: storagebackend.PathType depth: integer symlink: boolean end ``` #### tecs.io.files.DirectoryEntry.path field Read-only. Contains the complete immutable path to this entry. ```teal tecs.io.files.DirectoryEntry.path: Path ``` #### tecs.io.files.DirectoryEntry.name field Read-only. Contains the entry name relative to its immediate parent. ```teal tecs.io.files.DirectoryEntry.name: string ``` #### tecs.io.files.DirectoryEntry.kind field Read-only. Identifies the resolved object's kind. ```teal tecs.io.files.DirectoryEntry.kind: storagebackend.PathType ``` #### tecs.io.files.DirectoryEntry.depth field Read-only. Reports one for an immediate child and increases during a recursive glob. ```teal tecs.io.files.DirectoryEntry.depth: integer ``` #### tecs.io.files.DirectoryEntry.symlink field Read-only. Reports whether the final path itself is a symbolic link. ```teal tecs.io.files.DirectoryEntry.symlink: boolean ``` ### tecs.io.files.DirectoryStream record `DirectoryStream` is a caller-owned pull cursor over directory entries. ```teal record tecs.io.files.DirectoryStream is Closeable close: function(self): boolean, string next: function(self): DirectoryEntry, string skipDirectory: function(self): boolean toArray: function(self): {DirectoryEntry}, string end ``` #### Interfaces | Interface | | --- | | `Closeable` | #### tecs.io.files.DirectoryStream:close Instance ```teal function tecs.io.files.DirectoryStream.close(self): boolean, string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `DirectoryStream` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | | | `string` | | #### tecs.io.files.DirectoryStream:next Instance Returns the next entry. ```teal function tecs.io.files.DirectoryStream.next( self ): DirectoryEntry, string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `DirectoryStream` | The open stream to advance. | ##### Returns | Type | Description | | --- | --- | | [`DirectoryEntry`](/modules/io/files/#tecs.io.files.DirectoryEntry) | Returns the next entry, or nil at the end or after close. | | `string` | Returns a platform reason when traversal fails. Nil means end of stream. | #### tecs.io.files.DirectoryStream:skipDirectory Instance Prevents descent into the directory returned by the preceding `next` call. The method returns false when the preceding entry was not a directory, was a symbolic link, had no deeper pattern component, or has already been skipped. Calling `next` first makes an earlier directory no longer eligible for pruning. ```teal function tecs.io.files.DirectoryStream.skipDirectory(self): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `DirectoryStream` | The open stream whose pending descent the caller controls. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns whether a pending directory descent was removed. | #### tecs.io.files.DirectoryStream:toArray Instance Consumes the remaining entries into an array and closes the stream. Entries returned by earlier `next` calls are not repeated. A traversal failure discards the partial array and closes the stream. ```teal function tecs.io.files.DirectoryStream.toArray( self ): {DirectoryEntry}, string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `DirectoryStream` | The open stream to consume. | ##### Returns | Type | Description | | --- | --- | | `{`[`DirectoryEntry`](/modules/io/files/#tecs.io.files.DirectoryEntry)`}` | Returns every remaining entry, or nil when traversal fails. | | `string` | Returns the platform reason when the first return is nil. | ### tecs.io.files.File interface `File` is the seekable cursor returned by `open`. ```teal interface tecs.io.files.File is types.SeekableReader, types.SeekableWriter end ``` #### Interfaces | Interface | | --- | | [`types.SeekableReader`](/modules/io/#tecs.io.SeekableReader) | | [`types.SeekableWriter`](/modules/io/#tecs.io.SeekableWriter) | ### tecs.io.files.FileMode enum `FileMode` selects standard read, write, append, or update behavior. ```teal enum tecs.io.files.FileMode "a" "a+" "r" "r+" "w" "w+" end ``` ### tecs.io.files.GlobOptions record `GlobOptions` controls pattern matching and traversal depth. ```teal record tecs.io.files.GlobOptions caseInsensitive: boolean maxDepth: integer end ``` #### tecs.io.files.GlobOptions.caseInsensitive field Caller-writable. Matches ASCII letters without regard to case when true. Defaults to false, which compares every UTF-8 codepoint exactly on every platform including the ones whose filesystem is not. ```teal tecs.io.files.GlobOptions.caseInsensitive: boolean ``` #### tecs.io.files.GlobOptions.maxDepth field Caller-writable. Limits traversal to this many levels below the root. Zero yields no entries, one yields immediate children, and nil permits unlimited traversal. Defaults to nil. ```teal tecs.io.files.GlobOptions.maxDepth: integer ``` ### tecs.io.files.Info record `Info` describes one resolved path. ```teal record tecs.io.files.Info kind: PathType size: integer createdAt: number modifiedAt: number accessedAt: number readOnly: boolean end ``` #### tecs.io.files.Info.kind field Read-only. Identifies the resolved platform object's kind. ```teal tecs.io.files.Info.kind: PathType ``` #### tecs.io.files.Info.size field Read-only. Reports size in bytes. Zero for a directory. ```teal tecs.io.files.Info.size: integer ``` #### tecs.io.files.Info.createdAt field Read-only. Reports nanoseconds since the epoch as a double. Zero means the platform or filesystem does not record a creation time. ```teal tecs.io.files.Info.createdAt: number ``` #### tecs.io.files.Info.modifiedAt field Read-only. Reports modification time in nanoseconds since the epoch. ```teal tecs.io.files.Info.modifiedAt: number ``` #### tecs.io.files.Info.accessedAt field Read-only. Reports access time in nanoseconds since the epoch. ```teal tecs.io.files.Info.accessedAt: number ``` #### tecs.io.files.Info.readOnly field Read-only. Reports the portable read-only state of the resolved path. ```teal tecs.io.files.Info.readOnly: boolean ``` ### tecs.io.files.LineIterator type `LineIterator` yields each line in a file and returns nil at the end. ```teal type tecs.io.files.LineIterator = function(): string ``` ### tecs.io.files.PathInput type `PathInput` accepts either a platform path string or an immutable path object. ```teal type tecs.io.files.PathInput = string | Path ``` ### tecs.io.files.PathType enum `PathType` identifies a file, directory or other platform object after following symbolic links. ```teal enum tecs.io.files.PathType "directory" "file" "other" end ``` ### tecs.io.files.SymlinkKind enum `SymlinkKind` selects a file or directory symbolic link. ```teal enum tecs.io.files.SymlinkKind "directory" "file" end ``` ### tecs.io.files.TemporaryOptions record `TemporaryOptions` selects generated-name details. ```teal record tecs.io.files.TemporaryOptions directory: string | Path prefix: string suffix: string end ``` #### tecs.io.files.TemporaryOptions.directory field Caller-writable. Selects the parent directory. Nil uses the operating system's temporary directory. ```teal tecs.io.files.TemporaryOptions.directory: string | Path ``` #### tecs.io.files.TemporaryOptions.prefix field Caller-writable. Prefixes the generated name. Defaults to `"tecs-"`. ```teal tecs.io.files.TemporaryOptions.prefix: string ``` #### tecs.io.files.TemporaryOptions.suffix field Caller-writable. Suffixes the generated name. Defaults to an empty string. ```teal tecs.io.files.TemporaryOptions.suffix: string ``` ### tecs.io.files.TemporaryPath record `TemporaryPath` owns an automatically removed file or directory. ```teal record tecs.io.files.TemporaryPath is Closeable path: Path close: function(self): boolean, string persist: function(self, destination: string | Path): boolean, string end ``` #### Interfaces | Interface | | --- | | `Closeable` | #### tecs.io.files.TemporaryPath.path field Read-only. Contains the exclusively created path. ```teal tecs.io.files.TemporaryPath.path: Path ``` #### tecs.io.files.TemporaryPath:close Instance ```teal function tecs.io.files.TemporaryPath.close(self): boolean, string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `TemporaryPath` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | | | `string` | | #### tecs.io.files.TemporaryPath:persist Instance Moves the resource to a permanent, absent destination and relinquishes cleanup. ```teal function tecs.io.files.TemporaryPath.persist( self, destination: string | Path ): boolean, string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `TemporaryPath` | The open temporary resource. | | `destination` | string | [`Path`](/modules/io/Path/) | The caller supplies a destination which must not exist. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns whether the resource became permanent. | | `string` | Returns the platform reason when the first return is false. | ### tecs.io.files.UserFolder enum `UserFolder` identifies a well-known platform folder. ```teal enum tecs.io.files.UserFolder "desktop" "documents" "downloads" "home" "music" "pictures" "publicShare" "savedGames" "screenshots" "templates" "videos" end ``` ## Functions ### tecs.io.files.assetPath Static Resolves `relative` against the asset root. ```teal function tecs.io.files.assetPath(relative: PathInput): string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `relative` | [`PathInput`](/modules/io/files/#tecs.io.files.PathInput) | The caller supplies a path under the content root with `/` separators. The function joins it without checking existence. | #### Returns | Type | Description | | --- | --- | | `string` | Returns the absolute path. | ### tecs.io.files.assetRoot Static Returns the root from which the engine reads content. ```teal function tecs.io.files.assetRoot(): string ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | `string` | Returns `TECS_ASSETS` when set, otherwise the host-configured root or `basePath`. The value stays cached until the platform changes. | ### tecs.io.files.basePath Static Returns the directory that contains the executable. ```teal function tecs.io.files.basePath(): string ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | `string` | Returns the directory with a trailing separator. It returns an empty string rather than nil when the platform declines to say, which is the case on some targets and is not an error: a caller falls back to a relative path. | ### tecs.io.files.cachePath Static Returns the system cache directory for this application and creates it if absent. The operating system may empty this directory between any two runs. Store only reconstructable downloads, compiled artifacts, thumbnails and similar derived data here. Saves, settings and other durable state belong under `preferencePath`. ```teal function tecs.io.files.cachePath(): string ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | `string` | Returns the directory with a trailing platform separator, named from the preference identity. | ### tecs.io.files.copy Static Copies the file at `from` to `to`, replacing whatever was at `to`. The function refuses directories and requires an existing destination parent. ```teal function tecs.io.files.copy( from: PathInput, to: PathInput ): boolean, string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `from` | [`PathInput`](/modules/io/files/#tecs.io.files.PathInput) | The caller supplies the existing source file. | | `to` | [`PathInput`](/modules/io/files/#tecs.io.files.PathInput) | The caller supplies the destination file to replace. | #### Returns | Type | Description | | --- | --- | | `boolean` | Returns whether the platform copied the file. | | `string` | Returns the platform reason when the first return is false. | ### tecs.io.files.createDirectory Static Creates `path`, and any parents it needs. It succeeds when the directory already exists. ```teal function tecs.io.files.createDirectory(path: PathInput): boolean, string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `path` | [`PathInput`](/modules/io/files/#tecs.io.files.PathInput) | The caller supplies the directory to create. | #### Returns | Type | Description | | --- | --- | | `boolean` | Returns whether the platform created or found the directory. | | `string` | Returns the platform reason when the first return is false. | ### tecs.io.files.createSymlink Static Creates a symbolic link to a file or directory. The target spelling is stored unchanged, so a relative target is resolved from the link's parent when later opened. `kind` is required because Windows must choose a file or directory link even for a dangling target. ```teal function tecs.io.files.createSymlink( target: PathInput, link: PathInput, kind: SymlinkKind ): boolean, string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `target` | [`PathInput`](/modules/io/files/#tecs.io.files.PathInput) | The caller supplies the target spelling to store. | | `link` | [`PathInput`](/modules/io/files/#tecs.io.files.PathInput) | The caller supplies the new link path. | | `kind` | [`SymlinkKind`](/modules/io/files/#tecs.io.files.SymlinkKind) | The caller supplies `"file"` or `"directory"`. | #### Returns | Type | Description | | --- | --- | | `boolean` | Returns whether the platform created the link. | | `string` | Returns the platform or unsupported reason when the first return is false. | ### tecs.io.files.createTemporaryDirectory Static Creates an empty temporary directory owned by the returned resource. Closing recursively removes its contents without following symbolic links. Call `persist` to keep the complete tree under an absent destination. ```teal function tecs.io.files.createTemporaryDirectory( options: TemporaryOptions ): TemporaryPath, string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `options` | [`TemporaryOptions`](/modules/io/files/#tecs.io.files.TemporaryOptions) | The caller may select the parent, prefix, and suffix. | #### Returns | Type | Description | | --- | --- | | [`TemporaryPath`](/modules/io/files/#tecs.io.files.TemporaryPath) | Returns the caller-owned temporary directory. | | `string` | Returns the platform or unsupported reason when creation fails. | ### tecs.io.files.createTemporaryFile Static Creates an empty temporary file owned by the returned resource. Creation is exclusive. Closing removes the file; garbage collection is a leak safety net, while [`tecs.scoped`](/modules/#tecs.scoped) provides deterministic cleanup. Call `persist` to keep the file under an absent permanent destination. ```teal function tecs.io.files.createTemporaryFile( options: TemporaryOptions ): TemporaryPath, string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `options` | [`TemporaryOptions`](/modules/io/files/#tecs.io.files.TemporaryOptions) | The caller may select the parent, prefix, and suffix. | #### Returns | Type | Description | | --- | --- | | [`TemporaryPath`](/modules/io/files/#tecs.io.files.TemporaryPath) | Returns the caller-owned temporary file. | | `string` | Returns the platform or unsupported reason when creation fails. | ### tecs.io.files.currentDirectory Static Returns the process working directory. Command-line tools receive relative paths and resolve them against this directory. It is not where a game reads content from or writes state to: `assetRoot`, `preferencePath` and `cachePath` answer that, and say why. A platform with no working directory answers nil, which is most of the ones that are not a desktop. ```teal function tecs.io.files.currentDirectory(): string, string ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | `string` | Returns the directory with a trailing separator, or nil when the platform has no working directory. | | `string` | Returns the platform reason when the first return is nil. | ### tecs.io.files.exists Static Returns whether anything occupies `path`. ```teal function tecs.io.files.exists(path: PathInput): boolean ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `path` | [`PathInput`](/modules/io/files/#tecs.io.files.PathInput) | The caller supplies an absolute or platform-resolvable path. | #### Returns | Type | Description | | --- | --- | | `boolean` | Returns true when the platform finds any object. | ### tecs.io.files.glob Static Opens a pull stream over entries under `path` that match `pattern`. **Recursive unless the pattern stops it**: with no pattern this walks the whole tree below `path`. `*` and `?` never match a separator, and `?` consumes one UTF-8 codepoint, so `"*"` is one level, `"*/*"` is exactly two, and `"*.png"` matches only immediate children. Results include directories alongside files. Symbolic links are yielded but never followed. The order is the platform's own and is not sorted. The stream retains only the unvisited names in each directory on its current descent. Close it when leaving early. After receiving a directory, call `skipDirectory` before `next` to prune that complete subtree. Call `toArray` to consume and close the stream when every remaining entry should be retained. A fixed bound belongs in `maxDepth`; `skipDirectory` remains useful when descent depends on the entry itself. The checked example combines both. ```teal function tecs.io.files.glob( path: PathInput, pattern: string, options: GlobOptions ): DirectoryStream, string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `path` | [`PathInput`](/modules/io/files/#tecs.io.files.PathInput) | The caller supplies the directory to enumerate. | | `pattern` | `string` | The caller supplies `/`-separated `*` and `?` wildcards, or nil to include every descendant recursively. | | `options` | [`GlobOptions`](/modules/io/files/#tecs.io.files.GlobOptions) | The caller supplies matching and depth options, or omits them for case-sensitive, unlimited traversal. | #### Returns | Type | Description | | --- | --- | | [`DirectoryStream`](/modules/io/files/#tecs.io.files.DirectoryStream) | Returns a caller-owned stream, or nil when enumeration cannot start. | | `string` | Returns the platform reason when the first return is nil. | #### Examples ```teal local files = tecs.io.files tecs.scoped( "scan levels", function(scope: tecs.Scope) local stream, reason = files.glob( files.assetPath("levels"), nil, {maxDepth = 2} ) if stream == nil then error(reason) end scope:own(stream) while true do local entry, nextReason = stream:next() if entry == nil then if nextReason ~= nil then error(nextReason) end break end print(entry.depth, entry.path) if entry.kind == "directory" and entry.name == "generated" then stream:skipDirectory() end end end ) ``` ### tecs.io.files.info Static Returns information about `path`, or nil when nothing occupies it. Nil is the answer for a path that does not exist, for a broken symbolic link, and when the platform cannot search a directory; the second return separates them. ```teal function tecs.io.files.info(path: PathInput): Info, string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `path` | [`PathInput`](/modules/io/files/#tecs.io.files.PathInput) | The caller supplies an absolute or platform-resolvable path. | #### Returns | Type | Description | | --- | --- | | [`Info`](/modules/io/files/#tecs.io.files.Info) | Returns the path information, or nil when unavailable. | | `string` | Returns the platform reason when the first return is nil. | ### tecs.io.files.isDirectory Static Returns whether `path` resolves to a directory. ```teal function tecs.io.files.isDirectory(path: PathInput): boolean ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `path` | [`PathInput`](/modules/io/files/#tecs.io.files.PathInput) | The caller supplies an absolute or platform-resolvable path. | #### Returns | Type | Description | | --- | --- | | `boolean` | Returns true when the resolved object is a directory. | ### tecs.io.files.isFile Static Returns whether `path` resolves to a regular file. ```teal function tecs.io.files.isFile(path: PathInput): boolean ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `path` | [`PathInput`](/modules/io/files/#tecs.io.files.PathInput) | The caller supplies an absolute or platform-resolvable path. | #### Returns | Type | Description | | --- | --- | | `boolean` | Returns true when the resolved object is a regular file. | ### tecs.io.files.isSymlink Static Returns whether the final object named by `path` is a symbolic link. Unlike `info`, this function does not follow the final link. It does follow links in parent directories, so the result is path introspection and not a security boundary. A path that is absent or cannot be inspected returns false. ```teal function tecs.io.files.isSymlink(path: PathInput): boolean ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `path` | [`PathInput`](/modules/io/files/#tecs.io.files.PathInput) | The caller supplies an absolute or platform-resolvable path. | #### Returns | Type | Description | | --- | --- | | `boolean` | Returns true when the final path itself is a symbolic link. | ### tecs.io.files.lines Static Iterates over the lines in a whole file. The function reads and closes the file before it returns the iterator, so breaking the loop retains only the immutable bytes and no file descriptor. It strips LF and CRLF terminators, returns empty lines between adjacent terminators, and does not invent another empty line after a final terminator. Use `open` when the input is too large to hold whole. ```teal function tecs.io.files.lines(path: PathInput): LineIterator, string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `path` | [`PathInput`](/modules/io/files/#tecs.io.files.PathInput) | The caller supplies the source file. | #### Returns | Type | Description | | --- | --- | | [`LineIterator`](/modules/io/files/#tecs.io.files.LineIterator) | Returns a [`LineIterator`](/modules/io/files/#tecs.io.files.LineIterator) that yields each line in order, or nil when the platform cannot read the file. | | `string` | Returns the failure reason when the first return is nil. | #### Examples ```teal local files = require("tecs.io.files") local nextLine = assert(files.lines("save.txt")) for line in nextLine do print(line) end ``` ### tecs.io.files.load Static Compiles a Lua file without running it. The function reads through the installed storage backend rather than `loadfile`, records the source for file watching, and uses `@path` as the compiler's chunk name. The default environment is the caller's global environment. A supplied environment is installed exactly as given and is not a security boundary unless the caller makes it one. ```teal function tecs.io.files.load( path: PathInput, environment: {string: any} ): function(...: any): any..., string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `path` | [`PathInput`](/modules/io/files/#tecs.io.files.PathInput) | The caller supplies the Lua source file. | | `environment` | `{string : any}` | The caller supplies the globals visible when the chunk runs, or omits them to use the process globals. | #### Returns | Type | Description | | --- | --- | | `function(...: any): any...` | Returns the compiled chunk, or nil when reading or compilation fails. Calling the returned function executes the file. | | `string` | Returns the read or compiler reason when the first return is nil. | ### tecs.io.files.open Static Opens a file through one seekable cursor. The mode follows Lua's standard file modes. `"r"` opens an existing file for reading and is the default. `"w"` creates or truncates for writing. `"a"` creates or preserves for writes that always land at the end. Adding `+` permits both reading and writing. The cursor starts at byte zero except for `"a"`; `"a+"` starts at byte zero for reading while every write still lands at the current end. Seeks may not move past the end. Close the file even after its last write succeeds. `close` flushes buffered bytes and can report a delayed storage failure. A backend without direct random access retains the complete file until close. ```teal function tecs.io.files.open( path: PathInput, mode: FileMode ): File, string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `path` | [`PathInput`](/modules/io/files/#tecs.io.files.PathInput) | The caller supplies the file as a string or immutable [`Path`](/modules/io/Path/). | | `mode` | [`FileMode`](/modules/io/files/#tecs.io.files.FileMode) | The caller selects `"r"`, `"w"`, `"a"`, `"r+"`, `"w+"`, or `"a+"`, or omits it for `"r"`. | #### Returns | Type | Description | | --- | --- | | [`File`](/modules/io/files/#tecs.io.files.File) | Returns a caller-owned [`File`](/modules/io/files/#tecs.io.files.File), or nil when the platform cannot open it. | | `string` | Returns the platform reason when the first return is nil. | #### Examples ```teal local files = require("tecs.io.files") local file, reason = files.open("world.pack", "w+") if file == nil then error(reason) end local wrote, writeReason = file:write("HEADpayload bytes") if not wrote then file:close() error(writeReason) end local position, seekReason = file:seek("start") if position == nil then file:close() error(seekReason) end local header = assert(file:read(4)) assert(header == "HEAD") local closed, closeReason = file:close() assert(closed, closeReason) ``` ### tecs.io.files.preferenceIdentity Static Returns the publisher and game names used for writable user data. The pair starts as `"tecs", "tecs"` and changes when `setPreferenceIdentity` succeeds. ```teal function tecs.io.files.preferenceIdentity(): string, string ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | `string` | Returns the current publisher, studio, or organization name. | | `string` | Returns the current game or application name. | ### tecs.io.files.preferencePath Static Returns the writable directory for this application and creates it if absent. ```teal function tecs.io.files.preferencePath(): string ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | `string` | Returns the directory with a trailing separator, named from the preference identity. This is where a build writes durable state; `cachePath` is the other writable root and holds only reconstructable data. | ### tecs.io.files.read Static Reads a whole file and returns its bytes, or nil when there is none there. The binary string retains embedded NUL bytes. On the SDL backend the transfer uses SDL AsyncIO, suspending a system or blocking a direct caller while the same private completion advances. ```teal function tecs.io.files.read( path: PathInput, kind: string ): string, string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `path` | [`PathInput`](/modules/io/files/#tecs.io.files.PathInput) | The caller supplies an absolute path or one from `assetPath`. | | `kind` | `string` | The caller supplies the content kind for watching, or omits it to record a document. | #### Returns | Type | Description | | --- | --- | | `string` | Returns the file bytes, or nil when the platform cannot read them. | | `string` | Returns the failure reason when the first return is nil. The reason names the path, and on the SDL backend it carries the operating system's own detail behind it. | ### tecs.io.files.readInto Static Reads a whole file directly into an owned buffer. The operation preserves bytes before `offset`, grows the destination as needed, and returns the number of bytes written. On the SDL backend the file transfer runs through SDL AsyncIO and never materializes a Lua string. Inside a system it may suspend the logical update; elsewhere the identical call blocks its caller until the transfer settles. ```teal function tecs.io.files.readInto( path: PathInput, destination: IOBuffer, offset: integer, kind: string ): integer, string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `path` | [`PathInput`](/modules/io/files/#tecs.io.files.PathInput) | The caller supplies an absolute path or one from `assetPath`. | | `destination` | [`IOBuffer`](/modules/io/#tecs.io.Buffer) | The caller supplies an open destination buffer and keeps ownership of it. | | `offset` | `integer` | The caller supplies a zero-based destination offset or omits it for zero. | | `kind` | `string` | The caller supplies the content kind for watching, or omits it to record a document. | #### Returns | Type | Description | | --- | --- | | `integer` | Returns the number of bytes copied into the destination. | | `string` | Returns the platform reason when the first return is nil. | ### tecs.io.files.readLink Static Returns the target spelling stored in a symbolic link. A relative target remains relative. The function does not resolve it against the link's parent and fails when `path` is not a symbolic link. ```teal function tecs.io.files.readLink(path: PathInput): Path, string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `path` | [`PathInput`](/modules/io/files/#tecs.io.files.PathInput) | The caller supplies the symbolic link to inspect. | #### Returns | Type | Description | | --- | --- | | [`Path`](/modules/io/Path/) | Returns the stored target as an immutable [`Path`](/modules/io/Path/). | | `string` | Returns the platform or unsupported reason when the first return is nil. | ### tecs.io.files.remove Static Removes a file, or an empty directory. It does not recurse and fails on a directory with anything in it. It **succeeds when there is nothing at `path`**: the result says the path is gone, not that this call is what removed it. Emptying a tree requires a glob and a deepest-first loop: local stream = assert(files.glob(root)) local paths = {} while true do local entry, reason = stream:next() if entry == nil then if reason ~= nil then error(reason) end break end paths[#paths + 1] = entry.path end stream:close() table.sort(paths, function(a, b) return #(a:toString()) > #(b:toString()) end) for _, path in ipairs(paths) do files.remove(path) end files.remove(root) ```teal function tecs.io.files.remove(path: PathInput): boolean, string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `path` | [`PathInput`](/modules/io/files/#tecs.io.files.PathInput) | The caller supplies the file or empty directory to remove. | #### Returns | Type | Description | | --- | --- | | `boolean` | Returns true when nothing remains at `path`. | | `string` | Returns the platform reason when the first return is false. | ### tecs.io.files.rename Static Moves `from` to `to`, replacing whatever was at `to`. Directories move as well as files, and an existing destination is overwritten with no warning and nothing to undo it. Whether this works across filesystems is the platform's business. ```teal function tecs.io.files.rename( from: PathInput, to: PathInput ): boolean, string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `from` | [`PathInput`](/modules/io/files/#tecs.io.files.PathInput) | The caller supplies the existing source path. | | `to` | [`PathInput`](/modules/io/files/#tecs.io.files.PathInput) | The caller supplies the destination path to replace. | #### Returns | Type | Description | | --- | --- | | `boolean` | Returns whether the platform moved the path. | | `string` | Returns the platform reason when the first return is false. | ### tecs.io.files.setAssetRoot Static Overrides the asset root, for a test or a tool. ```teal function tecs.io.files.setAssetRoot(root: PathInput) ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `root` | [`PathInput`](/modules/io/files/#tecs.io.files.PathInput) | The caller supplies the new content directory. The function adds a trailing separator only when needed. | #### Returns None. ### tecs.io.files.setPreferenceIdentity Static Sets the publisher and game names used for writable user data. SDL combines these values with the current user and platform conventions to choose the directory returned by `preferencePath`. The defaults are `"tecs"` and `"tecs"`. Later calls change which directory the next `preferencePath`, `writablePath`, or `cachePath` resolves. ```teal function tecs.io.files.setPreferenceIdentity( organization: string, application: string ) ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `organization` | `string` | The caller supplies a non-empty publisher, studio, or organization name. | | `application` | `string` | The caller supplies a non-empty game or application name. | #### Returns None. ### tecs.io.files.setReadOnly Static Changes the portable read-only state of a path. On Unix, making a path read-only clears every write bit and making it writable restores only the owner's write bit. The API deliberately does not model platform ACLs, ownership, or executable bits. ```teal function tecs.io.files.setReadOnly( path: PathInput, readOnly: boolean ): boolean, string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `path` | [`PathInput`](/modules/io/files/#tecs.io.files.PathInput) | The caller supplies the existing path to change. | | `readOnly` | `boolean` | The caller supplies true to prevent ordinary writes. | #### Returns | Type | Description | | --- | --- | | `boolean` | Returns whether the platform changed the state. | | `string` | Returns the platform or unsupported reason when the first return is false. | ### tecs.io.files.userFolder Static Returns one of the platform's well-known folders. **Often nil, and legitimately so.** A platform that has no such concept says so rather than inventing a path: macOS has no saved-games, screenshots or templates folder and answers nil for all three, and a platform that is not a desktop may have none of them. Treat every one of these as absent until it is not, and never as a place a build may write; `preferencePath` and `cachePath` are the two writable roots, with different durability. ```teal function tecs.io.files.userFolder(which: UserFolder): string, string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `which` | [`UserFolder`](/modules/io/files/#tecs.io.files.UserFolder) | The caller supplies the well-known folder to resolve. | #### Returns | Type | Description | | --- | --- | | `string` | Returns the folder with a trailing separator, or nil when the platform has no matching folder. | | `string` | Returns the platform reason when the first return is nil. | ### tecs.io.files.writablePath Static Resolves `relative` against the writable root. ```teal function tecs.io.files.writablePath(relative: PathInput): string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `relative` | [`PathInput`](/modules/io/files/#tecs.io.files.PathInput) | The caller supplies a path under the preference directory. | #### Returns | Type | Description | | --- | --- | | `string` | Returns the absolute path. | ### tecs.io.files.write Static Writes bytes to a file, replacing its complete contents. The default SDL backend transfers through its bounded asynchronous file queue. The call suspends a logical update or blocks an ordinary caller until the complete write settles. It retains an immutable source range for SDL instead of copying a buffer through a Lua string. Embedded NUL bytes remain data and an empty input creates an empty file. ```teal function tecs.io.files.write( path: PathInput, bytes: storagebackend.ByteInput ): boolean, string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `path` | [`PathInput`](/modules/io/files/#tecs.io.files.PathInput) | The caller supplies the destination file. | | `bytes` | `storagebackend.ByteInput` | The caller supplies an immutable string or byte view and retains ownership. | #### Returns | Type | Description | | --- | --- | | `boolean` | Returns whether the platform wrote the complete file. | | `string` | Returns the platform reason when the first return is false. | ### tecs.io.files.writeAtomic Static Durably and atomically writes a complete file. Inside a system on the SDL storage backend, the copy and commit run on a worker and suspend the logical world update. Outside a world update, the same call completes synchronously. Buffers and views are copied before a suspended write begins. ```teal function tecs.io.files.writeAtomic( path: PathInput, bytes: ByteInput ): boolean, string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `path` | [`PathInput`](/modules/io/files/#tecs.io.files.PathInput) | The caller supplies the destination file. | | `bytes` | `ByteInput` | The caller supplies the complete binary contents. | #### Returns | Type | Description | | --- | --- | | `boolean` | Returns whether the platform confirmed the atomic durable write. | | `string` | Returns the platform or unsupported reason when the first return is false. | #### Examples ```teal local save = tecs.io.Path.new( tecs.io.files.writablePath("saves"), "slot1.bin" ) local ok, reason = tecs.io.files.writeAtomic(save, "snapshot bytes") if not ok then error(reason) end ``` --- ## tecs.io.http # tecs.io.http Cooperative HTTP and HTTPS. ```teal local client = tecs.io.http.newClient({userAgent = "mygame/1.0"}) local response = client:send({ url = tecs.io.URI.new("https://example.com/manifest.json"), }) print(response.status) print(assert(response.body:readAll(1024 * 1024))) ``` `send` returns when status and headers exist. Inside a system it suspends only until that boundary; outside an update it blocks its caller. The body is a one-shot streaming Reader backed by bounded native chunks, so a slow consumer applies transport backpressure instead of accumulating the full response. Arbitrary streaming request bodies run in client-owned cooperative work, so their Reader may itself wait on a socket, process, transform, or HTTP response without blocking the SDL pump. On the SDL storage backend, a file stream is opened and read directly by Tokio instead. It stays bounded without retaining the complete file or crossing Lua for every chunk. An HTTP error status still returns a response. DNS, connection, TLS, timeout, or streaming failures raise at the suspended call. Raw `tecs.io` sockets use the process-wide `mio` readiness reactor. HTTP deliberately uses Reqwest and Tokio for its connection pool, TLS, redirects, and protocol work. Both implementations resume the same direct Lua call, and neither parks one worker thread per waiting socket. A request holds one of its client's `maxConnections` slots until its response body ends, so close or discard a body you do not intend to read. Call `Client:close` when you no longer need its connection pool. Losing the last Lua reference does not cancel its requests. Application shutdown closes any remaining clients and drains their internal upload tasks to settlement, within a bounded number of scheduler steps that it logs whenever it has to abandon work. An [`Application`](/modules/Application/) owns progress; a direct headless call drives the client while it blocks. ## Module contents ### Constructors | Constructor | Description | | --- | --- | | [`newClient`](/modules/io/http/#tecs.io.http.newClient) | Builds a client. | ### Types | Type | Kind | Description | | --- | --- | --- | | [`Client`](/modules/io/http/#tecs.io.http.Client) | interface | A Client owns one connection pool and its active requests. | | [`ClientOptions`](/modules/io/http/#tecs.io.http.ClientOptions) | record | Settings copied when an HTTP client is built. | | [`plugin`](/modules/io/http/#tecs.io.http.plugin) | record | Read-only. plugin exposes requests and responses as ECS components. | | [`Request`](/modules/io/http/#tecs.io.http.Request) | record | Request describes one request and requires only url. | | [`Response`](/modules/io/http/#tecs.io.http.Response) | record | Response describes status, headers, and a progressive response body. | ### Functions | Function | Kind | Description | | --- | --- | --- | | [`getOpenClientCount`](/modules/io/http/#tecs.io.http.getOpenClientCount) | Static | Returns the open-client count. | ## Constructors ### tecs.io.http.newClient Static Builds a client. ```teal function tecs.io.http.newClient(options: ClientOptions): Client ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `options` | [`ClientOptions`](/modules/io/http/#tecs.io.http.ClientOptions) | The caller supplies client defaults or omits this record to use every default. Each `insecureHosts` entry logs a warning. | #### Returns | Type | Description | | --- | --- | | [`Client`](/modules/io/http/#tecs.io.http.Client) | Returns an open client that remains active until `close`. | #### Examples ```teal local http = tecs.io.http local endpoint, reason = tecs.io.URI.new("https://example.com/status") if endpoint == nil then error(reason) end local client = http.newClient({ userAgent = "mygame/1.0", timeoutMs = 10000, maxBytes = 1024 * 1024, }) local response = client:send({url = endpoint}) print(response.status, response.url:host()) response.body:close() client:close() ``` ## Types ### tecs.io.http.Client interface A `Client` owns one connection pool and its active requests. Closing the client cancels every pending request, releases the pool, and remains safe to repeat. The Application drives active requests. ```teal interface tecs.io.http.Client is Closeable pending: function(self): integer send: function(self, request: Request): Response end ``` #### Interfaces | Interface | | --- | | [`Closeable`](/modules/#tecs.Closeable) | #### tecs.io.http.Client:pending Instance Returns the number of unsettled requests. ```teal function tecs.io.http.Client.pending(self): integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Client` | The client to inspect. | ##### Returns | Type | Description | | --- | --- | | `integer` | Returns zero after every request settles or is canceled. | #### tecs.io.http.Client:send Instance Sends one HTTP request and returns its response. A transport failure raises. An HTTP error status returns normally with that status in the response. Inside a system, the call suspends the world update only while the transfer is pending. ```teal function tecs.io.http.Client.send(self, request: Request): Response ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Client` | The open client that owns the request. | | `request` | `Request` | The caller supplies the URL and optional method, headers, upload body, timeouts, and byte limit. | ##### Returns | Type | Description | | --- | --- | | `Response` | Returns status, headers, and a one-shot streaming body. | ### tecs.io.http.ClientOptions record Settings copied when an HTTP client is built. `userAgent` and `headers` set request defaults. `timeoutMs` defaults to 30000, `connectTimeoutMs` to 10000, and `stallTimeoutMs` to zero, which disables the no-progress timeout. `maxRedirects` defaults to five, `maxConnections` to 16, and `maxConnectionsPerHost` to six; a request holds one slot of each until its response body ends. `maxBytes` defaults to zero for no response limit. `compressed` defaults to true. `insecureHosts` disables certificate verification for named hosts and logs a warning for each. `proxy` accepts the textual spelling of an absolute [`URI`](/modules/io/URI/), follows environment variables when nil, and forces a direct connection when empty. `noProxy` uses Reqwest's exclusion syntax, and `proxyCredentials` uses `user:password`. ```teal record tecs.io.http.ClientOptions userAgent: string headers: {string: string} timeoutMs: number connectTimeoutMs: number stallTimeoutMs: number maxRedirects: integer maxConnections: integer maxConnectionsPerHost: integer maxBytes: integer compressed: boolean insecureHosts: {string} proxy: string noProxy: string proxyCredentials: string end ``` #### tecs.io.http.ClientOptions.userAgent field Caller-writable. Sets `User-Agent` on every request. ```teal tecs.io.http.ClientOptions.userAgent: string ``` #### tecs.io.http.ClientOptions.headers field Caller-writable. Sets headers sent on every request. Per-request headers override these without regard to case. ```teal tecs.io.http.ClientOptions.headers: {string: string} ``` #### tecs.io.http.ClientOptions.timeoutMs field Caller-writable. Sets positive milliseconds allowed for a whole transfer. Defaults to 30000. ```teal tecs.io.http.ClientOptions.timeoutMs: number ``` #### tecs.io.http.ClientOptions.connectTimeoutMs field Caller-writable. Sets positive milliseconds allowed to establish a connection. Defaults to 10000. ```teal tecs.io.http.ClientOptions.connectTimeoutMs: number ``` #### tecs.io.http.ClientOptions.stallTimeoutMs field Caller-writable. Sets milliseconds a response may make no progress. Zero disables this timeout. ```teal tecs.io.http.ClientOptions.stallTimeoutMs: number ``` #### tecs.io.http.ClientOptions.maxRedirects field Caller-writable. Sets redirects followed. Defaults to five; zero follows none. ```teal tecs.io.http.ClientOptions.maxRedirects: integer ``` #### tecs.io.http.ClientOptions.maxConnections field Caller-writable. Sets requests allowed to use sockets at once. Defaults to 16. A request holds its slot from the moment it acquires one until its response body ends, because the socket stays open for that whole time and this limit is what bounds open sockets. A caller that reads a body slowly therefore keeps its slot, and a caller that never reads one keeps it until `timeoutMs` expires. Close or discard a body you do not intend to read, and raise this limit rather than expecting an unread body to release its slot early. ```teal tecs.io.http.ClientOptions.maxConnections: integer ``` #### tecs.io.http.ClientOptions.maxConnectionsPerHost field Caller-writable. Sets requests allowed to use sockets to one host. Defaults to six. A request holds its host slot for as long as it holds a `maxConnections` slot. ```teal tecs.io.http.ClientOptions.maxConnectionsPerHost: integer ``` #### tecs.io.http.ClientOptions.maxBytes field Caller-writable. Sets maximum response-body bytes. Zero is unbounded. ```teal tecs.io.http.ClientOptions.maxBytes: integer ``` #### tecs.io.http.ClientOptions.compressed field Caller-writable. Accepts gzip and deflate response compression when true. Defaults to true. ```teal tecs.io.http.ClientOptions.compressed: boolean ``` #### tecs.io.http.ClientOptions.insecureHosts field Caller-writable. Lists host names whose TLS certificates are not verified. ```teal tecs.io.http.ClientOptions.insecureHosts: {string} ``` #### tecs.io.http.ClientOptions.proxy field Caller-writable. Sets the textual spelling of an absolute proxy [`URI`](/modules/io/URI/). Nil follows the environment; an empty string forces a direct connection. This option accepts a string, not a `URI` object. ```teal tecs.io.http.ClientOptions.proxy: string ``` #### tecs.io.http.ClientOptions.noProxy field Caller-writable. Lists hosts excluded from an explicitly configured proxy. ```teal tecs.io.http.ClientOptions.noProxy: string ``` #### tecs.io.http.ClientOptions.proxyCredentials field Caller-writable. Sets proxy credentials in `user:password` form. ```teal tecs.io.http.ClientOptions.proxyCredentials: string ``` ### tecs.io.http.plugin record Read-only. `plugin` exposes requests and responses as ECS components. ```teal record tecs.io.http.plugin record Request is Component url: URI method: string headers: {string: string} body: string | ReadableStream timeoutMs: number stallTimeoutMs: number maxBytes: integer end record Response is Component status: integer headers: {string: string} body: Stream url: URI error: string end record Pending is Component end clientOf: function(World): Client close: function(World) install: function(World, Options) end ``` #### tecs.io.http.plugin.Request record What a game spawns to make a request. The fields of a `Request`, because there is no second vocabulary for the same thing. ```teal record tecs.io.http.plugin.Request is Component url: URI method: string headers: {string: string} body: string | ReadableStream timeoutMs: number stallTimeoutMs: number maxBytes: integer end ``` ##### Interfaces | Interface | | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | ##### tecs.io.http.plugin.Request.url field Caller-writable. Sets an absolute HTTP or HTTPS [`URI`](/modules/io/URI/). ```teal tecs.io.http.plugin.Request.url: URI ``` ##### tecs.io.http.plugin.Request.method field Caller-writable. Sets the method. Defaults to `"GET"`. ```teal tecs.io.http.plugin.Request.method: string ``` ##### tecs.io.http.plugin.Request.headers field Caller-writable. Sets headers merged over the client's defaults without regard to case. A `Content-Length` must contain decimal digits and must match a body whose length is known. ```teal tecs.io.http.plugin.Request.headers: {string: string} ``` ##### tecs.io.http.plugin.Request.body field Caller-writable. Sets what to send. A snapshot rejects a `tecs.io.newHandleStream` here because its live handle cannot be reconstructed. ```teal tecs.io.http.plugin.Request.body: string | ReadableStream ``` ##### tecs.io.http.plugin.Request.timeoutMs field Caller-writable. Sets milliseconds for this transfer, overriding the client's value. ```teal tecs.io.http.plugin.Request.timeoutMs: number ``` ##### tecs.io.http.plugin.Request.stallTimeoutMs field Caller-writable. Sets tolerated milliseconds without progress, overriding the client's value. ```teal tecs.io.http.plugin.Request.stallTimeoutMs: number ``` ##### tecs.io.http.plugin.Request.maxBytes field Caller-writable. Sets the body byte limit, overriding the client's value. ```teal tecs.io.http.plugin.Request.maxBytes: integer ``` #### tecs.io.http.plugin.Response record What replaces a `Request` once the transfer settles. Present whatever happened, because "the request finished" is the event a system waits for and a failure is one of the ways it can finish. `error` is what says which. ```teal record tecs.io.http.plugin.Response is Component status: integer headers: {string: string} body: Stream url: URI error: string end ``` ##### Interfaces | Interface | | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | ##### tecs.io.http.plugin.Response.status field Engine-owned. Reports the HTTP status code, or zero when the transfer never received one. ```teal tecs.io.http.plugin.Response.status: integer ``` ##### tecs.io.http.plugin.Response.headers field Engine-owned. Reports response headers with lower-cased names, in the joined form `Response.headers` uses. A component is one value per name, so a repeated `set-cookie` keeps only its first value here; a system that needs every cookie sends its request through a client and reads `Response:getAll`. ```teal tecs.io.http.plugin.Response.headers: {string: string} ``` ##### tecs.io.http.plugin.Response.body field Engine-owned. Provides a one-shot progressive response body. Despawning this entity closes a body that remains unread. ```teal tecs.io.http.plugin.Response.body: Stream ``` ##### tecs.io.http.plugin.Response.url field Engine-owned. Reports the URI that actually answered, or nil when the request failed before it supplied a valid URI. ```teal tecs.io.http.plugin.Response.url: URI ``` ##### tecs.io.http.plugin.Response.error field Engine-owned. Reports why the transfer did not complete, or nil when it did. A 404 is not one of these: it is a `status` of 404 and no error. ```teal tecs.io.http.plugin.Response.error: string ``` #### tecs.io.http.plugin.Pending record On an entity whose request is in flight. Internal, and the reason a request is sent once rather than every frame. A marker, while the callback and transfer state remain in the plugin. It is transient because runtime transport state cannot be saved. A save taken mid-request keeps the `Request`, drops this marker, and sends the request again after loading. ```teal record tecs.io.http.plugin.Pending is Component end ``` ##### Interfaces | Interface | | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | #### tecs.io.http.plugin.clientOf Static The world's client, or nil when the plugin is not installed. ```teal function tecs.io.http.plugin.clientOf(World): Client ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `#1` | [`World`](/modules/ecs/#tecs.World) | | ##### Returns | Type | Description | | --- | --- | | `Client` | | #### tecs.io.http.plugin.close Static Closes the world's client and forgets it. ```teal function tecs.io.http.plugin.close(World) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `#1` | [`World`](/modules/ecs/#tecs.World) | | ##### Returns None. #### tecs.io.http.plugin.install Static Installs the plugin: `world:addPlugin(tecs.io.http.plugin.install)`. Takes options because the plugin builds the world's client, and a game that wants a `userAgent` on it has nowhere else to say so: `world:addPlugin(function(w) http.plugin.install(w, {...}) end)`. ```teal function tecs.io.http.plugin.install(World, Options) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `#1` | [`World`](/modules/ecs/#tecs.World) | | | `#2` | `Options` | | ##### Returns None. ### tecs.io.http.Request record `Request` describes one request and requires only `url`. ```teal record tecs.io.http.Request url: URI method: string headers: {string: string} body: string | iotypes.ReadableStream timeoutMs: number stallTimeoutMs: number maxBytes: integer end ``` #### tecs.io.http.Request.url field Caller-writable. Sets an absolute URL with an `http` or `https` scheme. ```teal tecs.io.http.Request.url: URI ``` #### tecs.io.http.Request.method field Caller-writable. Sets the method. Defaults to GET. ```teal tecs.io.http.Request.method: string ``` #### tecs.io.http.Request.headers field Caller-writable. Sets headers merged over the client's defaults. A `Content-Length` must contain decimal digits and must match a body whose length is known. ```teal tecs.io.http.Request.headers: {string: string} ``` #### tecs.io.http.Request.body field Caller-writable. Sets bytes to send. A general stream opens one reader and feeds a bounded upload queue. On the SDL storage backend, a file stream is opened and read directly by the native HTTP lane. The client owns cooperative work that waits on a composed reader. ```teal tecs.io.http.Request.body: string | iotypes.ReadableStream ``` #### tecs.io.http.Request.timeoutMs field Caller-writable. Overrides the client's positive whole-transfer timeout. ```teal tecs.io.http.Request.timeoutMs: number ``` #### tecs.io.http.Request.stallTimeoutMs field Caller-writable. Overrides the client's no-progress timeout. ```teal tecs.io.http.Request.stallTimeoutMs: number ``` #### tecs.io.http.Request.maxBytes field Caller-writable. Overrides the client's body limit. Zero is unbounded. ```teal tecs.io.http.Request.maxBytes: integer ``` ### tecs.io.http.Response record `Response` describes status, headers, and a progressive response body. `headers` joins a repeated header's values, and `Response:getAll` reports them one by one, which is what `set-cookie` needs. ```teal record tecs.io.http.Response status: integer headers: {string: string} body: iotypes.Stream url: URI getAll: function(self, name: string): {string} ok: function(self): boolean end ``` #### tecs.io.http.Response.status field Read-only. Reports the status after redirects. ```teal tecs.io.http.Response.status: integer ``` #### tecs.io.http.Response.headers field Read-only. Reports final response headers with lower-cased names. A name the server sent more than once holds its values joined with `", "`, which RFC 9110 permits for every field except `Set-Cookie`. A repeated `set-cookie` holds only the first value here, because joining cookies changes what they mean. `Response:getAll` reports every value of any name. ```teal tecs.io.http.Response.headers: {string: string} ``` #### tecs.io.http.Response.body field Read-only. Provides a one-shot progressive response body. Reading may suspend a system or block a direct caller until bytes arrive. The caller must consume, discard, or close this owned stream. ```teal tecs.io.http.Response.body: iotypes.Stream ``` #### tecs.io.http.Response.url field Read-only. Reports the effective URL after redirects. ```teal tecs.io.http.Response.url: URI ``` #### tecs.io.http.Response:getAll Instance Returns every value the server sent for one header name. This is the accessor for a repeated header, and the only correct way to read `set-cookie`, which cannot be joined. ```teal function tecs.io.http.Response.getAll(self, name: string): {string} ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Response` | The response to read. | | `name` | `string` | The caller supplies a header name, and the lookup ignores case. | ##### Returns | Type | Description | | --- | --- | | `{string}` | Returns a new array in the order the server sent the values, or an empty array when the response has no such header. The array belongs to the caller. | #### tecs.io.http.Response:ok Instance Whether the status is in the 2xx range. ```teal function tecs.io.http.Response.ok(self): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Response` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | | ## Functions ### tecs.io.http.getOpenClientCount Static Returns the open-client count. ```teal function tecs.io.http.getOpenClientCount(): integer ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | `integer` | Returns the process-wide count of clients not yet closed. | --- ## tecs.io # tecs.io Input, output, and cooperative network transport. A [`Stream`](/modules/io/#tecs.io.Stream) describes binary storage without keeping a cursor. A [`Reader`](/modules/io/#tecs.io.Reader) and [`Writer`](/modules/io/#tecs.io.Writer) are the caller-owned directional endpoints opened from it. Strings, buffers, borrowed bytes, paths, and handles all use that one descriptor contract. A Lua `FILE` is also a basic reader and writer. `transfer` accepts files directly and uses `readInto` or `writeFrom` accelerators when an endpoint provides them. Transform constructors take ownership of either a Tecs endpoint or a file and close it with the wrapper. Readers, writers, transforms, and whole-source operations return their result directly. A memory operation completes inline. An unresolved file, socket, or process operation suspends a normal system and resumes at the call; the same call blocks its caller outside a world update. They return nil and a reason when an endpoint cannot be opened, read, written, flushed, or closed: ```teal tecs.scoped( "read save", function(scope: tecs.Scope) local save = scope:own( tecs.io.newFileStream("save.bin") ) local bytes = assert(save:transferToBuffer()) scope:own(bytes) print(bytes:length()) end ) ``` TCP is an ordered byte stream: one write may take several reads to receive, several writes may arrive in one read, and message framing belongs to the caller. UDP preserves one send as one datagram, but the network may lose, duplicate, or reorder it. General immutable URIs live under `tecs.io.URI`; HTTP and HTTPS live under `tecs.io.http`; the MCP debug server lives under `tecs.io.mcp`. Hot TCP loops can reuse a `Buffer`. `TCPSocket:readInto` lets the native socket fill its FFI allocation directly, and `TCPSocket:writeFrom` queues a buffer range without constructing an intermediate Lua string. `UDPSocket:receiveInto` is the datagram form: it fills a reused buffer, reports its sender through `sourceHost` and `sourcePort`, and allocates no packet, address, or payload string. `UDPSocket:source` builds an owned address from the recorded sender when a reply needs one. `Buffer:view` retains an immutable zero-copy snapshot. `writeView` on sockets, writers, and writable streams consumes that retained range directly, while `ByteView:newStream` gives it replayable stream behavior without copying payload bytes. Transform readers and writers compose those endpoints without materializing a whole source. `newInflateReader`, `newBase64DecodeReader`, `newHexDecodeReader`, and `newTranscodeReader` pull transformed bytes as their consumer asks. The corresponding deflate, encode, and transcode writers push transformed chunks toward a destination and finish format trailers or partial character state on `close`. Each wrapper owns the endpoint it wraps. Memory endpoints lend source pointers and reserve destination ranges directly; other endpoints reuse pooled chunks, so a warmed transfer allocates no intermediate payload storage. Ownership is explicit at construction boundaries. A stream over a buffer or Lua file handle borrows that resource and never closes it. A stream over a byte view retains its own immutable view. Transform readers and writers take ownership of the endpoint they wrap and close it with themselves. Opening a buffer or byte-view reader retains an immutable snapshot that outlives later closure; opening a buffer writer borrows the mutable buffer, clears its logical length, and requires it to remain open through the writer's close. Network operations return their values directly. Inside a system, resolution, connection, acceptance, socket reads, and datagram receives transparently suspend only while the requested operation cannot make progress: ```teal world:addSystem({ name = "game.ReceiveCommand", phase = tecs.ecs.phases.Update, run = function() tecs.scoped( "serve client", function(scope) local client = scope:own( assert(listener:accept()) ) local command = assert(client:read()) applyCommand(command) end ) end, }) ``` The Application owns progress; there is no public pump to drive. Resolution, connection, reads, acceptance, sends, and receives all block appropriately for their context. A call never changes into a `WouldBlock` result merely because it ran outside a system. The explicit `wait` methods remain timeout and readiness checks rather than a second transfer API. One endpoint permits one suspended readiness operation at a time. Do not park both `read` and `drain` on one TCP socket, both `accept` and `wait` on one listener, or both `receive` and `wait` on one UDP socket. Use one owner to serialize operations on an endpoint; a second overlapping wait fails at that call instead of replacing the first waiter. The ready path performs the native operation immediately. Only a native `WouldBlock` arms a one-shot watch in the process-wide `mio` reactor. The reactor never calls Lua; the Application drains its tokens on the main thread and the scheduler resumes the same system. Socket writes finish when their bounded local queue accepts the bytes; `drain` is the explicit transport delivery boundary. Every address, TCP socket, listener, UDP socket, and UDP packet owns a resource. Close it explicitly. A GC finalizer releases an abandoned network handle as a last resort, but cannot report a close error. Closing is idempotent; using a closed value returns a failure. Naming `tecs.io` loads none of its children. Reading `files`, `http`, `mcp`, `Path`, `Process`, `URI`, or `watcher` loads only that child. Declares the directional binary interfaces published by `tecs.io`. This module is the cycle-safe home of `Reader`, `Writer`, and the stream descriptor interfaces. It is not a second public namespace: `tecs.io` re-exports the contracts, while resource modules require them without requiring their public parent. ## Module contents ### Submodules | Submodule | Description | | --- | --- | | [`tecs.io.Path`](/modules/io/Path/) | Immutable UTF-8 filesystem paths with platform-native semantics. | | [`tecs.io.Process`](/modules/io/Process/) | Streaming child processes with cooperative waits and backpressured standard I/O. | | [`tecs.io.URI`](/modules/io/URI/) | Immutable absolute URIs with component-aware modification. | | [`tecs.io.files`](/modules/io/files/) | Asset, persistent, and cache paths with direct and worker-backed file operations | | [`tecs.io.http`](/modules/io/http/) | Cooperative HTTP requests, streaming bodies, connection pools, and ECS request entities | | [`tecs.io.mcp`](/modules/io/mcp/) | MCP server setup, world inspection, safe mutation, custom tools, and transport | | [`tecs.io.watcher`](/modules/io/watcher/) | Development-time polling and reload dispatch for loaded content | ### Constructors | Constructor | Description | | --- | --- | | [`newBase64DecodeReader`](/modules/io/#tecs.io.newBase64DecodeReader) | Creates a reader that incrementally decodes RFC 4648 Base64 text. | | [`newBase64EncodeWriter`](/modules/io/#tecs.io.newBase64EncodeWriter) | Creates a writer that incrementally encodes bytes as RFC 4648 Base64. | | [`newBuffer`](/modules/io/#tecs.io.newBuffer) | Creates an owned growable byte buffer. | | [`newByteReader`](/modules/io/#tecs.io.newByteReader) | Creates a reader over borrowed FFI memory. | | [`newByteStream`](/modules/io/#tecs.io.newByteStream) | Creates a replayable read-only stream over borrowed FFI bytes. | | [`newDeflateWriter`](/modules/io/#tecs.io.newDeflateWriter) | Creates a writer that incrementally deflates bytes into a destination. | | [`newEmptyStream`](/modules/io/#tecs.io.newEmptyStream) | Creates a replayable readable empty stream. | | [`newFileStream`](/modules/io/#tecs.io.newFileStream) | Creates a replayable file stream. | | [`newHandleStream`](/modules/io/#tecs.io.newHandleStream) | Creates a one-shot stream over a borrowed Lua file handle. | | [`newHexDecodeReader`](/modules/io/#tecs.io.newHexDecodeReader) | Creates a reader that incrementally decodes hexadecimal text. | | [`newHexEncodeWriter`](/modules/io/#tecs.io.newHexEncodeWriter) | Creates a writer that incrementally encodes bytes as lowercase hexadecimal text. | | [`newInflateReader`](/modules/io/#tecs.io.newInflateReader) | Creates a reader that incrementally inflates a compressed source. | | [`newStringReader`](/modules/io/#tecs.io.newStringReader) | Creates a reader over an immutable Lua string. | | [`newStringStream`](/modules/io/#tecs.io.newStringStream) | Creates a replayable read-only string stream. | | [`newTranscodeReader`](/modules/io/#tecs.io.newTranscodeReader) | Creates a reader that incrementally converts character encodings. | | [`newTranscodeWriter`](/modules/io/#tecs.io.newTranscodeWriter) | Creates a writer that incrementally converts character encodings. | ### Types | Type | Kind | Description | | --- | --- | --- | | [`Address`](/modules/io/#tecs.io.Address) | record | An Address owns one resolved network address. | | [`Buffer`](/modules/io/#tecs.io.Buffer) | interface | A Buffer is owned growable FFI-backed byte storage. | | [`ByteView`](/modules/io/#tecs.io.ByteView) | interface | A ByteView retains an immutable zero-copy range from a buffer. | | [`DeflateWriterOptions`](/modules/io/#tecs.io.DeflateWriterOptions) | record | Options control an incremental deflate writer. | | [`InflateReaderOptions`](/modules/io/#tecs.io.InflateReaderOptions) | record | Options control an incremental inflate reader. | | [`ReadableStream`](/modules/io/#tecs.io.ReadableStream) | interface | A ReadableStream opens readers and supplies whole-source transfers. | | [`Reader`](/modules/io/#tecs.io.Reader) | interface | A Reader supplies bytes in order and releases its owned state on close. | | [`ReadWriteStream`](/modules/io/#tecs.io.ReadWriteStream) | interface | A ReadWriteStream supports both directional interfaces. | | [`Seekable`](/modules/io/#tecs.io.Seekable) | interface | Seekable supplies random-access cursor operations shared by readers and writers. | | [`SeekableReader`](/modules/io/#tecs.io.SeekableReader) | interface | A SeekableReader supplies bytes through a repositionable cursor. | | [`SeekableWriter`](/modules/io/#tecs.io.SeekableWriter) | interface | A SeekableWriter patches a destination through a repositionable cursor. | | [`Stream`](/modules/io/#tecs.io.Stream) | interface | A Stream describes binary storage without retaining a cursor. | | [`TCPListener`](/modules/io/#tecs.io.TCPListener) | record | A TCPListener listens for TCP clients. | | [`TCPSocket`](/modules/io/#tecs.io.TCPSocket) | record | A TCPSocket owns one connected TCP byte stream. | | [`UDPPacket`](/modules/io/#tecs.io.UDPPacket) | record | A UDPPacket owns one received UDP datagram and its source address. | | [`UDPSocket`](/modules/io/#tecs.io.UDPSocket) | record | A UDPSocket sends and receives UDP packets. | | [`WritableStream`](/modules/io/#tecs.io.WritableStream) | interface | A WritableStream opens writers and supplies whole-destination transfers. | | [`Writer`](/modules/io/#tecs.io.Writer) | interface | A Writer accepts bytes in order and finishes its destination on close. | ### Functions | Function | Kind | Description | | --- | --- | --- | | [`bind`](/modules/io/#tecs.io.bind) | Static | Binds a UDP socket. | | [`connect`](/modules/io/#tecs.io.connect) | Static | Connects to a resolved address. | | [`init`](/modules/io/#tecs.io.init) | Static | Starts networking for this module. | | [`listen`](/modules/io/#tecs.io.listen) | Static | Binds a TCP listener. | | [`pending`](/modules/io/#tecs.io.pending) | Static | Returns the number of pending network operations and readiness watches. | | [`resolve`](/modules/io/#tecs.io.resolve) | Static | Resolves a hostname. | | [`shutdown`](/modules/io/#tecs.io.shutdown) | Static | Stops this module's networking instance. | | [`transfer`](/modules/io/#tecs.io.transfer) | Static | Transfers every remaining byte between directional endpoints. | ## Constructors ### tecs.io.newBase64DecodeReader Static Creates a reader that incrementally decodes RFC 4648 Base64 text. The reader owns `source` and retains at most one undecoded quantum between source reads. ```teal function tecs.io.newBase64DecodeReader( source: types.Reader | FILE ): types.Reader ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `source` | [`types.Reader`](/modules/io/#tecs.io.Reader) | FILE | The caller supplies the reader of Base64 text. | #### Returns | Type | Description | | --- | --- | | [`types.Reader`](/modules/io/#tecs.io.Reader) | Returns a caller-owned reader of decoded bytes. | #### Examples ```teal tecs.scoped( "decode base64", function(scope: tecs.Scope) local reader = scope:own( tecs.io.newBase64DecodeReader( tecs.io.newStringReader("c25hcHNob3QgYnl0ZXM=") ) ) local decoded , reason = reader:read(1024) assert(decoded, reason) assert(decoded == "snapshot bytes") end ) ``` ### tecs.io.newBase64EncodeWriter Static Creates a writer that incrementally encodes bytes as RFC 4648 Base64. The writer owns `destination`. `close` emits padding for the final partial quantum before closing the destination. ```teal function tecs.io.newBase64EncodeWriter( destination: types.Writer | FILE ): types.Writer ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `destination` | [`types.Writer`](/modules/io/#tecs.io.Writer) | FILE | The caller supplies the writer receiving Base64 text. | #### Returns | Type | Description | | --- | --- | | [`types.Writer`](/modules/io/#tecs.io.Writer) | Returns a caller-owned writer that accepts unencoded bytes. | #### Examples ```teal tecs.scoped( "encode base64", function(scope: tecs.Scope) local encoded = scope:own(tecs.io.newBuffer()) local writer = scope:own( tecs.io.newBase64EncodeWriter(encoded:newWriter()) ) assert(writer:write("snapshot bytes")) assert(writer:close()) assert(encoded:getString() == "c25hcHNob3QgYnl0ZXM=") end ) ``` ### tecs.io.newBuffer Static Creates an owned growable byte buffer. ```teal function tecs.io.newBuffer(initial: integer | string): IOBuffer ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `initial` | integer | string | The caller supplies a non-negative zero-filled logical length, a string to copy, or nil for an empty buffer. | #### Returns | Type | Description | | --- | --- | | [`IOBuffer`](/modules/io/#tecs.io.Buffer) | Returns a caller-owned buffer. | #### Examples ```teal local bytes = tecs.io.newBuffer("save data") print(bytes:length()) bytes:close() ``` ### tecs.io.newByteReader Static Creates a reader over borrowed FFI memory. The reader never frees or modifies the allocation. The caller keeps its owner reachable and the bytes unchanged until the reader closes. ```teal function tecs.io.newByteReader( pointer: loader.CValue, length: integer ): types.Reader ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `pointer` | `loader.CValue` | The caller supplies the first readable byte. | | `length` | `integer` | The caller supplies the non-negative readable byte count. | #### Returns | Type | Description | | --- | --- | | [`types.Reader`](/modules/io/#tecs.io.Reader) | Returns a caller-owned reader whose cursor starts at zero. | #### Examples ```teal local ffi = require("ffi") local bytes = "borrowed bytes" local pointer = ffi.cast("const uint8_t *", bytes) local reader = tecs.io.newByteReader(pointer as any, #bytes) assert(reader:read(1024) == bytes) reader:close() ``` ### tecs.io.newByteStream Static Creates a replayable read-only stream over borrowed FFI bytes. Closing the stream or its readers never frees the allocation. ```teal function tecs.io.newByteStream( pointer: loader.CValue, length: integer, contentType: string ): types.ReadableStream ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `pointer` | `loader.CValue` | The caller keeps the backing allocation alive and unchanged until every reader closes and every transfer returns. | | `length` | `integer` | The caller supplies the non-negative byte count. | | `contentType` | `string` | The caller supplies optional media type metadata. | #### Returns | Type | Description | | --- | --- | | [`types.ReadableStream`](/modules/io/#tecs.io.ReadableStream) | Returns a readable stream. | #### Examples ```teal local ffi = require("ffi") local bytes = "borrowed bytes" local pointer = ffi.cast("const uint8_t *", bytes) local source = tecs.io.newByteStream(pointer as any, #bytes) print(source:readAll()) source:close() ``` ### tecs.io.newDeflateWriter Static Creates a writer that incrementally deflates bytes into a destination. The writer owns `destination`. `flush` emits a synchronization point without ending the compressed stream, and `close` emits its trailer before closing the destination. ```teal function tecs.io.newDeflateWriter( destination: types.Writer | FILE, options: types.DeflateWriterOptions ): types.Writer ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `destination` | [`types.Writer`](/modules/io/#tecs.io.Writer) | FILE | The caller supplies the writer receiving compressed bytes. | | `options` | [`types.DeflateWriterOptions`](/modules/io/#tecs.io.DeflateWriterOptions) | The caller selects raw framing and a compression level or omits it for zlib framing and zlib's default level. | #### Returns | Type | Description | | --- | --- | | [`types.Writer`](/modules/io/#tecs.io.Writer) | Returns a caller-owned writer that accepts uncompressed bytes. | #### Examples ```teal tecs.scoped( "deflate save", function(scope: tecs.Scope) local compressed = scope:own(tecs.io.newBuffer()) local writer = scope:own( tecs.io.newDeflateWriter(compressed:newWriter()) ) assert(writer:write("snapshot ")) assert(writer:write("bytes")) assert(writer:close()) assert(compressed:length() > 0) end ) ``` ### tecs.io.newEmptyStream Static Creates a replayable readable empty stream. ```teal function tecs.io.newEmptyStream( contentType: string ): types.ReadableStream ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `contentType` | `string` | The caller supplies optional media type metadata. | #### Returns | Type | Description | | --- | --- | | [`types.ReadableStream`](/modules/io/#tecs.io.ReadableStream) | Returns a source whose length is zero. | #### Examples ```teal local source = tecs.io.newEmptyStream( "application/octet-stream" ) print(source:readAll()) source:close() ``` ### tecs.io.newFileStream Static Creates a replayable file stream. Readers open the current path through `tecs.io.files`; writers replace it. A descriptor keeps no open file between endpoints. HTTP uploads on the SDL storage backend reopen the path directly on the native HTTP lane and stream it without retaining the complete file in Lua. ```teal function tecs.io.newFileStream( path: string | Path, contentType: string ): types.ReadWriteStream ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `path` | string | [`Path`](/modules/io/Path/) | The caller supplies the source or destination as a string or [`Path`](/modules/io/Path/). | | `contentType` | `string` | The caller supplies optional media type metadata. | #### Returns | Type | Description | | --- | --- | | [`types.ReadWriteStream`](/modules/io/#tecs.io.ReadWriteStream) | Returns a readable and writable stream. | #### Examples ```teal local path = tecs.io.Path.new("save.bin") local save = tecs.io.newFileStream( path, "application/octet-stream" ) assert(save:writeAll("save data")) print(save:readAll()) save:close() ``` ### tecs.io.newHandleStream Static Creates a one-shot stream over a borrowed Lua file handle. The first reader or writer claims its current cursor. Closing the stream or endpoint does not close the caller's handle. ```teal function tecs.io.newHandleStream( handle: FILE, length: integer, contentType: string ): types.ReadWriteStream ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `handle` | `FILE` | The caller keeps the Lua file handle open through the endpoint's close. | | `length` | `integer` | The caller supplies the remaining byte count when known. | | `contentType` | `string` | The caller supplies optional media type metadata. | #### Returns | Type | Description | | --- | --- | | [`types.ReadWriteStream`](/modules/io/#tecs.io.ReadWriteStream) | Returns a non-replayable readable and writable stream. | #### Examples ```teal local handle = assert(io.open("save.bin", "rb")) local source = tecs.io.newHandleStream(handle) print(source:readAll()) source:close() handle:close() ``` ### tecs.io.newHexDecodeReader Static Creates a reader that incrementally decodes hexadecimal text. The reader owns `source` and carries one unmatched nibble across source reads. ```teal function tecs.io.newHexDecodeReader( source: types.Reader | FILE ): types.Reader ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `source` | [`types.Reader`](/modules/io/#tecs.io.Reader) | FILE | The caller supplies the reader of hexadecimal text. | #### Returns | Type | Description | | --- | --- | | [`types.Reader`](/modules/io/#tecs.io.Reader) | Returns a caller-owned reader of decoded bytes. | #### Examples ```teal tecs.scoped( "decode hexadecimal", function(scope: tecs.Scope) local reader = scope:own( tecs.io.newHexDecodeReader( tecs.io.newStringReader("73617665") ) ) local decoded , reason = reader:read(1024) assert(decoded, reason) assert(decoded == "save") end ) ``` ### tecs.io.newHexEncodeWriter Static Creates a writer that incrementally encodes bytes as lowercase hexadecimal text. The writer owns `destination` and forwards each encoded chunk without retaining input bytes. ```teal function tecs.io.newHexEncodeWriter( destination: types.Writer | FILE ): types.Writer ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `destination` | [`types.Writer`](/modules/io/#tecs.io.Writer) | FILE | The caller supplies the writer receiving hexadecimal text. | #### Returns | Type | Description | | --- | --- | | [`types.Writer`](/modules/io/#tecs.io.Writer) | Returns a caller-owned writer that accepts unencoded bytes. | #### Examples ```teal tecs.scoped( "encode hexadecimal", function(scope: tecs.Scope) local encoded = scope:own(tecs.io.newBuffer()) local writer = scope:own( tecs.io.newHexEncodeWriter(encoded:newWriter()) ) assert(writer:write("save")) assert(writer:close()) assert(encoded:getString() == "73617665") end ) ``` ### tecs.io.newInflateReader Static Creates a reader that incrementally inflates a compressed source. The reader owns `source` and closes it when the wrapper closes. Reads retain only bounded compressed and decompressed chunks. A raw stream has no zlib header, trailer, or checksum. ```teal function tecs.io.newInflateReader( source: types.Reader | FILE, options: types.InflateReaderOptions ): types.Reader ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `source` | [`types.Reader`](/modules/io/#tecs.io.Reader) | FILE | The caller supplies the reader of compressed bytes. | | `options` | [`types.InflateReaderOptions`](/modules/io/#tecs.io.InflateReaderOptions) | The caller selects raw framing and an output ceiling or omits it for a zlib stream and the default ceiling. | #### Returns | Type | Description | | --- | --- | | [`types.Reader`](/modules/io/#tecs.io.Reader) | Returns a caller-owned reader of decompressed bytes. | #### Examples ```teal tecs.scoped( "inflate save", function(scope: tecs.Scope) local compressed = scope:own(tecs.io.newBuffer()) local restored = scope:own(tecs.io.newBuffer()) local compressor = scope:own( tecs.io.newDeflateWriter(compressed:newWriter()) ) assert(compressor:write("snapshot bytes")) assert(compressor:close()) local reader = scope:own( tecs.io.newInflateReader(compressed:newReader()) ) local sink = scope:own(restored:newWriter()) local count , reason = tecs.io.transfer(reader, sink) assert(count, reason) assert(restored:getString() == "snapshot bytes") end ) ``` ### tecs.io.newStringReader Static Creates a reader over an immutable Lua string. The reader retains `bytes` without copying it. Closing the reader drops that retained reference; there is no separate resource to close. ```teal function tecs.io.newStringReader(bytes: string): types.Reader ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `bytes` | `string` | The caller supplies complete binary contents. | #### Returns | Type | Description | | --- | --- | | [`types.Reader`](/modules/io/#tecs.io.Reader) | Returns a caller-owned reader whose cursor starts at zero. | #### Examples ```teal local reader = tecs.io.newStringReader("snapshot bytes") assert(reader:read(1024) == "snapshot bytes") reader:close() ``` ### tecs.io.newStringStream Static Creates a replayable read-only string stream. The stream retains the immutable string without copying it. ```teal function tecs.io.newStringStream( bytes: string, contentType: string ): types.ReadableStream ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `bytes` | `string` | The caller supplies complete binary contents. | | `contentType` | `string` | The caller supplies optional media type metadata. | #### Returns | Type | Description | | --- | --- | | [`types.ReadableStream`](/modules/io/#tecs.io.ReadableStream) | Returns a readable stream. | #### Examples ```teal local source = tecs.io.newStringStream("hello", "text/plain") print(source:readAll()) source:close() ``` ### tecs.io.newTranscodeReader Static Creates a reader that incrementally converts character encodings. The reader owns `source`. It preserves conversion state and carries an incomplete multibyte character across source reads. ```teal function tecs.io.newTranscodeReader( source: types.Reader | FILE, fromEncoding: string, toEncoding: string ): types.Reader ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `source` | [`types.Reader`](/modules/io/#tecs.io.Reader) | FILE | The caller supplies the reader of encoded text. | | `fromEncoding` | `string` | The caller supplies an SDL iconv source encoding name such as `UTF-8`. | | `toEncoding` | `string` | The caller supplies an SDL iconv destination encoding name such as `UTF-16LE`. | #### Returns | Type | Description | | --- | --- | | [`types.Reader`](/modules/io/#tecs.io.Reader) | Returns a caller-owned reader of converted text. | #### Examples ```teal tecs.scoped( "transcode input", function(scope: tecs.Scope) local reader = scope:own( tecs.io.newTranscodeReader( tecs.io.newStringReader("A\0\233\0"), "UTF-16LE", "UTF-8" ) ) local utf8 , reason = reader:read(1024) assert(utf8, reason) assert(utf8 == "A\195\169") end ) ``` ### tecs.io.newTranscodeWriter Static Creates a writer that incrementally converts character encodings. The writer owns `destination`. It preserves conversion state and carries an incomplete multibyte character across writes; `close` rejects a truncated final character and emits any target shift state. ```teal function tecs.io.newTranscodeWriter( destination: types.Writer | FILE, fromEncoding: string, toEncoding: string ): types.Writer ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `destination` | [`types.Writer`](/modules/io/#tecs.io.Writer) | FILE | The caller supplies the writer receiving converted text. | | `fromEncoding` | `string` | The caller supplies an SDL iconv source encoding name such as `UTF-8`. | | `toEncoding` | `string` | The caller supplies an SDL iconv destination encoding name such as `UTF-16LE`. | #### Returns | Type | Description | | --- | --- | | [`types.Writer`](/modules/io/#tecs.io.Writer) | Returns a caller-owned writer that accepts bytes in `fromEncoding`. | #### Examples ```teal tecs.scoped( "transcode output", function(scope: tecs.Scope) local utf16 = scope:own(tecs.io.newBuffer()) local writer = scope:own( tecs.io.newTranscodeWriter( utf16:newWriter(), "UTF-8", "UTF-16LE" ) ) assert(writer:write("A\195")) assert(writer:write("\169")) assert(writer:close()) assert(utf16:getString() == "A\0\233\0") end ) ``` ## Types ### tecs.io.Address record An `Address` owns one resolved network address. Closing it releases the address and remains safe to repeat. ```teal record tecs.io.Address is Closeable host: string text: string close: function(self): boolean, string isClosed: function(self): boolean end ``` #### Interfaces | Interface | | --- | | [`Closeable`](/modules/#tecs.Closeable) | #### tecs.io.Address.host field Read-only. Networking sets `host` at resolution or peer lookup. It contains the requested hostname or numeric peer address. ```teal tecs.io.Address.host: string ``` #### tecs.io.Address.text field Read-only. Networking sets `text` to the printable numeric address when it creates the object. ```teal tecs.io.Address.text: string ``` #### tecs.io.Address:close Instance ```teal function tecs.io.Address.close(self): boolean, string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Address` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | | | `string` | | #### tecs.io.Address:isClosed Instance Returns whether `close` has released this address. ```teal function tecs.io.Address.isClosed(self): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Address` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns true after `close`. | ### tecs.io.Buffer interface A `Buffer` is owned growable FFI-backed byte storage. ```teal interface tecs.io.Buffer is Closeable interface WriteRange is Closeable capacity: function(self): integer commit: function(self, used: integer) getFFIPointer: function(self): loader.BytePointer end capacity: function(self): integer clear: function(self) ensureCapacity: function(self, minimum: integer) getFFIPointer: function(self): loader.BytePointer getString: function(self, offset: integer, count: integer): string isReleased: function(self): boolean length: function(self): integer newReader: function(self): Reader newStream: function(self, contentType: string): ReadWriteStream newWriter: function(self): Writer reserveRange: function( self, offset: integer, minimum: integer ): WriteRange resize: function(self, length: integer) setString: function(self, bytes: string, offset: integer) view: function(self, offset: integer, count: integer): ByteView end ``` #### Interfaces | Interface | | --- | | [`Closeable`](/modules/#tecs.Closeable) | #### tecs.io.Buffer.WriteRange interface An exclusive, zero-copy writable range reserved from a [`Buffer`](/modules/io/#tecs.io.Buffer). Closing abandons the range without changing the buffer's logical length. ```teal interface tecs.io.Buffer.WriteRange is Closeable capacity: function(self): integer commit: function(self, used: integer) getFFIPointer: function(self): loader.BytePointer end ``` ##### Interfaces | Interface | | --- | | [`Closeable`](/modules/#tecs.Closeable) | ##### tecs.io.Buffer.WriteRange:capacity Instance Returns the maximum byte count the native writer may fill. ```teal function tecs.io.Buffer.WriteRange.capacity(self): integer ``` ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `WriteRange` | The open range to inspect. | ###### Returns | Type | Description | | --- | --- | | `integer` | Returns the reserved byte capacity. | ##### tecs.io.Buffer.WriteRange:commit Instance Commits bytes written through the borrowed pointer. The call closes the range. A gap before the committed bytes is zero-filled. ```teal function tecs.io.Buffer.WriteRange.commit(self, used: integer) ``` ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `WriteRange` | The open range to commit. | | `used` | `integer` | The caller supplies the written count up to `capacity`. | ###### Returns None. ##### tecs.io.Buffer.WriteRange:getFFIPointer Instance Borrows the reserved range's writable address. ```teal function tecs.io.Buffer.WriteRange.getFFIPointer( self ): loader.BytePointer ``` ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `WriteRange` | The open range whose storage to borrow. | ###### Returns | Type | Description | | --- | --- | | `loader.BytePointer` | Returns a pointer valid until `commit` or `close`. | #### tecs.io.Buffer:capacity Instance Returns the allocated byte capacity. ```teal function tecs.io.Buffer.capacity(self): integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Buffer` | The buffer to inspect. | ##### Returns | Type | Description | | --- | --- | | `integer` | Returns the byte count available without reallocating. | #### Examples ```teal local bytes = tecs.io.newBuffer("data") assert(bytes:capacity() >= bytes:length()) bytes:close() ``` #### tecs.io.Buffer:clear Instance Removes every logical byte without releasing the allocation. ```teal function tecs.io.Buffer.clear(self) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Buffer` | The buffer to clear. | ##### Returns None. #### Examples ```teal local bytes = tecs.io.newBuffer("reusable") local capacity = bytes:capacity() bytes:clear() assert(bytes:length() == 0) assert(bytes:capacity() == capacity) bytes:close() ``` #### tecs.io.Buffer:ensureCapacity Instance Ensures at least `minimum` bytes fit without another allocation. Growth is geometric. A call at or below the current capacity leaves pointers returned by `getFFIPointer` valid. ```teal function tecs.io.Buffer.ensureCapacity(self, minimum: integer) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Buffer` | The buffer to grow. | | `minimum` | `integer` | The caller supplies a non-negative byte count. | ##### Returns None. #### Examples ```teal local bytes = tecs.io.newBuffer("data") bytes:ensureCapacity(1024) assert(bytes:capacity() >= 1024) assert(bytes:length() == 4) bytes:close() ``` #### tecs.io.Buffer:getFFIPointer Instance Borrows the address of the first byte. The pointer is invalid after capacity grows or `close` runs. The buffer must remain reachable for the whole native call that uses it. ```teal function tecs.io.Buffer.getFFIPointer(self): loader.BytePointer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Buffer` | The buffer whose storage to borrow. | ##### Returns | Type | Description | | --- | --- | | `loader.BytePointer` | Returns a mutable FFI byte pointer. | #### Examples ```teal local bytes = tecs.io.newBuffer("native bytes") local pointer = bytes:getFFIPointer() print(pointer) bytes:close() ``` #### tecs.io.Buffer:getString Instance Copies a range into a Lua string. ```teal function tecs.io.Buffer.getString( self, offset: integer, count: integer ): string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Buffer` | The source buffer. | | `offset` | `integer` | The caller supplies a zero-based offset or omits it for zero. | | `count` | `integer` | The caller supplies a byte count or omits it for the remainder. | ##### Returns | Type | Description | | --- | --- | | `string` | Returns a fresh binary string. | #### Examples ```teal local bytes = tecs.io.newBuffer("headerpayload") print(bytes:getString(6, 7)) bytes:close() ``` #### tecs.io.Buffer:isReleased Instance Returns whether `close` has given up the allocation. ```teal function tecs.io.Buffer.isReleased(self): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Buffer` | The buffer to inspect. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns true after `close`. | #### Examples ```teal local bytes = tecs.io.newBuffer() assert(not bytes:isReleased()) bytes:close() assert(bytes:isReleased()) ``` #### tecs.io.Buffer:length Instance Returns the number of logical bytes. ```teal function tecs.io.Buffer.length(self): integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Buffer` | The buffer to inspect. | ##### Returns | Type | Description | | --- | --- | | `integer` | Returns the bytes visible to `getString`. | #### Examples ```teal local bytes = tecs.io.newBuffer("four") print(bytes:length()) bytes:close() ``` #### tecs.io.Buffer:newReader Instance Opens a reader over an immutable snapshot of the current bytes. The reader retains the allocation independently, so the caller may close or mutate the buffer after this call. Its cursor starts at zero. ```teal function tecs.io.Buffer.newReader(self): Reader ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Buffer` | The open source buffer. | ##### Returns | Type | Description | | --- | --- | | `Reader` | Returns a caller-owned reader. | #### Examples ```teal local bytes = tecs.io.newBuffer("snapshot") local reader = bytes:newReader() bytes:close() assert(reader:read(1024) == "snapshot") reader:close() ``` #### tecs.io.Buffer:newStream Instance Creates a stream sharing this buffer. The stream borrows this buffer. The caller keeps it open while using the stream and closes it separately. Closing the stream never closes the buffer. `transferToBuffer` returns this same borrowed object without transferring ownership. ```teal function tecs.io.Buffer.newStream( self, contentType: string ): ReadWriteStream ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Buffer` | The open buffer the stream borrows. | | `contentType` | `string` | The caller supplies optional media type metadata. | ##### Returns | Type | Description | | --- | --- | | `ReadWriteStream` | Returns a caller-owned readable and writable stream. | #### Examples ```teal local bytes = tecs.io.newBuffer("old") local stream = bytes:newStream() assert(stream:writeAll("new")) print(bytes:getString()) stream:close() bytes:close() ``` #### tecs.io.Buffer:newWriter Instance Opens a writer that replaces the current bytes. The call clears the logical length while retaining capacity. The caller keeps the buffer open until the writer closes. ```teal function tecs.io.Buffer.newWriter(self): Writer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Buffer` | The open destination buffer. | ##### Returns | Type | Description | | --- | --- | | `Writer` | Returns a caller-owned writer. | #### Examples ```teal local bytes = tecs.io.newBuffer("old") local writer = bytes:newWriter() assert(writer:write("new")) assert(writer:close()) assert(bytes:getString() == "new") bytes:close() ``` #### tecs.io.Buffer:reserveRange Instance Reserves an exclusive range for a native writer without an intermediate string. The buffer detaches from retained views before exposing mutable storage. ```teal function tecs.io.Buffer.reserveRange( self, offset: integer, minimum: integer ): WriteRange ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Buffer` | The open buffer to reserve. | | `offset` | `integer` | The caller supplies a zero-based write offset. | | `minimum` | `integer` | The caller supplies the minimum writable capacity. | ##### Returns | Type | Description | | --- | --- | | [`WriteRange`](/modules/io/#tecs.io.Buffer.WriteRange) | Returns a caller-owned exclusive range. | #### tecs.io.Buffer:resize Instance Changes the logical length. Expansion fills every newly visible byte with zero. Shrinking keeps the allocation for reuse. ```teal function tecs.io.Buffer.resize(self, length: integer) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Buffer` | The buffer to resize. | | `length` | `integer` | The caller supplies a non-negative byte count. | ##### Returns None. #### Examples ```teal local bytes = tecs.io.newBuffer("data") bytes:resize(6) assert(bytes:getString() == "data\0\0") bytes:resize(2) assert(bytes:getString() == "da") bytes:close() ``` #### tecs.io.Buffer:setString Instance Copies a Lua string into the buffer. The write extends the logical length as needed. A gap between the old end and `offset` is zero-filled. ```teal function tecs.io.Buffer.setString(self, bytes: string, offset: integer) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Buffer` | The destination buffer. | | `bytes` | `string` | The caller supplies a binary string. | | `offset` | `integer` | The caller supplies a zero-based offset or omits it for zero. | ##### Returns None. #### Examples ```teal local bytes = tecs.io.newBuffer("abcdefgh") bytes:setString("XY", 3) assert(bytes:getString() == "abcXYfgh") bytes:setString("!", 10) assert(bytes:getString(8, 3) == "\0\0!") bytes:close() ``` #### tecs.io.Buffer:view Instance Returns a zero-copy immutable snapshot of a byte range. ```teal function tecs.io.Buffer.view( self, offset: integer, count: integer ): ByteView ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Buffer` | The open buffer to retain. | | `offset` | `integer` | The caller supplies a zero-based offset or omits it for zero. | | `count` | `integer` | The caller supplies a byte count or omits it for the remainder. | ##### Returns | Type | Description | | --- | --- | | [`ByteView`](/modules/io/#tecs.io.ByteView) | Returns a caller-owned retained view. | ### tecs.io.ByteView interface A `ByteView` retains an immutable zero-copy range from a buffer. ```teal interface tecs.io.ByteView is Closeable getFFIPointer: function(self): loader.BytePointer getString: function(self): string isReleased: function(self): boolean length: function(self): integer newReader: function(self): Reader newStream: function(self, contentType: string): ReadableStream view: function(self, offset: integer, count: integer): ByteView end ``` #### Interfaces | Interface | | --- | | [`Closeable`](/modules/#tecs.Closeable) | #### tecs.io.ByteView:getFFIPointer Instance Borrows the retained range's read-only address. LuaJIT FFI cannot enforce constness. Writing through a cast pointer is unsafe and breaks the snapshot guarantee. ```teal function tecs.io.ByteView.getFFIPointer(self): loader.BytePointer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `ByteView` | The open view whose storage to borrow. | ##### Returns | Type | Description | | --- | --- | | `loader.BytePointer` | Returns a pointer valid until `close`. | #### tecs.io.ByteView:getString Instance Copies the retained range into a Lua string. ```teal function tecs.io.ByteView.getString(self): string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `ByteView` | The open view to copy. | ##### Returns | Type | Description | | --- | --- | | `string` | Returns a fresh binary string. | #### tecs.io.ByteView:isReleased Instance Returns whether the view has released its retained allocation. ```teal function tecs.io.ByteView.isReleased(self): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `ByteView` | The view to inspect. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns true after `close`. | #### tecs.io.ByteView:length Instance Returns the retained range's byte length. ```teal function tecs.io.ByteView.length(self): integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `ByteView` | The open view to inspect. | ##### Returns | Type | Description | | --- | --- | | `integer` | Returns the number of readable bytes. | #### tecs.io.ByteView:newReader Instance Opens a reader over this immutable range. The reader retains the allocation independently, so the caller may close this view immediately after the call. Its cursor starts at zero. ```teal function tecs.io.ByteView.newReader(self): Reader ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `ByteView` | The open source view. | ##### Returns | Type | Description | | --- | --- | | `Reader` | Returns a caller-owned reader. | #### Examples ```teal local bytes = tecs.io.newBuffer("headerpayload") local payload = bytes:view(6) local reader = payload:newReader() payload:close() bytes:close() assert(reader:read(1024) == "payload") reader:close() ``` #### tecs.io.ByteView:newStream Instance Creates a replayable read-only stream retaining this view. The stream retains its own view, so the caller may close this view immediately after construction. Closing the stream closes its retained view after already-open readers close theirs. ```teal function tecs.io.ByteView.newStream( self, contentType: string ): ReadableStream ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `ByteView` | The open immutable byte view. | | `contentType` | `string` | The caller supplies optional media type metadata. | ##### Returns | Type | Description | | --- | --- | | `ReadableStream` | Returns a caller-owned readable stream. | #### Examples ```teal local bytes = tecs.io.newBuffer("headerpayload") local payload = bytes:view(6) local source = payload:newStream("application/octet-stream") payload:close() bytes:close() assert(source:readAll() == "payload") source:close() ``` #### tecs.io.ByteView:view Instance Retains a zero-copy subrange. ```teal function tecs.io.ByteView.view( self, offset: integer, count: integer ): ByteView ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `ByteView` | The open source view. | | `offset` | `integer` | The caller supplies a zero-based offset or omits it for zero. | | `count` | `integer` | The caller supplies a byte count or omits it for the remainder. | ##### Returns | Type | Description | | --- | --- | | [`ByteView`](/modules/io/#tecs.io.ByteView) | Returns a caller-owned view retaining the same allocation. | ### tecs.io.DeflateWriterOptions record Options control an incremental deflate writer. ```teal record tecs.io.DeflateWriterOptions raw: boolean level: integer end ``` #### tecs.io.DeflateWriterOptions.raw field Caller-writable. Selects raw DEFLATE when true and zlib framing when false or omitted. ```teal tecs.io.DeflateWriterOptions.raw: boolean ``` #### tecs.io.DeflateWriterOptions.level field Caller-writable. Sets zlib's compression level from minus one through nine or uses its default when omitted. ```teal tecs.io.DeflateWriterOptions.level: integer ``` ### tecs.io.InflateReaderOptions record Options control an incremental inflate reader. ```teal record tecs.io.InflateReaderOptions raw: boolean maxBytes: integer end ``` #### tecs.io.InflateReaderOptions.raw field Caller-writable. Selects raw DEFLATE when true and zlib framing when false or omitted. ```teal tecs.io.InflateReaderOptions.raw: boolean ``` #### tecs.io.InflateReaderOptions.maxBytes field Caller-writable. Sets the hard decompressed-byte ceiling or uses 268,435,456 bytes when omitted. ```teal tecs.io.InflateReaderOptions.maxBytes: integer ``` ### tecs.io.ReadableStream interface A `ReadableStream` opens readers and supplies whole-source transfers. ```teal interface tecs.io.ReadableStream is Stream, Closeable discard: function(self): integer, string newReader: function(self): Reader, string readAll: function(self, maxBytes: integer): string, string transferTo: function( self, destination: WritableStream ): integer, string transferToBuffer: function(self, maxBytes: integer): Buffer, string transferToFile: function(self, path: string): integer, string end ``` #### Interfaces | Interface | | --- | | [`Stream`](/modules/io/#tecs.io.Stream) | | [`Closeable`](/modules/#tecs.Closeable) | #### tecs.io.ReadableStream:discard Instance Reads and discards every source byte. The operation opens, consumes, and closes one reader. On a non-replayable stream, this claims its one endpoint. ```teal function tecs.io.ReadableStream.discard(self): integer, string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `ReadableStream` | The readable descriptor. | ##### Returns | Type | Description | | --- | --- | | `integer` | Returns the bytes discarded. | | `string` | Returns the source's reason when the first return is nil. | #### Examples ```teal local source = tecs.io.newFileStream("download.tmp") local count = source:discard() print(count) source:close() ``` #### tecs.io.ReadableStream:newReader Instance Opens a reader with a new cursor. ```teal function tecs.io.ReadableStream.newReader(self): Reader, string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `ReadableStream` | The readable descriptor. | ##### Returns | Type | Description | | --- | --- | | [`Reader`](/modules/io/#tecs.io.Reader) | Returns a caller-owned reader, or nil when unavailable. | | `string` | Returns the reason when the first return is nil. | #### Examples ```teal local source = tecs.io.newStringStream("data") local reader = assert(source:newReader()) print(reader:read(4)) reader:close() source:close() ``` #### tecs.io.ReadableStream:readAll Instance Reads the complete source into a Lua string. The operation opens, consumes, and closes one reader. On a non-replayable stream, this claims its one endpoint. ```teal function tecs.io.ReadableStream.readAll( self, maxBytes: integer ): string, string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `ReadableStream` | The readable descriptor. | | `maxBytes` | `integer` | The caller supplies a non-negative limit or omits it for no explicit limit. | ##### Returns | Type | Description | | --- | --- | | `string` | Returns the complete bytes. | | `string` | Returns the source's reason when the first return is nil. | #### Examples ```teal local source = tecs.io.newStringStream("data") local bytes = source:readAll(1024) print(bytes) source:close() ``` #### tecs.io.ReadableStream:transferTo Instance Copies every source byte into `destination`. The operation opens and closes one source reader and one destination writer. Either non-replayable descriptor is claimed by the call. ```teal function tecs.io.ReadableStream.transferTo( self, destination: WritableStream ): integer, string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `ReadableStream` | The readable descriptor. | | `destination` | `WritableStream` | The caller supplies an available writable stream. | ##### Returns | Type | Description | | --- | --- | | `integer` | Returns the bytes written. | | `string` | Returns the source or destination reason when the first return is nil. | #### Examples ```teal local source = tecs.io.newStringStream("data") local output = tecs.io.newBuffer() local destination = output:newStream() local count = source:transferTo(destination) print(count, output:getString()) source:close() destination:close() output:close() ``` #### tecs.io.ReadableStream:transferToBuffer Instance Transfers the complete source into an owned buffer. A stream created by `Buffer:newStream` returns that same borrowed buffer without copying and does not transfer ownership. Other streams return a newly allocated buffer owned by the caller. The operation opens, consumes, and closes one reader when it must copy. On a non-replayable stream, this claims its one endpoint. ```teal function tecs.io.ReadableStream.transferToBuffer( self, maxBytes: integer ): Buffer, string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `ReadableStream` | The readable descriptor. | | `maxBytes` | `integer` | The caller supplies a non-negative limit or omits it for no explicit limit. | ##### Returns | Type | Description | | --- | --- | | [`Buffer`](/modules/io/#tecs.io.Buffer) | Returns a caller-owned buffer. | | `string` | Returns the source's reason when the first return is nil. | #### Examples ```teal local source = tecs.io.newFileStream("save.bin") local bytes = source:transferToBuffer(1024 * 1024) print(bytes:length()) bytes:close() source:close() ``` #### tecs.io.ReadableStream:transferToFile Instance Copies every source byte into a file. The operation opens and closes one source reader. On a non-replayable stream, this claims its one endpoint. ```teal function tecs.io.ReadableStream.transferToFile( self, path: string ): integer, string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `ReadableStream` | The readable descriptor. | | `path` | `string` | The caller supplies the destination path to replace. | ##### Returns | Type | Description | | --- | --- | | `integer` | Returns the bytes written. | | `string` | Returns the source or file reason when the first return is nil. | #### Examples ```teal local source = tecs.io.newStringStream("save data") local count = source:transferToFile("save.bin") print(count) source:close() ``` ### tecs.io.Reader interface A `Reader` supplies bytes in order and releases its owned state on `close`. ```teal interface tecs.io.Reader is Closeable read: function(self, count: integer): string, string readInto: function( self, destination: Buffer, offset: integer, count: integer ): integer, string end ``` #### Interfaces | Interface | | --- | | [`Closeable`](/modules/#tecs.Closeable) | #### tecs.io.Reader:read Instance Caller-writable. Supplies the operation that reads the next bytes. ```teal function tecs.io.Reader.read(self, count: integer): string, string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Reader` | The reader whose cursor advances. | | `count` | `integer` | The maximum number of bytes to return when positive. Zero and negative counts are treated as one so the reader makes progress. | ##### Returns | Type | Description | | --- | --- | | `string` | Returns up to `max(1, count)` bytes, fewer at the end, or an empty string after the source is exhausted. | | `string` | Returns the source's reason when the first return is nil. | #### Examples ```teal local source = tecs.io.newStringStream("abcdefgh") local reader = assert(source:newReader()) local chunk , reason = reader:read(4) assert(chunk, reason) print(chunk) reader:close() source:close() ``` #### tecs.io.Reader:readInto Instance Reads bytes directly into a buffer and advances the cursor. The operation grows the destination as needed and preserves bytes outside the written range. It returns zero after the source is exhausted. ```teal function tecs.io.Reader.readInto( self, destination: Buffer, offset: integer, count: integer ): integer, string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Reader` | The open reader whose cursor advances. | | `destination` | [`Buffer`](/modules/io/#tecs.io.Buffer) | The caller supplies an open destination buffer and keeps ownership of it. | | `offset` | `integer` | The caller supplies a non-negative zero-based destination offset or omits it for zero. | | `count` | `integer` | The caller supplies the maximum non-negative byte count or omits it for 16,384. | ##### Returns | Type | Description | | --- | --- | | `integer` | Returns the number of bytes written into the destination. | | `string` | Returns the source's reason when the first return is nil. | #### Examples ```teal local source = tecs.io.newStringStream("abcdefgh") local reader = assert(source:newReader()) local bytes = tecs.io.newBuffer("prefix:") local count , reason = reader:readInto(bytes, bytes:length(), 4) assert(count, reason) print(bytes:getString()) reader:close() source:close() bytes:close() ``` ### tecs.io.ReadWriteStream interface A `ReadWriteStream` supports both directional interfaces. ```teal interface tecs.io.ReadWriteStream is ReadableStream, Stream, Closeable, WritableStream end ``` #### Interfaces | Interface | | --- | | [`ReadableStream`](/modules/io/#tecs.io.ReadableStream) | | [`Stream`](/modules/io/#tecs.io.Stream) | | [`Closeable`](/modules/#tecs.Closeable) | | [`WritableStream`](/modules/io/#tecs.io.WritableStream) | ### tecs.io.Seekable interface `Seekable` supplies random-access cursor operations shared by readers and writers. ```teal interface tecs.io.Seekable enum Origin "current" "end" "start" end seek: function( self, origin: Origin, offset: integer ): integer, string size: function(self): integer, string tell: function(self): integer, string end ``` #### tecs.io.Seekable.Origin enum `Origin` selects the reference point for a seek. ```teal enum tecs.io.Seekable.Origin "current" "end" "start" end ``` #### tecs.io.Seekable:seek Instance Repositions the cursor and returns its new absolute position. `"start"` measures `offset` from byte zero, `"current"` measures it from the cursor, and `"end"` measures it from the current byte length. The offset defaults to zero and may be negative for the latter two origins. A result before byte zero or past the current end fails without moving the cursor. ```teal function tecs.io.Seekable.seek( self, origin: Origin, offset: integer ): integer, string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Seekable` | The open cursor to reposition. | | `origin` | [`Origin`](/modules/io/#tecs.io.Seekable.Origin) | The caller selects the reference point. | | `offset` | `integer` | The caller supplies a signed integer byte offset or omits it for zero. | ##### Returns | Type | Description | | --- | --- | | `integer` | Returns the new zero-based cursor position. | | `string` | Returns the storage reason when the first return is nil. | #### tecs.io.Seekable:size Instance Returns the storage's current byte length. The result includes every write already accepted even when buffered bytes have not reached the underlying destination yet. ```teal function tecs.io.Seekable.size(self): integer, string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Seekable` | The open cursor to inspect. | ##### Returns | Type | Description | | --- | --- | | `integer` | Returns the current byte length. | | `string` | Returns the storage reason when the first return is nil. | #### tecs.io.Seekable:tell Instance Returns the current zero-based cursor position. ```teal function tecs.io.Seekable.tell(self): integer, string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Seekable` | The open cursor to inspect. | ##### Returns | Type | Description | | --- | --- | | `integer` | Returns the cursor's byte position. | | `string` | Returns the storage reason when the first return is nil. | ### tecs.io.SeekableReader interface A `SeekableReader` supplies bytes through a repositionable cursor. ```teal interface tecs.io.SeekableReader is Reader, Closeable, Seekable end ``` #### Interfaces | Interface | | --- | | [`Reader`](/modules/io/#tecs.io.Reader) | | [`Closeable`](/modules/#tecs.Closeable) | | [`Seekable`](/modules/io/#tecs.io.Seekable) | ### tecs.io.SeekableWriter interface A `SeekableWriter` patches a destination through a repositionable cursor. ```teal interface tecs.io.SeekableWriter is Writer, Closeable, Seekable end ``` #### Interfaces | Interface | | --- | | [`Writer`](/modules/io/#tecs.io.Writer) | | [`Closeable`](/modules/#tecs.Closeable) | | [`Seekable`](/modules/io/#tecs.io.Seekable) | ### tecs.io.Stream interface A `Stream` describes binary storage without retaining a cursor. ```teal interface tecs.io.Stream is Closeable contentLength: function(self): integer | nil contentType: function(self): string | nil isReadable: function(self): boolean isReplayable: function(self): boolean isWritable: function(self): boolean withMetadata: function( self, contentType: string, contentLength: integer, isReplayable: boolean ): T end ``` #### Interfaces | Interface | | --- | | [`Closeable`](/modules/#tecs.Closeable) | #### tecs.io.Stream:contentLength Instance Returns the byte length when it is known. ```teal function tecs.io.Stream.contentLength(self): integer | nil ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Stream` | The descriptor to inspect. | ##### Returns | Type | Description | | --- | --- | | integer | nil | Returns the current byte length, or nil when it cannot be known before reading. | #### Examples ```teal local source = tecs.io.newStringStream("four") local length = source:contentLength() if length ~= nil then print(length) end source:close() ``` #### tecs.io.Stream:contentType Instance Returns the media type supplied at construction. ```teal function tecs.io.Stream.contentType(self): string | nil ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Stream` | The descriptor to inspect. | ##### Returns | Type | Description | | --- | --- | | string | nil | Returns the media type, or nil when none was supplied. | #### Examples ```teal local source = tecs.io.newStringStream("{}", "application/json") print(source:contentType()) source:close() ``` #### tecs.io.Stream:isReadable Instance Returns whether the descriptor can open a reader. ```teal function tecs.io.Stream.isReadable(self): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Stream` | The descriptor to inspect. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns true when `newReader` is supported. | #### Examples ```teal local source = tecs.io.newStringStream("data") print(source:isReadable()) source:close() ``` #### tecs.io.Stream:isReplayable Instance Returns whether each reader starts from the beginning. True means the descriptor can open independent readers repeatedly. False means opening an endpoint or running a whole-source operation may claim the descriptor permanently; `newReader` reports when it can no longer open one. ```teal function tecs.io.Stream.isReplayable(self): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Stream` | The descriptor to inspect. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns true when opening a later reader replays all bytes. | #### Examples ```teal local source = tecs.io.newStringStream("data") print(source:isReplayable()) source:close() ``` #### tecs.io.Stream:isWritable Instance Returns whether the descriptor can open a writer. ```teal function tecs.io.Stream.isWritable(self): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Stream` | The descriptor to inspect. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns true when `newWriter` is supported. | #### Examples ```teal local save = tecs.io.newFileStream("save.bin") print(save:isWritable()) save:close() ``` #### tecs.io.Stream:withMetadata Instance Creates a lazy view that overrides this descriptor's metadata. Nil inherits each value from the receiver. The view retains and delegates to the receiver, preserves its static and runtime directional interface, and closes the receiver when it closes. If every supplied value already matches, this returns the receiver. ```teal function tecs.io.Stream.withMetadata( self, contentType: string, contentLength: integer, isReplayable: boolean ): T ``` ##### Type Parameters | Name | Constraint | Description | | --- | --- | --- | | `T` | [`Stream`](/modules/io/#tecs.io.Stream) | | ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `T` | The stream whose storage and operations the view retains. | | `contentType` | `string` | The caller overrides the media type or omits it. | | `contentLength` | `integer` | The caller overrides the non-negative byte length or omits it. | | `isReplayable` | `boolean` | The caller overrides replayability or omits it. | ##### Returns | Type | Description | | --- | --- | | `T` | Returns a metadata view with the receiver's stream type. | #### Examples ```teal local source = tecs.io.newStringStream("{}") local json = source:withMetadata("application/json") print(json:contentType()) json:close() ``` ### tecs.io.TCPListener record A `TCPListener` listens for TCP clients. Closing it stops listening, leaves accepted streams open, and remains safe to repeat. The listener permits one suspended `accept` or `wait` call at a time. ```teal record tecs.io.TCPListener is Closeable port: integer accept: function(self): TCPSocket, string close: function(self): boolean, string isClosed: function(self): boolean wait: function(self, timeoutMs: integer): boolean, string end ``` #### Interfaces | Interface | | --- | | [`Closeable`](/modules/#tecs.Closeable) | #### tecs.io.TCPListener.port field Read-only. Networking sets `port` from the value passed to `listen` and never changes it. Zero remains zero. ```teal tecs.io.TCPListener.port: integer ``` #### tecs.io.TCPListener:accept Instance Takes one client, waiting cooperatively inside a system. Inside a system, the call suspends until a client is ready. Outside a system it blocks the caller. The caller owns the returned stream. ```teal function tecs.io.TCPListener.accept(self): TCPSocket, string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `TCPListener` | | ##### Returns | Type | Description | | --- | --- | | [`TCPSocket`](/modules/io/#tecs.io.TCPSocket) | Returns one caller-owned client, or nil when none waits or acceptance fails. | | `string` | Returns the reason only when acceptance fails. | #### tecs.io.TCPListener:close Instance ```teal function tecs.io.TCPListener.close(self): boolean, string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `TCPListener` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | | | `string` | | #### tecs.io.TCPListener:isClosed Instance Returns whether `close` has released this listener. ```teal function tecs.io.TCPListener.isClosed(self): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `TCPListener` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns true after `close`. | #### tecs.io.TCPListener:wait Instance Waits up to `timeoutMs` for a client to become ready to accept. Returns false without an error on timeout. This does not accept the client; call `accept` afterwards. ```teal function tecs.io.TCPListener.wait( self, timeoutMs: integer ): boolean, string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `TCPListener` | | | `timeoutMs` | `integer` | The caller supplies 0 to 2147483647 milliseconds or omits it for zero. Zero polls without blocking. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns true when at least one client waits. | | `string` | Returns the reason on failure. Timeout returns false without a reason. | ### tecs.io.TCPSocket record A `TCPSocket` owns one connected TCP byte stream. Closing it may discard queued writes, so a caller drains delivery-sensitive output first. Repeated closure remains safe. One owner serializes operations because the socket permits one suspended readiness wait. ```teal record tecs.io.TCPSocket is Closeable close: function(self): boolean, string drain: function(self, timeoutMs: integer): boolean, string isClosed: function(self): boolean peer: function(self): Address, string pendingWrites: function(self): integer, string read: function(self, maxBytes: integer): string, string readInto: function( self, destination: IOBuffer, offset: integer, maxBytes: integer ): integer, string wait: function(self, timeoutMs: integer): boolean, string write: function(self, bytes: string): boolean, string writeFrom: function( self, source: IOBuffer, offset: integer, count: integer ): integer, string writeView: function( self, source: types.ByteView, offset: integer, count: integer ): integer, string end ``` #### Interfaces | Interface | | --- | | [`Closeable`](/modules/#tecs.Closeable) | #### tecs.io.TCPSocket:close Instance ```teal function tecs.io.TCPSocket.close(self): boolean, string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `TCPSocket` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | | | `string` | | #### tecs.io.TCPSocket:drain Instance Waits up to `timeoutMs` for queued writes to be sent. Returns false without an error on timeout. ```teal function tecs.io.TCPSocket.drain( self, timeoutMs: integer ): boolean, string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `TCPSocket` | | | `timeoutMs` | `integer` | The caller supplies 0 to 2147483647 milliseconds or omits it for 5000. Zero polls without blocking. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns true when the local send queue reaches zero before timeout. | | `string` | Returns the reason on failure. Timeout returns false without a reason. | #### tecs.io.TCPSocket:isClosed Instance Returns whether `close` has released this connection. ```teal function tecs.io.TCPSocket.isClosed(self): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `TCPSocket` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns true after `close`. | #### tecs.io.TCPSocket:peer Instance Returns a newly owned address for the remote peer. The caller closes the returned address. ```teal function tecs.io.TCPSocket.peer(self): Address, string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `TCPSocket` | | ##### Returns | Type | Description | | --- | --- | | [`Address`](/modules/io/#tecs.io.Address) | Returns a caller-owned address that outlives the connection, or nil on failure. | | `string` | Returns the reason when the first return is nil. | #### tecs.io.TCPSocket:pendingWrites Instance Returns bytes accepted but not yet sent. ```teal function tecs.io.TCPSocket.pendingWrites(self): integer, string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `TCPSocket` | | ##### Returns | Type | Description | | --- | --- | | `integer` | Returns the queued byte count, zero after sending, or nil on failure. | | `string` | Returns the reason when the first return is nil. | #### tecs.io.TCPSocket:read Instance Reads up to `maxBytes`, waiting cooperatively inside a system. Inside a system, the call suspends when the socket would block and resumes when it becomes readable. Outside a system the same call blocks its caller. A successful read may contain fewer bytes than requested. ```teal function tecs.io.TCPSocket.read( self, maxBytes: integer ): string, string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `TCPSocket` | | | `maxBytes` | `integer` | The caller supplies a ceiling from 1 to 65536 bytes or omits it for 16384. Invalid values raise. | ##### Returns | Type | Description | | --- | --- | | `string` | Returns a fresh string of available bytes, or nil when no bytes arrived or the read failed. | | `string` | Returns the reason only when reading fails or the peer disconnects. | #### tecs.io.TCPSocket:readInto Instance Reads bytes directly into reusable FFI memory. The call writes at a zero-based offset and extends the buffer through the last byte read. It suspends a system or blocks an ordinary caller when no bytes are ready. Reserving the requested range may still grow capacity. A zero count returns zero without consuming input. ```teal function tecs.io.TCPSocket.readInto( self, destination: IOBuffer, offset: integer, maxBytes: integer ): integer, string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `TCPSocket` | | | `destination` | [`IOBuffer`](/modules/io/#tecs.io.Buffer) | The caller supplies an open [`Buffer`](/modules/io/#tecs.io.Buffer). | | `offset` | `integer` | The caller supplies a zero-based destination offset or omits it for zero. | | `maxBytes` | `integer` | The caller supplies a ceiling from 0 to 65536 bytes or omits it for 16384. Invalid values raise. | ##### Returns | Type | Description | | --- | --- | | `integer` | Returns the bytes read, zero for a zero-byte request, or nil when no bytes arrived or the read failed. | | `string` | Returns the reason only when reading fails or the peer disconnects. | #### tecs.io.TCPSocket:wait Instance Waits up to `timeoutMs` for input or disconnection. Returns false without an error on timeout. This does not consume input; call `read` afterwards. ```teal function tecs.io.TCPSocket.wait( self, timeoutMs: integer ): boolean, string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `TCPSocket` | | | `timeoutMs` | `integer` | The caller supplies 0 to 2147483647 milliseconds or omits it for zero. Zero polls without blocking. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns true when input arrives or the peer disconnects. | | `string` | Returns the reason on failure. Timeout returns false without a reason. | #### tecs.io.TCPSocket:write Instance Queues one chunk for reliable ordered delivery. Inside a system, the call suspends until the local send queue drains. Outside a system, accepting the bytes does not mean the peer has received them; use `pendingWrites` or `drain` when that distinction matters. ```teal function tecs.io.TCPSocket.write(self, bytes: string): boolean, string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `TCPSocket` | | | `bytes` | `string` | The caller supplies at most 16777216 bytes. Larger values raise; an empty string succeeds without queuing data. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns true only when the connection accepts the whole chunk. | | `string` | Returns the reason when the first return is false. | #### tecs.io.TCPSocket:writeFrom Instance Queues a buffer range for reliable ordered delivery. The native send queue copies the range before this call returns, so the caller may mutate or close the buffer afterwards. This avoids constructing an intermediate Lua string but does not bypass the queue's ownership copy. Inside a system, the call suspends until the local send queue drains. ```teal function tecs.io.TCPSocket.writeFrom( self, source: IOBuffer, offset: integer, count: integer ): integer, string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `TCPSocket` | | | `source` | [`IOBuffer`](/modules/io/#tecs.io.Buffer) | The caller supplies an open [`Buffer`](/modules/io/#tecs.io.Buffer). | | `offset` | `integer` | The caller supplies a zero-based source offset or omits it for zero. | | `count` | `integer` | The caller supplies at most 16777216 bytes or omits it for the remainder. Invalid ranges raise. | ##### Returns | Type | Description | | --- | --- | | `integer` | Returns the number of bytes accepted. | | `string` | Returns the reason when the first return is nil. | #### tecs.io.TCPSocket:writeView Instance Queues a retained byte view without constructing an intermediate string. The native send queue copies the range before this call returns. ```teal function tecs.io.TCPSocket.writeView( self, source: types.ByteView, offset: integer, count: integer ): integer, string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `TCPSocket` | | | `source` | [`types.ByteView`](/modules/io/#tecs.io.ByteView) | The caller supplies an open [`ByteView`](/modules/io/#tecs.io.ByteView). | | `offset` | `integer` | The caller supplies a zero-based source offset or omits it for zero. | | `count` | `integer` | The caller supplies at most 16777216 bytes or omits it for the remainder. Invalid ranges raise. | ##### Returns | Type | Description | | --- | --- | | `integer` | Returns the number of bytes accepted. | | `string` | Returns the reason when the first return is nil. | ### tecs.io.UDPPacket record A `UDPPacket` owns one received UDP datagram and its source address. Closing it releases both the address and byte buffer and remains safe to repeat. ```teal record tecs.io.UDPPacket is Closeable address: Address port: integer bytes: IOBuffer close: function(self): boolean, string end ``` #### Interfaces | Interface | | --- | | [`Closeable`](/modules/#tecs.Closeable) | #### tecs.io.UDPPacket.address field Read-only. Networking sets `address` when receiving the packet. The packet owns it until `close`. ```teal tecs.io.UDPPacket.address: Address ``` #### tecs.io.UDPPacket.port field Read-only. Networking sets `port` to the remote source port when receiving the packet. ```teal tecs.io.UDPPacket.port: integer ``` #### tecs.io.UDPPacket.bytes field Read-only. Networking sets `bytes` to one caller-owned [`Buffer`](/modules/io/#tecs.io.Buffer) containing the complete datagram. A zero-length datagram uses an empty buffer. `close` releases it. ```teal tecs.io.UDPPacket.bytes: IOBuffer ``` #### tecs.io.UDPPacket:close Instance ```teal function tecs.io.UDPPacket.close(self): boolean, string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `UDPPacket` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | | | `string` | | ### tecs.io.UDPSocket record A `UDPSocket` sends and receives UDP packets. Closing it releases the socket and remains safe to repeat. The socket permits one suspended send, receive, or wait call at a time. ```teal record tecs.io.UDPSocket is Closeable port: integer sourceHost: string sourcePort: integer close: function(self): boolean, string isClosed: function(self): boolean receive: function(self): UDPPacket, string receiveInto: function( self, destination: IOBuffer, offset: integer, maxBytes: integer ): integer, string send: function( self, address: Address, port: integer, bytes: string ): boolean, string source: function(self): Address, string wait: function(self, timeoutMs: integer): boolean, string end ``` #### Interfaces | Interface | | --- | | [`Closeable`](/modules/#tecs.Closeable) | #### tecs.io.UDPSocket.port field Read-only. Networking sets `port` from the value passed to `bind` and never changes it. Zero remains zero. ```teal tecs.io.UDPSocket.port: integer ``` #### tecs.io.UDPSocket.sourceHost field Read-only. Networking sets `sourceHost` to the numeric address of the sender on each successful `receiveInto` and leaves it nil until the first one. `receive` reports its sender through the packet instead and leaves this field alone. ```teal tecs.io.UDPSocket.sourceHost: string ``` #### tecs.io.UDPSocket.sourcePort field Read-only. Networking sets `sourcePort` to the remote port of the sender on each successful `receiveInto` and leaves it nil until the first one. ```teal tecs.io.UDPSocket.sourcePort: integer ``` #### tecs.io.UDPSocket:close Instance ```teal function tecs.io.UDPSocket.close(self): boolean, string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `UDPSocket` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | | | `string` | | #### tecs.io.UDPSocket:isClosed Instance Returns whether `close` has released this socket. ```teal function tecs.io.UDPSocket.isClosed(self): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `UDPSocket` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns true after `close`. | #### tecs.io.UDPSocket:receive Instance Takes one packet, waiting cooperatively inside a system. Inside a system, the call suspends until a packet is ready. Outside a system it blocks the caller. The packet owns its source address, which the caller closes. ```teal function tecs.io.UDPSocket.receive(self): UDPPacket, string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `UDPSocket` | | ##### Returns | Type | Description | | --- | --- | | [`UDPPacket`](/modules/io/#tecs.io.UDPPacket) | Returns one caller-owned complete packet, or nil when none waits or reception fails. | | `string` | Returns the reason only when reception fails. | #### tecs.io.UDPSocket:receiveInto Instance Takes one packet directly into reusable FFI memory. The call writes at a zero-based offset and extends the buffer through the last byte received. It suspends a system or blocks an ordinary caller until a datagram arrives, and it allocates no packet, buffer, or address. A datagram longer than the requested ceiling keeps the leading bytes and loses the remainder, which is what the platform does. A zero-length datagram returns zero and leaves the buffer's contents unchanged. Each success replaces `sourceHost` and `sourcePort`. Call `source` for an owned [`Address`](/modules/io/#tecs.io.Address) to reply to. ```teal function tecs.io.UDPSocket.receiveInto( self, destination: IOBuffer, offset: integer, maxBytes: integer ): integer, string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `UDPSocket` | | | `destination` | [`IOBuffer`](/modules/io/#tecs.io.Buffer) | The caller supplies an open [`Buffer`](/modules/io/#tecs.io.Buffer). | | `offset` | `integer` | The caller supplies a zero-based destination offset or omits it for zero. | | `maxBytes` | `integer` | The caller supplies a ceiling from 0 to 65507 bytes or omits it for 65507. Invalid values raise. A zero ceiling returns zero and consumes no datagram. | ##### Returns | Type | Description | | --- | --- | | `integer` | Returns the bytes received, or nil when reception fails. | | `string` | Returns the reason only when reception fails. | #### tecs.io.UDPSocket:send Instance Sends one packet, waiting cooperatively on backpressure in a system. The call suspends a system or blocks an ordinary caller while the socket cannot accept the datagram. ```teal function tecs.io.UDPSocket.send( self, address: Address, port: integer, bytes: string ): boolean, string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `UDPSocket` | | | `address` | [`Address`](/modules/io/#tecs.io.Address) | The caller supplies an open address from this module. | | `port` | `integer` | The caller supplies a remote port from 1 to 65535. | | `bytes` | `string` | The caller supplies one datagram of at most 65507 bytes. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns true when the network accepts the complete datagram. | | `string` | Returns the reason when the first return is false. | #### tecs.io.UDPSocket:source Instance Returns a newly owned address for the most recent sender. The caller closes the returned address. Both `receive` and `receiveInto` record the sender, and this call allocates the address only when a caller asks for one. ```teal function tecs.io.UDPSocket.source(self): Address, string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `UDPSocket` | | ##### Returns | Type | Description | | --- | --- | | [`Address`](/modules/io/#tecs.io.Address) | Returns a caller-owned address that outlives the socket, or nil when the socket has received no datagram or the call fails. | | `string` | Returns the reason when the first return is nil. | #### tecs.io.UDPSocket:wait Instance Waits up to `timeoutMs` for a packet. Returns false without an error on timeout. This does not consume a packet; call `receive` afterwards. ```teal function tecs.io.UDPSocket.wait( self, timeoutMs: integer ): boolean, string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `UDPSocket` | | | `timeoutMs` | `integer` | The caller supplies 0 to 2147483647 milliseconds or omits it for zero. Zero polls without blocking. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns true when at least one packet waits. | | `string` | Returns the reason on failure. Timeout returns false without a reason. | ### tecs.io.WritableStream interface A `WritableStream` opens writers and supplies whole-destination transfers. ```teal interface tecs.io.WritableStream is Stream, Closeable newWriter: function(self): Writer, string writeAll: function(self, bytes: string): integer, string writeBuffer: function( self, buffer: Buffer, offset: integer, count: integer ): integer, string writeView: function( self, view: ByteView, offset: integer, count: integer ): integer, string end ``` #### Interfaces | Interface | | --- | | [`Stream`](/modules/io/#tecs.io.Stream) | | [`Closeable`](/modules/#tecs.Closeable) | #### tecs.io.WritableStream:newWriter Instance Opens a writer with a new cursor. Opening replaces the destination's previous bytes. ```teal function tecs.io.WritableStream.newWriter(self): Writer, string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `WritableStream` | The writable descriptor. | ##### Returns | Type | Description | | --- | --- | | [`Writer`](/modules/io/#tecs.io.Writer) | Returns a caller-owned writer, or nil when unavailable. | | `string` | Returns the reason when the first return is nil. | #### Examples ```teal local bytes = tecs.io.newBuffer() local destination = bytes:newStream() local writer = assert(destination:newWriter()) assert(writer:write("data")) assert(writer:close()) destination:close() bytes:close() ``` #### tecs.io.WritableStream:writeAll Instance Replaces the destination with a Lua string. The operation opens, finishes, and closes one writer. On a non-replayable stream, this claims its one endpoint. ```teal function tecs.io.WritableStream.writeAll( self, bytes: string ): integer, string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `WritableStream` | The writable descriptor. | | `bytes` | `string` | The caller supplies the complete binary contents. | ##### Returns | Type | Description | | --- | --- | | `integer` | Returns the bytes written. | | `string` | Returns the destination's reason when the first return is nil. | #### Examples ```teal local destination = tecs.io.newFileStream("save.bin") local count = destination:writeAll("save data") print(count) destination:close() ``` #### tecs.io.WritableStream:writeBuffer Instance Replaces the destination with a buffer range. The operation opens, finishes, and closes one writer. On a non-replayable stream, this claims its one endpoint. ```teal function tecs.io.WritableStream.writeBuffer( self, buffer: Buffer, offset: integer, count: integer ): integer, string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `WritableStream` | The writable descriptor. | | `buffer` | [`Buffer`](/modules/io/#tecs.io.Buffer) | The caller keeps the source buffer open and unchanged through the call. | | `offset` | `integer` | The caller supplies a zero-based offset or omits it for zero. | | `count` | `integer` | The caller supplies a byte count or omits it for the remainder. | ##### Returns | Type | Description | | --- | --- | | `integer` | Returns the bytes written. | | `string` | Returns the destination's reason when the first return is nil. | #### Examples ```teal local bytes = tecs.io.newBuffer("save data") local destination = tecs.io.newFileStream("save.bin") local count = destination:writeBuffer(bytes) print(count) destination:close() bytes:close() ``` #### tecs.io.WritableStream:writeView Instance Replaces the destination with a retained immutable byte range. The operation opens, finishes, and closes one writer. On a non-replayable stream, this claims its one endpoint. The caller keeps `view` open through the call. ```teal function tecs.io.WritableStream.writeView( self, view: ByteView, offset: integer, count: integer ): integer, string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `WritableStream` | The writable descriptor. | | `view` | [`ByteView`](/modules/io/#tecs.io.ByteView) | The caller supplies an open [`ByteView`](/modules/io/#tecs.io.ByteView). | | `offset` | `integer` | The caller supplies a zero-based offset or omits it for zero. | | `count` | `integer` | The caller supplies a byte count or omits it for the remainder. | ##### Returns | Type | Description | | --- | --- | | `integer` | Returns the bytes written. | | `string` | Returns the destination's reason when the first return is nil. | ### tecs.io.Writer interface A `Writer` accepts bytes in order and finishes its destination on `close`. ```teal interface tecs.io.Writer is Closeable flush: function(self): boolean, string write: function(self, bytes: string): boolean, string writeFrom: function( self, source: Buffer, offset: integer, count: integer ): integer, string writeView: function( self, source: ByteView, offset: integer, count: integer ): integer, string end ``` #### Interfaces | Interface | | --- | | [`Closeable`](/modules/#tecs.Closeable) | #### tecs.io.Writer:flush Instance Flushes buffered bytes without closing the destination. ```teal function tecs.io.Writer.flush(self): boolean, string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Writer` | The writer to flush. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns whether buffered bytes reached the destination. | | `string` | Returns the destination's reason when the first return is false. | #### Examples ```teal local destination = tecs.io.newFileStream("save.bin") local writer = assert(destination:newWriter()) assert(writer:write("checkpoint")) assert(writer:flush()) assert(writer:close()) destination:close() ``` #### tecs.io.Writer:write Instance Caller-writable. Supplies the operation that writes the next bytes. ```teal function tecs.io.Writer.write(self, bytes: string): boolean, string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Writer` | The writer receiving the bytes. | | `bytes` | `string` | The binary string to append, including any embedded NUL bytes. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns whether every byte reached the destination. | | `string` | Returns the destination's reason when the first return is false. | #### Examples ```teal local bytes = tecs.io.newBuffer() local writer = bytes:newWriter() local wrote , reason = writer:write("data") assert(wrote, reason) assert(writer:close()) print(bytes:getString()) bytes:close() ``` #### tecs.io.Writer:writeFrom Instance Writes a complete buffer range and advances the cursor. ```teal function tecs.io.Writer.writeFrom( self, source: Buffer, offset: integer, count: integer ): integer, string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Writer` | The open writer whose cursor advances. | | `source` | [`Buffer`](/modules/io/#tecs.io.Buffer) | The caller supplies an open source buffer and retains ownership. | | `offset` | `integer` | The caller supplies a zero-based source offset or omits it for zero. | | `count` | `integer` | The caller supplies a byte count or omits it for the remainder. | ##### Returns | Type | Description | | --- | --- | | `integer` | Returns the complete byte count. | | `string` | Returns the destination's reason when the first return is nil. | #### Examples ```teal local source = tecs.io.newBuffer("header:data") local destination = tecs.io.newBuffer() local writer = destination:newWriter() local count , reason = writer:writeFrom(source, 7, 4) assert(count, reason) assert(writer:close()) print(destination:getString()) source:close() destination:close() ``` #### tecs.io.Writer:writeView Instance Writes a complete immutable-view range and advances the cursor. ```teal function tecs.io.Writer.writeView( self, source: ByteView, offset: integer, count: integer ): integer, string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Writer` | The open writer whose cursor advances. | | `source` | [`ByteView`](/modules/io/#tecs.io.ByteView) | The caller supplies an open source view and retains ownership. | | `offset` | `integer` | The caller supplies a zero-based source offset or omits it for zero. | | `count` | `integer` | The caller supplies a byte count or omits it for the remainder. | ##### Returns | Type | Description | | --- | --- | | `integer` | Returns the complete byte count. | | `string` | Returns the destination's reason when the first return is nil. | #### Examples ```teal local source = tecs.io.newBuffer("header:data") local view = source:view(7, 4) local destination = tecs.io.newBuffer() local writer = destination:newWriter() local count , reason = writer:writeView(view) assert(count, reason) assert(writer:close()) print(destination:getString()) view:close() source:close() destination:close() ``` ## Functions ### tecs.io.bind Static Binds a UDP socket. A nil address listens on all local interfaces. ```teal function tecs.io.bind( port: integer, address: Address ): UDPSocket, string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `port` | `integer` | The caller supplies a local port from 0 to 65535. Zero lets the platform choose one but leaves `UDPSocket.port` at zero. | | `address` | [`Address`](/modules/io/#tecs.io.Address) | The caller supplies a local address or omits it for all interfaces. | #### Returns | Type | Description | | --- | --- | | [`UDPSocket`](/modules/io/#tecs.io.UDPSocket) | Returns a caller-owned socket, or nil on failure. | | `string` | Returns the reason when the first return is nil. | ### tecs.io.connect Static Connects to a resolved address. ```teal function tecs.io.connect( address: Address, port: integer ): TCPSocket ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `address` | [`Address`](/modules/io/#tecs.io.Address) | The caller supplies an address from this module. A closed address raises. | | `port` | `integer` | The caller supplies a remote port from 1 to 65535. | #### Returns | Type | Description | | --- | --- | | [`TCPSocket`](/modules/io/#tecs.io.TCPSocket) | Returns a caller-owned connection. The call suspends its system while connecting, or blocks outside a world update. | ### tecs.io.init Static Starts networking for this module. Safe to call repeatedly. `resolve`, `connect`, `listen` and `bind` start it automatically. ```teal function tecs.io.init(): boolean, string ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | `boolean` | Returns true when networking runs. | | `string` | Returns the startup reason when the first return is false. | ### tecs.io.listen Static Binds a TCP listener. A nil address listens on all local interfaces. ```teal function tecs.io.listen( port: integer, address: Address ): TCPListener, string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `port` | `integer` | The caller supplies a local port from 0 to 65535. Zero lets the platform choose one but leaves `TCPListener.port` at zero. | | `address` | [`Address`](/modules/io/#tecs.io.Address) | The caller supplies a local address or omits it for all interfaces. | #### Returns | Type | Description | | --- | --- | | [`TCPListener`](/modules/io/#tecs.io.TCPListener) | Returns a caller-owned listener, or nil on failure. | | `string` | Returns the reason when the first return is nil. | #### Examples ```teal local listener , listenReason = tecs.io.listen(8080) assert(listener, listenReason) -- `accept` and `read` return only when their operation completes. In a normal -- system they park that logical update; in a headless server they block this -- caller. A persistent server belongs in a headless host or an engine-owned -- bounded service instead of waiting forever in a gameplay system. local client , acceptReason = listener:accept() assert(client, acceptReason) local request , readReason = client:read() assert(request, readReason) local body = "Hello from Tecs\n" local response = ( "HTTP/1.1 200 OK\r\nContent-Length: %d\r\n" .. "Content-Type: text/plain\r\nConnection: close\r\n\r\n%s" ):format(#body, body) local sent , sendReason = client:write(response) assert(sent, sendReason) assert(client:drain(1000)) client:close() listener:close() ``` ### tecs.io.pending Static Returns the number of pending network operations and readiness watches. ```teal function tecs.io.pending(): integer ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | `integer` | Returns zero after every operation settles or cancels. | ### tecs.io.resolve Static Resolves a hostname. ```teal function tecs.io.resolve(host: string): Address ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `host` | `string` | The caller supplies a DNS name or numeric address of at most 253 bytes. Empty strings and embedded NUL bytes raise. | #### Returns | Type | Description | | --- | --- | | [`Address`](/modules/io/#tecs.io.Address) | Returns a caller-owned address. The call suspends its system while DNS is pending, or blocks outside a world update. | ### tecs.io.shutdown Static Stops this module's networking instance. It refuses while an address, TCP socket, listener, UDP socket, packet or pending resolution, connection, or readiness watch remains live. ```teal function tecs.io.shutdown(): boolean, string ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | `boolean` | Returns true after shutdown or when networking never started. | | `string` | Returns the live-resource reason when shutdown refuses. | ### tecs.io.transfer Static Transfers every remaining byte between directional endpoints. Lua files satisfy the basic endpoint protocol directly. Tecs readers and writers may additionally provide `readInto` and `writeFrom` fast paths, which this call selects without changing the source code using them. The call borrows both endpoints and closes neither one. ```teal function tecs.io.transfer( source: types.Reader | FILE, destination: types.Writer | FILE ): integer, string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `source` | [`types.Reader`](/modules/io/#tecs.io.Reader) | FILE | The caller supplies an open reader or Lua file positioned at its first byte to transfer. | | `destination` | [`types.Writer`](/modules/io/#tecs.io.Writer) | FILE | The caller supplies an open writer or Lua file positioned where transferred bytes should begin. | #### Returns | Type | Description | | --- | --- | | `integer` | Returns the number of bytes written. | | `string` | Returns the source or destination reason when the first return is nil. | #### Examples ```teal local source = assert(tecs.io.files.open("save.bin")) local destination = assert(tecs.io.files.open("save.copy", "w")) local count , reason = tecs.io.transfer(source, destination) assert(count, reason) source:close() destination:close() ``` --- ## tecs.io.mcp # tecs.io.mcp MCP debug server and tool registry. ## Server lifecycle Set `Application.Config.mcpPort` for the built-in server. A custom host can call `listen`, call `Server:poll` once per frame, and call `Server:destroy` at teardown. The listener accepts loopback connections only. Each call to `poll` runs at most one queued handler. The handler sees a committed world, but it must return quickly because the frame cannot end until it does. ## Custom tools ```teal local placed = world:newQuery({include = {tecs.Transform2D}}) tecs.io.mcp.register({ name = "placed_count", description = "Count entities that carry a Transform2D", inputSchema = {["type"] = "object", properties = {}}, readOnly = true, handler = function(_arguments: {string: any}): {string: any} return {count = placed:count()} end, }) ``` The registry produces both the advertised tool list and dispatch table. Tool handlers can use the world captured when the game registers them. ## World inspection Start with `components_info`, `query`, and `info`. Use `modify` for a partial component update and `set` for a complete value or a missing component. `modify` skips entities without the component and marks changed columns dirty. Prefer these structured tools to `run_lua`; the Lua sandbox limits accidents but does not create a security boundary. ## Crash access The server keeps polling after an uncaught gameplay error. `ping`, `context`, `get_logs`, and `send_event` remain available; tools that touch the world report the stored traceback instead. ## Module contents ### Types | Type | Kind | Description | | --- | --- | --- | | [`Server`](/modules/io/mcp/#tecs.io.mcp.Server) | record | Represents a listening MCP endpoint. | | [`Tool`](/modules/io/mcp/#tecs.io.mcp.Tool) | record | Describes one registered tool and its handler. | | [`ToolHandler`](/modules/io/mcp/#tecs.io.mcp.ToolHandler) | type | Defines the decoded arguments a tool receives and the structured content it returns. | ### Functions | Function | Kind | Description | | --- | --- | --- | | [`crashed`](/modules/io/mcp/#tecs.io.mcp.crashed) | Static | Returns the crash traceback, or nil while the game is healthy. | | [`dispatch`](/modules/io/mcp/#tecs.io.mcp.dispatch) | Static | Runs one JSON-RPC request and returns the response text. | | [`listen`](/modules/io/mcp/#tecs.io.mcp.listen) | Static | Starts the server on port. | | [`register`](/modules/io/mcp/#tecs.io.mcp.register) | Static | Registers a tool. | | [`setCrashed`](/modules/io/mcp/#tecs.io.mcp.setCrashed) | Static | Records a crash. | | [`tools`](/modules/io/mcp/#tecs.io.mcp.tools) | Static | Returns every registered tool in registration order. | ## Types ### tecs.io.mcp.Server record Represents a listening MCP endpoint. It answers only while something calls `poll`. ```teal record tecs.io.mcp.Server port: integer destroy: function(self) poll: function(self): boolean end ``` #### tecs.io.mcp.Server.port field Read-only. This is the bound TCP port, which is 7100 when `listen` was given none. ```teal tecs.io.mcp.Server.port: integer ``` #### tecs.io.mcp.Server:destroy Instance Releases the server. Safe to call more than once. One way: the port is given up and the server cannot be made to listen again. The tool registry is module-wide and is left untouched, so a later `listen` answers the same tools. ```teal function tecs.io.mcp.Server.destroy(self) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Server` | | ##### Returns None. #### tecs.io.mcp.Server:poll Instance Answers at most one tool call. Call once per frame. ```teal function tecs.io.mcp.Server.poll(self): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Server` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | True when one tool handler ran. Protocol-only traffic does not count. False when no tool call is waiting or after destruction. | ### tecs.io.mcp.Tool record Describes one registered tool and its handler. ```teal record tecs.io.mcp.Tool name: string description: string inputSchema: {string: any} outputSchema: {string: any} handler: ToolHandler readOnly: boolean destructive: boolean whenCrashed: boolean end ``` #### tecs.io.mcp.Tool.name field Caller-writable. This is the name an agent calls and the registry key. Established MCP tool names follow the protocol ecosystem's spelling, including `run_lua` and `send_event`, so they use snake_case where the rest of this tree uses camelCase. This Do not rename this compatibility surface. Required. ```teal tecs.io.mcp.Tool.name: string ``` #### tecs.io.mcp.Tool.description field Caller-writable. This is one line shown to the agent alongside the name. Optional; nil is sent as an empty string rather than omitted. ```teal tecs.io.mcp.Tool.description: string ``` #### tecs.io.mcp.Tool.inputSchema field Caller-writable. This JSON Schema describes the arguments and is sent verbatim in the tool list. ```teal tecs.io.mcp.Tool.inputSchema: {string: any} ``` #### tecs.io.mcp.Tool.outputSchema field Caller-writable. This JSON Schema describes the structured content the handler returns, so an agent knows the shape of an answer before it makes the call. Optional; omitting it advertises no output schema rather than an empty one. ```teal tecs.io.mcp.Tool.outputSchema: {string: any} ``` #### tecs.io.mcp.Tool.handler field Caller-writable. This handler runs synchronously inside `poll`. Required; its absence raises at registration rather than at call time. ```teal tecs.io.mcp.Tool.handler: ToolHandler ``` #### tecs.io.mcp.Tool.readOnly field Caller-writable. This declares whether the tool only reads state so an agent can judge a call before making it. ```teal tecs.io.mcp.Tool.readOnly: boolean ``` #### tecs.io.mcp.Tool.destructive field Caller-writable. This declares whether a call changes state that a caller would not want changed without asking. Optional, false when nil. Advisory, like `readOnly`: the server reports both values to the agent and enforces neither, so a tool declared read-only may still write. ```teal tecs.io.mcp.Tool.destructive: boolean ``` #### tecs.io.mcp.Tool.whenCrashed field Caller-writable. This allows the tool to remain callable after the game has crashed. False by default, because a crashed world cannot answer anything about itself, and a tool that read it would return nonsense rather than an error. ```teal tecs.io.mcp.Tool.whenCrashed: boolean ``` ### tecs.io.mcp.ToolHandler type Defines the decoded arguments a tool receives and the structured content it returns. ```teal type tecs.io.mcp.ToolHandler = function({string: any}): {string: any} ``` ## Functions ### tecs.io.mcp.crashed Static Returns the crash traceback, or nil while the game is healthy. ```teal function tecs.io.mcp.crashed(): string ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | `string` | nil means no code has recorded a crash. Nothing here polls the game to confirm that it remains healthy. | ### tecs.io.mcp.dispatch Static Runs one JSON-RPC request and returns the response text. Lets tests exercise the protocol without a socket, which covers most of the failure surface. The three methods answered, `initialize`, `tools/list` and `tools/call`, use the names from the MCP specification, which this tree must not rename. Anything else is a method-not-found error. When a handler raises, the server sets `isError` for the agent and continues. A handler may return nil, which produces an empty `structuredContent`. ```teal function tecs.io.mcp.dispatch(text: string): string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `text` | `string` | One JSON-RPC request object. The server does not support a batch, which decodes as a JSON array with no `method` and produces an invalid-request error rather than dispatching each element. | #### Returns | Type | Description | | --- | --- | | `string` | Always a response as JSON text, never nil, and never a raise, since the response reports bad input in-band. The server still answers a request with no `id`, omitting the `id` field from the response. | ### tecs.io.mcp.listen Static Starts the server on `port`. ```teal function tecs.io.mcp.listen(port: integer): mcp.Server ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `port` | `integer` | Defaults to 7100. A port already in use raises rather than selecting another port, since a debugger could not find a moved server. | #### Returns | Type | Description | | --- | --- | | [`mcp.Server`](/modules/io/mcp/#tecs.io.mcp.Server) | Listening, but answering nothing until something calls `poll`. | ### tecs.io.mcp.register Static Registers a tool. Re-registering a name replaces it. Replacing keeps the name's original position in the list, so the order a client sees is first-registration order rather than last. ```teal function tecs.io.mcp.register(tool: mcp.Tool) ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `tool` | [`mcp.Tool`](/modules/io/mcp/#tecs.io.mcp.Tool) | Stored by reference and never copied, so mutating the table afterwards changes what the server dispatches. Registration is what publishes the tool list, so mutating a registered table in place does not change what a connected agent is told. The call requires `name` and `handler` and raises when either is absent; the rest may be nil. | #### Returns None. ### tecs.io.mcp.setCrashed Static Records a crash. Every subsequent world-touching call reports it. ```teal function tecs.io.mcp.setCrashed(traceback: string) ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `traceback` | `string` | Stored as given and handed back verbatim by `crashed`, and sent as the whole body of the error a blocked tool answers with. Passing nil clears the crashed state and lets every tool run again. | #### Returns None. ### tecs.io.mcp.tools Static Returns every registered tool in registration order. ```teal function tecs.io.mcp.tools(): {mcp.Tool} ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | `{`[`mcp.Tool`](/modules/io/mcp/#tecs.io.mcp.Tool)`}` | A fresh list each call, so the caller may keep it. The [`Tool`](/modules/io/mcp/#tecs.io.mcp.Tool) values in it are the registered tables themselves rather than copies, so writing to one writes to the registry. | --- ## tecs.io.watcher # tecs.io.watcher Polls loaded content and dispatches changes to reload handlers. ```teal local watcher = tecs.io.watcher if watcher.isSupported() then watcher.on( "document", function(change: watcher.Change) print("reload " .. change.path) end ) watcher.install({intervalSeconds = 0.25}) end ``` An [`Application`](/modules/Application/) with `watch` in its configuration installs and advances the watcher. Settled changes enter a bounded queue and handlers run in the scheduler-owned `Ingress` phase. They never interrupt a half-completed phase, and a handler may use the same cooperative operations as an ordinary system. Game systems, plugins, and update functions do not poll. The watcher scans only paths recorded by `tecs.io.files.read` and the asset loaders. It waits for a changed file's size and modification time to settle before dispatching it. Empty, missing, and directory paths do not dispatch. Repeated queued changes to one path coalesce. `Config.capacity` bounds distinct pending paths; an overflow is logged and retried by a later scan instead of growing memory without limit. A handler error reaches the log and does not replace the current resource. Release builds report `isSupported() == false`. ## Module contents ### Types | Type | Kind | Description | | --- | --- | --- | | [`Change`](/modules/io/watcher/#tecs.io.watcher.Change) | record | Change describes one settled content change. | | [`ChangeHandler`](/modules/io/watcher/#tecs.io.watcher.ChangeHandler) | type | ChangeHandler receives one settled change. | | [`Config`](/modules/io/watcher/#tecs.io.watcher.Config) | record | Config controls polling frequency, settling and path scope. | ### Functions | Function | Kind | Description | | --- | --- | --- | | [`dispatched`](/modules/io/watcher/#tecs.io.watcher.dispatched) | Static | Returns the changes dispatched since install. | | [`install`](/modules/io/watcher/#tecs.io.watcher.install) | Static | Starts watching loaded content. | | [`isInstalled`](/modules/io/watcher/#tecs.io.watcher.isInstalled) | Static | Returns whether the watcher is running. | | [`isSupported`](/modules/io/watcher/#tecs.io.watcher.isSupported) | Static | Returns whether this build supports hot reload. | | [`kinds`](/modules/io/watcher/#tecs.io.watcher.kinds) | Static | Returns registered content kinds in sorted order. | | [`on`](/modules/io/watcher/#tecs.io.watcher.on) | Static | Registers the handler for a content kind. | | [`scan`](/modules/io/watcher/#tecs.io.watcher.scan) | Static | Looks at every watched path once, whatever the interval says. | | [`uninstall`](/modules/io/watcher/#tecs.io.watcher.uninstall) | Static | Stops watching and forgets every path's state. | | [`unsettled`](/modules/io/watcher/#tecs.io.watcher.unsettled) | Static | Returns unsettled changed paths in sorted order. | | [`watching`](/modules/io/watcher/#tecs.io.watcher.watching) | Static | Returns watched paths in sorted order. | ## Types ### tecs.io.watcher.Change record `Change` describes one settled content change. ```teal record tecs.io.watcher.Change path: string kind: string end ``` #### tecs.io.watcher.Change.path field Read-only. The watcher sets `path` to the changed loaded path before it calls the handler. ```teal tecs.io.watcher.Change.path: string ``` #### tecs.io.watcher.Change.kind field Read-only. The watcher sets `kind` to the content kind recorded by the original load. Built-in loaders use `"image"`, `"sound"`, `"font"`, `"shader"` or `"document"`; a custom loader supplies its own kind to `tecs.io.files.read`. ```teal tecs.io.watcher.Change.kind: string ``` ### tecs.io.watcher.ChangeHandler type `ChangeHandler` receives one settled change. ```teal type tecs.io.watcher.ChangeHandler = function(Change) ``` ### tecs.io.watcher.Config record `Config` controls polling frequency, settling and path scope. ```teal record tecs.io.watcher.Config intervalSeconds: number settle: integer root: string capacity: integer end ``` #### tecs.io.watcher.Config.intervalSeconds field Caller-writable. Sets the delay between automatic polls in seconds. It defaults to 0.5. Direct calls to `scan` ignore this delay. ```teal tecs.io.watcher.Config.intervalSeconds: number ``` #### tecs.io.watcher.Config.settle field Caller-writable. Requires this many additional matching scans after a change is first observed. It defaults to 1, so one scan observes the change and the next confirms it. Zero dispatches immediately. ```teal tecs.io.watcher.Config.settle: integer ``` #### tecs.io.watcher.Config.root field Caller-writable. Limits watched paths to this root. It defaults to the content root. ```teal tecs.io.watcher.Config.root: string ``` #### tecs.io.watcher.Config.capacity field Caller-writable. Limits settled changes waiting for the next logical update. Defaults to 1024. Repeated changes to one path coalesce while queued. ```teal tecs.io.watcher.Config.capacity: integer ``` ## Functions ### tecs.io.watcher.dispatched Static Returns the changes dispatched since `install`. ```teal function tecs.io.watcher.dispatched(): integer ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | `integer` | Returns the cumulative handler count. | ### tecs.io.watcher.install Static Starts watching loaded content. The watcher records the current state of loaded paths and dispatches only later changes. It raises when the build lacks hot-reload support. ```teal function tecs.io.watcher.install(config: watcher.Config) ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `config` | [`watcher.Config`](/modules/io/watcher/#tecs.io.watcher.Config) | The caller supplies polling options or omits them for the documented defaults. | #### Returns None. #### Examples ```teal local watcher = tecs.io.watcher local function enableReloading() watcher.on( "document", function(change: watcher.Change) print("changed " .. change.path) end ) watcher.install({intervalSeconds = 0.25}) end -- The Application advances the watcher and dispatches the handler from its -- Ingress phase. Call watcher.uninstall() to stop it. enableReloading() ``` ### tecs.io.watcher.isInstalled Static Returns whether the watcher is running. ```teal function tecs.io.watcher.isInstalled(): boolean ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | `boolean` | Returns true after `install` and before `uninstall`. | ### tecs.io.watcher.isSupported Static Returns whether this build supports hot reload. ```teal function tecs.io.watcher.isSupported(): boolean ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | `boolean` | Returns true when this build can install the watcher. | ### tecs.io.watcher.kinds Static Returns registered content kinds in sorted order. ```teal function tecs.io.watcher.kinds(): {string} ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | `{string}` | Returns a fresh caller-owned list. | ### tecs.io.watcher.on Static Registers the handler for a content kind. Re-registering a kind replaces its handler. Nil removes it. ```teal function tecs.io.watcher.on( kind: string, handler: watcher.ChangeHandler ) ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `kind` | `string` | The caller supplies the content kind to register. | | `handler` | [`watcher.ChangeHandler`](/modules/io/watcher/#tecs.io.watcher.ChangeHandler) | The caller supplies the replacement handler or nil to remove the current one. | #### Returns None. ### tecs.io.watcher.scan Static Looks at every watched path once, whatever the interval says. Call this directly when a headless tool or test controls poll timing. ```teal function tecs.io.watcher.scan(): integer ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | `integer` | Returns how many handlers ran, or zero when no watcher runs. | ### tecs.io.watcher.uninstall Static Stops watching and forgets every path's state. Registered handlers remain available for a later installation. ```teal function tecs.io.watcher.uninstall() ``` #### Arguments None. #### Returns None. ### tecs.io.watcher.unsettled Static Returns unsettled changed paths in sorted order. ```teal function tecs.io.watcher.unsettled(): {string} ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | `{string}` | Returns a fresh caller-owned list. | ### tecs.io.watcher.watching Static Returns watched paths in sorted order. ```teal function tecs.io.watcher.watching(): {string} ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | `{string}` | Returns a fresh caller-owned list. | --- ## tecs.log # tecs.log Named, leveled logging. Create one named logger per subsystem. Logger names form the stable filtering surface: ```teal local logger = tecs.log.get("game.combat") logger:info("wave %d spawned %d enemies", 3, 12) logger:setLevel(tecs.log.DEBUG) ``` Messages go to the platform log destination, including logcat on Android and Console on Apple platforms. [`Logger`](/modules/log/#tecs.log.Logger) methods check priority before formatting. Pass raw arguments rather than formatting first. Guard work that exists only to build a message: ```teal local snapshot = {frame = 12, player = "Alice"} if logger:enabled(tecs.log.DEBUG) then logger:debug("snapshot: %s", tecs.data.encodeJSON(snapshot)) end ``` ## JSON Lines output Add a queryable file beside the platform destination: ```teal local path = tecs.io.files.writablePath("game.jsonl") if not tecs.log.openFile(path) then logger:error("could not open %s", path) end ``` With file output open, the earlier `info` call writes a line like: ```json {"time":12.345,"level":"INFO","logger":"game.combat","message":"wave 3 spawned 12 enemies"} ``` Each line records elapsed platform time, level, logger, and message. `closeFile` stops file output and leaves the platform destination active. ## Module contents ### Types | Type | Kind | Description | | --- | --- | --- | | [`Logger`](/modules/log/#tecs.log.Logger) | record | Represents a named log category. | ### Functions | Function | Kind | Description | | --- | --- | --- | | [`categoryName`](/modules/log/#tecs.log.categoryName) | Static | Returns the name registered for a category. | | [`closeFile`](/modules/log/#tecs.log.closeFile) | Static | Stops writing to the file. | | [`filePath`](/modules/log/#tecs.log.filePath) | Static | Returns the open log file path, or nil when no file is open. | | [`get`](/modules/log/#tecs.log.get) | Static | Returns the logger for name, creating it the first time. | | [`loggers`](/modules/log/#tecs.log.loggers) | Static | Returns every registered logger name. | | [`openFile`](/modules/log/#tecs.log.openFile) | Static | Writes every accepted line to path as JSON Lines and truncates the file. | | [`setLevel`](/modules/log/#tecs.log.setLevel) | Static | Sets the minimum priority for every category. | ### Values | Value | Type | Description | | --- | --- | --- | | [`CRITICAL`](/modules/log/#tecs.log.CRITICAL) | `integer` | Read-only. Contains the critical priority. | | [`DEBUG`](/modules/log/#tecs.log.DEBUG) | `integer` | Read-only. Contains the debug priority. | | [`ERROR`](/modules/log/#tecs.log.ERROR) | `integer` | Read-only. Contains the error priority. | | [`INFO`](/modules/log/#tecs.log.INFO) | `integer` | Read-only. Contains the informational priority. | | [`TRACE`](/modules/log/#tecs.log.TRACE) | `integer` | Read-only. Contains the trace priority. | | [`VERBOSE`](/modules/log/#tecs.log.VERBOSE) | `integer` | Read-only. Contains the verbose priority. | | [`WARN`](/modules/log/#tecs.log.WARN) | `integer` | Read-only. Contains the warning priority. | ## Types ### tecs.log.Logger record Represents a named log category. ```teal record tecs.log.Logger name: string category: integer critical: function(self, message: string, ...: any) debug: function(self, message: string, ...: any) enabled: function(self, priority: integer): boolean error: function(self, message: string, ...: any) info: function(self, message: string, ...: any) level: function(self): integer setLevel: function(self, priority: integer) trace: function(self, message: string, ...: any) verbose: function(self, message: string, ...: any) warn: function(self, message: string, ...: any) end ``` #### tecs.log.Logger.name field Read-only. Contains the registered logger name written to the platform log. ```teal tecs.log.Logger.name: string ``` #### tecs.log.Logger.category field Engine-owned. Contains the category allocated during this run. The number may change across runs, so game code should filter by `name`. ```teal tecs.log.Logger.category: integer ``` #### tecs.log.Logger:critical Instance Logs at `CRITICAL`. See `trace` for the formatting rules. ```teal function tecs.log.Logger.critical(self, message: string, ...: any) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Logger` | | | `message` | `string` | | | `...` | `any` | | ##### Returns None. #### tecs.log.Logger:debug Instance Logs at `DEBUG`. See `trace` for the formatting rules. ```teal function tecs.log.Logger.debug(self, message: string, ...: any) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Logger` | | | `message` | `string` | | | `...` | `any` | | ##### Returns None. #### tecs.log.Logger:enabled Instance Reports whether the logger would emit a message at `priority`. Call this directly to guard work that only exists to build a log message, such as serializing a table. The level methods already guard themselves. ```teal function tecs.log.Logger.enabled(self, priority: integer): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Logger` | | | `priority` | `integer` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | | #### tecs.log.Logger:error Instance Logs at `ERROR`. See `trace` for the formatting rules. Logging is all this does; it neither raises nor returns anything. ```teal function tecs.log.Logger.error(self, message: string, ...: any) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Logger` | | | `message` | `string` | | | `...` | `any` | | ##### Returns None. #### tecs.log.Logger:info Instance Logs at `INFO`. See `trace` for the formatting rules. ```teal function tecs.log.Logger.info(self, message: string, ...: any) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Logger` | | | `message` | `string` | | | `...` | `any` | | ##### Returns None. #### tecs.log.Logger:level Instance Returns the minimum priority this logger emits. ```teal function tecs.log.Logger.level(self): integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Logger` | | ##### Returns | Type | Description | | --- | --- | | `integer` | Returns the platform priority until something sets one. | #### tecs.log.Logger:setLevel Instance Sets the minimum priority this logger emits. ```teal function tecs.log.Logger.setLevel(self, priority: integer) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Logger` | | | `priority` | `integer` | One of the constants on `log`. The logger emits messages at this priority and drops anything lower. | ##### Returns None. #### tecs.log.Logger:trace Instance Logs at `TRACE` and applies `string.format` specifiers only when emitted. The method checks the level before formatting, so a filtered call costs a load and a compare and the arguments are never touched. Building those arguments is still the caller's cost; guard that with `enabled`. ```teal function tecs.log.Logger.trace(self, message: string, ...: any) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Logger` | | | `message` | `string` | A `string.format` template when arguments follow, and a literal otherwise, so a `%` in a plain message is safe. | | `...` | `any` | Formatted into `message`. A wrong count or a wrong specifier raises from `string.format`, and only on the calls that are actually emitted. | ##### Returns None. #### tecs.log.Logger:verbose Instance Logs at `VERBOSE`. See `trace` for the formatting rules. ```teal function tecs.log.Logger.verbose(self, message: string, ...: any) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Logger` | | | `message` | `string` | | | `...` | `any` | | ##### Returns None. #### tecs.log.Logger:warn Instance Logs at `WARN`. See `trace` for the formatting rules. ```teal function tecs.log.Logger.warn(self, message: string, ...: any) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Logger` | | | `message` | `string` | | | `...` | `any` | | ##### Returns None. ## Functions ### tecs.log.categoryName Static Returns the name registered for a category. ```teal function tecs.log.categoryName(category: integer): string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `category` | `integer` | | #### Returns | Type | Description | | --- | --- | | `string` | Never returns nil. An unregistered category comes back as `"category:"` rather than as an error. | ### tecs.log.closeFile Static Stops writing to the file. Does nothing when no file is open, so it is safe to call on a shutdown path that does not know whether one was ever asked for. ```teal function tecs.log.closeFile() ``` #### Arguments None. #### Returns None. ### tecs.log.filePath Static Returns the open log file path, or nil when no file is open. ```teal function tecs.log.filePath(): string ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | `string` | The path as it was given, not resolved against the working directory, so a relative one is only meaningful to a reader in the same place the game ran. | ### tecs.log.get Static Returns the logger for `name`, creating it the first time. Names are the unit of filtering, so they are what a subsystem should use: `tecs.gfx`, `tecs.debug.events`. Each maps to one SDL category, which is what `SDL_SetLogPriority` takes. ```teal function tecs.log.get(name: string): log.Logger ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `name` | `string` | Compared byte for byte, so two spellings are two categories with two levels. | #### Returns | Type | Description | | --- | --- | | [`log.Logger`](/modules/log/#tecs.log.Logger) | Returns the same object on every call for a name. A new logger starts at the platform's default priority. | ### tecs.log.loggers Static Returns every registered logger name. ```teal function tecs.log.loggers(): {string} ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | `{string}` | A fresh array each call, sorted, holding only names something has already asked `get` for. A subsystem that has not run yet is absent. | ### tecs.log.openFile Static Writes every accepted line to `path` as JSON Lines and truncates the file. The platform destination keeps receiving the human-readable form, so The platform still sends its human-readable stream to `tail -f`, logcat, or Console.app. The file makes a log queryable after the fact: seek to an offset, read to the end. Calling this while another file is open closes the previous file and moves subsequent lines to `path`. ```teal function tecs.log.openFile(path: string): boolean ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `path` | `string` | Truncated if it exists, created if it does not, including when it is the file already open, so this is not a way to check one is. Directories are not made. | #### Returns | Type | Description | | --- | --- | | `boolean` | False when the file cannot open. In that case nothing else changes and a file already open keeps receiving lines. | ### tecs.log.setLevel Static Sets the minimum priority for every category. ```teal function tecs.log.setLevel(priority: integer) ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `priority` | `integer` | Overrides every per-logger level already set, so this is a reset and not a floor. | #### Returns None. ## Values ### tecs.log.CRITICAL variable Read-only. Contains the critical priority. ```teal tecs.log.CRITICAL: integer ``` ### tecs.log.DEBUG variable Read-only. Contains the debug priority. ```teal tecs.log.DEBUG: integer ``` ### tecs.log.ERROR variable Read-only. Contains the error priority. ```teal tecs.log.ERROR: integer ``` ### tecs.log.INFO variable Read-only. Contains the informational priority. ```teal tecs.log.INFO: integer ``` ### tecs.log.TRACE variable Read-only. Contains the trace priority. ```teal tecs.log.TRACE: integer ``` ### tecs.log.VERBOSE variable Read-only. Contains the verbose priority. ```teal tecs.log.VERBOSE: integer ``` ### tecs.log.WARN variable Read-only. Contains the warning priority. ```teal tecs.log.WARN: integer ``` --- ## tecs.math # tecs.math Angle math and the two-dimensional geometry beneath it. `wrapAngle` and `deltaAngle` operate on angles rather than vectors, so they remain directly on `tecs.math`. Vector and point operations live under `tecs.math.vec2`; native procedural fields live under `tecs.math.noise`. ```teal local correction = tecs.math.deltaAngle( rotation, targetRotation ) rotation = tecs.math.wrapAngle(rotation + correction * response) local directionX, directionY = tecs.math.vec2.normalize( targetX - x, targetY - y ) ``` Every angle uses radians. Both functions return a value in `[-pi, pi)`, so an exact positive half-turn becomes negative pi. Naming `tecs.math` does not load either subordinate module. Reading `vec2` or `noise` loads that child once and returns its module table. ## Module contents ### Submodules | Submodule | Description | | --- | --- | | [`tecs.math.noise`](/modules/math/noise/) | Native procedural scalar fields with configurable algorithms and fractal composition | | [`tecs.math.vec2`](/modules/math/vec2/) | Allocation-free two-dimensional vector and point operations over separate coordinates | ### Functions | Function | Kind | Description | | --- | --- | --- | | [`deltaAngle`](/modules/math/#tecs.math.deltaAngle) | Static | Computes the shortest signed turn from one angle to another. | | [`wrapAngle`](/modules/math/#tecs.math.wrapAngle) | Static | Wraps an angle to one turn centered on zero. | ## Functions ### tecs.math.deltaAngle Static Computes the shortest signed turn from one angle to another. ```teal function tecs.math.deltaAngle(from: number, to: number): number ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `from` | `number` | Starting angle in radians. | | `to` | `number` | Destination angle in radians. | #### Returns | Type | Description | | --- | --- | | `number` | `to - from` wrapped to `[-pi, pi)`. An exact half-turn is negative pi. | #### Examples Finds the shortest turn toward a target rotation. ```teal assert(tecs.math.deltaAngle(0, math.pi) == -math.pi) ``` ### tecs.math.wrapAngle Static Wraps an angle to one turn centered on zero. ```teal function tecs.math.wrapAngle(radians: number): number ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `radians` | `number` | A finite angle in radians. | #### Returns | Type | Description | | --- | --- | | `number` | The equivalent angle in `[-pi, pi)`, so positive pi becomes negative pi. | #### Examples Keeps a rotation within one turn before storing it. ```teal assert(tecs.math.wrapAngle(math.pi) == -math.pi) ``` --- ## tecs.math.noise # tecs.math.noise Native procedural scalar fields in two and three dimensions. ```teal local terrain = tecs.math.noise.new({ algorithm = "perlin", fractal = "fbm", seed = 1234, frequency = 0.01, octaves = 4, }) local height = terrain:sample2(worldX, worldY) ``` A field is immutable after construction. The seed, algorithm, and options therefore determine every sample, and sampling carries no snapshot state. `fill2` crosses into native code once for a complete row-major grid and reuses native scratch storage when the same field fills another grid. FastNoise Lite supplies the algorithms. Its output is stable within the version Tecs pins, but upgrading that dependency may change a generated field. Persist the seed and options rather than generated values only when accepting that versioned behavior. ## Module contents ### Constructors | Constructor | Description | | --- | --- | | [`new`](/modules/math/noise/#tecs.math.noise.new) | Creates an immutable native procedural field. | ### Types | Type | Kind | Description | | --- | --- | --- | | [`Algorithm`](/modules/math/noise/#tecs.math.noise.Algorithm) | enum | Selects the native scalar-field algorithm. | | [`Fractal`](/modules/math/noise/#tecs.math.noise.Fractal) | enum | Selects how the field combines frequency octaves. | | [`Noise`](/modules/math/noise/#tecs.math.noise.Noise) | record | Represents an immutable native procedural field. | | [`Options`](/modules/math/noise/#tecs.math.noise.Options) | record | Configures one immutable native noise field. | ## Constructors ### tecs.math.noise.new Static Creates an immutable native procedural field. Invalid enum names, non-finite numeric options, a seed outside the signed 32-bit range, an octave count outside `1..32`, or a weighted strength outside `[0, 1]` raise at the call site. ```teal function tecs.math.noise.new(options: Options): Noise ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `options` | [`Options`](/modules/math/noise/#tecs.math.noise.Options) | The field configuration. Nil takes every documented default. | #### Returns | Type | Description | | --- | --- | | [`Noise`](/modules/math/noise/#tecs.math.noise.Noise) | A field whose native allocation the Lua collector releases. | ## Types ### tecs.math.noise.Algorithm enum Selects the native scalar-field algorithm. ```teal enum tecs.math.noise.Algorithm "cellular" "perlin" "simplex" "simplexSmooth" "value" "valueCubic" end ``` ### tecs.math.noise.Fractal enum Selects how the field combines frequency octaves. ```teal enum tecs.math.noise.Fractal "fbm" "none" "pingPong" "ridged" end ``` ### tecs.math.noise.Noise record Represents an immutable native procedural field. ```teal record tecs.math.noise.Noise fill2: function( self, values: {number}, width: integer, height: integer, originX: number, originY: number, stepX: number, stepY: number ): {number} sample2: function(self, x: number, y: number): number sample3: function(self, x: number, y: number, z: number): number end ``` #### tecs.math.noise.Noise:fill2 Instance Fills a list with a row-major two-dimensional grid. Rows advance by `stepY`; columns advance by `stepX`. The method replaces indices `1..width*height`, removes any old trailing entries, and retains the table and native scratch allocation for reuse. ```teal function tecs.math.noise.Noise.fill2( self, values: {number}, width: integer, height: integer, originX: number, originY: number, stepX: number, stepY: number ): {number} ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Noise` | | | `values` | `{number}` | The caller-owned list the method fills and returns. | | `width` | `integer` | The number of columns, as an integer in `[0, 2147483647]`. | | `height` | `integer` | The number of rows, as an integer in `[0, 2147483647]`. | | `originX` | `number` | The first column's coordinate. Defaults to zero. | | `originY` | `number` | The first row's coordinate. Defaults to zero. | | `stepX` | `number` | The coordinate distance between columns. Defaults to one. | | `stepY` | `number` | The coordinate distance between rows. Defaults to one. | ##### Returns | Type | Description | | --- | --- | | `{number}` | `values` itself after replacing its contents. | #### tecs.math.noise.Noise:sample2 Instance Samples the field at a two-dimensional coordinate. ```teal function tecs.math.noise.Noise.sample2( self, x: number, y: number ): number ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Noise` | | | `x` | `number` | A finite horizontal coordinate, scaled by the field's frequency. | | `y` | `number` | A finite vertical coordinate, scaled by the field's frequency. | ##### Returns | Type | Description | | --- | --- | | `number` | A value in `[-1, 1]`. | #### tecs.math.noise.Noise:sample3 Instance Samples the field at a three-dimensional coordinate. ```teal function tecs.math.noise.Noise.sample3( self, x: number, y: number, z: number ): number ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Noise` | | | `x` | `number` | A finite horizontal coordinate, scaled by the field's frequency. | | `y` | `number` | A finite vertical coordinate, scaled by the field's frequency. | | `z` | `number` | A finite depth coordinate, scaled by the field's frequency. | ##### Returns | Type | Description | | --- | --- | | `number` | A value in `[-1, 1]`. | ### tecs.math.noise.Options record Configures one immutable native noise field. ```teal record tecs.math.noise.Options algorithm: Algorithm fractal: Fractal seed: integer frequency: number octaves: integer lacunarity: number gain: number weightedStrength: number pingPongStrength: number end ``` #### tecs.math.noise.Options.algorithm field Caller-writable. Selects the field algorithm. Defaults to `"perlin"`. ```teal tecs.math.noise.Options.algorithm: Algorithm ``` #### tecs.math.noise.Options.fractal field Caller-writable. Selects octave composition. Defaults to `"none"`. ```teal tecs.math.noise.Options.fractal: Fractal ``` #### tecs.math.noise.Options.seed field Caller-writable. Selects the repeatable field. Defaults to `0x5EED1234`. ```teal tecs.math.noise.Options.seed: integer ``` #### tecs.math.noise.Options.frequency field Caller-writable. Scales every coordinate before sampling. Defaults to `0.01`. ```teal tecs.math.noise.Options.frequency: number ``` #### tecs.math.noise.Options.octaves field Caller-writable. Sets the octave count for a fractal field. Defaults to three. ```teal tecs.math.noise.Options.octaves: integer ``` #### tecs.math.noise.Options.lacunarity field Caller-writable. Multiplies frequency between octaves. Defaults to two. ```teal tecs.math.noise.Options.lacunarity: number ``` #### tecs.math.noise.Options.gain field Caller-writable. Multiplies amplitude between octaves. Defaults to `0.5`. ```teal tecs.math.noise.Options.gain: number ``` #### tecs.math.noise.Options.weightedStrength field Caller-writable. Lets one octave's value influence the next octave's amplitude. Defaults to zero and accepts values in `[0, 1]`. ```teal tecs.math.noise.Options.weightedStrength: number ``` #### tecs.math.noise.Options.pingPongStrength field Caller-writable. Sets the fold strength for `"pingPong"`. Defaults to two. ```teal tecs.math.noise.Options.pingPongStrength: number ``` --- ## tecs.math.vec2 # tecs.math.vec2 Allocation-free two-dimensional vector and point math. `vec2` takes separate coordinates and returns multiple values, so a system can update an archetype column without allocating vector objects: ```teal local record Homing is tecs.ecs.Component targetX: number targetY: number speed: number end tecs.ecs.newFFIComponent({ name = "Homing", container = Homing, fields = { {"targetX", "float"}, {"targetY", "float"}, {"speed", "float"}, }, }) return tecs.newApplication({ plugin = function(world: tecs.World) local homing = world:newQuery({ include = {tecs.Transform2D, Homing}, }) world:addSystem({ name = "game.Homing", phase = tecs.ecs.phases.Update, run = function(dt: number) for archetype, length in homing:iter() do local transforms = archetype:getMut( tecs.Transform2D ) local targets = archetype:get(Homing) for row = 1, length do local transform = transforms[row] local target = targets[row] transform.x, transform.y = tecs.math.vec2.moveTowards( transform.x, transform.y, target.targetX, target.targetY, target.speed * dt ) transform.rotation = tecs.math.vec2.angle( target.targetX - transform.x, target.targetY - transform.y ) end end end, }) end, }) ``` The writable [`Transform2D`](/modules/ecs/#tecs.ecs.Transform2D) column comes from `getMut`; the read-only target column comes from `get`. Taking both through `getMut` would dirty `Homing` every frame and defeat consumers that skip clean columns. ## Coordinates and boundaries Every angle uses radians. A positive quarter turn maps `(1, 0)` to `(0, 1)`, which appears clockwise when screen y grows downward. Operations without a unique geometric answer return stable values. `normalize(0, 0)`, either angle-between function with a zero vector, and `project` onto a zero axis return zero. `reflect` with a zero normal returns the original vector. `lerp` does not clamp, and `moveTowards` never overshoots. ## Module contents ### Functions | Function | Kind | Description | | --- | --- | --- | | [`add`](/modules/math/vec2/#tecs.math.vec2.add) | Static | Adds two vectors. | | [`angle`](/modules/math/vec2/#tecs.math.vec2.angle) | Static | Computes a vector's direction. | | [`angleBetween`](/modules/math/vec2/#tecs.math.vec2.angleBetween) | Static | Computes the smaller unsigned angle between two vectors. | | [`cross`](/modules/math/vec2/#tecs.math.vec2.cross) | Static | Computes the scalar two-dimensional cross product. | | [`distance`](/modules/math/vec2/#tecs.math.vec2.distance) | Static | Computes the distance between two points. | | [`distanceSquared`](/modules/math/vec2/#tecs.math.vec2.distanceSquared) | Static | Computes squared distance without taking a square root. | | [`dot`](/modules/math/vec2/#tecs.math.vec2.dot) | Static | Computes the dot product of two vectors. | | [`length`](/modules/math/vec2/#tecs.math.vec2.length) | Static | Computes vector length. | | [`lengthSquared`](/modules/math/vec2/#tecs.math.vec2.lengthSquared) | Static | Computes squared vector length without taking a square root. | | [`lerp`](/modules/math/vec2/#tecs.math.vec2.lerp) | Static | Interpolates linearly between two points. | | [`moveTowards`](/modules/math/vec2/#tecs.math.vec2.moveTowards) | Static | Moves a point toward another by at most a given distance. | | [`normalize`](/modules/math/vec2/#tecs.math.vec2.normalize) | Static | Produces a unit vector in the same direction. | | [`project`](/modules/math/vec2/#tecs.math.vec2.project) | Static | Projects a vector onto another vector. | | [`reflect`](/modules/math/vec2/#tecs.math.vec2.reflect) | Static | Reflects a vector across the line perpendicular to a normal. | | [`rotate`](/modules/math/vec2/#tecs.math.vec2.rotate) | Static | Rotates a vector around the origin. | | [`scale`](/modules/math/vec2/#tecs.math.vec2.scale) | Static | Multiplies both coordinates by a scalar. | | [`signedAngleBetween`](/modules/math/vec2/#tecs.math.vec2.signedAngleBetween) | Static | Computes the smaller signed angle from one vector to another. | | [`subtract`](/modules/math/vec2/#tecs.math.vec2.subtract) | Static | Subtracts the second vector from the first. | ## Functions ### tecs.math.vec2.add Static Adds two vectors. ```teal function tecs.math.vec2.add( ax: number, ay: number, bx: number, by: number ): number, number ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `ax` | `number` | First vector's x coordinate. | | `ay` | `number` | First vector's y coordinate. | | `bx` | `number` | Second vector's x coordinate. | | `by` | `number` | Second vector's y coordinate. | #### Returns | Type | Description | | --- | --- | | `number` | The sum's x coordinate. | | `number` | The sum's y coordinate. | #### Examples Combines a position with a per-frame velocity step. ```teal local x, y = tecs.math.vec2.add(1, 2, 3, 4) assert(x == 4 and y == 6) ``` ### tecs.math.vec2.angle Static Computes a vector's direction. ```teal function tecs.math.vec2.angle(x: number, y: number): number ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `x` | `number` | Vector's x coordinate. | | `y` | `number` | Vector's y coordinate. | #### Returns | Type | Description | | --- | --- | | `number` | `atan2(y, x)` in `[-pi, pi]`. A zero vector returns zero. | #### Examples Converts a direction into a rotation for a sprite. ```teal assert(tecs.math.vec2.angle(0, 1) == math.pi / 2) ``` ### tecs.math.vec2.angleBetween Static Computes the smaller unsigned angle between two vectors. ```teal function tecs.math.vec2.angleBetween( ax: number, ay: number, bx: number, by: number ): number ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `ax` | `number` | First vector's x coordinate. | | `ay` | `number` | First vector's y coordinate. | | `bx` | `number` | Second vector's x coordinate. | | `by` | `number` | Second vector's y coordinate. | #### Returns | Type | Description | | --- | --- | | `number` | Radians in `[0, pi]`. If either vector is zero, returns zero. | #### Examples Measures the unsigned separation of two facing directions. ```teal assert(tecs.math.vec2.angleBetween(1, 0, 0, 1) == math.pi / 2) ``` ### tecs.math.vec2.cross Static Computes the scalar two-dimensional cross product. ```teal function tecs.math.vec2.cross( ax: number, ay: number, bx: number, by: number ): number ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `ax` | `number` | First vector's x coordinate. | | `ay` | `number` | First vector's y coordinate. | | `bx` | `number` | Second vector's x coordinate. | | `by` | `number` | Second vector's y coordinate. | #### Returns | Type | Description | | --- | --- | | `number` | `ax * by - ay * bx`; positive puts the second vector on the positive rotation side of the first. | #### Examples Determines which side of a direction another vector lies on. ```teal assert(tecs.math.vec2.cross(1, 0, 0, 1) == 1) ``` ### tecs.math.vec2.distance Static Computes the distance between two points. ```teal function tecs.math.vec2.distance( ax: number, ay: number, bx: number, by: number ): number ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `ax` | `number` | First point's x coordinate. | | `ay` | `number` | First point's y coordinate. | | `bx` | `number` | Second point's x coordinate. | | `by` | `number` | Second point's y coordinate. | #### Returns | Type | Description | | --- | --- | | `number` | The nonnegative Euclidean distance between the points. | #### Examples Measures how far apart two positions are. ```teal assert(tecs.math.vec2.distance(1, 2, 4, 6) == 5) ``` ### tecs.math.vec2.distanceSquared Static Computes squared distance without taking a square root. Prefer this to `distance` for radius tests. ```teal function tecs.math.vec2.distanceSquared( ax: number, ay: number, bx: number, by: number ): number ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `ax` | `number` | First point's x coordinate. | | `ay` | `number` | First point's y coordinate. | | `bx` | `number` | Second point's x coordinate. | | `by` | `number` | Second point's y coordinate. | #### Returns | Type | Description | | --- | --- | | `number` | The squared Euclidean distance between the points. | #### Examples Tests whether a target lies inside a radius without a square root. ```teal assert(tecs.math.vec2.distanceSquared(1, 2, 4, 6) == 25) ``` ### tecs.math.vec2.dot Static Computes the dot product of two vectors. ```teal function tecs.math.vec2.dot( ax: number, ay: number, bx: number, by: number ): number ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `ax` | `number` | First vector's x coordinate. | | `ay` | `number` | First vector's y coordinate. | | `bx` | `number` | Second vector's x coordinate. | | `by` | `number` | Second vector's y coordinate. | #### Returns | Type | Description | | --- | --- | | `number` | `ax * bx + ay * by`. | #### Examples Tests how closely two directions face each other. ```teal assert(tecs.math.vec2.dot(2, 3, 4, 5) == 23) ``` ### tecs.math.vec2.length Static Computes vector length. ```teal function tecs.math.vec2.length(x: number, y: number): number ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `x` | `number` | Vector's x coordinate. | | `y` | `number` | Vector's y coordinate. | #### Returns | Type | Description | | --- | --- | | `number` | The nonnegative Euclidean length. | #### Examples Measures a velocity's speed. ```teal assert(tecs.math.vec2.length(3, 4) == 5) ``` ### tecs.math.vec2.lengthSquared Static Computes squared vector length without taking a square root. Prefer this to `length` when comparing magnitudes: compare the answer with the other distance squared, such as `radius * radius`. ```teal function tecs.math.vec2.lengthSquared(x: number, y: number): number ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `x` | `number` | Vector's x coordinate. | | `y` | `number` | Vector's y coordinate. | #### Returns | Type | Description | | --- | --- | | `number` | `x * x + y * y`. | #### Examples Compares speed without taking a square root. ```teal assert(tecs.math.vec2.lengthSquared(3, 4) == 25) ``` ### tecs.math.vec2.lerp Static Interpolates linearly between two points. ```teal function tecs.math.vec2.lerp( ax: number, ay: number, bx: number, by: number, t: number ): number, number ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `ax` | `number` | Starting point's x coordinate. | | `ay` | `number` | Starting point's y coordinate. | | `bx` | `number` | Destination point's x coordinate. | | `by` | `number` | Destination point's y coordinate. | | `t` | `number` | Unclamped interpolation amount. Zero returns the start, one returns the destination, and values outside that interval extrapolate. | #### Returns | Type | Description | | --- | --- | | `number` | The interpolated x coordinate. | | `number` | The interpolated y coordinate. | #### Examples Blends halfway between two positions. ```teal local x, y = tecs.math.vec2.lerp(2, 4, 6, 8, 0.5) assert(x == 4 and y == 6) ``` ### tecs.math.vec2.moveTowards Static Moves a point toward another by at most a given distance. A frame-rate-independent chase passes the speed for this frame: ```teal function tecs.math.vec2.moveTowards( x: number, y: number, targetX: number, targetY: number, maxDistance: number ): number, number ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `x` | `number` | Starting point's x coordinate. | | `y` | `number` | Starting point's y coordinate. | | `targetX` | `number` | Destination point's x coordinate. | | `targetY` | `number` | Destination point's y coordinate. | | `maxDistance` | `number` | Greatest distance to travel. A nonpositive value leaves the starting point unchanged. | #### Returns | Type | Description | | --- | --- | | `number` | The moved x coordinate. Reaching the destination returns it exactly and never overshoots. | | `number` | The moved y coordinate, with the same endpoint rule. | #### Examples Moves toward a target without overshooting it. ```teal local x, y = tecs.math.vec2.moveTowards(0, 0, 3, 4, 2) assert(x == 1.2 and y == 1.6) ``` ### tecs.math.vec2.normalize Static Produces a unit vector in the same direction. ```teal function tecs.math.vec2.normalize(x: number, y: number): number, number ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `x` | `number` | Vector's x coordinate. | | `y` | `number` | Vector's y coordinate. | #### Returns | Type | Description | | --- | --- | | `number` | The normalized x coordinate. A zero vector returns zero because it has no direction. | | `number` | The normalized y coordinate, with the same zero-vector rule. | #### Examples Builds a unit direction toward a target. ```teal local x, y = tecs.math.vec2.normalize(3, 4) assert(x == 0.6 and y == 0.8) ``` ### tecs.math.vec2.project Static Projects a vector onto another vector. The projection axis may have any length: ```teal function tecs.math.vec2.project( x: number, y: number, ontoX: number, ontoY: number ): number, number ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `x` | `number` | The vector's x coordinate. | | `y` | `number` | The vector's y coordinate. | | `ontoX` | `number` | Projection axis's x coordinate. | | `ontoY` | `number` | Projection axis's y coordinate. | #### Returns | Type | Description | | --- | --- | | `number` | The projected x coordinate. The axis may have any length; a zero axis returns zero. | | `number` | The projected y coordinate, with the same zero-axis rule. | #### Examples Keeps only the component of a velocity along an axis. ```teal local x, y = tecs.math.vec2.project(3, 4, 1, 0) assert(x == 3 and y == 0) ``` ### tecs.math.vec2.reflect Static Reflects a vector across the line perpendicular to a normal. The normal may have any length: ```teal function tecs.math.vec2.reflect( x: number, y: number, normalX: number, normalY: number ): number, number ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `x` | `number` | The vector's x coordinate. | | `y` | `number` | The vector's y coordinate. | | `normalX` | `number` | Surface normal's x coordinate. | | `normalY` | `number` | Surface normal's y coordinate. | #### Returns | Type | Description | | --- | --- | | `number` | The reflected x coordinate. The normal may have any length; a zero normal returns the original x coordinate. | | `number` | The reflected y coordinate, with the same zero-normal rule. | #### Examples Bounces a velocity from a surface normal. ```teal local x, y = tecs.math.vec2.reflect(1, -1, 0, 1) assert(x == 1 and y == 1) ``` ### tecs.math.vec2.rotate Static Rotates a vector around the origin. ```teal function tecs.math.vec2.rotate( x: number, y: number, radians: number ): number, number ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `x` | `number` | Vector's x coordinate. | | `y` | `number` | Vector's y coordinate. | | `radians` | `number` | Rotation in radians. A positive quarter turn maps `(1, 0)` to `(0, 1)`. | #### Returns | Type | Description | | --- | --- | | `number` | The rotated x coordinate. | | `number` | The rotated y coordinate. | #### Examples Rotates a facing direction by a quarter turn. ```teal local x, y = tecs.math.vec2.rotate(1, 0, math.pi / 2) assert(math.abs(x) < 0.000001 and math.abs(y - 1) < 0.000001) ``` ### tecs.math.vec2.scale Static Multiplies both coordinates by a scalar. ```teal function tecs.math.vec2.scale( x: number, y: number, factor: number ): number, number ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `x` | `number` | Vector's x coordinate. | | `y` | `number` | Vector's y coordinate. | | `factor` | `number` | Multiplier applied to both coordinates. | #### Returns | Type | Description | | --- | --- | | `number` | The scaled x coordinate. | | `number` | The scaled y coordinate. | #### Examples Turns a unit direction into a movement step. ```teal local x, y = tecs.math.vec2.scale(2, 3, 4) assert(x == 8 and y == 12) ``` ### tecs.math.vec2.signedAngleBetween Static Computes the smaller signed angle from one vector to another. Use the answer directly as the shortest directional correction: ```teal function tecs.math.vec2.signedAngleBetween( ax: number, ay: number, bx: number, by: number ): number ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `ax` | `number` | Starting vector's x coordinate. | | `ay` | `number` | Starting vector's y coordinate. | | `bx` | `number` | Destination vector's x coordinate. | | `by` | `number` | Destination vector's y coordinate. | #### Returns | Type | Description | | --- | --- | | `number` | Radians in `[-pi, pi)`, positive in the same sense as `rotate`. If either vector is zero, returns zero; an exact half-turn is negative pi. | #### Examples Chooses the shortest directional correction toward a target. ```teal assert(tecs.math.vec2.signedAngleBetween(1, 0, 0, 1) == math.pi / 2) ``` ### tecs.math.vec2.subtract Static Subtracts the second vector from the first. ```teal function tecs.math.vec2.subtract( ax: number, ay: number, bx: number, by: number ): number, number ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `ax` | `number` | First vector's x coordinate. | | `ay` | `number` | First vector's y coordinate. | | `bx` | `number` | Second vector's x coordinate. | | `by` | `number` | Second vector's y coordinate. | #### Returns | Type | Description | | --- | --- | | `number` | The difference's x coordinate. | | `number` | The difference's y coordinate. | #### Examples Finds the offset from a position to a target. ```teal local x, y = tecs.math.vec2.subtract(5, 4, 1, 2) assert(x == 4 and y == 2) ``` --- ## tecs.physics # tecs.physics Rapier 2D as part of the world. Install the plugin, spawn an entity with a [`Transform2D`](/modules/ecs/#tecs.ecs.Transform2D), and attach a body: ```teal world:addPlugin(tecs.physics.plugin({gravity = {0.0, 980.0}})) local ground = world:spawn(tecs.Transform2D(320, 460)) tecs.physics.attach( world, ground, {["type"] = "static", halfWidth = 320, halfHeight = 20} ) local crate = world:spawn(tecs.Transform2D(320, 100)) tecs.physics.attach( world, crate, {halfWidth = 16, halfHeight = 16, friction = 0.4} ) tecs.physics.applyImpulse(world, crate, 400.0, 0.0) ``` The solver writes each body's [`Transform2D`](/modules/ecs/#tecs.ecs.Transform2D) back, so nothing else has to move the entity. `detach` takes a body out of the solve without despawning its entity. Public positions, extents, linear velocities, impulses, forces, and gravity use pixels. Angles and angular velocities use radians. Rapier solves in meters through the `pixelsPerMeter` conversion. [`Body`](/modules/physics/#tecs.physics.Body) and [`Collider`](/modules/physics/#tecs.physics.Collider) declare what the game requested. [`Motion`](/modules/physics/#tecs.physics.Motion) preserves simulation state across pauses. Engine-owned [`RigidBody`](/modules/physics/#tecs.physics.RigidBody) keeps the attachment visible to tools; ordinary game code should ignore it. A secondary collider lives on its own entity and relates back through [`ColliderOf`](/modules/physics/#tecs.physics.ColliderOf). `FixedUpdate` steps each ECS world's simulation from `physics.of`. A fixed timestep makes replay and snapshot continuation deterministic, and snapshots preserve the complete Rapier state. ## Module contents ### Types | Type | Kind | Description | | --- | --- | --- | | [`Body`](/modules/physics/#tecs.physics.Body) | record | Body declares a body's simulation behavior. | | [`BodyOptions`](/modules/physics/#tecs.physics.BodyOptions) | record | BodyOptions describes one complete body or secondary collider declaration. | | [`Collider`](/modules/physics/#tecs.physics.Collider) | record | Collider declares a body's geometry, material and collision filtering. | | [`ContactBegin`](/modules/physics/#tecs.physics.ContactBegin) | record | ContactBegin reports the start of contact between two bodies. | | [`ContactEnd`](/modules/physics/#tecs.physics.ContactEnd) | record | ContactEnd reports the end of contact between two bodies. | | [`Motion`](/modules/physics/#tecs.physics.Motion) | record | Motion preserves a body's saved velocity in pixels and radians per second. | | [`PhysicsOptions`](/modules/physics/#tecs.physics.PhysicsOptions) | record | PhysicsOptions configures a Rapier world before plugin installation. | | [`QueryOptions`](/modules/physics/#tecs.physics.QueryOptions) | record | QueryOptions filters the shapes a raycast may hit. | | [`RaycastHit`](/modules/physics/#tecs.physics.RaycastHit) | record | RaycastHit reports where a ray first met a collider. | | [`RigidBody`](/modules/physics/#tecs.physics.RigidBody) | record | A RigidBody identifies one live body in a Rapier simulation. | | [`SensorBegin`](/modules/physics/#tecs.physics.SensorBegin) | record | SensorBegin reports a body entering a sensor. | | [`SensorEnd`](/modules/physics/#tecs.physics.SensorEnd) | record | SensorEnd reports a body leaving a sensor. | ### Functions | Function | Kind | Description | | --- | --- | --- | | [`angularVelocity`](/modules/physics/#tecs.physics.angularVelocity) | Static | Reads angular velocity in radians per second. | | [`applyForce`](/modules/physics/#tecs.physics.applyForce) | Static | Applies a continuous force at the body's center and wakes it. | | [`applyForceAt`](/modules/physics/#tecs.physics.applyForceAt) | Static | Applies a continuous force at a world-space point and wakes the body. | | [`applyImpulse`](/modules/physics/#tecs.physics.applyImpulse) | Static | Pushes a body once, at its center of mass. | | [`applyImpulseAt`](/modules/physics/#tecs.physics.applyImpulseAt) | Static | Applies an impulse at a world-space point, so it spins the body as well as moving it. | | [`applyImpulseTo`](/modules/physics/#tecs.physics.applyImpulseTo) | Static | Applies a center-of-mass impulse to a RigidBody row. | | [`applyTorque`](/modules/physics/#tecs.physics.applyTorque) | Static | Applies torque and wakes the body. | | [`attach`](/modules/physics/#tecs.physics.attach) | Static | Declares a body on entity. | | [`attachCollider`](/modules/physics/#tecs.physics.attachCollider) | Static | Adds another collider to a declared body. | | [`detach`](/modules/physics/#tecs.physics.detach) | Static | Removes a body's declaration. | | [`hasBody`](/modules/physics/#tecs.physics.hasBody) | Static | Returns whether Rapier is still solving a body for entity. | | [`isAwake`](/modules/physics/#tecs.physics.isAwake) | Static | Returns whether Rapier currently considers the body awake. | | [`of`](/modules/physics/#tecs.physics.of) | Static | Returns the Rapier simulation installed in world. | | [`plugin`](/modules/physics/#tecs.physics.plugin) | Static | Installs the simulation and its sync. | | [`raycast`](/modules/physics/#tecs.physics.raycast) | Static | Casts a segment and returns its nearest collider, in pixels. | | [`setAngularVelocity`](/modules/physics/#tecs.physics.setAngularVelocity) | Static | Sets angular velocity in radians per second and wakes the body. | | [`setAwake`](/modules/physics/#tecs.physics.setAwake) | Static | Wakes or sleeps a body. | | [`setVelocity`](/modules/physics/#tecs.physics.setVelocity) | Static | Sets a body's velocity, in pixels per second, and wakes it. | | [`teleport`](/modules/physics/#tecs.physics.teleport) | Static | Teleports a body and immediately updates its Transform2D. | | [`velocity`](/modules/physics/#tecs.physics.velocity) | Static | Reads a body's velocity, live from Rapier, in pixels per second. | ### Values | Value | Type | Description | | --- | --- | --- | | [`ColliderOf`](/modules/physics/#tecs.physics.ColliderOf) | [`Component`](/modules/ecs/#tecs.ecs.Component) | Read-only. ColliderOf relates a secondary collider to its owning body. | | [`pixelsPerMeter`](/modules/physics/#tecs.physics.pixelsPerMeter) | `number` | Read-only. pixelsPerMeter reports the fixed conversion between public pixels and Rapier meters. | ## Types ### tecs.physics.Body record `Body` declares a body's simulation behavior. Read-only. `Body` names the caller-writable body component. ```teal record tecs.physics.Body is Component kind: number fixedRotation: number isBullet: number sleepEnabled: number gravityScale: number linearDamping: number angularDamping: number end ``` #### Interfaces | Interface | | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | #### tecs.physics.Body.kind field Caller-writable. The caller sets `kind` to 0 for static, 1 for kinematic or 2 for dynamic through `getMut`. Physics applies the change during the next fixed update. ```teal tecs.physics.Body.kind: number ``` #### tecs.physics.Body.fixedRotation field Caller-writable. The caller sets `fixedRotation` to 1 to lock rotation or 0 to unlock it. Physics applies the change during the next fixed update. ```teal tecs.physics.Body.fixedRotation: number ``` #### tecs.physics.Body.isBullet field Caller-writable. The caller sets `isBullet` to 1 for continuous collision or 0 for discrete collision. Physics applies the change during the next fixed update. ```teal tecs.physics.Body.isBullet: number ``` #### tecs.physics.Body.sleepEnabled field Caller-writable. The caller sets `sleepEnabled` to 1 to permit sleeping or 0 to keep solving. Physics applies the change during the next fixed update. ```teal tecs.physics.Body.sleepEnabled: number ``` #### tecs.physics.Body.gravityScale field Caller-writable. The caller sets `gravityScale` through `getMut`. Physics applies it during the next fixed update. ```teal tecs.physics.Body.gravityScale: number ``` #### tecs.physics.Body.linearDamping field Caller-writable. The caller sets `linearDamping` through `getMut`. Physics applies it during the next fixed update. ```teal tecs.physics.Body.linearDamping: number ``` #### tecs.physics.Body.angularDamping field Caller-writable. The caller sets `angularDamping` through `getMut`. Physics applies it during the next fixed update. ```teal tecs.physics.Body.angularDamping: number ``` ### tecs.physics.BodyOptions record `BodyOptions` describes one complete body or secondary collider declaration. `attachCollider` reads only the shape, material, filter and offset fields. It ignores body-level fields because a secondary collider has no damping or gravity of its own. ```teal global record tecs.physics.BodyOptions type: string halfWidth: number halfHeight: number radius: number density: number friction: number restitution: number fixedRotation: boolean gravityScale: number linearDamping: number angularDamping: number sleepEnabled: boolean isBullet: boolean categoryBits: number maskBits: number isSensor: boolean offsetX: number offsetY: number capsuleLength: number end ``` #### tecs.physics.BodyOptions.type field Caller-writable. The caller sets `type` before `attach` reads it to `"static"`, `"kinematic"` or `"dynamic"`. It defaults to dynamic, and an unrecognized string raises. ```teal tecs.physics.BodyOptions.type: string ``` #### tecs.physics.BodyOptions.halfWidth field Caller-writable. The caller sets `halfWidth` in pixels before `attach` reads it. A 32-pixel square uses 16. It defaults to 8. ```teal tecs.physics.BodyOptions.halfWidth: number ``` #### tecs.physics.BodyOptions.halfHeight field Caller-writable. The caller sets `halfHeight` in pixels with `halfWidth`. It defaults to 8. ```teal tecs.physics.BodyOptions.halfHeight: number ``` #### tecs.physics.BodyOptions.radius field Caller-writable. The caller sets `radius` in pixels before `attach` reads it. Naming it selects a circle over box extents. A capsule requires a positive value. ```teal tecs.physics.BodyOptions.radius: number ``` #### tecs.physics.BodyOptions.density field Caller-writable. The caller sets `density` as mass per unit area before `attach` reads it. It defaults to Rapier's 1.0. Rapier treats zero mass as infinite mass. ```teal tecs.physics.BodyOptions.density: number ``` #### tecs.physics.BodyOptions.friction field Caller-writable. The caller sets `friction` before `attach` reads it. It defaults to Rapier's 0.6. A contact takes the geometric mean of the two shapes', so zero on either one is a frictionless slide however rough the other is. ```teal tecs.physics.BodyOptions.friction: number ``` #### tecs.physics.BodyOptions.restitution field Caller-writable. The caller sets `restitution` before `attach` reads it. It defaults to Rapier's 0.0. One produces a perfectly elastic bounce, and above one a body gains energy on every contact. A contact takes the larger of the two shapes', so the bouncier surface decides and a deadened one cannot damp it. ```teal tecs.physics.BodyOptions.restitution: number ``` #### tecs.physics.BodyOptions.fixedRotation field Caller-writable. The caller sets `fixedRotation` before `attach` reads it. True locks the angle; it defaults to false. ```teal tecs.physics.BodyOptions.fixedRotation: boolean ``` #### tecs.physics.BodyOptions.gravityScale field Caller-writable. The caller sets `gravityScale` before `attach` reads it. It defaults to 1. Zero floats, and negative falls upward. ```teal tecs.physics.BodyOptions.gravityScale: number ``` #### tecs.physics.BodyOptions.linearDamping field Caller-writable. The caller sets `linearDamping` before `attach` reads it. It defaults to 0 and uses Rapier's damping term. ```teal tecs.physics.BodyOptions.linearDamping: number ``` #### tecs.physics.BodyOptions.angularDamping field Caller-writable. The caller sets `angularDamping` before `attach` reads it. It defaults to 0 and uses Rapier's damping term. ```teal tecs.physics.BodyOptions.angularDamping: number ``` #### tecs.physics.BodyOptions.sleepEnabled field Caller-writable. The caller sets `sleepEnabled` before `attach` reads it. It defaults to true, and only an explicit false disables it. Rapier stops solving a sleeping body until contact or an explicit wake. ```teal tecs.physics.BodyOptions.sleepEnabled: boolean ``` #### tecs.physics.BodyOptions.isBullet field Caller-writable. The caller sets `isBullet` before `attach` reads it to enable continuous collision. It defaults to false. ```teal tecs.physics.BodyOptions.isBullet: boolean ``` #### tecs.physics.BodyOptions.categoryBits field Caller-writable. The caller sets `categoryBits` before `attach` reads it. It defaults to 1 and uses the low 32 Rapier collision-group bits. ```teal tecs.physics.BodyOptions.categoryBits: number ``` #### tecs.physics.BodyOptions.maskBits field Caller-writable. The caller sets `maskBits` before `attach` reads it. It defaults to every category. Two shapes touch only when each mask names the other category. ```teal tecs.physics.BodyOptions.maskBits: number ``` #### tecs.physics.BodyOptions.isSensor field Caller-writable. The caller sets `isSensor` before `attach` reads it. True reports overlap events without collision response. It defaults to false. ```teal tecs.physics.BodyOptions.isSensor: boolean ``` #### tecs.physics.BodyOptions.offsetX field Caller-writable. The caller sets `offsetX` in body-frame pixels before `attach` reads it, so it turns with the body. It defaults to 0. ```teal tecs.physics.BodyOptions.offsetX: number ``` #### tecs.physics.BodyOptions.offsetY field Caller-writable. The caller sets `offsetY` with `offsetX` before `attach` reads it. It defaults to 0. ```teal tecs.physics.BodyOptions.offsetY: number ``` #### tecs.physics.BodyOptions.capsuleLength field Caller-writable. The caller sets `capsuleLength` before `attach` reads it. It selects a vertical capsule and gives the distance between end centers, excluding the caps. `radius` supplies the positive cap radius. ```teal tecs.physics.BodyOptions.capsuleLength: number ``` ### tecs.physics.Collider record `Collider` declares a body's geometry, material and collision filtering. Write its fields through `getMut`; physics applies changes during the next fixed update. Read-only. `Collider` names the caller-writable collider component. ```teal record tecs.physics.Collider is Component shape: number halfWidth: number halfHeight: number radius: number length: number offsetX: number offsetY: number density: number friction: number restitution: number categoryBits: number maskBits: number isSensor: number end ``` #### Interfaces | Interface | | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | #### tecs.physics.Collider.shape field Caller-writable. The caller sets `shape` to 0 for box, 1 for circle or 2 for capsule through `getMut`. Physics applies the change during the next fixed update. ```teal tecs.physics.Collider.shape: number ``` #### tecs.physics.Collider.halfWidth field Caller-writable. The caller sets `halfWidth` in pixels for a box. ```teal tecs.physics.Collider.halfWidth: number ``` #### tecs.physics.Collider.halfHeight field Caller-writable. The caller sets `halfHeight` in pixels for a box. ```teal tecs.physics.Collider.halfHeight: number ``` #### tecs.physics.Collider.radius field Caller-writable. The caller sets `radius` in pixels for a circle or capsule. ```teal tecs.physics.Collider.radius: number ``` #### tecs.physics.Collider.length field Caller-writable. The caller sets `length` to the pixel distance between capsule end centers. ```teal tecs.physics.Collider.length: number ``` #### tecs.physics.Collider.offsetX field Caller-writable. The caller sets `offsetX` in body-frame pixels. ```teal tecs.physics.Collider.offsetX: number ``` #### tecs.physics.Collider.offsetY field Caller-writable. The caller sets `offsetY` in body-frame pixels. ```teal tecs.physics.Collider.offsetY: number ``` #### tecs.physics.Collider.density field Caller-writable. The caller sets `density` as mass per unit area. ```teal tecs.physics.Collider.density: number ``` #### tecs.physics.Collider.friction field Caller-writable. The caller sets `friction` as Coulomb friction. ```teal tecs.physics.Collider.friction: number ``` #### tecs.physics.Collider.restitution field Caller-writable. The caller sets `restitution` for bounce. ```teal tecs.physics.Collider.restitution: number ``` #### tecs.physics.Collider.categoryBits field Caller-writable. The caller sets `categoryBits` to the shape's collision categories. ```teal tecs.physics.Collider.categoryBits: number ``` #### tecs.physics.Collider.maskBits field Caller-writable. The caller sets `maskBits` to categories this shape may contact. ```teal tecs.physics.Collider.maskBits: number ``` #### tecs.physics.Collider.isSensor field Caller-writable. The caller sets `isSensor` to 1 for overlap events without collision response, or 0 for a solid shape. ```teal tecs.physics.Collider.isSensor: number ``` ### tecs.physics.ContactBegin record `ContactBegin` reports the start of contact between two bodies. Read-only. `ContactBegin` names the contact-start event. ```teal record tecs.physics.ContactBegin is events.Event entityA: integer entityB: integer end ``` #### Interfaces | Interface | | --- | | [`events.Event`](/modules/events/#tecs.events.Event) | #### tecs.physics.ContactBegin.entityA field Read-only. Physics sets `entityA` to one contact body before emitting the event. ```teal tecs.physics.ContactBegin.entityA: integer ``` #### tecs.physics.ContactBegin.entityB field Read-only. Physics sets `entityB` to the other contact body. ```teal tecs.physics.ContactBegin.entityB: integer ``` ### tecs.physics.ContactEnd record `ContactEnd` reports the end of contact between two bodies. Read-only. `ContactEnd` names the contact-end event. ```teal record tecs.physics.ContactEnd is events.Event entityA: integer entityB: integer end ``` #### Interfaces | Interface | | --- | | [`events.Event`](/modules/events/#tecs.events.Event) | #### tecs.physics.ContactEnd.entityA field Read-only. Physics sets `entityA` to one former contact body before emitting the event. ```teal tecs.physics.ContactEnd.entityA: integer ``` #### tecs.physics.ContactEnd.entityB field Read-only. Physics sets `entityB` to the other former contact body. ```teal tecs.physics.ContactEnd.entityB: integer ``` ### tecs.physics.Motion record `Motion` preserves a body's saved velocity in pixels and radians per second. Read-only. `Motion` names the engine-updated pause and snapshot state. ```teal record tecs.physics.Motion is Component vx: number vy: number omega: number end ``` #### Interfaces | Interface | | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | #### tecs.physics.Motion.vx field Read-only. Physics stores horizontal velocity in pixels per second during pause, snapshot and restore operations. Use `velocity` for live state. ```teal tecs.physics.Motion.vx: number ``` #### tecs.physics.Motion.vy field Read-only. Physics stores vertical velocity in pixels per second with `vx`. ```teal tecs.physics.Motion.vy: number ``` #### tecs.physics.Motion.omega field Read-only. Physics stores angular velocity in radians per second with `vx`. ```teal tecs.physics.Motion.omega: number ``` ### tecs.physics.PhysicsOptions record `PhysicsOptions` configures a Rapier world before plugin installation. ```teal global record tecs.physics.PhysicsOptions gravity: {number} subStepCount: integer workerCount: integer end ``` #### tecs.physics.PhysicsOptions.gravity field Caller-writable. The caller sets `gravity` before the plugin reads it. It contains x then y in pixels per second squared and defaults to 980 downward. ```teal tecs.physics.PhysicsOptions.gravity: {number} ``` #### tecs.physics.PhysicsOptions.subStepCount field Caller-writable. The caller sets `subStepCount` before plugin installation. It defaults to Rapier's 4. ```teal tecs.physics.PhysicsOptions.subStepCount: integer ``` #### tecs.physics.PhysicsOptions.workerCount field Caller-writable. The caller sets `workerCount` before the first physics world initializes the process-wide Rapier pool. Later worlds must use the same value. ```teal tecs.physics.PhysicsOptions.workerCount: integer ``` ### tecs.physics.QueryOptions record `QueryOptions` filters the shapes a raycast may hit. ```teal global record tecs.physics.QueryOptions categoryBits: number maskBits: number end ``` #### tecs.physics.QueryOptions.categoryBits field Caller-writable. The caller sets `categoryBits` before `raycast` reads it. It defaults to all categories. The ray skips a shape whose mask excludes every ray category. ```teal tecs.physics.QueryOptions.categoryBits: number ``` #### tecs.physics.QueryOptions.maskBits field Caller-writable. The caller sets `maskBits` before `raycast` reads it. It defaults to all categories. ```teal tecs.physics.QueryOptions.maskBits: number ``` ### tecs.physics.RaycastHit record `RaycastHit` reports where a ray first met a collider. ```teal global record tecs.physics.RaycastHit entity: integer x: number y: number normalX: number normalY: number fraction: number end ``` #### tecs.physics.RaycastHit.entity field Read-only. `raycast` sets `entity` to the shape owner. A secondary collider reports its own entity rather than its body's. ```teal tecs.physics.RaycastHit.entity: integer ``` #### tecs.physics.RaycastHit.x field Read-only. `raycast` sets `x` to the contact point in world pixels. ```teal tecs.physics.RaycastHit.x: number ``` #### tecs.physics.RaycastHit.y field Read-only. `raycast` sets `y` with `x`, positive downward. ```teal tecs.physics.RaycastHit.y: number ``` #### tecs.physics.RaycastHit.normalX field Read-only. `raycast` sets `normalX` to the outward unit normal. It does not scale this value to pixels. ```teal tecs.physics.RaycastHit.normalX: number ``` #### tecs.physics.RaycastHit.normalY field Read-only. `raycast` sets `normalY` with `normalX`. ```teal tecs.physics.RaycastHit.normalY: number ``` #### tecs.physics.RaycastHit.fraction field Read-only. `raycast` sets `fraction` from 0 at the segment start to 1 at the segment end. ```teal tecs.physics.RaycastHit.fraction: number ``` ### tecs.physics.RigidBody record A `RigidBody` identifies one live body in a Rapier simulation. Engine-owned and transient. Ordinary game code should use [`Body`](/modules/physics/#tecs.physics.Body), [`Motion`](/modules/physics/#tecs.physics.Motion), and the functions on `tecs.physics`. Engine-owned. `RigidBody` exposes live attachment state for tools. Ordinary game code ignores it. ```teal record tecs.physics.RigidBody is Component index1: number world0: number generation: number end ``` #### Interfaces | Interface | | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | #### tecs.physics.RigidBody.index1 field Engine-owned. Identifies the body slot; ordinary game code should ignore it. ```teal tecs.physics.RigidBody.index1: number ``` #### tecs.physics.RigidBody.world0 field Engine-owned. Identifies the simulation; ordinary game code should ignore it. ```teal tecs.physics.RigidBody.world0: number ``` #### tecs.physics.RigidBody.generation field Engine-owned. Rejects stale body slots; ordinary game code should ignore it. ```teal tecs.physics.RigidBody.generation: number ``` ### tecs.physics.SensorBegin record `SensorBegin` reports a body entering a sensor. Read-only. `SensorBegin` names the sensor-enter event. ```teal record tecs.physics.SensorBegin is events.Event sensor: integer visitor: integer end ``` #### Interfaces | Interface | | --- | | [`events.Event`](/modules/events/#tecs.events.Event) | #### tecs.physics.SensorBegin.sensor field Read-only. Physics sets `sensor` to the sensor entity before emitting the event. ```teal tecs.physics.SensorBegin.sensor: integer ``` #### tecs.physics.SensorBegin.visitor field Read-only. Physics sets `visitor` to the entering entity. ```teal tecs.physics.SensorBegin.visitor: integer ``` ### tecs.physics.SensorEnd record `SensorEnd` reports a body leaving a sensor. Read-only. `SensorEnd` names the sensor-exit event. ```teal record tecs.physics.SensorEnd is events.Event sensor: integer visitor: integer end ``` #### Interfaces | Interface | | --- | | [`events.Event`](/modules/events/#tecs.events.Event) | #### tecs.physics.SensorEnd.sensor field Read-only. Physics sets `sensor` to the sensor entity before emitting the event. ```teal tecs.physics.SensorEnd.sensor: integer ``` #### tecs.physics.SensorEnd.visitor field Read-only. Physics sets `visitor` to the leaving entity. ```teal tecs.physics.SensorEnd.visitor: integer ``` ## Functions ### tecs.physics.angularVelocity Static Reads angular velocity in radians per second. Radians need no conversion, so this is the same number Rapier holds, unlike the linear velocity beside it. ```teal function tecs.physics.angularVelocity( world: types.World, entity: integer ): number ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`types.World`](/modules/ecs/#tecs.World) | The caller supplies the world holding the entity. | | `entity` | `integer` | The caller supplies the target entity. | #### Returns | Type | Description | | --- | --- | | `number` | Returns live Rapier angular velocity in radians per second, or zero without a live body. | ### tecs.physics.applyForce Static Applies a continuous force at the body's center and wakes it. Cleared by Rapier at the end of every step, so holding a body up against gravity means calling this every fixed step rather than once. ```teal function tecs.physics.applyForce( world: types.World, entity: integer, x: number, y: number ) ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`types.World`](/modules/ecs/#tecs.World) | The caller supplies the world holding the entity. | | `entity` | `integer` | The caller supplies the target entity. A missing live body makes the call a no-op. | | `x` | `number` | The caller supplies pixel-scaled force along x. | | `y` | `number` | The caller supplies pixel-scaled force along y, positive downward. | #### Returns None. ### tecs.physics.applyForceAt Static Applies a continuous force at a world-space point and wakes the body. Cleared at the end of every step, like `applyForce`. ```teal function tecs.physics.applyForceAt( world: types.World, entity: integer, x: number, y: number, pointX: number, pointY: number ) ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`types.World`](/modules/ecs/#tecs.World) | The caller supplies the world holding the entity. | | `entity` | `integer` | The caller supplies the target entity. A missing live body makes the call a no-op. | | `x` | `number` | The caller supplies pixel-scaled force along x. | | `y` | `number` | The caller supplies pixel-scaled force along y, positive downward. | | `pointX` | `number` | The caller supplies the world-pixel application point along x. | | `pointY` | `number` | The caller supplies the world-pixel application point along y. | #### Returns None. ### tecs.physics.applyImpulse Static Pushes a body once, at its center of mass. An impulse rather than a force, so the effect does not depend on how long the step happened to be. Wakes the body: Rapier lets a resting island sleep, and pushing a sleeping body without waking it does nothing. ```teal function tecs.physics.applyImpulse( world: types.World, entity: integer, x: number, y: number ) ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`types.World`](/modules/ecs/#tecs.World) | The caller supplies the world holding the entity. | | `entity` | `integer` | The caller supplies the target entity. A missing live body makes the call a no-op. | | `x` | `number` | The caller supplies pixel-scaled impulse along x. | | `y` | `number` | The caller supplies pixel-scaled impulse along y, positive downward. | #### Returns None. ### tecs.physics.applyImpulseAt Static Applies an impulse at a world-space point, so it spins the body as well as moving it. ```teal function tecs.physics.applyImpulseAt( world: types.World, entity: integer, x: number, y: number, pointX: number, pointY: number ) ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`types.World`](/modules/ecs/#tecs.World) | The caller supplies the world holding the entity. | | `entity` | `integer` | The caller supplies the target entity. A missing live body makes the call a no-op. | | `x` | `number` | The caller supplies pixel-scaled impulse along x. | | `y` | `number` | The caller supplies pixel-scaled impulse along y, positive downward. | | `pointX` | `number` | The caller supplies the world-pixel impact position along x. | | `pointY` | `number` | The caller supplies the world-pixel impact position along y. | #### Returns None. ### tecs.physics.applyImpulseTo Static Applies a center-of-mass impulse to a [`RigidBody`](/modules/physics/#tecs.physics.RigidBody) row. ```teal function tecs.physics.applyImpulseTo( row: RigidBody, x: number, y: number ) ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `row` | [`RigidBody`](/modules/physics/#tecs.physics.RigidBody) | The caller supplies a row from a [`RigidBody`](/modules/physics/#tecs.physics.RigidBody) column. A stale or null row does nothing. | | `x` | `number` | The caller supplies pixel-scaled impulse along x. | | `y` | `number` | The caller supplies pixel-scaled impulse along y, positive downward. | #### Returns None. ### tecs.physics.applyTorque Static Applies torque and wakes the body. Cleared at the end of every step, like the two forces above. ```teal function tecs.physics.applyTorque( world: types.World, entity: integer, torque: number ) ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`types.World`](/modules/ecs/#tecs.World) | The caller supplies the world holding the entity. | | `entity` | `integer` | The caller supplies the target entity. A missing live body makes the call a no-op. | | `torque` | `number` | The caller supplies Newton-meters in Rapier's native torque units. Positive values raise angular velocity. | #### Returns None. ### tecs.physics.attach Static Declares a body on `entity`. Physics creates the Rapier body during the next fixed update. ```teal function tecs.physics.attach( world: types.World, entity: integer, options: BodyOptions ) ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`types.World`](/modules/ecs/#tecs.World) | The caller supplies a world with `physics.plugin`; otherwise the call raises. | | `entity` | `integer` | The caller supplies the entity that gains [`Body`](/modules/physics/#tecs.physics.Body), [`Collider`](/modules/physics/#tecs.physics.Collider), [`Transform2D`](/modules/ecs/#tecs.ecs.Transform2D), and [`Motion`](/modules/physics/#tecs.physics.Motion) as needed. | | `options` | [`BodyOptions`](/modules/physics/#tecs.physics.BodyOptions) | The caller supplies the initial body and collider settings. Unknown body types and invalid capsules raise. | #### Returns None. ### tecs.physics.attachCollider Static Adds another collider to a declared body. The collider is its own entity, which makes each shape independently inspectable and mutable. ```teal function tecs.physics.attachCollider( world: types.World, entity: integer, body: integer, options: BodyOptions ) ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`types.World`](/modules/ecs/#tecs.World) | The caller supplies the world that owns both entities. | | `entity` | `integer` | The caller supplies the secondary collider entity, which gains [`Collider`](/modules/physics/#tecs.physics.Collider) and [`ColliderOf`](/modules/physics/#tecs.physics.ColliderOf). | | `body` | `integer` | The caller supplies an entity with [`Body`](/modules/physics/#tecs.physics.Body); otherwise the call raises. | | `options` | [`BodyOptions`](/modules/physics/#tecs.physics.BodyOptions) | The caller supplies shape, material, filter and offset settings. Body-level fields have no effect. | #### Returns None. ### tecs.physics.detach Static Removes a body's declaration. Physics destroys its Rapier body during the next fixed update. ```teal function tecs.physics.detach(world: types.World, entity: integer) ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`types.World`](/modules/ecs/#tecs.World) | The caller supplies the world holding the entity. | | `entity` | `integer` | The caller supplies the entity that loses [`Body`](/modules/physics/#tecs.physics.Body). Other physics declaration components remain. | #### Returns None. ### tecs.physics.hasBody Static Returns whether Rapier is still solving a body for `entity`. False for an entity whose RigidBody came out of a snapshot. A load restores the row as the null handle rather than a body id this run never issued, so this is how a game tells "was simulating and is not any more" from "never had a body", and the two are worth telling apart: nothing rebuilds a body on load. ```teal function tecs.physics.hasBody( world: types.World, entity: integer ): boolean ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`types.World`](/modules/ecs/#tecs.World) | The caller supplies the world holding the entity. | | `entity` | `integer` | The caller supplies any entity. | #### Returns | Type | Description | | --- | --- | | `boolean` | Returns true only while Rapier holds a valid live body. It returns false before the first fixed update and after destruction. | ### tecs.physics.isAwake Static Returns whether Rapier currently considers the body awake. ```teal function tecs.physics.isAwake( world: types.World, entity: integer ): boolean ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`types.World`](/modules/ecs/#tecs.World) | The caller supplies the world holding the entity. | | `entity` | `integer` | The caller supplies any entity. | #### Returns | Type | Description | | --- | --- | | `boolean` | Returns true while Rapier actively solves the body, or false when it sleeps or does not exist. | ### tecs.physics.of Static Returns the Rapier simulation installed in `world`. ```teal function tecs.physics.of(world: types.World): World ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`types.World`](/modules/ecs/#tecs.World) | The caller supplies a world with or without the plugin. | #### Returns | Type | Description | | --- | --- | | `World` | Returns the world's live Rapier simulation, or nil before installation and after shutdown. | ### tecs.physics.plugin Static Installs the simulation and its sync. ```teal function tecs.physics.plugin(options: PhysicsOptions): function( types.World ) ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `options` | [`PhysicsOptions`](/modules/physics/#tecs.physics.PhysicsOptions) | The caller supplies Rapier settings or nil for Earth-like downward gravity and Rapier's default substep count. | #### Returns | Type | Description | | --- | --- | | `function(`[`types.World`](/modules/ecs/#tecs.World)`)` | Returns a plugin for `world:addPlugin`. Each world receives an independent Rapier simulation; all worlds share one worker pool. | ### tecs.physics.raycast Static Casts a segment and returns its nearest collider, in pixels. The cast tests one segment and ignores everything beyond `x2, y2`, so a miss may indicate insufficient length. ```teal function tecs.physics.raycast( world: types.World, x1: number, y1: number, x2: number, y2: number, options: QueryOptions ): RaycastHit ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`types.World`](/modules/ecs/#tecs.World) | The caller supplies a world. A world without physics returns nil. | | `x1` | `number` | The caller supplies the segment start in world pixels along x. | | `y1` | `number` | The caller supplies the segment start in world pixels along y. | | `x2` | `number` | The caller supplies the segment end in world pixels along x. | | `y2` | `number` | The caller supplies the segment end in world pixels along y. | | `options` | [`QueryOptions`](/modules/physics/#tecs.physics.QueryOptions) | The caller supplies collision filters or nil to test every shape, including sensors. | #### Returns | Type | Description | | --- | --- | | [`RaycastHit`](/modules/physics/#tecs.physics.RaycastHit) | Returns a fresh caller-owned nearest hit, or nil on a miss. | ### tecs.physics.setAngularVelocity Static Sets angular velocity in radians per second and wakes the body. ```teal function tecs.physics.setAngularVelocity( world: types.World, entity: integer, omega: number ) ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`types.World`](/modules/ecs/#tecs.World) | The caller supplies the world holding the entity. | | `entity` | `integer` | The caller supplies the target entity. A missing live body makes the call a no-op. | | `omega` | `number` | The caller supplies angular velocity in radians per second. | #### Returns None. ### tecs.physics.setAwake Static Wakes or sleeps a body. ```teal function tecs.physics.setAwake( world: types.World, entity: integer, awake: boolean ) ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`types.World`](/modules/ecs/#tecs.World) | The caller supplies the world holding the entity. | | `entity` | `integer` | The caller supplies the target entity. A missing live body makes the call a no-op. | | `awake` | `boolean` | The caller passes true to wake the body or false to sleep it immediately. | #### Returns None. ### tecs.physics.setVelocity Static Sets a body's velocity, in pixels per second, and wakes it. Does nothing for an entity with no live body, which matches the zero value returned by `physics.velocity`. ```teal function tecs.physics.setVelocity( world: types.World, entity: integer, vx: number, vy: number ) ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`types.World`](/modules/ecs/#tecs.World) | The caller supplies the world holding the entity. | | `entity` | `integer` | The caller supplies the target entity. A missing live body makes the call a no-op. | | `vx` | `number` | The caller supplies horizontal velocity in pixels per second. | | `vy` | `number` | The caller supplies vertical velocity in pixels per second, positive downward. | #### Returns None. ### tecs.physics.teleport Static Teleports a body and immediately updates its Transform2D. A move rather than a push: velocity is left exactly as it was, so a falling body carries on falling from wherever it lands. Teleportation leaves [`PreviousTransform2D`](/modules/gfx/#tecs.gfx.PreviousTransform2D) unchanged. `FixedFirst` snapshots that column and the renderer interpolates from it, so a teleport in any phase after that is drawn as one frame of travel between the two positions rather than as a jump. Teleporting before `FixedFirst`, or accepting the one frame, are the two ways round it. ```teal function tecs.physics.teleport( world: types.World, entity: integer, x: number, y: number, angle: number ) ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`types.World`](/modules/ecs/#tecs.World) | The caller supplies the world holding the entity. | | `entity` | `integer` | The caller supplies the target entity. A missing live body makes the call a no-op, including [`Transform2D`](/modules/ecs/#tecs.ecs.Transform2D). | | `x` | `number` | The caller supplies the body's origin in world pixels along x. | | `y` | `number` | The caller supplies the body's origin in world pixels along y, positive downward. | | `angle` | `number` | The caller supplies radians or nil to keep the current `Transform2D.rotation`. Teleportation does not sweep the gap. | #### Returns None. ### tecs.physics.velocity Static Reads a body's velocity, live from Rapier, in pixels per second. Pixels match every other linear number this module accepts and returns: extents, radius, impulse components and plugin gravity. A caller that wants meters divides by `physics.pixelsPerMeter`. This reports what a body does now. [`Motion`](/modules/physics/#tecs.physics.Motion) stores velocity only at a pause or save; it does not mirror live motion. ```teal function tecs.physics.velocity( world: types.World, entity: integer ): number, number ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`types.World`](/modules/ecs/#tecs.World) | The caller supplies the world holding the entity. | | `entity` | `integer` | The caller supplies the target entity. | #### Returns | Type | Description | | --- | --- | | `number` | Returns horizontal velocity in pixels per second, or zero without a live body. | | `number` | Returns vertical velocity in pixels per second, positive downward, or zero without a live body. | ## Values ### tecs.physics.ColliderOf variable Read-only. [`ColliderOf`](/modules/physics/#tecs.physics.ColliderOf) relates a secondary collider to its owning body. ```teal tecs.physics.ColliderOf: Component ``` ### tecs.physics.pixelsPerMeter variable Read-only. `pixelsPerMeter` reports the fixed conversion between public pixels and Rapier meters. ```teal tecs.physics.pixelsPerMeter: number ``` --- ## tecs.platform.events # tecs.platform.events One typed platform event stream. Every platform delivers the same vocabulary through the world's event bus. Each kind has one ECS event type at address zero: ```teal world:observe( 0, tecs.platform.events.on.dropFile, function(event: tecs.platform.events.Event) loadFile(event.text) end ) local pending : {tecs.platform.events.Event} = {} world:observe( 0, tecs.platform.events.on.keyDown, function(event: tecs.platform.events.Event) pending[#pending + 1] = tecs.platform.events.copy(event) end ) ``` The application seals one event batch at a logical-update boundary, folds the whole batch into latched input, then dispatches observers from the scheduler-owned `Ingress` phase before `First`. An observer may suspend; its batch remains unchanged and later SDL events wait for the next logical update. An observer therefore sees final latched input for its complete batch. SDL converts `SIGINT` and `SIGTERM` into `events.on.quit`, so an application uses the same normal shutdown path for a terminal interrupt and a window close. A headless program without an application uses `tecs.platform.os.newSignalListener`; reading that listener transfers pending native signal flags itself. ## Borrowed records The converter reuses one wide [`Event`](/modules/platform/events/#tecs.platform.events.Event) record. `kind` determines which payload fields carry meaning. Read the record during the observer call, or retain an independent value through `events.copy`. Mouse and converted touch positions use logical window coordinates. `timestamp` uses the platform event clock. `arrival` uses the monotonic seconds returned by `tecs.platform.time.now`, which makes it suitable for input-latency measurement. Unrecognized platform events arrive as `unknown` with their numeric type. Finger and touch-device identities remain opaque strings. ## Synthetic and replayed input Tests can push an engine event through the host queue: ```teal tecs.platform.events.push( "mouseDown", { button = 1, x = 120, y = 64, } ) ``` Install `events.source` to replace the host queue during replay. Pair it with `tecs.platform.time.provider` to replay event order and frame deltas together. Set the source to nil to resume the host queue. ## Module contents ### Types | Type | Kind | Description | | --- | --- | --- | | [`Event`](/modules/platform/events/#tecs.platform.events.Event) | record | Represents a borrowed platform event routed through the ECS event bus. | | [`KindTypes`](/modules/platform/events/#tecs.platform.events.KindTypes) | record | Maps each event kind to an ECS event type observed at address zero. | ### Functions | Function | Kind | Description | | --- | --- | --- | | [`copy`](/modules/platform/events/#tecs.platform.events.copy) | Static | Returns an independent copy, for code that must retain an event past the handler that received it. | | [`kinds`](/modules/platform/events/#tecs.platform.events.kinds) | Static | Returns every kind this build recognizes. | | [`push`](/modules/platform/events/#tecs.platform.events.push) | Static | Pushes a synthetic event onto SDL's own queue. | | [`source`](/modules/platform/events/#tecs.platform.events.source) | Static | Caller-writable. Installs a replay driver in place of the host queue. | | [`typeOf`](/modules/platform/events/#tecs.platform.events.typeOf) | Static | Returns the event type for a kind, or nil for an unknown name. | ### Values | Value | Type | Description | | --- | --- | --- | | [`on`](/modules/platform/events/#tecs.platform.events.on) | [`KindTypes`](/modules/platform/events/#tecs.platform.events.KindTypes) | Read-only. Exposes one ECS event type per kind, to observe at address zero. | ## Types ### tecs.platform.events.Event record Represents a borrowed platform event routed through the ECS event bus. Read-only. Exposes the `Event` type, discriminated by `kind`. The converter reuses the record passed to a handler, so anything retaining one takes a copy through `events.copy`. ```teal record tecs.platform.events.Event is genericEvents.Event kind: string timestamp: number arrival: number sequence: number scancode: integer keycode: integer modifiers: integer repeated: boolean down: boolean text: string start: integer length: integer candidates: {string} selected: integer horizontal: boolean x: number y: number dx: number dy: number normalX: number normalY: number button: integer clicks: integer wheelX: number wheelY: number wheelTicksX: integer wheelTicksY: integer flipped: boolean scale: number axis: integer value: number pressure: number eraser: boolean penState: integer which: number synthetic: boolean finger: string touchDevice: string touchpad: integer fingerIndex: integer sensor: integer sensorX: number sensorY: number sensorZ: number sensorData: {number} sensorTimestamp: number source: string mimeTypes: {string} owner: boolean recording: boolean data1: integer data2: integer sdlType: integer end ``` #### Interfaces | Interface | | --- | | [`genericEvents.Event`](/modules/events/#tecs.events.Event) | #### tecs.platform.events.Event.kind field Read-only. Reports what happened and determines which other fields carry meaning. ```teal tecs.platform.events.Event.kind: string ``` #### tecs.platform.events.Event.timestamp field Read-only. Reports nanoseconds on the platform event clock. This clock orders events against each other, not against `time.now`. ```teal tecs.platform.events.Event.timestamp: number ``` #### tecs.platform.events.Event.arrival field Read-only. Reports monotonic seconds on the clock `time.now` reads, converted by the host from the event's platform timestamp. Nil for an event that did not come through the host queue, such as a replayed one. ```teal tecs.platform.events.Event.arrival: number ``` #### tecs.platform.events.Event.sequence field Read-only. Reports the host's monotonic event sequence. The value is stable across a suspended logical update and nil for replayed or synthetic records that did not pass through the native host. ```teal tecs.platform.events.Event.sequence: number ``` #### tecs.platform.events.Event.scancode field Read-only. Reports the physical key position, which is what a movement binding wants: WASD stays in the same place on every layout. ```teal tecs.platform.events.Event.scancode: integer ``` #### tecs.platform.events.Event.keycode field Read-only. Reports the key the layout produces, which is what a text binding and a prompt want. The event carries both because neither answers the other's question. ```teal tecs.platform.events.Event.keycode: integer ``` #### tecs.platform.events.Event.modifiers field Read-only. Reports modifier keys held when the event occurred as a mask. ```teal tecs.platform.events.Event.modifiers: integer ``` #### tecs.platform.events.Event.repeated field Read-only. Reports true when the platform repeats a held key rather than reporting a new press. ```teal tecs.platform.events.Event.repeated: boolean ``` #### tecs.platform.events.Event.down field Read-only. Reports whether the button or key went down. ```teal tecs.platform.events.Event.down: boolean ``` #### tecs.platform.events.Event.text field Read-only. Contains text committed or composed by an input method, or dropped onto the window. ```teal tecs.platform.events.Event.text: string ``` #### tecs.platform.events.Event.start field Read-only. Reports the cursor position within composing text. ```teal tecs.platform.events.Event.start: integer ``` #### tecs.platform.events.Event.length field Read-only. Reports the selection length within composing text. ```teal tecs.platform.events.Event.length: integer ``` #### tecs.platform.events.Event.candidates field Read-only. Contains candidates an input method offers. It is nil when the platform offers none. ```teal tecs.platform.events.Event.candidates: {string} ``` #### tecs.platform.events.Event.selected field Read-only. Reports the selected candidate index. ```teal tecs.platform.events.Event.selected: integer ``` #### tecs.platform.events.Event.horizontal field Read-only. Reports whether the platform lays its candidates out horizontally. ```teal tecs.platform.events.Event.horizontal: boolean ``` #### tecs.platform.events.Event.x field Read-only. Reports the pointer's x position in window coordinates. ```teal tecs.platform.events.Event.x: number ``` #### tecs.platform.events.Event.y field Read-only. Reports the pointer's y position in window coordinates. ```teal tecs.platform.events.Event.y: number ``` #### tecs.platform.events.Event.dx field Read-only. Reports horizontal movement since the previous event of this kind. ```teal tecs.platform.events.Event.dx: number ``` #### tecs.platform.events.Event.dy field Read-only. Reports vertical movement since the previous event of this kind. ```teal tecs.platform.events.Event.dy: number ``` #### tecs.platform.events.Event.normalX field Read-only. Reports horizontal pointer position as the platform reported it, in 0..1 across the window. The platform normalizes touch at source, so the event carries it unscaled as well as converted: a normalized position survives a resize and a window coordinate does not. ```teal tecs.platform.events.Event.normalX: number ``` #### tecs.platform.events.Event.normalY field Read-only. Reports vertical pointer position from 0 to 1 down the window. ```teal tecs.platform.events.Event.normalY: number ``` #### tecs.platform.events.Event.button field Read-only. Reports the mouse, gamepad or pen button. ```teal tecs.platform.events.Event.button: integer ``` #### tecs.platform.events.Event.clicks field Read-only. Reports the platform's count of quick successive presses for this button under its own double-click interval. One for a single click, two for a double, and up from there. ```teal tecs.platform.events.Event.clicks: integer ``` #### tecs.platform.events.Event.wheelX field Read-only. Reports horizontal wheel movement, with one meaning whatever the platform is set to: positive `wheelY` is a scroll away from the player and positive `wheelX` is a scroll to the right. A platform reporting natural scrolling sends the opposite pair and flags it, and the conversion undoes that, so nothing above binds a sign twice. ```teal tecs.platform.events.Event.wheelX: number ``` #### tecs.platform.events.Event.wheelY field Read-only. Reports vertical wheel movement under the same sign convention as `wheelX`. ```teal tecs.platform.events.Event.wheelY: number ``` #### tecs.platform.events.Event.wheelTicksX field Read-only. Reports horizontal wheel movement accumulated to whole notches by the platform, on the same sign convention. What a menu stepping one item per notch reads, rather than deciding for itself where a fraction of a notch becomes a step. ```teal tecs.platform.events.Event.wheelTicksX: integer ``` #### tecs.platform.events.Event.wheelTicksY field Read-only. Reports vertical whole-notch wheel movement under the same sign convention as `wheelTicksX`. ```teal tecs.platform.events.Event.wheelTicksY: integer ``` #### tecs.platform.events.Event.flipped field Read-only. Reports whether the platform marked this wheel event as flipped for its configured scroll direction. `wheelX` and `wheelY` remain normalized; a consumer that intentionally follows the user's scrolling preference can reverse the normalized pair once when this is true. ```teal tecs.platform.events.Event.flipped: boolean ``` #### tecs.platform.events.Event.scale field Read-only. Reports the zoom factor since the previous pinch event. Below one is a pinch closed, above one is a pinch opened. ```teal tecs.platform.events.Event.scale: number ``` #### tecs.platform.events.Event.axis field Read-only. Reports the gamepad or pen axis. ```teal tecs.platform.events.Event.axis: integer ``` #### tecs.platform.events.Event.value field Read-only. Reports the axis value in -1..1. ```teal tecs.platform.events.Event.value: number ``` #### tecs.platform.events.Event.pressure field Read-only. Reports finger pressure from 0 to 1 on a touch surface or gamepad touchpad. A pen reports its pressure as an axis instead, so a pen event leaves this holding whatever the last touch put there. ```teal tecs.platform.events.Event.pressure: number ``` #### tecs.platform.events.Event.eraser field Read-only. Reports whether the pen's eraser end is in use. ```teal tecs.platform.events.Event.eraser: boolean ``` #### tecs.platform.events.Event.penState field Read-only. Reports pen state at the instant of the event as a mask: the tip down, the eraser end in use, whether it is in proximity, and each barrel button. `eraser` and `down` answer for one stroke; this answers which barrel button is held right now. The bits are the platform's `SDL_PEN_INPUT_*` values. Not carried on the proximity events, which report no state. ```teal tecs.platform.events.Event.penState: integer ``` #### tecs.platform.events.Event.which field Read-only. Reports the device that produced the event: a gamepad, a keyboard, a mouse, a pen, a sensor, a display, or a window. These identifiers are 32 bits and fit a Lua number exactly. ```teal tecs.platform.events.Event.which: number ``` #### tecs.platform.events.Event.synthetic field Read-only. Reports true when the platform produced this mouse event from a touch or a pen rather than from a mouse. A game that also handles touch would otherwise act on the same gesture twice. ```teal tecs.platform.events.Event.synthetic: boolean ``` #### tecs.platform.events.Event.finger field Read-only. Reports the touch finger identity as an opaque string. The 64-bit value does not fit a double, so the event carries a string rather than numbers that might silently round. ```teal tecs.platform.events.Event.finger: string ``` #### tecs.platform.events.Event.touchDevice field Read-only. Reports the touch device identity as an opaque string. ```teal tecs.platform.events.Event.touchDevice: string ``` #### tecs.platform.events.Event.touchpad field Read-only. Reports the gamepad touchpad index. Position on the device identifies a touchpad finger, not a 64-bit id. ```teal tecs.platform.events.Event.touchpad: integer ``` #### tecs.platform.events.Event.fingerIndex field Read-only. Reports the finger's slot on the gamepad touchpad. ```teal tecs.platform.events.Event.fingerIndex: integer ``` #### tecs.platform.events.Event.sensor field Read-only. Reports the sensor identity. ```teal tecs.platform.events.Event.sensor: integer ``` #### tecs.platform.events.Event.sensorX field Read-only. Reports the first component of the latest sensor reading. ```teal tecs.platform.events.Event.sensorX: number ``` #### tecs.platform.events.Event.sensorY field Read-only. Reports the second component of the latest sensor reading. ```teal tecs.platform.events.Event.sensorY: number ``` #### tecs.platform.events.Event.sensorZ field Read-only. Reports the third component of the latest sensor reading. ```teal tecs.platform.events.Event.sensorZ: number ``` #### tecs.platform.events.Event.sensorData field Read-only. Contains the latest sensor reading. A gamepad sensor reports three components; a standalone sensor reports up to six. ```teal tecs.platform.events.Event.sensorData: {number} ``` #### tecs.platform.events.Event.sensorTimestamp field Read-only. Reports when the hardware took the reading, in nanoseconds on the sensor's own clock. Like `timestamp` it orders readings against each other and against nothing else, which is what integrating a rotation over them needs and is all it is for. ```teal tecs.platform.events.Event.sensorTimestamp: number ``` #### tecs.platform.events.Event.source field Read-only. Reports where a dropped file or text came from when the platform supplies a source. ```teal tecs.platform.events.Event.source: string ``` #### tecs.platform.events.Event.mimeTypes field Read-only. Lists the clipboard formats on offer. ```teal tecs.platform.events.Event.mimeTypes: {string} ``` #### tecs.platform.events.Event.owner field Read-only. Reports whether the clipboard's new contents belong to this application. ```teal tecs.platform.events.Event.owner: boolean ``` #### tecs.platform.events.Event.recording field Read-only. Reports whether an audio device records input. ```teal tecs.platform.events.Event.recording: boolean ``` #### tecs.platform.events.Event.data1 field Read-only. Reports the first window, display or user payload integer. A user event's code arrives in `data1`; the two data pointers beside it do not cross, since a pointer is not a value this vocabulary can carry. ```teal tecs.platform.events.Event.data1: integer ``` #### tecs.platform.events.Event.data2 field Read-only. Reports the second window, display or user payload integer. ```teal tecs.platform.events.Event.data2: integer ``` #### tecs.platform.events.Event.sdlType field Engine-owned. Stores the numeric platform event type and is the only useful field on `unknown`. Ordinary game code should ignore this field for known events. ```teal tecs.platform.events.Event.sdlType: integer ``` ### tecs.platform.events.KindTypes record Maps each event kind to an ECS event type observed at address zero. A game asks for the kinds it wants rather than for the stream: `world:observe(0, events.on.dropFile, handler)` runs `handler` for dropped files and for nothing else. That is what a type per kind buys over one type carrying the kind, where every subscriber receives every kind and filters it back out again. Written as a record rather than looked up by name, so a misspelled kind is a compile error on the key instead of an observer that is never called. `events.typeOf` is the same table for code holding a kind as a string. The converter reuses the record an observer receives, so the borrow rule is the one this module states everywhere: read it, or copy it with `events.copy`, but do not keep it. ```teal global record tecs.platform.events.KindTypes quit: Event terminating: Event lowMemory: Event appWillEnterBackground: Event appDidEnterBackground: Event appWillEnterForeground: Event appDidEnterForeground: Event localeChanged: Event themeChanged: Event displayOrientation: Event displayAdded: Event displayRemoved: Event displayMoved: Event displayScaleChanged: Event displayDesktopModeChanged: Event displayCurrentModeChanged: Event displayUsableBoundsChanged: Event windowShown: Event windowHidden: Event windowExposed: Event windowMoved: Event windowResized: Event windowPixelSizeChanged: Event windowMinimized: Event windowMaximized: Event windowRestored: Event windowMouseEnter: Event windowMouseLeave: Event windowFocusGained: Event windowFocusLost: Event windowCloseRequested: Event windowDisplayChanged: Event windowDisplayScaleChanged: Event windowSafeAreaChanged: Event windowOccluded: Event windowEnterFullscreen: Event windowLeaveFullscreen: Event keyDown: Event keyUp: Event textEditing: Event textCandidates: Event textInput: Event keymapChanged: Event keyboardAdded: Event keyboardRemoved: Event screenKeyboardShown: Event screenKeyboardHidden: Event mouseMotion: Event mouseDown: Event mouseUp: Event mouseWheel: Event mouseAdded: Event mouseRemoved: Event fingerDown: Event fingerUp: Event fingerMotion: Event fingerCanceled: Event pinchBegin: Event pinchUpdate: Event pinchEnd: Event penProximityIn: Event penProximityOut: Event penDown: Event penUp: Event penMotion: Event penButtonDown: Event penButtonUp: Event penAxis: Event gamepadAdded: Event gamepadRemoved: Event gamepadRemapped: Event gamepadButtonDown: Event gamepadButtonUp: Event gamepadAxis: Event gamepadSensor: Event gamepadTouchpadDown: Event gamepadTouchpadUp: Event gamepadTouchpadMotion: Event dropFile: Event dropText: Event dropBegin: Event dropComplete: Event dropPosition: Event clipboardUpdate: Event audioDeviceAdded: Event audioDeviceRemoved: Event audioDeviceFormatChanged: Event sensorUpdate: Event user: Event unknown: Event end ``` #### tecs.platform.events.KindTypes.quit field Read-only. Exposes the `"quit"` event type for a requested application shutdown, including a window close, `SIGINT`, or `SIGTERM`. ```teal tecs.platform.events.KindTypes.quit: Event ``` #### tecs.platform.events.KindTypes.terminating field Read-only. Exposes the `"terminating"` event type. ```teal tecs.platform.events.KindTypes.terminating: Event ``` #### tecs.platform.events.KindTypes.lowMemory field Read-only. Exposes the `"lowMemory"` event type. ```teal tecs.platform.events.KindTypes.lowMemory: Event ``` #### tecs.platform.events.KindTypes.appWillEnterBackground field Read-only. Exposes the `"appWillEnterBackground"` event type. ```teal tecs.platform.events.KindTypes.appWillEnterBackground: Event ``` #### tecs.platform.events.KindTypes.appDidEnterBackground field Read-only. Exposes the `"appDidEnterBackground"` event type. ```teal tecs.platform.events.KindTypes.appDidEnterBackground: Event ``` #### tecs.platform.events.KindTypes.appWillEnterForeground field Read-only. Exposes the `"appWillEnterForeground"` event type. ```teal tecs.platform.events.KindTypes.appWillEnterForeground: Event ``` #### tecs.platform.events.KindTypes.appDidEnterForeground field Read-only. Exposes the `"appDidEnterForeground"` event type. ```teal tecs.platform.events.KindTypes.appDidEnterForeground: Event ``` #### tecs.platform.events.KindTypes.localeChanged field Read-only. Exposes the `"localeChanged"` event type. ```teal tecs.platform.events.KindTypes.localeChanged: Event ``` #### tecs.platform.events.KindTypes.themeChanged field Read-only. Exposes the `"themeChanged"` event type. ```teal tecs.platform.events.KindTypes.themeChanged: Event ``` #### tecs.platform.events.KindTypes.displayOrientation field Read-only. Exposes the `"displayOrientation"` event type. ```teal tecs.platform.events.KindTypes.displayOrientation: Event ``` #### tecs.platform.events.KindTypes.displayAdded field Read-only. Exposes the `"displayAdded"` event type. ```teal tecs.platform.events.KindTypes.displayAdded: Event ``` #### tecs.platform.events.KindTypes.displayRemoved field Read-only. Exposes the `"displayRemoved"` event type. ```teal tecs.platform.events.KindTypes.displayRemoved: Event ``` #### tecs.platform.events.KindTypes.displayMoved field Read-only. Exposes the `"displayMoved"` event type. ```teal tecs.platform.events.KindTypes.displayMoved: Event ``` #### tecs.platform.events.KindTypes.displayScaleChanged field Read-only. Exposes the `"displayScaleChanged"` event type. ```teal tecs.platform.events.KindTypes.displayScaleChanged: Event ``` #### tecs.platform.events.KindTypes.displayDesktopModeChanged field Read-only. Exposes the `"displayDesktopModeChanged"` event type. ```teal tecs.platform.events.KindTypes.displayDesktopModeChanged: Event ``` #### tecs.platform.events.KindTypes.displayCurrentModeChanged field Read-only. Exposes the `"displayCurrentModeChanged"` event type. ```teal tecs.platform.events.KindTypes.displayCurrentModeChanged: Event ``` #### tecs.platform.events.KindTypes.displayUsableBoundsChanged field Read-only. Exposes the `"displayUsableBoundsChanged"` event type. ```teal tecs.platform.events.KindTypes.displayUsableBoundsChanged: Event ``` #### tecs.platform.events.KindTypes.windowShown field Read-only. Exposes the `"windowShown"` event type. ```teal tecs.platform.events.KindTypes.windowShown: Event ``` #### tecs.platform.events.KindTypes.windowHidden field Read-only. Exposes the `"windowHidden"` event type. ```teal tecs.platform.events.KindTypes.windowHidden: Event ``` #### tecs.platform.events.KindTypes.windowExposed field Read-only. Exposes the `"windowExposed"` event type. ```teal tecs.platform.events.KindTypes.windowExposed: Event ``` #### tecs.platform.events.KindTypes.windowMoved field Read-only. Exposes the `"windowMoved"` event type. ```teal tecs.platform.events.KindTypes.windowMoved: Event ``` #### tecs.platform.events.KindTypes.windowResized field Read-only. Exposes the `"windowResized"` event type. ```teal tecs.platform.events.KindTypes.windowResized: Event ``` #### tecs.platform.events.KindTypes.windowPixelSizeChanged field Read-only. Exposes the `"windowPixelSizeChanged"` event type. ```teal tecs.platform.events.KindTypes.windowPixelSizeChanged: Event ``` #### tecs.platform.events.KindTypes.windowMinimized field Read-only. Exposes the `"windowMinimized"` event type. ```teal tecs.platform.events.KindTypes.windowMinimized: Event ``` #### tecs.platform.events.KindTypes.windowMaximized field Read-only. Exposes the `"windowMaximized"` event type. ```teal tecs.platform.events.KindTypes.windowMaximized: Event ``` #### tecs.platform.events.KindTypes.windowRestored field Read-only. Exposes the `"windowRestored"` event type. ```teal tecs.platform.events.KindTypes.windowRestored: Event ``` #### tecs.platform.events.KindTypes.windowMouseEnter field Read-only. Exposes the `"windowMouseEnter"` event type. ```teal tecs.platform.events.KindTypes.windowMouseEnter: Event ``` #### tecs.platform.events.KindTypes.windowMouseLeave field Read-only. Exposes the `"windowMouseLeave"` event type. ```teal tecs.platform.events.KindTypes.windowMouseLeave: Event ``` #### tecs.platform.events.KindTypes.windowFocusGained field Read-only. Exposes the `"windowFocusGained"` event type. ```teal tecs.platform.events.KindTypes.windowFocusGained: Event ``` #### tecs.platform.events.KindTypes.windowFocusLost field Read-only. Exposes the `"windowFocusLost"` event type. ```teal tecs.platform.events.KindTypes.windowFocusLost: Event ``` #### tecs.platform.events.KindTypes.windowCloseRequested field Read-only. Exposes the `"windowCloseRequested"` event type. ```teal tecs.platform.events.KindTypes.windowCloseRequested: Event ``` #### tecs.platform.events.KindTypes.windowDisplayChanged field Read-only. Exposes the `"windowDisplayChanged"` event type. ```teal tecs.platform.events.KindTypes.windowDisplayChanged: Event ``` #### tecs.platform.events.KindTypes.windowDisplayScaleChanged field Read-only. Exposes the `"windowDisplayScaleChanged"` event type. ```teal tecs.platform.events.KindTypes.windowDisplayScaleChanged: Event ``` #### tecs.platform.events.KindTypes.windowSafeAreaChanged field Read-only. Exposes the `"windowSafeAreaChanged"` event type. ```teal tecs.platform.events.KindTypes.windowSafeAreaChanged: Event ``` #### tecs.platform.events.KindTypes.windowOccluded field Read-only. Exposes the `"windowOccluded"` event type. ```teal tecs.platform.events.KindTypes.windowOccluded: Event ``` #### tecs.platform.events.KindTypes.windowEnterFullscreen field Read-only. Exposes the `"windowEnterFullscreen"` event type. ```teal tecs.platform.events.KindTypes.windowEnterFullscreen: Event ``` #### tecs.platform.events.KindTypes.windowLeaveFullscreen field Read-only. Exposes the `"windowLeaveFullscreen"` event type. ```teal tecs.platform.events.KindTypes.windowLeaveFullscreen: Event ``` #### tecs.platform.events.KindTypes.keyDown field Read-only. Exposes the `"keyDown"` event type. ```teal tecs.platform.events.KindTypes.keyDown: Event ``` #### tecs.platform.events.KindTypes.keyUp field Read-only. Exposes the `"keyUp"` event type. ```teal tecs.platform.events.KindTypes.keyUp: Event ``` #### tecs.platform.events.KindTypes.textEditing field Read-only. Exposes the `"textEditing"` event type. ```teal tecs.platform.events.KindTypes.textEditing: Event ``` #### tecs.platform.events.KindTypes.textCandidates field Read-only. Exposes the `"textCandidates"` event type. ```teal tecs.platform.events.KindTypes.textCandidates: Event ``` #### tecs.platform.events.KindTypes.textInput field Read-only. Exposes the `"textInput"` event type. ```teal tecs.platform.events.KindTypes.textInput: Event ``` #### tecs.platform.events.KindTypes.keymapChanged field Read-only. Exposes the `"keymapChanged"` event type. ```teal tecs.platform.events.KindTypes.keymapChanged: Event ``` #### tecs.platform.events.KindTypes.keyboardAdded field Read-only. Exposes the `"keyboardAdded"` event type. ```teal tecs.platform.events.KindTypes.keyboardAdded: Event ``` #### tecs.platform.events.KindTypes.keyboardRemoved field Read-only. Exposes the `"keyboardRemoved"` event type. ```teal tecs.platform.events.KindTypes.keyboardRemoved: Event ``` #### tecs.platform.events.KindTypes.screenKeyboardShown field Read-only. Exposes the `"screenKeyboardShown"` event type. ```teal tecs.platform.events.KindTypes.screenKeyboardShown: Event ``` #### tecs.platform.events.KindTypes.screenKeyboardHidden field Read-only. Exposes the `"screenKeyboardHidden"` event type. ```teal tecs.platform.events.KindTypes.screenKeyboardHidden: Event ``` #### tecs.platform.events.KindTypes.mouseMotion field Read-only. Exposes the `"mouseMotion"` event type. ```teal tecs.platform.events.KindTypes.mouseMotion: Event ``` #### tecs.platform.events.KindTypes.mouseDown field Read-only. Exposes the `"mouseDown"` event type. ```teal tecs.platform.events.KindTypes.mouseDown: Event ``` #### tecs.platform.events.KindTypes.mouseUp field Read-only. Exposes the `"mouseUp"` event type. ```teal tecs.platform.events.KindTypes.mouseUp: Event ``` #### tecs.platform.events.KindTypes.mouseWheel field Read-only. Exposes the `"mouseWheel"` event type. ```teal tecs.platform.events.KindTypes.mouseWheel: Event ``` #### tecs.platform.events.KindTypes.mouseAdded field Read-only. Exposes the `"mouseAdded"` event type. ```teal tecs.platform.events.KindTypes.mouseAdded: Event ``` #### tecs.platform.events.KindTypes.mouseRemoved field Read-only. Exposes the `"mouseRemoved"` event type. ```teal tecs.platform.events.KindTypes.mouseRemoved: Event ``` #### tecs.platform.events.KindTypes.fingerDown field Read-only. Exposes the `"fingerDown"` event type. ```teal tecs.platform.events.KindTypes.fingerDown: Event ``` #### tecs.platform.events.KindTypes.fingerUp field Read-only. Exposes the `"fingerUp"` event type. ```teal tecs.platform.events.KindTypes.fingerUp: Event ``` #### tecs.platform.events.KindTypes.fingerMotion field Read-only. Exposes the `"fingerMotion"` event type. ```teal tecs.platform.events.KindTypes.fingerMotion: Event ``` #### tecs.platform.events.KindTypes.fingerCanceled field Read-only. Exposes the `"fingerCanceled"` event type. ```teal tecs.platform.events.KindTypes.fingerCanceled: Event ``` #### tecs.platform.events.KindTypes.pinchBegin field Read-only. Exposes the `"pinchBegin"` event type. ```teal tecs.platform.events.KindTypes.pinchBegin: Event ``` #### tecs.platform.events.KindTypes.pinchUpdate field Read-only. Exposes the `"pinchUpdate"` event type. ```teal tecs.platform.events.KindTypes.pinchUpdate: Event ``` #### tecs.platform.events.KindTypes.pinchEnd field Read-only. Exposes the `"pinchEnd"` event type. ```teal tecs.platform.events.KindTypes.pinchEnd: Event ``` #### tecs.platform.events.KindTypes.penProximityIn field Read-only. Exposes the `"penProximityIn"` event type. ```teal tecs.platform.events.KindTypes.penProximityIn: Event ``` #### tecs.platform.events.KindTypes.penProximityOut field Read-only. Exposes the `"penProximityOut"` event type. ```teal tecs.platform.events.KindTypes.penProximityOut: Event ``` #### tecs.platform.events.KindTypes.penDown field Read-only. Exposes the `"penDown"` event type. ```teal tecs.platform.events.KindTypes.penDown: Event ``` #### tecs.platform.events.KindTypes.penUp field Read-only. Exposes the `"penUp"` event type. ```teal tecs.platform.events.KindTypes.penUp: Event ``` #### tecs.platform.events.KindTypes.penMotion field Read-only. Exposes the `"penMotion"` event type. ```teal tecs.platform.events.KindTypes.penMotion: Event ``` #### tecs.platform.events.KindTypes.penButtonDown field Read-only. Exposes the `"penButtonDown"` event type. ```teal tecs.platform.events.KindTypes.penButtonDown: Event ``` #### tecs.platform.events.KindTypes.penButtonUp field Read-only. Exposes the `"penButtonUp"` event type. ```teal tecs.platform.events.KindTypes.penButtonUp: Event ``` #### tecs.platform.events.KindTypes.penAxis field Read-only. Exposes the `"penAxis"` event type. ```teal tecs.platform.events.KindTypes.penAxis: Event ``` #### tecs.platform.events.KindTypes.gamepadAdded field Read-only. Exposes the `"gamepadAdded"` event type. ```teal tecs.platform.events.KindTypes.gamepadAdded: Event ``` #### tecs.platform.events.KindTypes.gamepadRemoved field Read-only. Exposes the `"gamepadRemoved"` event type. ```teal tecs.platform.events.KindTypes.gamepadRemoved: Event ``` #### tecs.platform.events.KindTypes.gamepadRemapped field Read-only. Exposes the `"gamepadRemapped"` event type. ```teal tecs.platform.events.KindTypes.gamepadRemapped: Event ``` #### tecs.platform.events.KindTypes.gamepadButtonDown field Read-only. Exposes the `"gamepadButtonDown"` event type. ```teal tecs.platform.events.KindTypes.gamepadButtonDown: Event ``` #### tecs.platform.events.KindTypes.gamepadButtonUp field Read-only. Exposes the `"gamepadButtonUp"` event type. ```teal tecs.platform.events.KindTypes.gamepadButtonUp: Event ``` #### tecs.platform.events.KindTypes.gamepadAxis field Read-only. Exposes the `"gamepadAxis"` event type. ```teal tecs.platform.events.KindTypes.gamepadAxis: Event ``` #### tecs.platform.events.KindTypes.gamepadSensor field Read-only. Exposes the `"gamepadSensor"` event type. ```teal tecs.platform.events.KindTypes.gamepadSensor: Event ``` #### tecs.platform.events.KindTypes.gamepadTouchpadDown field Read-only. Exposes the `"gamepadTouchpadDown"` event type. ```teal tecs.platform.events.KindTypes.gamepadTouchpadDown: Event ``` #### tecs.platform.events.KindTypes.gamepadTouchpadUp field Read-only. Exposes the `"gamepadTouchpadUp"` event type. ```teal tecs.platform.events.KindTypes.gamepadTouchpadUp: Event ``` #### tecs.platform.events.KindTypes.gamepadTouchpadMotion field Read-only. Exposes the `"gamepadTouchpadMotion"` event type. ```teal tecs.platform.events.KindTypes.gamepadTouchpadMotion: Event ``` #### tecs.platform.events.KindTypes.dropFile field Read-only. Exposes the `"dropFile"` event type. ```teal tecs.platform.events.KindTypes.dropFile: Event ``` #### tecs.platform.events.KindTypes.dropText field Read-only. Exposes the `"dropText"` event type. ```teal tecs.platform.events.KindTypes.dropText: Event ``` #### tecs.platform.events.KindTypes.dropBegin field Read-only. Exposes the `"dropBegin"` event type. ```teal tecs.platform.events.KindTypes.dropBegin: Event ``` #### tecs.platform.events.KindTypes.dropComplete field Read-only. Exposes the `"dropComplete"` event type. ```teal tecs.platform.events.KindTypes.dropComplete: Event ``` #### tecs.platform.events.KindTypes.dropPosition field Read-only. Exposes the `"dropPosition"` event type. ```teal tecs.platform.events.KindTypes.dropPosition: Event ``` #### tecs.platform.events.KindTypes.clipboardUpdate field Read-only. Exposes the `"clipboardUpdate"` event type. ```teal tecs.platform.events.KindTypes.clipboardUpdate: Event ``` #### tecs.platform.events.KindTypes.audioDeviceAdded field Read-only. Exposes the `"audioDeviceAdded"` event type. ```teal tecs.platform.events.KindTypes.audioDeviceAdded: Event ``` #### tecs.platform.events.KindTypes.audioDeviceRemoved field Read-only. Exposes the `"audioDeviceRemoved"` event type. ```teal tecs.platform.events.KindTypes.audioDeviceRemoved: Event ``` #### tecs.platform.events.KindTypes.audioDeviceFormatChanged field Read-only. Exposes the `"audioDeviceFormatChanged"` event type. ```teal tecs.platform.events.KindTypes.audioDeviceFormatChanged: Event ``` #### tecs.platform.events.KindTypes.sensorUpdate field Read-only. Exposes the `"sensorUpdate"` event type. ```teal tecs.platform.events.KindTypes.sensorUpdate: Event ``` #### tecs.platform.events.KindTypes.user field Read-only. Exposes the `"user"` event type. ```teal tecs.platform.events.KindTypes.user: Event ``` #### tecs.platform.events.KindTypes.unknown field Read-only. Selects an event kind this build has no name for. Such an event carries `sdlType` and nothing else worth reading. ```teal tecs.platform.events.KindTypes.unknown: Event ``` ## Functions ### tecs.platform.events.copy Static Returns an independent copy, for code that must retain an event past the handler that received it. Shallow, which is enough because every field is a number, a string or a boolean except the three lists, and those are freshly built per event rather than reused. Allocates, so this is for the events a recorder or a tool keeps and not for the stream. ```teal function tecs.platform.events.copy(event: Event): Event ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `event` | [`Event`](/modules/platform/events/#tecs.platform.events.Event) | The borrowed event to copy. | #### Returns | Type | Description | | --- | --- | | [`Event`](/modules/platform/events/#tecs.platform.events.Event) | A new event record that the caller can retain. | ### tecs.platform.events.kinds Static Returns every kind this build recognizes. ```teal function tecs.platform.events.kinds(): {string} ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | `{string}` | | ### tecs.platform.events.push Static Pushes a synthetic event onto SDL's own queue. Takes the engine's vocabulary rather than an SDL union, so a test or a tool can inject input without knowing SDL's layout. Only the fields a kind uses are read. This function fills payloads for the key, mouse, wheel, pen, gamepad, device, pinch, window and display kinds. It pushes any other recognized kind with its type and nothing else, which is what a kind whose payload nobody injects needs and is not what a caller expecting a full event would assume. The function writes a wheel payload the way the platform does, so `flipped` sets the direction and the axes beside it are left as given. The conversion then negates them, which is what a caller asking for a flipped scroll is asking to see: the round trip is the normalization, not the identity. The platform stamps the timestamp, so a pushed event arrives ordered against real ones rather than ahead of them. ```teal function tecs.platform.events.push(kind: string, fields: Event) ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `kind` | `string` | One of `events.kinds()`. An unrecognized name raises. | | `fields` | [`Event`](/modules/platform/events/#tecs.platform.events.Event) | | #### Returns None. ### tecs.platform.events.source Static Caller-writable. Installs a replay driver in place of the host queue. The driver invokes the handler for each recorded event. ```teal function tecs.platform.events.source(handler: function(Event)) ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `handler` | `function(`[`Event`](/modules/platform/events/#tecs.platform.events.Event)`)` | Receives each event the replay source supplies. | #### Returns None. ### tecs.platform.events.typeOf Static Returns the event type for a kind, or nil for an unknown name. What `on` is for code holding a kind it cannot spell in source, which is the conversion itself and the debug tools. ```teal function tecs.platform.events.typeOf(kind: string): Event ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `kind` | `string` | A kind name as `on` spells it. Names are this build's, so a kind SDL added later is unknown here rather than an error. | #### Returns | Type | Description | | --- | --- | | [`Event`](/modules/platform/events/#tecs.platform.events.Event) | The event type to observe, or nil for a name this build does not carry. | ## Values ### tecs.platform.events.on variable Read-only. Exposes one ECS event type per kind, to observe at address zero. See `events.on`. ```teal tecs.platform.events.on: KindTypes ``` --- ## tecs.platform # tecs.platform `tecs.platform` groups facilities supplied by the host. Naming it loads nothing; reading one of its child modules loads that facility without loading its siblings. ## Module contents ### Submodules | Submodule | Description | | --- | --- | | [`tecs.platform.events`](/modules/platform/events/) | Typed platform events delivered through the ECS event bus | | [`tecs.platform.os`](/modules/platform/os/) | Runtime capabilities, process signals, clipboard access, and desktop services | | [`tecs.platform.time`](/modules/platform/time/) | Platform clocks, calendar conversion, delays, and frame timing | | [`tecs.platform.window`](/modules/platform/window/) | Creating an OS window, choosing display modes, and using screen coordinates and pixels correctly | --- ## tecs.platform.os # tecs.platform.os Runtime capabilities, clipboard access, and desktop services. ## Capabilities Ask the installed build instead of inferring support from a target name. `capabilities` distinguishes targets that share an operating system but differ in JIT, worker, shader, storage, or device support. It reports the process sandbox separately because that environment can restrict filesystem and child process access without changing the target. ## Clipboard The clipboard belongs to the desktop and can change between frames. `events.clipboardUpdate` reports a change; read the current value when the event arrives or when the user requests paste. Text stays UTF-8, keeps its line endings, and stops at the first NUL. `clipboardData` preserves NULs. Headless builds report the clipboard as unavailable. Primary selection remains independent of the clipboard. On platforms without a shared primary selection, reads may return only the value this process wrote. ## Process signals SDL applications already receive `SIGINT` and `SIGTERM` as the public `tecs.platform.events.on.quit` event and then run their normal shutdown lifecycle. A headless loop can retain a [`SignalListener`](/modules/platform/os/#tecs.platform.os.SignalListener) instead. `next` transfers native signal flags and drains one without waiting: ```teal tecs.scoped( "listen for signals", function(scope: tecs.Scope) local signals = scope:own( tecs.platform.os.newSignalListener() ) local running = true while running do local signal = signals:next() if signal == "interrupt" or signal == "terminate" then running = false end end end ) ``` The native handler only sets atomic flags. It never calls Lua, allocates, or locks. Repeated instances of one signal may coalesce before the next listener read, as operating-system signals ordinarily may. When different signals are pending in one read, `next` returns them in enum order rather than claiming an arrival order the operating system did not preserve. Closing the last listener stops interception, so a later signal follows the platform's termination behavior again. ## Native dialogs File and folder dialogs return their result directly. Inside a system they suspend the logical world update while the operating system owns the dialog; outside an update they block while pumping the native bridge. The Rust bridge copies the platform callback result into a queue, and Lua observes it only from the Application or the blocking call. ## Module contents ### Constructors | Constructor | Description | | --- | --- | | [`newSignalListener`](/modules/platform/os/#tecs.platform.os.newSignalListener) | Creates a listener for process signals in a headless loop. | ### Types | Type | Kind | Description | | --- | --- | --- | | [`Capabilities`](/modules/platform/os/#tecs.platform.os.Capabilities) | record | Describes what this build can do on this target. | | [`DialogFilter`](/modules/platform/os/#tecs.platform.os.DialogFilter) | record | Describes one file-type filter. | | [`DialogOptions`](/modules/platform/os/#tecs.platform.os.DialogOptions) | record | Describes dialog configuration. | | [`DialogResult`](/modules/platform/os/#tecs.platform.os.DialogResult) | record | Describes a completed dialog. | | [`Locale`](/modules/platform/os/#tecs.platform.os.Locale) | record | Describes one language the user prefers. | | [`Power`](/modules/platform/os/#tecs.platform.os.Power) | record | Describes the machine's power source and remaining charge. | | [`ProcessSignal`](/modules/platform/os/#tecs.platform.os.ProcessSignal) | enum | ProcessSignal identifies an interrupt, termination, hangup, or quit request sent to this process. | | [`SignalListener`](/modules/platform/os/#tecs.platform.os.SignalListener) | interface | A SignalListener receives process signals in a headless loop. | ### Functions | Function | Kind | Description | | --- | --- | --- | | [`capabilities`](/modules/platform/os/#tecs.platform.os.capabilities) | Static | Reads the capabilities of the running build. | | [`clearClipboard`](/modules/platform/os/#tecs.platform.os.clearClipboard) | Static | Withdraws what this application put on the system. | | [`clipboardAvailable`](/modules/platform/os/#tecs.platform.os.clipboardAvailable) | Static | Reports whether a clipboard is available. | | [`clipboardData`](/modules/platform/os/#tecs.platform.os.clipboardData) | Static | Returns the clipboard bytes for mimeType, or nil when it offers none. | | [`clipboardMimeTypes`](/modules/platform/os/#tecs.platform.os.clipboardMimeTypes) | Static | Returns the clipboard MIME types in platform order. | | [`clipboardText`](/modules/platform/os/#tecs.platform.os.clipboardText) | Static | Returns the clipboard text, or an empty string when it holds none. | | [`hasClipboardData`](/modules/platform/os/#tecs.platform.os.hasClipboardData) | Static | Reports whether the clipboard offers mimeType. | | [`hasClipboardText`](/modules/platform/os/#tecs.platform.os.hasClipboardText) | Static | Reports whether the clipboard holds text. | | [`hasPrimarySelection`](/modules/platform/os/#tecs.platform.os.hasPrimarySelection) | Static | Reports whether the primary selection holds text. | | [`messageBox`](/modules/platform/os/#tecs.platform.os.messageBox) | Static | Shows a native informational, warning or error dialog. | | [`openFile`](/modules/platform/os/#tecs.platform.os.openFile) | Static | Opens a native file picker and returns its selection. | | [`openFolder`](/modules/platform/os/#tecs.platform.os.openFolder) | Static | Opens a native folder picker and returns its selection. | | [`openURL`](/modules/platform/os/#tecs.platform.os.openURL) | Static | Opens an absolute URI with the operating system's preferred application. | | [`power`](/modules/platform/os/#tecs.platform.os.power) | Static | Returns the current battery or external-power state. | | [`preferredLocales`](/modules/platform/os/#tecs.platform.os.preferredLocales) | Static | Returns the user's preferred locales in priority order. | | [`primarySelection`](/modules/platform/os/#tecs.platform.os.primarySelection) | Static | Returns text from the platform's primary selection. | | [`resetCapabilities`](/modules/platform/os/#tecs.platform.os.resetCapabilities) | Static | Forgets the cached answer. | | [`saveFile`](/modules/platform/os/#tecs.platform.os.saveFile) | Static | Opens a native save-file picker and returns at most one path. | | [`setClipboardText`](/modules/platform/os/#tecs.platform.os.setClipboardText) | Static | Puts text on the clipboard, replacing whatever was there. | | [`setPrimarySelection`](/modules/platform/os/#tecs.platform.os.setPrimarySelection) | Static | Puts text in the primary selection. | ## Constructors ### tecs.platform.os.newSignalListener Static Creates a listener for process signals in a headless loop. The listener receives `"interrupt"` for `SIGINT`, `"terminate"` for `SIGTERM`, `"hangup"` for `SIGHUP`, and `"quit"` for `SIGQUIT` on POSIX. Windows maps Ctrl-C and Ctrl-Break to `"interrupt"`, and console close, logoff, and shutdown to `"terminate"`. Unsupported signals simply never arrive on that platform. SDL applications normally observe `tecs.platform.events.on.quit` instead. SDL converts SIGINT and SIGTERM to that event and the application performs its normal shutdown lifecycle before the process-wide runtime pump. ```teal function tecs.platform.os.newSignalListener( selected: {ProcessSignal} ): SignalListener, string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `selected` | `{`[`ProcessSignal`](/modules/platform/os/#tecs.platform.os.ProcessSignal)`}` | The caller supplies the signal names to retain, or omits the list to retain every supported name. An empty list and duplicate or unknown names raise as programmer errors. | #### Returns | Type | Description | | --- | --- | | [`SignalListener`](/modules/platform/os/#tecs.platform.os.SignalListener) | Returns a caller-owned listener. | | `string` | Returns the platform reason when native signal handling cannot be installed. | #### Examples ```teal tecs.scoped( "listen for signals", function(scope: tecs.Scope) local signals, reason = tecs.platform.os.newSignalListener({ "interrupt", "terminate" }) if signals == nil then error(reason) end scope:own(signals) local running = true local function update() local signal = signals:next() if signal ~= nil then running = false end end while running do update() end end ) ``` ## Types ### tecs.platform.os.Capabilities record Describes what this build can do on this target. ```teal record tecs.platform.os.Capabilities target: string architecture: string sandbox: string jit: boolean ffi: boolean dynamicLibraries: boolean hotReload: boolean runtimeShaders: boolean packagedShaders: boolean shaderFormats: {string} touch: boolean gamepad: boolean sensors: boolean workers: boolean cores: integer writableStorage: boolean end ``` #### tecs.platform.os.Capabilities.target field Read-only. This is the platform name, such as "macOS" or "Android". A licensed port answers with its own name instead, since everything here describes the platform actually installed. ```teal tecs.platform.os.Capabilities.target: string ``` #### tecs.platform.os.Capabilities.architecture field Read-only. This is the CPU architecture this build targets. ```teal tecs.platform.os.Capabilities.architecture: string ``` #### tecs.platform.os.Capabilities.sandbox field Read-only. Reports `"none"`, `"unknownContainer"`, `"flatpak"`, `"snap"` or `"macOS"` for the process environment SDL detects. A sandbox can restrict filesystem and child-process access independently of the target and the build's other capabilities. ```teal tecs.platform.os.Capabilities.sandbox: string ``` #### tecs.platform.os.Capabilities.jit field Read-only. This reports whether the runtime generates machine code. False on a target that requires interpretation. ```teal tecs.platform.os.Capabilities.jit: boolean ``` #### tecs.platform.os.Capabilities.ffi field Read-only. This is always true because every supported build requires the FFI. ```teal tecs.platform.os.Capabilities.ffi: boolean ``` #### tecs.platform.os.Capabilities.dynamicLibraries field Read-only. Reports whether a library can load by name at run time. It is false where every build links every library into the executable. ```teal tecs.platform.os.Capabilities.dynamicLibraries: boolean ``` #### tecs.platform.os.Capabilities.hotReload field Read-only. Reports whether this build supports development-time content reload. Named independently of any one kind the watcher reloads. ```teal tecs.platform.os.Capabilities.hotReload: boolean ``` #### tecs.platform.os.Capabilities.runtimeShaders field Read-only. Reports whether this build can compile shaders from source at run time. ```teal tecs.platform.os.Capabilities.runtimeShaders: boolean ``` #### tecs.platform.os.Capabilities.packagedShaders field Read-only. Reports whether the engine reads shaders from a packaged artifact. Independent of `runtimeShaders`: a development build may have both, and a release has only this. ```teal tecs.platform.os.Capabilities.packagedShaders: boolean ``` #### tecs.platform.os.Capabilities.shaderFormats field Read-only. Lists the shader formats this target consumes. It contains one entry, since a build supplies one format. ```teal tecs.platform.os.Capabilities.shaderFormats: {string} ``` #### tecs.platform.os.Capabilities.touch field Read-only. Reports whether the machine currently has a touch device. This describes the machine rather than of the target: a desktop with a touchscreen has one. ```teal tecs.platform.os.Capabilities.touch: boolean ``` #### tecs.platform.os.Capabilities.gamepad field Read-only. Reports true because every target reaches gamepads through the same subsystem, so only the connected devices vary, which the `gamepads` method on [`Input`](/modules/input/#tecs.input.Input) answers. ```teal tecs.platform.os.Capabilities.gamepad: boolean ``` #### tecs.platform.os.Capabilities.sensors field Read-only. Reports whether the device carries a gyroscope or an accelerometer. This says nothing about a gamepad's sensors, which `hasSensor` on [`Gamepad`](/modules/input/#tecs.input.Gamepad) answers per device. ```teal tecs.platform.os.Capabilities.sensors: boolean ``` #### tecs.platform.os.Capabilities.workers field Read-only. Reports whether work can run off the main thread. ```teal tecs.platform.os.Capabilities.workers: boolean ``` #### tecs.platform.os.Capabilities.cores field Read-only. Reports the number of logical cores for sizing a worker pool. ```teal tecs.platform.os.Capabilities.cores: integer ``` #### tecs.platform.os.Capabilities.writableStorage field Read-only. Reports true because every target has somewhere to write, and `tecs.io.files.preferencePath` and `tecs.io.files.cachePath` answer where: the field exists so a caller can ask rather than assume, not because a target answers no. ```teal tecs.platform.os.Capabilities.writableStorage: boolean ``` ### tecs.platform.os.DialogFilter record Describes one file-type filter. ```teal record tecs.platform.os.DialogFilter name: string pattern: string end ``` #### tecs.platform.os.DialogFilter.name field Caller-writable. Sets the label shown beside the filter, such as `"Images"`. ```teal tecs.platform.os.DialogFilter.name: string ``` #### tecs.platform.os.DialogFilter.pattern field Caller-writable. Sets semicolon-separated extensions, such as `"png;jpg;jpeg"`. ```teal tecs.platform.os.DialogFilter.pattern: string ``` ### tecs.platform.os.DialogOptions record Describes dialog configuration. ```teal record tecs.platform.os.DialogOptions window: Window filters: {DialogFilter} defaultLocation: string multiple: boolean end ``` #### tecs.platform.os.DialogOptions.window field Caller-writable. Sets the window that owns the dialog. ```teal tecs.platform.os.DialogOptions.window: Window ``` #### tecs.platform.os.DialogOptions.filters field Caller-writable. Sets the available file-type filters. ```teal tecs.platform.os.DialogOptions.filters: {DialogFilter} ``` #### tecs.platform.os.DialogOptions.defaultLocation field Caller-writable. Sets the initial file or folder. Omit it to let the platform choose. ```teal tecs.platform.os.DialogOptions.defaultLocation: string ``` #### tecs.platform.os.DialogOptions.multiple field Caller-writable. Allows the user to select more than one path. ```teal tecs.platform.os.DialogOptions.multiple: boolean ``` ### tecs.platform.os.DialogResult record Describes a completed dialog. ```teal record tecs.platform.os.DialogResult paths: {string} filter: integer canceled: boolean end ``` #### tecs.platform.os.DialogResult.paths field Read-only. Contains selected paths. It is empty when the user canceled. ```teal tecs.platform.os.DialogResult.paths: {string} ``` #### tecs.platform.os.DialogResult.filter field Read-only. Reports the one-based selected filter, or zero when no filter applies. ```teal tecs.platform.os.DialogResult.filter: integer ``` #### tecs.platform.os.DialogResult.canceled field Read-only. Reports whether the user canceled the dialog. ```teal tecs.platform.os.DialogResult.canceled: boolean ``` ### tecs.platform.os.Locale record Describes one language the user prefers. ```teal record tecs.platform.os.Locale language: string country: string end ``` #### tecs.platform.os.Locale.language field Read-only. Contains an ISO 639 language code, such as `"en"`. ```teal tecs.platform.os.Locale.language: string ``` #### tecs.platform.os.Locale.country field Read-only. Contains an ISO 3166 country code, such as `"US"`, or an empty string when the platform names only a language. ```teal tecs.platform.os.Locale.country: string ``` ### tecs.platform.os.Power record Describes the machine's power source and remaining charge. ```teal record tecs.platform.os.Power state: string seconds: integer percent: integer end ``` #### tecs.platform.os.Power.state field Read-only. Reports `"unknown"`, `"onBattery"`, `"noBattery"`, `"charging"`, `"charged"` or `"error"`. ```teal tecs.platform.os.Power.state: string ``` #### tecs.platform.os.Power.seconds field Read-only. Reports seconds of battery life remaining, or -1 when unavailable. ```teal tecs.platform.os.Power.seconds: integer ``` #### tecs.platform.os.Power.percent field Read-only. Reports charge percentage in 0..100, or -1 when unavailable. ```teal tecs.platform.os.Power.percent: integer ``` ### tecs.platform.os.ProcessSignal enum `ProcessSignal` identifies an interrupt, termination, hangup, or quit request sent to this process. ```teal enum tecs.platform.os.ProcessSignal "hangup" "interrupt" "quit" "terminate" end ``` ### tecs.platform.os.SignalListener interface A `SignalListener` receives process signals in a headless loop. ```teal interface tecs.platform.os.SignalListener is Closeable isClosed: function(self): boolean next: function(self): ProcessSignal pending: function(self): integer end ``` #### Interfaces | Interface | | --- | | [`Closeable`](/modules/#tecs.Closeable) | #### tecs.platform.os.SignalListener:isClosed Instance Returns whether this listener has been closed. ```teal function tecs.platform.os.SignalListener.isClosed(self): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `SignalListener` | The listener to inspect. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns true after `close`. | #### tecs.platform.os.SignalListener:next Instance Removes and returns one queued signal without waiting. The call returns nil when no selected signal has arrived since the listener was created or last drained. Signals transferred by the same runtime poll are returned in enum order, not arrival order. ```teal function tecs.platform.os.SignalListener.next(self): ProcessSignal ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `SignalListener` | The open listener whose queue advances. | ##### Returns | Type | Description | | --- | --- | | [`ProcessSignal`](/modules/platform/os/#tecs.platform.os.ProcessSignal) | Returns one selected signal, or nil when none is pending. | #### tecs.platform.os.SignalListener:pending Instance Returns the number of signals waiting to be read. Signals of the same kind may coalesce before a read. The count covers deliveries already transferred to this listener. ```teal function tecs.platform.os.SignalListener.pending(self): integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `SignalListener` | The open listener to inspect. | ##### Returns | Type | Description | | --- | --- | | `integer` | Returns the queued delivery count. | ## Functions ### tecs.platform.os.capabilities Static Reads the capabilities of the running build. The function caches its answer because these values do not change while a platform remains installed. The platform generation keys the cache, so installing a platform drops the value without anybody calling `resetCapabilities`. ```teal function tecs.platform.os.capabilities(): Capabilities ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | [`Capabilities`](/modules/platform/os/#tecs.platform.os.Capabilities) | The same table on every call until the platform changes. Shared, so a caller that means to keep it does not also mean to edit it. | ### tecs.platform.os.clearClipboard Static Withdraws what this application put on the system. The clipboard is left empty rather than restored to what preceded the write, because nothing anywhere remembers what that was. ```teal function tecs.platform.os.clearClipboard(): boolean ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | `boolean` | | ### tecs.platform.os.clipboardAvailable Static Reports whether a clipboard is available. False in a process that never brought up video, where every other function here answers empty or false. Distinguishes that from a clipboard that is simply empty, which no other return value can. ```teal function tecs.platform.os.clipboardAvailable(): boolean ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | `boolean` | | ### tecs.platform.os.clipboardData Static Returns the clipboard bytes for `mimeType`, or nil when it offers none. The caller releases the returned [`Buffer`](/modules/io/#tecs.io.Buffer). Returns nil rather than an empty buffer because the clipboard can offer a MIME type with no bytes, which differs from not offering that type. ```teal function tecs.platform.os.clipboardData(mimeType: string): IOBuffer ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `mimeType` | `string` | | #### Returns | Type | Description | | --- | --- | | [`IOBuffer`](/modules/io/#tecs.io.Buffer) | | ### tecs.platform.os.clipboardMimeTypes Static Returns the clipboard MIME types in platform order. The same list `clipboardUpdate` carries, for a caller that wants to ask rather than wait to be told. Empty when the clipboard is empty. ```teal function tecs.platform.os.clipboardMimeTypes(): {string} ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | `{string}` | | ### tecs.platform.os.clipboardText Static Returns the clipboard text, or an empty string when it holds none. Empty is also the answer when the clipboard holds something that is not text, and when there is no video. Ask `hasClipboardText` to tell those from text that is genuinely empty. ```teal function tecs.platform.os.clipboardText(): string ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | `string` | | ### tecs.platform.os.hasClipboardData Static Reports whether the clipboard offers `mimeType`. ```teal function tecs.platform.os.hasClipboardData(mimeType: string): boolean ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `mimeType` | `string` | | #### Returns | Type | Description | | --- | --- | | `boolean` | | ### tecs.platform.os.hasClipboardText Static Reports whether the clipboard holds text. Cheaper than reading it: no allocation crosses the boundary, and on a platform where a read negotiates with the owning application, no negotiation happens. ```teal function tecs.platform.os.hasClipboardText(): boolean ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | `boolean` | | ### tecs.platform.os.hasPrimarySelection Static Reports whether the primary selection holds text. ```teal function tecs.platform.os.hasPrimarySelection(): boolean ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | `boolean` | | ### tecs.platform.os.messageBox Static Shows a native informational, warning or error dialog. ```teal function tecs.platform.os.messageBox( kind: string, title: string, message: string, window: Window ): boolean, string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `kind` | `string` | | | `title` | `string` | | | `message` | `string` | | | `window` | `Window` | | #### Returns | Type | Description | | --- | --- | | `boolean` | | | `string` | | ### tecs.platform.os.openFile Static Opens a native file picker and returns its selection. Inside a system, the call suspends until the dialog closes. Outside a world update, it blocks while pumping the native bridge. ```teal function tecs.platform.os.openFile(options: DialogOptions): DialogResult ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `options` | [`DialogOptions`](/modules/platform/os/#tecs.platform.os.DialogOptions) | The caller supplies the owner window, filters, starting location, and multiple-selection flag, or omits every option. | #### Returns | Type | Description | | --- | --- | | [`DialogResult`](/modules/platform/os/#tecs.platform.os.DialogResult) | Returns selected paths and filter state. Cancellation returns a result with `canceled` true rather than failing. | ### tecs.platform.os.openFolder Static Opens a native folder picker and returns its selection. Inside a system, the call suspends until the dialog closes. Outside a world update, it blocks while pumping the native bridge. ```teal function tecs.platform.os.openFolder( options: DialogOptions ): DialogResult ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `options` | [`DialogOptions`](/modules/platform/os/#tecs.platform.os.DialogOptions) | The caller supplies the owner window, starting location, and multiple-selection flag, or omits every option. Filters are ignored. | #### Returns | Type | Description | | --- | --- | | [`DialogResult`](/modules/platform/os/#tecs.platform.os.DialogResult) | Returns selected folders. Cancellation returns a result with `canceled` true rather than failing. | ### tecs.platform.os.openURL Static Opens an absolute URI with the operating system's preferred application. This is not limited to web URLs. The operating system may route `https:` to a browser, `mailto:` to a mail client, and application-specific schemes such as `steam:` to their registered handler. Tecs passes the string to SDL without parsing it, so the platform decides which schemes it accepts and which application handles them. ```teal function tecs.platform.os.openURL(url: string): boolean, string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `url` | `string` | The caller supplies a non-empty absolute URI. Embedded NUL bytes are not supported by SDL's C-string boundary. | #### Returns | Type | Description | | --- | --- | | `boolean` | Returns true after the operating system accepts the launch request. | | `string` | Returns SDL's reason when the URI is empty, unsupported, malformed, or cannot be opened. | ### tecs.platform.os.power Static Returns the current battery or external-power state. ```teal function tecs.platform.os.power(): Power ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | [`Power`](/modules/platform/os/#tecs.platform.os.Power) | | ### tecs.platform.os.preferredLocales Static Returns the user's preferred locales in priority order. ```teal function tecs.platform.os.preferredLocales(): {Locale} ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | `{`[`Locale`](/modules/platform/os/#tecs.platform.os.Locale)`}` | | ### tecs.platform.os.primarySelection Static Returns text from the platform's primary selection. On X11 and Wayland, selecting text places it in a clipboard separate from the ordinary copy-and-paste clipboard, commonly pasted with the middle mouse button. Platforms without that concept expose only the value this process last supplied through `setPrimarySelection`; other applications do not see it. This function does not read `clipboardText`. ```teal function tecs.platform.os.primarySelection(): string ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | `string` | Returns UTF-8 text, or an empty string when the selection is empty or no video subsystem is available. | ### tecs.platform.os.resetCapabilities Static Forgets the cached answer. Capability lookup notices a platform change without this. ```teal function tecs.platform.os.resetCapabilities() ``` #### Arguments None. #### Returns None. ### tecs.platform.os.saveFile Static Opens a native save-file picker and returns at most one path. Inside a system, the call suspends until the dialog closes. Outside a world update, it blocks while pumping the native bridge. ```teal function tecs.platform.os.saveFile(options: DialogOptions): DialogResult ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `options` | [`DialogOptions`](/modules/platform/os/#tecs.platform.os.DialogOptions) | The caller supplies the owner window, filters, and starting location, or omits every option. The `multiple` field is ignored. | #### Returns | Type | Description | | --- | --- | | [`DialogResult`](/modules/platform/os/#tecs.platform.os.DialogResult) | Returns the selected path and filter state. Cancellation returns a result with `canceled` true rather than failing. | ### tecs.platform.os.setClipboardText Static Puts `text` on the clipboard, replacing whatever was there. The platform copies the string before this call returns. Returns false when the platform refused, which includes having no video. ```teal function tecs.platform.os.setClipboardText(text: string): boolean ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `text` | `string` | | #### Returns | Type | Description | | --- | --- | | `boolean` | | ### tecs.platform.os.setPrimarySelection Static Puts `text` in the primary selection. Succeeds on a platform that has no shared primary selection, where the value remains visible only to this process. ```teal function tecs.platform.os.setPrimarySelection(text: string): boolean ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `text` | `string` | | #### Returns | Type | Description | | --- | --- | | `boolean` | | --- ## tecs.platform.time # tecs.platform.time Platform clocks, calendar time, blocking delays, and frame timing. The application calls `step` once per iteration and passes the result to the world. `maxDelta` clamps long stalls before simulation sees them. Game code may read `now`, but it must not call `step` because that moves the frame baseline. `now` is monotonic and is the clock for elapsed time and deadlines. `wallNow` reads the adjustable realtime clock instead and returns an exact Unix timestamp: ```teal local stamp = assert(tecs.platform.time.wallNow()) local localTime = assert( tecs.platform.time.toDateTime(stamp, true) ) print( ("%04d-%02d-%02d %02d:%02d"):format( localTime.year, localTime.month, localTime.day, localTime.hour, localTime.minute ) ) ``` The delay functions block the calling thread. They belong in startup, headless tools, and measurements, not in a frame. `delayPrecise` may busy wait to approach its requested duration. SDL's callback timer registration is deliberately absent. SDL invokes those callbacks from a timer thread, which cannot safely enter LuaJIT. Game timers store deadlines against `now` and dispatch from a system, or use `tecs.sequence` when they belong to simulated time. Install a provider to replace measured frame deltas during replay: ```teal local recorded : {number} = {1 / 60, 1 / 60, 1 / 30} local frame = 0 tecs.platform.time.provider = function(realDt: number): number frame = frame + 1 -- Nil keeps the measurement, so a run past the end of the recording is -- live again. return recorded[frame] end ``` Returning nil keeps the measured delta. Pair this provider with `tecs.platform.events.source` to replay frame timing and platform events together. `now` always reads the monotonic platform clock and ignores the replay provider. ## Module contents ### Types | Type | Kind | Description | | --- | --- | --- | | [`DateTime`](/modules/platform/time/#tecs.platform.time.DateTime) | record | Represents a calendar date, time, and UTC offset. | | [`LocalePreferences`](/modules/platform/time/#tecs.platform.time.LocalePreferences) | record | Represents the current locale's preferred date and clock layouts. | | [`Timestamp`](/modules/platform/time/#tecs.platform.time.Timestamp) | record | Represents an exact instant on the Unix realtime clock. | | [`WindowsTime`](/modules/platform/time/#tecs.platform.time.WindowsTime) | record | Represents the two exact words of a Windows FILETIME value. | ### Functions | Function | Kind | Description | | --- | --- | --- | | [`dayOfWeek`](/modules/platform/time/#tecs.platform.time.dayOfWeek) | Static | Returns the weekday of a calendar date. | | [`dayOfYear`](/modules/platform/time/#tecs.platform.time.dayOfYear) | Static | Returns the zero-based day within a calendar year. | | [`daysInMonth`](/modules/platform/time/#tecs.platform.time.daysInMonth) | Static | Returns the number of days in a calendar month. | | [`delay`](/modules/platform/time/#tecs.platform.time.delay) | Static | Blocks the calling thread for at least a number of milliseconds. | | [`delayNanoseconds`](/modules/platform/time/#tecs.platform.time.delayNanoseconds) | Static | Blocks the calling thread for at least a number of nanoseconds. | | [`delayPrecise`](/modules/platform/time/#tecs.platform.time.delayPrecise) | Static | Blocks while approaching a nanosecond delay as closely as possible. | | [`fromDateTime`](/modules/platform/time/#tecs.platform.time.fromDateTime) | Static | Converts calendar fields to an exact Unix timestamp. | | [`fromWindowsTime`](/modules/platform/time/#tecs.platform.time.fromWindowsTime) | Static | Converts a Windows FILETIME to an exact Unix timestamp. | | [`localePreferences`](/modules/platform/time/#tecs.platform.time.localePreferences) | Static | Reads the current locale's preferred date order and clock layout. | | [`microsecondsToNanoseconds`](/modules/platform/time/#tecs.platform.time.microsecondsToNanoseconds) | Static | Converts whole microseconds to nanoseconds. | | [`millisecondsToNanoseconds`](/modules/platform/time/#tecs.platform.time.millisecondsToNanoseconds) | Static | Converts whole milliseconds to nanoseconds. | | [`nanosecondsToMicroseconds`](/modules/platform/time/#tecs.platform.time.nanosecondsToMicroseconds) | Static | Converts nanoseconds to microseconds. | | [`nanosecondsToMilliseconds`](/modules/platform/time/#tecs.platform.time.nanosecondsToMilliseconds) | Static | Converts nanoseconds to milliseconds. | | [`nanosecondsToSeconds`](/modules/platform/time/#tecs.platform.time.nanosecondsToSeconds) | Static | Converts nanoseconds to seconds. | | [`now`](/modules/platform/time/#tecs.platform.time.now) | Static | Reads the monotonic counter in seconds. | | [`performanceCounter`](/modules/platform/time/#tecs.platform.time.performanceCounter) | Static | Reads the platform's raw high-resolution counter. | | [`performanceFrequency`](/modules/platform/time/#tecs.platform.time.performanceFrequency) | Static | Reads the frequency of the platform's high-resolution counter. | | [`provider`](/modules/platform/time/#tecs.platform.time.provider) | Static | Caller-writable. Installs a replay hook that receives the measured frame delta. | | [`reset`](/modules/platform/time/#tecs.platform.time.reset) | Static | Resets the baseline so the next step measures from now. | | [`secondsToNanoseconds`](/modules/platform/time/#tecs.platform.time.secondsToNanoseconds) | Static | Converts whole seconds to nanoseconds. | | [`step`](/modules/platform/time/#tecs.platform.time.step) | Static | Advances one frame and returns the dt the world should receive. | | [`ticksMilliseconds`](/modules/platform/time/#tecs.platform.time.ticksMilliseconds) | Static | Reads milliseconds elapsed since SDL initialized. | | [`ticksNanoseconds`](/modules/platform/time/#tecs.platform.time.ticksNanoseconds) | Static | Reads nanoseconds elapsed since SDL initialized. | | [`toDateTime`](/modules/platform/time/#tecs.platform.time.toDateTime) | Static | Converts an exact Unix timestamp to calendar fields. | | [`toWindowsTime`](/modules/platform/time/#tecs.platform.time.toWindowsTime) | Static | Converts an exact Unix timestamp to a Windows FILETIME. | | [`wallNow`](/modules/platform/time/#tecs.platform.time.wallNow) | Static | Reads the adjustable realtime clock. | ### Values | Value | Type | Description | | --- | --- | --- | | [`maxDelta`](/modules/platform/time/#tecs.platform.time.maxDelta) | `number` | Caller-writable. Sets the longest frame delta in seconds that step returns. | | [`microsecondsPerSecond`](/modules/platform/time/#tecs.platform.time.microsecondsPerSecond) | `integer` | Read-only. Reports the number of microseconds in one second. | | [`millisecondsPerSecond`](/modules/platform/time/#tecs.platform.time.millisecondsPerSecond) | `integer` | Read-only. Reports the number of milliseconds in one second. | | [`nanosecondsPerMicrosecond`](/modules/platform/time/#tecs.platform.time.nanosecondsPerMicrosecond) | `integer` | Read-only. Reports the number of nanoseconds in one microsecond. | | [`nanosecondsPerMillisecond`](/modules/platform/time/#tecs.platform.time.nanosecondsPerMillisecond) | `integer` | Read-only. Reports the number of nanoseconds in one millisecond. | | [`nanosecondsPerSecond`](/modules/platform/time/#tecs.platform.time.nanosecondsPerSecond) | `integer` | Read-only. Reports the number of nanoseconds in one second. | | [`nominal`](/modules/platform/time/#tecs.platform.time.nominal) | `number` | Caller-writable. Sets the nominal frame delta in seconds. | ## Types ### tecs.platform.time.DateTime record Represents a calendar date, time, and UTC offset. Read-only. Exposes the calendar date and time record. ```teal record tecs.platform.time.DateTime year: integer month: integer day: integer hour: integer minute: integer second: integer nanosecond: integer dayOfWeek: integer utcOffset: integer end ``` #### tecs.platform.time.DateTime.year field Caller-writable. Sets the calendar year. ```teal tecs.platform.time.DateTime.year: integer ``` #### tecs.platform.time.DateTime.month field Caller-writable. Sets the month from 1 through 12. ```teal tecs.platform.time.DateTime.month: integer ``` #### tecs.platform.time.DateTime.day field Caller-writable. Sets the day of the month from 1 through 31, bounded further by the selected month and year. ```teal tecs.platform.time.DateTime.day: integer ``` #### tecs.platform.time.DateTime.hour field Caller-writable. Sets the hour from 0 through 23. ```teal tecs.platform.time.DateTime.hour: integer ``` #### tecs.platform.time.DateTime.minute field Caller-writable. Sets the minute from 0 through 59. ```teal tecs.platform.time.DateTime.minute: integer ``` #### tecs.platform.time.DateTime.second field Caller-writable. Sets the second from 0 through 60. The last value represents a possible leap second. ```teal tecs.platform.time.DateTime.second: integer ``` #### tecs.platform.time.DateTime.nanosecond field Caller-writable. Sets the fractional second from 0 through 999999999 nanoseconds. ```teal tecs.platform.time.DateTime.nanosecond: integer ``` #### tecs.platform.time.DateTime.dayOfWeek field Read-only. Reports the weekday from 0 through 6, with Sunday at zero. `fromDateTime` ignores this field. ```teal tecs.platform.time.DateTime.dayOfWeek: integer ``` #### tecs.platform.time.DateTime.utcOffset field Caller-writable. Sets seconds east of UTC. A value of zero represents UTC, and `toDateTime` fills the platform's offset for local time. ```teal tecs.platform.time.DateTime.utcOffset: integer ``` ### tecs.platform.time.LocalePreferences record Represents the current locale's preferred date and clock layouts. Read-only. Exposes the locale preference record. ```teal record tecs.platform.time.LocalePreferences dateFormat: string timeFormat: string end ``` #### tecs.platform.time.LocalePreferences.dateFormat field Read-only. Reports `"yearMonthDay"`, `"dayMonthYear"` or `"monthDayYear"`. ```teal tecs.platform.time.LocalePreferences.dateFormat: string ``` #### tecs.platform.time.LocalePreferences.timeFormat field Read-only. Reports `"twentyFourHour"` or `"twelveHour"`. ```teal tecs.platform.time.LocalePreferences.timeFormat: string ``` ### tecs.platform.time.Timestamp record Represents an exact instant on the Unix realtime clock. Read-only. Exposes the exact Unix timestamp record. ```teal record tecs.platform.time.Timestamp seconds: integer nanosecond: integer end ``` #### tecs.platform.time.Timestamp.seconds field Caller-writable. Sets whole seconds since 1970-01-01 00:00:00 UTC. A negative value names an instant before the epoch. ```teal tecs.platform.time.Timestamp.seconds: integer ``` #### tecs.platform.time.Timestamp.nanosecond field Caller-writable. Sets the normalized remainder within `seconds`, from zero through 999999999. ```teal tecs.platform.time.Timestamp.nanosecond: integer ``` ### tecs.platform.time.WindowsTime record Represents the two exact words of a Windows FILETIME value. Read-only. Exposes the Windows FILETIME record. ```teal record tecs.platform.time.WindowsTime low: integer high: integer end ``` #### tecs.platform.time.WindowsTime.low field Caller-writable. Sets the low unsigned 32 bits. ```teal tecs.platform.time.WindowsTime.low: integer ``` #### tecs.platform.time.WindowsTime.high field Caller-writable. Sets the high unsigned 32 bits. ```teal tecs.platform.time.WindowsTime.high: integer ``` ## Functions ### tecs.platform.time.dayOfWeek Static Returns the weekday of a calendar date. ```teal function tecs.platform.time.dayOfWeek( year: integer, month: integer, day: integer ): integer, string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `year` | `integer` | The caller supplies the calendar year. | | `month` | `integer` | The caller supplies a month from 1 through 12. | | `day` | `integer` | The caller supplies a valid day within that month. | #### Returns | Type | Description | | --- | --- | | `integer` | A value from 0 through 6, with Sunday at zero, or nil for an invalid date. | | `string` | The SDL error when no value is returned. | ### tecs.platform.time.dayOfYear Static Returns the zero-based day within a calendar year. ```teal function tecs.platform.time.dayOfYear( year: integer, month: integer, day: integer ): integer, string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `year` | `integer` | The caller supplies the calendar year. | | `month` | `integer` | The caller supplies a month from 1 through 12. | | `day` | `integer` | The caller supplies a valid day within that month. | #### Returns | Type | Description | | --- | --- | | `integer` | A value from 0 through 365 on success, or nil for an invalid date. | | `string` | The SDL error when no value is returned. | ### tecs.platform.time.daysInMonth Static Returns the number of days in a calendar month. ```teal function tecs.platform.time.daysInMonth( year: integer, month: integer ): integer, string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `year` | `integer` | The caller supplies the year whose leap status controls February. | | `month` | `integer` | The caller supplies a month from 1 through 12. | #### Returns | Type | Description | | --- | --- | | `integer` | The number of days on success, or nil for an invalid month. | | `string` | The SDL error when no count is returned. | ### tecs.platform.time.delay Static Blocks the calling thread for at least a number of milliseconds. OS scheduling may make the delay longer. Do not call this from a frame. ```teal function tecs.platform.time.delay(milliseconds: integer) ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `milliseconds` | `integer` | The caller supplies a nonnegative integer through 4294967295. | #### Returns None. ### tecs.platform.time.delayNanoseconds Static Blocks the calling thread for at least a number of nanoseconds. OS scheduling may make the delay longer. Do not call this from a frame. ```teal function tecs.platform.time.delayNanoseconds(nanoseconds: number) ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `nanoseconds` | `number` | The caller supplies a nonnegative safe integer. | #### Returns None. ### tecs.platform.time.delayPrecise Static Blocks while approaching a nanosecond delay as closely as possible. SDL may busy wait, and OS scheduling may still make the delay longer. Do not call this from a frame. ```teal function tecs.platform.time.delayPrecise(nanoseconds: number) ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `nanoseconds` | `number` | The caller supplies a nonnegative safe integer. | #### Returns None. ### tecs.platform.time.fromDateTime Static Converts calendar fields to an exact Unix timestamp. `utcOffset` defaults to zero. `hour`, `minute`, `second`, and `nanosecond` also default to zero. SDL ignores `dayOfWeek`. ```teal function tecs.platform.time.fromDateTime( dateTime: DateTime ): Timestamp, string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `dateTime` | [`DateTime`](/modules/platform/time/#tecs.platform.time.DateTime) | The caller supplies a valid Gregorian calendar date and seconds east of UTC. | #### Returns | Type | Description | | --- | --- | | [`Timestamp`](/modules/platform/time/#tecs.platform.time.Timestamp) | A normalized timestamp on success, or nil for invalid fields or a date outside SDL's range. | | `string` | The SDL error when no timestamp is returned. | ### tecs.platform.time.fromWindowsTime Static Converts a Windows FILETIME to an exact Unix timestamp. SDL clamps a FILETIME outside its roughly 1677 through 2262 range. The resulting nanoseconds are a multiple of 100. ```teal function tecs.platform.time.fromWindowsTime( windowsTime: WindowsTime ): Timestamp ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `windowsTime` | [`WindowsTime`](/modules/platform/time/#tecs.platform.time.WindowsTime) | The caller supplies exact unsigned 32-bit words. | #### Returns | Type | Description | | --- | --- | | [`Timestamp`](/modules/platform/time/#tecs.platform.time.Timestamp) | A normalized timestamp. | ### tecs.platform.time.localePreferences Static Reads the current locale's preferred date order and clock layout. The platform query may be slow and preferences may change outside the process. A caller that needs the result frequently should cache it and refresh deliberately. ```teal function tecs.platform.time.localePreferences(): LocalePreferences, string ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | [`LocalePreferences`](/modules/platform/time/#tecs.platform.time.LocalePreferences) | A fresh preference record on success, or nil on failure. | | `string` | The SDL error when preferences are unavailable. | ### tecs.platform.time.microsecondsToNanoseconds Static Converts whole microseconds to nanoseconds. ```teal function tecs.platform.time.microsecondsToNanoseconds( microseconds: integer ): number ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `microseconds` | `integer` | The caller supplies a nonnegative integer no larger than 9007199254740 so the integer result remains exact. | #### Returns | Type | Description | | --- | --- | | `number` | The same duration in nanoseconds. | ### tecs.platform.time.millisecondsToNanoseconds Static Converts whole milliseconds to nanoseconds. ```teal function tecs.platform.time.millisecondsToNanoseconds( milliseconds: integer ): number ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `milliseconds` | `integer` | The caller supplies a nonnegative integer no larger than 9007199254 so the integer result remains exact. | #### Returns | Type | Description | | --- | --- | | `number` | The same duration in nanoseconds. | ### tecs.platform.time.nanosecondsToMicroseconds Static Converts nanoseconds to microseconds. ```teal function tecs.platform.time.nanosecondsToMicroseconds( nanoseconds: number ): number ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `nanoseconds` | `number` | The caller supplies any numeric duration. | #### Returns | Type | Description | | --- | --- | | `number` | The duration divided by one thousand. | ### tecs.platform.time.nanosecondsToMilliseconds Static Converts nanoseconds to milliseconds. ```teal function tecs.platform.time.nanosecondsToMilliseconds( nanoseconds: number ): number ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `nanoseconds` | `number` | The caller supplies any numeric duration. | #### Returns | Type | Description | | --- | --- | | `number` | The duration divided by one million. | ### tecs.platform.time.nanosecondsToSeconds Static Converts nanoseconds to seconds. ```teal function tecs.platform.time.nanosecondsToSeconds( nanoseconds: number ): number ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `nanoseconds` | `number` | The caller supplies any numeric duration. | #### Returns | Type | Description | | --- | --- | | `number` | The duration divided by one billion. | ### tecs.platform.time.now Static Reads the monotonic counter in seconds. Not affected by the provider. ```teal function tecs.platform.time.now(): number ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | `number` | A high-resolution elapsed-time reading with an arbitrary origin. | ### tecs.platform.time.performanceCounter Static Reads the platform's raw high-resolution counter. Prefer `now` for intervals. A raw counter may lose low bits when converted from SDL's unsigned 64-bit value to a Lua number. ```teal function tecs.platform.time.performanceCounter(): number ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | `number` | A platform-specific count with an arbitrary origin. | ### tecs.platform.time.performanceFrequency Static Reads the frequency of the platform's high-resolution counter. ```teal function tecs.platform.time.performanceFrequency(): number ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | `number` | Raw counter units per second. | ### tecs.platform.time.provider Static Caller-writable. Installs a replay hook that receives the measured frame delta. Returning a number replaces it; returning nil keeps it. ```teal function tecs.platform.time.provider(realDt: number): number ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `realDt` | `number` | The application supplies the measured frame delta in seconds. | #### Returns | Type | Description | | --- | --- | | `number` | A number replaces the measured delta, while nil keeps it. | ### tecs.platform.time.reset Static Resets the baseline so the next `step` measures from now. Called after startup so the first frame's dt excludes load time. ```teal function tecs.platform.time.reset() ``` #### Arguments None. #### Returns None. ### tecs.platform.time.secondsToNanoseconds Static Converts whole seconds to nanoseconds. ```teal function tecs.platform.time.secondsToNanoseconds( seconds: integer ): number ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `seconds` | `integer` | The caller supplies a nonnegative integer no larger than 9007199 so the integer result remains exact. | #### Returns | Type | Description | | --- | --- | | `number` | The same duration in nanoseconds. | ### tecs.platform.time.step Static Advances one frame and returns the dt the world should receive. Clamps to `maxDelta`, then offers the value to `provider`. ```teal function tecs.platform.time.step(): number ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | `number` | The elapsed simulation time for this frame, in seconds. | ### tecs.platform.time.ticksMilliseconds Static Reads milliseconds elapsed since SDL initialized. ```teal function tecs.platform.time.ticksMilliseconds(): number ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | `number` | A monotonic millisecond count. Its origin differs from `now`, so values from the two functions are not directly comparable. | ### tecs.platform.time.ticksNanoseconds Static Reads nanoseconds elapsed since SDL initialized. A Lua number stops distinguishing adjacent nanoseconds after about 104 days, while elapsed differences remain useful at the precision its magnitude permits. ```teal function tecs.platform.time.ticksNanoseconds(): number ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | `number` | A monotonic nanosecond count. Its origin differs from `now`, so values from the two functions are not directly comparable. | ### tecs.platform.time.toDateTime Static Converts an exact Unix timestamp to calendar fields. ```teal function tecs.platform.time.toDateTime( timestamp: Timestamp, localTime: boolean ): DateTime, string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `timestamp` | [`Timestamp`](/modules/platform/time/#tecs.platform.time.Timestamp) | The caller supplies a normalized value returned here or built within the range from about 1677 through 2262. | | `localTime` | `boolean` | The caller requests local time when true. Omitted or false requests UTC. | #### Returns | Type | Description | | --- | --- | | [`DateTime`](/modules/platform/time/#tecs.platform.time.DateTime) | A fresh calendar record on success, or nil for an invalid or unsupported timestamp. | | `string` | The validation or SDL error when no record is returned. | ### tecs.platform.time.toWindowsTime Static Converts an exact Unix timestamp to a Windows FILETIME. FILETIME counts 100-nanosecond intervals since 1601, so this conversion discards the last two decimal digits of `nanosecond`. ```teal function tecs.platform.time.toWindowsTime( timestamp: Timestamp ): WindowsTime, string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `timestamp` | [`Timestamp`](/modules/platform/time/#tecs.platform.time.Timestamp) | The caller supplies a normalized timestamp in SDL's representable range. | #### Returns | Type | Description | | --- | --- | | [`WindowsTime`](/modules/platform/time/#tecs.platform.time.WindowsTime) | The exact high and low FILETIME words, or nil for an invalid timestamp. | | `string` | The validation error when no words are returned. | ### tecs.platform.time.wallNow Static Reads the adjustable realtime clock. ```teal function tecs.platform.time.wallNow(): Timestamp, string ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | [`Timestamp`](/modules/platform/time/#tecs.platform.time.Timestamp) | An exact Unix timestamp on success, or nil on failure. | | `string` | The SDL error when no timestamp is available. | ## Values ### tecs.platform.time.maxDelta variable Caller-writable. Sets the longest frame delta in seconds that `step` returns. ```teal tecs.platform.time.maxDelta: number ``` ### tecs.platform.time.microsecondsPerSecond variable Read-only. Reports the number of microseconds in one second. ```teal tecs.platform.time.microsecondsPerSecond: integer ``` ### tecs.platform.time.millisecondsPerSecond variable Read-only. Reports the number of milliseconds in one second. ```teal tecs.platform.time.millisecondsPerSecond: integer ``` ### tecs.platform.time.nanosecondsPerMicrosecond variable Read-only. Reports the number of nanoseconds in one microsecond. ```teal tecs.platform.time.nanosecondsPerMicrosecond: integer ``` ### tecs.platform.time.nanosecondsPerMillisecond variable Read-only. Reports the number of nanoseconds in one millisecond. ```teal tecs.platform.time.nanosecondsPerMillisecond: integer ``` ### tecs.platform.time.nanosecondsPerSecond variable Read-only. Reports the number of nanoseconds in one second. ```teal tecs.platform.time.nanosecondsPerSecond: integer ``` ### tecs.platform.time.nominal variable Caller-writable. Sets the nominal frame delta in seconds. The application initializes it from the target frame rate. ```teal tecs.platform.time.nominal: number ``` --- ## tecs.platform.window # tecs.platform.window ## Windows `tecs.platform.window` creates OS windows and reads the displays around them. [`Application`](/modules/Application/) creates the usual game window as `app.window`. Tools and tests that call `newWindow` directly must call `destroy` when they finish. ```teal local window = tecs.platform.window.newWindow({ title = "Starfarer", width = 1600, height = 900, hidden = true, }) window:center() window:show() ``` ### Screen coordinates and pixels Window layout and pointer input use screen coordinates. Render targets use pixels. A high-density display may put several pixels behind one screen coordinate. ```teal local width , height = window:getSize() local pixelWidth , pixelHeight = window:getPixelSize() local density = window:pixelDensity() assert(pixelWidth == width * density) assert(pixelHeight == height * density) ``` `displayScale` reports the desktop's preferred scale for text and interface metrics. It does not convert between the two sizes. ### Compositor changes Size, position and fullscreen setters may return before the compositor applies the request. Most games consume the corresponding event later. Call `sync` only before an immediate readback. ```teal window:setSize(1280, 720) if window:sync() then local width , height = window:getSize() end ``` ### Fullscreen modes Fullscreen uses the desktop resolution when the caller selects no mode. Exclusive fullscreen accepts only a mode that `fullscreenModes` or `closestFullscreenMode` returns. ```teal local mode = tecs.platform.window.Window.closestFullscreenMode( savedWidth, savedHeight, savedRefreshRate, true ) if mode ~= nil then window:setFullscreenMode(mode) window:setFullscreen(true) end ``` ### Display placement Desktop positions span every attached display and may contain negative coordinates. Use usable bounds to keep windows clear of a taskbar, dock or menu bar. ```teal for _, displayId in ipairs(tecs.platform.window.Window.displays()) do local x , y , width , height = tecs.platform.window.Window.usableBounds( displayId ) end ``` Display and window events report changes. Getters report current state, including state that existed before the first event. Use `Window:id` to match a window event's `which` field. ### Custom window chrome A borderless window can return selected regions to the desktop for dragging and edge resizing. `setHitRegions` copies the complete list, so UI layout may reuse or change its records as soon as the call returns. The first region that contains a point wins. ```teal window:setHitRegions({ {kind = "draggable", x = 0, y = 0, width = 800, height = 40}, {kind = "resizeBottom", x = 0, y = 596, width = 800, height = 4}, }) ``` Hit testing runs inside the native window manager callback. Rust reads the copied regions there; SDL never calls a Lua function. ### Pointer ownership The window API owns mouse and keyboard confinement. `tecs.input` owns relative mouse mode, warping, capture and cursor visibility. The GPU device owns presentation pacing after it claims a window. ## Module contents ### Constructors | Constructor | Description | | --- | --- | | [`newWindow`](/modules/platform/window/#tecs.platform.window.newWindow) | Opens a window and raises when it cannot create a usable one. | ### Types | Type | Kind | Description | | --- | --- | --- | | [`DisplayMode`](/modules/platform/window/#tecs.platform.window.DisplayMode) | type | Describes one fullscreen mode a display offers. | | [`Flash`](/modules/platform/window/#tecs.platform.window.Flash) | type | Selects how a window asks for attention. | | [`HitRegion`](/modules/platform/window/#tecs.platform.window.HitRegion) | type | Describes one custom window region. | | [`HitRegionKind`](/modules/platform/window/#tecs.platform.window.HitRegionKind) | type | Selects how the desktop treats a custom window region. | | [`Options`](/modules/platform/window/#tecs.platform.window.Options) | type | Describes the caller-writable configuration that newWindow reads. | | [`Orientation`](/modules/platform/window/#tecs.platform.window.Orientation) | type | Describes which way up a display is. | | [`Progress`](/modules/platform/window/#tecs.platform.window.Progress) | type | Selects what a taskbar progress indicator shows. | | [`Theme`](/modules/platform/window/#tecs.platform.window.Theme) | type | Describes the desktop's light or dark preference. | | [`Window`](/modules/platform/window/#tecs.platform.window.Window) | record | A Window owns one open OS window and reads the displays around it. | ## Constructors ### tecs.platform.window.newWindow Static Opens a window and raises when it cannot create a usable one. It validates sizes and the icon before returning. Omitted fields take the defaults that [`Options`](/modules/platform/window/#tecs.platform.window.Window.Options) documents. ```teal function tecs.platform.window.newWindow(options: Window.Options): Window ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `options` | [`Window.Options`](/modules/platform/window/#tecs.platform.window.Window.Options) | The caller supplies the initial window configuration. | #### Returns | Type | Description | | --- | --- | | [`Window`](/modules/platform/window/#tecs.platform.window.Window) | Returns the caller-owned window. The caller releases it through `destroy`. | ## Types ### tecs.platform.window.DisplayMode type Describes one fullscreen mode a display offers. ```teal type tecs.platform.window.DisplayMode = Window.DisplayMode ``` ### tecs.platform.window.Flash type Selects how a window asks for attention. ```teal type tecs.platform.window.Flash = Window.Flash ``` ### tecs.platform.window.HitRegion type Describes one custom window region. ```teal type tecs.platform.window.HitRegion = Window.HitRegion ``` ### tecs.platform.window.HitRegionKind type Selects how the desktop treats a custom window region. ```teal type tecs.platform.window.HitRegionKind = Window.HitRegionKind ``` ### tecs.platform.window.Options type Describes the caller-writable configuration that `newWindow` reads. ```teal type tecs.platform.window.Options = Window.Options ``` ### tecs.platform.window.Orientation type Describes which way up a display is. ```teal type tecs.platform.window.Orientation = Window.Orientation ``` ### tecs.platform.window.Progress type Selects what a taskbar progress indicator shows. ```teal type tecs.platform.window.Progress = Window.Progress ``` ### tecs.platform.window.Theme type Describes the desktop's light or dark preference. ```teal type tecs.platform.window.Theme = Window.Theme ``` ### tecs.platform.window.Window record A `Window` owns one open OS window and reads the displays around it. Game code changes state through methods. Public fields expose framework state and remain read-only to callers. ```teal record tecs.platform.window.Window handle: loader.CPtr title: string enum Orientation "landscape" "landscapeFlipped" "portrait" "portraitFlipped" "unknown" end enum Theme "dark" "light" "unknown" end enum Flash "brief" "cancel" "untilFocused" end enum Progress "error" "indeterminate" "none" "normal" "paused" end enum HitRegionKind "draggable" "resizeBottom" "resizeBottomLeft" "resizeBottomRight" "resizeLeft" "resizeRight" "resizeTop" "resizeTopLeft" "resizeTopRight" end record HitRegion kind: HitRegionKind x: integer y: integer width: integer height: integer end record DisplayMode display: integer width: integer height: integer pixelDensity: number refreshRate: number end record Options title: string width: integer height: integer resizable: boolean highPixelDensity: boolean fullscreen: boolean borderless: boolean hidden: boolean alwaysOnTop: boolean transparent: boolean x: integer y: integer minWidth: integer minHeight: integer maxWidth: integer maxHeight: integer icon: string end closestFullscreenMode: function( width: integer, height: integer, refreshRate: number, highDensity: boolean, displayId: integer ): Window.DisplayMode contentScale: function(displayId: integer): number currentMode: function(displayId: integer): Window.DisplayMode desktopMode: function(displayId: integer): Window.DisplayMode displayBounds: function( displayId: integer ): integer, integer, integer, integer displayName: function(displayId: integer): string displays: function(): {integer} fullscreenModes: function(displayId: integer): {Window.DisplayMode} isSupported: function(): boolean naturalOrientation: function(displayId: integer): Window.Orientation orientation: function(displayId: integer): Window.Orientation primaryDisplay: function(): integer screenSaverEnabled: function(): boolean setScreenSaverEnabled: function(enabled: boolean): boolean theme: function(): Window.Theme usableBounds: function( displayId: integer ): integer, integer, integer, integer aspectRatio: function(self): number, number borderSize: function(self): integer, integer, integer, integer center: function(self, displayId: integer): boolean destroy: function(self) display: function(self): integer displayScale: function(self): number flash: function(self, operation: Window.Flash): boolean fullscreenMode: function(self): Window.DisplayMode getPixelSize: function(self): integer, integer getSize: function(self): integer, integer hasFocus: function(self): boolean hasMouseFocus: function(self): boolean hide: function(self): boolean id: function(self): integer isAlwaysOnTop: function(self): boolean isBordered: function(self): boolean isFocusable: function(self): boolean isFullscreen: function(self): boolean isMaximized: function(self): boolean isMinimized: function(self): boolean isOccluded: function(self): boolean isResizable: function(self): boolean isVisible: function(self): boolean keyboardGrab: function(self): boolean maximize: function(self): boolean maximumSize: function(self): integer, integer minimize: function(self): boolean minimumSize: function(self): integer, integer mouseGrab: function(self): boolean mouseRect: function(self): integer, integer, integer, integer opacity: function(self): number pixelDensity: function(self): number position: function(self): integer, integer progress: function(self): Window.Progress, number raise: function(self): boolean restore: function(self): boolean safeArea: function(self): integer, integer, integer, integer setAlwaysOnTop: function(self, onTop: boolean): boolean setAspectRatio: function( self, minimum: number, maximum: number ): boolean setBordered: function(self, bordered: boolean): boolean setFocusable: function(self, focusable: boolean): boolean setFullscreen: function(self, fullscreen: boolean): boolean setFullscreenMode: function(self, mode: Window.DisplayMode): boolean setHitRegions: function(self, regions: {Window.HitRegion}): boolean setIcon: function(self, path: string): boolean setKeyboardGrab: function(self, grabbed: boolean): boolean setMaximumSize: function( self, width: integer, height: integer ): boolean setMinimumSize: function( self, width: integer, height: integer ): boolean setMouseGrab: function(self, grabbed: boolean): boolean setMouseRect: function( self, x: integer, y: integer, width: integer, height: integer ): boolean setOpacity: function(self, opacity: number): boolean setPosition: function(self, x: integer, y: integer): boolean setProgress: function( self, state: Window.Progress, value: number ): boolean setResizable: function(self, resizable: boolean): boolean setSize: function(self, width: integer, height: integer): boolean setTitle: function(self, title: string): boolean show: function(self): boolean showSystemMenu: function(self, x: integer, y: integer): boolean sync: function(self): boolean end ``` #### tecs.platform.window.Window.handle field Engine-owned. The framework stores the native window handle here for the GPU device to claim for presentation and sets it to nil when `destroy` runs. Ordinary game code ignores this field. ```teal tecs.platform.window.Window.handle: loader.CPtr ``` #### tecs.platform.window.Window.title field Read-only. The framework stores the last title here when it creates the window or `setTitle` succeeds. Game code changes it through `setTitle`. ```teal tecs.platform.window.Window.title: string ``` #### tecs.platform.window.Window.Orientation enum `Orientation` describes a display's current or natural rotation. ```teal enum tecs.platform.window.Window.Orientation "landscape" "landscapeFlipped" "portrait" "portraitFlipped" "unknown" end ``` #### tecs.platform.window.Window.Theme enum `Theme` describes the desktop's requested appearance. ```teal enum tecs.platform.window.Window.Theme "dark" "light" "unknown" end ``` #### tecs.platform.window.Window.Flash enum `Flash` selects how long the desktop asks for attention. ```teal enum tecs.platform.window.Window.Flash "brief" "cancel" "untilFocused" end ``` #### tecs.platform.window.Window.Progress enum `Progress` selects what the taskbar or dock shows over the icon. ```teal enum tecs.platform.window.Window.Progress "error" "indeterminate" "none" "normal" "paused" end ``` #### tecs.platform.window.Window.HitRegionKind enum `HitRegionKind` selects how the desktop treats a window region. ```teal enum tecs.platform.window.Window.HitRegionKind "draggable" "resizeBottom" "resizeBottomLeft" "resizeBottomRight" "resizeLeft" "resizeRight" "resizeTop" "resizeTopLeft" "resizeTopRight" end ``` #### tecs.platform.window.Window.HitRegion record `HitRegion` gives part of a borderless window desktop behavior. ```teal record tecs.platform.window.Window.HitRegion kind: HitRegionKind x: integer y: integer width: integer height: integer end ``` ##### tecs.platform.window.Window.HitRegion.kind field Caller-writable. Selects dragging or one resize direction. ```teal tecs.platform.window.Window.HitRegion.kind: HitRegionKind ``` ##### tecs.platform.window.Window.HitRegion.x field Caller-writable. Sets the left edge in window client coordinates. It must be a non-negative integer. ```teal tecs.platform.window.Window.HitRegion.x: integer ``` ##### tecs.platform.window.Window.HitRegion.y field Caller-writable. Sets the top edge in window client coordinates. It must be a non-negative integer. ```teal tecs.platform.window.Window.HitRegion.y: integer ``` ##### tecs.platform.window.Window.HitRegion.width field Caller-writable. Sets a positive width in window client coordinates. ```teal tecs.platform.window.Window.HitRegion.width: integer ``` ##### tecs.platform.window.Window.HitRegion.height field Caller-writable. Sets a positive height in window client coordinates. ```teal tecs.platform.window.Window.HitRegion.height: integer ``` #### tecs.platform.window.Window.DisplayMode record `DisplayMode` describes one video mode returned by the platform. Callers treat every field as read-only and pass the record back to `setFullscreenMode`. ```teal record tecs.platform.window.Window.DisplayMode display: integer width: integer height: integer pixelDensity: number refreshRate: number end ``` ##### tecs.platform.window.Window.DisplayMode.display field Read-only. The platform sets `display` when it builds the mode. ```teal tecs.platform.window.Window.DisplayMode.display: integer ``` ##### tecs.platform.window.Window.DisplayMode.width field Read-only. The platform sets `width` in screen coordinates when it builds the mode. Callers multiply it by `pixelDensity` to obtain pixels. ```teal tecs.platform.window.Window.DisplayMode.width: integer ``` ##### tecs.platform.window.Window.DisplayMode.height field Read-only. The platform sets `height` in screen coordinates when it builds the mode. Callers multiply it by `pixelDensity` to obtain pixels. ```teal tecs.platform.window.Window.DisplayMode.height: integer ``` ##### tecs.platform.window.Window.DisplayMode.pixelDensity field Read-only. The platform sets `pixelDensity` when it builds the mode. A 1920 by 1080 mode at 2.0 draws 3840 by 2160 pixels. ```teal tecs.platform.window.Window.DisplayMode.pixelDensity: number ``` ##### tecs.platform.window.Window.DisplayMode.refreshRate field Read-only. The platform sets `refreshRate` in hertz when it builds the mode, or to zero when it cannot report a rate. ```teal tecs.platform.window.Window.DisplayMode.refreshRate: number ``` #### tecs.platform.window.Window.Options record Callers may set any `Options` field before passing the record to `newWindow`. The function reads the values and does not retain the record. ```teal record tecs.platform.window.Window.Options title: string width: integer height: integer resizable: boolean highPixelDensity: boolean fullscreen: boolean borderless: boolean hidden: boolean alwaysOnTop: boolean transparent: boolean x: integer y: integer minWidth: integer minHeight: integer maxWidth: integer maxHeight: integer icon: string end ``` ##### tecs.platform.window.Window.Options.title field Caller-writable. The caller sets `title` to the desktop title before `newWindow` reads it. It defaults to `"tecs"`. ```teal tecs.platform.window.Window.Options.title: string ``` ##### tecs.platform.window.Window.Options.width field Caller-writable. The caller sets `width` in screen coordinates before `newWindow` reads it. It defaults to 1280. ```teal tecs.platform.window.Window.Options.width: integer ``` ##### tecs.platform.window.Window.Options.height field Caller-writable. The caller sets `height` in screen coordinates before `newWindow` reads it. It defaults to 720. ```teal tecs.platform.window.Window.Options.height: integer ``` ##### tecs.platform.window.Window.Options.resizable field Caller-writable. The caller sets `resizable` before `newWindow` reads it. It defaults to true, and only false disables edge resizing. ```teal tecs.platform.window.Window.Options.resizable: boolean ``` ##### tecs.platform.window.Window.Options.highPixelDensity field Caller-writable. The caller sets `highPixelDensity` before `newWindow` reads it. It defaults to true and lets the drawable follow the display density instead of stretching. ```teal tecs.platform.window.Window.Options.highPixelDensity: boolean ``` ##### tecs.platform.window.Window.Options.fullscreen field Caller-writable. The caller sets `fullscreen` before `newWindow` reads it to start on the display selected by the window manager. It defaults to false. ```teal tecs.platform.window.Window.Options.fullscreen: boolean ``` ##### tecs.platform.window.Window.Options.borderless field Caller-writable. The caller sets `borderless` before `newWindow` reads it to remove the title bar and frame. It defaults to false. ```teal tecs.platform.window.Window.Options.borderless: boolean ``` ##### tecs.platform.window.Window.Options.hidden field Caller-writable. The caller sets `hidden` before `newWindow` reads it to defer mapping until `show`. It defaults to false. ```teal tecs.platform.window.Window.Options.hidden: boolean ``` ##### tecs.platform.window.Window.Options.alwaysOnTop field Caller-writable. The caller sets `alwaysOnTop` before `newWindow` reads it to keep the window above others. It defaults to false. ```teal tecs.platform.window.Window.Options.alwaysOnTop: boolean ``` ##### tecs.platform.window.Window.Options.transparent field Caller-writable. The caller sets `transparent` before `newWindow` reads it to request desktop alpha compositing. It defaults to false. ```teal tecs.platform.window.Window.Options.transparent: boolean ``` ##### tecs.platform.window.Window.Options.x field Caller-writable. The caller sets `x` with `y` before `newWindow` reads them. Without both fields, the window manager chooses the position. ```teal tecs.platform.window.Window.Options.x: integer ``` ##### tecs.platform.window.Window.Options.y field Caller-writable. The caller sets `y` with `x` before `newWindow` reads them. ```teal tecs.platform.window.Window.Options.y: integer ``` ##### tecs.platform.window.Window.Options.minWidth field Caller-writable. The caller sets `minWidth` with `minHeight` before `newWindow` reads them. ```teal tecs.platform.window.Window.Options.minWidth: integer ``` ##### tecs.platform.window.Window.Options.minHeight field Caller-writable. The caller sets `minHeight` with `minWidth` before `newWindow` reads them. ```teal tecs.platform.window.Window.Options.minHeight: integer ``` ##### tecs.platform.window.Window.Options.maxWidth field Caller-writable. The caller sets `maxWidth` with `maxHeight` before `newWindow` reads them. ```teal tecs.platform.window.Window.Options.maxWidth: integer ``` ##### tecs.platform.window.Window.Options.maxHeight field Caller-writable. The caller sets `maxHeight` with `maxWidth` before `newWindow` reads them. ```teal tecs.platform.window.Window.Options.maxHeight: integer ``` ##### tecs.platform.window.Window.Options.icon field Caller-writable. The caller sets `icon` to an image path before `newWindow` reads it. `newWindow` decodes it synchronously. ```teal tecs.platform.window.Window.Options.icon: string ``` #### tecs.platform.window.Window.closestFullscreenMode Static Finds the nearest fullscreen mode to a requested size. Use this when restoring a saved mode that the display may no longer offer exactly. ```teal function tecs.platform.window.Window.closestFullscreenMode( width: integer, height: integer, refreshRate: number, highDensity: boolean, displayId: integer ): Window.DisplayMode ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `width` | `integer` | The caller supplies the requested width in screen coordinates. | | `height` | `integer` | The caller supplies the requested height in screen coordinates. | | `refreshRate` | `number` | The caller supplies hertz, or zero or nil for the highest available rate. | | `highDensity` | `boolean` | The caller chooses whether the function may select modes above density 1. It defaults to false. | | `displayId` | `integer` | The caller omits this value to use the primary display. | ##### Returns | Type | Description | | --- | --- | | [`Window.DisplayMode`](/modules/platform/window/#tecs.platform.window.Window.DisplayMode) | Returns the nearest platform mode, or nil when none fits. | #### tecs.platform.window.Window.contentScale Static Reads a display's preferred content scale. ```teal function tecs.platform.window.Window.contentScale( displayId: integer ): number ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `displayId` | `integer` | The caller omits this value to use the primary display. | ##### Returns | Type | Description | | --- | --- | | `number` | Returns the scale, or zero without video. | #### tecs.platform.window.Window.currentMode Static Reads the display mode in use now. ```teal function tecs.platform.window.Window.currentMode( displayId: integer ): Window.DisplayMode ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `displayId` | `integer` | The caller omits this value to use the primary display. | ##### Returns | Type | Description | | --- | --- | | [`Window.DisplayMode`](/modules/platform/window/#tecs.platform.window.Window.DisplayMode) | Returns the mode, or nil without video. | #### tecs.platform.window.Window.desktopMode Static Reads the display mode the desktop started in. ```teal function tecs.platform.window.Window.desktopMode( displayId: integer ): Window.DisplayMode ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `displayId` | `integer` | The caller omits this value to use the primary display. | ##### Returns | Type | Description | | --- | --- | | [`Window.DisplayMode`](/modules/platform/window/#tecs.platform.window.Window.DisplayMode) | Returns the mode, or nil without video. | #### tecs.platform.window.Window.displayBounds Static Reads a display's desktop rectangle. ```teal function tecs.platform.window.Window.displayBounds( displayId: integer ): integer, integer, integer, integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `displayId` | `integer` | The caller omits this value to use the primary display. | ##### Returns | Type | Description | | --- | --- | | `integer` | Returns the horizontal position in desktop screen coordinates. | | `integer` | Returns the vertical position in desktop screen coordinates. | | `integer` | Returns the width in screen coordinates. | | `integer` | Returns the height in screen coordinates. | #### tecs.platform.window.Window.displayName Static Reads the desktop name for a display. ```teal function tecs.platform.window.Window.displayName( displayId: integer ): string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `displayId` | `integer` | The caller omits this value to use the primary display. | ##### Returns | Type | Description | | --- | --- | | `string` | Returns the platform name, a `"Display "` fallback when the platform supplies an empty label, or an empty string without video. | #### tecs.platform.window.Window.displays Static Lists attached displays in platform order. ```teal function tecs.platform.window.Window.displays(): {integer} ``` ##### Arguments None. ##### Returns | Type | Description | | --- | --- | | `{integer}` | Returns display ids, or an empty list without video. | #### tecs.platform.window.Window.fullscreenModes Static Lists the display's exclusive fullscreen modes, largest first. ```teal function tecs.platform.window.Window.fullscreenModes( displayId: integer ): {Window.DisplayMode} ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `displayId` | `integer` | The caller omits this value to use the primary display. | ##### Returns | Type | Description | | --- | --- | | `{`[`Window.DisplayMode`](/modules/platform/window/#tecs.platform.window.Window.DisplayMode)`}` | Returns modes accepted by `setFullscreenMode`, or an empty list without video. | #### tecs.platform.window.Window.isSupported Static Returns whether the platform can create and inspect windows. False in a headless process. Display and screen-saver functions answer harmless defaults there, while `newWindow` raises. ```teal function tecs.platform.window.Window.isSupported(): boolean ``` ##### Arguments None. ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns true when this process can create and inspect windows. | #### tecs.platform.window.Window.naturalOrientation Static Reads the orientation a display was built for. ```teal function tecs.platform.window.Window.naturalOrientation( displayId: integer ): Window.Orientation ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `displayId` | `integer` | The caller omits this value to use the primary display. | ##### Returns | Type | Description | | --- | --- | | [`Window.Orientation`](/modules/platform/window/#tecs.platform.window.Window.Orientation) | Returns the natural orientation, or `"unknown"` without video. | #### tecs.platform.window.Window.orientation Static Reads a display's current rotation. ```teal function tecs.platform.window.Window.orientation( displayId: integer ): Window.Orientation ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `displayId` | `integer` | The caller omits this value to use the primary display. | ##### Returns | Type | Description | | --- | --- | | [`Window.Orientation`](/modules/platform/window/#tecs.platform.window.Window.Orientation) | Returns the orientation, or `"unknown"` without video. | #### tecs.platform.window.Window.primaryDisplay Static Reads the desktop's primary display. ```teal function tecs.platform.window.Window.primaryDisplay(): integer ``` ##### Arguments None. ##### Returns | Type | Description | | --- | --- | | `integer` | Returns the display id, or zero without video. | #### tecs.platform.window.Window.screenSaverEnabled Static Returns whether the desktop may blank the display. Video startup disables the screen saver. Enable it for an unattended menu or cutscene. ```teal function tecs.platform.window.Window.screenSaverEnabled(): boolean ``` ##### Arguments None. ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns false without video. | #### tecs.platform.window.Window.setScreenSaverEnabled Static Allows or forbids the desktop blanking the display. ```teal function tecs.platform.window.Window.setScreenSaverEnabled( enabled: boolean ): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `enabled` | `boolean` | The caller chooses whether the screen saver may run. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns whether the platform accepted the change, or false without video. | #### tecs.platform.window.Window.theme Static Reads the desktop's light or dark preference. ```teal function tecs.platform.window.Window.theme(): Window.Theme ``` ##### Arguments None. ##### Returns | Type | Description | | --- | --- | | [`Window.Theme`](/modules/platform/window/#tecs.platform.window.Window.Theme) | Returns `"light"`, `"dark"` or `"unknown"`. | #### tecs.platform.window.Window.usableBounds Static Reads the display area not occupied by desktop chrome. ```teal function tecs.platform.window.Window.usableBounds( displayId: integer ): integer, integer, integer, integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `displayId` | `integer` | The caller omits this value to use the primary display. | ##### Returns | Type | Description | | --- | --- | | `integer` | Returns the horizontal position in desktop screen coordinates. | | `integer` | Returns the vertical position in desktop screen coordinates. | | `integer` | Returns the width in screen coordinates. | | `integer` | Returns the height in screen coordinates. | #### tecs.platform.window.Window:aspectRatio Instance Reads the allowed width-over-height range. ```teal function tecs.platform.window.Window.aspectRatio(self): number, number ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Window` | | ##### Returns | Type | Description | | --- | --- | | `number` | Returns the minimum ratio, or zero when unset. | | `number` | Returns the maximum ratio, or zero when unset. | #### tecs.platform.window.Window:borderSize Instance Reads the window decoration thickness in screen coordinates. ```teal function tecs.platform.window.Window.borderSize( self ): integer, integer, integer, integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Window` | | ##### Returns | Type | Description | | --- | --- | | `integer` | Returns the top thickness, or zero without decoration. | | `integer` | Returns the left thickness, or zero without decoration. | | `integer` | Returns the bottom thickness, or zero without decoration. | | `integer` | Returns the right thickness, or zero without decoration. | #### tecs.platform.window.Window:center Instance Centers the window on a display. ```teal function tecs.platform.window.Window.center( self, displayId: integer ): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Window` | | | `displayId` | `integer` | The caller omits this value to use the display the window occupies. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns whether the platform accepted the request. | #### tecs.platform.window.Window:destroy Instance Releases the window. Safe to call more than once. Every getter answers zero, false or nil after this call. ```teal function tecs.platform.window.Window.destroy(self) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Window` | | ##### Returns None. #### tecs.platform.window.Window:display Instance Reads the display this window mostly occupies. ```teal function tecs.platform.window.Window.display(self): integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Window` | | ##### Returns | Type | Description | | --- | --- | | `integer` | Returns the display id, or zero after `destroy`. | #### tecs.platform.window.Window:displayScale Instance Returns the desktop's preferred scale for content. This is not the ratio between `getSize` and `getPixelSize`; use `pixelDensity` for that. A theme or UI can multiply its natural metrics by this value. ```teal function tecs.platform.window.Window.displayScale(self): number ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Window` | | ##### Returns | Type | Description | | --- | --- | | `number` | Returns the scale, or zero after `destroy`. | #### tecs.platform.window.Window:flash Instance Asks for attention without taking focus. `"untilFocused"` keeps asking and `"cancel"` stops a request. An unknown operation raises. ```teal function tecs.platform.window.Window.flash( self, operation: Window.Flash ): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Window` | | | `operation` | [`Window.Flash`](/modules/platform/window/#tecs.platform.window.Window.Flash) | The caller chooses how long to ask for attention. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns whether the desktop accepted the request. | #### tecs.platform.window.Window:fullscreenMode Instance Reads the selected exclusive fullscreen mode. ```teal function tecs.platform.window.Window.fullscreenMode( self ): Window.DisplayMode ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Window` | | ##### Returns | Type | Description | | --- | --- | | [`Window.DisplayMode`](/modules/platform/window/#tecs.platform.window.Window.DisplayMode) | Returns the mode, or nil for borderless fullscreen and after `destroy`. | #### tecs.platform.window.Window:getPixelSize Instance Reads the drawable size in pixels. ```teal function tecs.platform.window.Window.getPixelSize( self ): integer, integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Window` | | ##### Returns | Type | Description | | --- | --- | | `integer` | Returns the pixel width, or zero after `destroy`. | | `integer` | Returns the pixel height, or zero after `destroy`. | #### tecs.platform.window.Window:getSize Instance Reads the window size in screen coordinates. Pointer positions use these units. Render targets use `getPixelSize`. ```teal function tecs.platform.window.Window.getSize(self): integer, integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Window` | | ##### Returns | Type | Description | | --- | --- | | `integer` | Returns the width, or zero after `destroy`. | | `integer` | Returns the height, or zero after `destroy`. | #### tecs.platform.window.Window:hasFocus Instance Returns whether keyboard events currently target this window. ```teal function tecs.platform.window.Window.hasFocus(self): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Window` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns false after `destroy`. | #### tecs.platform.window.Window:hasMouseFocus Instance Returns whether the pointer is over the window. Relative mouse mode has no pointer position and reports false. ```teal function tecs.platform.window.Window.hasMouseFocus(self): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Window` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns false after `destroy`. | #### tecs.platform.window.Window:hide Instance Unmaps the window without destroying it. A claimed swapchain yields no texture while the desktop hides the window. ```teal function tecs.platform.window.Window.hide(self): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Window` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns whether the platform hid the window. | #### tecs.platform.window.Window:id Instance Returns the id carried in a window event's `which` field. ```teal function tecs.platform.window.Window.id(self): integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Window` | | ##### Returns | Type | Description | | --- | --- | | `integer` | Returns the platform window id, or zero after `destroy`. | #### tecs.platform.window.Window:isAlwaysOnTop Instance Returns whether the desktop keeps the window above others. ```teal function tecs.platform.window.Window.isAlwaysOnTop(self): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Window` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns false after `destroy`. | #### tecs.platform.window.Window:isBordered Instance Returns whether the window has a title bar and frame. ```teal function tecs.platform.window.Window.isBordered(self): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Window` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns false after `destroy`. | #### tecs.platform.window.Window:isFocusable Instance Returns whether the window may take keyboard focus. ```teal function tecs.platform.window.Window.isFocusable(self): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Window` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns false after `destroy`. | #### tecs.platform.window.Window:isFullscreen Instance Returns whether the window occupies a fullscreen display. ```teal function tecs.platform.window.Window.isFullscreen(self): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Window` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns false after `destroy`. | #### tecs.platform.window.Window:isMaximized Instance Returns whether the window fills the display's usable bounds. ```teal function tecs.platform.window.Window.isMaximized(self): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Window` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns false after `destroy`. | #### tecs.platform.window.Window:isMinimized Instance Returns whether the window occupies its minimized state. ```teal function tecs.platform.window.Window.isMinimized(self): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Window` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns false after `destroy`. | #### tecs.platform.window.Window:isOccluded Instance Returns whether other windows cover this window completely. ```teal function tecs.platform.window.Window.isOccluded(self): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Window` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns false after `destroy`. | #### tecs.platform.window.Window:isResizable Instance Returns whether the user can resize the window by dragging an edge. ```teal function tecs.platform.window.Window.isResizable(self): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Window` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns false after `destroy`. | #### tecs.platform.window.Window:isVisible Instance Returns whether the desktop shows the window. Minimizing the window does not hide it. ```teal function tecs.platform.window.Window.isVisible(self): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Window` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns false for a hidden or destroyed window. | #### tecs.platform.window.Window:keyboardGrab Instance Returns whether the window intercepts desktop keyboard shortcuts. ```teal function tecs.platform.window.Window.keyboardGrab(self): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Window` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns false after `destroy`. | #### tecs.platform.window.Window:maximize Instance Maximizes the window to the display's usable bounds. ```teal function tecs.platform.window.Window.maximize(self): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Window` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns whether the platform accepted the request. | #### tecs.platform.window.Window:maximumSize Instance Reads the maximum resize limit. ```teal function tecs.platform.window.Window.maximumSize(self): integer, integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Window` | | ##### Returns | Type | Description | | --- | --- | | `integer` | Returns the maximum width, or zero when unset. | | `integer` | Returns the maximum height, or zero when unset. | #### tecs.platform.window.Window:minimize Instance Minimizes the window to the taskbar or dock. ```teal function tecs.platform.window.Window.minimize(self): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Window` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns whether the platform accepted the request. | #### tecs.platform.window.Window:minimumSize Instance Reads the minimum resize limit. ```teal function tecs.platform.window.Window.minimumSize(self): integer, integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Window` | | ##### Returns | Type | Description | | --- | --- | | `integer` | Returns the minimum width, or zero when unset. | | `integer` | Returns the minimum height, or zero when unset. | #### tecs.platform.window.Window:mouseGrab Instance Returns whether pointer confinement applies to the focused window. ```teal function tecs.platform.window.Window.mouseGrab(self): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Window` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns false when the window lacks focus or after `destroy`. | #### tecs.platform.window.Window:mouseRect Instance Reads the pointer confinement region. ```teal function tecs.platform.window.Window.mouseRect( self ): integer, integer, integer, integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Window` | | ##### Returns | Type | Description | | --- | --- | | `integer` | Returns the horizontal offset in screen coordinates. | | `integer` | Returns the vertical offset in screen coordinates. | | `integer` | Returns the width in screen coordinates. | | `integer` | Returns the height in screen coordinates. All four values contain zero when unset. | #### tecs.platform.window.Window:opacity Instance Reads the window opacity. ```teal function tecs.platform.window.Window.opacity(self): number ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Window` | | ##### Returns | Type | Description | | --- | --- | | `number` | Returns a value from 0 for invisible to 1 for solid, zero after `destroy`, or 1 on a platform without compositing. | #### tecs.platform.window.Window:pixelDensity Instance Returns the ratio of drawable pixels to screen coordinates. ```teal function tecs.platform.window.Window.pixelDensity(self): number ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Window` | | ##### Returns | Type | Description | | --- | --- | | `number` | Returns the ratio, 1 without high-density drawing, or zero after `destroy`. | #### tecs.platform.window.Window:position Instance Reads the top-left corner in desktop screen coordinates. ```teal function tecs.platform.window.Window.position(self): integer, integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Window` | | ##### Returns | Type | Description | | --- | --- | | `integer` | Returns the horizontal position, or zero after `destroy`. | | `integer` | Returns the vertical position, or zero after `destroy`. | #### tecs.platform.window.Window:progress Instance Reads taskbar or dock progress. ```teal function tecs.platform.window.Window.progress( self ): Window.Progress, number ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Window` | | ##### Returns | Type | Description | | --- | --- | | [`Window.Progress`](/modules/platform/window/#tecs.platform.window.Window.Progress) | Returns the current state, or `"none"` after `destroy`. | | `number` | Returns progress from 0 to 1, or zero after `destroy`. | #### tecs.platform.window.Window:raise Instance Brings the window forward and requests input focus. Prefer `flash` when the user did not request a focus change. ```teal function tecs.platform.window.Window.raise(self): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Window` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns whether the platform accepted the request. | #### tecs.platform.window.Window:restore Instance Restores a minimized or maximized window. ```teal function tecs.platform.window.Window.restore(self): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Window` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns whether the platform accepted the request. | #### tecs.platform.window.Window:safeArea Instance Reads the unobstructed region inside the window. A desktop normally returns the whole window. Phones and handhelds may exclude notches, rounded corners or gesture areas. ```teal function tecs.platform.window.Window.safeArea( self ): integer, integer, integer, integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Window` | | ##### Returns | Type | Description | | --- | --- | | `integer` | Returns the horizontal offset in screen coordinates. | | `integer` | Returns the vertical offset in screen coordinates. | | `integer` | Returns the width in screen coordinates. | | `integer` | Returns the height in screen coordinates. | #### tecs.platform.window.Window:setAlwaysOnTop Instance Enables or disables always-on-top behavior. ```teal function tecs.platform.window.Window.setAlwaysOnTop( self, onTop: boolean ): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Window` | | | `onTop` | `boolean` | The caller chooses whether to keep the window above others. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns whether the platform accepted the change. | #### tecs.platform.window.Window:setAspectRatio Instance Constrains the width-over-height ratio. Equal values pin the window to one shape. ```teal function tecs.platform.window.Window.setAspectRatio( self, minimum: number, maximum: number ): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Window` | | | `minimum` | `number` | The caller supplies the minimum ratio, or zero to remove that bound. | | `maximum` | `number` | The caller supplies the maximum ratio, or zero to remove that bound. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns whether the platform accepted the bounds. | #### tecs.platform.window.Window:setBordered Instance Adds or removes the title bar and frame. The client area keeps its size. ```teal function tecs.platform.window.Window.setBordered( self, bordered: boolean ): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Window` | | | `bordered` | `boolean` | The caller chooses whether to show desktop decoration. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns whether the platform accepted the change. | #### tecs.platform.window.Window:setFocusable Instance Allows or forbids keyboard focus. A non-focusable window suits an overlay. ```teal function tecs.platform.window.Window.setFocusable( self, focusable: boolean ): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Window` | | | `focusable` | `boolean` | The caller chooses whether the window may take focus. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns whether the platform accepted the change. | #### tecs.platform.window.Window:setFullscreen Instance Enters or leaves fullscreen. With no selected mode, fullscreen uses the desktop resolution. With a mode from `setFullscreenMode`, it changes the display mode. The swapchain follows the new size on the next acquired frame. ```teal function tecs.platform.window.Window.setFullscreen( self, fullscreen: boolean ): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Window` | | | `fullscreen` | `boolean` | The caller passes true to enter fullscreen or false to leave it. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns whether the platform accepted the request. | #### tecs.platform.window.Window:setFullscreenMode Instance Selects the video mode used by fullscreen. The mode must come from `fullscreenModes` or `closestFullscreenMode`; a hand-built record raises. Nil selects borderless fullscreen at the desktop resolution. ```teal function tecs.platform.window.Window.setFullscreenMode( self, mode: Window.DisplayMode ): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Window` | | | `mode` | [`Window.DisplayMode`](/modules/platform/window/#tecs.platform.window.Window.DisplayMode) | The caller supplies a platform mode, or nil for borderless fullscreen. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns whether the platform accepted the mode. | #### tecs.platform.window.Window:setHitRegions Instance Replaces the regions used for native window hit testing. The first region containing a point wins. Left and top edges are included; right and bottom edges are excluded. SDL copies no Lua state: Rust copies every record before this call returns and answers the native callback without entering LuaJIT. Pass nil or an empty list to restore ordinary desktop hit testing. Regions normally accompany a borderless window and must be updated after layout changes that move a title bar or resize border. ```teal function tecs.platform.window.Window.setHitRegions( self, regions: {Window.HitRegion} ): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Window` | | | `regions` | `{`[`Window.HitRegion`](/modules/platform/window/#tecs.platform.window.Window.HitRegion)`}` | The caller supplies special regions in priority order, or nil to clear every region. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns whether the platform installed, replaced or cleared the regions. | #### tecs.platform.window.Window:setIcon Instance Sets the image the desktop shows for the window. The call decodes the file synchronously. ```teal function tecs.platform.window.Window.setIcon( self, path: string ): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Window` | | | `path` | `string` | The caller supplies the image path. Nil raises. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns false when the decoder cannot read the file or after `destroy`. | #### tecs.platform.window.Window:setKeyboardGrab Instance Enables or disables interception of desktop keyboard shortcuts. Some desktops refuse this or require user permission. ```teal function tecs.platform.window.Window.setKeyboardGrab( self, grabbed: boolean ): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Window` | | | `grabbed` | `boolean` | The caller chooses whether to intercept shortcuts while focused. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns whether the platform accepted the request. | #### tecs.platform.window.Window:setMaximumSize Instance Sets the maximum resize limit. ```teal function tecs.platform.window.Window.setMaximumSize( self, width: integer, height: integer ): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Window` | | | `width` | `integer` | The caller supplies the maximum width, or zero to remove that limit. | | `height` | `integer` | The caller supplies the maximum height, or zero to remove that limit. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns whether the platform accepted the limits. | #### tecs.platform.window.Window:setMinimumSize Instance Sets the minimum resize limit. ```teal function tecs.platform.window.Window.setMinimumSize( self, width: integer, height: integer ): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Window` | | | `width` | `integer` | The caller supplies the minimum width, or zero to remove that limit. | | `height` | `integer` | The caller supplies the minimum height, or zero to remove that limit. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns whether the platform accepted the limits. | #### tecs.platform.window.Window:setMouseGrab Instance Confines the pointer to the window or releases it. Unlike relative mouse mode, this leaves the pointer visible. An unfocused window records the request and resumes it on focus. ```teal function tecs.platform.window.Window.setMouseGrab( self, grabbed: boolean ): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Window` | | | `grabbed` | `boolean` | The caller chooses whether to confine the pointer. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns whether the platform accepted the request. | #### tecs.platform.window.Window:setMouseRect Instance Confines the pointer to part of the window. This is independent of `setMouseGrab`. Call with no arguments to remove the region. ```teal function tecs.platform.window.Window.setMouseRect( self, x: integer, y: integer, width: integer, height: integer ): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Window` | | | `x` | `integer` | The caller supplies the horizontal offset, or nil to remove the region. | | `y` | `integer` | The caller supplies the vertical offset with `x`. | | `width` | `integer` | The caller supplies the region width with `x`. | | `height` | `integer` | The caller supplies the region height with `x`. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns whether the platform accepted the request. | #### tecs.platform.window.Window:setOpacity Instance Sets the window opacity. ```teal function tecs.platform.window.Window.setOpacity( self, opacity: number ): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Window` | | | `opacity` | `number` | The caller supplies a value from 0 for invisible to 1 for solid. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns whether the desktop accepted the change. | #### tecs.platform.window.Window:setPosition Instance Moves the window in desktop screen coordinates. The coordinate space spans all displays and may contain negative positions. ```teal function tecs.platform.window.Window.setPosition( self, x: integer, y: integer ): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Window` | | | `x` | `integer` | The caller supplies the horizontal position. | | `y` | `integer` | The caller supplies the vertical position. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns whether the platform accepted the request. | #### tecs.platform.window.Window:setProgress Instance Shows progress on the taskbar or dock. ```teal function tecs.platform.window.Window.setProgress( self, state: Window.Progress, value: number ): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Window` | | | `state` | [`Window.Progress`](/modules/platform/window/#tecs.platform.window.Window.Progress) | The caller supplies `"none"`, `"indeterminate"`, `"normal"`, `"paused"` or `"error"`. An unknown value raises. | | `value` | `number` | The caller supplies progress from 0 to 1. The desktop reads it for `"normal"`, `"paused"` and `"error"`; omit it to keep the current value. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns whether the desktop accepted the state and value. | #### tecs.platform.window.Window:setResizable Instance Allows or forbids user resizing. ```teal function tecs.platform.window.Window.setResizable( self, resizable: boolean ): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Window` | | | `resizable` | `boolean` | The caller chooses whether the user may resize the window. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns whether the platform accepted the change. | #### tecs.platform.window.Window:setSize Instance Resizes the window in screen coordinates. A fullscreen window ignores the request. A compositor may apply it after this returns; use `sync` before an immediate readback. ```teal function tecs.platform.window.Window.setSize( self, width: integer, height: integer ): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Window` | | | `width` | `integer` | The caller supplies the new width in screen coordinates. | | `height` | `integer` | The caller supplies the new height in screen coordinates. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns whether the platform accepted the request. | #### tecs.platform.window.Window:setTitle Instance Sets the title and updates the public `title` field. ```teal function tecs.platform.window.Window.setTitle( self, title: string ): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Window` | | | `title` | `string` | The caller supplies the new title. Nil raises. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns whether the platform changed the title. | #### tecs.platform.window.Window:show Instance Maps a hidden window onto the desktop. ```teal function tecs.platform.window.Window.show(self): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Window` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns whether the platform showed the window. | #### tecs.platform.window.Window:showSystemMenu Instance Opens the desktop's standard menu for this window. This supplies the platform menu used for operations such as moving the window between displays or workspaces. Unsupported desktops ignore the request. ```teal function tecs.platform.window.Window.showSystemMenu( self, x: integer, y: integer ): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Window` | | | `x` | `integer` | The caller supplies the horizontal position relative to the top-left of the window's client area. | | `y` | `integer` | The caller supplies the vertical position relative to the top-left of the window's client area. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns whether the desktop accepted the request. | #### tecs.platform.window.Window:sync Instance Waits for the compositor to apply pending window changes. Size, position and fullscreen changes may land after their setters return. Call this only when code must read a changed value back immediately. ```teal function tecs.platform.window.Window.sync(self): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Window` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | Returns false when the platform times out or after `destroy`. | --- ## tecs.regex # tecs.regex Compiled regular expressions over Lua byte strings. Patterns use the Rust `regex` crate's syntax. Compilation builds the automaton once; every method on the returned [`Regex`](/modules/regex/#tecs.regex.Regex) reuses it. Inline flags such as `(?i)` and `(?m)` configure a pattern without a second options vocabulary. ```teal local trailingDigits = tecs.regex.compile([[(\d+)$]]) local frame = trailingDigits:find("sprites/hero_04") if frame ~= nil then print(frame.value, frame.first, frame.last) end print(trailingDigits:replaceAll("hero_04 orc_11", "N")) ``` Subjects remain byte strings, including strings that are not valid UTF-8. Match positions therefore follow Lua's string functions: 1-based, inclusive byte indices. An empty match has `last == first - 1`. ## Module contents ### Types | Type | Kind | Description | | --- | --- | --- | | [`Captures`](/modules/regex/#tecs.regex.Captures) | record | Contains a whole match and its populated capture groups. | | [`Match`](/modules/regex/#tecs.regex.Match) | record | Describes one matched byte range. | | [`Regex`](/modules/regex/#tecs.regex.Regex) | record | Represents a compiled Rust regular expression. | ### Functions | Function | Kind | Description | | --- | --- | --- | | [`compile`](/modules/regex/#tecs.regex.compile) | Static | Compiles a Rust regular expression. | ## Types ### tecs.regex.Captures record Contains a whole match and its populated capture groups. ```teal record tecs.regex.Captures whole: Match groups: {Match} groupCount: integer named: {string: Match} end ``` #### tecs.regex.Captures.whole field Read-only. Contains group zero, which covers the whole match. ```teal tecs.regex.Captures.whole: Match ``` #### tecs.regex.Captures.groups field Read-only. Contains explicit groups by their 1-based index. An unmatched optional group leaves a hole. ```teal tecs.regex.Captures.groups: {Match} ``` #### tecs.regex.Captures.groupCount field Read-only. Reports the number of explicit groups in the pattern, including groups that did not match. ```teal tecs.regex.Captures.groupCount: integer ``` #### tecs.regex.Captures.named field Read-only. Maps matched group names to the same values in `groups`. ```teal tecs.regex.Captures.named: {string: Match} ``` ### tecs.regex.Match record Describes one matched byte range. ```teal record tecs.regex.Match value: string first: integer last: integer index: integer name: string end ``` #### tecs.regex.Match.value field Read-only. Contains the bytes copied from the matched range. ```teal tecs.regex.Match.value: string ``` #### tecs.regex.Match.first field Read-only. Reports the first matched byte using a 1-based index. ```teal tecs.regex.Match.first: integer ``` #### tecs.regex.Match.last field Read-only. Reports the inclusive last matched byte. An empty match reports `first - 1`. ```teal tecs.regex.Match.last: integer ``` #### tecs.regex.Match.index field Read-only. Reports the capture-group index, with zero for the whole match. ```teal tecs.regex.Match.index: integer ``` #### tecs.regex.Match.name field Read-only. Contains the capture name, or nil for an unnamed group. ```teal tecs.regex.Match.name: string ``` ### tecs.regex.Regex record Represents a compiled Rust regular expression. ```teal record tecs.regex.Regex pattern: string captures: function(self, subject: string, init: integer): Captures find: function(self, subject: string, init: integer): Match isMatch: function(self, subject: string): boolean replace: function( self, subject: string, replacement: string ): string replaceAll: function( self, subject: string, replacement: string ): string end ``` #### tecs.regex.Regex.pattern field Read-only. Contains the source pattern unchanged. ```teal tecs.regex.Regex.pattern: string ``` #### tecs.regex.Regex:captures Instance Finds the first match and every capture group it populated. `groups` may contain holes when optional groups do not participate; iterate through `groupCount`, not `#groups`. A named entry aliases the same [`Match`](/modules/regex/#tecs.regex.Match) stored at its numbered index. ```teal function tecs.regex.Regex.captures( self, subject: string, init: integer ): Captures ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Regex` | | | `subject` | `string` | Arbitrary bytes. | | `init` | `integer` | First byte considered, under the same rules as `find`. | ##### Returns | Type | Description | | --- | --- | | [`Captures`](/modules/regex/#tecs.regex.Captures) | The whole match, numbered groups and named aliases, or nil when nothing matches. | #### tecs.regex.Regex:find Instance Finds the first match at or after a byte position. ```teal function tecs.regex.Regex.find( self, subject: string, init: integer ): Match ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Regex` | | | `subject` | `string` | Arbitrary bytes. | | `init` | `integer` | First byte considered, under `string.find`'s rules: 1 by default, a negative value counts back from the end, and the method clamps a value before 1 to 1. | ##### Returns | Type | Description | | --- | --- | | [`Match`](/modules/regex/#tecs.regex.Match) | The matched bytes and their range, or nil when nothing matches. | #### Examples ```teal local regex = require("tecs.regex").compile([[\d+]]) local found = assert(regex:find("hp=100")) assert(found.value == "100") ``` #### tecs.regex.Regex:isMatch Instance Reports whether any part of a subject matches. ```teal function tecs.regex.Regex.isMatch(self, subject: string): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Regex` | | | `subject` | `string` | Arbitrary bytes. Unlike the pattern, these need not be UTF-8. | ##### Returns | Type | Description | | --- | --- | | `boolean` | True when the expression matches at least once. | #### Examples ```teal local regex = require("tecs.regex").compile([[\.(png|jpg)$]]) assert(regex:isMatch("sprite.png")) ``` #### tecs.regex.Regex:replace Instance Replaces the first match. The method expands capture references in the replacement: `$0` is the whole match, `$1` is the first group, `$name` and `${name}` select a named group, and `$$` writes one dollar sign. A reference the pattern does not declare expands to an empty string. ```teal function tecs.regex.Regex.replace( self, subject: string, replacement: string ): string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Regex` | | | `subject` | `string` | Arbitrary bytes. | | `replacement` | `string` | Arbitrary bytes, with capture references interpreted as described above. | ##### Returns | Type | Description | | --- | --- | | `string` | A new string when something matched, or the original string unchanged when nothing did. | #### Examples ```teal local regex = require("tecs.regex").compile([[(\w+)=(\d+)]]) assert(regex:replace("hp=100 mp=50", "$1: $2") == "hp: 100 mp=50") ``` #### tecs.regex.Regex:replaceAll Instance Replaces every non-overlapping match. ```teal function tecs.regex.Regex.replaceAll( self, subject: string, replacement: string ): string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Regex` | | | `subject` | `string` | Arbitrary bytes. | | `replacement` | `string` | The same capture-reference syntax as `replace`. | ##### Returns | Type | Description | | --- | --- | | `string` | A new string when something matched, or the original string unchanged when nothing did. | #### Examples ```teal local regex = require("tecs.regex").compile([[\d+]]) assert(regex:replaceAll("room 12, floor 3", "#") == "room #, floor #") ``` ## Functions ### tecs.regex.compile Static Compiles a Rust regular expression. Compilation raises on malformed syntax or a pattern that is not UTF-8. Rust's Unicode character classes work by default; use inline flags to change the expression, such as `(?i)` for case-insensitive matching or `(?-u:.)` for one arbitrary byte. Keep the returned object and reuse it. The Lua collector releases its native allocation automatically. ```teal function tecs.regex.compile(pattern: string): Regex ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `pattern` | `string` | Rust regex syntax, with NUL bytes allowed where that syntax allows them. | #### Returns | Type | Description | | --- | --- | | [`Regex`](/modules/regex/#tecs.regex.Regex) | A compiled expression whose native allocation the Lua collector releases. | --- ## tecs.sequence # tecs.sequence Snapshot-safe programs, waits, ownership, actions, and tween timelines. Sequences store control flow as data. Snapshots, rewind, and hot reload can therefore preserve a playback's program, instruction pointer, waits, bindings, and parameters. ## Programs and actions Register an action on the world, name it from a program, and bind each playback to the entities it acts on: ```teal local sequence = tecs.sequence sequence.registerAction( world, "game.lockControls", function(_world, _ctx) controlsLocked = true end ) local intro = sequence.define( "game.bossIntro", { sequence.call("game.lockControls"), sequence.wait(1.5), sequence.emit("boss.ready"), } ) local playback = sequence.play( world, intro, { owner = encounter, bindings = {boss = bossEntity}, } ) ``` Program names form a snapshot compatibility surface. Use stable, qualified names for game and plugin programs. Redefining a name creates a new version. Running playbacks keep their original instructions, while later calls to `play` use the new program. Action bodies resolve by name when they run, so a reload can replace action code without moving a live program counter. Actions run synchronously and cannot yield. An action that raises faults its playback after preserving mutations it already completed. Keep wall-clock, file, and network access outside deterministic actions. ## Clocks and waits `"fixed"` advances once per fixed step and serves deterministic gameplay. `"frame"` advances once per gameplay frame. `"presentation"` advances with display time and serves values simulation never reads. Durations use seconds. Step and tick values count whole advances of the program's clock. Every positive duration waits at least one tick. `waitSteps(0)` yields until the next tick and gives a loop a fresh instruction budget. ## Ownership `owner` ties a playback to an entity. Despawning that entity cancels the playback. A named `channel` gives one owner a replaceable slot, so starting a second playback on the same channel cancels the first. Pause uses named holders. The playback resumes only after every holder releases it. Branches inherit owner, bindings, parameters, budget, and pause state. A branch cannot outlive its parent. Each playback receives a bounded instruction budget per tick. Exceeding it faults with `budgetExceeded` instead of hanging the frame. ## Timelines Timelines compile tweens into the same runtime and retain the same ownership, clock, channel, pause, and snapshot rules. Targets interpolate component fields. Parallel blocks share a start time, while sequential blocks start after the previous block ends. ## Module contents ### Types | Type | Kind | Description | | --- | --- | --- | | [`Action`](/modules/sequence/#tecs.sequence.Action) | type | Defines a registered effect that runs synchronously and cannot yield. | | [`ActionContext`](/modules/sequence/#tecs.sequence.ActionContext) | record | Provides the context received by a registered action. | | [`Argument`](/modules/sequence/#tecs.sequence.Argument) | type | Represents constants passed from call to its action. | | [`Awaitable`](/modules/sequence/#tecs.sequence.Awaitable) | record | Defines the response required from an awaitable provider. | | [`ClockId`](/modules/sequence/#tecs.sequence.ClockId) | enum | Selects the clock against which a program runs. | | [`DefineOptions`](/modules/sequence/#tecs.sequence.DefineOptions) | record | Configures define. | | [`EasingFunction`](/modules/sequence/#tecs.sequence.EasingFunction) | type | Maps normalized input progress to eased output progress. | | [`EasingName`](/modules/sequence/#tecs.sequence.EasingName) | enum | Names a built-in easing curve and describes the shape of any curve. | | [`EntityRef`](/modules/sequence/#tecs.sequence.EntityRef) | record | References an entity supplied at play time and resolves it when the instruction that uses it runs. | | [`Evaluator`](/modules/sequence/#tecs.sequence.Evaluator) | record | Defines the evaluator run by an eval step every tick. | | [`FaultReason`](/modules/sequence/#tecs.sequence.FaultReason) | enum | Explains why a cursor stopped running. | | [`Handle`](/modules/sequence/#tecs.sequence.Handle) | type | Identifies one playback through a generation-checked reference that remains meaningful across a snapshot load. | | [`Node`](/modules/sequence/#tecs.sequence.Node) | interface | Represents one authored step produced by the node constructors below. | | [`PlaybackMode`](/modules/sequence/#tecs.sequence.PlaybackMode) | enum | Selects how a timeline repeats. | | [`PlaybackState`](/modules/sequence/#tecs.sequence.PlaybackState) | enum | Describes the lifecycle state of one playback. | | [`PlayOptions`](/modules/sequence/#tecs.sequence.PlayOptions) | record | Configures play. | | [`Program`](/modules/sequence/#tecs.sequence.Program) | interface | Represents a compiled, immutable program produced by define and shared by every playback of it. | | [`QueryCondition`](/modules/sequence/#tecs.sequence.QueryCondition) | enum | Describes the condition awaited by a waitQuery step. | | [`RunOptions`](/modules/sequence/#tecs.sequence.RunOptions) | record | Configures how tweenRun plays its nested timeline. | | [`Status`](/modules/sequence/#tecs.sequence.Status) | record | Reports a playback's current state. | | [`Step`](/modules/sequence/#tecs.sequence.Step) | record | Describes one step a playback will reach without branching. | | [`Target`](/modules/sequence/#tecs.sequence.Target) | interface | Describes the component and one to four numeric fields written by a timeline operation. | | [`TargetName`](/modules/sequence/#tecs.sequence.TargetName) | enum | Names a built-in component-field target. | | [`TimelineNode`](/modules/sequence/#tecs.sequence.TimelineNode) | type | Represents one authored timeline operation. | | [`TimelineOptions`](/modules/sequence/#tecs.sequence.TimelineOptions) | record | Configures timeline. | | [`TimelineSpec`](/modules/sequence/#tecs.sequence.TimelineSpec) | type | Lists timeline operations in execution order. | | [`TrackSource`](/modules/sequence/#tecs.sequence.TrackSource) | interface | Provides a live component-field tracking source. | | [`TweenOutcome`](/modules/sequence/#tecs.sequence.TweenOutcome) | enum | Reports how the tween awaited by waitTween ended. | ### Functions | Function | Kind | Description | | --- | --- | --- | | [`activeCount`](/modules/sequence/#tecs.sequence.activeCount) | Static | Returns the number of live playbacks for tests and diagnostics. | | [`await`](/modules/sequence/#tecs.sequence.await) | Static | Waits for work outside the sequencer to finish. | | [`bind`](/modules/sequence/#tecs.sequence.bind) | Static | References an entity supplied through PlayOptions.bindings. | | [`call`](/modules/sequence/#tecs.sequence.call) | Static | Runs a registered action. | | [`cancel`](/modules/sequence/#tecs.sequence.cancel) | Static | Stops a playback and releases its cursor. | | [`cancelOwnedBy`](/modules/sequence/#tecs.sequence.cancelOwnedBy) | Static | Cancels every playback owned by an entity. | | [`currentStep`](/modules/sequence/#tecs.sequence.currentStep) | Static | Returns a clock's current tick as counted by the sequencer. | | [`dataOps`](/modules/sequence/#tecs.sequence.dataOps) | Static | Returns the step names accepted by defineData, sorted. | | [`define`](/modules/sequence/#tecs.sequence.define) | Static | Compiles a program under a stable symbolic name. | | [`defineData`](/modules/sequence/#tecs.sequence.defineData) | Static | Compiles a program written as plain data instead of node calls. | | [`disassemble`](/modules/sequence/#tecs.sequence.disassemble) | Static | Renders a program as readable instructions. | | [`emit`](/modules/sequence/#tecs.sequence.emit) | Static | Emits an ECS event at address zero. | | [`eval`](/modules/sequence/#tecs.sequence.eval) | Static | Evaluates a registered evaluator every tick until it finishes. | | [`fork`](/modules/sequence/#tecs.sequence.fork) | Static | Starts a branch that runs alongside the rest of the program. | | [`hasAction`](/modules/sequence/#tecs.sequence.hasAction) | Static | Returns whether this world has registered an action name. | | [`hasQuery`](/modules/sequence/#tecs.sequence.hasQuery) | Static | Returns whether this world has registered a query name. | | [`join`](/modules/sequence/#tecs.sequence.join) | Static | Waits for every branch that has not yet joined. | | [`loop`](/modules/sequence/#tecs.sequence.loop) | Static | Repeats a block. | | [`parallel`](/modules/sequence/#tecs.sequence.parallel) | Static | Forks several blocks and waits for all of them. | | [`pause`](/modules/sequence/#tecs.sequence.pause) | Static | Suspends a playback, its branches, and anything it started. | | [`play`](/modules/sequence/#tecs.sequence.play) | Static | Starts a program. | | [`playbacks`](/modules/sequence/#tecs.sequence.playbacks) | Static | Returns handles for every live playback, branches included, in a stable order. | | [`playTween`](/modules/sequence/#tecs.sequence.playTween) | Static | Plays a registered tween preset on a bound entity. | | [`plugin`](/modules/sequence/#tecs.sequence.plugin) | Static | Installs the sequencer. | | [`program`](/modules/sequence/#tecs.sequence.program) | Static | Returns the newest version of a defined program or a requested version. | | [`programNames`](/modules/sequence/#tecs.sequence.programNames) | Static | Returns the names of every defined program, sorted. | | [`registerAction`](/modules/sequence/#tecs.sequence.registerAction) | Static | Registers an action that a call node can name. | | [`registerAwaitable`](/modules/sequence/#tecs.sequence.registerAwaitable) | Static | Registers a provider that an await step can name. | | [`registerEvaluator`](/modules/sequence/#tecs.sequence.registerEvaluator) | Static | Registers an evaluator that an eval step can name. | | [`registerQuery`](/modules/sequence/#tecs.sequence.registerQuery) | Static | Registers a query that a waitQuery step can name. | | [`resume`](/modules/sequence/#tecs.sequence.resume) | Static | Releases one holder's claim. | | [`setInstructionBudget`](/modules/sequence/#tecs.sequence.setInstructionBudget) | Static | Sets the per-step instruction budget for playbacks without their own. | | [`signal`](/modules/sequence/#tecs.sequence.signal) | Static | Raises a named signal and wakes every playback blocked on it. | | [`signalOnEvent`](/modules/sequence/#tecs.sequence.signalOnEvent) | Static | Raises a signal whenever an event emits at address zero. | | [`status`](/modules/sequence/#tecs.sequence.status) | Static | Returns playback status, or nil for a handle this world never issued. | | [`timeline`](/modules/sequence/#tecs.sequence.timeline) | Static | Compiles a tween timeline into a program under a stable name. | | [`tweenAdjust`](/modules/sequence/#tecs.sequence.tweenAdjust) | Static | Interpolates by a relative delta from the starting value. | | [`tweenEmit`](/modules/sequence/#tecs.sequence.tweenEmit) | Static | Emits a named sequence.Event when the cursor reaches this point. | | [`tweenParallel`](/modules/sequence/#tecs.sequence.tweenParallel) | Static | Runs timeline nodes concurrently and ends with the longest. | | [`tweenRun`](/modules/sequence/#tecs.sequence.tweenRun) | Static | Runs a nested timeline from its own spec. | | [`tweenTo`](/modules/sequence/#tecs.sequence.tweenTo) | Static | Interpolates to an absolute destination. | | [`tweenTrack`](/modules/sequence/#tecs.sequence.tweenTrack) | Static | Interpolates toward a destination that keeps moving. | | [`tweenWait`](/modules/sequence/#tecs.sequence.tweenWait) | Static | Advances the timeline cursor without changing anything. | | [`upcoming`](/modules/sequence/#tecs.sequence.upcoming) | Static | Returns the actions a playback will certainly perform next and their timing. | | [`wait`](/modules/sequence/#tecs.sequence.wait) | Static | Waits for a duration in seconds. | | [`waitingOn`](/modules/sequence/#tecs.sequence.waitingOn) | Static | Returns how many playbacks currently wait on a signal name. | | [`waitQuery`](/modules/sequence/#tecs.sequence.waitQuery) | Static | Blocks until a registered query matches or stops matching. | | [`waitSignal`](/modules/sequence/#tecs.sequence.waitSignal) | Static | Blocks until signal raises the named signal. | | [`waitSteps`](/modules/sequence/#tecs.sequence.waitSteps) | Static | Waits for a whole number of ticks of the program's clock. | | [`waitTween`](/modules/sequence/#tecs.sequence.waitTween) | Static | Waits for the tween started by the most recent playTween. | ### Values | Value | Type | Description | | --- | --- | --- | | [`easing`](/modules/sequence/#tecs.sequence.easing) | `Easings` | Read-only. Exposes built-in easing curves such as sequence.easing.quadOut. | | [`Event`](/modules/sequence/#tecs.sequence.Event) | [`Event`](/modules/sequence/#tecs.sequence.Event) | Read-only. Exposes the event emitted by an emit step at address zero. | | [`source`](/modules/sequence/#tecs.sequence.source) | `Sources` | Read-only. Exposes sources from which tweenTrack reads changing values: sequence.source.own(Transform2D, "x"). | | [`target`](/modules/sequence/#tecs.sequence.target) | `Targets` | Read-only. Exposes built-in targets and constructors for new ones: sequence.target.translateX,... | | [`TrackingTarget`](/modules/sequence/#tecs.sequence.TrackingTarget) | [`TrackingTarget`](/modules/sequence/#tecs.sequence.TrackingTarget) | Read-only. Exposes the component that selects a dynamic tracking-source entity. | ## Types ### tecs.sequence.Action type Defines a registered effect that runs synchronously and cannot yield. ```teal type tecs.sequence.Action = function(World, ActionContext) ``` ### tecs.sequence.ActionContext record Provides the context received by a registered action. The context and its `args` belong to the sequencer, which reuses them for the next call. ```teal record tecs.sequence.ActionContext world: World handle: Handle owner: integer args: {any} params: {string: any} bind: function(self, string, integer) entity: function(self, string): integer end ``` #### tecs.sequence.ActionContext.world field Read-only. The world this playback belongs to. ```teal tecs.sequence.ActionContext.world: World ``` #### tecs.sequence.ActionContext.handle field Read-only. The playback, for `status` or `cancel` from inside an action. ```teal tecs.sequence.ActionContext.handle: Handle ``` #### tecs.sequence.ActionContext.owner field Read-only. The `owner` supplied at `play`, or 0 for a world-scoped sequence. ```teal tecs.sequence.ActionContext.owner: integer ``` #### tecs.sequence.ActionContext.args field Read-only. Constants supplied by the `call` node, with any `bind` references already resolved to entity ids. ```teal tecs.sequence.ActionContext.args: {any} ``` #### tecs.sequence.ActionContext.params field Read-only. The `params` supplied at `play`, or nil. Unlike `args`, this is the playback's own table rather than borrowed scratch, and it is the same one every step of the playback sees. ```teal tecs.sequence.ActionContext.params: {string: any} ``` #### tecs.sequence.ActionContext:bind Instance Name an entity for the steps that follow, so an action that creates one can hand it on. Bindings travel with the playback and survive a snapshot; passing nil forgets the name. ```teal function tecs.sequence.ActionContext.bind(self, string, integer) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `ActionContext` | | | `#2` | `string` | | | `#3` | `integer` | | ##### Returns None. #### tecs.sequence.ActionContext:entity Instance Resolve a named binding. Returns nil when the binding was not supplied or its entity is no longer alive. ```teal function tecs.sequence.ActionContext.entity(self, string): integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `ActionContext` | | | `#2` | `string` | | ##### Returns | Type | Description | | --- | --- | | `integer` | | ### tecs.sequence.Argument type Represents constants passed from `call` to its action. ```teal type tecs.sequence.Argument = any ``` ### tecs.sequence.Awaitable record Defines the response required from an awaitable provider. ```teal record tecs.sequence.Awaitable isPending: function(World, integer, string): boolean setPaused: function(World, integer, string, boolean) end ``` #### tecs.sequence.Awaitable.isPending Static Whether the work named by `entity` and `key` is still going. Called at a step boundary for every cursor parked on this name, so it must be cheap and must not mutate the world. Returning false for something that never started is correct: a program waiting for an animation that is already over should carry on, not hang. ```teal function tecs.sequence.Awaitable.isPending( World, integer, string ): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `#1` | [`World`](/modules/ecs/#tecs.World) | | | `#2` | `integer` | | | `#3` | `string` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | | #### tecs.sequence.Awaitable.setPaused Static Stop and start the work, when the provider knows how. Called when the playback parked on it is paused or resumed, so a cutscene that is waiting for an animation stops it rather than leaving it running underneath. Optional: a provider that cannot pause its work simply does not offer this. ```teal function tecs.sequence.Awaitable.setPaused( World, integer, string, boolean ) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `#1` | [`World`](/modules/ecs/#tecs.World) | | | `#2` | `integer` | | | `#3` | `string` | | | `#4` | `boolean` | | ##### Returns None. ### tecs.sequence.ClockId enum Selects the clock against which a program runs. ```teal enum tecs.sequence.ClockId "fixed" "frame" "presentation" end ``` ### tecs.sequence.DefineOptions record Configures `define`. ```teal record tecs.sequence.DefineOptions clock: ClockId end ``` #### tecs.sequence.DefineOptions.clock field Caller-writable. Selects the clock counted by program waits and defaults to `"fixed"`. ```teal tecs.sequence.DefineOptions.clock: ClockId ``` ### tecs.sequence.EasingFunction type Maps normalized input progress to eased output progress. Easing shapes the value, not the schedule: the argument is how far through its own window the interpolation is, clamped to [0, 1], and the result is the fraction of the way from start to destination to place the target at. A curve that leaves [0, 1] overshoots the destination rather than the duration, which is what `backOut` and `elasticOut` are for. Compilation fixes the window, so no curve can make a timeline longer or shorter or move what follows it. ```teal type tecs.sequence.EasingFunction = function(number): number ``` ### tecs.sequence.EasingName enum Names a built-in easing curve and describes the shape of any curve. ```teal enum tecs.sequence.EasingName "backIn" "backInOut" "backOut" "backOutIn" "bounceIn" "bounceInOut" "bounceOut" "bounceOutIn" "cubicIn" "cubicInOut" "cubicOut" "cubicOutIn" "elasticIn" "elasticInOut" "elasticOut" "elasticOutIn" "expoIn" "expoInOut" "expoOut" "expoOutIn" "linear" "quadIn" "quadInOut" "quadOut" "quadOutIn" "quartIn" "quartInOut" "quartOut" "quartOutIn" "quintIn" "quintInOut" "quintOut" "quintOutIn" "sineIn" "sineInOut" "sineOut" "sineOutIn" end ``` ### tecs.sequence.EntityRef record References an entity supplied at `play` time and resolves it when the instruction that uses it runs. ```teal record tecs.sequence.EntityRef bindName: string isBinding: boolean end ``` #### tecs.sequence.EntityRef.bindName field Read-only. Name of the binding this reference resolves. ```teal tecs.sequence.EntityRef.bindName: string ``` #### tecs.sequence.EntityRef.isBinding field Read-only. Marker distinguishing a reference from an ordinary table argument. ```teal tecs.sequence.EntityRef.isBinding: boolean ``` ### tecs.sequence.Evaluator record Defines the evaluator run by an `eval` step every tick. ```teal record tecs.sequence.Evaluator load: function(ProgramImpl, any, any): any newState: function(ProgramImpl, any, Cursor): any resolve: function(any): any save: function(any): any step: function(World, Cursor, number): boolean end ``` #### tecs.sequence.Evaluator.load Static ```teal function tecs.sequence.Evaluator.load(ProgramImpl, any, any): any ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `#1` | `ProgramImpl` | | | `#2` | `any` | | | `#3` | `any` | | ##### Returns | Type | Description | | --- | --- | | `any` | | #### tecs.sequence.Evaluator.newState Static Per-cursor working state, or nil when it needs none. `data` is what the `eval` step carried, resolved; the cursor is there for what belongs to the playback rather than the program, chiefly its `owner` and its `params`. ```teal function tecs.sequence.Evaluator.newState(ProgramImpl, any, Cursor): any ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `#1` | `ProgramImpl` | | | `#2` | `any` | | | `#3` | `Cursor` | | ##### Returns | Type | Description | | --- | --- | | `any` | | #### tecs.sequence.Evaluator.resolve Static Turn the constants an `eval` step carries into whatever form is cheapest to read every tick: functions bound, names resolved. Called once per program constant and the result shared by every playback of it, so this is where work that does not depend on the playback belongs. Omit it to hand the constants over as authored. ```teal function tecs.sequence.Evaluator.resolve(any): any ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `#1` | `any` | | ##### Returns | Type | Description | | --- | --- | | `any` | | #### tecs.sequence.Evaluator.save Static Turn working state into something a snapshot can carry, and back. Without both, an evaluating playback comes back at the start of its state rather than where it was. ```teal function tecs.sequence.Evaluator.save(any): any ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `#1` | `any` | | ##### Returns | Type | Description | | --- | --- | | `any` | | #### tecs.sequence.Evaluator.step Static Advance one cursor by `dt`. Returns true when it is finished and the cursor should move on to the next instruction. ```teal function tecs.sequence.Evaluator.step(World, Cursor, number): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `#1` | [`World`](/modules/ecs/#tecs.World) | | | `#2` | `Cursor` | | | `#3` | `number` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | | ### tecs.sequence.FaultReason enum Explains why a cursor stopped running. ```teal enum tecs.sequence.FaultReason "actionError" "branchFaulted" "budgetExceeded" "unregisteredAction" "unregisteredAwaitable" "unregisteredEvaluator" "unregisteredQuery" "unregisteredTween" end ``` ### tecs.sequence.Handle type Identifies one playback through a generation-checked reference that remains meaningful across a snapshot load. ```teal type tecs.sequence.Handle = seqtypes.Handle ``` ### tecs.sequence.Node interface Represents one authored step produced by the node constructors below. Treat the returned value as opaque and do not reuse a node across programs. ```teal interface tecs.sequence.Node end ``` ### tecs.sequence.PlaybackMode enum Selects how a timeline repeats. ```teal enum tecs.sequence.PlaybackMode "loop" "once" "pingPong" end ``` ### tecs.sequence.PlaybackState enum Describes the lifecycle state of one playback. ```teal enum tecs.sequence.PlaybackState "canceled" "completed" "faulted" "paused" "running" end ``` ### tecs.sequence.PlayOptions record Configures `play`. ```teal record tecs.sequence.PlayOptions owner: integer bindings: {string: integer} params: {string: any} budget: integer channel: string end ``` #### tecs.sequence.PlayOptions.owner field Caller-writable. Entity whose lifetime governs the playback. When it despawns, the playback is canceled. Omit for a world-scoped sequence. ```teal tecs.sequence.PlayOptions.owner: integer ``` #### tecs.sequence.PlayOptions.bindings field Caller-writable. Entities the program acts on, addressed by `sequence.bind(name)`. ```teal tecs.sequence.PlayOptions.bindings: {string: integer} ``` #### tecs.sequence.PlayOptions.params field Caller-writable. Constants this playback runs with, readable by its actions and its evaluators. A program is shared by every playback of it, so anything that differs between two of them belongs here rather than in the program. Plain data, for the same reason a `call` argument is: it travels with a snapshot. ```teal tecs.sequence.PlayOptions.params: {string: any} ``` #### tecs.sequence.PlayOptions.budget field Caller-writable. Per-step instruction budget for this playback. Defaults to the world's configured budget. See `setInstructionBudget`. ```teal tecs.sequence.PlayOptions.budget: integer ``` #### tecs.sequence.PlayOptions.channel field Caller-writable. Names a slot this playback occupies on its owner. Starting another playback on the same owner and channel cancels this one, which is how a second fade replaces the first rather than fighting it. ```teal tecs.sequence.PlayOptions.channel: string ``` ### tecs.sequence.Program interface Represents a compiled, immutable program produced by `define` and shared by every playback of it. See `internal/types` for the full contract. ```teal interface tecs.sequence.Program name: string version: integer end ``` #### tecs.sequence.Program.name field Read-only. The name passed to `define`. ```teal tecs.sequence.Program.name: string ``` #### tecs.sequence.Program.version field Read-only. Monotonic version, starting at 1. Incremented per redefinition. ```teal tecs.sequence.Program.version: integer ``` ### tecs.sequence.QueryCondition enum Describes the condition awaited by a `waitQuery` step. ```teal enum tecs.sequence.QueryCondition "any" "empty" end ``` ### tecs.sequence.RunOptions record Configures how `tweenRun` plays its nested timeline. Omitting it runs the nested timeline once. A nested `"loop"` or `"pingPong"` must set `count`, because a parent has to know how long its own window is; only the root timeline of a `sequence.timeline`, through `params`, may repeat without one. ```teal record tecs.sequence.RunOptions mode: PlaybackMode count: integer end ``` #### tecs.sequence.RunOptions.mode field Caller-writable. Nested timeline playback mode. Defaults to "once". ```teal tecs.sequence.RunOptions.mode: PlaybackMode ``` #### tecs.sequence.RunOptions.count field Caller-writable. Pass count for the nested timeline. ```teal tecs.sequence.RunOptions.count: integer ``` ### tecs.sequence.Status record Reports a playback's current state. ```teal record tecs.sequence.Status state: PlaybackState program: string version: integer pc: integer wakeAt: integer waitingFor: string waitingQuery: string waitingCondition: QueryCondition waitingAwaitable: string waitingAwaitableEntity: integer waitingTween: integer tweenOutcome: TweenOutcome branches: integer joining: boolean fault: FaultReason faultMessage: string end ``` #### tecs.sequence.Status.state field Read-only. ```teal tecs.sequence.Status.state: PlaybackState ``` #### tecs.sequence.Status.program field Read-only. Program name this playback is running. ```teal tecs.sequence.Status.program: string ``` #### tecs.sequence.Status.version field Read-only. Program version this playback is running. ```teal tecs.sequence.Status.version: integer ``` #### tecs.sequence.Status.pc field Read-only. Instruction index, for disassembly and debugging. ```teal tecs.sequence.Status.pc: integer ``` #### tecs.sequence.Status.wakeAt field Read-only. Fixed step this playback next runs on. Nil when it is blocked on a signal rather than a time. ```teal tecs.sequence.Status.wakeAt: integer ``` #### tecs.sequence.Status.waitingFor field Read-only. Signal name this playback is blocked on, when it is. ```teal tecs.sequence.Status.waitingFor: string ``` #### tecs.sequence.Status.waitingQuery field Read-only. Query name this playback is blocked on, when it is, and the condition it is waiting for. ```teal tecs.sequence.Status.waitingQuery: string ``` #### tecs.sequence.Status.waitingCondition field Read-only. Query condition this playback is waiting for. ```teal tecs.sequence.Status.waitingCondition: QueryCondition ``` #### tecs.sequence.Status.waitingAwaitable field Read-only. Awaitable provider this playback is blocked on, when it is, and the entity it is waiting on. ```teal tecs.sequence.Status.waitingAwaitable: string ``` #### tecs.sequence.Status.waitingAwaitableEntity field Read-only. Entity this playback's awaitable is waiting on. ```teal tecs.sequence.Status.waitingAwaitableEntity: integer ``` #### tecs.sequence.Status.waitingTween field Read-only. Tween playback token this playback is blocked on, when it is. ```teal tecs.sequence.Status.waitingTween: integer ``` #### tecs.sequence.Status.tweenOutcome field Read-only. How the last tween it waited on ended. ```teal tecs.sequence.Status.tweenOutcome: TweenOutcome ``` #### tecs.sequence.Status.branches field Read-only. Live branches this playback forked and has not joined. ```teal tecs.sequence.Status.branches: integer ``` #### tecs.sequence.Status.joining field Read-only. Whether it is parked at a `join` waiting for those branches. ```teal tecs.sequence.Status.joining: boolean ``` #### tecs.sequence.Status.fault field Read-only. Set when `state` is `"faulted"`. ```teal tecs.sequence.Status.fault: FaultReason ``` #### tecs.sequence.Status.faultMessage field Read-only. Error message set when `state` is `"faulted"`. ```teal tecs.sequence.Status.faultMessage: string ``` ### tecs.sequence.Step record Describes one step a playback will reach without branching. ```teal record tecs.sequence.Step ticks: integer kind: string name: string args: {any} end ``` #### tecs.sequence.Step.ticks field Read-only. Ticks of the program's clock from the reference point until it runs. ```teal tecs.sequence.Step.ticks: integer ``` #### tecs.sequence.Step.kind field Read-only. `"call"` or `"emit"`. ```teal tecs.sequence.Step.kind: string ``` #### tecs.sequence.Step.name field Read-only. Action name for a call, event name for an emit. ```teal tecs.sequence.Step.name: string ``` #### tecs.sequence.Step.args field Read-only. Constants the step carries, unresolved. ```teal tecs.sequence.Step.args: {any} ``` ### tecs.sequence.Target interface Describes the component and one to four numeric fields written by a timeline operation. numeric fields of it to interpolate. Opaque, and built only through `sequence.target`: a built-in such as `sequence.target.translateXY`, or `sequence.target.field(C, "hp")` for a component of your own. Every operation that takes one also accepts a [`TargetName`](/modules/sequence/#tecs.sequence.TargetName) naming the same built-in, which is the form that survives `defineData`. Safe to share between timelines: a target holds no per-playback state. ```teal interface tecs.sequence.Target end ``` ### tecs.sequence.TargetName enum Names a built-in component-field target. ```teal enum tecs.sequence.TargetName "color.a" "color.rgba" "transform.rotation" "transform.rotationShortest" "transform.scaleX" "transform.scaleXY" "transform.scaleY" "transform.x" "transform.xy" "transform.y" end ``` ### tecs.sequence.TimelineNode type Represents one authored timeline operation. ```teal type tecs.sequence.TimelineNode = {any} ``` ### tecs.sequence.TimelineOptions record Configures `timeline`. ```teal record tecs.sequence.TimelineOptions clock: ClockId end ``` #### tecs.sequence.TimelineOptions.clock field Caller-writable. Selects the clock used to evaluate the timeline. It defaults to `"presentation"`, which moves at the display's rate. `"fixed"` makes it deterministic from a snapshot alone, at the cost of stepping at the simulation's rate: the renderer interpolates only [`Transform2D`](/modules/ecs/#tecs.ecs.Transform2D) between fixed steps, so a fixed-clock tween of anything else visibly steps. If the simulation can observe the value, put it on `"fixed"`. ```teal tecs.sequence.TimelineOptions.clock: ClockId ``` ### tecs.sequence.TimelineSpec type Lists timeline operations in execution order. Entries run one after the next, each starting where the one before it ended, except that a `tweenParallel` runs its arguments from a shared start. A nested list is read as a block and behaves the same as one. `timeline` and `tweenRun` compile the resolved form into the same table, consuming the spec. Pass a fresh spec to each timeline. ```teal type tecs.sequence.TimelineSpec = {TimelineNode} ``` ### tecs.sequence.TrackSource interface Provides a live component-field tracking source. ```teal interface tecs.sequence.TrackSource end ``` ### tecs.sequence.TweenOutcome enum Reports how the tween awaited by `waitTween` ended. ```teal enum tecs.sequence.TweenOutcome "canceled" "completed" "replaced" "targetLost" end ``` ## Functions ### tecs.sequence.activeCount Static Returns the number of live playbacks for tests and diagnostics. ```teal function tecs.sequence.activeCount(world: World): integer ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`World`](/modules/ecs/#tecs.World) | The world owns the playbacks. | #### Returns | Type | Description | | --- | --- | | `integer` | Branches counted individually, since a branch is a playback of its own, and so are the playbacks a `playTween` started. | ### tecs.sequence.await Static Waits for work outside the sequencer to finish. A subsystem registers a provider under a name and answers whether the work is still going; this parks the playback until it is not. The sequencer never learns what the work is, which is how a program waits on a sprite animation without the sequencer requiring the renderer. A binding that is missing or dead is not a fault: there is nothing to wait for and the program carries on. So is a provider reporting a thing that never started. sequence.call("game.playTag", sequence.bind("hero"), "hurt"), sequence.await("game.spriteAnimation", sequence.bind("hero")), ```teal function tecs.sequence.await( provider: string, target: EntityRef, key: string ): Node ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `provider` | `string` | Resolved globally when the instruction runs, and checked before the binding: a name no build registered faults the playback with `unregisteredAwaitable`. The runtime asks the provider again at each fixed step boundary while the playback waits. An error faults the playback with `actionError`. | | `target` | [`EntityRef`](/modules/sequence/#tecs.sequence.EntityRef) | A `bind` reference and nothing else, as `playTween`'s is. An entity that dies while the playback waits releases it rather than stranding it for the life of the world. | | `key` | `string` | Handed to the provider unread, for a provider that answers about more than one thing per entity. Nil when omitted, which is what a provider with one answer per entity sees. | #### Returns | Type | Description | | --- | --- | | [`Node`](/modules/sequence/#tecs.sequence.Node) | A node for `define`, and not to be put in a second program. | ### tecs.sequence.bind Static References an entity supplied through `PlayOptions.bindings`. Resolved when the instruction using it runs, not at `play`, so a binding may name an entity that does not exist yet. ```teal function tecs.sequence.bind(name: string): EntityRef ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `name` | `string` | Must be non-empty, and an empty one raises here rather than at `define`. The runtime matches it against `PlayOptions.bindings`. A missing binding resolves to nothing instead of faulting. | #### Returns | Type | Description | | --- | --- | | [`EntityRef`](/modules/sequence/#tecs.sequence.EntityRef) | A reference to put in a `call` or `emit` argument list, or to hand to `playTween` or `await`. The program interns it in the const pool, and each playback resolves it against its own bindings. | ### tecs.sequence.call Static Runs a registered action. ```teal function tecs.sequence.call(action: string, ...: Argument): Node ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `action` | `string` | Resolved per world when the instruction runs, not at `define`, so a program may name an action registered later. A name still unregistered when the step runs faults the playback. | | `...` | [`Argument`](/modules/sequence/#tecs.sequence.Argument) | Compiled into the program's const pool, so they are the same values for every playback of it. Put anything that differs between two playbacks in `PlayOptions.params` instead. | #### Returns | Type | Description | | --- | --- | | [`Node`](/modules/sequence/#tecs.sequence.Node) | A node for `define`, and not to be put in a second program. | ### tecs.sequence.cancel Static Stops a playback and releases its cursor. This remains safe on a finished handle. It takes the branches it forked with it, and a playback parked in a `waitTween` on the one canceled is told `canceled` and resumes. What a `playTween` started is a playback in its own right and is not canceled with the one that started it, unlike a pause, which cascades to it: stop it through its owner with `cancelOwnedBy`. ```teal function tecs.sequence.cancel(world: World, handle: Handle): boolean ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`World`](/modules/ecs/#tecs.World) | The world owns the playback. | | `handle` | [`Handle`](/modules/sequence/#tecs.sequence.Handle) | Read against this world's cursor arena, so one issued by another world is meaningless here rather than reliably ignored. | #### Returns | Type | Description | | --- | --- | | `boolean` | false when the handle names nothing running, whether it already finished or was already canceled. | ### tecs.sequence.cancelOwnedBy Static Cancels every playback owned by an entity. `reason` is what anything waiting on one of them is told. The owner despawning reports `targetLost`, which lets a waiter distinguish cancellation from target loss. ```teal function tecs.sequence.cancelOwnedBy( world: World, owner: integer, reason: TweenOutcome ): integer ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`World`](/modules/ecs/#tecs.World) | The world owns the playbacks. | | `owner` | `integer` | An entity id. A playback started without an owner is nothing owns a playback started without an owner, rather than entity 0, so no argument reaches one and only `cancel` on its handle stops it. | | `reason` | [`TweenOutcome`](/modules/sequence/#tecs.sequence.TweenOutcome) | What a playback parked in a `waitTween` on one of these is told, and what its `status` reports afterwards as `tweenOutcome`. Omitted, a waiter is told `canceled`. | #### Returns | Type | Description | | --- | --- | | `integer` | Returns the number of canceled playbacks, or zero when the entity owns none. The call reaches branches through their inherited owner and counts each cursor once. | ### tecs.sequence.currentStep Static Returns a clock's current tick as counted by the sequencer. It defaults to the fixed clock. ```teal function tecs.sequence.currentStep( world: World, clock: ClockId ): integer ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`World`](/modules/ecs/#tecs.World) | The world owns the clock counters. | | `clock` | [`ClockId`](/modules/sequence/#tecs.sequence.ClockId) | Which of the three counters to read. Omitted, `"fixed"`. | #### Returns | Type | Description | | --- | --- | | `integer` | Ticks of that one clock, rising by one each time the sequencer advances it and by nothing else. It counts from when this world's the world created its sequencer state, not when the process started. A snapshot load puts back the count the snapshot carried. Comparable only against another reading of the same clock. | ### tecs.sequence.dataOps Static Returns the step names accepted by `defineData`, sorted. ```teal function tecs.sequence.dataOps(): {string} ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | `{string}` | A fresh table each call, which the caller owns. It is narrower than the node constructors: `await` and `eval` have no data form and are not among them. | ### tecs.sequence.define Static Compiles a program under a stable symbolic name. `options.clock` picks what the program's waits count. `"fixed"`, the default, counts fixed steps and is what gameplay logic wants. `"frame"` counts gameplay frames, one tick per frame however many fixed steps that frame runs, which is what scripted input needs. `"presentation"` also ticks once per gameplay frame carrying the frame's real elapsed time. ```teal function tecs.sequence.define( name: string, nodes: {Node}, options: DefineOptions ): Program ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `name` | `string` | Defining over a name already in use publishes a new version rather than replacing the old one: playbacks already running keep the version they started on, and only later `play` calls see the new one. | | `nodes` | `{`[`Node`](/modules/sequence/#tecs.sequence.Node)`}` | Accepts an empty list, which compiles to a program that completes on its first tick. | | `options` | [`DefineOptions`](/modules/sequence/#tecs.sequence.DefineOptions) | Omitted, the program counts fixed steps. A `clock` that is none of the three names raises here. | #### Returns | Type | Description | | --- | --- | | [`Program`](/modules/sequence/#tecs.sequence.Program) | The compiled program, immutable and shared by every playback of it, so the same value plays on any number of worlds. | ### tecs.sequence.defineData Static Compiles a program written as plain data instead of node calls. The same authoring surface as a list of steps, for callers that cannot invoke Lua: a tool over MCP, a file on disk, a hand-written table. Returns nil plus the path to the first bad entry. sequence.defineData("game.intro", { {op = "call", action = "game.lockControls"}, {op = "wait", seconds = 1.5}, }) ```teal function tecs.sequence.defineData( name: string, rows: {any}, options: DefineOptions ): Program, string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `name` | `string` | As `define`'s, and sharing its namespace and its versioning. | | `rows` | `{any}` | One table per step, keyed by `op` and the fields that step takes. `dataOps` lists the step names. This function rejects an empty list, unlike `define`, because it has nothing to run. | | `options` | [`DefineOptions`](/modules/sequence/#tecs.sequence.DefineOptions) | As `define`'s. | #### Returns | Type | Description | | --- | --- | | [`Program`](/modules/sequence/#tecs.sequence.Program) | The compiled program, or nil when a row is bad. | | `string` | nil on success; on failure the path to the first bad entry, as `program[2].loop[1]`, and what it was missing. That is why the two are never both set. A row whose shape is right and whose value is not, a negative wait among them, raises instead: the path covers what this decoder checks, not what the node constructors do. | ### tecs.sequence.disassemble Static Renders a program as readable instructions. ```teal function tecs.sequence.disassemble( program: Program, pc: integer ): string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `program` | [`Program`](/modules/sequence/#tecs.sequence.Program) | A value from `define` or `timeline`; anything else raises. | | `pc` | `integer` | Mark this instruction, as a playback's `status` reports it. Omit for an unmarked listing. One past the end, or any other address the program does not hold, marks nothing. | #### Returns | Type | Description | | --- | --- | | `string` | A header naming the program and its version, then one line per instruction, newline separated. For reading and not for parsing: the format is free to change. | ### tecs.sequence.emit Static Emits an ECS event at address zero. Occupies no tick: the step after it runs in the same one. ```teal function tecs.sequence.emit(event: string, ...: Argument): Node ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `event` | `string` | Carried as the `sequence.Event`'s name. Observers watch `sequence.Event` itself and filter on this, rather than each name being an event type of its own. | | `...` | [`Argument`](/modules/sequence/#tecs.sequence.Argument) | Const-pool values, as `call`'s are, delivered on the event's `args` with any `bind` reference already resolved to an entity id. Each emit copies the list and sends it with the event, so an observer may keep it, unlike the one an action receives. | #### Returns | Type | Description | | --- | --- | | [`Node`](/modules/sequence/#tecs.sequence.Node) | A node for `define`, and not to be put in a second program. | ### tecs.sequence.eval Static Evaluates a registered evaluator every tick until it finishes. Unlike every other step, this one does not hand the tick back: the playback joins its clock's active set, and the runtime steps it each tick. Values that move every frame need this behavior. Compilation stores `data` beside the evaluator's name in the program, so it travels with a snapshot and must remain plain data for the same reason a `call` argument does. What the evaluator makes of it is its own business. ```teal function tecs.sequence.eval(evaluator: string, data: any): Node ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `evaluator` | `string` | Resolved globally when the instruction runs, so a program may name an evaluator registered later. A name still unregistered when the step runs faults the playback with `unregisteredEvaluator`, and an evaluator that raises while resolving the data or building its state faults it with `actionError`. | | `data` | `any` | Passed through the evaluator's `resolve` once per program constant and the answer shared by every playback of it, so an evaluator must not write per-playback state into what it gets back. Omit it for an evaluator that needs none. | #### Returns | Type | Description | | --- | --- | | [`Node`](/modules/sequence/#tecs.sequence.Node) | A node for `define`, and not to be put in a second program. | ### tecs.sequence.fork Static Starts a branch that runs alongside the rest of the program. The branch is a playback of its own running the same program at a different instruction, and it inherits the owner, bindings, params and instruction budget of the playback that forked it. It starts once that playback next waits, joins, or ends, within the same tick. A branch never outlives the playback that forked it: finishing or canceling that playback cancels the branches it has not joined. A branch that faults takes its parent with it, faulted `branchFaulted`, rather than leaving a `join` one branch short forever. ```teal function tecs.sequence.fork(nodes: {Node}): Node ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `nodes` | `{`[`Node`](/modules/sequence/#tecs.sequence.Node)`}` | The branch body, run in order. Must be non-empty. It reads and writes the forking playback's own bindings table rather than a copy, so both see any name an action binds. | #### Returns | Type | Description | | --- | --- | | [`Node`](/modules/sequence/#tecs.sequence.Node) | A node for `define`, and not to be put in a second program. | ### tecs.sequence.hasAction Static Returns whether this world has registered an action name. ```teal function tecs.sequence.hasAction(world: World, name: string): boolean ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`World`](/modules/ecs/#tecs.World) | The world owns the action registry. | | `name` | `string` | The caller supplies the action name to find. | #### Returns | Type | Description | | --- | --- | | `boolean` | false for a name this world never registered, including one another world has: actions are per world. | ### tecs.sequence.hasQuery Static Returns whether this world has registered a query name. ```teal function tecs.sequence.hasQuery(world: World, name: string): boolean ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`World`](/modules/ecs/#tecs.World) | The world owns the query registry. | | `name` | `string` | The caller supplies the query name to find. | #### Returns | Type | Description | | --- | --- | | `boolean` | false for a name this world never registered, including one another world has: queries are per world. | ### tecs.sequence.join Static Waits for every branch that has not yet joined. Falls straight through when there are none. When the last branch finishes, the waiting playback resumes within that same tick. ```teal function tecs.sequence.join(): Node ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | [`Node`](/modules/sequence/#tecs.sequence.Node) | A node for `define`, and not to be put in a second program. | ### tecs.sequence.loop Static Repeats a block. ```teal function tecs.sequence.loop(count: integer, nodes: {Node}): Node ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `count` | `integer` | Iterations, or nil to repeat until canceled. A positive whole number. This function rejects zero and fractions. | | `nodes` | `{`[`Node`](/modules/sequence/#tecs.sequence.Node)`}` | The block, run in order and started again from its first node. Must be non-empty. A block with no wait in it spends the playback's per-step instruction budget within one tick and faults it with `budgetExceeded`, which is what that budget is for. | #### Returns | Type | Description | | --- | --- | | [`Node`](/modules/sequence/#tecs.sequence.Node) | A node for `define`, and not to be put in a second program. | ### tecs.sequence.parallel Static Forks several blocks and waits for all of them. Sugar for a `fork` per block followed by one `join`. Blocks start in argument order. ```teal function tecs.sequence.parallel(...: {Node}): Node ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `...` | `{`[`Node`](/modules/sequence/#tecs.sequence.Node)`}` | Supplies at least one non-empty node list, one per branch. Each shares the playback's bindings table, as a `fork` does. | #### Returns | Type | Description | | --- | --- | | [`Node`](/modules/sequence/#tecs.sequence.Node) | A node for `define`, and not to be put in a second program. | ### tecs.sequence.pause Static Suspends a playback, its branches, and anything it started. Pause is holder counted: two systems can hold the same playback for unrelated reasons, and it runs again only when the last one lets go, so whichever resumes first cannot undo the other. `holder` names who is holding it and defaults to `"user"`. Its wait, if any, resumes from where it paused. What it is waiting on stops with it when the awaitable provider knows how. ```teal function tecs.sequence.pause( world: World, handle: Handle, holder: string ): boolean ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`World`](/modules/ecs/#tecs.World) | The world owns the playback. | | `handle` | [`Handle`](/modules/sequence/#tecs.sequence.Handle) | The caller supplies the playback to pause. | | `holder` | `string` | Any string, and a set rather than a count: two pauses under the same holder are one hold, and one `resume` under it releases both. Two callers that both leave it at the default therefore share a single hold. | #### Returns | Type | Description | | --- | --- | | `boolean` | Whether the playback stopped. A second holder taking a hold on one that is already paused registers the hold and returns false, because nothing observable changed. So does a handle that names nothing running. | ### tecs.sequence.play Static Starts a program. The first instruction runs on the next tick of the program's own clock, never inside this call. ```teal function tecs.sequence.play( world: World, program: Program, options: PlayOptions ): Handle ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`World`](/modules/ecs/#tecs.World) | The world owns the new playback. | | `program` | [`Program`](/modules/sequence/#tecs.sequence.Program) | A value from `define` or `timeline`, not a name. The playback runs this version for its whole life, even if another `define` replaces the name. Anything other than a compiled program raises. | | `options` | [`PlayOptions`](/modules/sequence/#tecs.sequence.PlayOptions) | Omitted, the playback has no owner, no bindings, no params, no channel, and the world's instruction budget. Taking a channel another playback on the same owner holds cancels that one first, reporting `replaced` to anything waiting on it. | #### Returns | Type | Description | | --- | --- | | [`Handle`](/modules/sequence/#tecs.sequence.Handle) | A handle to the new playback, valid until it ends. A handle carries a generation, so one whose playback has ended never names a later playback that took the same slot. | ### tecs.sequence.playbacks Static Returns handles for every live playback, branches included, in a stable order. For diagnostics and the debugger, not for hot paths. ```teal function tecs.sequence.playbacks(world: World): {Handle} ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`World`](/modules/ecs/#tecs.World) | The world owns the playbacks. | #### Returns | Type | Description | | --- | --- | | `{`[`Handle`](/modules/sequence/#tecs.sequence.Handle)`}` | Returns a fresh table ordered by cursor slot. Slot order remains stable within a run but differs from creation order because new playbacks reuse free slots. The table also includes playbacks started by `playTween`. | ### tecs.sequence.playTween Static Plays a registered tween preset on a bound entity. Needs the sequencer, which `tecs.newApplication` installs. A binding that is missing or dead is not a fault: nothing plays, and a following `waitTween` resumes at once reporting `targetLost`. The step itself costs no tick. It starts a separate playback owned by the bound entity, running on the named program's clock and using this playback's instruction budget. ```teal function tecs.sequence.playTween( timeline: string, target: EntityRef, params: {string: any} ): Node ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `timeline` | `string` | Resolved when the instruction runs, and always to the newest version of the name; any program defined under it will do, though `timeline` is what normally publishes one. A name that is not defined faults the playback with `unregisteredTween`. | | `target` | [`EntityRef`](/modules/sequence/#tecs.sequence.EntityRef) | A `bind` reference and nothing else: an entity id raises here, for the same reason `define` rejects one as a constant. | | `params` | `{string : any}` | The started playback's `params`, so `mode`, `count`, `speed` and `delay` shape it as they do for `play`. A `channel` in it names the slot the playback takes on that entity, canceling whatever held the slot and reporting `replaced` to anything waiting on it. | #### Returns | Type | Description | | --- | --- | | [`Node`](/modules/sequence/#tecs.sequence.Node) | A node for `define`, and not to be put in a second program. | ### tecs.sequence.plugin Static Installs the sequencer. `tecs.newApplication` installs it automatically, and it remains safe to call again: a second install on the same world does nothing. ```teal function tecs.sequence.plugin(world: World) ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`World`](/modules/ecs/#tecs.World) | The world receives sequencer state and systems. | #### Returns None. ### tecs.sequence.program Static Returns the newest version of a defined program or a requested version. Every version a playback still runs stays reachable. ```teal function tecs.sequence.program( name: string, version: integer ): Program ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `name` | `string` | The caller supplies a name previously passed to `define` or `timeline`. | | `version` | `integer` | Versions start at 1 and rise by one per `define` of the same name. Omit it for the newest. | #### Returns | Type | Description | | --- | --- | | [`Program`](/modules/sequence/#tecs.sequence.Program) | nil when the name was never defined, or when that version was after a newer definition replaces it and its last playback ends. The newest version of a name is never dropped. | ### tecs.sequence.programNames Static Returns the names of every defined program, sorted. ```teal function tecs.sequence.programNames(): {string} ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | `{string}` | A fresh table each call, which the caller owns. Registration is global, so this spans every world in the process. | ### tecs.sequence.registerAction Static Registers an action that a `call` node can name. Actions must obey the deterministic-action contract to keep replay and rewind meaningful: no wall-clock reads, no file or network access, and randomness only from a generator whose state travels with the snapshot. ```teal function tecs.sequence.registerAction( world: World, name: string, action: Action ) ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`World`](/modules/ecs/#tecs.World) | The world owns the action registry. | | `name` | `string` | Per world, unlike an evaluator or an awaitable. Registering it twice replaces the action, and a playback resolves the name at each `call`, so one already running picks the new one up. An empty name raises. | | `action` | [`Action`](/modules/sequence/#tecs.sequence.Action) | Must be a function, or this raises. Raising when it runs faults the playback with `actionError`, and whatever it already wrote to the world stays written: the sequencer rolls nothing back. | #### Returns None. ### tecs.sequence.registerAwaitable Static Registers a provider that an `await` step can name. Registration remains global, like an evaluator, because a provider is code. ```teal function tecs.sequence.registerAwaitable( name: string, provider: Awaitable ) ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `name` | `string` | Registering a name twice replaces the provider, which is what a hot reload wants: a playback already parked on the name asks the new one at the next step. An empty name raises. | | `provider` | [`Awaitable`](/modules/sequence/#tecs.sequence.Awaitable) | Must carry `isPending`, or this raises. `setPaused` is optional. Without it, the provider leaves its work running when a pause stops the waiting playback. | #### Returns None. ### tecs.sequence.registerEvaluator Static Registers an evaluator that an `eval` step can name. Registration is global, like a program: an evaluator is code, and two worlds evaluating the same name run the same thing. ```teal function tecs.sequence.registerEvaluator( name: string, evaluator: Evaluator ) ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `name` | `string` | Registering a name twice replaces it. An empty name raises. | | `evaluator` | `Evaluator` | Must carry `step`, or this raises. The rest are optional, and without `save` and `load` a playback evaluating this name comes back from a snapshot at the start of its state rather than where it was. | #### Returns None. ### tecs.sequence.registerQuery Static Registers a query that a `waitQuery` step can name. The query subscribes to archetype transitions, so a wait on it stays event driven rather than polling. Registration is startup work: a name registered twice keeps the newer query, and the replaced one holds its subscriptions for the life of the world. ```teal function tecs.sequence.registerQuery( world: World, name: string, descriptor: types.Query.Descriptor ) ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`World`](/modules/ecs/#tecs.World) | The world owns the query registry. | | `name` | `string` | Empty raises. Registering it again re-tests every playback already parked on the name, against the new query, at the next fixed step. | | `descriptor` | [`types.Query.Descriptor`](/modules/ecs/#tecs.Query.Descriptor) | The function copies this descriptor and wraps its `onEntitiesAdded` and `onEntitiesRemoved` callbacks. It marks the name before calling the supplied callbacks. A non-table raises. | #### Returns None. ### tecs.sequence.resume Static Releases one holder's claim. ```teal function tecs.sequence.resume( world: World, handle: Handle, holder: string ): boolean ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`World`](/modules/ecs/#tecs.World) | The world owns the playback. | | `handle` | [`Handle`](/modules/sequence/#tecs.sequence.Handle) | The caller supplies the paused playback. | | `holder` | `string` | Must be the string that took the hold; releasing one that never held it changes nothing. Defaults to `"user"`, as `pause`'s does. | #### Returns | Type | Description | | --- | --- | | `boolean` | Whether the playback started running again, which is only true for the last holder to let go. A wake time that passed while it was held runs it on the next tick rather than immediately. | ### tecs.sequence.setInstructionBudget Static Sets the per-step instruction budget for playbacks without their own. ```teal function tecs.sequence.setInstructionBudget( world: World, instructions: integer ) ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`World`](/modules/ecs/#tecs.World) | The world applies the budget to new playbacks. | | `instructions` | `integer` | Must be at least 1. It caps how many instructions one playback runs per tick, and exceeding it faults the playback with `budgetExceeded` rather than deferring the rest, so it is a guard against a `loop` with no wait in it, not a scheduler. Applies to playbacks started after this call; a running one keeps the budget it started with. | #### Returns None. ### tecs.sequence.signal Static Raises a named signal and wakes every playback blocked on it. Delivery happens on the next fixed step. Raising a signal nothing is waiting on is not an error and is not remembered: a playback that reaches `waitSignal` afterwards keeps waiting. ```teal function tecs.sequence.signal(world: World, name: string): integer ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`World`](/modules/ecs/#tecs.World) | The world owns the signal state. | | `name` | `string` | Any string, registered nowhere. An empty one raises. | #### Returns | Type | Description | | --- | --- | | `integer` | Returns the number of playbacks waiting on the name now. The next fixed step wakes the set waiting then, which may differ. | ### tecs.sequence.signalOnEvent Static Raises a signal whenever an event emits at address zero. An ECS event is an edge, like a signal, so this wires one to the other rather than adding a separate kind of wait. Delivery follows the ordinary signal rule: waiters wake on the next fixed step. ```teal function tecs.sequence.signalOnEvent( world: World, name: string, event: T ) ``` #### Type Parameters | Name | Constraint | Description | | --- | --- | --- | | `T` | [`events.Event`](/modules/events/#tecs.events.Event) | | #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`World`](/modules/ecs/#tecs.World) | The world owns the event observer and signal state. | | `name` | `string` | The signal to raise, once per emission. An empty one raises. | | `event` | `T` | Observed at address 0, and only there. The observer lasts for the life of the world; wiring the same pair twice observes it twice, and there is nothing that unwires one. | #### Returns None. ### tecs.sequence.status Static Returns playback status, or nil for a handle this world never issued. ```teal function tecs.sequence.status(world: World, handle: Handle): Status ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`World`](/modules/ecs/#tecs.World) | The world owns the playback. | | `handle` | [`Handle`](/modules/sequence/#tecs.sequence.Handle) | Read against this world's cursor arena, as `cancel`'s is. | #### Returns | Type | Description | | --- | --- | | [`Status`](/modules/sequence/#tecs.sequence.Status) | A fresh table each call, which the caller owns. A finished playback still answers, with `state` saying how it ended, until its a later `play` takes its slot. The runtime fills waiting fields only while the playback lives, so a finished playback reports its program, the `pc` it stopped at, how it ended and its last tween outcome, with the rest nil. | ### tecs.sequence.timeline Static Compiles a tween timeline into a program under a stable name. The compiled slots travel in the program's const pool, so a timeline is snapshot-safe for the same reason every other program is. Play it with `play`, giving the entity it animates as the `owner`. It reads four `params`, all optional: mode "once" (default), "loop", or "pingPong" count passes for a finite loop or ping-pong; endless without it speed playback multiplier, defaulting to 1 delay seconds to wait before the first frame local fade = sequence.timeline("game.fade", { sequence.tweenTo(0.4, "quadOut", "color.a", 0), }) sequence.play(world, fade, {owner = e, params = {delay = 0.2}}) ```teal function tecs.sequence.timeline( name: string, spec: TimelineSpec, options: TimelineOptions ): Program ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `name` | `string` | Shared with `define`'s namespace, and versioned the same way: compiling over a live name publishes a new version and leaves playbacks of the old one running it. | | `spec` | [`TimelineSpec`](/modules/sequence/#tecs.sequence.TimelineSpec) | The compiler consumes this table in place. Do not compile it again. | | `options` | [`TimelineOptions`](/modules/sequence/#tecs.sequence.TimelineOptions) | Omitted, the timeline runs on the presentation clock. `"fixed"` and `"presentation"` are the only two accepted; `"frame"` raises, though `define` takes it. | #### Returns | Type | Description | | --- | --- | | [`Program`](/modules/sequence/#tecs.sequence.Program) | The compiled program, also reachable by name through `program`. Play it on an entity: a playback with no `owner` has nothing to write to and completes on its first tick without animating anything. The `params` above are read when that playback builds its state, so a `speed` of zero or less, a negative `delay`, or a repeat of a zero-length timeline faults the playback with `actionError` rather than raising from `play`. | ### tecs.sequence.tweenAdjust Static Interpolates by a relative delta from the starting value. Identical to `tweenTo` except that `t1` through `t4` are the amount to move by rather than where to end up, so the destination depends on the entity's value when the operation evaluates its first tick. ```teal function tecs.sequence.tweenAdjust( duration: number, curve: EasingName | EasingFunction, target: TargetName | Target, t1: number, t2: number, t3: number, t4: number ): TimelineNode ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `duration` | `number` | Seconds, and strictly positive. | | `curve` | [`EasingName`](/modules/sequence/#tecs.sequence.EasingName) | [`EasingFunction`](/modules/sequence/#tecs.sequence.EasingFunction) | As `tweenTo`'s, and built in for the same reason. | | `target` | [`TargetName`](/modules/sequence/#tecs.sequence.TargetName) | [`Target`](/modules/sequence/#tecs.sequence.Target) | Which component fields move, and in which order `t1` through `t4` line up with them. | | `t1` | `number` | Change applied to the target's first field, in that field's own units, and signed: negative moves the other way. | | `t2` | `number` | Change applied to the second field. | | `t3` | `number` | Change applied to the third field. | | `t4` | `number` | Change applied to the fourth field. An argument past the The compiler ignores arguments past the target's field count and treats an omitted argument as zero. | #### Returns | Type | Description | | --- | --- | | [`TimelineNode`](/modules/sequence/#tecs.sequence.TimelineNode) | Returns a node for a [`TimelineSpec`](/modules/sequence/#tecs.sequence.TimelineSpec). Its compiling timeline consumes it. | ### tecs.sequence.tweenEmit Static Emits a named `sequence.Event` when the cursor reaches this point. Occupies no time, so what follows it starts at the same instant. The event carries the entity the timeline is animating as its one argument, and the timeline's own playback as its handle. It fires as the cursor passes the point going forward, so a `pingPong`'s return leg does not fire it a second time. ```teal function tecs.sequence.tweenEmit(name: string): TimelineNode ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `name` | `string` | Carried on the event as its name; the sequencer attaches no meaning to it. | #### Returns | Type | Description | | --- | --- | | [`TimelineNode`](/modules/sequence/#tecs.sequence.TimelineNode) | Returns a node for a [`TimelineSpec`](/modules/sequence/#tecs.sequence.TimelineSpec). | ### tecs.sequence.tweenParallel Static Runs timeline nodes concurrently and ends with the longest. Every argument starts at the point the `tweenParallel` sits at, and what follows starts after the last of them ends. Two arguments writing the same component field is not an error and the later one in argument order wins each tick. ```teal function tecs.sequence.tweenParallel(...: TimelineNode): TimelineNode ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `...` | [`TimelineNode`](/modules/sequence/#tecs.sequence.TimelineNode) | One node per concurrent operation. A list of nodes counts as one argument and runs in sequence within it, which describes a branch longer than one operation. | #### Returns | Type | Description | | --- | --- | | [`TimelineNode`](/modules/sequence/#tecs.sequence.TimelineNode) | Returns a node for a [`TimelineSpec`](/modules/sequence/#tecs.sequence.TimelineSpec). | ### tecs.sequence.tweenRun Static Runs a nested timeline from its own spec. The nested timeline occupies a window of the parent equal to its own length times its pass count, so a repeating nested run needs a finite `count`. Its own operations keep their per-playback state separately from the parent's. ```teal function tecs.sequence.tweenRun( spec: TimelineSpec, options: RunOptions ): TimelineNode ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `spec` | [`TimelineSpec`](/modules/sequence/#tecs.sequence.TimelineSpec) | Compiled in place if it has not been already, so the table given here belongs to this node afterwards. | | `options` | [`RunOptions`](/modules/sequence/#tecs.sequence.RunOptions) | Omit to run the nested timeline once. `"loop"` and `"pingPong"` need a `count` here, and compiling raises without one. | #### Returns | Type | Description | | --- | --- | | [`TimelineNode`](/modules/sequence/#tecs.sequence.TimelineNode) | Returns a node for a [`TimelineSpec`](/modules/sequence/#tecs.sequence.TimelineSpec). | ### tecs.sequence.tweenTo Static Interpolates to an absolute destination. The operation reads starting values from the entity on its first tick, not during compilation, so the same timeline played on two entities starts from wherever each of them is. ```teal function tecs.sequence.tweenTo( duration: number, curve: EasingName | EasingFunction, target: TargetName | Target, t1: number, t2: number, t3: number, t4: number ): TimelineNode ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `duration` | `number` | Supplies a strictly positive number of seconds. The compiler rejects zero. Use `tweenWait` for a gap that moves nothing. | | `curve` | [`EasingName`](/modules/sequence/#tecs.sequence.EasingName) | [`EasingFunction`](/modules/sequence/#tecs.sequence.EasingFunction) | A name from `sequence.easing`, or one of those functions itself. The compiler rejects a custom curve: a compiled slot carries the curve's name so it can travel in a const pool, and a function it cannot name has none. | | `target` | [`TargetName`](/modules/sequence/#tecs.sequence.TargetName) | [`Target`](/modules/sequence/#tecs.sequence.Target) | Which component fields move, and in which order `t1` through `t4` line up with them. | | `t1` | `number` | Destination for the target's first field, in that field's own units: pixels for a translate, radians for a rotation, 0 to 1 for a color channel. A shortest-path rotation target still takes an absolute angle and picks the short way round to it. | | `t2` | `number` | Destination for the second field, for a target that has one. | | `t3` | `number` | Destination for the third field, for a four-field target. | | `t4` | `number` | Destination for the fourth field, for a four-field target. The compiler ignores arguments past the target's field count and treats each omitted target field as zero. | #### Returns | Type | Description | | --- | --- | | [`TimelineNode`](/modules/sequence/#tecs.sequence.TimelineNode) | Returns a node for a [`TimelineSpec`](/modules/sequence/#tecs.sequence.TimelineSpec). Its compiling timeline consumes it. | ### tecs.sequence.tweenTrack Static Interpolates toward a destination that keeps moving. The first tick fixes the start, as with `tweenTo`, but each later tick reads the destination again from `from` while the operation remains inside its window, so the entity chases a value that is still changing. Once the window ends, the operation holds its last destination. ```teal function tecs.sequence.tweenTrack( duration: number, curve: EasingName | EasingFunction, target: TargetName | Target, from: TrackSource ): TimelineNode ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `duration` | `number` | Seconds, and strictly positive. | | `curve` | [`EasingName`](/modules/sequence/#tecs.sequence.EasingName) | [`EasingFunction`](/modules/sequence/#tecs.sequence.EasingFunction) | As `tweenTo`'s. It shapes how far toward the current destination the value sits, and that destination keeps moving underneath it. | | `target` | [`TargetName`](/modules/sequence/#tecs.sequence.TargetName) | [`Target`](/modules/sequence/#tecs.sequence.Target) | Which component fields move, and which fields of `from` line up with them: the source is read into the same one to four numbers the target writes. | | `from` | [`TrackSource`](/modules/sequence/#tecs.sequence.TrackSource) | Where the destination is read each tick. A source whose entity or component is missing reads as zero rather than faulting, so an entity chasing something that despawned drifts to the origin. A source named by world key is the exception and requires the key to be set. | #### Returns | Type | Description | | --- | --- | | [`TimelineNode`](/modules/sequence/#tecs.sequence.TimelineNode) | Returns a node for a [`TimelineSpec`](/modules/sequence/#tecs.sequence.TimelineSpec). Its compiling timeline consumes it. | ### tecs.sequence.tweenWait Static Advances the timeline cursor without changing anything. ```teal function tecs.sequence.tweenWait(duration: number): TimelineNode ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `duration` | `number` | Supplies seconds. Zero performs no work. | #### Returns | Type | Description | | --- | --- | | [`TimelineNode`](/modules/sequence/#tecs.sequence.TimelineNode) | Returns a node for a [`TimelineSpec`](/modules/sequence/#tecs.sequence.TimelineSpec). | ### tecs.sequence.upcoming Static Returns the actions a playback will certainly perform next and their timing. Walks the straight-line run ahead of where it sits, accumulating waits, and stops at the first instruction whose successor requires execution. It counts ticks in the playback's own clock, from now. ```teal function tecs.sequence.upcoming( world: World, handle: Handle, withinTicks: integer ): {Step} ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`World`](/modules/ecs/#tecs.World) | The world owns the playback. | | `handle` | [`Handle`](/modules/sequence/#tecs.sequence.Handle) | The caller supplies the playback to inspect. | | `withinTicks` | `integer` | Report only what is due within this many ticks. Counted in the playback's own clock, from now. Omit for everything the walk can reach. | #### Returns | Type | Description | | --- | --- | | `{Step}` | A fresh table each call, holding the calls and emits ahead in order. Empty for a handle that names nothing running, and also for one parked on a signal, a query, a join, a tween or an evaluator, since none of those has a predictable start. A wait written in seconds also ends the walk, because converting it needs a clock the program does not carry. Each step's `args` is the program's own constant list rather than a copy of it, so read it and do not write to it. A paused playback still answers, counted as though it were running. | ### tecs.sequence.wait Static Waits for a duration in seconds. Converted to whole ticks when the instruction runs, rounded to nearest, with any non-zero duration waiting at least one tick. A program on the fixed clock divides the duration by the world's fixed timestep; the frame and presentation clocks both tick once per frame, so both divide by the loop's nominal frame dt. Programs therefore stay independent of any one world's timing, and the same duration is the same wall-clock wait on all three. Use `waitSteps` when the exact tick count matters more than the wall-clock duration. ```teal function tecs.sequence.wait(seconds: number): Node ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `seconds` | `number` | Supplies a non-negative duration. This function rejects a negative value. Zero still costs a tick: the cursor resumes on the program's next tick rather than carrying on within this one, so no duration makes `wait` free. | #### Returns | Type | Description | | --- | --- | | [`Node`](/modules/sequence/#tecs.sequence.Node) | A node for `define`, and not to be put in a second program. | ### tecs.sequence.waitingOn Static Returns how many playbacks currently wait on a signal name. ```teal function tecs.sequence.waitingOn(world: World, name: string): integer ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`World`](/modules/ecs/#tecs.World) | The world owns the waiting playbacks. | | `name` | `string` | The caller supplies the signal name to count. | #### Returns | Type | Description | | --- | --- | | `integer` | 0 for a name nothing waits on, including one never signaled. Counts current waiters. A signal raised during this step remains in the count until the next step delivers it. | ### tecs.sequence.waitQuery Static Blocks until a registered query matches or stops matching. Unlike a signal, which is an edge, this is a condition on the world: a wait whose condition already holds resumes rather than waiting for a transition that has already happened. The runtime evaluates the condition at the start of the next fixed step, never at the instruction itself, so it never reads a world that a spawn or the world has not committed a current-step despawn yet. A query wait therefore costs at least one step. ```teal function tecs.sequence.waitQuery( name: string, condition: QueryCondition ): Node ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `name` | `string` | Resolved per world when the instruction runs, not at `define`, so a program may name a query registered later. A name still unregistered when the step runs faults the playback with `unregisteredQuery`. | | `condition` | [`QueryCondition`](/modules/sequence/#tecs.sequence.QueryCondition) | Rejected here, not at compile time, when it is neither `"any"` nor `"empty"`. | #### Returns | Type | Description | | --- | --- | | [`Node`](/modules/sequence/#tecs.sequence.Node) | A node for `define`, and not to be put in a second program. | ### tecs.sequence.waitSignal Static Blocks until `signal` raises the named signal. The next fixed step delivers signals after `signal` runs, so a signal raised by one sequence never runs another within the same step. That bounds a chain of signals to one link per step and keeps delivery order independent of which cursor happened to run first. Delivery runs on the fixed clock whatever clock the program is on, so a frame or presentation program wakes at its own clock's next tick after the fixed step that delivered it. ```teal function tecs.sequence.waitSignal(name: string): Node ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `name` | `string` | Matched by value against `signal`'s, and registered nowhere: any string names a channel. A name nothing raises parks the playback until something cancels it, because a signal raised before the wait is not remembered. | #### Returns | Type | Description | | --- | --- | | [`Node`](/modules/sequence/#tecs.sequence.Node) | A node for `define`, and not to be put in a second program. | ### tecs.sequence.waitSteps Static Waits for a whole number of ticks of the program's clock. `waitSteps(0)` yields: the cursor resumes on its program's next tick rather than continuing within the current one. ```teal function tecs.sequence.waitSteps(steps: integer): Node ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `steps` | `integer` | Ticks of the program's own clock, so a program defined against `"frame"` or `"presentation"` counts those and not fixed steps. This function requires a non-negative whole number. | #### Returns | Type | Description | | --- | --- | | [`Node`](/modules/sequence/#tecs.sequence.Node) | A node for `define`, and not to be put in a second program. | ### tecs.sequence.waitTween Static Waits for the tween started by the most recent `playTween`. Resumes when that specific playback completes, gets canceled, loses its channel, or loses its entity, so it never waits forever. `status` reports the outcome as `tweenOutcome`. A `waitTween` that no `playTween` in this playback preceded falls straight through without costing a tick. ```teal function tecs.sequence.waitTween(): Node ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | [`Node`](/modules/sequence/#tecs.sequence.Node) | A node for `define`, and not to be put in a second program. | ## Values ### tecs.sequence.easing variable Read-only. Exposes built-in easing curves such as `sequence.easing.quadOut`. ```teal tecs.sequence.easing: tweeneval.Easings ``` ### tecs.sequence.Event variable Read-only. Exposes the event emitted by an `emit` step at address zero. ```teal tecs.sequence.Event: seqtypes.Event ``` ### tecs.sequence.source variable Read-only. Exposes sources from which `tweenTrack` reads changing values: `sequence.source.own(Transform2D, "x")`. ```teal tecs.sequence.source: tweeneval.Sources ``` ### tecs.sequence.target variable Read-only. Exposes built-in targets and constructors for new ones: `sequence.target.translateX`, `sequence.target.field(C, "hp")`. ```teal tecs.sequence.target: tweeneval.Targets ``` ### tecs.sequence.TrackingTarget variable Read-only. Exposes the component that selects a dynamic tracking-source entity. ```teal tecs.sequence.TrackingTarget: tweeneval.TrackingTarget ``` --- ## tecs.ui # tecs.ui Build retained interfaces from ordinary Tecs entities. `tecs.ui` supplies responsive layout, intrinsic sizing, clipping, scrolling, hit testing, keyboard focus, and semantic interaction events for HUDs, menus, inspectors, inventories, and in-world panels. A UI tree is an ECS hierarchy: spawn entities, relate them with `ChildOf`, describe their boxes with [`Style`](/modules/ui/#tecs.ui.Style), and let the plugin update only the layout and GPU instance data that changed. Screen roots follow window resizing and pixel density automatically. The Compose demo scene combining an intrinsic image, a stretched rectangle, a fixed circle, and text Use the module when several visual entities must size and position one another, when content must scroll or clip, or when pointer and keyboard input should target the same retained boxes that were drawn. It is not a DOM or a separate widget renderer. Existing rectangles, circles, sprites, images, and text remain the visible pieces, with their current materials, shaders, batching, and dirty tracking. `tecs.ui` composes those pieces and gives them layout and interaction. [`Style`](/modules/ui/#tecs.ui.Style) describes layout rather than appearance. Put a drawing component on the same entity or on a child, and add `Paint(true)` when its transform should stretch to the computed box. Drawing remains in the existing instanced producers and shaders. The [complete interface guide](/ui/) contains 37 copyable recipes for layout, visual composition, intrinsic sizing, images, scrolling, interaction, focus, and dirty-aware updates. The examples below provide a complete starting point without leaving this API reference. The plugin retains one native layout tree keyed by ECS entity ID. `ChildOf` is the layout hierarchy as well as the transform hierarchy. The layout system writes `Layout` only when the box changes and writes `RelativeTransform2D` only when layout or scrolling changes its result. Query membership observers retain structural invalidation, the ECS dirty-archetype set gates value updates, and the native tree computes only dirty roots before returning a compact list of changed boxes. `px`, `%`, and `auto` dimension strings are parsed only when a `Style` enters or dirties the retained tree. Unchanged frames skip layout computation, layout export, clip reconstruction, and hit-rectangle reconstruction. A screen root installed through `plugin(app)` follows the application's logical window size and pixel density automatically. The plugin observes viewport changes on the world's platform event bus, so unchanged frames do not poll the window. [`Intrinsic`](/modules/ui/#tecs.ui.Intrinsic) copies cached text, sprite-region, or custom leaf metrics into Taffy when the source component dirties. Wrapping text uses a bounded retained convergence pass: Taffy chooses its width, SDL_ttf measures that width exactly, and no native layout callback enters Lua. `Paint(true)` centers and stretches a drawing leaf over its box. Keep containers at unit scale and put their rectangle, circle, or image on an absolute child with `Paint`: transform scale composes through `ChildOf`, so a stretched container would also scale its descendants. ## Flex sizing and wrapping The Flex demo scene comparing a fixed-width child with flex-grow children and a wrapped grid The first row compares a fixed `88px` child with `flexGrow` weights one and two. The cards below use `flexWrap = "wrap"`, so the screenshot shows both remaining-space distribution and line wrapping against one retained parent. ## Absolute positioning and depth The Overlay demo scene with four absolutely positioned corner cards and a centered higher-depth circle Absolute children anchor to their retained parent without consuming flex space. Renderer depth remains a separate concern, which is why the center circle and its label can sit above the four anchored cards. ## Scrolling and clipping [`Scroll`](/modules/ui/#tecs.ui.Scroll) turns its entity's layout box into a viewport. Its `x` and `y` move descendants, not the viewport entity itself. Every nested viewport is intersected before the plugin assigns the existing `tecs.gfx.Clip` component, so each drawable still carries one clip index in the ordinary GPU instance. `firstClip` and `lastClip` reserve which of the renderer’s 255 clip rectangles the UI owns. Content extent includes negative, absolute, and nested descendants. Wheel distance that a deepest viewport cannot consume passes to its ancestors. `reveal` exposes a descendant programmatically, and `Scrollbar` derives a composed thumb transform without adding a renderer primitive. Snapshots retain scroll offsets and derive content extent again after load. The Scroll demo scene with a fixed clipped viewport, overflowing rows, and a composed block scrollbar Screen roots use logical UI coordinates and multiply clips by `pixelDensity`. World roots project viewport corners through the renderer camera. Clip rectangles stay axis-aligned; rotating a scroll viewport therefore does not rotate its clip. ## Interaction and navigation Passing the application to [`plugin`](/modules/ui/#tecs.ui.plugin) supplies its renderer and input, enabling clip-aware hit testing. Manual options remain available to tests and tools. [`Interaction`](/modules/ui/#tecs.ui.Interaction) opts in a layout box, and [`InteractionState`](/modules/ui/#tecs.ui.InteractionState) reports hover, per-pointer capture, drag, and focus without replacing application state. The plugin emits one bubbling [`Event`](/modules/ui/#tecs.ui.Event) at entity addresses. Mouse and touch identities capture independently; releasing over a captured target emits `"click"` and then `"activate"`, while draggable targets emit semantic drag events. Tab and Shift-Tab traverse visible focusable controls and repeat while held, and Return or Space emits `"activate"` at the focused control. Programmatic focus, focus reveal, and modal focus scopes use the same state. A `"wheel"` event begins at the deepest viewport under the pointer; setting `event.consumed = true` prevents both ancestor delivery and default scrolling. Hit priority uses the renderer's layer-depth calculation. A depth tie prefers the deeper UI node, explicit `Interaction.order`, and then stable retained insertion order. `Style.style.order` independently orders layout siblings. Give overlapping controls explicit orders and distinct renderer depth when both their input and drawing order are meaningful. ## Complete application Keep entity composition separate from plugin setup. This first block creates a full-window root, a centered panel, an ordinary GPU-rendered background, and intrinsically sized text: ```teal local tecs = require("tecs") local ui = tecs.ui local ChildOf = tecs.ecs.ChildOf local RelativeTransform2D = tecs.ecs.RelativeTransform2D local Tint = tecs.gfx.Tint local Material = tecs.gfx.Material local Renderable2D = tecs.gfx.Renderable2D local function spawnInterface( world: tecs.World, app: tecs.Application, font: tecs.gfx.Font ) local root = world:spawn( ui.Style({ width = "100%", height = "100%", justifyContent = "center", alignItems = "center", }), ui.Root("screen"), tecs.Transform2D(0, 0, 0, 16) ) local panel = world:spawn( ui.Style({ width = 420, height = 240, flexDirection = "column", padding = 20, gap = 12, }), RelativeTransform2D(), ChildOf(root) ) world:spawn( ui.Style({position = "absolute", inset = 0}), ui.Paint(true), RelativeTransform2D(0, 0, 0), Tint(0.035, 0.055, 0.09, 0.96), Material(tecs.gfx.materials.id("rounded"), 0.05), Renderable2D(), ChildOf(panel) ) world:spawn( ui.Style({maxWidth = "100%"}), ui.Intrinsic("text", {wrap = true}), RelativeTransform2D(0, 0, 1), Tint(0.94, 0.97, 1.0, 1.0), tecs.gfx.Text.new({ text = "This text and panel are ordinary ECS entities.", font = font, size = 16, }), ChildOf(panel) ) end ``` The application plugin installs text and UI before a `Startup` system spawns the retained entities. Passing `app` to `ui.plugin` supplies the renderer, input, window size, and pixel density: ```teal local function gamePlugin(world: tecs.World, app: tecs.Application) tecs.gfx.layers.configure( 16, { sort = "z", screenSpace = true, unlit = true, } ) world:addPlugin(tecs.gfx.textPlugin({renderer = app.renderer})) world:addPlugin(ui.plugin(app)) world:addSystem({ name = "game.SpawnInterface", phase = tecs.ecs.phases.Startup, run = function() local font = tecs.gfx.newTTF({ source = "fonts/JetBrainsMono-ExtraBold.ttf", name = "game-ui-16", size = 16, raster = "alpha", }) spawnInterface(world, app, font) end, }) end return tecs.newApplication({plugin = gamePlugin}) ``` The standalone [`docs/examples/ui.tl`](https://github.com/tecs-dev/tecs/blob/main/docs/examples/ui.tl) application expands that setup into a scrollable, keyboard-navigable panel: The standalone retained UI example with a centered panel and scrollable controls ## Common recipes ### Stretch an ordinary rectangle over a layout box Put the drawing component on an absolute child. `Paint(true)` copies the computed width and height into its relative transform without scaling the container's descendants: ```teal local card = world:spawn( ui.Style({width = 280, height = 120}), RelativeTransform2D(), ChildOf(panel) ) world:spawn( ui.Style({position = "absolute", inset = 0}), ui.Paint(true), RelativeTransform2D(0, 0, 0), Tint(0.10, 0.17, 0.27, 1.0), Material(tecs.gfx.materials.id("rounded"), 0.08), Renderable2D(), ChildOf(card) ) ``` ### Load an image and preserve its aspect ratio The asset loader decodes pixels asynchronously. The renderer registers them and returns the ordinary sprite consumed by `Intrinsic("image")`: ```teal local image = tecs.assets.loadImage( tecs.io.files.assetPath("images/portrait.png") ) local sprite = app.renderer.sprites:registerImage(image) world:spawn( ui.Style({height = 64}), ui.Intrinsic("image"), ui.Paint(true), RelativeTransform2D(0, 0, 2), sprite, Tint(1, 1, 1, 1), Renderable2D(), ChildOf(panel) ) ``` ### Create a clipped scrolling list `Scroll` belongs on the fixed viewport. Its larger child creates overflow, and the scrollbar is an ordinary composed drawing entity rather than a Taffy or renderer primitive: ```teal local viewport = world:spawn( ui.Style({width = "100%", height = 240}), ui.Scroll(), RelativeTransform2D(), ChildOf(panel) ) local content = world:spawn( ui.Style({ width = "100%", height = 600, flexDirection = "column", gap = 8, }), RelativeTransform2D(), ChildOf(viewport) ) world:spawn( ui.Scrollbar("vertical", 12, 4, 28), ui.Interaction({focusable = false, draggable = true, order = 100}), RelativeTransform2D(0, 0, 10), Tint(0.32, 0.82, 1.0, 1.0), Material(tecs.gfx.materials.id("rounded"), 0), Renderable2D(), ChildOf(viewport) ) ``` ### Observe pointer and keyboard activation `Interaction` makes the computed box selectable but draws nothing. Events bubble through `ChildOf`, and Return or Space produces the same `activate` event as a click: ```teal local type uiTypes = require("tecs.ui") local type UiEvent = uiTypes.Event local button = world:spawn( ui.Style({width = 180, height = 44}), ui.Interaction({tabIndex = 1}), RelativeTransform2D(), ChildOf(content) ) world:observe( button, ui.Event, function(event: UiEvent) if event.kind == "activate" then print("activated via", event.source) end end ) ``` ### Change a retained style Use `getMut` only when a property actually changes. This dirties the retained style once; unchanged frames do not parse the string or recompute the tree: ```teal local style = world:getMut(panel, ui.Style) style.style.width = "100%" style.style.maxWidth = "480px" ``` ### Focus and reveal a control Programmatic focus uses the same state and navigation order as Tab. Focusing a clipped descendant reveals it through every ancestor viewport: ```teal ui.focus(world, button) ui.reveal(world, button, "center") local focused = ui.focused(world) if focused == button then ui.blur(world) end ``` ## Module contents ### Types | Type | Kind | Description | | --- | --- | --- | | [`Event`](/modules/ui/#tecs.ui.Event) | record | Event reports one semantic interaction at an entity address. | | [`EventKind`](/modules/ui/#tecs.ui.EventKind) | enum | Identifies one semantic UI interaction. | | [`FocusScope`](/modules/ui/#tecs.ui.FocusScope) | record | FocusScope marks an entity that may become the active navigation boundary. | | [`Interaction`](/modules/ui/#tecs.ui.Interaction) | record | Interaction makes one computed layout box a UI input target. | | [`InteractionSource`](/modules/ui/#tecs.ui.InteractionSource) | enum | Identifies what initiated a semantic UI event. | | [`InteractionState`](/modules/ui/#tecs.ui.InteractionState) | record | InteractionState reports transient state derived by the UI plugin. | | [`Intrinsic`](/modules/ui/#tecs.ui.Intrinsic) | record | Intrinsic supplies cached leaf metrics to layout without a Lua callback. | | [`IntrinsicSource`](/modules/ui/#tecs.ui.IntrinsicSource) | enum | Selects which leaf component owns natural dimensions. | | [`Layout`](/modules/ui/#tecs.ui.Layout) | record | Layout reports the last box computed by retained layout. | | [`Options`](/modules/ui/#tecs.ui.Options) | record | Options configures rendering, input, scrolling, and the owned clip-index range. | | [`Overrides`](/modules/ui/#tecs.ui.Overrides) | record | Overrides changes scrolling and clip allocation without repeating an application's renderer and input. | | [`Paint`](/modules/ui/#tecs.ui.Paint) | record | Paint controls how a drawing component consumes its layout box. | | [`PointerType`](/modules/ui/#tecs.ui.PointerType) | enum | Identifies the pointer device associated with an event. | | [`RevealAlign`](/modules/ui/#tecs.ui.RevealAlign) | enum | Selects where reveal places a descendant in each viewport. | | [`Root`](/modules/ui/#tecs.ui.Root) | record | Root supplies the available space and coordinate mapping for one tree. | | [`RootSizing`](/modules/ui/#tecs.ui.RootSizing) | enum | Selects where a root receives its available dimensions. | | [`RootSpace`](/modules/ui/#tecs.ui.RootSpace) | enum | Selects whether a root uses screen or world coordinates. | | [`Scroll`](/modules/ui/#tecs.ui.Scroll) | record | Scroll offsets descendants and clips them to this entity's layout box. | | [`ScrollAxis`](/modules/ui/#tecs.ui.ScrollAxis) | enum | Selects the dimension controlled by a scrollbar thumb. | | [`Scrollbar`](/modules/ui/#tecs.ui.Scrollbar) | record | Scrollbar derives one composed thumb transform from an ancestor viewport. | | [`Style`](/modules/ui/#tecs.ui.Style) | record | Style stores the layout properties that place one UI entity. | ### Functions | Function | Kind | Description | | --- | --- | --- | | [`blur`](/modules/ui/#tecs.ui.blur) | Static | Clears the world's focused interaction. | | [`focus`](/modules/ui/#tecs.ui.focus) | Static | Moves focus to one enabled, focusable interaction. | | [`focused`](/modules/ui/#tecs.ui.focused) | Static | Returns the world's focused interaction. | | [`plugin`](/modules/ui/#tecs.ui.plugin) | Static | Returns a plugin that derives layout, transforms, clipping, and optional interaction from retained layout nodes. | | [`popFocusScope`](/modules/ui/#tecs.ui.popFocusScope) | Static | Pops the active navigation boundary and restores its saved focus. | | [`pushFocusScope`](/modules/ui/#tecs.ui.pushFocusScope) | Static | Pushes one modal navigation boundary and remembers the current focus. | | [`reveal`](/modules/ui/#tecs.ui.reveal) | Static | Scrolls every ancestor viewport enough to expose one descendant. | ### Values | Value | Type | Description | | --- | --- | --- | | [`Node`](/modules/ui/#tecs.ui.Node) | [`Component`](/modules/ecs/#tecs.ecs.Component) | Read-only. Exposes the tag that marks an entity as participating in UI layout. | ## Types ### tecs.ui.Event record `Event` reports one semantic interaction at an entity address. Read-only. Exposes the event type delivered to an interaction target and its `ChildOf` ancestors. ```teal record tecs.ui.Event is events.Event kind: EventKind target: integer currentTarget: integer x: number y: number button: integer pointerId: string pointerType: PointerType deltaX: number deltaY: number source: InteractionSource consumed: boolean init: function( event: Event, kind: EventKind, target: integer, x: number, y: number, button: integer, pointerId: string, pointerType: PointerType, deltaX: number, deltaY: number, source: InteractionSource ) end ``` #### Interfaces | Interface | | --- | | [`events.Event`](/modules/events/#tecs.events.Event) | #### Examples Handles pointer and keyboard activation through the same observer. ```teal local tecs = require("tecs") local ui = tecs.ui local type uiTypes = require("tecs.ui") local type UiEvent = uiTypes.Event local world = tecs.ecs.newWorld() local button = world:spawn(ui.Interaction({tabIndex = 1})) world:observe( button, ui.Event, function(event: UiEvent) if event.kind == "activate" then print("activated via", event.source) end end ) ``` #### tecs.ui.Event.kind field Read-only. Reports `"pointerEnter"`, `"pointerLeave"`, `"pointerDown"`, `"pointerMove"`, `"pointerUp"`, `"click"`, `"wheel"`, `"focus"`, `"blur"`, `"activate"`, `"dragStart"`, `"dragMove"`, `"dragEnd"`, or `"dragCancel"`. ```teal tecs.ui.Event.kind: EventKind ``` #### tecs.ui.Event.target field Read-only. Identifies the entity where dispatch began. ```teal tecs.ui.Event.target: integer ``` #### tecs.ui.Event.currentTarget field Read-only. Identifies the entity whose observers are currently receiving this bubbling event. ```teal tecs.ui.Event.currentTarget: integer ``` #### tecs.ui.Event.x field Read-only. Reports the pointer's horizontal window coordinate. ```teal tecs.ui.Event.x: number ``` #### tecs.ui.Event.y field Read-only. Reports the pointer's vertical window coordinate. ```teal tecs.ui.Event.y: number ``` #### tecs.ui.Event.button field Read-only. Reports the mouse button, or zero for touch and events that have no button. ```teal tecs.ui.Event.button: integer ``` #### tecs.ui.Event.pointerId field Read-only. Identifies `"mouse"` or one opaque touch identity. Keyboard, controller, and programmatic events use the empty string. ```teal tecs.ui.Event.pointerId: string ``` #### tecs.ui.Event.pointerType field Read-only. Reports `"mouse"`, `"touch"`, `"pen"`, or `"none"`. ```teal tecs.ui.Event.pointerType: PointerType ``` #### tecs.ui.Event.deltaX field Read-only. Reports horizontal movement for pointer-move, drag, and wheel events, or zero for other events. ```teal tecs.ui.Event.deltaX: number ``` #### tecs.ui.Event.deltaY field Read-only. Reports vertical movement for pointer-move, drag, and wheel events, or zero for other events. ```teal tecs.ui.Event.deltaY: number ``` #### tecs.ui.Event.source field Read-only. Reports `"pointer"`, `"keyboard"`, `"controller"`, or `"programmatic"` as the activation source. ```teal tecs.ui.Event.source: InteractionSource ``` #### tecs.ui.Event.consumed field Caller-writable. An observer may set this field to stop bubbling and suppress the default wheel scroll. ```teal tecs.ui.Event.consumed: boolean ``` #### tecs.ui.Event.init Static ```teal function tecs.ui.Event.init( event: Event, kind: EventKind, target: integer, x: number, y: number, button: integer, pointerId: string, pointerType: PointerType, deltaX: number, deltaY: number, source: InteractionSource ) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `event` | [`Event`](/modules/ui/#tecs.ui.Event) | | | `kind` | [`EventKind`](/modules/ui/#tecs.ui.EventKind) | | | `target` | `integer` | | | `x` | `number` | | | `y` | `number` | | | `button` | `integer` | | | `pointerId` | `string` | | | `pointerType` | [`PointerType`](/modules/ui/#tecs.ui.PointerType) | | | `deltaX` | `number` | | | `deltaY` | `number` | | | `source` | [`InteractionSource`](/modules/ui/#tecs.ui.InteractionSource) | | ##### Returns None. ### tecs.ui.EventKind enum Identifies one semantic UI interaction. ```teal enum tecs.ui.EventKind "activate" "blur" "click" "dragCancel" "dragEnd" "dragMove" "dragStart" "focus" "pointerDown" "pointerEnter" "pointerLeave" "pointerMove" "pointerUp" "wheel" end ``` ### tecs.ui.FocusScope record `FocusScope` marks an entity that may become the active navigation boundary. Read-only. Exposes the marker used by `pushFocusScope`. ```teal record tecs.ui.FocusScope is Component end ``` #### Interfaces | Interface | | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | #### Examples Marks a dialog as a boundary for modal pointer and keyboard navigation. ```teal local tecs = require("tecs") local ui = tecs.ui local world = tecs.ecs.newWorld() local root = world:spawn(ui.Style({width = 1280, height = 720})) local dialog = world:spawn( ui.Style({width = 440, height = 260}), ui.FocusScope(), tecs.ecs.RelativeTransform2D(), tecs.ecs.ChildOf(root) ) ``` ### tecs.ui.Interaction record `Interaction` makes one computed layout box a UI input target. Read-only. Exposes the component that admits a layout box to hit testing and keyboard navigation. ```teal record tecs.ui.Interaction is Component enabled: boolean focusable: boolean tabIndex: integer order: integer draggable: boolean end ``` #### Interfaces | Interface | | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | #### Examples Creates a focusable control with an explicit Tab position. ```teal local tecs = require("tecs") local ui = tecs.ui local world = tecs.ecs.newWorld() local panel = world:spawn(ui.Style({width = 480, height = 360})) local button = world:spawn( ui.Style({width = 180, height = 44}), ui.Interaction({tabIndex = 1, draggable = false}), tecs.ecs.RelativeTransform2D(), tecs.ecs.ChildOf(panel) ) ``` #### tecs.ui.Interaction.enabled field Caller-writable. The caller controls whether hit testing and navigation can select this entity. The default is true. ```teal tecs.ui.Interaction.enabled: boolean ``` #### tecs.ui.Interaction.focusable field Caller-writable. The caller controls whether clicking or keyboard navigation can focus this entity. The default is true. ```teal tecs.ui.Interaction.focusable: boolean ``` #### tecs.ui.Interaction.tabIndex field Caller-writable. The caller orders keyboard focus. Positive values come first in ascending order, zero values follow in authorial order, and negative values skip keyboard navigation. ```teal tecs.ui.Interaction.tabIndex: integer ``` #### tecs.ui.Interaction.order field Caller-writable. The caller supplies an authorial tie-breaker for hit testing and focus when controls share renderer depth and tab index. Higher values hit later; lower values focus first. ```teal tecs.ui.Interaction.order: integer ``` #### tecs.ui.Interaction.draggable field Caller-writable. The caller enables semantic drag events after pointer movement crosses the plugin's drag threshold. ```teal tecs.ui.Interaction.draggable: boolean ``` ### tecs.ui.InteractionSource enum Identifies what initiated a semantic UI event. ```teal enum tecs.ui.InteractionSource "controller" "keyboard" "pointer" "programmatic" end ``` ### tecs.ui.InteractionState record `InteractionState` reports transient state derived by the UI plugin. Read-only. Exposes transient hover, press, and focus state. ```teal record tecs.ui.InteractionState is Component hovered: boolean pressed: boolean focused: boolean dragging: boolean end ``` #### Interfaces | Interface | | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | #### Examples Reads transient focus state without dirtying the retained control. ```teal local tecs = require("tecs") local ui = tecs.ui local world = tecs.ecs.newWorld() local button = world:spawn(ui.Interaction({tabIndex = 1})) local state = world:get(button, ui.InteractionState) if state ~= nil and state.focused then print("button has keyboard focus") end ``` #### tecs.ui.InteractionState.hovered field Engine-owned. The engine reports whether the pointer is over the entity's visible, clipped box. Ordinary game code may read this field to choose its visual state and must not write it. ```teal tecs.ui.InteractionState.hovered: boolean ``` #### tecs.ui.InteractionState.pressed field Engine-owned. The engine reports whether at least one pointer was pressed on the entity and remains captured. Ordinary game code may read this field and must not write it. ```teal tecs.ui.InteractionState.pressed: boolean ``` #### tecs.ui.InteractionState.focused field Engine-owned. The engine reports whether keyboard activation currently targets the entity. Ordinary game code may read this field and must not write it. ```teal tecs.ui.InteractionState.focused: boolean ``` #### tecs.ui.InteractionState.dragging field Engine-owned. The engine reports whether at least one captured pointer is dragging this entity. Ordinary game code may read this field and must not write it. ```teal tecs.ui.InteractionState.dragging: boolean ``` ### tecs.ui.Intrinsic record `Intrinsic` supplies cached leaf metrics to layout without a Lua callback. Read-only. Exposes cached custom, text, or image leaf measurement. ```teal record tecs.ui.Intrinsic is Component source: IntrinsicSource width: number height: number minWidth: number scale: number wrap: boolean end ``` #### Interfaces | Interface | | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | #### Examples Selects natural text, image, or custom leaf measurements. ```teal local tecs = require("tecs") local ui = tecs.ui local wrappedText = ui.Intrinsic("text", {wrap = true}) local imageSize = ui.Intrinsic("image") local customSize = ui.Intrinsic( "custom", {width = 96, height = 28, minWidth = 48} ) ``` #### tecs.ui.Intrinsic.source field Caller-writable. The caller selects `"custom"`, `"text"`, or `"image"`. Text reads `tecs.gfx.text.Text`; image reads `tecs.components.Sprite`; custom reads the dimensions below. ```teal tecs.ui.Intrinsic.source: IntrinsicSource ``` #### tecs.ui.Intrinsic.width field Caller-writable. The caller supplies the preferred custom width in logical units. Text and image sources ignore this field. ```teal tecs.ui.Intrinsic.width: number ``` #### tecs.ui.Intrinsic.height field Caller-writable. The caller supplies the preferred custom height in logical units. Text and image sources ignore this field. ```teal tecs.ui.Intrinsic.height: number ``` #### tecs.ui.Intrinsic.minWidth field Caller-writable. The caller supplies the custom minimum-content width. Zero uses `width`. Text derives its longest unbroken run and image sources preserve their aspect ratio instead. ```teal tecs.ui.Intrinsic.minWidth: number ``` #### tecs.ui.Intrinsic.scale field Caller-writable. The caller scales text or source-image units into UI units. The default is one. ```teal tecs.ui.Intrinsic.scale: number ``` #### tecs.ui.Intrinsic.wrap field Caller-writable. The caller lets Taffy choose a text line width and lets the plugin write that result to `Text.wrapWidth`. Other sources ignore this field. ```teal tecs.ui.Intrinsic.wrap: boolean ``` ### tecs.ui.IntrinsicSource enum Selects which leaf component owns natural dimensions. ```teal enum tecs.ui.IntrinsicSource "custom" "image" "text" end ``` ### tecs.ui.Layout record `Layout` reports the last box computed by retained layout. Read-only. Exposes the component that reports a computed box without becoming a drawing primitive. ```teal record tecs.ui.Layout is Component x: number y: number width: number height: number end ``` #### Interfaces | Interface | | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | #### Examples Reads the final logical size after the UI layout phase. ```teal local tecs = require("tecs") local ui = tecs.ui local world = tecs.ecs.newWorld() local panel = world:spawn(ui.Style({width = 480, height = 320})) local box = world:get(panel, ui.Layout) if box ~= nil then print(("%.0f x %.0f"):format(box.width, box.height)) end ``` #### tecs.ui.Layout.x field Read-only. The engine reports the box's X offset from its parent. ```teal tecs.ui.Layout.x: number ``` #### tecs.ui.Layout.y field Read-only. The engine reports the box's Y offset from its parent. ```teal tecs.ui.Layout.y: number ``` #### tecs.ui.Layout.width field Read-only. The engine reports the box width in logical units. ```teal tecs.ui.Layout.width: number ``` #### tecs.ui.Layout.height field Read-only. The engine reports the box height in logical units. ```teal tecs.ui.Layout.height: number ``` ### tecs.ui.Options record `Options` configures rendering, input, scrolling, and the owned clip-index range. ```teal record tecs.ui.Options renderer: Renderer firstClip: integer lastClip: integer input: any layer: any wheelStep: number dragThreshold: number end ``` #### tecs.ui.Options.renderer field Caller-writable. The caller supplies the renderer whose camera and GPU clip table the UI uses. ```teal tecs.ui.Options.renderer: Renderer ``` #### tecs.ui.Options.firstClip field Caller-writable. The caller reserves the first UI-owned clip index. The default is one. ```teal tecs.ui.Options.firstClip: integer ``` #### tecs.ui.Options.lastClip field Caller-writable. The caller reserves the last UI-owned clip index. The default is 255. ```teal tecs.ui.Options.lastClip: integer ``` #### tecs.ui.Options.input field Caller-writable. The caller supplies the application's folded input to enable hit testing, scrolling, focus, and activation. Omitting it installs layout and clipping only. ```teal tecs.ui.Options.input: any ``` #### tecs.ui.Options.layer field Caller-writable. The caller supplies the input layer the UI may read. Omitting it reads the base layer. ```teal tecs.ui.Options.layer: any ``` #### tecs.ui.Options.wheelStep field Caller-writable. The caller sets logical scroll units per wheel unit. The default is 40. ```teal tecs.ui.Options.wheelStep: number ``` #### tecs.ui.Options.dragThreshold field Caller-writable. The caller sets pointer movement in logical units before a draggable capture emits `"dragStart"`. The default is four. ```teal tecs.ui.Options.dragThreshold: number ``` ### tecs.ui.Overrides record `Overrides` changes scrolling and clip allocation without repeating an application's renderer and input. ```teal record tecs.ui.Overrides firstClip: integer lastClip: integer layer: any wheelStep: number dragThreshold: number end ``` #### tecs.ui.Overrides.firstClip field Caller-writable. The caller reserves the first UI-owned clip index. The default is one. ```teal tecs.ui.Overrides.firstClip: integer ``` #### tecs.ui.Overrides.lastClip field Caller-writable. The caller reserves the last UI-owned clip index. The default is 255. ```teal tecs.ui.Overrides.lastClip: integer ``` #### tecs.ui.Overrides.layer field Caller-writable. The caller supplies the input layer the UI may read. Omitting it reads the base layer. ```teal tecs.ui.Overrides.layer: any ``` #### tecs.ui.Overrides.wheelStep field Caller-writable. The caller sets logical scroll units per wheel unit. The default is 40. ```teal tecs.ui.Overrides.wheelStep: number ``` #### tecs.ui.Overrides.dragThreshold field Caller-writable. The caller sets pointer movement in logical units before a draggable capture emits `"dragStart"`. The default is four. ```teal tecs.ui.Overrides.dragThreshold: number ``` ### tecs.ui.Paint record `Paint` controls how a drawing component consumes its layout box. Read-only. Exposes the component that optionally stretches an existing primitive to its computed box. ```teal record tecs.ui.Paint is Component stretch: boolean end ``` #### Interfaces | Interface | | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | #### Examples Stretches an ordinary rounded rectangle over its retained layout box. ```teal local tecs = require("tecs") local ui = tecs.ui local world = tecs.ecs.newWorld() local panel = world:spawn(ui.Style({width = 480, height = 320})) world:spawn( ui.Style({position = "absolute", inset = 0}), ui.Paint(true), tecs.ecs.RelativeTransform2D(0, 0, 1), tecs.gfx.Tint(0.1, 0.2, 0.3, 1), tecs.gfx.Material(tecs.gfx.materials.id("rounded"), 0.1), tecs.gfx.Renderable2D(), tecs.ecs.ChildOf(panel) ) ``` #### tecs.ui.Paint.stretch field Caller-writable. The caller enables `stretch` to copy the layout width and height into `RelativeTransform2D.scaleX` and `scaleY`. ```teal tecs.ui.Paint.stretch: boolean ``` ### tecs.ui.PointerType enum Identifies the pointer device associated with an event. ```teal enum tecs.ui.PointerType "mouse" "none" "pen" "touch" end ``` ### tecs.ui.RevealAlign enum Selects where `reveal` places a descendant in each viewport. ```teal enum tecs.ui.RevealAlign "center" "end" "nearest" "start" end ``` ### tecs.ui.Root record `Root` supplies the available space and coordinate mapping for one tree. Read-only. Exposes the component that gives a retained UI tree its available size and coordinate space. ```teal record tecs.ui.Root is Component space: RootSpace width: number height: number pixelDensity: number sizing: RootSizing end ``` #### Interfaces | Interface | | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | #### Examples Creates a screen root that follows the application window automatically. ```teal local tecs = require("tecs") local ui = tecs.ui local world = tecs.ecs.newWorld() local root = world:spawn( ui.Style({width = "100%", height = "100%"}), ui.Root("screen"), tecs.Transform2D(0, 0, 0, 16) ) ``` #### tecs.ui.Root.space field Caller-writable. The caller chooses `"screen"` for logical screen coordinates or `"world"` for coordinates projected by the camera. ```teal tecs.ui.Root.space: RootSpace ``` #### tecs.ui.Root.width field Caller-writable. The caller supplies the layout root width. An application-backed screen root with automatic sizing follows the logical window width; a camera-sized world root derives world units from the physical viewport and camera zoom. ```teal tecs.ui.Root.width: number ``` #### tecs.ui.Root.height field Caller-writable. The caller supplies the layout root height. An application-backed screen root with automatic sizing follows the logical window height; a camera-sized world root derives world units from the physical viewport and camera zoom. ```teal tecs.ui.Root.height: number ``` #### tecs.ui.Root.pixelDensity field Caller-writable. The caller supplies target pixels per logical point. The default is one. Automatic screen and camera-sized world roots follow the window's current pixel density. ```teal tecs.ui.Root.pixelDensity: number ``` #### tecs.ui.Root.sizing field Caller-writable. The caller selects `"auto"` for the standard behavior, `"manual"` to preserve every authored field, or `"camera"` on a world root to derive its extent and transform from the active camera. `"auto"` follows the window for screen roots and is manual for world roots. ```teal tecs.ui.Root.sizing: RootSizing ``` ### tecs.ui.RootSizing enum Selects where a root receives its available dimensions. ```teal enum tecs.ui.RootSizing "auto" "camera" "manual" end ``` ### tecs.ui.RootSpace enum Selects whether a root uses screen or world coordinates. ```teal enum tecs.ui.RootSpace "screen" "world" end ``` ### tecs.ui.Scroll record `Scroll` offsets descendants and clips them to this entity's layout box. Read-only. Exposes the component that offsets descendants and clips their existing GPU instances. ```teal record tecs.ui.Scroll is Component x: number y: number contentWidth: number contentHeight: number contentX: number contentY: number end ``` #### Interfaces | Interface | | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | #### Examples Creates a fixed viewport and changes its retained vertical offset. ```teal local tecs = require("tecs") local ui = tecs.ui local world = tecs.ecs.newWorld() local panel = world:spawn(ui.Style({width = 480, height = 360})) local viewport = world:spawn( ui.Style({width = "100%", height = 240}), ui.Scroll(), tecs.ecs.RelativeTransform2D(), tecs.ecs.ChildOf(panel) ) local scroll = world:getMut(viewport, ui.Scroll) scroll.y = scroll.y + 80 ``` #### tecs.ui.Scroll.x field Caller-writable. The caller supplies the horizontal scroll offset in logical units. ```teal tecs.ui.Scroll.x: number ``` #### tecs.ui.Scroll.y field Caller-writable. The caller supplies the vertical scroll offset in logical units. ```teal tecs.ui.Scroll.y: number ``` #### tecs.ui.Scroll.contentWidth field Engine-owned. The engine reports the horizontal extent of retained content, including nested and absolute descendants. Ordinary game code should ignore this field. ```teal tecs.ui.Scroll.contentWidth: number ``` #### tecs.ui.Scroll.contentHeight field Engine-owned. The engine reports the vertical extent of retained content, including nested and absolute descendants. Ordinary game code should ignore this field. ```teal tecs.ui.Scroll.contentHeight: number ``` #### tecs.ui.Scroll.contentX field Engine-owned. The engine reports the smallest horizontal content coordinate, including negative-positioned descendants. Ordinary game code should ignore this field. ```teal tecs.ui.Scroll.contentX: number ``` #### tecs.ui.Scroll.contentY field Engine-owned. The engine reports the smallest vertical content coordinate, including negative-positioned descendants. Ordinary game code should ignore this field. ```teal tecs.ui.Scroll.contentY: number ``` ### tecs.ui.ScrollAxis enum Selects the dimension controlled by a scrollbar thumb. ```teal enum tecs.ui.ScrollAxis "horizontal" "vertical" end ``` ### tecs.ui.Scrollbar record `Scrollbar` derives one composed thumb transform from an ancestor viewport. Read-only. Exposes a composed scrollbar thumb driven by its parent viewport. ```teal record tecs.ui.Scrollbar is Component axis: ScrollAxis thickness: number inset: number minLength: number end ``` #### Interfaces | Interface | | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | #### Examples Composes a square vertical thumb from ordinary renderer components. ```teal local tecs = require("tecs") local ui = tecs.ui local world = tecs.ecs.newWorld() local viewport = world:spawn( ui.Style({width = 480, height = 240}), ui.Scroll() ) world:spawn( ui.Scrollbar("vertical", 12, 4, 28), ui.Interaction({focusable = false, draggable = true}), tecs.ecs.RelativeTransform2D(0, 0, 10), tecs.gfx.Tint(0.32, 0.82, 1, 1), tecs.gfx.Material(tecs.gfx.materials.id("rounded"), 0), tecs.gfx.Renderable2D(), tecs.ecs.ChildOf(viewport) ) ``` #### tecs.ui.Scrollbar.axis field Caller-writable. The caller selects `"horizontal"` or `"vertical"`. ```teal tecs.ui.Scrollbar.axis: ScrollAxis ``` #### tecs.ui.Scrollbar.thickness field Caller-writable. The caller sets the thumb thickness in logical units. ```teal tecs.ui.Scrollbar.thickness: number ``` #### tecs.ui.Scrollbar.inset field Caller-writable. The caller leaves this much room inside each end of the viewport track. ```teal tecs.ui.Scrollbar.inset: number ``` #### tecs.ui.Scrollbar.minLength field Caller-writable. The caller sets the smallest thumb length in logical units. ```teal tecs.ui.Scrollbar.minLength: number ``` ### tecs.ui.Style record `Style` stores the layout properties that place one UI entity. Read-only. Exposes the component that supplies retained layout properties and adds `Layout` and `tecs.gfx.Clip`. ```teal record tecs.ui.Style is Component style: {string: any} end ``` #### Interfaces | Interface | | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | #### Examples Builds a column whose width follows its parent up to 480 logical pixels. ```teal local tecs = require("tecs") local ui = tecs.ui local world = tecs.ecs.newWorld() local root = world:spawn(ui.Style({width = 1280, height = 720})) local panel = world:spawn( ui.Style({ width = "100%", maxWidth = 480, flexDirection = "column", padding = 20, gap = 12, }), tecs.ecs.RelativeTransform2D(), tecs.ecs.ChildOf(root) ) ``` #### tecs.ui.Style.style field Caller-writable. The caller supplies a table and marks `Style` dirty after mutating it. Numbers and `"24px"` strings use logical UI pixels, `"50%"` is parent-relative, and `"auto"` selects automatic sizing. Invalid strings raise when the plugin synchronizes the style. The older `{value=number, unit="percent"}` and `unit="points"` table forms remain supported. Supported keys are `display`, `position`, `flexDirection`, `flexWrap`, `justifyContent`, `alignItems`, `alignContent`, `flexGrow`, `flexShrink`, `flexBasis`, `width`, `height`, `minWidth`, `minHeight`, `maxWidth`, `maxHeight`, `margin`, `padding`, `border`, `gap`, `rowGap`, `inset`, and the Tecs-only integer `order` used to retain authorial sibling order. Edge values accept one dimension or a table with `left`, `right`, `top`, and `bottom`. ```teal tecs.ui.Style.style: {string: any} ``` ## Functions ### tecs.ui.blur Static Clears the world's focused interaction. ```teal function tecs.ui.blur(world: World): boolean ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`World`](/modules/ecs/#tecs.World) | The caller supplies a world with the UI plugin. | #### Returns | Type | Description | | --- | --- | | `boolean` | Returns false when the plugin is missing. | #### Examples Clears the current keyboard focus when one exists. ```teal local tecs = require("tecs") local ui = tecs.ui local world = tecs.ecs.newWorld() if ui.focused(world) ~= nil then ui.blur(world) end ``` ### tecs.ui.focus Static Moves focus to one enabled, focusable interaction. ```teal function tecs.ui.focus( world: World, entity: integer ): boolean ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`World`](/modules/ecs/#tecs.World) | The caller supplies a world with the UI plugin. | | `entity` | `integer` | The caller supplies the interaction to focus. | #### Returns | Type | Description | | --- | --- | | `boolean` | Returns false when the plugin is missing or the entity cannot receive focus. | #### Examples Moves keyboard focus to an enabled, focusable control. ```teal local tecs = require("tecs") local ui = tecs.ui local world = tecs.ecs.newWorld() local saveButton = world:spawn(ui.Interaction({tabIndex = 1})) if not ui.focus(world, saveButton) then print("save button cannot receive focus") end ``` ### tecs.ui.focused Static Returns the world's focused interaction. ```teal function tecs.ui.focused(world: World): integer ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`World`](/modules/ecs/#tecs.World) | The caller supplies a world with or without the UI plugin. | #### Returns | Type | Description | | --- | --- | | `integer` | Returns the focused entity, or nil when nothing is focused. | #### Examples Finds the interaction that currently owns keyboard focus. ```teal local tecs = require("tecs") local ui = tecs.ui local world = tecs.ecs.newWorld() local current = ui.focused(world) if current ~= nil then print("focused entity", current) end ``` ### tecs.ui.plugin Static Returns a plugin that derives layout, transforms, clipping, and optional interaction from retained layout nodes. The plugin owns every renderer clip index from `firstClip` through `lastClip`, inclusive. Reserve a smaller range when another system also calls `renderer.sprites:setClipRegion`. Exhausting the range raises rather than drawing unclipped content. Passing an [`Application`](/modules/Application/) supplies its renderer, input, and window. The plugin samples the viewport initially, then observes platform resize, pixel-size, display, and display-scale events at world address zero. It coalesces them and refreshes automatic roots before layout without polling the window on unchanged frames. The optional second argument overrides clip allocation, the input layer, and wheel distance. Passing `Options` directly preserves manual wiring for tests, tools, and worlds not owned by an application. When input is present, the plugin reads its configured layer in `PreUpdate`. It uses the hit geometry derived after the previous frame's layout and clipping, which is the geometry the player was shown when the input arrived. ```teal function tecs.ui.plugin( source: any, overrides: Overrides ): function(World) ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `source` | `any` | The caller supplies an application or manually wired options containing a renderer. | | `overrides` | [`Overrides`](/modules/ui/#tecs.ui.Overrides) | The caller may override source defaults without repeating its renderer and input. | #### Returns | Type | Description | | --- | --- | | `function(`[`World`](/modules/ecs/#tecs.World)`)` | Returns a plugin for `world:addPlugin`. Each world receives a separate retained tree and clip allocator. | #### Examples Installs UI with explicit dependencies in a test or manually constructed world. ```teal local tecs = require("tecs") local type Renderer = require("tecs.Renderer") local type inputTypes = require("tecs.input") local function installManualUi( world: tecs.World, renderer: Renderer, input: inputTypes.Input ) world:addPlugin(tecs.ui.plugin({ renderer = renderer, input = input, firstClip = 32, lastClip = 63, })) end ``` Installs UI from an application so roots follow its window and input. ```teal local tecs = require("tecs") local function gamePlugin(world: tecs.World, app: tecs.Application) world:addPlugin(tecs.ui.plugin( app, { wheelStep = 36, dragThreshold = 4, } )) end ``` ### tecs.ui.popFocusScope Static Pops the active navigation boundary and restores its saved focus. ```teal function tecs.ui.popFocusScope(world: World, scope: integer): boolean ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`World`](/modules/ecs/#tecs.World) | The caller supplies a world with the UI plugin. | | `scope` | `integer` | The caller may require a particular active scope. | #### Returns | Type | Description | | --- | --- | | `boolean` | Returns false when the plugin is missing or the requested scope is not active. | #### Examples Closes the active modal boundary and restores its saved focus. ```teal local tecs = require("tecs") local ui = tecs.ui local world = tecs.ecs.newWorld() local dialog = world:spawn(ui.FocusScope()) if not ui.popFocusScope(world, dialog) then print("dialog is not the active focus scope") end ``` ### tecs.ui.pushFocusScope Static Pushes one modal navigation boundary and remembers the current focus. ```teal function tecs.ui.pushFocusScope(world: World, scope: integer): boolean ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`World`](/modules/ecs/#tecs.World) | The caller supplies a world with the UI plugin. | | `scope` | `integer` | The caller supplies an entity carrying `FocusScope`. | #### Returns | Type | Description | | --- | --- | | `boolean` | Returns false when the plugin or scope is missing. | #### Examples Activates a modal scope and focuses its first control. ```teal local tecs = require("tecs") local ui = tecs.ui local world = tecs.ecs.newWorld() local dialog = world:spawn(ui.FocusScope()) local firstDialogControl = world:spawn( ui.Interaction({tabIndex = 1}), tecs.ecs.ChildOf(dialog) ) if ui.pushFocusScope(world, dialog) then ui.focus(world, firstDialogControl) end ``` ### tecs.ui.reveal Static Scrolls every ancestor viewport enough to expose one descendant. ```teal function tecs.ui.reveal( world: World, entity: integer, align: RevealAlign ): boolean ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`World`](/modules/ecs/#tecs.World) | The caller supplies a world with the UI plugin. | | `entity` | `integer` | The caller supplies a retained UI descendant. | | `align` | [`RevealAlign`](/modules/ui/#tecs.ui.RevealAlign) | The caller selects `"nearest"`, `"start"`, `"center"`, or `"end"`; omission selects `"nearest"`. | #### Returns | Type | Description | | --- | --- | | `boolean` | Returns false when the plugin or layout is missing. | #### Examples Centers a selected row through every retained ancestor viewport. ```teal local tecs = require("tecs") local ui = tecs.ui local world = tecs.ecs.newWorld() local viewport = world:spawn( ui.Style({height = 240}), ui.Scroll() ) local selectedRow = world:spawn( ui.Style({height = 44}), tecs.ecs.RelativeTransform2D(), tecs.ecs.ChildOf(viewport) ) if not ui.reveal(world, selectedRow, "center") then print("selected row has no retained layout") end ``` ## Values ### tecs.ui.Node variable Read-only. Exposes the tag that marks an entity as participating in UI layout. `Style`, `Root`, and `Scroll` add it automatically. ```teal tecs.ui.Node: Component ``` --- ## tecs.workers # tecs.workers Worker threads and channels. A worker is source text and two queues. It shares nothing with the state that spawned it, so it reaches its own ends through `workers.current`, and `spawn` pairs with `stop`: ```teal local worker = tecs.workers.spawn({ source = [[ local tecs = require("tecs") local self = tecs.workers.current() while true do local job = self:receive() if job == nil then break end self:send({name = job.name, hash = tecs.data.fnv1a64(job.bytes)}) end ]], }) worker:send({name = "level1", bytes = "..."}) local answer = worker:receive(1000) worker:stop() ``` A worker that answers requests rather than streaming results is a call server, and `Worker:call` reads as an ordinary function call on this side while `Self:serve` runs the loop on the other: ```teal local worker = tecs.workers.spawn({ source = [[ local tecs = require("tecs") tecs.workers.current():serve(function(job) return {name = job.name, hash = tecs.data.fnv1a64(job.bytes)} end) ]], }) local answer = worker:call({name = "level1", bytes = "..."}) worker:stop() ``` A channel is a stream, so `call` numbers each request and takes the reply that carries the same number. Messages the worker sends outside a call stay queued for `receive`, and a reply nobody awaits any more is discarded. The worker's source writes a require line because it is a separate state with a separate global table, which is the one place in a game that does. This is the only sanctioned way to run work off the main thread. Raw thread creation is deliberately not exposed: a LuaJIT FFI callback invoked from a thread the VM did not create is unsafe, and a thread entry point written in Lua is exactly that mistake. LuaJIT has no shared mutable heap across threads, so a worker cannot see the spawning state's objects at all. Values cross as serialized bytes, encoded here with `string.buffer`. What can cross is therefore what `string.buffer` can encode: numbers, strings, booleans, and tables of those. Not functions, not userdata, and not cdata pointers into another state's heap. ## Waiting on each side The two `receive` calls have opposite defaults, and the asymmetry is deliberate rather than an oversight. `Worker:receive` runs on the thread SDL drives, so it polls by default and never blocks that thread when asked to wait. A timeout suspends the calling system cooperatively and resumes it where it left off; outside a system it blocks the caller while the worker makes progress, which is what startup, shutdown, and headless tools want. `Self:receive`, the worker side, waits by default. It runs on the worker's own thread, where blocking costs nothing a frame can see, and an idle worker that polled instead would spin a core. A cooperative wait is served by the runtime pump, which runs once per frame, so a result costs up to one extra frame against the arrival that produced it. That is the same trade every other Tecs producer makes, and it buys a frame that keeps rendering while the worker computes. `Worker:call` pays the same frame, and outside a system it blocks its caller and pays none of it. ## Several calls at once One wait parks the whole logical update, so two calls written in a row cost the sum of both. `tecs.batch` runs them at the same time and returns their results in the order the callbacks were given, whatever order the replies arrive in: ```teal local answers = tecs.batch({ function(): any return hashers[1]:call({name = "level1", bytes = first}) end, function(): any return hashers[2]:call({name = "level2", bytes = second}) end, }) ``` One worker serves its own requests one at a time, so overlapping the waits buys time when the calls go to different workers and buys none when they do not. ## Polling and shutdown Closing the inbox wakes the worker and returns nil after queued messages drain. The worker closes its outbox when its source ends, so a spawner waiting on a result learns that no result is coming rather than waiting forever. `stop` closes the inbox, joins the thread, and releases both channels. Worker source must leave its receive loop when the inbox closes or shutdown can wait indefinitely. `stop` also releases every suspended `Worker:receive` on that worker with nil, so shutting a worker down never strands a system. Set `TECS_TRACEPROF` to print a worker's trace aborts when its inbox closes. Repeated `failed to allocate mcode memory` messages identify LuaJIT machine-code allocation failures. ## Module contents ### Constructors | Constructor | Description | | --- | --- | | [`newChannel`](/modules/workers/#tecs.workers.newChannel) | Creates an independent channel. | ### Types | Type | Kind | Description | | --- | --- | --- | | [`Channel`](/modules/workers/#tecs.workers.Channel) | record | Carries serialized messages between two states. | | [`Self`](/modules/workers/#tecs.workers.Self) | record | Contains the worker's two channel endpoints returned by workers.current. | | [`SpawnOptions`](/modules/workers/#tecs.workers.SpawnOptions) | record | Options for workers.spawn. | | [`Worker`](/modules/workers/#tecs.workers.Worker) | record | Represents a thread with its own Lua state and two message channels. | ### Functions | Function | Kind | Description | | --- | --- | --- | | [`current`](/modules/workers/#tecs.workers.current) | Static | Returns the channels for the current worker. | | [`parked`](/modules/workers/#tecs.workers.parked) | Static | Reports how many Worker:receive and Worker:call waits are suspended. | | [`spawn`](/modules/workers/#tecs.workers.spawn) | Static | Starts a worker running options.source on its own thread and state. | ### Values | Value | Type | Description | | --- | --- | --- | | [`path`](/modules/workers/#tecs.workers.path) | `string` | Read-only. Contains the absolute path of the loaded native library. | ## Constructors ### tecs.workers.newChannel Static Creates an independent channel. ```teal function tecs.workers.newChannel(): Channel ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | [`Channel`](/modules/workers/#tecs.workers.Channel) | An owning handle. `destroy` releases the native queue, so exactly one state should hold it. Raises when the queue cannot be created. | ## Types ### tecs.workers.Channel record Carries serialized messages between two states. The channel copies every value across and never shares it, so the receiver gets a separate object and mutating either one is invisible to the other. A `nil` is encodable and does cross, but `receive` also answers nil for "nothing here", so nil is not usable as a message a reader can recognize. Read-only. Exposes the `Channel` type. ```teal record tecs.workers.Channel is Closeable handle: loader.CPtr wrap: function(handle: loader.CPtr): Channel count: function(self): integer destroy: function(self) isClosed: function(self): boolean receive: function(self, timeoutMs: number): any send: function(self, value: any) end ``` #### Interfaces | Interface | | --- | | `Closeable` | #### tecs.workers.Channel.handle field Engine-owned. Stores the native queue pointer until `destroy` runs. Ordinary game code should ignore this field. ```teal tecs.workers.Channel.handle: loader.CPtr ``` #### tecs.workers.Channel.wrap Static Wraps a channel pointer handed over by the native side. ```teal function tecs.workers.Channel.wrap(handle: loader.CPtr): Channel ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `handle` | `loader.CPtr` | The native side keeps this pointer alive for the wrapper's lifetime. | ##### Returns | Type | Description | | --- | --- | | [`Channel`](/modules/workers/#tecs.workers.Channel) | A borrowing handle. `destroy` does nothing, so the state that created the queue remains responsible for releasing it. | #### tecs.workers.Channel:count Instance ```teal function tecs.workers.Channel.count(self): integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Channel` | | ##### Returns | Type | Description | | --- | --- | | `integer` | | #### tecs.workers.Channel:destroy Instance ```teal function tecs.workers.Channel.destroy(self) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Channel` | | ##### Returns None. #### tecs.workers.Channel:isClosed Instance Reports whether the channel is closed. Closed and empty is the one state that will never produce another value, which is what separates a reader that should give up from one that should wait. Queued messages still arrive after a close, so a closed channel that still has a count is not yet finished. ```teal function tecs.workers.Channel.isClosed(self): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Channel` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | True once `close` has run on either side, whether or not messages remain queued. A destroyed channel also reports true. | #### tecs.workers.Channel:receive Instance Takes the next value, or nil if none arrived. A zero timeout polls, a negative timeout waits indefinitely, and any other value waits that many milliseconds. ```teal function tecs.workers.Channel.receive(self, timeoutMs: number): any ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Channel` | | | `timeoutMs` | `number` | The timeout uses milliseconds. Omit it to poll, which is the opposite of the worker-side `receive`, where omission waits. | ##### Returns | Type | Description | | --- | --- | | `any` | The next value, or nil. Nil covers an empty queue, a timeout, and a closed and drained channel. A sent nil also decodes to nil, so callers cannot distinguish those cases and should not send nil. | #### tecs.workers.Channel:send Instance Serializes `value` and queues it without blocking. Each direction accepts at most 1024 messages and 256 MiB of serialized bytes. The call raises when either bound is full; it never blocks the sending thread. ```teal function tecs.workers.Channel.send(self, value: any) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Channel` | | | `value` | `any` | The channel copies this value before returning. A function, userdata, cdata, a table nested past 32 levels, or a table with a key of any of those raises and names the path to it. | ##### Returns None. ### tecs.workers.Self record Contains the worker's two channel endpoints returned by `workers.current`. ```teal global record tecs.workers.Self receive: function(self, timeoutMs: number): any send: function(self, value: any) serve: function(self, handler: function(any): any) end ``` #### tecs.workers.Self:receive Instance Takes the next task, or nil when the inbox closes. ```teal function tecs.workers.Self.receive(self, timeoutMs: number): any ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Self` | | | `timeoutMs` | `number` | Milliseconds. Omitted waits indefinitely, which is the opposite of `Channel:receive`; zero polls. Only the waiting form's nil means the spawner has asked this worker to stop, since a poll answers nil for a merely empty queue. | ##### Returns | Type | Description | | --- | --- | | `any` | The next task, decoded into this state's own tables, so it shares nothing with what the spawner sent and mutating it is safe. Nil is the stop signal or an empty queue, on the terms above. | #### tecs.workers.Self:send Instance Returns a result to the spawner. ```teal function tecs.workers.Self.send(self, value: any) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Self` | | | `value` | `any` | Subject to the same item and byte limits as `Channel:send`, and raises the same way. | ##### Returns None. #### tecs.workers.Self:serve Instance Answers `Worker:call` requests until the inbox closes. This is the worker side of a call: it reads a request, runs `handler`, and sends what the handler returned back under the identifier the request arrived with, which is what lets the spawner match a reply to the call that is waiting for it. Every message reaches `handler`, so a worker that also receives ordinary `Worker:send` messages serves both from one loop. A message that is not a call request takes no reply, and the handler's return value for one is discarded; answer those with `send` instead. A handler that raises does not end the worker. Its reason crosses back and `Worker:call` raises it at the call site, and the loop reads the next request. ```teal local tecs = require("tecs") tecs.workers.current():serve(function(job: any): any return {hash = tecs.data.fnv1a64(job.bytes)} end) ``` ```teal function tecs.workers.Self.serve(self, handler: function(any): any) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Self` | | | `handler` | `function(any): any` | The worker runs this once per message and sends its first return value back as the reply. A return value that cannot be serialized reaches the caller as a failure rather than raising on this thread. Nil raises. | ##### Returns None. ### tecs.workers.SpawnOptions record Options for `workers.spawn`. ```teal global record tecs.workers.SpawnOptions source: string luaPath: string end ``` #### tecs.workers.SpawnOptions.source field Caller-writable. Sets the Lua source the worker runs. It reaches its channels through `workers.current()`. Required; nil raises. Source text, not a path, and it runs in a state that shares nothing with this one, so it cannot close over anything here. ```teal tecs.workers.SpawnOptions.source: string ``` #### tecs.workers.SpawnOptions.luaPath field Caller-writable. Sets `package.path` for the worker's state. Defaults to this state's, so a worker resolves the same modules the spawner does. ```teal tecs.workers.SpawnOptions.luaPath: string ``` ### tecs.workers.Worker record Represents a thread with its own Lua state and two message channels. The worker shares no globals, upvalues or loaded modules with the spawning state. The worker reruns its own requires against `luaPath`. Read-only. Exposes the `Worker` type. ```teal record tecs.workers.Worker pending: integer available: function(self): integer call: function(self, value: any): any receive: function(self, timeoutMs: number): any send: function(self, value: any) stop: function(self): integer end ``` #### tecs.workers.Worker.pending field Read-only. Reports the messages the worker had not taken after the last `send`. The value does not refresh between sends. ```teal tecs.workers.Worker.pending: integer ``` #### tecs.workers.Worker:available Instance ```teal function tecs.workers.Worker.available(self): integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Worker` | | ##### Returns | Type | Description | | --- | --- | | `integer` | | #### tecs.workers.Worker:call Instance Sends a request, waits for its answer, and returns it. The call reads as an ordinary function call: the worker runs the request and the reply arrives at this line. A channel is a stream, so each call carries an identifier and takes only the reply that carries the same one. Messages the worker sends outside a call stay queued for `receive` and are never consumed here. A wait suspends the calling system cooperatively and resumes it at this call, so other systems, rendering, and input keep running; outside a system the call blocks its own caller instead. A suspended call is served by the runtime pump, which runs once per frame, so a reply can arrive up to one frame after the worker sent it. Several calls placed in `tecs.batch` run at the same time and their results come back in argument order. Both halves cross as serialized bytes, exactly as `send` does, so a request and a reply carry numbers, strings, booleans, and tables of those. A live handle, a socket, a file, or cdata cannot cross, and a worker that returns one has its call fail rather than its thread. The worker answers with [`Self:serve`](/modules/workers/#tecs.workers.Self), which is the loop that reads a request and sends its handler's result back under the same identifier. ```teal function tecs.workers.Worker.call(self, value: any): any ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Worker` | | | `value` | `any` | The request the worker receives, under the same limits and errors as `Worker:send`. | ##### Returns | Type | Description | | --- | --- | | `any` | The value the worker's handler returned for this request. Raises the worker's own reason when its handler failed, and raises when the worker ended, was stopped, or had already been stopped, because no reply can follow any of those. | #### tecs.workers.Worker:receive Instance Takes a result, waiting for one when asked. A ready result returns inline. A wait suspends the calling system cooperatively and resumes it at this call, so other systems, rendering, and input keep running; outside a system the call blocks its caller instead. Never blocks the SDL thread from inside a system. A suspended wait is served by the runtime pump, which runs once per frame, so a result can arrive up to one frame after the worker sent it. ```teal function tecs.workers.Worker.receive(self, timeoutMs: number): any ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Worker` | | | `timeoutMs` | `number` | The timeout uses milliseconds. Omit it or pass zero to poll and return immediately, which is the opposite of the worker-side `receive`, where omission waits. A negative value waits until a result arrives or the worker ends, and any other value waits at most that long. | ##### Returns | Type | Description | | --- | --- | | `any` | The next result, or nil. Nil covers an empty queue, an expired timeout, and a worker that has ended without sending one. A stopped worker answers nil without waiting. | #### tecs.workers.Worker:send Instance Queues a value for the worker and refreshes `pending`. ```teal function tecs.workers.Worker.send(self, value: any) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Worker` | | | `value` | `any` | The worker applies the same limits and errors as `Channel:send`. | ##### Returns None. #### tecs.workers.Worker:stop Instance ```teal function tecs.workers.Worker.stop(self): integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Worker` | | ##### Returns | Type | Description | | --- | --- | | `integer` | | ## Functions ### tecs.workers.current Static Returns the channels for the current worker. Only valid in a worker's state, where the native entry point installed the pointers as globals before running the source. ```teal function tecs.workers.current(): Self ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | [`Self`](/modules/workers/#tecs.workers.Self) | A fresh object each call, borrowing channels the spawner owns. Call it once and keep the result: a second call makes a second pair of wrappers over the same two queues, and under `TECS_TRACEPROF` a second trace session, which raises. | ### tecs.workers.parked Static Reports how many `Worker:receive` and `Worker:call` waits are suspended. A suspended receiver is a system parked on a worker result or a call reply. The count exists for tests and debug tooling that need to see the wait rather than infer it; ordinary game code has no use for it. ```teal function tecs.workers.parked(): integer ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | `integer` | The number of receivers waiting across every worker. Zero once they have all resumed, been canceled, or had their worker stopped. | ### tecs.workers.spawn Static Starts a worker running `options.source` on its own thread and state. Returns as soon as the thread starts, not when the source has run, so a failure inside the source surfaces as `stop`'s status rather than here. ```teal function tecs.workers.spawn(options: SpawnOptions): Worker ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `options` | [`SpawnOptions`](/modules/workers/#tecs.workers.SpawnOptions) | | #### Returns | Type | Description | | --- | --- | | [`Worker`](/modules/workers/#tecs.workers.Worker) | A worker the caller must stop. Collection does not join the thread or release its channels. | ## Values ### tecs.workers.path variable Read-only. Contains the absolute path of the loaded native library. ```teal tecs.workers.path: string ``` --- ## Building interfaces # Building interfaces Tecs provides retained layout without introducing a DOM or a second renderer. A UI node is an entity. [`tecs.ecs.ChildOf`](/modules/ecs/builtins#childof) defines both its layout hierarchy and its transform hierarchy, while the existing rectangle, circle, image, sprite, and text producers keep drawing the result through the normal instanced GPU pipeline. The Compose demo scene combining an intrinsic image, a stretched rectangle, a fixed circle, and text The Compose scene shows the central rule: Taffy sizes retained ECS nodes, while the existing image, rectangle, circle, and text producers draw their leaves. The boundary is intentionally narrow: - [`tecs.ui.Style`](/modules/ui#tecs.ui.Style) supplies layout properties. - [`tecs.ui.Layout`](/modules/ui#tecs.ui.Layout) reports the computed box. - [`tecs.ui.Paint`](/modules/ui#tecs.ui.Paint) optionally stretches an existing drawing leaf to that box. - [`tecs.ui.Intrinsic`](/modules/ui#tecs.ui.Intrinsic) lets text, images, and custom leaves contribute their natural size. - [`tecs.ui.Scroll`](/modules/ui#tecs.ui.Scroll) offsets descendants and assigns the existing renderer clip component. - [`tecs.ui.Scrollbar`](/modules/ui#tecs.ui.Scrollbar) drives a composed thumb; it is not a special renderer primitive. - [`tecs.ui.Interaction`](/modules/ui#tecs.ui.Interaction) opts a box into hit testing, focus, and activation. Layout changes only dirty transforms whose computed results changed. Drawing components retain their normal batching, materials, dirty tracking, and GPU instancing. ## Start with a complete screen UI These first two blocks form one complete `main.tl`. The first block contains only imports and retained entity composition. It creates a full-window root, a panel, an ordinary rounded rectangle behind it, and one intrinsic text leaf. It does not install plugins or register systems. ```teal local tecs = require("tecs") local ui = tecs.ui local ChildOf = tecs.ecs.ChildOf local RelativeTransform2D = tecs.ecs.RelativeTransform2D local Tint = tecs.gfx.Tint local Material = tecs.gfx.Material local Renderable2D = tecs.gfx.Renderable2D local function spawnInterface( world: tecs.World, app: tecs.Application, font: tecs.gfx.Font ) local materials = tecs.gfx.materials local root = world:spawn( ui.Style({ width = "100%", height = "100%", justifyContent = "center", alignItems = "center", padding = "24px", }), ui.Root("screen"), tecs.Transform2D(0, 0, 0, 16) ) local panel = world:spawn( ui.Style({ width = 420, height = 240, flexDirection = "column", padding = 20, gap = 12, }), RelativeTransform2D(), ChildOf(root) ) world:spawn( ui.Style({position = "absolute", inset = 0}), ui.Paint(true), RelativeTransform2D(0, 0, 0), Tint(0.035, 0.055, 0.09, 0.96), Material(materials.id("rounded"), 0.05), Renderable2D(), ChildOf(panel) ) world:spawn( ui.Style({maxWidth = "100%"}), ui.Intrinsic("text", {wrap = true}), RelativeTransform2D(0, 0, 1), Tint(0.94, 0.97, 1.0, 1.0), tecs.gfx.Text.new({ text = "This text and its panel are ordinary ECS entities.", font = font, size = 16, }), ChildOf(panel) ) end ``` When the plugin receives an application, it keeps every screen root's logical size and pixel density synchronized with the window. Percentage dimensions therefore follow a resize without a game observer or per-frame system. World roots have two explicit contracts. A manually sized root preserves its authored extent for a render target or world-space panel: ```teal ui.Root("world", layoutWidth, layoutHeight, 1, "manual") ``` A camera-sized root follows the active camera and physical viewport. Its layout units are world units, its origin is the camera's top-left world point, and camera zoom or rotation updates its root transform: ```teal ui.Root("world", 0, 0, 1, "camera") ``` Camera2D sizing requires the application form of `ui.plugin`, because the window supplies the viewport. Screen roots accept `"auto"` or `"manual"`; world roots accept `"camera"` in addition to the manual behavior selected by `"auto"`. ## Install from the application plugin The application plugin configures the UI layer, installs one UI plugin in the world, and registers the system that calls `spawnInterface`. Passing the application supplies the renderer and folded input. A second table can override clip allocation, the input layer, or wheel distance. Paste this setup block after `spawnInterface`. It installs text and UI separately, loads an exact-size alpha font for crisp fixed-size UI text, and spawns the retained tree from `Startup`. ```teal local function gamePlugin(world: tecs.World, app: tecs.Application) tecs.gfx.layers.configure( 16, { sort = "z", screenSpace = true, unlit = true, } ) world:addPlugin(tecs.gfx.textPlugin({renderer = app.renderer})) world:addPlugin(tecs.ui.plugin(app, {wheelStep = 36})) world:addSystem({ name = "game.SpawnInterface", phase = tecs.ecs.phases.Startup, run = function() local font = tecs.gfx.newTTF({ source = "fonts/JetBrainsMono-ExtraBold.ttf", name = "game-ui-16", size = 16, raster = "alpha", }) spawnInterface(world, app, font) end, }) end return tecs.newApplication({ window = { title = "My game", width = 1280, height = 720, }, plugin = gamePlugin, }) ``` For UI over a 3D scene, configure the layer above with `overlay = true`. That selects the sprite forward lane even for fully opaque UI, and the lane runs after opaque and transparent meshes. The layer orders UI elements against one another; Tecs does not compare a `Camera3D` distance with a 2D layer band. Bloom is also composed before this lane, so HUD text and panels remain crisp. Plugin setup is for composition and registration. `Startup` is for spawning the retained entities, after all application plugins have been installed and before the first frame. Tests, tools, and manually constructed worlds can wire the dependencies directly instead: ```teal world:addPlugin(ui.plugin({ renderer = renderer, input = input, })) ``` That manual form has no window to synchronize. Give its screen roots explicit logical dimensions and pixel density with `ui.Root("screen", width, height, pixelDensity)`. ## Layout recipes Every recipe below belongs inside `spawnInterface` and attaches to the `root` created there. A layout container needs `Style`, `RelativeTransform2D`, and `ChildOf`. It needs no drawing component unless the container itself should be visible. ### Anchor a panel to a corner The root is a row by default. `justifyContent` chooses the main-axis position and `alignItems` chooses the cross-axis position. This puts a fixed-width panel at the top right while its height follows its children: ```teal local sidebar = world:spawn( ui.Style({ width = 360, flexDirection = "column", gap = 12, padding = 16, margin = {left = "auto"}, }), RelativeTransform2D(), ChildOf(root) ) ``` For a true overlay that takes no space from siblings, use absolute positioning: ```teal local overlay = world:spawn( ui.Style({ position = "absolute", width = 360, inset = {right = 24, top = 24, bottom = 24}, flexDirection = "column", gap = 12, }), RelativeTransform2D(), ChildOf(root) ) ``` ### Build a horizontal toolbar ```teal local toolbar = world:spawn( ui.Style({ width = "100%", height = 48, flexDirection = "row", alignItems = "center", gap = 8, padding = {left = 12, right = 12}, }), RelativeTransform2D(), ChildOf(root) ) for index = 1, 4 do world:spawn( ui.Style({width = 96, height = 32}), RelativeTransform2D(), ChildOf(toolbar) ) end ``` ### Divide remaining space between children `flexGrow` consumes space left on the container's main axis. This creates a fixed navigation column and a content area that fills the rest: ```teal local body = world:spawn( ui.Style({ width = "100%", flexGrow = 1, flexDirection = "row", gap = 16 }), RelativeTransform2D(), ChildOf(root) ) local navigation = world:spawn( ui.Style({width = 220, height = "100%"}), RelativeTransform2D(), ChildOf(body) ) local contentArea = world:spawn( ui.Style({flexGrow = 1, height = "100%"}), RelativeTransform2D(), ChildOf(body) ) ``` ### Wrap cards into rows ```teal local cards = world:spawn( ui.Style({ width = "100%", flexDirection = "row", flexWrap = "wrap", gap = 12, }), RelativeTransform2D(), ChildOf(root) ) for index = 1, 12 do world:spawn( ui.Style({width = 180, height = 96}), RelativeTransform2D(), ChildOf(cards) ) end ``` The Flex demo scene comparing a fixed-width child with flex-grow children and a wrapped grid The Flex scene makes both behaviors measurable: the first row compares `88px`, `flexGrow = 1`, and `flexGrow = 2`, while the cards below wrap at the panel edge. ### Overlay a badge without affecting layout An absolute child is positioned relative to its retained parent and does not consume flex space: ```teal world:spawn( ui.Style({ position = "absolute", width = 20, height = 20, inset = {right = -6, top = -6}, }), ui.Paint(true), RelativeTransform2D(0, 0, 5), Tint(1.0, 0.25, 0.2, 1.0), Material(tecs.gfx.materials.id("circle"), 0), Renderable2D(), ChildOf(contentArea) ) ``` The Overlay demo scene with four absolutely positioned corner cards and a centered higher-depth circle The Overlay scene shows that absolute children anchor to their retained parent without consuming flex space. The center circle and label also demonstrate that renderer depth remains independent from layout order. ### Remove a subtree from layout Change `display` through `getMut` so the UI plugin sees the write: ```teal local style = world:getMut(sidebar, ui.Style) style.style.display = "none" -- Later: local visible = world:getMut(sidebar, ui.Style) visible.style.display = "flex" ``` `display = "none"` removes the entity and its descendants from Taffy layout. It does not remove ordinary rectangle, image, or text instances from their GPU producers. A component that owns a whole-screen scene should therefore pair the layout state with its normal rendering visibility state. The repository demo's scene selector removes `Renderable2D` from inactive drawing leaves and restores it when their scene becomes active. ## Compose visuals from ordinary entities A container normally has `Style`, `RelativeTransform2D`, and `ChildOf`. Put its visual on an absolute child. `Paint(true)` centers that child and copies the computed width and height into its relative transform scale. The following entity snippets belong in `spawnInterface`, after the root is created. ```teal local panel = world:spawn( ui.Style({ width = 360, height = 520, flexDirection = "column", padding = 20, gap = 12, }), tecs.ecs.RelativeTransform2D(), tecs.ecs.ChildOf(root) ) world:spawn( ui.Style({position = "absolute", inset = 0}), ui.Paint(true), tecs.ecs.RelativeTransform2D(0, 0, 0), tecs.gfx.Tint(0.035, 0.055, 0.09, 0.96), tecs.gfx.Material(tecs.gfx.materials.id("rounded"), 0.04), tecs.gfx.Renderable2D(), tecs.ecs.ChildOf(panel) ) ``` The same pattern works with every drawing producer. A circle keeps the circle material, text keeps the glyph instance producer, and an image keeps its sprite. Add `Intrinsic` when a leaf should size itself instead of stretching: ```teal world:spawn( ui.Style({width = 32, height = 32}), ui.Paint(true), tecs.ecs.RelativeTransform2D(0, 0, 2), tecs.gfx.Tint(0.32, 0.82, 1.0, 1.0), tecs.gfx.Material(tecs.gfx.materials.id("circle"), 0), tecs.gfx.Renderable2D(), tecs.ecs.ChildOf(panel) ) world:spawn( ui.Style({maxWidth = "100%"}), ui.Intrinsic("text", {wrap = true}), tecs.ecs.RelativeTransform2D(0, 0, 2), tecs.gfx.Tint(0.94, 0.97, 1.0, 1.0), tecs.gfx.Text.new({ text = "Composed, not replaced", font = font, size = 22, }), tecs.ecs.ChildOf(panel) ) ``` ### Draw a stretched rectangle There is no UI rectangle type. This is the normal renderer quad, stretched to the box computed for its parent: ```teal local card = world:spawn( ui.Style({width = 280, height = 120}), RelativeTransform2D(), ChildOf(root) ) world:spawn( ui.Style({position = "absolute", inset = 0}), ui.Paint(true), RelativeTransform2D(0, 0, 0), Tint(0.10, 0.17, 0.27, 1.0), Material(tecs.gfx.materials.id("rounded"), 0.08), Renderable2D(), ChildOf(card) ) ``` ### Draw a circle at a fixed size ```teal world:spawn( ui.Style({width = 40, height = 40}), ui.Paint(true), RelativeTransform2D(0, 0, 1), Tint(0.32, 0.82, 1.0, 1.0), Material(tecs.gfx.materials.id("circle"), 0), Renderable2D(), ChildOf(card) ) ``` ### Render crisp fixed-size UI text Load an alpha font at the exact size used by `Text`. The same text component, atlas, producer, clipping, and instancing are used as SDF text. Exact-size alpha text on a screen-space layer also snaps its origin to a pixel: ```teal local uiFont = tecs.gfx.newTTF({ source = "fonts/JetBrainsMono-ExtraBold.ttf", name = "settings-ui-16", size = 16, raster = "alpha", }) world:spawn( ui.Style({maxWidth = "100%"}), ui.Intrinsic("text"), RelativeTransform2D(0, 0, 2), Tint(0.94, 0.97, 1.0, 1.0), tecs.gfx.Text.new({ text = "Video settings", font = uiFont, size = 16, }), ChildOf(card) ) ``` Use the default `raster = "sdf"` when the same font must animate through many scales or live in the world. Use separate `name` values when loading the same source in multiple sizes or raster modes because snapshots resolve fonts by name. ### Wrap text to the available width ```teal world:spawn( ui.Style({width = "100%", maxWidth = 320}), ui.Intrinsic("text", {wrap = true}), RelativeTransform2D(0, 0, 2), Tint(0.72, 0.80, 0.90, 1.0), tecs.gfx.Text.new({ text = "This paragraph wraps when its parent becomes narrower.", font = uiFont, size = 16, }), ChildOf(card) ) ``` ### Load an image and preserve its aspect ratio Images load off-thread and return directly, suspending a system only when the decode is not ready. This example fixes the height at 64 logical pixels and lets `Intrinsic("image")` derive the width from the registered sprite region: ```teal local image = tecs.assets.loadImage( tecs.io.files.assetPath("images/portrait.png") ) local sprite = app.renderer.sprites:registerImage(image) world:spawn( ui.Style({height = 64}), ui.Intrinsic("image"), ui.Paint(true), RelativeTransform2D(0, 0, 2), sprite, Tint(1, 1, 1, 1), Renderable2D(), ChildOf(card) ) ``` The system resumes only after the image is ready and then spawns it without a placeholder sprite or per-frame polling. ### Give another leaf a natural size Use custom intrinsic metrics when a dedicated producer already knows its preferred size. `Paint(true)` below makes the ordinary rectangle consume those metrics: ```teal world:spawn( ui.Style({maxWidth = "100%"}), ui.Intrinsic( "custom", { width = 96, height = 28, minWidth = 48, } ), ui.Paint(true), RelativeTransform2D(0, 0, 2), Tint(0.22, 0.56, 0.68, 1.0), Material(tecs.gfx.materials.id("rounded"), 0.12), Renderable2D(), ChildOf(card) ) ``` `ui.Intrinsic("image")` reads the selected sprite region's source dimensions from the renderer and preserves its aspect ratio when only one axis is constrained. `ui.Intrinsic("custom", {width = 80, height = 24})` supplies cached metrics for another leaf producer. `scale` converts source units into UI units. Text wrapping is a retained convergence step. Taffy chooses the available width, the UI plugin asks SDL_ttf for the exact wrapped height, and only that dirty root is solved again. No native-to-Lua callback runs inside Taffy and unchanged text is not reshaped each frame. The chosen width is retained in `Text.wrapWidth`, so the ordinary text instance producer draws the same lines that layout measured. Do not stretch a container that owns layout children. Its transform scale would compose into every descendant. Stretch a dedicated absolute drawing child instead. ## Style values and mutation Numbers and `px` strings are logical UI pixels; pixel density maps them to the render target. Percent strings are parent-relative, and `auto` selects automatic size: ```teal ui.Style({ width = "100%", minHeight = "120px", flexBasis = "auto", flexGrow = 1, margin = {left = "8px", right = "8px"}, }) ``` Plain numbers remain the compact form of logical pixels, so `padding = 24` and `padding = "24px"` are equivalent. The older `{value = 100, unit = "percent"}` representation remains supported. Strings are converted to retained typed dimensions only when a style is added or marked dirty. Window resizing changes the root's available space without reparsing unchanged styles. Unchanged frames also skip layout export, clipping, and hit-rectangle reconstruction. Supported properties are `display`, `position`, `flexDirection`, `flexWrap`, `justifyContent`, `alignItems`, `alignContent`, `flexGrow`, `flexShrink`, `flexBasis`, `width`, `height`, minimum and maximum dimensions, `margin`, `padding`, `border`, `gap`, `rowGap`, and `inset`. See the [`Style.style`](/modules/ui#tecs.ui.Style.style) contract for accepted values. Styles are retained component data. Mutate through `getMut` so the plugin can synchronize the changed node into the retained layout tree: ```teal local style = world:getMut(panel, ui.Style) style.style.width = 440 ``` ### Switch a responsive layout at a breakpoint Screen roots resize automatically, but a game may still want a deliberate layout change at a logical-width breakpoint. Cache the selected direction so `getMut` is called only when the breakpoint changes: System recipes belong in `gamePlugin`, beside the `Startup` system, rather than inside `spawnInterface`. Keep the entity ids in plugin-local variables, assign them from `Startup`, and guard the first update until spawning has finished. This abbreviated recipe assumes `root` and `body` are those retained ids: ```teal local narrow = false world:addSystem({ name = "game.ResponsiveUi", phase = tecs.ecs.phases.Update, run = function() if root == 0 or body == 0 then return end local rootValue = world:get(root, ui.Root) local nextNarrow = rootValue.width < 720 if nextNarrow ~= narrow then narrow = nextNarrow local style = world:getMut(body, ui.Style) style.style.flexDirection = narrow and "column" or "row" end end, }) ``` ### Read a computed box `Layout` is engine-owned output. Read it after UI layout when another system needs the final logical dimensions: ```teal world:addSystem({ name = "game.ReadPanelLayout", phase = tecs.ecs.phases.Last, run = function() local box = world:get(panel, ui.Layout) if box ~= nil then print( ("panel is %.0f by %.0f"):format(box.width, box.height) ) end end, }) ``` Do not write `Layout`, `Transform2D`, or `RelativeTransform2D` to resize a retained node. Change its `Style`; the plugin owns the computed outputs. ## Scroll and clip descendants Add `Scroll` to the viewport and make the larger content entity its child. Wheel input targets the deepest viewport under the pointer. The plugin clamps the offset, moves descendants after layout, intersects nested viewports, and writes ordinary `tecs.gfx.Clip` indices to drawing entities. The Scroll demo scene with a fixed clipped viewport, overflowing rows, and a composed block scrollbar The Scroll scene labels the edge cases directly: fixed viewport size, clipped descendants, nested wheel handoff, focus reveal, and a scrollbar assembled from ordinary rectangle entities. ```teal local viewport = world:spawn( ui.Style({ width = "100%", height = 240, }), ui.Scroll(), tecs.ecs.RelativeTransform2D(), tecs.ecs.ChildOf(panel) ) local content = world:spawn( ui.Style({ width = "100%", height = 600, flexDirection = "column", gap = 8, }), tecs.ecs.RelativeTransform2D(), tecs.ecs.ChildOf(viewport) ) ``` Populate the content with ordinary retained rows. The larger authored content height is what creates overflow: ```teal local function spawnRow(label: string, tabIndex: integer) local row = world:spawn( ui.Style({width = "100%", height = 44}), ui.Interaction({tabIndex = tabIndex}), RelativeTransform2D(), ChildOf(content) ) world:spawn( ui.Style({position = "absolute", inset = 0}), ui.Paint(true), RelativeTransform2D(0, 0, 1), Tint(0.12, 0.20, 0.30, 1.0), Material(tecs.gfx.materials.id("rounded"), 0.08), Renderable2D(), ChildOf(row) ) world:spawn( ui.Style({ position = "absolute", inset = {left = 12, top = 14} }), ui.Intrinsic("text"), RelativeTransform2D(0, 0, 2), Tint(0.9, 0.95, 1.0, 1.0), tecs.gfx.Text.new({text = label, font = font, size = 16}), ChildOf(row) ) return row end local lastRow = 0 for index = 1, 20 do lastRow = spawnRow(("Item %02d"):format(index), index) end ``` Programmatic scrolling writes `Scroll.x` or `Scroll.y` through `getMut`. Offsets clamp to the complete retained extent, including negative and absolute descendants. `contentX`, `contentY`, `contentWidth`, and `contentHeight` are engine-owned measurements. Snapshots retain `x` and `y`; the derived content fields are rebuilt after load. ```teal -- Scroll by a logical amount. The plugin clamps it during layout. local scroll = world:getMut(viewport, ui.Scroll) scroll.y = scroll.y + 80 -- Or expose a particular descendant through every nested viewport. ui.reveal(world, lastRow, "center") ``` Wheel movement starts at the deepest viewport and hands any unused distance to its ancestors. It follows the platform's configured scroll direction while leaving `Input.wheelX` and `Input.wheelY` available to directional gameplay bindings with their stable sign convention. Shift converts a vertical wheel into horizontal movement when the device reports no horizontal axis. Call `ui.reveal(world, entity)` to expose a descendant through every ancestor viewport. Focusing an entity does this automatically. Taffy does not create or draw a scrollbar. It lays out the viewport and content. The UI system turns those retained measurements and `Scroll.y` into the thumb's `RelativeTransform2D`; the thumb itself uses the same ordinary material and instance components used elsewhere: ```teal world:spawn( ui.Scrollbar("vertical", 6, 2, 18), ui.Interaction({focusable = false, draggable = true, order = 100}), tecs.ecs.RelativeTransform2D(0, 0, 10), tecs.gfx.Tint(0.4, 0.7, 0.9, 0.9), tecs.gfx.Material(tecs.gfx.materials.id("rounded"), 0.5), tecs.gfx.Renderable2D(), tecs.ecs.ChildOf(viewport) ) ``` The thumb receives a zero length when its axis has no overflow. Horizontal scrollbars use the same component with `"horizontal"`. A rail, border, end blocks, arrow controls, or grip marks are optional composed entities. The repository demo uses squared rectangles for a chunky retro skin; none of that appearance lives in Taffy or in the scrollbar component. Here is a complete squared, blocky scrollbar skin. Only the middle entity carries `Scrollbar`; the track and end blocks are ordinary decoration: ```teal world:spawn( ui.Style({ position = "absolute", width = 14, inset = {right = 0, top = 0, bottom = 0}, }), ui.Paint(true), RelativeTransform2D(0, 0, 8), Tint(0.05, 0.09, 0.14, 1.0), Material(tecs.gfx.materials.id("rounded"), 0), Renderable2D(), ChildOf(viewport) ) world:spawn( ui.Scrollbar("vertical", 12, 16, 28), ui.Interaction({focusable = false, draggable = true, order = 100}), RelativeTransform2D(0, 0, 10), Tint(0.32, 0.82, 1.0, 1.0), Material(tecs.gfx.materials.id("rounded"), 0), Renderable2D(), ChildOf(viewport) ) for _, top in ipairs({true, false}) do world:spawn( ui.Style({ position = "absolute", width = 14, height = 14, inset = top and {right = 0, top = 0} or {right = 0, bottom = 0}, }), ui.Paint(true), RelativeTransform2D(0, 0, 11), Tint(0.22, 0.56, 0.68, 1.0), Material(tecs.gfx.materials.id("rounded"), 0), Renderable2D(), ChildOf(viewport) ) end ``` ## Observe clicks and keyboard activation `Interaction` adds clip-aware hit testing and transient `InteractionState`. Every mouse or touch identity captures independently. Releasing over the same target emits `click` followed by `activate`. A draggable interaction emits `dragStart`, `dragMove`, and `dragEnd`; losing its input layer emits `dragCancel`. Tab and Shift-Tab move focus and repeat after a short hold; Return and Space activate the focused control. ```teal local type uiTypes = require("tecs.ui") local type UiEvent = uiTypes.Event local button = world:spawn( ui.Style({width = 180, height = 44}), ui.Interaction({tabIndex = 1}), tecs.ecs.RelativeTransform2D(), tecs.ecs.ChildOf(content) ) world:observe( button, ui.Event, function(event: UiEvent) if event.kind == "activate" then print("activated via", event.source) end end ) ``` `Interaction` makes the container selectable; it does not draw a button. Add ordinary visual and text children to complete it: ```teal local buttonVisual = world:spawn( ui.Style({position = "absolute", inset = 0}), ui.Paint(true), RelativeTransform2D(0, 0, 1), Tint(0.12, 0.20, 0.30, 1.0), Material(tecs.gfx.materials.id("rounded"), 0.16), Renderable2D(), ChildOf(button) ) world:spawn( ui.Style({position = "absolute", inset = {left = 14, top = 13}}), ui.Intrinsic("text"), RelativeTransform2D(0, 0, 2), Tint(0.9, 0.95, 1.0, 1.0), tecs.gfx.Text.new({text = "Apply", font = font, size = 16}), ChildOf(button) ) ``` Events bubble through `ChildOf`. An observer can set `event.consumed = true` to stop ancestor delivery. Consuming a wheel event also suppresses default scrolling. ```teal world:observe( panel, ui.Event, function(event: UiEvent) if event.kind == "activate" then print("panel saw activation from", event.target) event.consumed = true elseif event.kind == "wheel" and menuIsLocked then event.consumed = true end end ) ``` `event.pointerId` distinguishes simultaneous touches and `event.pointerType` reports `"mouse"` or `"touch"`. Pointer movement and drag events put their movement in `deltaX` and `deltaY`. ### Update hover, press, focus, and drag colors Keep the control and its drawing child as separate entities. The update system reads transient state from the control and dirties the GPU tint only when the selected color changes: ```teal world:addSystem({ name = "game.ButtonVisualState", phase = tecs.ecs.phases.Update, run = function() local state = world:get(button, ui.InteractionState) local tint = world:get(buttonVisual, Tint) local r, g, b = 0.12, 0.20, 0.30 if state.dragging or state.pressed then r, g, b = 0.14, 0.47, 0.62 elseif state.hovered then r, g, b = 0.17, 0.34, 0.48 elseif state.focused then r, g, b = 0.18, 0.29, 0.46 end if tint.r ~= r or tint.g ~= g or tint.b ~= b then local changed = world:getMut(buttonVisual, Tint) changed.r, changed.g, changed.b = r, g, b end end, }) ``` Register update systems from `gamePlugin`, beside the `Startup` system. Keep the spawned entity ids in plugin-local variables so those systems can read them after startup. ### Handle dragging and simultaneous pointers ```teal local slider = world:spawn( ui.Style({width = 240, height = 32}), ui.Interaction({tabIndex = 2, draggable = true}), RelativeTransform2D(), ChildOf(panel) ) local dragByPointer: {string: number} = {} world:observe( slider, ui.Event, function(event: UiEvent) if event.kind == "dragStart" then dragByPointer[event.pointerId] = 0 elseif event.kind == "dragMove" then dragByPointer[event.pointerId] = ( dragByPointer[event.pointerId] or 0 ) + event.deltaX elseif event.kind == "dragEnd" or event.kind == "dragCancel" then dragByPointer[event.pointerId] = nil end end ) ``` Each touch and the mouse has its own `pointerId`, capture, and drag lifecycle. Do not use one global dragging boolean when a control needs to distinguish simultaneous touches. Call `ui.focus`, `ui.blur`, and `ui.focused` for programmatic focus. A modal panel can carry `ui.FocusScope()` and constrain navigation until popped: ```teal ui.focus(world, firstField) ui.pushFocusScope(world, dialog) -- The active scope now owns Tab traversal and pointer targeting. ui.popFocusScope( world, dialog ) -- Restores the prior focus when possible. ``` This is a complete focus-scope container. Controls spawned below `dialog` become the only Tab and pointer targets while the scope is active: ```teal local dialog = world:spawn( ui.Style({ position = "absolute", width = 440, height = 260, inset = {left = "25%", top = "20%"}, flexDirection = "column", padding = 20, gap = 12, }), ui.FocusScope(), RelativeTransform2D(0, 0, 20), ChildOf(root) ) local cancelButton = world:spawn( ui.Style({width = 120, height = 44}), ui.Interaction({tabIndex = 1}), RelativeTransform2D(), ChildOf(dialog) ) ui.pushFocusScope(world, dialog) ui.focus(world, cancelButton) world:observe( cancelButton, ui.Event, function(event: UiEvent) if event.kind == "activate" then ui.popFocusScope(world, dialog) world:despawn(dialog) end end ) ``` ### Choose deterministic overlap and navigation order ```teal local back = world:spawn( ui.Style({ position = "absolute", width = 160, height = 48, order = 1 }), ui.Interaction({tabIndex = 1, order = 1}), RelativeTransform2D(0, 0, 4), ChildOf(panel) ) local front = world:spawn( ui.Style({ position = "absolute", width = 160, height = 48, order = 2 }), ui.Interaction({tabIndex = 2, order = 2}), RelativeTransform2D(0, 0, 5), ChildOf(panel) ) ``` `Style.style.order` places siblings in layout. `Interaction.order` breaks hit and focus ties. Transform2D Z determines renderer depth. Set all three when two controls intentionally overlap rather than relying on entity creation order. `Interaction.tabIndex` defines the primary navigation order. `Interaction.order` is the explicit authorial tie-breaker for focus and hit testing, while `Style.style.order` controls retained sibling layout order. Equal values fall back to the order in which entities first entered the retained tree, not to entity identity. Give overlapping controls explicit orders when their stacking is meaningful. Read `InteractionState` from an update system to select hover, pressed, and focused colors. Only call `getMut` on the visual component when the selected color actually changes, preserving dirty-gated GPU synchronization. ## Editing and accessibility boundary The current interaction layer stops at semantic activation and dragging. Text editing, selection, IME composition, clipboard commands, controller spatial navigation, and platform accessibility exposure are designed but not yet implemented. In particular, a `Text` drawing entity is not implicitly an edit control, and an `Interaction` does not invent a role or accessible name. Those features will remain composed ECS state: editing state beside a `Text` leaf, platform text input entered only while that entity is focused, controller actions routed through the input layer, and a semantic snapshot bridged from Rust without an FFI callback into Lua. They do not require a DOM, CSS cascade, or parallel widget renderer. ## Run the examples The repository demo shows layout, rectangle and circle materials, text, an cooperatively loaded image, nested clipping, wheel scrolling, pointer capture, bubbling events, and keyboard navigation: ```bash cargo xtask example ui-demo ``` The standalone retained UI example with a centered panel and scrollable controls The smaller [standalone UI example](https://github.com/tecs-dev/tecs/blob/main/docs/examples/ui.tl) is suitable as a project's `main.tl`.