Camper System (cv_camper)¶
The camper system turns a spawned Journey (or Journey2) persistent vehicle into a mobile drug-production lab. Players enter an IPL interior tied to the vehicle, deposit raw materials into a whitelisted stash, and the server automatically processes them into product over time.
Files¶
| File | Purpose |
|---|---|
resources/[char]/cv_camper/config.lua |
Vehicle models, entry cooldown, IPL coords, storage & auto-production settings |
resources/[char]/cv_camper/client/main.lua |
E-key interaction — detect nearby camper, trigger enter/leave callbacks |
resources/[char]/cv_camper/server/main.lua |
Enter/leave logic, storage deposit/withdraw callbacks, auto-production catchup |
Configuration¶
All parameters live in config.lua under Config.Camper:
Config.Camper = {
VehicleModels = { `journey`, `journey2` },
EntryCooldown = 5, -- seconds between entries (same player)
IplLocation = vec4(482.33, -2623.80, -50.06, 182.47),
}
Config.Camper.Storage = {
containerType = 'stash',
maxWeight = 70.0,
maxSlots = 10,
persistent = true,
useWhitelist = true,
allowedItems = { 'cocaine_paste', 'baggy', 'propane_gas', 'lab_equipment', 'cocaine_bag' },
}
Config.Camper.AutoProduction = {
enabled = true,
intervalMs = 30000, -- 0.5 min per cycle
maxCatchupSteps = 20,
recipes = {
{
id = 'coke_standard',
requirements = {
{ name = 'cocaine_paste', amount = 2, consume = true, consumeChance = 1.0 },
{ name = 'baggy', amount = 1, consume = true, consumeChance = 1.0 },
{ name = 'propane_gas', amount = 1, consume = true, consumeChance = 0.01 },
{ name = 'lab_equipment', amount = 1, consume = false },
},
output = { name = 'cocaine_bag', amount = 1 },
}
}
}
Storage whitelist¶
Only items in allowedItems can be deposited into the camper stash. Anything outside that list is rejected by the framework inventory layer.
How It Works¶
sequenceDiagram
participant C as Client
participant SRV as Server (cv_camper)
participant INS as cv_framework Instance
participant INV as cv_framework Inventory
C->>SRV: cv_camper:server:enterCamper (E key near Journey)
SRV->>SRV: cooldown check + persistentVehId lookup
SRV->>INS: cv.instance.joinCamper(playerId, camperId, opts)
INS-->>C: teleport to IplLocation, move to vehicle bucket
SRV-->>C: success → inCamper = true
Note over INV: Production runs in the background every 30 s
C->>SRV: cv_camper:server:leaveCamper (E key at exit marker)
SRV->>SRV: calculateCamperExitCoords (live vehicle position)
SRV->>INS: cv.instance.leave(playerId)
INS-->>C: teleport back to exit coords
SRV-->>C: success → inCamper = false
Entry flow¶
- Client detects a Journey/Journey2 within 3 m via
cv.utils.getNearbyVehicles. - Vehicle must not be locked (
GetVehicleDoorLockStatus ≠ 2) and must have apersistentVehIdstate bag. - Server enforces a 5-second entry cooldown per player source.
cv.instance.joinCamperteleports the player into the IPL and moves them to the vehicle's routing bucket.- A custom inventory (the stash) is attached to the instance with the settings from
Config.Camper.Storage.
Exit flow¶
- Client detects the player is within 1.5 m of the IPL exit marker (
Config.Camper.IplLocation). - Server recalculates exit coords from the vehicle's current world position (side-offset by 1.5 m). Falls back to the coords captured at entry if the vehicle entity is gone.
cv.instance.leaveteleports the player back to the exit coords.
Auto-production (catchup)¶
Production is driven by cv.processing.runStorageCatchup. It is triggered in two places:
| Trigger | Handler |
|---|---|
| Player opens the secondary inventory UI | cv_inventory:server:secondaryInventoryOpening event |
| External call via export | exports.cv_camper:RunStorageCatchup(playerSource, containerId) |
Catchup computes how many full intervalMs cycles have elapsed since the last run and applies up to maxCatchupSteps iterations. This means production continues offline — the stash processes materials whenever the inventory is next opened.
Server Callbacks¶
All callbacks are registered with cv.callback.register and triggered from the client with cv.callback.trigger.
cv_camper:server:enterCamper¶
Joins the player to the camper instance for the nearest eligible vehicle.
| Returns | Meaning |
|---|---|
true |
Player successfully joined the instance |
false |
No eligible vehicle found, cooldown active, or joinCamper failed |
cv_camper:server:leaveCamper¶
Leaves the current instance and teleports the player back.
Always returns true.
cv_camper:server:getStorage¶
Returns the current stash contents and capacity.
-- Response shape
{
success = true,
storageId = "camper:veh_abc123:storage",
items = { ... },
maxWeight = 70.0,
maxSlots = 10,
}
Triggers a processing catchup before reading items.
cv_camper:server:depositStorageItem(itemName, amount)¶
Moves amount of itemName from the player's character inventory into the camper stash. Validates whitelist, weight, and slot limits before transferring. Rolls back if the stash addContainerItem fails.
cv_camper:server:withdrawStorageItem(itemName, amount)¶
Moves amount of itemName from the camper stash into the player's character inventory. Rolls back if the player inventory addItem fails.
Exports¶
RunStorageCatchup(playerSource, containerId) → boolean¶
Manually triggers a production catchup for the given player's active camper instance.
Returns false if the player has no active camper instance or cv.processing is unavailable.
Lifecycle Cleanup¶
| Event | Action |
|---|---|
cv_framework:player:characterUnloaded |
Clears cooldownEntry and camperBySource for the player |
playerDropped |
Same as above |
onResourceStop |
Resets both tables |
Known API Notes¶
cv.player.getFromId(source) — not getPlayer¶
The correct function to look up a player object server-side is cv.player.getFromId(source). cv.player.getPlayer does not exist; calling it throws a nil-function error that is swallowed by the callback pcall, returning false to the client with no visible feedback.
cv.veh.getPersistentVehicle(veh_id) — correct namespace¶
Persistent vehicle lookup lives under cv.veh, not directly on cv. Always guard against the module not being loaded:
if cv.veh and cv.veh.getPersistentVehicle then
local vehData = cv.veh.getPersistentVehicle(camperId)
end
GetVehicleDoorLockStatus on the server¶
This native is client-only. Calling it server-side returns 0 (appears unlocked). The lock check is therefore only reliable on the client, which already performs it before triggering the callback. The server skips this check.
Dependencies¶
| Resource | Usage |
|---|---|
cv_framework |
Player object, instance system, inventory, utils, callbacks, notify |
oxmysql |
Pulled in via @oxmysql/lib/MySQL.lua (available to server scripts) |