Zum Inhalt

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

  1. Client detects a Journey/Journey2 within 3 m via cv.utils.getNearbyVehicles.
  2. Vehicle must not be locked (GetVehicleDoorLockStatus ≠ 2) and must have a persistentVehId state bag.
  3. Server enforces a 5-second entry cooldown per player source.
  4. cv.instance.joinCamper teleports the player into the IPL and moves them to the vehicle's routing bucket.
  5. A custom inventory (the stash) is attached to the instance with the settings from Config.Camper.Storage.

Exit flow

  1. Client detects the player is within 1.5 m of the IPL exit marker (Config.Camper.IplLocation).
  2. 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.
  3. cv.instance.leave teleports 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.

local ok = exports['cv_camper']:RunStorageCatchup(source, containerId)

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)