# `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.