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 or what a domain costs; their return values are described with each hook.

Unless noted otherwise, hooks allow everything by default. Hooks for a feature are only asked while that feature is on, such as Config.Domains.enabled or Config.Racks.enabled.

#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. host is a domain such as mysite.com.


#How Reason Strings Work

#Server-side reasons

The server sends the reason to the client, which shows it as a notification or in the app, 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
CanRegisterDomaindomains:error:notAllowed
CanTransferDomaindomains:error:transferNotAllowed
CanUseDomainProviderdomains:error:providerNotAllowed
CanEditSitesites:error:editNotAllowed
CanPublishSitesites:error:publishNotAllowed
CanSubmitFormsites:form:error:notAllowed
CanBooksites:booking:error:notAllowed
CanChangeVehiclePlatedmv:error:notAllowed
CanUseCrypto, CanBuyCrypto, CanSellCrypto, CanSendCryptocrypto:error:notAllowed
CanUseMessengermessengerServer:notAllowed
CanUseMailmailServer:notAllowed
CanSendMail, CanCreateMailboxmail:error:notAllowed
CanBuyLaptopShopItemshopNotAllowed
CanOpenLaptopStoragestorageNotAllowed
CanStartHackhacking:reason:denied
CanPlaceRackracks:world:notAllowed
CanUseRackracks:world:notAllowed at the rack, racks:error:notAllowed in Hive
CanSellBusinessItemmdt:error:notAllowed

Hooks that return only a boolean, such as CanUseMdt or CanWriteListingReview, take no reason; the app shows its own refusal.

#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. A few hooks note that they may yield; the laptop reads its state again after 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, open its lid, screen, ports, or service view, or touch a rack, and IsAtLaptop returns false. The client hook of the same name is asked as well, through a state bag, and a player carrying a box off Digital Den's shelves is refused too; this hook covers what the server knows itself.

By default it checks handcuffs on the ped and Z.isPlayerDead. Replace or extend it for a death or restraint script zyke_lib doesn't know.

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

    return Z.isPlayerDead(plyId)
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; anything else counts as outside an area and prints a warning. 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. A computer set up in such a bucket tells the player it is temporary, and a laptop rack can't be set down there at all. 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 or fitted at the shop or the Digital Den counter, 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", see shared/locked/ports.lua
---@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; only exactly true allows it. 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. 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 @ 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. Only exactly true allows it. 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 Write Listing Review

Return false to stop a character from reviewing a listing on Ratewell, for example new characters. Asked after the laptop's own rules: the listing is active, it isn't one they manage, and they don't hold its job. allowed is therefore always true; return it as it is to keep the default. Only exactly true allows the review. Asked on every review written. Keep it fast.

listing is {id, kind, ref, slug, name}. kind is business, job, site, place, or player, and ref is the department id, job name, host, or place id.

Example:

lua
---@param plyId integer
---@param listing ReviewListingInfo
---@param allowed boolean
---@return boolean allowed
function CanWriteListingReview(plyId, listing, allowed)
    -- Police can't review their own station's competitors
    if (listing.kind == "business" and listing.ref == "sheriff" and Z.isJob(plyId, "police")) then return false end

    return allowed
end

#Can Manage Review Listing

Decides who manages a business or job listing on Ratewell: replies, reports, and edits. Only asked for players who hold one of the listing's jobs, at any grade. ranked is whether they hold a boss grade or reach Config.Reviews.owners.grades; return it as it is to keep that rule. Only exactly true allows it. Site, place, and player listings are run by their owner and never get here. Asked on every owner action. Keep it fast.

Example:

lua
---@param plyId integer
---@param listing ReviewListingInfo
---@param ranked boolean
---@return boolean allowed
function CanManageReviewListing(plyId, listing, ranked)
    -- Every Burger Shot employee may answer reviews
    if (listing.kind == "business" and listing.ref == "burgershot") then return true end

    return ranked
end

#Can Use MDT

Return false to keep a player out of their department's MDT, for example without a badge item or a certain licence. Asked once the player's job belongs to the department and its requireDuty passed; only exactly true lets them in. Asked on every MDT action. Keep it fast.

Example:

lua
---@param plyId integer
---@param department string @ A key of Config.Mdt.departments, such as "lspd"
---@return boolean allowed
function CanUseMdt(plyId, department)
    if (department == "lspd" and not Z.hasItem(plyId, "police_badge")) then return false end

    return true
end

#Can Use MDT Permission

Return true or false to change what a member may do in the MDT. allowed is the answer from the department's permissions in Config.Mdt, where boss grades have every permission; return it as it is to keep that rule. Only exactly true allows it. Asked once per permission and action, and the answer is kept for that action. Keep it fast.

Example:

lua
---@param plyId integer
---@param department string @ A key of Config.Mdt.departments
---@param permission MdtPermission @ Such as "reports.create" or "warrants.approve"
---@param allowed boolean
---@return boolean allowed
function CanUseMdtPermission(plyId, department, permission, allowed)
    -- Warrants need a judge's sign-off on this server
    if (permission == "warrants.approve") then return Z.isJob(plyId, "judge") end

    return allowed
end

#Can Register Domain

Called before a player registers a domain at Namestead, after the name, reserved, availability, and maxPerCharacter checks and before the money is taken. Use it to keep some endings for businesses or to require a licence. It may yield.

Example:

lua
---@param plyId integer
---@param host string @ Such as "mysite.com"
---@param mode "buy" | "rent"
---@return boolean allowed
---@return string? reason
function CanRegisterDomain(plyId, host, mode)
    if (host:find("%.gov$") and not Z.isJob(plyId, "government")) then return false, "domains:error:notAllowed" end

    return true
end

#Get Domain Price

Returns what a domain costs this player. price is the ending's price in Config.Domains.tlds with the premium for short names and common words; for rent it is the price of one period. Return a whole number of 0 or more; anything else uses price. Asked for every search result and again at purchase. Keep it fast.

Example:

lua
---@param plyId integer
---@param host string @ Such as "mysite.com"
---@param mode "buy" | "rent"
---@param price integer
---@return integer price
function GetDomainPrice(plyId, host, mode, price)
    -- Weazel News gets half off
    if (Z.isJob(plyId, "weazelnews")) then return math.floor(price / 2) end

    return price
end

#Can Transfer Domain

Called before an owner offers their domain to another character, after the receiver's domain limit is checked. The receiver still has to accept the offer. It may yield.

Example:

lua
---@param plyId integer
---@param host string @ Such as "mysite.com"
---@param targetId integer @ The player the domain is offered to
---@param price integer @ What the receiver pays, 0 for a gift
---@return boolean allowed
---@return string? reason
function CanTransferDomain(plyId, host, targetId, price)
    if (host:find("%.gov$")) then return false, "domains:error:transferNotAllowed" end

    return true
end

#Can Use Domain Provider

Called before an owner points their domain at a provider in My domains, and before a Sitewright site is published on a domain pointed elsewhere, with the provider id sitewright. Use it to keep a provider for a job.

Example:

lua
---@param plyId integer
---@param host string @ Such as "mysite.com"
---@param providerId string @ The id the providing resource registered with RegisterDomainProvider
---@return boolean allowed
---@return string? reason
function CanUseDomainProvider(plyId, host, providerId)
    -- my_news stands for a resource that registered a news site provider
    if (providerId == "my_news" and not Z.isJob(plyId, "weazelnews")) then return false, "domains:error:providerNotAllowed" end

    return true
end

#Can Edit Site

Called when the owner or a co-editor opens a domain's site in Sitewright, and before every save and delete. Use it to require a job or a licence. Keep it fast.

Example:

lua
---@param plyId integer
---@param host string @ Such as "mysite.com"
---@param role SiteRole @ "owner", "publisher" or "editor"
---@return boolean allowed
---@return string? reason
function CanEditSite(plyId, host, role)
    if (Player(plyId).state.inJail == true) then return false, "sites:error:editNotAllowed" end

    return true
end

#Can Publish Site

Called before the owner or a publisher puts a Sitewright site live, after the staff lock and provider checks. doc is the saved draft that would go live; see nui_source/src/sites/doc/types.ts for its fields. Use it to filter words or hold sites for staff review. It may yield.

Example:

lua
---@param plyId integer
---@param host string @ Such as "mysite.com"
---@param doc SiteDoc
---@param role SiteRole @ "owner" or "publisher"
---@return boolean allowed
---@return string? reason
function CanPublishSite(plyId, host, doc, role)
    -- Only staff publish on .gov
    if (host:find("%.gov$") and not Z.hasPermission(plyId, "admin")) then return false, "sites:error:publishNotAllowed" end

    return true
end

#Can Submit Form

Called before a visitor sends a contact form on a Sitewright site, after the answers are checked and before the site's limit is counted. Use it to filter words or mute a player.

Example:

lua
---@param plyId integer
---@param host string @ Such as "mysite.com"
---@param answers {name: string, phone: string, subject: string, message: string} @ Empty for the fields the form hides
---@return boolean allowed
---@return string? reason
function CanSubmitForm(plyId, host, answers)
    if (answers.message:lower():find("free money")) then return false, "sites:form:error:notAllowed" end

    return true
end

#Can Book

Called before a visitor books a time with a Sitewright site's booking block, after the time is checked against the block's hours and before its places are counted. Use it to require a licence for a service or to keep someone from booking. It may yield.

Example:

lua
---@param plyId integer
---@param host string @ Such as "mysite.com"
---@param booking {block: string, service: string, serviceName: string, startsAt: integer, endsAt: integer, name: string, note: string} @ Times are os.time() values
---@return boolean allowed
---@return string? reason
function CanBook(plyId, host, booking)
    -- my_licences stands for your own licence resource
    if (booking.service == "flight_lesson" and not exports["my_licences"]:HasLicence(plyId, "medical")) then return false, "sites:booking:error:notAllowed" end

    return true
end

#Can Change Vehicle Plate

Called before a player changes a plate on the DMV site, after the format check and before the garage resource checks the plate. Asked on the site's availability check and again before the fee is taken. Use it to keep custom plates for donors or refuse words you don't want on the road.

Example:

lua
---@param plyId integer
---@param vehicleId string @ The garage resource's id for the vehicle, the VIN with zyke_garages
---@param plate string @ The new plate, uppercase and in the site's format
---@return boolean allowed
---@return string? reason
function CanChangeVehiclePlate(plyId, vehicleId, plate)
    if (plate:find("LSPD")) then return false, "dmv:error:notAllowed" end

    return true
end

#Get Vehicle Plate Price

Returns what a plate change costs this player for this vehicle. price is Config.Dmv.price. Return a whole number; anything else uses price, and a negative one is free. Asked for every vehicle the site lists. Keep it fast.

Example:

lua
---@param plyId integer
---@param vehicleId string @ The garage resource's id for the vehicle, the VIN with zyke_garages
---@param price integer
---@return integer price
function GetVehiclePlatePrice(plyId, vehicleId, price)
    if (Z.isJob(plyId, "police")) then return 0 end

    return price
end

#Can Use Crypto

Called before every request Satchel, the crypto app, makes, before the rate limit. Use it to require a job or a phone item. Keep it fast.

Example:

lua
---@param plyId integer
---@return boolean allowed
---@return string? reason
function CanUseCrypto(plyId)
    if (not Z.hasItem(plyId, "phone")) then return false, "crypto:error:notAllowed" end

    return true
end

#Can Buy Crypto

Called before a player buys a coin, after the order size and price checks, before the daily caps and before the money is taken. It may yield; the price is read again after it.

Example:

lua
---@param plyId integer
---@param coinId string @ Such as "BSN"
---@param fiat integer @ The money the order spends, fee included
---@return boolean allowed
---@return string? reason
function CanBuyCrypto(plyId, coinId, fiat)
    -- Large orders need a trading licence
    if (fiat > 50000 and not Z.hasItem(plyId, "trading_licence")) then return false, "crypto:error:notAllowed" end

    return true
end

#Can Sell Crypto

Called before a player sells a coin, before the price, order size, and daily cap checks and before the coins leave their wallet.

Example:

lua
---@param plyId integer
---@param coinId string @ Such as "BSN"
---@param units integer @ Base units sold, 100000000 to a Bastion
---@return boolean allowed
---@return string? reason
function CanSellCrypto(plyId, coinId, units)
    return true
end

#Can Send Crypto

Called before a player sends coins to another wallet, after the amount, fee, and recipient checks, before the coins leave the wallet. Use it to block transfers between characters of the same player.

Example:

lua
---@param plyId integer
---@param coinId string @ Such as "BSN"
---@param units integer @ Base units sent, without the network fee
---@param recipient string @ The receiving character's identifier, who may be offline
---@return boolean allowed
---@return string? reason
function CanSendCrypto(plyId, coinId, units, recipient)
    -- my_accounts stands for your own multicharacter resource
    if (exports["my_accounts"]:IsSamePlayer(Z.getIdentifier(plyId), recipient)) then return false, "crypto:error:notAllowed" end

    return true
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 Use Mail

Called when Mail starts its session and before every request. A refusal on a request also ends the session; the reason is only shown when the session starts. Use it to require a job or a licence. Keep it fast.

Example:

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

    return true
end

#Can Send Mail

Called before a player sends a mail, after every address and the text are checked. Use it to keep a company address for outgoing mail on duty only.

Example:

lua
---@param plyId integer
---@param from string @ The address it goes out from, such as "[email protected]"
---@param recipients string[] @ Every To, Cc and Bcc address
---@return boolean allowed
---@return string? reason
function CanSendMail(plyId, from, recipients)
    if (from:find("@lspd%.gov$") and not Z.getJob(plyId).onDuty) then return false, "mail:error:notAllowed" end

    return true
end

#Can Create Mailbox

Called before a player makes an address on a domain, for the domain's owner and for the characters they let make their own addresses, after the name and limit checks. Use it to keep names such as ceo@ for the owner.

Example:

lua
---@param plyId integer
---@param address string @ Such as "[email protected]"
---@return boolean allowed
---@return string? reason
function CanCreateMailbox(plyId, address)
    if (address:find("^ceo@") or address:find("^admin@")) then return false, "mail:error:notAllowed" end

    return true
end

#Can Buy Laptop Shop Item

Called before something is bought at the shop, before the money is taken, and for each offer rung up at the Digital Den counter, where plyId is the customer. 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)
    -- Only the text of this kind is built, the device texts need a device
    local texts = {
        newDevice = function() return T("cloud:server:alert:newDevice", {data.device}) end,
        resetAsked = function() return T("cloud:server:alert:resetAsked") end,
        signedOut = function() return T("cloud:server:alert:signedOut", {data.device}) end,
        reset = function() return T("cloud:server:alert:reset") end,
    }

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

#Can Start Hack

Called when another resource asks for an intrusion job with RequestHack or StartHack, after the busy and cooldown checks, and again when the player starts the job on their laptop. Whether the player may work on the target at all is the calling resource's check. It may yield.

A refusal gives the calling resource the reason, and the player sees it. A reason with a : is used as a locale key as it is; a plain code becomes hacking:reason:<code>.

Example:

lua
---@param plyId integer
---@param job LaptopHackJob @ target, coords, bucket, game, mode, difficulty, timeLimit and needsAdapter
---@param resource string @ The resource that asked for the job
---@return boolean allowed
---@return string? reason
function CanStartHack(plyId, job, resource)
    -- Police can't run jobs while on duty
    if (Z.isJob(plyId, "police") and Z.getJob(plyId).onDuty) then return false, "hacking:reason:denied" end

    return true
end

#On Hack Trace

Called with a job's trace, for police alerts. Called when the job starts with trace.reason set to started, and again when it ends with the result, both times with the same trace.id. Which fields are filled depends on the player's OS, VPN, and Onion, and on Config.Hacking.traces. The server event zyke_laptop:HackTrace gets the same. It returns nothing and can't change the job's result. Does nothing by default.

Example:

lua
---@param plyId integer
---@param trace LaptopHackTrace
function OnHackTrace(plyId, trace)
    if (trace.reason ~= "started" or not trace.coords) then return end

    -- my_dispatch stands for your own dispatch resource
    exports["my_dispatch"]:Alert("police", "Network intrusion", trace.coords)
end

#Can Place Rack

Called before a player sets a laptop rack down, after the Config.Racks.maxPerCharacter and persistent bucket checks. Use it to keep racks out of public places or limit them to a job.

Example:

lua
---@param plyId integer
---@param coords vector4 @ w is the heading
---@param bucket integer
---@return boolean allowed
---@return string? reason
function CanPlaceRack(plyId, coords, bucket)
    -- Racks only go in housing buckets
    if (bucket < 1000 or bucket >= 2000) then return false, "racks:world:notAllowed" end

    return true
end

#Can Use Rack

Called before a player does something to a rack. At the rack, action is insert, take, power, or pickUp; it is asked after the reach and Config.Racks.physicalAccess checks. In Hive, it is open, power, profile, shares, rename, addMember, or removeMember, asked after the member, owner-only, and rate limit checks. Only a rack's owner and members run it from Hive. A rack picked up through another resource is asked with pickUp too.

Example:

lua
---@param plyId integer
---@param rackId integer
---@param action RackAction
---@return boolean allowed
---@return string? reason
function CanUseRack(plyId, rackId, action)
    -- Only the crypto farm job changes a rack's profile
    if (action == "profile" and not Z.isJob(plyId, "cryptofarm")) then return false, "racks:error:notAllowed" end

    return true
end

#Can Remove Rack

Return true to let a player who doesn't own an empty rack pick it up, for example the police. Asked after CanUseRack with pickUp and the empty check; the owner always may. Returns false by default. Everyone may take the laptops out of it first while Config.Racks.physicalAccess is anyone. A refusal shows racks:world:notOwner.

Example:

lua
---@param plyId integer
---@param rackId integer
---@param owner string @ Identifier of the character who set it down
---@return boolean allowed
function CanRemoveRack(plyId, rackId, owner)
    return Z.isJob(plyId, "police")
end

#Get Rack Power Supply

Returns the watts a rack may draw before its breaker trips. supply is Config.Racks.power.supply. Return 0 to cut its power, such as while a property's electricity is off, or more for a warehouse with three-phase power; a negative number counts as 0, and anything else uses supply. The SetRackPowerSupply export overrides it per rack. Keep it fast: it is asked every tick for every rack.

Example:

lua
---@param rackId integer
---@param owner string
---@param coords vector4
---@param bucket integer
---@param supply number @ The configured supply
---@return number supply
function GetRackPowerSupply(rackId, owner, coords, bucket, supply)
    -- my_power stands for your own electricity resource
    if (not exports["my_power"]:HasPower(bucket)) then return 0 end

    return supply
end

#Can Make Laptop Payment

Called before a player pays through the laptop's pay sheet for another resource's app or site, after the method and connection checks and before the money moves. A refusal answers the app with the code notAllowed; the returned reason is not passed on.

Example:

lua
---@param plyId integer
---@param payment SdkPayment @ id, resource, app, label, amount, currency, payee and more
---@return boolean allowed
---@return string? reason
function CanMakeLaptopPayment(plyId, payment)
    if (Player(plyId).state.inJail == true) then return false end

    return true
end

#Can Use Laptop App

Return false to hide an app or site of another resource from a player and refuse its server calls, after its own access rules passed. Only exactly false refuses. The answer is kept for 5 seconds per player and app, and cleared when their job changes. It also decides the CanUseApp export.

Example:

lua
---@param plyId integer
---@param kind "app" | "site"
---@param id string
---@param resource string
---@return boolean allowed
function CanUseLaptopApp(plyId, kind, id, resource)
    -- my_darkweb stands for your own resource
    if (resource == "my_darkweb" and Z.isJob(plyId, "police")) then return false end

    return true
end

#Can Sell Business Item

Called once per line before a business's bill goes out, at the MDT register and in its website order block. Use it to card customers for alcohol or cap how much one customer buys. staffId is nil for a website order, which shows its own refusal instead of the reason.

Example:

lua
---@param staffId? integer @ The member at the register, nil for a website order
---@param customerId integer
---@param departmentId string @ The business, see shared/unlocked/business.lua
---@param entry MdtCatalogueEntry @ kind "goods" names its item, "service" moves none
---@param qty integer
---@return boolean allowed
---@return string? reason
function CanSellBusinessItem(staffId, customerId, departmentId, entry, qty)
    -- my_licences stands for your own licence resource
    if (entry.item == "beer" and not exports["my_licences"]:IsAdult(customerId)) then return false, "mdt:error:notAllowed" end

    return true
end

#Client-Sided Hooks

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

#Can Use Laptop

Called before the player uses a laptop. IsPlayerIncapacitated is checked first. The actions:

ActionWhen
takeOutTaking a laptop out
use, pickUpUsing or picking up a placed laptop or a demo unit
portsOpening a laptop's ports, or plugging into a charging outlet
serviceOpening the service view
screenAnother resource opens the screen with the OpenScreen export
mountUsing a vehicle's mounted laptop
placeComputer, removeComputerSetting a computer up or taking one away
placeRack, rackSetting a laptop rack down or working at one
placeRouter, routerSetting a router or extender down or picking one up
placePrinter, printerSetting a printer down or using one

By default it refuses while swimming, and in a vehicle for every action but takeOut, mount, ports, and screen. Laptops in vehicles follow shared/unlocked/vehicles.lua; return false for takeOut or mount while IsPedInAnyVehicle to keep them out of vehicles, or out of some of them.

Example:

lua
local inVehicleActions = {takeOut = true, mount = true, ports = true, screen = true}

---@param action "takeOut" | "use" | "pickUp" | "ports" | "service" | "screen" | "mount" | "placeComputer" | "removeComputer" | "placeRack" | "rack" | "placeRouter" | "router" | "placePrinter" | "printer"
---@return boolean allowed
---@return string? notification
function CanUseLaptop(action)
    local ped = PlayerPedId()
    if (IsPedSwimming(ped)) then return false, "laptopCantUse" end
    if (IsPedInAnyVehicle(ped, true) and not inVehicleActions[action]) 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 in use, 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 through a state bag so it refuses its own entry points meanwhile. Keep it fast.

By default it checks death on the ped, Z.isPlayerDead, which covers the death scripts zyke_lib supports and QBCore's last stand, and Z.isPlayerCuffed, which covers the game's cuffs and qb-policejob's. Replace or extend it for a death or restraint script zyke_lib doesn't know.

Example:

lua
---@return boolean incapacitated
function IsPlayerIncapacitated()
    if (IsEntityDead(PlayerPedId())) then return true end

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

    return Z.isPlayerDead() or Z.isPlayerCuffed()
end