# Temporal Kit

Shared time utilities for **gameplay**, **session services**, and host clocks. Implementation lives under `src/hikari/sdk/src/temporal/`. Exports: `hi.temporal` plus aliases `hi.Timer` / `hi.Interval` / `hi.Cooldown` / `hi.Stopwatch` / `hi.Deadline` / `hi.Flow` / `hi.Timers` / `hi.nowNs` / `hi.wallTimeMs` / `hi.wallTimeSec` / `hi.wallTimeNs` (and the `*Io` variants).

## Goal

Delays, rate limits, intervals, multi-step routines, and clock helpers under hot-path constraints: plain values, no per-frame heap, poll edges — no callback graphs on the hot path.

## Non-goals

- Managing suspended call stacks. Use [hi.Routine](/docs/guides/concurrency.md) for engine-scheduled stackful gameplay routines; `Flow` / `Timer` remain small caller-driven helpers.
- Host-mediated scene-load waits (use `world.sceneLoadSnapshot` / readiness stages).
- Calendar / timezone / wall-clock scheduling.
- Replacing Motion Kit duration/delay fields on tweens.

`Timer` / `Cooldown` / `Interval` cover single delays and rate limits. **`Flow`** is the multi-step story: explicit phases on caller-owned state, short `update` steps, no hidden suspension or heap iterators. Coming from another engine's coroutine model? See [Migration](/docs/guides/migration.md#time-and-routines).

## Where it sits

```text
entity / GameSubsystem state
        │
        ▼
   temporal.*   (Timer, Interval, Cooldown, Flow, Stopwatch, Deadline, Timers bus)
        │
        └─► tick with TickContext.dt / total_time each frame
```

Wall/mono “now” lives in `sdk/src/time.zig` (`nowNs`, `wallTimeMs` / `Sec` / `Ns`, Io variants; backed by `std.Io` from the `hikari_std` module). Temporal re-exports those as `hi.nowNs` / `hi.wallTimeMs` / … and as `hi.temporal.nowNs`. Session sim `dt` stays session-owned `Time` — do not call Zig `std.Io.Timestamp` from product code.

## Core types

| Type | Role |
|------|------|
| `Timer` | One-shot countdown; `after`, `tick`, `pulse` (finish edge) |
| `Interval` | Repeating period; `tick` → fire count (catch-up) |
| `Cooldown` | Rate limit; `ready` / `trigger` / `tick` |
| `Flow` | Multi-step routine; `tick` then `wait` / `waitUntil` / `waitFrame` / `onEnter` / `advance` |
| `Stopwatch` | Accumulator; start/pause/unpause |
| `Deadline` | Absolute threshold vs `total_time` |
| `Timers` | Dense keyed one-shot bus |

## Usage

### Single delay / cooldown

```zig
// Inside component Data. Runtime state is not authored or serialized.
state: struct {
    door_timer: hi.Timer = .after(0.5),
    fire_cd: hi.Cooldown = .init(0.35),
} = .{},

pub fn update(self: *@This(), _: hi.ActorContext, ctx: *const hi.TickContext) void {
    if (self.state.door_timer.pulse(ctx.dt)) { /* open */ }
    self.state.fire_cd.tick(ctx.dt);
    if (hi.world().actionPressed("fire") and self.state.fire_cd.trigger()) { /* shoot */ }
}
```

### Multi-step routine (`Flow`)

```zig
const Phase = enum(u32) { windup, sound, wait_player, open, _ };

state: struct { flow: hi.Flow = .begin() } = .{},

pub fn update(self: *@This(), actor: hi.ActorContext, ctx: *const hi.TickContext) void {
    const f = &self.state.flow;
    if (!f.running()) return;
    f.tick(ctx.dt);

    _ = f.wait(@intFromEnum(Phase.windup), 0.5);
    if (f.onEnter(@intFromEnum(Phase.sound))) {
        // play sound
        f.advance();
    }
    // Guard expensive predicates: ordinary Zig arguments evaluate eagerly.
    if (f.isAt(@intFromEnum(Phase.wait_player))) {
        _ = f.waitUntil(@intFromEnum(Phase.wait_player), playerNear(actor));
    }
    if (f.onEnter(@intFromEnum(Phase.open))) {
        // open door
        f.stop();
    }
}
```

Keyed session delays: `hi.Timers` (`after` / `tick` / `pulse` / `cancel`).

## Timing and lifetime contracts

Call `flow.tick(dt)` exactly once per owning update, before polling. `wait(step, seconds)` no longer accepts `dt`. A flow belongs to one clock and one update lane; do not drive the same value from both `update` and `fixedUpdate`.

- The first active positive-duration wait consumes the tick's elapsed-time budget, even if it does not finish. Repeated polls and subsequent waits cannot spend it again. Excess elapsed time is discarded, not carried to the next phase. A newly armed first wait may consume the current tick; this is a tick-budget helper, not an absolute-deadline scheduler.
- Zero/negative/non-finite durations complete immediately. They do not consume elapsed time. Positive waits capture their duration when armed; changing the argument while waiting does not retime them.
- `onEnter` can perform setup before a wait on the same phase. Each phase uses one wait kind; changing between timed/frame/condition waits requires `goto` or `advance`.
- `waitFrame` completes only after a later `tick`, regardless of repeated polling. It means the next **owner update**, which may be throttled, and is not a rendered-frame fence. `tick(0)` still establishes a new owner update; it freezes positive timed waits but does not suppress frame waits or conditions. Skip the entire flow update to pause all progress. Ordinary gameplay callbacks already respect the World simulation gate.
- `advance` from the last usable phase stops. The reserved `Flow.stopped` value cannot execute actions or waits. `start`, `reset`, and `goto` reset phase entry and pending waits, but do not replenish elapsed time already spent this tick.
- `Flow` does not manage owner lifetime or run cleanup. Components must author disable/re-enable policy and resource cleanup in lifecycle hooks. Never retain `ActorContext` or borrowed sibling pointers in flow state. Loops over immediate phases are caller-owned and must be bounded.

`Timer.pulse(dt)` reports an active-to-finished edge only: an immediate timer is already done and never pulses. Use `done()` for level readiness. The keyed `Timers` bus deliberately retains one completion pulse, including immediate timers, until consumed, replaced, or cancelled. `Timers.active()` counts running countdowns, not pending pulses. Completed slots retain their keys until `release`/`cancel`; both operations recycle without allocating. `after` may allocate at capacity boundaries; reserve the maximum concurrent keys before hot-path use. Lookup and ticking remain linear in admitted slots; this is a small keyed table, use `hi.Routine` for engine-scheduled suspended routines.

Negative and non-finite deltas are ignored by countdowns, intervals, cooldowns, and stopwatches. Non-finite/negative durations normalize to zero. `Interval` clamps invalid or sub-epsilon periods to `floatEps(f32)`, computes catch-up arithmetically, and retains a double-precision fractional remainder. Its count saturates at `maxInt(u32)` and discards excess whole periods; game code must still bound work performed for returned counts. Stopwatches saturate at the largest finite `f32`. `Deadline` expects a finite advancing absolute clock; its delay follows the duration normalization rule.

## Validation

From `src/hikari`, run `zig build test-temporal -Dgame-src=<absolute-game-src>`. This suite is also included in `test-sdk` and `test`. It covers tick-budget reuse, immediate waits, repeated frame polling, phase/stop transitions, invalid time, interval catch-up, and timer allocation failures.

## Files

```text
src/hikari/sdk/src/temporal/
  temporal.zig   barrel
  clock.zig      unit helpers
  timer.zig      Timer, Interval, Cooldown, Stopwatch, Deadline
  flow.zig       Flow
  bus.zig        Timers
```
