Skip to content

Camera and screen: the view is something you have to give back - #2

Merged
dop42 merged 1 commit into
mainfrom
feat/view
Sep 20, 2026
Merged

dop42 merged 1 commit into
mainfrom
feat/view

Conversation

@dop42

@dop42 dop42 commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

Two modules that together cover "something other than the player owns the view".
Both hand a resource something it must return, and both fail in a way that looks like a
crash from the player's chair.

The release guarantee, which is the point

The platform already releases the view on resource stop, reload, an uncaught coroutine
error, death, an unreadable body, world exit, shutdown, destroy and supersede. Every
one of those fires on a generation that has already failed or gone away.
A live
caller is covered by none of them: a pcall-caught error, an early return down an
untested branch, a panel closed by the one path that forgets the hand-back.

Camera.Shot(id, options, body) closes exactly that gap — take, await the blend, run
the body under pcall, hand the view back on every exit, await the returning blend.
When it returns, the player has their own eyes back, on them rather than mid-blend.

A raise becomes shot_raised rather than an unwind, because propagating it after the
restore is the one ordering that loses the error and keeps the symptom. A rejected take
means the shot never happened, so the body does not run — it would draw over a view
somebody else now owns. Screen.Black is the same scope for the image.

The honest boundary is written into the header: a body that never returns is never left
by any path, and there the platform's own rules are the only backstop.

The promise that arrives second

Seven natives answer a promise as a second value, which Native.Call cannot carry —
it keeps the first. They are reached directly, and the promise rides on the Result under
a name that says what it settles on: .blend, .covered, .restored, .finished. A
cut carries none, and Await(nil) answers Ok so no call site needs the if.

Refusals that answer the question they leave you with

camera_held_by:<resource> is lifted onto .holder with no extra native call; a bare
camera_held spends one cameras() read to answer the same question; if the build will
say neither, the detail says that rather than inventing a holder.
camera_budget_exhausted names 16 and 64. screen_busy says the slot is shared with
the server relay and points at isFaded().

One source disagreement, recorded rather than resolved

The reference and the devkit's own example read cameras().held two incompatible ways —
"true only for you" versus "true for anybody". They cannot both be right and there is no
game here to settle it. State therefore decides on the holder name, compared
against our own, which is correct under either reading; held is consulted only when
there is no name, and answers a deliberately vague 'held'.

Verification

375 → 547 checks, 0 failed. Every one of the fourteen new blocks was mutation-tested
— the source broken once per block, the block confirmed red, then reverted. All six CI
gates run by hand.

Every native verified against the devkit at op77.76; nothing unverified. But the live
server runs 82+, so this is "verified at 76", not "verified live" — and no promise
rejection token, no camera_not_active tolerance and no fadeIn refusal has ever been
observed in a game.

🤖 Generated with Claude Code

Both namespaces hand a resource something it must return, and both fail in a
way that looks like a crash from the player's chair. The cameras guide opens
its release section with "a stuck camera is indistinguishable from a crash",
and the screen slot is single and shared with the server relay.

The platform already releases the view on stop, reload, an uncaught coroutine
error, death, an unreadable body, world exit, shutdown, destroy and supersede.
Every one of those fires on a generation that has already failed or gone away.
A LIVE caller is covered by none of them: a pcall-caught error, an early return
down an untested branch, a panel closed by the one path that forgets.

`Camera.Shot(id, options, body)` closes exactly that gap -- take, await the
blend, run the body under pcall, hand the view back on every exit, and await
the returning blend. When it returns the player has their own eyes back, on
them rather than mid-blend. A raise becomes `shot_raised` rather than an
unwind, because propagating it after the restore is the one ordering that loses
the error and keeps the symptom. A rejected take means the shot never happened,
so the body does not run -- it would draw over a view somebody else now owns.
`Screen.Black` is the same scope for the image.

The honest boundary is in the header: a body that never returns is never left,
and there the platform's own rules are the only backstop.

Seven natives answer a promise as a SECOND value, which `Native.Call` cannot
carry -- it keeps the first. They are reached directly, and the promise rides
on the Result under a name that says what it settles on: `.blend`, `.covered`,
`.restored`, `.finished`. A cut carries none, and `Await(nil)` answers Ok so no
call site needs the `if`.

Refusals name what the platform leaves you asking: `camera_held_by:` is lifted
onto `.holder` with no extra call, a bare `camera_held` spends one read to
answer the same question, and `camera_budget_exhausted` names 16 and 64.

One source disagreement is recorded rather than resolved: the reference and the
devkit's own example read `cameras().held` two incompatible ways. `State`
therefore decides on the holder NAME, which is correct under either.

547 checks, 0 failed. All fourteen new blocks mutation-tested.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
@dop42
dop42 merged commit a42c2a5 into main Sep 20, 2026
1 check passed
@dop42
dop42 deleted the feat/view branch September 20, 2026 00:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant