Personal Vehicle Captures
Integrate player-owned vehicle captures safely.
Personal vehicle captures let another server resource open Image Studio for one authoritative vehicle record. The integration owns access checks and persistence; Image Studio owns the isolated capture session, processing queue, temporary upload credential, output rules, and Media Studio URL validation.
Set Config.PlayerCapture.enabled = true to allow requests. There is no public player command for starting this flow.
Create sessions only from trusted server code. Do not let a client choose the vehicle record, model, stored properties, owner, reference, destination field, or completion side effects.
#Integration Flow
- The calling server resource authorizes the player and loads the vehicle record.
- It calls
requestPlayerVehicleCapturewith a server-owned reference, model, and properties. - Image Studio opens the vehicle in an isolated capture session.
- The player captures and edits the image.
- The player supplies a temporary
upload.createtoken for the destination Media Studio workspace. - Image Studio queues final processing, uploads the image, and validates the returned CDN URL.
- Image Studio emits
zyke_imagestudio:PlayerVehicleCaptureCompleted. - The calling resource correlates the session, re-fetches the vehicle, reauthorizes the player, and saves the URL.
zyke_imagestudio:PlayerVehicleCaptureEndedclears any remaining integration state when the session ends.
#Request a Session
Authorize the player and load authoritative vehicle properties before calling the server export:
lua---@type table<string, {plyId: integer, reference: string}>
local pendingCaptures = {}
---@param plyId integer
---@param vin string
---@param vehicleModel string
---@param vehicleProperties table
---@param displayLabel? string
local function requestVehicleImage(plyId, vin, vehicleModel, vehicleProperties, displayLabel)
-- Perform the integration's ownership / permission check before this call
local result = exports["zyke_imagestudio"]:requestPlayerVehicleCapture(plyId, {
reference = vin,
model = vehicleModel,
vehicleProperties = vehicleProperties,
label = displayLabel,
})
if (not result.success) then
print(("Image Studio request failed: %s"):format(result.reason))
return
end
pendingCaptures[result.sessionId] = {
plyId = plyId,
reference = vin,
}
end
#Request Fields
| Field | Required | Contract |
|---|---|---|
reference | Yes | Non-empty server-owned correlation value, at most 128 characters. A VIN or internal vehicle ID is recommended. |
model | Yes | Vehicle model name, at most 64 characters. Image Studio sanitizes it for the capture filename. |
vehicleProperties | Yes | Authoritative server-loaded properties table. The detached JSON snapshot is limited to 65,536 bytes. |
label | No | Player-facing vehicle label, at most 96 characters. Defaults to the sanitized model. |
The player ID must identify a connected player. Only one personal capture session can be active for that player.
#Result
A successful result contains the opaque session ID:
lua{
success = true,
sessionId = "..."
}
Failures return success = false with one of these reasons:
| Reason | Meaning |
|---|---|
disabled | Config.PlayerCapture.enabled is not enabled. |
hosting_unavailable | The Media Studio capture transport has not finished starting. |
player_unavailable | The player is not connected or has no usable identifier. |
session_active | The player already has a personal capture session. |
invalid_request | A required field, length, type, model, or property snapshot is invalid. |
#Processing Queue
Capture processing and final-image rendering share the player FIFO queue.
Config.PlayerCapture.processingConcurrencycontrols active stages.maxQueuedProcessingcontrols how much work can wait.processingTimeoutSecondsbounds one active lease.sessionInactivityMinutesremoves abandoned sessions.
Editing remains client-side and does not hold a processing slot. A queue-full or processing-timeout result leaves saved vehicle data unchanged.
#Temporary Upload Token
The player creates a temporary Media Studio token with upload.create access in the workspace that should own the personal image. The NUI masks the token and sends it once for the active session after the player chooses to save.
The token is held only in the in-memory player capture service for the immediate final upload. It is not written to:
- Lua configuration
- KVP
- Database records
- Publish-job state
- Logs or analytics
- Generated files
Lua and JavaScript cannot cryptographically zero immutable strings, so the value necessarily exists briefly in NUI, event, and upload-call memory. The implementation limits its lifetime and does not persist, log, or replay it.
The server-wide zyke_imagestudio_host_token still handles normal Studio capture processing. It does not select the destination workspace for a personal capture and does not replace the player's temporary token.
#Output & Upload Behavior
Output is controlled by Config.PlayerCapture.output:
luaoutput = {
format = "webp",
width = 800,
quality = 95,
uploadSource = false,
}
The final image is never upscaled beyond the rendered dimensions. It uploads to the destination library root with versioning behavior for duplicate filenames.
When uploadSource = true, the transparent PNG source uploads second as <filename>_source. A failed source upload does not invalidate a successful final-image upload; the completion event receives sourceUrl = nil.
#Handle Completion
Image Studio emits the local server event only after the final Media Studio URL passes validation:
lua---@param sessionId string
---@param originResource string
---@param plyId integer
---@param reference string
---@param finalUrl string
---@param sourceUrl? string
AddEventHandler("zyke_imagestudio:PlayerVehicleCaptureCompleted", function(sessionId, originResource, plyId, reference, finalUrl, sourceUrl)
local pending = pendingCaptures[sessionId]
pendingCaptures[sessionId] = nil
if (originResource ~= GetCurrentResourceName()) then return end
if (not pending or pending.plyId ~= plyId or pending.reference ~= reference) then return end
-- Re-fetch reference from the database
-- Confirm plyId may still edit that vehicle
-- Save finalUrl only after both checks pass
end)
Do not trust completion only because the session started successfully. Ownership, role, vehicle state, or permission can change while the player captures and edits the image.
The event provides:
| Value | Meaning |
|---|---|
sessionId | Opaque Image Studio session used to match pending integration state. |
originResource | Resource that called the request export. |
plyId | Player who completed the capture. |
reference | Original server-owned correlation value. |
finalUrl | Validated final CDN URL. |
sourceUrl | Optional validated source CDN URL. |
#Handle Session Cleanup
Use the ended event to clear pending state after cancellation, inactivity expiry, upload failure cleanup, requesting-resource stop, or normal completion:
lua---@param sessionId string
---@param originResource string
---@param plyId integer
---@param reference string
AddEventHandler("zyke_imagestudio:PlayerVehicleCaptureEnded", function(sessionId, originResource, plyId, reference)
pendingCaptures[sessionId] = nil
end)
This event is cleanup notification only. It does not mean the image was uploaded; only the completed event carries a successful final URL.
#Trust Boundary
The integration can trust Image Studio to:
- Isolate and queue the capture stages.
- Apply the configured output format, width, quality, and source-upload behavior.
- Keep the temporary token non-persistent.
- Validate the returned Media Studio CDN URL.
- Correlate internal stages to one session.
The integration must still own:
- Entry authorization.
- Vehicle identity and authoritative properties.
- Pending-session correlation.
- Completion-time reauthorization.
- Database mutation and destination field.
- Any limits on how frequently a player may request captures.
A modified FiveM client can falsify rendered pixels. Treat the resulting image as presentation data, not proof of vehicle state, value, modifications, or ownership.