Quick navigation

Go to

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.

7 min read1,621 wordsMarkdown
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 why isFaded() 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 fade only. glitch returns unsupported_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

// Guides Β· Lua API

Search the docs

1,905 guides and functions indexed

Try