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.

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.

Module contents

Types

TypeKindDescription
ApplicationrecordA host-driven application and its world.
ConfigtypeSettings copied by newApplication.
HostEventtypeA platform event already translated by the Rust host.
PlugintypeInstalls a game's entities, resources, observers, and systems before startup.
StatetypeThe lifecycle state visible to host diagnostics.

Functions

FunctionKindDescription
newApplicationfunctionBuilds an inert application without opening platform services.

Values

ValueKindDescription
DEFAULT_IDENTITYvariableRead-only.

Types#

Applicationrecord#

record Application is mcpbindings.Application ...

A host-driven application and its world.

Members

NameKindDescription
worldfield
assetsfieldRead-only.
debugServerfieldRead-only.
inputfield
windowfield
quitRequestedfield
suspendedfield
elapsedfield
framefield
pausedfieldRead-only.
statefield
pluginfield
onEventfield
bindDebugmethod
mcpPortfield
debugfield
debugMaxFramesfield
failurefield
shutdownDonefield
stepsfield
stepDtfield
heldfield
injectedfield
pollmethodPolls optional debugging once per host turn, outside the world update scope.
initmethod
iteratemethod
requestQuitmethod
setSuspendedmethod
crashedmethod
clearCrashmethod
shutdownmethod
getStatemethod
queueEventmethodQueues a platform event for the next frame that runs.
pausemethodHolds the simulation from the next iteration on.
resumemethodLets the simulation run on the host's measured delta again.
stepmethodGrants frames that run while paused, one per host iteration.
pendingStepsmethodReturns the frames step granted that have not started.
stepDeltamethodReturns the fixed delta granted frames run with.
pendingEventsmethodReturns how many events wait for the next frame that runs.

worldfield#

world: ecs.World

assetsfield#

assets: assets.Assets

Read-only. Holds this application's loader, which shuts down with the application.

debugServerfield#

debugServer: mcp.Server?

Read-only. Holds the optional loopback debug listener after initialization.

inputfield#

input: input.Input

windowfield#

window: platformwindow.Window

quitRequestedfield#

quitRequested: boolean

suspendedfield#

suspended: boolean

elapsedfield#

elapsed: number

framefield#

frame: integer

pausedfield#

paused: boolean

Read-only. Reports whether pause holds the simulation. Change it through pause and resume.

statefield#

state: State
@private

pluginfield#

plugin: Plugin?
@private

onEventfield#

onEvent: (function(event: platformevents.Event, exclusive app: Application): nil)?
@private

bindDebugmethod#

bindDebug: function(): nil
@private
Returns
TypeDescription
nil

mcpPortfield#

mcpPort: integer?
@private

debugfield#

debug: boolean
@private

debugMaxFramesfield#

debugMaxFrames: integer?
@private

failurefield#

failure: string?
@private

shutdownDonefield#

shutdownDone: boolean
@private

stepsfield#

steps: integer
@private

stepDtfield#

stepDt: number
@private

heldfield#

held: {platformevents.Event}
@private

injectedfield#

injected: {platformevents.Event}
@private

pollmethod#

poll: function(exclusive self: Application, parked: boolean?): boolean

Polls optional debugging once per host turn, outside the world update scope.

Arguments
NameTypeDescription
exclusive selfApplication

the application to serve.

parkedboolean?

whether its lifecycle coroutine remains suspended.

Returns
TypeDescription
boolean

whether one debug request was answered.

initmethod#

init: function(exclusive self: Application): boolean
Arguments
NameTypeDescription
exclusive selfApplication
Returns
TypeDescription
boolean

iteratemethod#

iterate: function(exclusive self: Application, dt: number, events: {platformevents.Event}?): boolean
Arguments
NameTypeDescription
exclusive selfApplication
dtnumber
events{platformevents.Event}?
Returns
TypeDescription
boolean

requestQuitmethod#

requestQuit: function(exclusive self: Application): nil
Arguments
NameTypeDescription
exclusive selfApplication
Returns
TypeDescription
nil

setSuspendedmethod#

setSuspended: function(exclusive self: Application, suspended: boolean): nil
Arguments
NameTypeDescription
exclusive selfApplication
suspendedboolean
Returns
TypeDescription
nil

crashedmethod#

crashed: function(borrows self: Application): string?
Arguments
NameTypeDescription
borrows selfApplication
Returns
TypeDescription
string?

clearCrashmethod#

clearCrash: function(exclusive self: Application): (boolean, string?)
Arguments
NameTypeDescription
exclusive selfApplication
Returns
TypeDescription
boolean
string?

shutdownmethod#

shutdown: function(exclusive self: Application): boolean
Arguments
NameTypeDescription
exclusive selfApplication
Returns
TypeDescription
boolean

getStatemethod#

getState: function(borrows self: Application): State
Arguments
NameTypeDescription
borrows selfApplication
Returns
TypeDescription
State

queueEventmethod#

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
NameTypeDescription
exclusive selfApplication

the application to deliver to

eventplatformevents.Event

the event, delivered as if the platform had sent it

Returns
TypeDescription
nil

pausemethod#

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
NameTypeDescription
exclusive selfApplication

the application to hold

Returns
TypeDescription
nil

resumemethod#

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
NameTypeDescription
exclusive selfApplication

the application to release

Returns
TypeDescription
nil

stepmethod#

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
NameTypeDescription
exclusive selfApplication

the paused application

framesinteger

how many more frames to run, at least one

dtnumber

the fixed delta of each granted frame, in seconds

Returns
TypeDescription
nil
Raises
TypeCondition
string

when the application is not paused

string

when frames is not a positive integer or dt is not a positive finite number

pendingStepsmethod#

pendingSteps: function(borrows self: Application): integer

Returns the frames step granted that have not started.

Arguments
NameTypeDescription
borrows selfApplication

the application to inspect

Returns
TypeDescription
integer

the count, zero while running or once every granted frame ran

stepDeltamethod#

stepDelta: function(borrows self: Application): number

Returns the fixed delta granted frames run with.

Arguments
NameTypeDescription
borrows selfApplication

the application to inspect

Returns
TypeDescription
number

the delta in seconds the last step named, or 1/60 before one

pendingEventsmethod#

pendingEvents: function(borrows self: Application): integer

Returns how many events wait for the next frame that runs.

Arguments
NameTypeDescription
borrows selfApplication

the application to inspect

Returns
TypeDescription
integer

the count of queued and held events

Configtype#

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.

HostEventtype#

A platform event already translated by the Rust host.

Plugintype#

type Plugin = function(exclusive app: Application): nil

Installs a game's entities, resources, observers, and systems before startup.

Statetype#

type State = "created" | "running" | "stopped"

The lifecycle state visible to host diagnostics.

Functions#

newApplicationfunction#

function newApplication(config: Config?): Application

Builds an inert application without opening platform services.

Arguments

NameTypeDescription
configConfig?

the copied world and lifecycle settings

Returns

TypeDescription
Application

an application waiting for init

Raises

TypeCondition
string

when the debug frame limit or MCP port is invalid

Values#

DEFAULT_IDENTITYvariable#

Read-only. Names the organization and the application newApplication selects when a game named neither.