Cameras & cinematics
Native screen transitions
Use Cyberpunk's native screen fades from client Lua: fade out, hold and return, color options, completion events, resource ownership and automatic recovery. Includes server-coordinated transitions and HUD limitations.
On this page8 sections
Native screen transitions require client 2.31.13+op77.56 or later. They are client-local and do not require a matching server feature.
Open77.screen exposes Cyberpunk's native quest fade manager to client Lua.
It does not create a WebUI overlay, load a quest, apply a Relic status effect or
change network routing. This is a new client capability; on older clients first
check Open77.screen and Open77.screen.catalog exist, then inspect catalog().
Manifest and quick start
resource "transitions"
version "1.0.0"
client_script "client.lua"
permissions { "screen.effects" }
-- Fade to black, remain covered for 750 ms, then return to the game.
local id, reason = Open77.screen.transition("fade", {
durationMs = 500,
holdMs = 750,
fadeInMs = 500,
})
if not id then print("Transition unavailable: " .. reason) end
All durations are integer milliseconds, not seconds. Unknown options, NaN, infinity, numeric strings and values outside the documented ranges are rejected.
API
All eight functions require screen.effects. They are client-local. A server
can drive a fade directly with Open77.players.fade,
or ask a client resource to run a richer transition over an ordinary targeted
network event.
| Function | Result | Purpose |
|---|---|---|
Open77.screen.fadeOut(options?) |
id, promise or nil, reason |
Apply a native black/color fade and hold until this resource returns the image or the safety deadline expires. The promise settles when the screen is covered. |
Open77.screen.fadeIn(id, options?) |
true, promise or false, reason |
Return the image for this resource's active transition. Options: durationMs only. Can reverse an outgoing fade. The promise settles when the image is back. |
Open77.screen.transition("fade", options?) |
id, promise or nil, reason |
Run the complete fade/hold/return sequence. The promise settles when it has finished. |
Open77.screen.state(id) |
table or nil, reason |
Owner-scoped. Read this resource generation's active or recent completed transition. |
Open77.screen.isFaded() |
true/false or nil, reason |
Unowned. Is the screen covered right now, whoever covered it. |
Open77.screen.nativeState() |
table or nil, reason |
Unowned. The engine's own fade state: faded, fading, out, busy, ready, remainingMs. |
Open77.screen.cancel(id) |
true or false, reason |
Restore the image immediately, including during the return fade. |
Open77.screen.catalog() |
array or nil, reason |
Read supported presets and whether their native backend is available on this executable. |
IDs are opaque strings. They are not player IDs or native pointers. A different resource, another host or a reloaded generation cannot act on an old ID.
Owned and unowned reads
state(id) is about your transition; isFaded() and nativeState() are
about the screen. The second pair is what a spawn or teleport flow needs:
the screen may already be black because another resource faded it, because the
server did, or because a quest or loading fade took it. Stacking a second fade
on top of that is how a player ends up watching two fades in a row.
if not Open77.screen.isFaded() then
Open77.screen.fadeOut({durationMs = 400})
end
nativeState().fading separates on its way to black from already black, so
a caller that must not interrupt an incoming fade can wait instead of racing it.
Both return nil, reason rather than a confident false when the guarded
backend is unavailable: a wrong false here is exactly what stacks two fades.
Options
| Key | Default | Range / availability |
|---|---|---|
durationMs |
500 | 0β10000; outgoing fade, or the return duration passed to fadeIn. Zero is an instant cut. |
holdMs |
250 | 0β30000; transition only. Starts after the native outgoing fade completes. |
fadeInMs |
500 | 0β10000; transition only. |
timeoutMs |
At least 15000 | 1000β60000; the entire transition's real-time safety deadline. Must exceed planned outgoing/hold/return time by at least 500 ms. If omitted it grows to fit the sequence plus 2000 ms. |
color |
{r=0,g=0,b=0,a=255} |
Integer channels 0β255; alpha must be 255. Available on fadeOut and transition. Omitted channels keep their defaults. |
For example, color={r=255,g=255,b=255} selects the native white-tinted fade.
The engine controls its rendering: non-black colors can retain a blurred/tinted
scene rather than produce a flat, opaque RGB rectangle. Use black to conceal the
3D world, not a colored fade as an occlusion guarantee. Use flashes sparingly;
the API deliberately does not implement a repeating strobe preset.
Awaiting a fade
fadeOut, fadeIn and transition return an ordinary
Open77.Promise as a second value, so a caller can
fade, act, and fade back without hand-rolling a timer. The first return value is
unchanged: local id = Open77.screen.fadeOut(...) still works exactly as before.
CreateThread(function()
local id, covered = Open77.screen.fadeOut({durationMs = 400, timeoutMs = 8000})
if not id then return print(covered) end -- failure is still nil, reason
local state, reason = covered:await()
if not state then return print("fade ended early: " .. reason) end
-- The screen is black here, on the engine's own timing.
doSomethingInvisible()
Open77.screen.fadeIn(id, {durationMs = 400})
end)
The promise resolves with the same state table the event carries. It rejects
-- nil, reason from :await() -- if the transition ends any other way:
cancelled, timed out, a vanilla fade taking over, an error in this resource's
Lua, or the resource stopping. That is deliberate. A promise that could stay
pending forever would leave the coroutine that was going to call fadeIn
parked, and a fade nobody returns is indistinguishable from a crash to the
player looking at it.
cancel returns no promise: it is already terminal when it returns.
Fading a player from the server
Open77.players.fade(playerId, out, durationMs, options?) covers or uncovers one
player's screen from server Lua. Permission: players.screen.
-- Bracket a move. Requires players.screen (and whatever moves the player).
Open77.players.fade(source, true, 400)
Open77.players.fadeOut(source, 400, {timeoutMs = 8000}) -- the same call, named
Open77.players.fadeIn(source, 400)
The relay frame is consumed by the client host itself, so the target does not
need to run a client resource of its own -- which is the point: a teleport or
a spawn should not require every server to ship a companion script. It drives
the same native quest fade Open77.screen uses, through the same arbitration:
if a client resource already owns a fade, the server's request is refused rather
than stealing it, and the resource's own fadeIn/cancel continue to work.
options.timeoutMs (1000-60000, default 15000) is a ceiling the client
enforces on real time, not a promise the server keeps. A server that fades a
player out and then drops the route, crashes or simply forgets still gives the
image back. There is deliberately no "fade out forever" spelling of this call.
The frame is reserved transport: a resource holding only network.events cannot
send it by name, and a peer cannot forge one for another player.
Holding black during a server operation
Do not assume that receiving an ID means the screen is black. Wait for the
owner-scoped local event, request the server operation, then return the image
after the server acknowledges completion. Add network.events to the manifest
when using network events.
local transitionId
RegisterNetEvent("demo:prepareTransition", function()
local reason
transitionId, reason = Open77.screen.fadeOut({
durationMs = 600,
timeoutMs = 8000,
})
if not transitionId then print(reason) end
end)
AddEventHandler("open77:screen:black", function(state)
if state.id ~= transitionId then return end
TriggerServerEvent("demo:screenCovered")
end)
RegisterNetEvent("demo:operationComplete", function()
if transitionId then
Open77.screen.fadeIn(transitionId, {durationMs = 600})
end
end)
local function ended(state)
if state.id == transitionId then transitionId = nil end
end
AddEventHandler("open77:screen:finished", ended)
AddEventHandler("open77:screen:cancelled", ended)
AddEventHandler("open77:screen:failed", ended)
The server must validate the sender, the pending operation, destination and
permissions itself. A client's screenCovered message is not authorization
to teleport or change bucket. If the server never responds, the client safety
deadline restores the image. fadeIn does not extend that deadline.
Events and state
Subscribe with AddEventHandler, not RegisterNetEvent:
| Event | Meaning |
|---|---|
open77:screen:covered |
Native outgoing fade has completed, including non-black colors. |
open77:screen:black |
Same boundary, only when all RGB channels are zero. |
open77:screen:finished |
Native return fade has completed. |
open77:screen:cancelled |
Requested cancellation, safety timeout, world loss or a vanilla transition taking over. |
open77:screen:failed |
A native operation/restoration failed. |
The event argument and state(id) share this shape:
{
id = "12",
generation = 1,
phase = "covered", -- fading_out, covered, fading_in, finished, cancelled, failed
reason = "", -- completed, requested, timeout, native_interrupted, ...
backend = "native_quest_fade",
timing = "native_state",
automatic = false,
elapsedMs = 650,
remainingMs = 0, -- remaining native fade duration, not remaining hold time
timeoutMs = 8000,
black = true,
color = {r=0,g=0,b=0,a=255},
}
Completion comes from the native manager's direction and remaining duration,
not an estimated Lua Wait. It is not a GPU presentation fence or proof that
world streaming has completed. Hold and safety deadlines use monotonic real
time; pause/time dilation cannot extend the safety deadline while the game
continues ticking. No in-process API can recover while the entire game thread
is hung or suspended.
Only the owner generation receives these events, and only the owner generation
can read state(id). That is event and transition scoping, not secrecy
about the screen: isFaded() and nativeState() are unowned and report a fade
any resource, the server or the engine itself put up. Recent terminal states are
bounded to 64 entries globally; stopped resources lose their history. Errors in
a resource's Lua startup or coroutine, resource stop/reload, session teardown
and the safety deadline release its own fade. An intentional pcall-caught
Lua error is handled by your script; the safety deadline still applies.
Arbitration and limits
- Only one Open77 transition can be active: another request returns
screen_busy. That single slot is shared with the server relay, which is whyisFaded()exists -- check it before adding a fade of your own. - A fade/loading operation already owned by the game returns
native_screen_busy. - A new native game request takes priority. Open77 drops ownership and will not later clear that vanilla fade during cleanup.
- The backend is guarded for the verified Cyberpunk 2.31 executable. Unsupported
native layouts fail closed with
screen_unavailable; no raw pointer is exposed. - These are the quest manager's native transitions, including its own native UI and input supervision. The API does not request a teleport, damage, audio effect, camera change or network operation. The native minimap disappears during black, but Open77's CEF health HUD, watermark and command hints remain visible. Hide your own WebUI explicitly if your transition needs that; this API does not take ownership of other resources' interfaces.
- The catalogue currently contains
fadeonly.glitchreturnsunsupported_screen_preset: the loading-screen glitch has not yet been isolated safely. Existing GlitchBegone/Relic protections remain unchanged.
Common failures include permission_denied:screen.effects,
screen_unavailable_on_this_host, screen_unavailable, screen_busy,
native_screen_busy, invalid_screen_option:<key>, invalid_screen_color,
screen_timeout_too_short, not_owner, transition_not_active,
transition_not_found and already_fading_in.
A rejected promise carries the transition's own reason -- requested,
timeout, native_interrupted, world_unavailable, restore_failed -- or
resource_error / resource_stopped when the release came from this resource
failing or stopping. One reason is ours rather than the engine's:
never_covered, when a fadeOut promise's transition returned the image
without ever reaching black (the engine calls that completed, which would read
as success).
The server relay adds permission_denied:players.screen and
reserved_screen_event.
See also visual/audio effects, native HUD visibility
and docs/research/screen-transitions-and-fades.md.
Loading-screen integration
For the screen opened by a long-distance teleport or another native load, use custom loading screens: state queries, start/progress/end events, a WebUI cover and temporary HUD hiding. Quest fades and loading cycles are separate signals.
// For agents & LLMsthis page as Markdownllms.txtllms-full.txt
