# `tecs.application`
The host-neutral application lifecycle.
The Rust host owns polling and passes one sealed event batch and elapsed delta
to `iterate`. This module owns only game lifecycle: plugin installation,
startup, world updates, crash containment, suspension, quit requests, and
shutdown. Windowing, rendering, audio, and platform event conversion are Rust
service boundaries rather than application fields.
Each application owns an asset loader. `Config.mcpPort` opts into one loopback
MCP listener, opened before plugin installation so startup failures remain
inspectable. Only one application per runtime may own that listener, because
the tool registry names one world. Nil opens no listener; zero chooses a port.
A host calls `poll` outside `iterate`, once per host turn, even while gameplay
is suspended or a lifecycle operation is parked. `tecs.host` does this and
parks asynchronous startup as well as updates. A custom host must follow the
same ordering. Diagnostics remain available during a parked operation; tools
that could touch its in-progress world report a retryable refusal.
## Driving frames from outside
A test harness or debugger drives an application deterministically with three
calls, which the MCP `send_event` and `clock_*` tools reach. `queueEvent`
queues a platform event for the next frame that runs, delivered after the
host's batch for that turn exactly as a platform event is, so per-frame edges
such as `keyPressed` survive the frame's `beginFrame`. `pause` holds the
simulation: an iteration that has no granted frame runs no system, folds no
input, leaves `frame` and `elapsed` unchanged, and keeps the host's events for
the frame that eventually runs, honoring only a quit. The host keeps rendering
the held world, so a screenshot requested while paused shows it. `step` grants
frames that run while paused, each with the fixed delta it names rather than
the host's measured one, so a paused application advances by exactly the
granted frames and simulated time.
```nupp
app:pause()
app:queueEvent(tecs.platform.events.key(true, "Space", " ", " ", 0, false, 0))
app:step(1, 1 / 60)
app:iterate(0.5, nil) -- runs one frame with dt 1/60 and sees Space pressed
app:iterate(0.5, nil) -- holds: no frame, no time
```
Shutdown first unwinds a parked host operation, then attempts world shutdown,
stops the loader, and closes the debug listener. Later application cleanup
still runs if world shutdown raises. Resident asset handles retain their
existing release contract: game shutdown systems release the holds they own.
Repeated application shutdown does nothing.
## Types
### `Application` _record_
```nupp
record Application is mcpbindings.Application ...
```
A host-driven application and its world.
#### Members
| Name | Kind | Description |
| --- | --- | --- |
| [`world`](#tecs.application.Application.world) | field | |
| [`assets`](#tecs.application.Application.assets) | field | Read-only. |
| [`debugServer`](#tecs.application.Application.debugServer) | field | Read-only. |
| [`input`](#tecs.application.Application.input) | field | |
| [`window`](#tecs.application.Application.window) | field | |
| [`quitRequested`](#tecs.application.Application.quitRequested) | field | |
| [`suspended`](#tecs.application.Application.suspended) | field | |
| [`elapsed`](#tecs.application.Application.elapsed) | field | |
| [`frame`](#tecs.application.Application.frame) | field | |
| [`paused`](#tecs.application.Application.paused) | field | Read-only. |
| [`state`](#tecs.application.Application.state) | field | |
| [`plugin`](#tecs.application.Application.plugin) | field | |
| [`onEvent`](#tecs.application.Application.onEvent) | field | |
| [`bindDebug`](#tecs.application.Application.bindDebug) | method | |
| [`mcpPort`](#tecs.application.Application.mcpPort) | field | |
| [`debug`](#tecs.application.Application.debug) | field | |
| [`debugMaxFrames`](#tecs.application.Application.debugMaxFrames) | field | |
| [`failure`](#tecs.application.Application.failure) | field | |
| [`shutdownDone`](#tecs.application.Application.shutdownDone) | field | |
| [`steps`](#tecs.application.Application.steps) | field | |
| [`stepDt`](#tecs.application.Application.stepDt) | field | |
| [`held`](#tecs.application.Application.held) | field | |
| [`injected`](#tecs.application.Application.injected) | field | |
| [`poll`](#tecs.application.Application.poll) | method | Polls optional debugging once per host turn, outside the world update scope. |
| [`init`](#tecs.application.Application.init) | method | |
| [`iterate`](#tecs.application.Application.iterate) | method | |
| [`requestQuit`](#tecs.application.Application.requestQuit) | method | |
| [`setSuspended`](#tecs.application.Application.setSuspended) | method | |
| [`crashed`](#tecs.application.Application.crashed) | method | |
| [`clearCrash`](#tecs.application.Application.clearCrash) | method | |
| [`shutdown`](#tecs.application.Application.shutdown) | method | |
| [`getState`](#tecs.application.Application.getState) | method | |
| [`queueEvent`](#tecs.application.Application.queueEvent) | method | Queues a platform event for the next frame that runs. |
| [`pause`](#tecs.application.Application.pause) | method | Holds the simulation from the next iteration on. |
| [`resume`](#tecs.application.Application.resume) | method | Lets the simulation run on the host's measured delta again. |
| [`step`](#tecs.application.Application.step) | method | Grants frames that run while paused, one per host iteration. |
| [`pendingSteps`](#tecs.application.Application.pendingSteps) | method | Returns the frames step granted that have not started. |
| [`stepDelta`](#tecs.application.Application.stepDelta) | method | Returns the fixed delta granted frames run with. |
| [`pendingEvents`](#tecs.application.Application.pendingEvents) | method | Returns how many events wait for the next frame that runs. |
#### `world` _field_
```nupp
world: ecs.World
```
#### `assets` _field_
```nupp
assets: assets.Assets
```
Read-only. Holds this application's loader, which shuts down with the
application.
#### `debugServer` _field_
```nupp
debugServer: mcp.Server?
```
Read-only. Holds the optional loopback debug listener after initialization.
#### `input` _field_
```nupp
input: input.Input
```
#### `window` _field_
```nupp
window: platformwindow.Window
```
#### `quitRequested` _field_
```nupp
quitRequested: boolean
```
#### `suspended` _field_
```nupp
suspended: boolean
```
#### `elapsed` _field_
```nupp
elapsed: number
```
#### `frame` _field_
```nupp
frame: integer
```
#### `paused` _field_
```nupp
paused: boolean
```
Read-only. Reports whether `pause` holds the simulation. Change it through
`pause` and `resume`.
#### `state` _field_
```nupp
state: State
```
`@private`
#### `plugin` _field_
```nupp
plugin: Plugin?
```
`@private`
#### `onEvent` _field_
```nupp
onEvent: (function(event: platformevents.Event, exclusive app: Application): nil)?
```
`@private`
#### `bindDebug` _method_
```nupp
bindDebug: function(): nil
```
`@private`
##### Returns
| Type | Description |
| --- | --- |
| `nil` | |
#### `mcpPort` _field_
```nupp
mcpPort: integer?
```
`@private`
#### `debug` _field_
```nupp
debug: boolean
```
`@private`
#### `debugMaxFrames` _field_
```nupp
debugMaxFrames: integer?
```
`@private`
#### `failure` _field_
```nupp
failure: string?
```
`@private`
#### `shutdownDone` _field_
```nupp
shutdownDone: boolean
```
`@private`
#### `steps` _field_
```nupp
steps: integer
```
`@private`
#### `stepDt` _field_
```nupp
stepDt: number
```
`@private`
#### `held` _field_
```nupp
held: {platformevents.Event}
```
`@private`
#### `injected` _field_
```nupp
injected: {platformevents.Event}
```
`@private`
#### `poll` _method_
```nupp
poll: function(exclusive self: Application, parked: boolean?): boolean
```
Polls optional debugging once per host turn, outside the world update scope.
##### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `exclusive self` | `Application` | the application to serve. |
| `parked` | `boolean?` | whether its lifecycle coroutine remains suspended. |
##### Returns
| Type | Description |
| --- | --- |
| `boolean` | whether one debug request was answered. |
#### `init` _method_
```nupp
init: function(exclusive self: Application): boolean
```
##### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `exclusive self` | `Application` | |
##### Returns
| Type | Description |
| --- | --- |
| `boolean` | |
#### `iterate` _method_
```nupp
iterate: function(exclusive self: Application, dt: number, events: {platformevents.Event}?): boolean
```
##### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `exclusive self` | `Application` | |
| `dt` | `number` | |
| `events` | `{platformevents.Event}?` | |
##### Returns
| Type | Description |
| --- | --- |
| `boolean` | |
#### `requestQuit` _method_
```nupp
requestQuit: function(exclusive self: Application): nil
```
##### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `exclusive self` | `Application` | |
##### Returns
| Type | Description |
| --- | --- |
| `nil` | |
#### `setSuspended` _method_
```nupp
setSuspended: function(exclusive self: Application, suspended: boolean): nil
```
##### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `exclusive self` | `Application` | |
| `suspended` | `boolean` | |
##### Returns
| Type | Description |
| --- | --- |
| `nil` | |
#### `crashed` _method_
```nupp
crashed: function(borrows self: Application): string?
```
##### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `borrows self` | `Application` | |
##### Returns
| Type | Description |
| --- | --- |
| `string?` | |
#### `clearCrash` _method_
```nupp
clearCrash: function(exclusive self: Application): (boolean, string?)
```
##### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `exclusive self` | `Application` | |
##### Returns
| Type | Description |
| --- | --- |
| `boolean` | |
| `string?` | |
#### `shutdown` _method_
```nupp
shutdown: function(exclusive self: Application): boolean
```
##### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `exclusive self` | `Application` | |
##### Returns
| Type | Description |
| --- | --- |
| `boolean` | |
#### `getState` _method_
```nupp
getState: function(borrows self: Application): State
```
##### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `borrows self` | `Application` | |
##### Returns
| Type | Description |
| --- | --- |
| `State` | |
#### `queueEvent` _method_
```nupp
queueEvent: function(exclusive self: Application, event: platformevents.Event): nil
```
Queues a platform event for the next frame that runs.
The event reaches window state, input state, the world's observers and
`Config.onEvent` after that frame's `beginFrame`, behind the host's own
batch for the same turn, so per-frame edges such as `keyPressed` read
true for exactly that frame. While paused the event waits for the next
frame `step` or `resume` lets run.
##### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `exclusive self` | `Application` | the application to deliver to |
| `event` | `platformevents.Event` | the event, delivered as if the platform had sent it |
##### Returns
| Type | Description |
| --- | --- |
| `nil` | |
#### `pause` _method_
```nupp
pause: function(exclusive self: Application): nil
```
Holds the simulation from the next iteration on.
A held iteration runs no system and folds no input, and leaves `frame`
and `elapsed` unchanged. Host events wait for the next frame that runs,
up to the latest 1024, except that a quit or close request still stops
the application. The
host keeps rendering the held world. Pausing again does nothing, and
frames already granted by `step` are kept.
##### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `exclusive self` | `Application` | the application to hold |
##### Returns
| Type | Description |
| --- | --- |
| `nil` | |
#### `resume` _method_
```nupp
resume: function(exclusive self: Application): nil
```
Lets the simulation run on the host's measured delta again.
Discards any frames `step` granted and has not run yet. Resuming a
running application does nothing.
##### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `exclusive self` | `Application` | the application to release |
##### Returns
| Type | Description |
| --- | --- |
| `nil` | |
#### `step` _method_
```nupp
step: function(exclusive self: Application, frames: integer, dt: number): nil
```
Grants frames that run while paused, one per host iteration.
Each granted frame passes `dt` to the world in place of the host's
delta, so a script of steps reproduces the same simulated time on every
run and every host. The delta applies to every granted frame not yet
run, including ones an earlier call granted.
##### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `exclusive self` | `Application` | the paused application |
| `frames` | `integer` | how many more frames to run, at least one |
| `dt` | `number` | the fixed delta of each granted frame, in seconds |
##### Returns
| Type | Description |
| --- | --- |
| `nil` | |
##### Raises
| Type | Condition |
| --- | --- |
| `string` | when the application is not paused |
| `string` | when `frames` is not a positive integer or `dt` is not a positive finite number |
#### `pendingSteps` _method_
```nupp
pendingSteps: function(borrows self: Application): integer
```
Returns the frames `step` granted that have not started.
##### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `borrows self` | `Application` | the application to inspect |
##### Returns
| Type | Description |
| --- | --- |
| `integer` | the count, zero while running or once every granted frame ran |
#### `stepDelta` _method_
```nupp
stepDelta: function(borrows self: Application): number
```
Returns the fixed delta granted frames run with.
##### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `borrows self` | `Application` | the application to inspect |
##### Returns
| Type | Description |
| --- | --- |
| `number` | the delta in seconds the last `step` named, or 1/60 before one |
#### `pendingEvents` _method_
```nupp
pendingEvents: function(borrows self: Application): integer
```
Returns how many events wait for the next frame that runs.
##### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `borrows self` | `Application` | the application to inspect |
##### Returns
| Type | Description |
| --- | --- |
| `integer` | the count of queued and held events |
### `Config` _type_
```nupp
type Config = {
world: ecs.WorldConfig?,
window: platformwindow.Options?,
plugin: Plugin?,
onEvent: (function(event: platformevents.Event, exclusive app: Application): nil)?,
--- Caller-writable. Enables loopback debugging on this port; nil disables it and
--- zero chooses a free port.
mcpPort: integer?,
debug: boolean?,
debugMaxFrames: integer?
}
```
Settings copied by `newApplication`.
### `HostEvent` _type_
```nupp
type HostEvent = Quit
| WindowCloseRequested
| WindowResized
| WindowScaleChanged
| WindowFocusGained
| WindowFocusLost
| KeyDown
| KeyUp
| MouseMotion
| MouseDown
| MouseUp
| MouseWheel
| TextInput
| FingerDown
| FingerUp
| FingerMotion
| FingerCanceled
| GamepadAdded
| GamepadRemoved
| GamepadButtonDown
| GamepadButtonUp
| GamepadAxis
```
A platform event already translated by the Rust host.
### `Plugin` _type_
```nupp
type Plugin = function(exclusive app: Application): nil
```
Installs a game's entities, resources, observers, and systems before startup.
### `State` _type_
```nupp
type State = "created" | "running" | "stopped"
```
The lifecycle state visible to host diagnostics.
## Functions
### `newApplication` _function_
```nupp
function newApplication(config: Config?): Application
```
Builds an inert application without opening platform services.
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `config` | `Config?` | the copied world and lifecycle settings |
#### Returns
| Type | Description |
| --- | --- |
| `Application` | an application waiting for `init` |
#### Raises
| Type | Condition |
| --- | --- |
| `string` | when the debug frame limit or MCP port is invalid |
## Values
### `DEFAULT_IDENTITY` _variable_
```nupp
const DEFAULT_IDENTITY
```
Read-only. Names the organization and the application `newApplication`
selects when a game named neither.