Quick navigation

Go to

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

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

  1. 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.
  2. 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.
  3. 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.
  4. 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

// Guides · Lua API

Search the docs

1,905 guides and functions indexed

Try