Features
Feature reference for oxide-baitcar — deployment, dashboard, remote, dashcam, remote actions, trigger detection, civilian search, recording, audit log, and officer blips.
Reference for every system oxide-baitcar ships. Each section explains the in-game behavior and the configuration keys that govern it.
Overview
A bait car is a regular civilian vehicle that an officer has chipped. To civilians it looks identical to any other car in the world. To officers in Config.AllowedJobs, every active deployment is visible as a map blip, in the dashboard or remote, and through a live event feed.
The system is asymmetric on purpose: civilians cannot tell a chipped car from a clean one. Even the screwdriver-use removal flow (triggered from inside any vehicle) returns identical "found nothing" messaging on a clean vehicle, a failed roll, and a rate-limited request. This keeps the item from being weaponized as a free bait-car detector.
Deployment
A deployment is created when an officer uses the chip item (Config.ChipItem, default baitcar_chip) while sitting in a target vehicle.
The install flow:
Officer uses the chip item from their inventory.
Client checks the officer is in a vehicle. The server re-verifies, requires the officer to be physically inside the vehicle (any seat), and rejects vehicle types other than automobile (bikes, boats, helis, planes, etc. are blocked).
A progress bar runs for Config.InstallDurationMs (default 4000 ms) with the mini@repair / fixing_a_ped animation. The bar is cancellable.
On completion, the server consumes one baitcar_chip from the officer's inventory and registers the deployment.
The oxide:baitcar entity statebag is set on the vehicle, replicating a public view of the deployment (plate plus the state booleans) only to clients with the entity in scope.
Deployment guard rails:
- Plate uniqueness. A plate that is already an active deployment is rejected. Servers using auto-generated plates inherit that uniqueness; servers reusing plates need to wait for the first deployment to be decommissioned before re-chipping.
- Per-officer cap. Officers can hold at most
Config.MaxDeploymentsPerOfficer(default3) active deployments simultaneously. The cap is per character id, not per source, so disconnecting does not free a slot. - Inventory check. The chip is only consumed after every other gate passes. A failed inventory remove cancels the deployment.
Operating Modes
Config.UseRemoteController picks between two operating modes. The two modes are mutually exclusive.
| Mode | UseRemoteController | Entry points |
|---|---|---|
| Remote controller (default) | true | The baitcar_remote item is the only entry point. The /baitcar command is not registered, and the radial submenu is not added. |
| Dashboard | false | The /baitcar command opens the dashboard. The o-link radial provider receives a Bait Car root entry with Open Manager and Watch Nearest Dashcam actions. |
In both modes the dashcam, all remote actions, and the dashcam recording hotkey work identically. The remote-controller mode is appropriate when you want the officer to physically carry the device (and risk losing it on death / search). Dashboard mode is appropriate when the controls should be available any time the officer is on duty.
Dashboard
The dashboard is the Vue 3 view that the /baitcar command opens. It is also reachable from the radial entry Bait Car -> Open Manager. The dashboard shows:
- Deployment list — every active deployment with plate, vehicle model, install timestamp, and current state (locked / engine / steering / speed limit / strobe / alarm / triggered flag).
- Stat tiles — count of active deployments, triggered count, and other rollups derived from the live deployment list.
- Event feed — the live officer-only feed (see Audit Log and Live Event Feed).
- Action buttons — lock / unlock, engine kill / restore, watch dashcam, set GPS waypoint, decommission.
Remote Controller
The remote is a Vue view styled as a handheld device. It is opened by using the baitcar_remote item (with Config.UseRemoteController = true). It shows:
- Signal strength bars derived from the officer's distance to the bait car.
- Live telemetry — speed, heading, current street and zone, fuel level, engine and body health, occupant list (when the entity is in scope).
- Full action surface — lock / unlock, engine kill / restore, steering modes, speed limiter, strobe, alarm, horn pulse, dashcam viewer, waypoint, and decommission.
Because the remote is an inventory item it is subject to your inventory's loss-on-death and search behaviors.
Dashcam
The dashcam attaches a scripted camera to the bait car and plays it back through a Vue HUD overlay. Officers can open the dashcam from the dashboard, the remote, or the radial Watch Nearest Dashcam shortcut.
Camera profile resolution is layered. For each field (fov, offset, rotation), the value is taken from the first layer that defines it:
Config.Dashcam.ByModel[<spawnName>] — exact model override.
Config.Dashcam.ByClass[<className>] — vehicle-class default. Class names match GTA's vehicle class integer mapping: compact, sedan, suv, coupe, muscle, sportsclassic, sports, super, offroad, industrial, utility, van, service, emergency, military, commercial, openwheel.
Config.Dashcam.Default — universal fallback.
Behavior:
- The camera is re-rotated every frame to track the vehicle's heading plus the configured
yawBias. - A slow poll (every
Config.Dashcam.pollIntervalMs, default2500ms) confirms the vehicle still exists, is in network scope, and still carries theoxide:baitcarstatebag. If any check fails the dashcam closes itself with a reason (feed_lost,chip_removed). - The exit keys default to Esc and Backspace (controls
200and177). Configure withConfig.Dashcam.exitKeys. - An optional timecycle modifier (
Config.Dashcam.timecycleEffect) and strength produce a stylized CCTV look. Empty string disables. - During playback the officer is invisible to themselves and core driving / combat controls are disabled to prevent input bleed.
The admin command /baitcardashcamtune lets you adjust offsets, pitch, yaw bias, and FOV live and produces a paste-ready Lua snippet. See Admin.
Remote Actions
All actions are server-authoritative. The client only fires the intent event; the server validates job permissions, looks up the deployment by plate, writes the new state into the entity statebag, logs an audit row, and broadcasts the deployment change to all officer clients.
| Action | Behavior |
|---|---|
| Lock / unlock | Sets SetVehicleDoorsLocked = 4 (locked-for-non-owners) and SetVehicleDoorsLockedForAllPlayers = true while locked; releases both on unlock. Enforced by a 250 ms client tick on every client that has the entity in scope; only the network owner's writes stick. |
| Engine kill / restore | Toggles SetVehicleUndriveable and forces SetVehicleEngineOn(false) while killed. |
| Steering: off / lock / left / right | Uses SetVehicleSteerBias (the physics-aware override). lock centers the wheel, left pushes bias to +Config.SteeringBias (default 1.0 = full left), right to -Config.SteeringBias. Analog steer input (controls 59 / 60) is disabled while any non-off mode is active. |
| Speed limiter | Caps the vehicle at Config.SpeedLimitMph (default 30 mph) via SetVehicleMaxSpeed. Releases by setting a deliberately-high cap (SetVehicleMaxSpeed(0) would clamp to zero). |
| Strobe | Toggles SetVehicleLights between 2 (force-on) and 1 (force-off) at Config.StrobeIntervalMs cadence (default 150 ms). |
| Alarm | Sets the alarm capability flag and restarts StartVehicleAlarm when the in-game alarm naturally expires (~30 s). Toggling off both clears the flag and zeroes any remaining alarm timer. |
| Horn | Fires a one-shot StartVehicleHorn for Config.HornDurationMs (default 1500 ms). Implemented via a separate oxide:baitcar:horn statebag pulse with a nonce, so only clients with the entity in scope receive the pulse and the bait-car netId is never broadcast globally. |
The lock, engineDisabled, steering, speedLimit, strobe, and alarm fields all live on the entity statebag (oxide:baitcar) so every officer's dashboard or remote stays in sync without polling the server.
Drive-Off Trigger
Trigger detection runs entirely on the client of any non-LEO civilian who is in the driver's seat of a vehicle with the oxide:baitcar statebag.
The detector:
Confirms the player is the driver, that LEO immunity does not apply (BaitCar.IsAllowed()), and that the plate has not already been triggered this session.
Watches GetEntitySpeed against Config.TriggerSpeedMps (default 8.0 m/s, ~29 km/h).
Accumulates time while above the threshold; when accumulated time reaches Config.TriggerDurationMs (default 1500 ms) and the engine is running, fires oxide:baitcar:server:trigger with the vehicle's network id.
Re-arms after the statebag is cleared (so re-deployed plates can trigger again on the same client).
Server-side trigger handling:
- Validates the source is actually the driver of the vehicle (anti-spoof).
- Rejects LEO sources (officers can't trigger their own deployments).
- One-shot per deployment: a
triggered = trueflag is set, so subsequent fires from the same or other clients are ignored. - Calls the configured dispatch adapter through
olink.dispatch.CreateAlertwith theConfig.Dispatchpayload, vehicle plate, model, and coords. - Optionally applies
Config.FakeWantedLevel(set to0to disable). - Notifies every authorized officer with a
Bait car triggered: <plate>toast. - Writes a
triggeredrow tobaitcar_eventswith the suspect char id and coords.
A second dispatch alert fires the first time a non-LEO sits in the bait car, before they drive off and trip the trigger threshold. This uses Config.EntryDispatch (separate code, title, priority, and blip from the trigger alert) and routes through the same job list as the trigger — so every responder is notified, not just the deploying officer. Entry alerts are gated by Config.NotifyOfficerOnEntry, rate-limited per plate (one alert every 30 s regardless of source), and skipped for LEOs.
Civilian Search
Civilians remove a chip by using Config.RemovalToolItem (default screwdriver) from their inventory while sitting inside a vehicle. The item must be registered as a usable item in the customer's inventory framework; oxide-baitcar binds the use callback through olink.framework.RegisterUsableItem.
The flow:
The civilian uses the screwdriver from their inventory. If they are not in a vehicle, the use is a silent no-op — no toast, no animation, nothing. This keeps the item generic so it can co-exist with other resources that may also consume it.
If the civilian is in any seat of a vehicle, a progress bar runs for Config.RemovalDurationMs (default 12000 ms) with movement / combat disabled. The bar is cancellable.
The server re-verifies the source is still seated in the same vehicle (trust boundary against modded clients spoofing arbitrary netIds), validates the source still holds the removal tool, rate-limits the call (one search per source every 3 s), and rolls math.random() against Config.RemovalSuccessChance (default 0.5).
Crucially, the same You search the vehicle but find nothing of value. message is returned for a non-bait vehicle, a failed roll, a rate-limited call, a missing entity, and a not-in-vehicle reject. The civilian cannot distinguish between them, so the screwdriver cannot be used as a bait-car detector.
On a successful roll against an actual bait car: the statebag clears, the deployment is removed, the chip is returned to the suspect's inventory, an audit row (chip_removed) is written, and every authorized officer is notified.
The entry dispatch alert still fires the first time a non-LEO sits in a bait car, so officers see the suspect on the map even if the suspect immediately uses a screwdriver and gets lucky on the roll. A future inventory item will introduce a way to bypass the entry alarm; the screwdriver intentionally cannot.
Dashcam Recording
The recording pipeline routes through oxide-police so clips appear in the MDT Video Storage tab alongside bodycam clips. The pipeline is gated on three conditions at all times:
Config.Recording.enabled = trueGetResourceState('oxide-police') == 'started'(rechecked on every call; safe to start / stopoxide-policeat runtime)- The officer's job is in
Config.AllowedJobs
Workflow:
Officer opens the dashcam. The client mounts the recorder NUI overlay and pre-fetches the officer's recorder name (first + last from olink.character.GetName).
Officer presses F8 (configurable via Config.Recording.hotkey) to start recording. A second press stops it.
The recorder auto-stops at Config.Recording.maxDurationS (default 45 seconds) regardless of user input.
On stop, the clip is uploaded:
Config.Recording.provider = 'fivemanage'(default) — the upload URL is fetched fromoxide-police'sGetVideoUploadUrlsexport and the clip is sent to Fivemanage. This requires theFIVEMANAGE_MEDIA_API_KEYconvar in yourserver.cfg(the same keyoxide-policeuses for mugshots and bodycam).Config.Recording.provider = 'discord'— the clip is forwarded tooxide-police'sUploadVideoToDiscordexport. Clips aboveConfig.Recording.discordMaxBytes(default 25 MiB) are rejected.
A thumbnail is captured at Config.Recording.thumbnail.captureSeconds (default 1.0 s into the clip) at Config.Recording.thumbnail.quality (default 0.7) if Config.Recording.thumbnail.enabled = true.
The metadata (plate, vehicle model label, recorder character id, deploying officer character id) is forwarded to oxide-police's SaveVideoRecording export. The plate is validated server-side against the active deployments before being written to metadata.deployedBy.
Closing the dashcam (or losing the feed) automatically flushes and unmounts the recorder.
Audit Log and Live Event Feed
Every state change writes a row into baitcar_events. The full set of event_type values:
| Event | Logged when |
|---|---|
deployed | Chip installed on a vehicle |
lock / unlock | Doors toggled |
engine_off / engine_on | Engine kill toggled |
steering_off / steering_lock / steering_left / steering_right | Steering mode set |
speed_limit_on / speed_limit_off | Limiter toggled |
strobe_on / strobe_off | Strobe toggled |
alarm_on / alarm_off | Alarm toggled |
horn | One-shot horn fired |
triggered | Drive-off detected (one-shot per deployment) |
chip_removed | Civilian successfully rolled the search |
decommissioned | Officer cleared the deployment |
arrest | Reserved (not currently emitted by any code path) |
Rows include officer_charid, plate, model, the optional suspect_charid, an optional JSON coords object, and an optional JSON extra object. Indexes on officer_charid, plate, and event_type keep audit queries fast.
In addition to the database row, every Events.Log call broadcasts a oxide:baitcar:client:event event to every authorized-job client. The Vue dashboard subscribes to this and renders the event feed in real time. Non-officers never receive the broadcast.
Officer Blips
When an officer's job becomes authorized (on login or olink:client:jobChanged), the client fetches the active deployment list and attaches a map blip to each bait-car entity. The blip uses Config.OfficerBlip:
sprite(default225)color(default5)scale(default0.9)label— combined with the plate as<label> [<plate>]
Blips are non-short-range so they're visible at any zoom level. They auto-clear when the deployment is removed or the resource stops.