Documentation

Racks & Hacking

Read and run laptop racks, register rack workloads, and ask for Shim intrusion jobs from another resource.

A laptop rack is a mining rig of stolen or spare laptops that players set down in the world and run from Hive, the rack app. Other resources read racks and what they draw, switch them and set their power supply, place and remove them, and register workloads that racks send compute to, such as a password cracker for a heist. The laptop's own mining pools are workloads registered the same way.

Shim is the laptop's intrusion toolkit. Another resource asks for a job on one of its own targets, such as a door panel or an ATM, and gets the result once the player has played the job's puzzle. Every job leaves a trace that police scripts can read.

The rack exports only exist while enabled is true in shared/unlocked/racks.lua. GetMiningNetwork also needs mining.enabled there and crypto, Config.Crypto.enabled, turned on. With them off, calling the exports fails, so a resource that works without racks wraps its calls in pcall.

The hacking exports always exist. While enabled is false in shared/unlocked/hacking.lua they refuse every job with "disabled".

General Information

Every export on this page is a server export. Racks belong to characters and are kept in the zyke_laptop_racks and zyke_laptop_rack_nodes tables. A rack's id stays the same through restarts.

Workloads belong to the resource that registered them and are removed when it stops. The laptop forgets them when it restarts, so register them on the server event zyke_laptop:Ready as well as when your resource starts.

Exports marked Yields wait on the database or on the player. Call them from a thread, an event handler or a command, not while your script loads.

The CanPlaceRack, CanUseRack, CanRemoveRack, GetRackPowerSupply and CanStartHack hooks in server/unlocked/hooks.lua add your own rules. See Can-Check Hooks.

Types / Classes

RackInfo, RackWork, RackWorkloadEarns, RackWorkloadInfo and MiningNetworkState are in types/racks.lua inside zyke_laptop. LaptopHackOptions and LaptopHackTrace are in types/hacking.lua.

#Rack Info

GetRack, GetRacks and GetCharacterRacks return racks as a RackInfo table.

FieldDescription
idThe rack's id.
nameThe name its owner gave it in Hive.
ownerIdentifier of the character who set it down.
membersIdentifiers of the characters who run it besides its owner.
coordsA vector4 on the floor under the rack, w the heading.
bucketIts routing bucket.
onWhether it is switched on.
trippedtrue while its breaker is tripped, until it is switched on again.
profileThe power profile: "eco", "balanced" or "max" by default.
laptopsEach laptop in it: {slot, serial, state, draw, compute, heat}. state is "off", "idle", "running", "throttled", "overheated", "noBoard" or "noMemory".
slotsHow many slots the rack has.
sharesThe percent of an algorithm's hashrate each workload gets, by workload id.
drawWatts it draws now, the controller's base load included while it is on.
computeThe laptops' compute it gives now, per second.
hashThe miner sticks' hashrate sent to workloads, by algorithm, such as {sha256 = 300}.
supplyWatts it may draw before its breaker trips.

The rack the server events and a workload's onWork get has laptops as a count and a serials list of {slot, serial} instead.

#Reading Racks

#Get Rack

Returns the rack, or nil for an unknown id.

Example:

lua
---@param rackId integer
---@return RackInfo? rack
local rack = exports["zyke_laptop"]:GetRack(3)

#Get Racks

Returns every rack that matches the filter. All filters are optional and all of them have to match. radius only counts together with coords. member matches the owner too. Without a filter it returns every rack.

Example:

lua
---@param filter? {owner?: string, member?: string, bucket?: integer, coords?: vector3, radius?: number, minDraw?: number, on?: boolean}
---@return RackInfo[] racks
-- The racks near the power station that draw over 1 kW, for a power company's tip-off
local racks = exports["zyke_laptop"]:GetRacks({
    coords = vector3(2742.1, 1513.6, 24.5),
    radius = 150.0,
    minDraw = 1000,
    on = true,
})

#Get Character Racks

Returns the racks a character owns or is a member of.

Example:

lua
---@param identifier string
---@return RackInfo[] racks
local racks = exports["zyke_laptop"]:GetCharacterRacks(Z.getIdentifier(plyId))

#Find Rack By Serial

Returns the id of the rack a laptop stands in and its slot, by the laptop's serial, such as to find a stolen laptop. Returns nil when the laptop is in no rack.

Example:

lua
---@param serial string
---@return integer? rackId
---@return integer? slot
local rackId, slot = exports["zyke_laptop"]:FindRackBySerial("ZL-4F7Q2K")

#Get Rack Power

Returns what the rack draws and gives now, and its supply. Returns 0, 0, 0 for an unknown rack.

Example:

lua
---@param rackId integer
---@return number draw @ Watts
---@return number compute @ Per second
---@return number supply @ Watts it may draw before its breaker trips
local draw, compute, supply = exports["zyke_laptop"]:GetRackPower(3)

#Is Rack Standing

Returns whether the rack still stands. Returns nil until the racks are loaded from the database on start.

Example:

lua
---@param rackId integer
---@return boolean? standing
local standing = exports["zyke_laptop"]:IsRackStanding(3)

#Running Racks

#Set Rack On

Switches the rack on or off, as its power switch does. Switching it on resets a tripped breaker. Fires zyke_laptop:racks:PowerChanged with your resource's name as the cause. Returns false for an unknown rack.

Example:

lua
---@param rackId integer
---@param on boolean
---@return boolean set
exports["zyke_laptop"]:SetRackOn(3, false)

#Set Rack Power Supply

Sets the watts one rack may draw, over the config's power.supply and the GetRackPowerSupply hook, such as from a generator. Below 0 counts as 0, which keeps the rack from running. false goes back to the config and the hook. It is not kept through a restart. Returns false for an unknown rack or a value that isn't a number or false.

Example:

lua
---@param rackId integer
---@param watts number | false
---@return boolean set
exports["zyke_laptop"]:SetRackPowerSupply(3, 5000)

#Set Rack Share

Sets the percent of the rack's hashrate of the workload's algorithm that the workload gets, 0 to 100, without asking the owner or the workload's canUse. 0 takes the share away. The shares of one algorithm add up to 100 at most, and the rest idles.

Returns false and "notFound" for an unknown rack, "invalid" for a share that isn't a whole number from 0 to 100, "unknownWorkload" for a share above 0 for a workload that isn't registered, or "overAllocated" when the algorithm's shares would add up to more than 100.

The laptop's mining pools have the ids mining:xmr, mining:btc and mining:scrypt by default, from mining.pools in shared/unlocked/racks.lua.

Example:

lua
---@param rackId integer
---@param workloadId string
---@param share integer
---@return boolean set
---@return string? err
local set, err = exports["zyke_laptop"]:SetRackShare(3, "crack:vault", 50)

#Placing and Removing Racks

#Place Rack

Yields. Sets a rack down for the player on a spot your resource picked, such as a workshop's floor grid, after you cancelled the Placing event. coords is a vector4 on the floor with the heading as w. The rack goes into the player's routing bucket and the player owns it.

It takes the rack item from the player, the one in options.slot first, or none with options.free = true, for an admin. The spot is checked as for a rack set down by hand: the racks per character, a persistent routing bucket and the CanPlaceRack hook. The player's reach is not checked, so check where they stand yourself.

Returns the rack's id, or nil and a locale key such as "racks:world:noRackItem" or "racks:world:placedLimit", after the player is told why. Returns nil without a key while the racks are still loading, or for invalid coords or an unknown player.

Example:

lua
---@param plyId integer
---@param coords vector4 @ w is the heading
---@param options? {slot?: integer, free?: boolean}
---@return integer? rackId
---@return string? refusal @ Locale key
local rackId, refusal = exports["zyke_laptop"]:PlaceRack(plyId, vector4(1003.2, -3105.8, -39.99, 90.0), {slot = slot})

#Pack Rack

Yields. Picks an empty rack up into the player's inventory, with the same rules as at the rack but no reach check. The player has to own it or be let by the CanRemoveRack hook, and CanUseRack is asked with "pickUp". Returns true, or false and a locale key such as "racks:world:notEmpty", "racks:world:notOwner" or "racks:world:noRoom", after the player is told why.

Example:

lua
---@param plyId integer
---@param rackId integer
---@return boolean pickedUp
---@return string? refusal @ Locale key
local pickedUp, refusal = exports["zyke_laptop"]:PackRack(plyId, 3)

#Remove Rack

Yields. Takes a rack and its laptops out of the world without giving anything to anyone, such as for a police raid. Returns the items, so your resource decides where they go, such as an evidence locker: each laptop with its metadata, worn as it ran, and the rack item last. Returns nil for an unknown rack or one that is being changed.

When the rack's row can't be deleted, the laptops still come out and the empty rack stays standing, without the rack item in the list. Fires zyke_laptop:racks:Removed.

Example:

lua
---@param rackId integer
---@return {name: string, metadata?: table}[]? items
local items = exports["zyke_laptop"]:RemoveRack(3)

for _, item in ipairs(items or {}) do
    -- Put item.name with item.metadata in your evidence locker
end

#Workloads

A workload is something racks send compute to. The rack calls its onWork every tick, every tickSeconds in shared/unlocked/racks.lua, with the work its share did. The owner, or SetRackShare, gives it a share of the rack in Hive.

#Register Rack Workload

Adds a workload, or replaces one your resource registered before. Returns true, or false and "invalid" for a bad id, no label or no onWork, or "taken" when another resource registered the id. Both print a warning in the server console, except a missing label. Fires zyke_laptop:racks:WorkloadsChanged.

The id is up to 48 letters, digits, _, - and :. When your resource stops, the racks keep their shares for the workload and Hive shows it as offline until it is registered again.

Example:

lua
---@param id string
---@param definition table
---@return boolean registered
---@return string? err
exports["zyke_laptop"]:RegisterRackWorkload("crack:vault", {
    label = "Vault cracker",
    description = "Brute-forces the Pacific vault's keypad codes.",
    icon = "key-round",
    algorithm = "cpu",
    earns = {perUnitHour = 0.002, unit = "codes"},
    canUse = function(identifier, rack) return true end,
    onWork = function(work, rack, state)
        state = state or {done = 0}
        state.done = state.done + work.units

        return {
            status = ("%d%% cracked"):format(math.floor(math.min(1, state.done / 50000) * 100)),
            progress = state.done / 50000,
            state = state,
        }
    end,
})
FieldDefaultDescription
labelRequiredShown in Hive, up to 48 characters.
onWorkRequiredfunction(work, rack, state), see below.
descriptionNoneUp to 200 characters.
icon"cpu"A Lucide icon name, such as "key-round".
algorithm"cpu"The hashrate it takes: "cpu" for the laptops' own compute, or a miner stick's, such as "sha256" or "scrypt".
earnsNone{perUnitHour, unit, money?} or a list of up to 4, what one unit earns an hour. Hive shows it as an estimate. unit is up to 12 characters, and money is what perUnitHour is worth.
getEarnsNonefunction() returning the same as earns, asked each time Hive shows it, for a rate that moves.
canUseEveryonefunction(identifier, rack). Only true lets the character give the workload a share in Hive.

onWork gets:

  • work: {units, rate, seconds, share}. units is the hashrate of its algorithm done since the last call, rate the same per second, and share the percent the rack gives it.
  • rack: the rack, with laptops as a count and serials, see Rack Info.
  • state: what it returned as state for this rack last time, kept through restarts.

The arguments are copies, so return the state rather than changing it. onWork may return:

FieldDescription
statusA line Hive shows, up to 120 characters.
statusKey / statusArgsA racks: locale key and its values, instead of status.
progress0 to 1, shown as a bar.
stateA table kept for the rack, 4 KiB of JSON at most. It is saved with the rack once a minute.
savetrue saves the state at once, such as right after paying something out.

Every onWork runs on its own thread, so it may wait. A rack doesn't call it again while it still runs; the ticks it missed come with the next call, in units and seconds. An error in it is printed in the server console.

#Unregister Rack Workload

Removes a workload your resource registered. Does nothing for another resource's workload.

Example:

lua
---@param id string
exports["zyke_laptop"]:UnregisterRackWorkload("crack:vault")

#Get Rack Workloads

Returns every registered workload without its functions, sorted by label: {id, label, description, icon, algorithm, earns, resource}.

Example:

lua
---@return RackWorkloadInfo[] workloads
local workloads = exports["zyke_laptop"]:GetRackWorkloads()

#Mining

#Get Mining Network

Returns a coin's mining network as it pays now, or nil for a coin without a network in mining.networks.

perUnitHour is what one unit of hashrate mines an hour now, before the pool's fee. pays is the share of its full rate, 0 to 1, which drops the more of the server mines the coin. server is the server's hashrate the difficulty counts, current the server's hashrate mining it now, and trend is "rising", "falling" or "steady".

Example:

lua
---@param coin string
---@return MiningNetworkState? network @ {coin, algorithm, perUnitHour, pays, server, current, outside, trend}
local network = exports["zyke_laptop"]:GetMiningNetwork("SHD")

#Intrusion Toolkit

A job takes one of three ways in, its mode: localAccess with the laptop near the target, wired with a console cable from a put-down laptop, or wireless with a Wi-Fi adapter and a network. A player and a laptop have one job at a time, and the same character and the same target of your resource wait cooldownSeconds before the next one. Only pass coordinates and options from your own server config, never from a client.

The result comes later, so check the player and the target again before you open or pay anything.

#Request Hack

Yields until the job ends, which can be minutes. Asks for a job and opens Shim for the player once they are signed in. Returns whether it succeeded, the reason and the job's trace, nil when it never started. A refused job returns at once with false and the reason. The player is told why a job was refused, except for "options".

Example:

lua
---@param plyId integer
---@param options LaptopHackOptions
---@return boolean success
---@return string reason
---@return LaptopHackTrace? trace
local success, reason, trace = exports["zyke_laptop"]:RequestHack(plyId, {
    target = "pacific-vault-panel",
    coords = vec4(253.1, 228.4, 101.7, 160.0),
    game = "circuit",
    mode = "wired",
    difficulty = 3,
})

if (success) then
    -- Check the player and the vault again, then open the door
end
FieldDefaultDescription
targetRequiredYour own id for the target, up to 96 characters, for the cooldown and the trace.
coordsRequiredThe target as a vector3, or a vector4 of the port the cable goes into, w the heading it faces so the cable goes in straight.
game"sequence""sequence", "circuit", "bruteforce" or "portscan".
mode"localAccess""localAccess", "wired" or "wireless".
difficulty21 to 5, before the cable and the signal change it.
timeLimitThe game's ownSeconds, within timeLimit.min and timeLimit.max in shared/unlocked/hacking.lua, 10 to 180 by default.
ownerIdNoneThe laptop id of a put-down laptop to use. Without it the job runs on the laptop the player uses or holds, or their own put-down laptop nearby.
bucketThe player'sThe routing bucket the job runs in.
hardwareNone{adapter = false} lets a wireless job run on the laptop's own Wi-Fi card.

#Start Hack

Asks for a job like RequestHack but returns at once, with the job's id, or nil and why it was refused. callback(success, reason, trace) is called when an accepted job ends. A refused job never calls it. Returns nil and "options" without a callback. It waits while the CanStartHack hook does.

Example:

lua
---@param plyId integer
---@param options LaptopHackOptions
---@param callback fun(success: boolean, reason: string, trace?: LaptopHackTrace)
---@return string? jobId
---@return string? reason
local jobId, reason = exports["zyke_laptop"]:StartHack(plyId, {
    target = "fleeca-atm-12",
    coords = vector3(147.6, -1035.8, 29.3),
    game = "bruteforce",
}, function(success, reason, trace)
    if (success) then
        -- Check the player is still at the ATM, then pay out
    end
end)

#Cancel Hack

Cancels the player's job, which ends with "cancelled". Only the resource that asked for the job can cancel it. Returns false when the player has no job with that id from your resource.

Example:

lua
---@param plyId integer
---@param jobId string
---@return boolean cancelled
exports["zyke_laptop"]:CancelHack(plyId, jobId)

#Get Hack Traces

Yields. Returns the traces of past jobs, newest first. All filters are optional and all of them have to match. since is a Unix time and limit 1 to 500, 100 by default. Traces are kept for traces.retentionDays days, 14 by default.

Which fields of a trace are filled follows the player's browsing tier: a direct connection gives the character, the laptop's serial, the network, where they stood and the serials of the parts and plugged devices, a VPN only the serial and the hardware, Onion routing nothing. A wired job always leaves the serial and the hardware.

Example:

lua
---@param filter? {id?: string, target?: string, resource?: string, serial?: string, character?: string, since?: integer, limit?: integer}
---@return LaptopHackTrace[] traces
local traces = exports["zyke_laptop"]:GetHackTraces({
    target = "pacific-vault-panel",
    since = os.time() - 86400,
    limit = 20,
})

#Hack Reasons

ReasonMeaning
successThe player won the job.
failedThe player lost the puzzle.
timeoutThe player wasn't set up within prepareSeconds, or ran out of time.
cancelledCancelHack or the player cancelled it.
stoppedYour resource or the laptop stopped.
disconnectedThe player left.
busyThe player already has a job.
cooldownThe character or the target is still on cooldown.
disabledHacking is turned off.
optionsWrong options. The player isn't told.
unavailableThe player isn't there or has no character.
deniedCanStartHack refused without a key of its own. A key it returns is passed on as the reason.
laptop, locked, os, hardware, placed, cable, adapter, network, rangeWhat the setup lacks.

#Events

Listen with AddEventHandler on the server. They cannot block an action; use the hooks for that.

#Rack Events

EventArgumentsWhen
zyke_laptop:racks:Placing{source, item, slot}The rack item is used. CancelEvent() to set the rack down your own way with PlaceRack.
zyke_laptop:racks:ReadyThe racks are loaded on start.
zyke_laptop:racks:Placedsource, rackA rack is set down.
zyke_laptop:racks:PickedUpsource, rackA rack is picked up.
zyke_laptop:racks:Removedrack, items, resourceRemoveRack took a rack away.
zyke_laptop:racks:LaptopInsertedsource, rackId, slot, serialA laptop goes into a slot.
zyke_laptop:racks:LaptopRemovedsource, rackId, slot, serialA laptop comes out of a slot.
zyke_laptop:racks:PowerChangedrackId, on, causeA rack is switched. cause is "switch", "app" or the resource that called SetRackOn.
zyke_laptop:racks:TrippedrackId, draw, supplyA rack's breaker trips.
zyke_laptop:racks:SharesChangedrackId, sharesA rack's shares change.
zyke_laptop:racks:MembersChangedrackId, identifier, addedA member is added or removed.
zyke_laptop:racks:WorkloadsChangedA workload is registered or removed.
zyke_laptop:racks:Tickrack, secondsEvery tick, for every rack that is on.
lua
-- Sets racks down on your own floor grid instead of the laptop's placement
AddEventHandler("zyke_laptop:racks:Placing", function(data)
    local spot = MyGrid.getFreeSpot(data.source)
    if (not spot) then return end

    CancelEvent()

    CreateThread(function()
        exports["zyke_laptop"]:PlaceRack(data.source, spot, {slot = data.slot})
    end)
end)

#Hack Trace

Fires when a job starts, with reason "started", and again with the result, both times with the same trace.id. The OnHackTrace hook gets the same, for police alerts.

Example:

lua
---@param plyId integer
---@param trace LaptopHackTrace
AddEventHandler("zyke_laptop:HackTrace", function(plyId, trace)
    if (trace.reason == "started" and trace.coords) then
        -- Alert the police near trace.coords
    end
end)

The hacking example in shared/unlocked/examples.lua sets up a test barrier on the LSIA apron with a panel for each game and way in. Its server side, examples/hacking/server.lua, shows StartHack from a panel the player uses.