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
| Side | File |
|---|---|
| Server | server/unlocked/hooks.lua |
| Client | client/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.plyIdis the requesting player's server ID.ownerIdis a laptop id: the holder's server id for a held laptop, or a placed laptop's negative id. Slot ids areboard,cooler,ram0,ram1,ssd,wifi, andbattery.
#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:
| Hook | Default key |
|---|---|
CanPlaceLaptop | laptopPlaceNotAllowed |
CanPlaceComputer | computerPlaceNotAllowed |
CanRemoveComputer | computerRemoveNotAllowed |
CanUseLaptopSkin | settings:skinNotAllowed in Settings, shopNotAllowed at the shop |
CanJoinHotspot | wifi:cannotJoin |
CanPlugLaptopDevice, CanUnplugLaptopDevice | ports:notAllowed |
CanCastToScreen | cast:error:notAllowed |
CanServiceLaptop, CanInstallPart, CanRemovePart | hardware:service:notAllowed |
CanUseCasino | casino:error:notAllowed |
CanUseMessenger | messengerServer:notAllowed |
CanBuyLaptopShopItem | shopNotAllowed |
CanOpenLaptopStorage | storageNotAllowed |
#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
truewhile 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, andIsAtLaptopreturnsfalse. 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, andishandcuffedmetadata. 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.maxPerCharacteror another area's. Returnnilfor anywhere else, wheremaxPerCharacterapplies.The area id is text of up to 64 characters. The limit is a number, or
falsefor no limit, so returning any area id withfalselifts 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 inConfig.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.maxPerCharacterchecks. Staff setting one up with thelaptop_computercommand 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
truefor 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. Returntruefor 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
PlugDeviceexport.metadatais 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
UnplugDeviceexport. 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
trueto 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.othersis 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
SetLaptopHardwareSlotexport.metadatais a copy of the part's metadata. When called through the export,ownerIdis 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
SetLaptopHardwareSlotexport.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.minAgeis 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
trueto let a player post listings on the Hireline job board and handle the applicants for their current job.rankedis whether they hold one of the job's boss grades or the grade inConfig.Settings.jobs.posting.grades; return it as it is to keep that rule. Jobs inposting.excludednever 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
falseto stop a player from reading who messaged whom and when through theGetMessengerMetadataexport. 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:
| Offer | Id |
|---|---|
| A laptop in its stock finish | laptop:<prebuilt>:stock |
| A laptop in a finish | laptop:<prebuilt>:<finish>, such as laptop:performance:midnight |
| Refinishing a laptop | refinish:stock, refinish:<finish> |
| A backplate | skin:<skinId> |
| A part | part:<item>:<model>, such as part:laptop_ram:d5_16 |
| A tool | tools:<item>, such as tools:thermal_paste |
| An accessory | accessories:<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
jobsare 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.alertsis 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.
kindisnewDevice(a laptop was linked),resetAsked,signedOut(a laptop left the account), orreset.data.deviceis the laptop's name fornewDeviceandsignedOut.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
OpenScreenexport.IsPlayerIncapacitatedis 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
truewhile 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 turnstrue, 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'sishandcuffedmetadata. Replace or extend it for a death or restraint scriptzyke_libdoesn'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