Vehicles & driving
Armed vehicles and weapon Lua API
Mounted weapons in multiplayer: driver authority, shots and damage, supported models, protocol 1.24, current limitations and twelve read-only client Lua APIs for weapon selection, ammo, heat and turret aim.
On this page16 sections
- Release and compatibility
- How multiplayer weapon sync works
- Model-specific armament
- Current damage policy
- Handheld guns on cars another player drives
- Start with a known model
- Manifest and runtime
- Function reference
- Model information
- Vehicle snapshot
- Weapon fields and indices
- Ammunition availability
- Turret aim
- Errors and streaming
- Diagnostics and compatibility
- Troubleshooting
- Sources
Read mounted armament from client Lua for vehicle HUDs and diagnostics. The API distinguishes declared model mounts, attached weapon objects and the driver's selected weapons. Find model IDs in the spawn catalogue.
Release and compatibility
Requires client and server 2.31.13+op77.53 or later, with compatible network protocols, on Cyberpunk 2077 2.31 / build 23100. The feature was introduced with protocol 1.24; protocol 1.23 cannot join a 1.24 session.
This is experimental functionality, not a claim that every armed model
or visual effect is fully validated. Keep open77_vehicles loaded for the shared
vehicle helpers and diagnostic commands. Native replication runs in the client
and server binaries; a HUD resource does not need to forward shots itself.
How multiplayer weapon sync works
- A server resource creates a canonical network vehicle. Model declarations come from the server's extracted catalogue, never a client-supplied list of permitted weapons.
- The seated driver with the current vehicle authority lease supplies actual mounted-shot events, selection and supported turret inputs. Merely simulating the chassis, sitting in a passenger seat, or reading a Lua snapshot does not grant weapon authority.
- The server checks the driver, seat, authority generation, routing bucket, model/slot, sequence, firing cadence and bounded geometry before accepting a shot. Accepted projectiles carry flight updates and a terminal impact, explosion or expiry; expiry alone is not an explosion.
- Observers receive weapon state and presentation events. Their effects do not spawn a second damaging projectile. Hit reports identify targets, not damage amounts; the server admits and prices damage through its normal combat rules.
Selection and in-flight state can be restored when a vehicle enters a client's stream. Completed shots are not replayed as new attacks. Seat exit, ownership changes, destruction and disconnect clear weapon selection or reject stale controller messages. Already-admitted projectiles may finish after the driver exits; getting out is not a way to cancel their consequences.
Do not call native NPC shooting or spawn another damaging attack to make an observer look like it fired. That duplicates gameplay instead of adding a visual effect. There is currently no public Lua mounted-weapon fire/select/ammo setter. Use the read-only API for HUDs; use server-side vehicle and combat policy for shared world changes.
Model-specific armament
Examples of model-specific armament. Other appearances can attach a different subset of declared mounts.
| Model | Armament |
|---|---|
Vehicle.v_militech_basilisk |
Four attached mounts, with cannon, missile and countermeasure projectile streams. |
Vehicle.v_sport1_herrera_outlaw_heist_player |
Two mounted machine guns. |
Vehicle.v_sport2_mizutani_shion_nomad_player_missiles |
Nine declared mounts; attached indices 5/6 are MGs and 8 is a missile launcher. |
Remote and passenger ammunition, heat, reload and lock details are not replicated. Observer weapon deployment and audio may differ from the driver's view; NPC damage and overlapping missile-hit association are experimental. weapon.active reports selection, not visible mesh deployment. Display unavailable native data as unavailable rather than estimating it.
Current damage policy
These are Open77 .53 server balance values, not extracted native damage prices and not a guarantee of final applied damage. Normal team, god-mode, invulnerability and Lua damage arbitration still apply.
| Weapon category | Nominal player HP | Nominal vehicle health fraction |
|---|---|---|
| Cannon | 70 | 0.20 |
| Missile | 30 | 0.06 |
| Countermeasure | 15 | 0.025 |
| Machine gun | 12 | 0.012 |
Damage requires an admitted projectile terminal and a plausible canonical target in the same bucket. Repeated projectile/target receipts are deduplicated; clients cannot set the damage value. NPC damage remains experimental.
A mounted missile, cannon or exploding round that the server admitted also moves nearby cars and
knocks down NPCs on every screen through the open77_weapons blast relay, which listens to the
reserved server event onVehicleWeaponExplosion(playerId, vehicleId, weapon, x, y, z, bucket, recordRadius, projectileId). See the blast relay.
Handheld guns on cars another player drives
Server option, off by default, experimental. A car's health belongs to the client that
simulates it, so a pistol round fired at a car someone else drives does not change it. The server
option combat.handheldVehicleDamage (in server.jsonc, default false) makes the server apply
those hits itself:
"combat": {
"handheldDischarges": true, // default; required
"handheldVehicleDamage": true // default false
}
Each round must be one the server admitted for the shooter's drawn weapon at that weapon class's
fire rate (combat.handheldDischarges, default true), each round damages a given car once, and
the price is fixed per class, as a fraction of the car's health: sniper 0.090, precision rifle
0.065, shotgun 0.060, revolver 0.045, handgun 0.028, assault rifle 0.020, SMG 0.015, machine gun
0.012. Melee weapons, grenade launchers and grenades are not priced this way. Invulnerable,
destroyed and exploded cars refuse, a car the shooter sits in or simulates is never a target, and
there is no Lua callback: protect a car with its invulnerable state
(Open77.vehicles.flags.invulnerable). An immortal car keeps a small health floor.
Start with a known model
Use the catalogue's spawn examples
to create a network vehicle from the server console or a resource with
world.vehicles. Creating a vehicle does not seat a player or prove its current
appearance attaches every declared mount. Enter normally or use the existing
server-authoritative seat APIs, then inspect it with getWeaponState.
Selection and firing follow the player's vehicle bindings. Do not hard-code weapon-selection keys. Use the exact armed model record: ordinary Outlaw or Shion variants may not carry weapons.
Manifest and runtime
resource "vehicle_weapon_hud"
version "1.0.0"
client_script "client/main.lua"
permissions { "vehicles.read" }
All functions below are client-side, synchronous, read-only calls under Open77.vehicles.
They return data directly, not a Promise. They cannot spawn a vehicle, select/fire an arm,
change ammunition or apply damage. Network authority remains on the server.
Pass the canonical network vehicle ID from Open77.vehicles.getPlayerSeat().vehicleId
or Open77.vehicles.all()[i].id. Do not pass the entity, engineEntity, weapon ID
or slot ID. Model queries instead take the full Vehicle.* record string.
Function reference
The searchable client vehicle API reference contains the individual signatures and examples. There are no server versions of these twelve armament getters.
| Function | Arguments | Result |
|---|---|---|
Open77.vehicles.getWeaponModel |
record |
Model table, or nil, reason. Reads the weapon mounts declared by a vehicle model. |
Open77.vehicles.modelHasWeaponMounts |
record |
boolean, or nil, reason. Checks whether a vehicle model declares any weapon mounts. |
Open77.vehicles.getWeaponState |
vehicleId |
Snapshot table, or nil, reason. Reads one coherent mounted-weapon snapshot. |
Open77.vehicles.isArmed |
vehicleId |
boolean, or nil, reason. Checks whether a streamed vehicle actually has attached weapons. |
Open77.vehicles.getWeapons |
vehicleId |
Array of weapon tables, or nil, reason. Lists the weapon objects actually attached to a streamed vehicle. |
Open77.vehicles.getActiveWeapons |
vehicleId |
Array of weapon tables, or nil, reason. Lists the attached weapons selected by the driver. |
Open77.vehicles.getWeapon |
vehicleId, mountIndex |
Weapon table, or nil, reason. Reads one weapon mount by its stable one-based index. |
Open77.vehicles.getWeaponAmmo |
vehicleId, mountIndex |
Ammo table, or nil, reason. Reads available native ammunition counters for a mounted weapon. |
Open77.vehicles.getWeaponType |
vehicleId, mountIndex |
Category string, or nil, reason. Reads a mounted weapon's stable category. |
Open77.vehicles.isWeaponActive |
vehicleId, mountIndex |
boolean, or nil, reason. Checks whether the driver selected a particular attached mount. |
Open77.vehicles.getWeaponCount |
vehicleId |
integer, or nil, reason. Counts weapon objects attached to a streamed vehicle. |
Open77.vehicles.getWeaponAim |
vehicleId |
Aim table, or nil, reason. Reads the Basilisk's captured or replicated turret inputs. |
Failures return nil, reason, including boolean queries. Test value == nil when
you need to distinguish an error from a valid false.
Model information
local model, reason = Open77.vehicles.getWeaponModel(
"Vehicle.v_militech_basilisk")
if not model then print(reason); return end
print(model.hasWeaponMounts, model.declaredCount)
for _, mount in ipairs(model.mounts) do
print(mount.index, mount.record, mount.slot, mount.type)
end
A model result contains record, hasWeaponMounts, declaredCount and mounts.
Mount entries include the static identification and timing fields described below;
they have no attached object or live ammo in a model-only query.
A declared mount is not proof of an equipped gun. Some game records share a list of
nine possible mounts while their current appearance attaches only three. Use isArmed(id)
or getWeapons(id) to inspect a real streamed vehicle. A valid unarmed model returns
hasWeaponMounts=false; an invalid/unknown model returns an error.
Vehicle snapshot
getWeaponState(vehicleId) returns:
| Field | Meaning |
|---|---|
vehicleId, record |
Canonical vehicle ID and model record. |
streamed |
The native vehicle is attached in this client's world. |
localDriver |
This client currently captures the settled, leased driver. |
hasWeaponMounts, declaredCount |
Static model declarations. |
armed, attachedCount |
Whether actual weapons are attached and how many. Omitted if the vehicle is not streamed. |
selectionKnown |
Whether native-local or received driver selection is available. |
activeMask, activeCount |
Selected mount bits and count of attached selected weapons. Omitted when selection is unknown. |
mounts |
All declared mounts, in stable model order, including unattached ones. |
aim |
Optional Basilisk turret input table: yaw, pitch, source. |
Reads of the same live vehicle share a short cache (at most 100 ms). Polling at 10 Hz is enough for HUD values; repeatedly calling every getter on every rendered frame does not provide more precise data. Tables are copies, so changing them in Lua changes nothing in the game.
The activeMask uses bit index - 1. Prefer weapon.active to manual bit operations.
Selection means selected by the driver, not “currently firing”, “has ammunition” or
“the remote mesh has finished its deployment animation”.
Weapon fields and indices
getWeapons and getActiveWeapons return dense arrays, but their array positions are
not mount identifiers. Always pass the returned weapon.index to single-weapon getters:
local weapons, reason = Open77.vehicles.getWeapons(vehicleId)
if weapons then
for _, weapon in ipairs(weapons) do
local ammo = Open77.vehicles.getWeaponAmmo(vehicleId, weapon.index)
print(weapon.index, weapon.type, ammo and ammo.source)
end
end
| Field | Meaning |
|---|---|
index |
One-based position in the model's declared mount list. Stable within this model; not across models. |
weaponId, slotId |
Numeric 40-bit TweakDB IDs. Two mounts may share one weapon item. |
record, slot |
Known Items.* and AttachmentSlots.* names; empty for an unknown extension, while IDs remain usable. |
type |
machine_gun, cannon, missile, countermeasure or unknown. |
attached |
The matching weapon object currently exists in this slot. |
active |
This attached mount is selected; omitted if selection is unknown. |
cycleSeconds |
Model's native firing-cycle parameter, not a remaining cooldown timer. |
burstProjectiles |
Declared number of projectiles per native burst; not an ammo count. |
ammo |
Availability/source and optional native counters, described below. |
overheat, overheated |
Optional raw native OverheatPercentage and forced-overheat-cooldown flag. |
reloadPending |
Optional native pending-reload flag; not a progress timer. |
targetLocked |
Optional native target-lock flag; not a target's network identity. |
triggerMode, itemType |
Optional native trigger-mode and item-type record names. Separate from the stable type category. |
Ammo/heat/trigger details are read from the local driver's actual weapon. They are not currently transmitted in the weapon-state packet. A passenger or observer must not display the untouched counters of its own visual weapon object as the driver's remaining ammunition.
Ammunition availability
getWeaponAmmo(id, index) returns an object even when counters are unavailable:
| Field | Meaning |
|---|---|
available |
At least one valid native magazine/total counter was read for the local driver. |
source |
native_local, not_replicated, not_attached or unavailable. |
magazine |
Optional rounds currently in the native magazine. Zero is a valid reading. |
capacity |
Optional native magazine capacity. |
total |
Optional native total-ammunition count. |
reserve |
total - magazine, only when both exist and total is not smaller than magazine. |
These are native counters, not a server inventory ledger. Some mounted weapons use a heat/cooldown cycle instead of conventional finite magazines. A zero or missing counter must not be interpreted as proof that firing is disabled, and this API does not fabricate an “infinite ammunition” flag.
In the tested Basilisk and Shion mounted weapons, total reports 2147483647
(Int32 maximum), while magazines still change after firing. Treat this as a native
special-case counter, not as a player carrying two billion spare rounds. Prefer
the magazine/capacity and heat fields for those HUDs.
local function ammoLabel(ammo)
if not ammo or not ammo.available then return "--" end
if ammo.magazine ~= nil and ammo.capacity ~= nil then
return ("%d / %d"):format(ammo.magazine, ammo.capacity)
end
return ammo.total ~= nil and tostring(ammo.total) or "--"
end
CreateThread(function()
while true do
local seat = Open77.vehicles.getPlayerSeat()
if seat then
local weapons = Open77.vehicles.getActiveWeapons(seat.vehicleId)
for _, weapon in ipairs(weapons or {}) do
-- Replace print with your HUD update, preferably only on changes.
print(weapon.type .. " · " .. ammoLabel(weapon.ammo))
end
end
Wait(100)
end
end)
Turret aim
getWeaponAim(id) exposes the Basilisk's named cannon graph inputs in degrees relative
to the chassis, not world coordinates or a muzzle ray. source is native_local
for the capturing driver or replicated for a received state. Unsupported models and
unknown state return nil, "weapon_aim_unavailable".
Errors and streaming
| Reason | Action |
|---|---|
permission_denied:vehicles.read |
Declare the read permission in the resource manifest. |
vehicle_weapons_unavailable |
This host/client does not provide the backend; use a compatible client build. |
invalid_vehicle_record, unknown_vehicle_record |
Supply a real Vehicle.* model, not an item ID. |
invalid_vehicle_id, unknown_vehicle |
Use a currently known canonical network vehicle ID. |
vehicle_not_streamed |
Wait for the native projection to attach; do not treat this as an unarmed vehicle. |
invalid_weapon_index |
Use a one-based integer from weapon.index for this model. |
weapon_state_unavailable |
Driver selection has not arrived; retry later. |
weapon_aim_unavailable |
No supported/current turret-angle source exists. |
A streamed but unarmed vehicle returns false from isArmed, zero from
getWeaponCount, and an empty array from getWeapons. A vehicle unknown after a
stream-out returns unknown_vehicle; a known pending projection can still provide its
model through getWeaponState with streamed=false.
Diagnostics and compatibility
With open77_vehicles loaded, the read-only event
open77:vehicles:weapons:probe logs up to 32 known vehicles using these public Lua APIs.
It reports mount/selection counts and the ammo source, allowing a HUD bug to be separated
from missing native data.
The server console or an authorized administrator can request it with
vehicle.weapons.probe <playerId>. This uses the ordinary restricted command/event
path; it does not require enabling local debug mutations during an active session.
Use type(Open77.vehicles.getWeaponState) == "function" for feature detection when
supporting older clients. These calls belong to the client runtime; they do not add
server exports or authorize client-side firing, ammo mutation or damage.
Troubleshooting
| Symptom | Check |
|---|---|
| Vehicle has declared mounts but no guns | Inspect streamed, attachedCount and the exact model/appearance. The catalogue is not an equipment guarantee. |
| A passenger's ammo reads unavailable | Expected: source="not_replicated". Show --; do not use an observer's native magazine as the driver's ammo. |
| Selection is missing immediately after streaming | Wait for selectionKnown; distinguish unknown state from a valid unselected weapon. |
| The wrong weapon is inspected | Pass weapon.index, not the position in the dense result array. Shion indices 5/6/8 are not 1/2/3. |
| A shot has no applied damage | Check current seat/lease, bucket, target protection and accepted terminal/hit evidence. A visible effect is not proof of admitted damage. |
| Client cannot join after updating | Confirm both peers use protocol 1.24 and a matching release before investigating the model. |
Sources
// For agents & LLMsthis page as Markdownllms.txtllms-full.txt
