Devices/Devices from Scripts

Devices from Scripts

Place your own laptop, computer or terminal for a mission, lend the tablet, run one app alone.

Devices pages

A mission, an event or a heist can put its own laptop, computer or terminal into the world for as long as it needs it, with its own app on it, and take it away again. The host does the rest as for any device: the prop, Use, the camera, the lock (one user at a time), the OS and what people nearby see.

spawnDevice

server.lua
local id, reason = exports.evolent_devices:spawnDevice({
    kind = 'laptop',                          -- 'laptop', 'pc' or 'terminal'
    coords = vector3(x, y, z),                -- the bottom centre of the model, on the table
    heading = 90.0,                           -- the heading of a player who stands at it
    label = 'Target laptop',
    apps = { 'mission_hack' },                -- app ids switched on on this device
    openApp = 'mission_hack',                 -- the OS opens it when the player starts using the device
    access = { mode = 'rules', rules = { { type = 'citizen', cid = cid } } },
    player = source,                          -- in the same space as this player
    temporary = true,                         -- the default: gone when your resource stops
})
FieldDefaultDescription
kindrequiredlaptop, pc or terminal. Tablets are held in hands, not placed.
coordsrequiredA vector3, a vector4 or a table with x, y, z (and w or h).
headingfrom coords, else 0In degrees.
labelnoneUp to 40 characters; the panel and getDevice show it.
appsnoneLaptop and computer: the apps that are on for this device. Terminal: one app, its page. { id = 'x', settings = {...} } works too.
openAppnoneAn app id; it is added to apps when missing. It opens only when the player may see it.
onlyAppnoneLaptop and computer: the device runs this app alone. See One app.
access'everyone', or 'owner' with an ownerWho may use it: 'everyone', 'owner', or a rule table as in the panel (citizen, job, gang, provider, and keys with a propertyId).
ownernoneA citizen id; the owner may always use it.
temporarytruefalse saves it as if an admin placed it: it stays over restarts until you or an admin remove it.
playernoneA server id: the device stands in that player's space (their home when they are in one, else their routing bucket). Without it the device is in the open world.
bucket, propertyId0, noneThe routing bucket, or the home it stands in; not together with player.

It answers the new id, or nil and a reason, and prints the reason once in the console.

Call it from a thread (an event handler, a callback or CreateThread): an owner lookup and a saved device wait for the database.

The host checks no table, slope or spacing, so place it where you know a table is, or let the player's client find one. It does pick a model no other device of that kind uses close by, so two screens nearby do not show the same page.

Your app follows the usual rules. Give it defaults = { kinds = { laptop = false } } so it shows only where your script switches it on, and canOpen to tie it to your mission. A new app from a script outside Config.trustedResources waits for Allow in the panel: until then the device still stands there with an empty desktop.

Temporary devices

  • Never saved. Their ids are far above the table's (over 1000000000) and start at a new place every time evolent_devices starts, so an id your script kept from before a restart finds nothing.
  • They go away by themselves when the resource that made them stops, and when evolent_devices restarts. Your resource hears evolent_devices:ready after such a restart: place the device again if your mission still runs.
  • Whoever is using one when it goes is taken off it.
  • The panel lists them with a "From your_resource" chip and a Temporary chip. Admins may rename them, change who may use them, close the session or remove them.

Letting a player pick the spot

On the client, placeDevice starts the same camera an admin uses in the panel: the ghost of the model, snap to a table, turn, height, the overlap and slope notes.

client.lua
local started, reason = exports.evolent_devices:placeDevice({ kind = 'laptop', accent = '#22C55E' }, function(spot, why)
    if not spot then return end -- why is 'cancelled'

    TriggerServerEvent('mission:place', spot) -- { x, y, z, h, surface }
end)

It answers true when the camera starts, or false and busy, invalid_kind or leave_vehicle. deviceId moves the ghost of a placed device instead. accent draws the ground markers in your colour. Release your own NUI focus first.

The spot is only a wish of the client: check the player's rights, the distance and the place on your server before you call spawnDevice or moveDevice.

moveDevice, removeDevice and getDevice

  • moveDevice(id, coords, heading) moves a device your resource placed: true, or false with unknown_device, not_your_device, invalid_position, device_in_use, overlap or device_changed. The device keeps its id, apps and rules.
  • removeDevice(id) takes away a device your resource placed: true, or false with unknown_device, not_your_device or device_changed. A saved device goes as a panel removal does (restorable for removedDays).
  • getDevice(id) works for any device and answers a copy: { id, kind, label, state, coords, heading, space, enabled, temporary, resource, owner, serial, apps, page, openApp, onlyApp, inUse }, where inUse is { source, name, since } or nil and space is 'b0' for the open world.

Events

Server events, local only (AddEventHandler, clients cannot fake them). They fire for every device: compare the id.

  • evolent_devices:deviceOpened(source, deviceId, info): a player started using a device; info = { kind, label, temporary, resource, cid, name }.
  • evolent_devices:deviceClosed(source, deviceId, reason): they stopped. device_closed (they left), device_reopened, device_timeout, device_left, device_too_far, access_lost, removed, disabled, admin, kind_off and the other close reasons.

A mission, step by step

Register the app

On start and on evolent_devices:ready, register the app mission_hack: laptops only, off by default, canOpen only for the player whose mission laptop it is.

Place the laptop

On /evmission the server notes the mission first (so a second /evmission is refused), the player's client finds the table in front of them, and the server checks the spot is close and calls spawnDevice with temporary = true, player = source, openApp = 'mission_hack' and a citizen rule for that player.

Use it

deviceOpened fires and the OS opens Hack. The app's onOpen hands the page the code and the time left. The page posts the typed code; the server callback answers only when appUser(source, 'mission_hack') says the app is open on the mission laptop (ctx.device.id).

Clean up

Right code: removeDevice(id) a moment later. Out of time, three wrong codes or /evmission stop: the same. deviceClosed tells the player the laptop is still there when they walk away early.

One app on a laptop or computer

A laptop or computer can run one app alone, like a terminal runs its page: a police records computer, a shop register, a mission laptop. Admins switch it on in the panel (the device page, Apps on it: Open only one app). Scripts pass onlyApp to spawnDevice, or call setOnlyApp(id, appId) for a device they placed (nil clears it). The app may be yours or a built-in one (notes, files, photos, settings), not the Store.

What the player gets:

  1. The lock screen and, when the device has one, the password.
  2. Then the app opens by itself and fills the whole screen: no desktop icons, no taskbar, no launcher, no notification centre, no other window.
  3. Closing it (the close button, an Escape your page does not use, os.close(), os.leave() or closeApp with no reason) leaves the device. closeApp with a reason shows the error page with that reason.
  4. When the app cannot open, the player sees the app's error page full screen with Leave, never the desktop. Retry asks the server again.
  5. People nearby see the same app full screen, or the same error page.

On the server the session is tied to that app: opening any other app answers single_app. setOnlyApp answers true, or false with unknown_device, not_your_device, invalid_only_app, only_app_kind, unknown_app, app_stopped, app_wrong_kind, app_not_player_devices, app_off or device_changed. A change applies from the next time someone uses the device.

Lending the tablet

A script whose app runs on the tablet can hand a player the Evolent tablet without the evolent_tablet item, for example when the player uses its own item:

server.lua
exports.evolent_devices:openTablet(source, { app = 'dealership', intent = { view = 'tablet' } })

The player's client takes the tablet out and opens your app with the intent (os.onIntent). The app must be yours (not_your_app) and support the tablet (app_wrong_kind); the intent is a table of at most 2 KB (invalid_intent). The grant is used once and lasts 15 seconds.

A lent tablet shows and opens only your app, with no Store and no Settings. Who gets it is up to your script: the use rules of the evolent_tablet item do not apply. When an admin switches tablets off, a lent tablet is refused too. A player who may take the tablet anyway (an admin, or everyone while Config.debug is on) gets the whole tablet with your app opened.

The 3D studio on a terminal

A terminal app can show a real car instead of a picture. While the user stands at the terminal, their camera moves to a hidden studio high over the sea, a copy of the car stands there (made on the user's client only, never networked) and the page is drawn over it with a see-through window where the car shows. People nearby keep seeing the page in the monitor. The studio is set in Config.devices.studio.

Check the studio

os.studio is the first view when the host offers the studio on this screen ({ yaw, pitch, zoom, zoomMin, zoomMax, spin }), else null. It is never set in watch mode.

Name the cars

On appOpened your client names the cars the page may ask for and their colours. Only the resource whose page is on the screen may call it; up to 80 cars.

client.lua
exports.evolent_devices:studioCars(
    { { key = 'msultan', model = 'sultan', paints = { 'red' } } },
    { { id = 'red', primary = 27, secondary = 27 } }
)

Send actions

The page sends actions with os.studioSend({ act: ... }):

  • { act: 'window', x, y, w, h }: the see-through window in page pixels (send it after layout and on resize).
  • { act: 'car', key }: show this car.
  • { act: 'view', yaw, pitch, zoom }, { act: 'reset' }: turn and zoom (pitch 2 to 65 degrees).
  • { act: 'door', part, open } with part front, rear, bonnet or boot; { act: 'lights', on }; { act: 'paint', id }.

Listen

The page hears { type: 'studio', state, key } with state loading, on, off or failed, and whenever the car changes { type: 'studioCar', key, doors, open, lights, paint, paints } through os.onMessage. Hide the picture under the window only while the state is on. Leaving the terminal ends the studio.