Launch sale · 9LABS Crane Tow Truck Job Ends in See the deal

Nothing in this manual matched.

9LABS Tow Truck Job · v1.0.9

A loader crane, and the job around it.

Clock on at the yard, take a company truck, work city calls, haggle the price with the customer and drop the car where it belongs. The crane is driven by a remote in your hand, lever by lever.

Introduction

A truck-mounted loader crane with a full towing job around it: company trucks, city calls, a cinematic price negotiation with the customer, pay, levels, and an in-game admin panel.

What is in the resource

  • A streamed crane truck with its own handling, variations and carcols.
  • A hand-held remote that works the crane lever by lever, with automatic hooking and bed loading.
  • Four call types: breakdowns, accidents, parking and repossessions.
  • Price negotiation with the customer, with personalities, tips and ratings.
  • Levels and XP that unlock the harder call types.
  • An admin panel that sets the yard, truck bays, call spots and drop-offs for your own map.
  • A job app for phones and tablets: the current call with GPS, levels, stats and the top operators. It installs itself on lb-phone, lb-tablet, Quasar, yPhone, okokPhone and GKS, and plugs into the Meteo Job Tablet as a full dispatch job.
  • Discord logging for shifts, deals, payouts, failures, admin actions and security events.

Almost nothing is configured in a file. Apart from the handful of options in config.lua, the whole job is set up in game through /hiabadmin, and changes save on the server and apply live.

Requirements

OneSync and a framework. Everything else is optional.

ServerOneSync.
FrameworkQbox, QBCore, ESX or standalone. Anything else can use the custom bridge.
StorageA writable storage/ folder. No SQL.
Optionalox_target or qb-target, ox_lib notifications, qbx_vehiclekeys or qb-vehiclekeys (any other keys script through the custom bridge), a fuel script, a phone or tablet.

Installation

Start it after your framework, then set up the map in game.

  1. Put 9labs_hiab in your resources folder and keep that exact name.
  2. Add it to server.cfg, after your framework, target, keys and fuel resources.
  3. Set the basics in config.lua.
  4. Restart the server.
  5. As an admin, run /hiabadmin and set the yard, truck bays, call spots and drop-offs for your map.
ensure 9labs_hiab

Remove any other resource that streams mulef. Two copies of the same vehicle model will fight each other.

Players must reconnect after you update the resource. It streams a vehicle, models and an animation, and those only reload when a client connects — a resource restart alone leaves people on the old data.

config.lua

Everything not listed here is set in game through /hiabadmin.

OptionValuesWhat it does
localeen, de, bsLanguage of the UI, notifications and logs
frameworkauto, qbx, qb, esx, standalone, customauto picks the first running one: qbx_core, qb-core, es_extended, otherwise standalone
keysauto, qbx, qb, none, customKeys for the company truck. auto uses qbx_vehiclekeys or qb-vehiclekeys, whichever is running; qbx and qb pick one of them. Any other keys script needs custom. With qb-vehiclekeys 1.6 and newer the keys go through its server exports
fuelauto, ox_fuel, LegacyFuel, cdn-fuel, customauto uses whichever of ox_fuel, LegacyFuel or cdn-fuel is running. Any other value, or a named script that is not running, sets native fuel. custom calls Custom.setFuel in client/bridge.lua
interaction9labs_ui, ox_target, qb-target, customHow players talk to the yard foreman
interactKey, interactKeyLabelcontrol id and labelKey for the built-in prompt; default 38 / E
notify9labs_ui, ox_lib, native, customNotification style
adminAceslist of ACEsWho can open /hiabadmin. command works for txAdmin admins without a restart
remoteKeykey nameDefault key for the crane remote (F7). Players rebind it in Settings → Key Bindings → FiveM
lockInventorytrue, false, customBlocks the inventory and its hotbar keys while the remote is in hand, because the remote uses 1, 2 and 3. true works with ox_inventory, qb-inventory and every inventory that respects inv_busy
appsphone / tablet namesWhich phones and tablets get the job app. Set one to false to skip it. See Phone and tablet apps
standalonestartingCash, startingBankStandalone only: starting balance of the built-in wallet, stored per licence
advanced.autoDispatchtrue / falsetrue (default): a call comes in by itself. false: calls only on request, see Calls on request
advanced.requestCommandcommand name or falseChat command that asks for a call when autoDispatch = false; default hiabcall
advanced.jobHudtrue / falsefalse hides the on-screen job panel and the "new call" banner. Notifications, blips, markers and the pay summary stay
advanced.privateTowtrue / falsetrue (default): the crane lifts any empty car, players' own cars too, also while a call is open; the customer's car is still picked first when it is in reach. false: during a call only the customer's car can be hooked
debugtrue / falseExtra console output

If you do not use the command ACE, give admin access the usual way:

add_ace group.admin hiab.admin allow

First setup

/hiabadmin writes to storage/settings.json on the server, and changes apply live.

Positions are set with Pick in world (aim, the mouse wheel turns drop-off bays, E confirms), with My position, or by typing X, Y, Z and H. Any coordinate box also accepts a pasted vector4(x, y, z, h).

SectionWhat you set
GeneralRequired job and grade (leave empty for everyone), truck model, deposit, yard and foreman position, foreman ped, whether the yard foreman is used at all, truck bays, return radius, map blip, and whether the crane works outside the job
PayAccount, base price, price per km, cap, bonus per level, max haggle, tip chance and maximum, late and damage penalties, cooldown after declining
LevelsXP per job, XP per level, max level, and which levels unlock accident and repossession calls
CallsTime between calls, time limit, arrival and drop-off radius, call types and their weights, customer car models, call spots, and drop-offs
CustomersPed models, time to decide in a conversation, and the weight of each personality
CraneSpeed of each movement, maximum distance from the truck for the remote, ground tolerance, leg reach, maximum hold time per lever, the maximum truck tilt (default 10°), and hooking: by hand (default) or automatic, with the size of the hook zone and the spreader angle tolerance
SoundVolume and range
Discord logsWebhook and what to log

Drop-off types

garageBreakdowns and accidents.
impoundAccidents, parking and repossessions.
scrapyardRepossessions.

A call goes to a drop-off of a matching type. If there is none of that type, it goes to any drop-off you have set up, so add drop-offs of each type for calls to end in the right place. The default settings have one impound, which takes every call until you add more. Calls only stop when there are no drop-offs at all or every call spot is taken.

Player controls

F7 takes the remote out beside the truck. Everything else is on the remote.

KeyAction
F7Take out or put away the remote, next to the truck
W / SHook out or in
A / DSlew left or right, always as seen from the camera
Shift / CtrlHook up or down
Q / ERotate the spreader
1Stabilizer legs down, or fold the crane away
2Hook the car once the hook hangs over it and the outline is green — on a call always the customer's car — or release it on the ground
3Strap the car on the bed, or lift it off the bed
SpaceEmergency stop
Backspace / EscPut the remote away
E, while driving the truckRoof beacons on or off — native siren lights, silent

On a call the remote only works once the deal is agreed. The customer's car has an outline on the ground: white when the crane cannot reach it, amber when it can, and green once the hook hangs over the car, low enough and with the spreader in line with it. Then 2 grabs the wheels. Servers can switch on automatic hooking in /hiabadmin → Crane. At the drop-off a green bay marks where the car goes, and the hanging car's outline turns green when the spot will be accepted.

Bring the truck back to get the deposit. Near the yard the truck bays show as green boxes. The foreman shows whether the truck is in the yard, and asks first before ending a shift without it.

The inventory is locked while the remote is in hand, so 1, 2 and 3 work the crane instead of the hotbar.

Commands

CommandWhoWhat
/hiabPlayerTakes out or puts away the crane remote next to the truck. F7 is bound to it (remoteKey), and players rebind it in Settings → Key Bindings → FiveM
/hiabadminAdminOpens the admin panel
/hiabcancelPlayerDrops the current call
/hiabcallPlayerAsks dispatch for a call. Only with advanced.autoDispatch = false; the name is advanced.requestCommand
hiabprof (F8)AnyoneMeasures this resource's client cost per part for five seconds and prints it to F8

Discord logs

Paste a webhook in the admin panel, save it, then send a test. Each category can be switched off.

In Discord: channel settings → Integrations → Webhooks → New webhook → Copy URL. Then in /hiabadmin → Discord logs, paste it, save, and press Send test.

CategoryContents
ShiftsStart with deposit, account, truck plate and location; end with reason, length, jobs, earnings and whether the deposit was refunded
DealsCall type, customer and personality, vehicle, pickup, drop-off, distance, offer, agreed price, and whether it was haggled
PayoutsAgreed price, late and damage penalties, pay, tip, total and account, payment result, rating, XP and level-up, and time on the call
Failed callsDeclined, customer left, abandoned or expired, with the reason
AdminSettings saved or reset with the exact changes, and test calls
SecurityRequest floods and attempts to use someone else's truck, at most once a minute per player and reason

The webhook stays on the server and is always masked in the panel. Players are logged with name, server ID, framework identity, licence and a Discord mention — and those mentions never ping.

Phone and tablet apps

The job has its own app, and adds it by itself to every supported phone or tablet that is running.

The app shows the current call with its steps (drive, load, deliver), price and time left, GPS to the car or the drop-off, cancelling the call, the player's level and stats, and the top operators. On a tablet it uses a wider two-column layout.

ResourceHow it is added
lb-phone, lb-tabletPreinstalled custom app
qs-smartphone-pro, qs-smartphoneCustom app
yseries, yphone, yflip-phonePreinstalled custom app
okokPhoneCustom app
gksphone, gks-tabletCustom app
meteo-jobtabletRegistered as a job: Clock In and Out, live dispatch panel, accept or cancel calls, levels, rankings, achievements, task history and tournaments
RoadPhone Pro, RoadPadOne entry in RoadPhone's own config, below

Each one can be switched off in config.lua:

apps = {
    ['lb-phone'] = true, ['lb-tablet'] = true,
    ['qs-smartphone-pro'] = true, ['qs-smartphone'] = true,
    ['yseries'] = true, ['yphone'] = true, ['yflip-phone'] = true,
    ['okokPhone'] = true, ['gksphone'] = true, ['gks-tablet'] = true,
    ['meteo-jobtablet'] = true,
},

Start 9labs_hiab after the phone or tablet. If the phone restarts, the app is added again by itself. If it does not show up, the player's F8 console says could not add the app to ….

Meteo Job Tablet

Clock In works at the yard, where the company truck is handed out. Anywhere else the tablet says so and sets the GPS to the yard. New calls appear as tasks: accepting one sets the GPS, and the panel follows the call through driving, loading and delivering. Pay, rating and XP are still handled by the job, and the tablet records earnings, reputation and history. If you renamed the tablet with Meteo's rename guide, change TABLET at the top of server/integrations.lua.

RoadPhone Pro

RoadPhone has no export for apps. Add this to the AppStore list in its public/static/config/config.json:

{
  "name": "HIAB Recovery",
  "light_icon": "https://cfx-nui-9labs_hiab/web/dist/app-icon.png",
  "dark_icon": "https://cfx-nui-9labs_hiab/web/dist/app-icon.png",
  "default": true,
  "category": "business",
  "custom_app_id": "9labs_hiab",
  "redirect": "custom_app",
  "url": "https://cfx-nui-9labs_hiab/web/dist/index.html?app=phone"
}

Any other phone or tablet

Anything that can show a web page works the same way. Point it at one of these, and the page talks to 9labs_hiab on its own:

https://cfx-nui-9labs_hiab/web/dist/index.html?app=phone
https://cfx-nui-9labs_hiab/web/dist/index.html?app=tablet

To add a phone permanently, copy one of the entries in client/integrations.lua, which stays open under Asset Escrow.

Calls on request

Out of the box a call comes in by itself when the shift starts and after every job. Servers that hand out work through their own phone, tablet or NPC can switch that off in the advanced block of config.lua.

advanced = {
    autoDispatch = false,      -- no call until the player asks for one
    requestCommand = 'hiabcall',
    jobHud = false,            -- optional: no on-screen job panel, the job lives on the phone
},

The player then asks for a call with the Request a call button in the job app, with /hiabcall, or through your script with RequestMission. The cooldown between jobs still applies, and the time limit of a call starts when it is handed out.

Example: the job on your own phone

9labs_hiab:stateChanged fires on every change of the shift or call, so the phone never has to poll. Any phone with a notification export works the same way.

local function text(shift)
    if not shift then return 'Off shift' end
    local m = shift.mission
    if not m then return shift.manual and 'Ready, request a call' or ('Next call in %ss'):format(shift.next or 0) end
    if m.stage == 'travel' then return ('Go to the %s'):format(m.label or m.model) end
    if m.stage == 'load' then return ('Load the %s'):format(m.label or m.model) end
    return ('Deliver to %s'):format(m.dropLabel or 'the drop-off')
end

AddEventHandler('9labs_hiab:stateChanged', function(src, shift)
    exports['your_phone']:SendNotification(src, { app = 'HIAB', title = 'Towing', text = text(shift) })
end)

AddEventHandler('9labs_hiab:jobPaid', function(src, data)
    exports['your_phone']:SendNotification(src, { app = 'HIAB', title = 'Paid', text = ('$%d'):format(data.total) })
end)

-- a code or button in your phone app asks for a call
RegisterNetEvent('your_phone:towing:request', function()
    local r = exports['9labs_hiab']:RequestMission(source)
    if not r.ok then exports['your_phone']:SendNotification(source, { app = 'HIAB', title = 'Towing', text = r.message }) end
end)

Custom bridges

config.lua, locales/, both bridge files, both integration files and data/*.meta stay open under Asset Escrow.

Framework, money, jobs and keys

Set framework = 'custom', and keys = 'custom' if you also handle keys, then fill in the Custom block at the top of server/bridge.lua:

function Custom.identity(src) return GetPlayerIdentifierByType(src, 'license') end -- stable character id
function Custom.name(src) return GetPlayerName(src) end
function Custom.addMoney(src, account, amount, reason) return false end          -- return true on success
function Custom.removeMoney(src, account, amount, reason) return false end
function Custom.getMoney(src, account) return 0 end
function Custom.hasJob(src, job, grade) return true end                          -- only used when a job is set
function Custom.giveKeys(src, vehicle, plate) end
function Custom.removeKeys(src, vehicle, plate) end

account is 'bank' or 'cash', whichever is set in /hiabadmin → Pay. The money hooks must return true only once the amount has really moved.

Prompt, target, notifications, fuel and inventory

Set interaction = 'custom', notify = 'custom', fuel = 'custom' or lockInventory = 'custom' as needed, then fill in the Custom block at the top of client/bridge.lua:

function Custom.showPrompt(text, key) end
function Custom.hidePrompt() end
function Custom.notify(text, kind) end                 -- kind: 'info', 'success', 'error'
function Custom.addTarget(id, coords, radius, options) return nil end -- options: { { label, icon, onSelect } }, return a handle
function Custom.removeTarget(handle) end
function Custom.setFuel(vehicle, level) end            -- used when fuel = 'custom'
function Custom.lockInventory(locked) end              -- used when lockInventory = 'custom'

Exports

Read the shift, the player's profile and the crane state from the server, start and end shifts from your own UI, and drive the crane from the client.

Server exports

-- true when the player is on a HIAB shift
local onDuty = exports['9labs_hiab']:IsOnDuty(source)

-- level and statistics of the player's current character, or nil
local p = exports['9labs_hiab']:GetProfile(source)
-- p.level, p.xp, p.levelXp, p.nextXp, p.maxLevel, p.jobs, p.earned, p.best, p.km, p.rating, p.name

-- the player's shift and current call, or false when off shift
local shift = exports['9labs_hiab']:GetShiftState(source)
-- shift.truck (net id), shift.deposit, shift.next (seconds until the next call, automatic mode only)
-- shift.manual = true and shift.ready (seconds until a call can be requested) with advanced.autoDispatch = false
-- shift.mission = { id, kind, stage ('travel' | 'load' | 'deliver'),
--   label, model, vehicle (net id of the customer's car), npc (net id of the customer),
--   npcName, price, km, dropLabel, spot, drop, deadline (unix time), limit (seconds) }

-- start or end a shift from your own script
local r = exports['9labs_hiab']:StartShift(source)        -- same rules as the foreman: only at the yard
local r = exports['9labs_hiab']:StartShift(source, { spawn = vector4(x, y, z, h) })
-- with an options table the yard distance is not checked; spawn is optional (default: a free truck bay)
-- job, deposit and keys work as usual. Returns { ok, truck (net id) } or { ok = false, error }

local r = exports['9labs_hiab']:EndShift(source)          -- { ok, summary }, or { ok = false, confirm = true, error } when the truck is not back
local r = exports['9labs_hiab']:EndShift(source, true)    -- end it anyway; the deposit is kept when the truck is not back
local r = exports['9labs_hiab']:EndShift(source, { force = true })   -- same as passing true
local r = exports['9labs_hiab']:EndShift(source, { refund = true })  -- your script decides the deposit refund
-- a truck that is back but still carries a car returns { ok = false, error } (no confirm): unload it first
local r = exports['9labs_hiab']:AbandonMission(source)    -- { ok }

-- hand out a call now (for advanced.autoDispatch = false, works in both modes)
local r = exports['9labs_hiab']:RequestMission(source)
-- { ok = true, mission } or { ok = false, reason, message, seconds }
-- reason: 'off_duty' | 'busy' | 'cooldown' (seconds left) | 'too_fast' (2 s between requests) | 'no_calls'
-- message is the localized text; the player is not notified, show it the way your script wants

-- current crane state of a truck by network id, or nil
local state = exports['9labs_hiab']:GetCraneState(netId)
-- state.pose (yaw, rotate, boom, jib, extension, legs, rigging, drop)
-- state.mode ('empty' | 'lifted' | 'secured'), state.load, state.operator

Your own NPC or job script instead of the foreman

Switch Yard foreman off in /hiabadmin → General. The foreman, its blip and prompt disappear, and shifts are started by your script — or by the Meteo Job Tablet, whose Clock In then works anywhere and uses a free truck bay. Calls, the crane, pay, levels and the phone and tablet apps keep working as usual.

-- server side, e.g. when your own NPC hands out the truck
local r = exports['9labs_hiab']:StartShift(source, { spawn = vector4(471.0, -1305.0, 29.2, 115.0) })
if not r.ok then print(r.error) end

-- when your NPC takes the truck back
exports['9labs_hiab']:EndShift(source, { refund = true })

Client exports

The action exports take the truck entity, or fall back to the nearest crane truck within reach when it is left out. GetCranePose needs the truck entity.

exports['9labs_hiab']:DeployStabilizers(truck)
exports['9labs_hiab']:StowCrane(truck)
exports['9labs_hiab']:AttachNearbyVehicle(truck)
exports['9labs_hiab']:LoadToBed(truck)
exports['9labs_hiab']:ReleaseLoad(truck)
local pose = exports['9labs_hiab']:GetCranePose(truck)

local shift = exports['9labs_hiab']:GetShiftState()       -- same shape as the server export
exports['9labs_hiab']:SetMissionWaypoint()                 -- GPS to the pickup or drop-off of the current call
exports['9labs_hiab']:SetMissionWaypoint('depot')          -- GPS to the yard
local r = exports['9labs_hiab']:RequestMission()           -- asks for a call for this player; the result message is shown as a notification

AddEventHandler('9labs_hiab:client:stateChanged', function(shift) end)

The server still checks distance, ownership and state exactly as it does for the remote, so a client export cannot reach a truck the player could not operate by hand.

Server events

All of these are server-local TriggerEvent, so a client cannot fake them.

AddEventHandler('9labs_hiab:shiftStarted', function(src, data)
    -- data.truck (net id), data.plate, data.deposit, data.account
end)

AddEventHandler('9labs_hiab:shiftEnded', function(src, data)
    -- data.reason: 'returned' | 'away' | 'truck_lost' | 'dropped' | 'character'
    -- data.jobs, data.earned, data.deposit, data.refunded (boolean)
end)

AddEventHandler('9labs_hiab:jobPaid', function(src, data)
    -- data.kind: 'breakdown' | 'accident' | 'parking' | 'repossession'
    -- data.pay, data.tip, data.total, data.paid (boolean), data.account
    -- data.stars, data.xp, data.leveled, data.km, data.late, data.damaged
end)

AddEventHandler('9labs_hiab:stateChanged', function(src, shift)
    -- every shift or call change; shift has the same shape as GetShiftState (false = off shift)
end)

AddEventHandler('9lh:craneEvent', function(truckNet, event, vehicleNet, at)
    -- event: 'deploy' | 'stow' | 'hooked' | 'secured' | 'unsecured' | 'released'
    -- at: world position where the car was set down ('released' only)
end)

Example: pay a society a cut of every job

AddEventHandler('9labs_hiab:jobPaid', function(src, data)
    if data.paid then
        exports['your_banking']:AddSocietyMoney('tow', math.floor(data.total * .1))
    end
end)

The truck

Model mulef, streamed by the resource with its own handling, variations and carcols in data/.

The roof beacons use siren setting id 243. It lives in data/carcols.meta and is referenced from data/carvariations.meta. If another resource on your server already uses 243, change it in both files to a free id between 200 and 254 — changing only one leaves the beacons dark.

Performance

Measured in game with the built-in profiler, not estimated.

SituationClient cost
Away from any truckabout 0.00 ms
Near or inside a truck0.03 to 0.05 ms
Operating the craneabout 0.12 ms

On the server, one light check runs every 15 seconds while nobody is on shift, and every 5 seconds during a shift. The crane only updates while a lever is held, and state is sent only when it changes.

Run hiabprof and open F8 to measure it yourself: it reports this resource's client cost per part over five seconds.

Troubleshooting

ProblemFix
Old models, missing animation or odd handling after an updateReconnect. Vehicle data and streamed files only reload on connect.
/hiabadmin says no permissionAdd one of adminAces to your admin group, or use the command ACE.
No calls come inCalls stop when there are no drop-offs at all or no free call spots; the player is told which. Add drop-offs and call spots in /hiabadmin → Calls. With advanced.autoDispatch = false calls only come on request.
Anticheat kicks when using the craneWhitelist the events below.
Beacons do not light upSiren id conflict. Change 243 in both meta files.
Discord test failsThe webhook must be a discord.com/api/webhooks/... link, and it has to be saved before you press Send test.
"The truck stands at X°"The truck is on steeper ground than the crane allows. Park on flatter ground, or raise Maximum truck tilt in /hiabadmin → Crane, up to 15°. Keep call spots off steep dirt.
Deposit not returnedThe truck must stand within the return radius of the foreman or of a truck bay (the green boxes), and must not be wrecked. The foreman shows whether the truck is in the yard.
"That is not the customer's car"During a call the crane only takes the customer's car. Park so its outline turns green.
Inventory still opens with the remote outYour inventory ignores invBusy and inv_busy. Set lockInventory = 'custom' and fill Custom.lockInventory in client/bridge.lua.
App missing on the phoneStart 9labs_hiab after the phone, and look for could not add the app in the player's F8 console.
9labs_hiab:rpc:q
9lh:*

Nothing in this manual matched.