Documentation

Can-Check Hooks

Hook functions that run before laptop actions are executed. Return false to deny with an optional reason.

Can-check hooks allow you to control player actions in zyke_laptop before they execute. Return false with an optional locale key to deny an action and notify the player. A few hooks answer a question instead, such as which placement area a spot is in; their return values are described with each hook.

Unless noted otherwise, hooks allow everything by default.

#File Locations

SideFile
Serverserver/unlocked/hooks.lua
Clientclient/unlocked/hooks.lua

Both files are unencrypted, ignored by escrow, and safe to edit directly. Edit the existing functions inside the laptop.

Types / Classes

All types / classes can be found in types/*.lua. plyId is the requesting player's server ID. ownerId is a laptop id: the holder's server id for a held laptop, or a placed laptop's negative id. Slot ids are board, cooler, ram0, ram1, ssd, wifi, and battery.


#How Reason Strings Work

#Server-side reasons

The server sends the reason to the client, which shows it as a notification or in the OS, so use a locale key. Each hook falls back to its own key when no reason is given:

HookDefault key
CanPlaceLaptoplaptopPlaceNotAllowed
CanPlaceComputercomputerPlaceNotAllowed
CanRemoveComputercomputerRemoveNotAllowed
CanUseLaptopSkinsettings:skinNotAllowed in Settings, shopNotAllowed at the shop
CanJoinHotspotwifi:cannotJoin
CanPlugLaptopDevice, CanUnplugLaptopDeviceports:notAllowed
CanCastToScreencast:error:notAllowed
CanServiceLaptop, CanInstallPart, CanRemoveParthardware:service:notAllowed
CanUseCasinocasino:error:notAllowed
CanUseMessengermessengerServer:notAllowed
CanBuyLaptopShopItemshopNotAllowed
CanOpenLaptopStoragestorageNotAllowed

#Client-side reasons

CanUseLaptop also returns a locale key and falls back to laptopCantUse. It gives immediate feedback; enforce trusted restrictions in the server hooks. Add custom reason keys to every locale file in locales/.

This server-side example only lets the Digital Den job plug devices into laptops that are not their own.

Example:

lua
---@param plyId integer
---@param ownerId integer
---@param portId string
---@param itemName string
---@param metadata table
---@return boolean allowed
---@return string? reason
function CanPlugLaptopDevice(plyId, ownerId, portId, itemName, metadata)
    if (exports["zyke_laptop"]:IsLaptopOwner(plyId, ownerId)) then return true end
    if (Z.isJob(plyId, "digitalden")) then return true end

    return false, "ports:notAllowed"
end

#Execution & Validation

Keep hooks synchronous and free of side effects. Returning true continues the normal flow; built-in reach, job, limit, item, and power checks still apply. Hooks marked keep it fast run very often, so never wait on anything in them.

Dead, downed, and cuffed players are kept off the laptop by IsPlayerIncapacitated before any other hook is asked. Admin commands such as laptop_computer skip the player hooks where noted.


#Server-Sided Hooks

All server-sided hooks are in server/unlocked/hooks.lua.

#Is Player Incapacitated

Return true while a player can't use a laptop: dead, downed, or cuffed. The server then refuses to take a laptop out, put one down, pick one up, or open its lid, screen, ports, or service view, and IsAtLaptop returns false. The client hook of the same name is asked as well; this one covers what the server knows itself.

By default it checks handcuffs on the ped and QBCore's isdead, inlaststand, and ishandcuffed metadata. Replace or extend it for your own death or restraint script.

Example:

lua
---@param plyId integer
---@return boolean incapacitated
function IsPlayerIncapacitated(plyId)
    local ped = GetPlayerPed(plyId)
    if (ped ~= 0 and IsPedHandcuffed(ped)) then return true end

    -- A state bag your own death script sets
    if (Player(plyId).state.isDowned == true) then return true end

    local player = Z.getPlayerData(plyId)
    local metadata = type(player) == "table" and type(player.PlayerData) == "table" and player.PlayerData.metadata
    if (type(metadata) ~= "table") then return false end

    return metadata.isdead == true or metadata.inlaststand == true or metadata.ishandcuffed == true
end

#Get Laptop Placement Area

Returns the area a laptop is put down in and how many laptops one character may have put down there, for rooms with a limit of their own such as a crypto farm's server room or a housing interior. Laptops in an area count only against that area's limit, never against placement.maxPerCharacter or another area's. Return nil for anywhere else, where maxPerCharacter applies.

The area id is text of up to 64 characters. The limit is a number, or false for no limit, so returning any area id with false lifts the limit wherever you decide. A laptop keeps the area it was put down in until it is picked up.

Asked as a laptop is put down, after the reach check and before CanPlaceLaptop. By default it returns the first matching area in Config.Settings.placement.areas.

Example:

lua
---@param plyId integer
---@param coords vector4 @ The spot picked, w is the heading
---@param bucket integer @ The player's routing bucket
---@return string? areaId
---@return integer | false | nil limit
function GetLaptopPlacementArea(plyId, coords, bucket)
    -- my_mining stands for your own resource
    local room = exports["my_mining"]:GetRoomAt(coords.xyz, bucket)
    if (room) then return "mining:" .. room.id, room.maxLaptops end

    local area = FindLaptopPlacementArea(coords.xyz, bucket)
    if (not area) then return nil end

    return area.id, area.limit
end

#Can Place Laptop

Called before a player puts a laptop down, after the reach and placement limit checks. Use it to keep laptops off other players' property or out of an event's bucket.

Example:

lua
---@param plyId integer
---@param coords vector4 @ The spot picked, w is the heading
---@param bucket integer @ The player's routing bucket, the laptop stands in it
---@return boolean allowed
---@return string? reason
function CanPlaceLaptop(plyId, coords, bucket)
    -- Bucket 500 is an event instance on this server
    if (bucket == 500) then return false, "laptopPlaceNotAllowed" end

    return true
end

#Can Place Computer

Called before a player sets up a desktop computer from its item, after the job and computer.maxPerCharacter checks. Staff setting one up with the laptop_computer command skip it.

Example:

lua
---@param plyId integer
---@param coords vector4 @ The spot picked, w is the heading
---@param bucket integer
---@return boolean allowed
---@return string? reason
function CanPlaceComputer(plyId, coords, bucket)
    -- Only inside the Mission Row station
    if (#(coords.xyz - vector3(441.0, -982.0, 30.7)) > 40.0) then return false, "computerPlaceNotAllowed" end

    return true
end

#Can Remove Computer

Called before a player takes a desktop computer away, once they are the character who set it up or staff. Use it to keep a business's own computers in place.

Example:

lua
---@param plyId integer
---@param ownerId integer @ The computer's laptop id
---@return boolean allowed
---@return string? reason
function CanRemoveComputer(plyId, ownerId)
    local laptop = exports["zyke_laptop"]:GetLaptop(ownerId)

    -- Computers in the station stay where they are
    if (laptop and laptop.coords and #(laptop.coords.xyz - vector3(441.0, -982.0, 30.7)) < 40.0) then return false, "computerRemoveNotAllowed" end

    return true
end

#Is Laptop Bucket Persistent

Return true for a routing bucket laptops stay put down in across restarts. A laptop in any other bucket stays only while the character who put it down is in that bucket, and goes back into their inventory after that. Return true for buckets that always mean the same place, such as housing that uses the property id as the bucket, never for instances handed out as needed, whose numbers are reused for strangers.

Defaults to Config.Settings.placement.persistentBuckets. Keep it fast: it is asked every two seconds for each placed laptop.

Example:

lua
---@param bucket integer
---@return boolean persistent
function IsLaptopBucketPersistent(bucket)
    -- Housing uses buckets 1000 to 1999, one per property
    if (bucket >= 1000 and bucket < 2000) then return true end

    local buckets = Config.Settings.placement.persistentBuckets or {0}

    for i = 1, #buckets do
        if (buckets[i] == bucket) then return true end
    end

    return false
end

#Can Use Laptop Skin

Called before a player puts a backplate skin on their laptop in Settings, before a skin is bought at the shop, and for the skin kept on the client when laptops have no item. Use it to keep some skins for donors or a job.

Example:

lua
---@param plyId integer
---@param skinId? string @ Id listed under Config.Settings.skins, nil for the stock lid
---@return boolean allowed
---@return string? reason
function CanUseLaptopSkin(plyId, skinId)
    if (skinId == "carbon_gold" and not IsPlayerAceAllowed(tostring(plyId), "zyke_laptop.donor")) then return false, "settings:skinNotAllowed" end

    return true
end

#Can Join Hotspot

Called when a player joins a Wi-Fi hotspot, after the range and password checks. Use it to keep a station's network for its job.

Example:

lua
---@param plyId integer
---@param hotspotId string @ Id from shared/unlocked/hotspots.lua
---@return boolean allowed
---@return string? reason
function CanJoinHotspot(plyId, hotspotId)
    if (hotspotId == "mission_row_pd" and not Z.isJob(plyId, "police")) then return false, "wifi:cannotJoin" end

    return true
end

#Can Plug Laptop Device

Called before a device goes into a laptop's port, from the port view and the PlugDevice export. metadata is a copy of the item's metadata. See the example under How Reason Strings Work.

Example:

lua
---@param plyId integer
---@param ownerId integer @ Laptop id
---@param portId string @ Such as "usba"
---@param itemName string
---@param metadata table
---@return boolean allowed
---@return string? reason
function CanPlugLaptopDevice(plyId, ownerId, portId, itemName, metadata)
    return true
end

#Can Unplug Laptop Device

Called before a device is pulled out of a laptop's port, from the port view and the UnplugDevice export. Anyone at a placed laptop can pull a device out by default, as with a real laptop.

Example:

lua
---@param plyId integer
---@param ownerId integer @ Laptop id
---@param portId string
---@param itemName string
---@param metadata table @ A copy of the device's metadata
---@return boolean allowed
---@return string? reason
function CanUnplugLaptopDevice(plyId, ownerId, portId, itemName, metadata)
    -- Only the owner pulls a power bank out
    if (itemName == "laptop_powerbank" and not exports["zyke_laptop"]:IsLaptopOwner(plyId, ownerId)) then return false, "ports:notAllowed" end

    return true
end

#Can Cast To Screen

Called when a cast starts, after the cable and range checks. Use it to keep a briefing room's screen for the police.

Example:

lua
---@param plyId integer
---@param screenId string @ "config:<id>" for a screen in the config, "placed:<number>" for one placed with the creator
---@return boolean allowed
---@return string? reason
function CanCastToScreen(plyId, screenId)
    -- mrpd_briefing is a screen added to Config.Settings.cast.screens
    if (screenId == "config:mrpd_briefing" and not Z.isJob(plyId, "police")) then return false, "cast:error:notAllowed" end

    return true
end

#Can Create Screen

Return true to let a player place, change, and delete screens with the screen creator. Asked on every change the creator makes. By default the players with the creator's ace permission, Config.Settings.cast.creator.permission.

Example:

lua
---@param plyId integer
---@return boolean allowed
function CanCreateScreen(plyId)
    return Z.hasPermission(plyId, Config.Settings.cast.creator.permission) or Z.isJob(plyId, "realestate")
end

#Can Service Laptop

Called before a player opens the service view of a laptop they didn't put down. Only asked while hardware.service.others is on; owners always may. Use it to leave repairs to a repair shop's job.

Example:

lua
---@param plyId integer
---@param ownerId integer @ Laptop id
---@return boolean allowed
---@return string? reason
function CanServiceLaptop(plyId, ownerId)
    if (not Z.isJob(plyId, "digitalden")) then return false, "hardware:service:notAllowed" end

    return true
end

#Can Install Part

Called before a part goes into a laptop, in the service view and through the SetLaptopHardwareSlot export. metadata is a copy of the part's metadata. When called through the export, ownerId is the player's own server id.

Example:

lua
---@param plyId integer
---@param ownerId integer @ Laptop id
---@param slotId string
---@param itemName string
---@param metadata table
---@return boolean allowed
---@return string? reason
function CanInstallPart(plyId, ownerId, slotId, itemName, metadata)
    -- Boards only go in at the repair shop
    if (slotId == "board" and not Z.isJob(plyId, "digitalden")) then return false, "hardware:service:notAllowed" end

    return true
end

#Can Remove Part

Called before a part comes out of a laptop, in the service view and through the SetLaptopHardwareSlot export.

Example:

lua
---@param plyId integer
---@param ownerId integer @ Laptop id
---@param slotId string
---@param itemName string
---@param metadata table @ A copy of the part's metadata
---@return boolean allowed
---@return string? reason
function CanRemovePart(plyId, ownerId, slotId, itemName, metadata)
    if (slotId == "board" and not Z.isJob(plyId, "digitalden")) then return false, "hardware:service:notAllowed" end

    return true
end

#Can Use Casino

Called before every casino action. Use it to require a job, a licence, or your own age rule; Config.Settings.casino.minAge is checked separately. Keep it fast.

Example:

lua
---@param plyId integer
---@return boolean allowed
---@return string? reason
function CanUseCasino(plyId)
    -- my_licences stands for your own licence resource
    if (exports["my_licences"]:HasLicence(plyId, "gambling")) then return true end

    return false, "casino:error:notAllowed"
end

#Can Post Job Listing

Return true to let a player post listings on the Hireline job board and handle the applicants for their current job. ranked is whether they hold one of the job's boss grades or the grade in Config.Settings.jobs.posting.grades; return it as it is to keep that rule. Jobs in posting.excluded never get here. Asked on every employer action. Keep it fast.

Example:

lua
---@param plyId integer
---@param jobName string
---@param ranked boolean
---@return boolean allowed
function CanPostJobListing(plyId, jobName, ranked)
    -- The government posts through the mayor's office only
    if (jobName == "government") then return Z.getJob(plyId).grade.level >= 4 end

    return ranked
end

#Can Use Messenger

Called when Natter starts its session and before every request. Use it to require a phone item or a job. Keep it fast.

Example:

lua
---@param plyId integer
---@return boolean allowed
---@return string? reason
function CanUseMessenger(plyId)
    if (Player(plyId).state.inJail == true) then return false, "messengerServer:notAllowed" end

    return true
end

#Can Read Messenger Metadata

Return false to stop a player from reading who messaged whom and when through the GetMessengerMetadata export. Message content is never part of it. Every request is allowed by default, since only server resources can call the export.

Example:

lua
---@param plyId integer @ The player asking, such as an officer
---@param number string @ The account looked into
---@return boolean allowed
function CanReadMessengerMetadata(plyId, number)
    return Z.isJob(plyId, "police")
end

#Can Buy Laptop Shop Item

Called before something is bought at the shop, before the money is taken. Use it to keep flagship laptops for donors or parts for a repair job. A new laptop bought with a backplate asks again for the skin:<id> offer.

Example:

lua
---@param plyId integer
---@param offerId string
---@return boolean allowed
---@return string? reason
function CanBuyLaptopShopItem(plyId, offerId)
    if (offerId:find("^laptop:flagship:") and not IsPlayerAceAllowed(tostring(plyId), "zyke_laptop.donor")) then return false, "shopNotAllowed" end

    return true
end

The offer ids offerId takes:

OfferId
A laptop in its stock finishlaptop:<prebuilt>:stock
A laptop in a finishlaptop:<prebuilt>:<finish>, such as laptop:performance:midnight
Refinishing a laptoprefinish:stock, refinish:<finish>
A backplateskin:<skinId>
A partpart:<item>:<model>, such as part:laptop_ram:d5_16
A tooltools:<item>, such as tools:thermal_paste
An accessoryaccessories:<item>, such as accessories:usb_drive

#Can Open Laptop Storage

Called every time a player opens one of the shop's storages, after the storage's jobs are checked. Use it to require being on duty or a rank.

Example:

lua
---@param plyId integer
---@param storageId string @ The storage's id in Config.Shop.storages
---@return boolean allowed
---@return string? reason
function CanOpenLaptopStorage(plyId, storageId)
    -- The stock room is for managers and up
    if (storageId == "digitalden_stock" and Z.getJob(plyId).grade.level < 3) then return false, "storageNotAllowed" end

    return true
end

#Send Laptop Cloud Alert

Texts a character about their Nimbus account while Config.Settings.cloud.alerts is on. By default it sends a phone text through the phone bridge; without a phone resource that takes texts nothing is sent. Replace it to send these through your own phone, mail, or notification resource. It runs in its own thread, so it may yield.

kind is newDevice (a laptop was linked), resetAsked, signedOut (a laptop left the account), or reset. data.device is the laptop's name for newDevice and signedOut.

Example:

lua
---@param identifier string @ The account's character, who may be offline
---@param kind "newDevice" | "resetAsked" | "signedOut" | "reset"
---@param data {device?: string}
function SendLaptopCloudAlert(identifier, kind, data)
    local texts = {
        newDevice = T("cloud:server:alert:newDevice", {data.device}),
        resetAsked = T("cloud:server:alert:resetAsked"),
        signedOut = T("cloud:server:alert:signedOut", {data.device}),
        reset = T("cloud:server:alert:reset"),
    }

    -- my_mail stands for your own mail resource
    exports["my_mail"]:SendMail(identifier, T("cloud:server:sender"), texts[kind])
end

#Client-Sided Hooks

All client-sided hooks are in client/unlocked/hooks.lua.

#Can Use Laptop

Called before the player takes a laptop out, uses, picks up, or services a placed one, opens its ports, sets a computer up or takes one away, and when another resource opens the screen with the OpenScreen export. IsPlayerIncapacitated is checked first.

By default it refuses while swimming, and in a vehicle for every action but screen, so a resource's own computer can be in a car, such as a police MDT.

Example:

lua
---@param action "takeOut" | "use" | "pickUp" | "ports" | "service" | "screen" | "placeComputer" | "removeComputer"
---@return boolean allowed
---@return string? notification
function CanUseLaptop(action)
    local ped = PlayerPedId()
    if (IsPedSwimming(ped)) then return false, "laptopCantUse" end
    if (action ~= "screen" and IsPedInAnyVehicle(ped, true)) then return false, "laptopCantUse" end

    -- my_minigames stands for your own resource
    if (exports["my_minigames"]:IsPlaying()) then return false, "laptopCantUse" end

    return true
end

#Is Player Incapacitated

Return true while the player can't use a laptop with their hands: dead, downed, or cuffed. It is checked before they start using a laptop, a few times a second while one is held, used, serviced, or its ports are open, and once a second while one is near. A laptop in use is put away or closed as soon as it turns true, and the server is told so it refuses its own entry points meanwhile. Keep it fast.

By default it checks death and cuffs on the ped, Z.isPlayerDead, which covers QBCore's last stand, wasabi, sky, osp, and p_ambulancejob, and QBCore's ishandcuffed metadata. Replace or extend it for a death or restraint script zyke_lib doesn't know.

Example:

lua
---@return boolean incapacitated
function IsPlayerIncapacitated()
    local ped = PlayerPedId()
    if (IsEntityDead(ped) or IsPedCuffed(ped)) then return true end
    if (Z.isPlayerDead()) then return true end

    -- A state bag your own restraint script sets
    if (LocalPlayer.state.ziptied == true) then return true end
    if (Framework ~= "QB") then return false end

    local player = Z.getPlayerData()
    local metadata = player and player.metadata

    return type(metadata) == "table" and metadata.ishandcuffed == true
end