Weapons & consumables
Weapon Lua API
Weapon templates, slots, holstering and server assignments.
On this page10 sections
- Grenades and healing charges
- Client permissions and methods
- Server-targeted API
- Weapon components: scopes, muzzles and mods
- Gadgets: grenades in the quick slots
- Explosions on every screen: the blast relay
- Reading a player's weapons from the server, synchronously
- It is a cache, and the staleness is yours to judge
- onPlayerWeaponChanged
- How the cache is fed
- ACL-gated admin commands
- Authority and failures
- The admin package's weapons screen
Manage the local player's three EquipmentArea.Weapon slots with the asynchronous client API. Use open77_weapons for authenticated server-to-owner requests and exact TweakDB IDs such as Items.Preset_Lexington_Default.
For reload speed, recoil, advanced native statistics and impact blasts,
see Weapon customization and the English
rp_weapons_effect workshop.
Tuning requires a compatible development client and uses separate, resource-owned
setTuning, clearTuning and tuning calls.
The three-slot surface intentionally rejects heavy weapons (WeaponHeavy) and
arm cyberware (ArmsCW): those equipment areas have different REDengine
lifecycles. Grenades are not weapon slots either β they are QuickSlot
gadgets thrown from a hotkey β and have their own calls, below.
Parts that go onto a weapon (a scope, a silencer, a mod) are
components.
Grenades and healing charges
Use Open77.consumables to configure usable grenade and
healing charges, select MaxDoc or Bounce Back, equip or unequip the hotkey,
and control regeneration. giveGadget changes inventory membership; its stack
count is not the number of throws. The consumables guide covers asynchronous
completion, actual-use events and server-authoritative healing.
Client permissions and methods
Declare player.weapons.read for slots, snapshot, and all. Declare
player.weapons.edit for mutation.
| Method | Purpose |
|---|---|
Open77.weapons.slots() |
Return { 1, 2, 3 }. |
Open77.weapons.assign(record, slot, options?) |
Put a template in one exact slot. |
Open77.weapons.setActive(slot) |
Select and draw an assigned slot. |
Open77.weapons.setActive(record, options?) |
Reuse or equip a template, then draw it. |
Open77.weapons.activate(...) |
Alias of setActive. |
Open77.weapons.remove(slot) / unequip(slot) |
Clear a slot without deleting inventory. |
Open77.weapons.holster() |
Holster without changing the loadout. |
Open77.weapons.setAmmo(slot, amounts) |
Set exact spare rounds and/or magazine rounds. |
Open77.weapons.snapshot() / all() |
Request every slot's verified state. |
Open77.weapons.setComponent(slot, part, options?) |
Install a scope, muzzle or mod on the weapon in a slot. |
Open77.weapons.removeComponent(slot, attachmentSlot) |
Empty one attachment slot of that weapon. |
Open77.weapons.components(slot) |
List that weapon's attachment slots, taken or empty. |
Open77.weapons.giveGadget(record, count?, options?) |
Grant grenades and put them on the throw hotkey. |
Open77.weapons.takeGadget(record?, count?) |
Take grenades back. |
Open77.weapons.gadgets() |
List the quick slots. |
The bundled open77_weapons resource adds one more as an export:
exports.open77_weapons:clear() empties all three slots at once. It is three
native removals aggregated into one answer rather than three the caller has to
correlate, and a slot that was already empty is not a failure.
assign options are active (default false) and addToInventory (default
true). Template-form setActive accepts slot as a placement fallback and
addToInventory; if the template is already assigned, its existing slot wins.
setAmmo accepts { reserve = 120, magazine = 18, activate = true }. At least
one count is required; an omitted count is preserved. reserve is the spare
HUD count, while magazine is the loaded count and may not exceed the live
weapon capacity. activate = true selects and draws the requested slot when
needed; otherwise a non-instantiated weapon returns weapon_not_drawn.
local requestId, reason = Open77.weapons.assign(
"Items.Preset_Lexington_Default", 1,
{ active = true, addToInventory = true })
assert(requestId, reason)
Open77.weapons.assign("Items.Preset_Katana_Default", 2)
Open77.weapons.setActive(2)
Open77.weapons.setAmmo(1, { reserve = 120, magazine = 18, activate = true })
The returned request ID only proves that the request was validated and queued.
EquipmentSystemPlayerData owns the actual loadout and runs in REDscript, so
completion is asynchronous:
AddEventHandler("open77:weapons:completed", function(
requestId, operation, accepted, reason, slot, record, tweakDbId, active, drawn,
ammoRecord, ammoTweakDbId, ammoTotal, ammoReserve, magazine, capacity)
if accepted ~= "true" then
print("weapon request failed", operation, reason)
end
end)
Client event arguments use the generic string event transport. Compare boolean
fields with "true", IDs with tostring(requestId), and retain tweakDbId as
an opaque hexadecimal string.
A snapshot emits one state row per slot before its completion:
AddEventHandler("open77:weapons:state", function(
requestId, slot, record, tweakDbId, active, drawn, locked,
ammoRecord, ammoTweakDbId, ammoTotal, ammoReserve, magazine, capacity)
print(slot, record, active, drawn, locked)
end)
An empty tweakDbId means an empty slot. record is restored for templates
previously validated through this API; it may remain empty for an item equipped
by another pipeline because shipping REDengine builds do not always retain the
reverse TweakDB name table. Use equipped/tweakDbId as the presence test.
Ammo fields report the shared total, spare reserve, loaded magazine and live
capacity. An inactive slot can still report its ammo type and total, but its
reserve/magazine/capacity are unavailable (-1) until the weapon object exists.
Server-targeted API
A server resource declares:
dependency "open77_weapons >=0.1.0"
permission "network.events"
It can then target the authenticated owner client:
local requestId, reason = Open77.weapons.assign(
playerId, "Items.Preset_Lexington_Default", 1, { active = true })
assert(requestId, reason)
Open77.weapons.setActive(playerId, 1)
Open77.weapons.setActive(
playerId, "Items.Preset_Katana_Default", { slot = 2 })
Open77.weapons.remove(playerId, 3)
Open77.weapons.holster(playerId)
Open77.weapons.setAmmo(
playerId, 1, { reserve = 120, magazine = 18, activate = true })
Open77.weapons.requestSnapshot(playerId)
-- Every slot at once: the RemoveAllPedWeapons equivalent.
Open77.weapons.clear(playerId)
clear is one request, not three remove calls. The owner runs the three
native removals itself and answers once, so there is one request id to correlate
instead of three and no way to end up half-cleared because one leg was refused. A
slot that was already empty answers weapon_slot_empty natively and is not
counted as a failure β "remove every weapon" succeeded on a slot that had none.
The completion's result carries cleared and a slots array of
{ slot, accepted, reason }.
Weapon components: scopes, muzzles and mods
A scope, a silencer or a generic mod is a part sitting in an attachment
slot of the weapon item. 2.31 installs one through the vanilla
ItemModificationSystem β the scripted system the inventory screen queues
InstallItemPart / RemoveItemPart into β over two natives,
TransactionSystem.ForcePartInSlot and RemovePart. Open77 drives exactly that
path from the bridge script, so a part installed here is a real part: it shows
on the owner's weapon, it survives holstering, and the vanilla inventory screen
sees it as its own.
The vocabulary is TweakDB's, not Open77's:
| What | Records | Examples |
|---|---|---|
| Slots | AttachmentSlots.* |
Scope, PowerModule (muzzles), Power_Handgun_WeaponMod1 / ..2 (generic mods, one pair per evolution and class) |
| Parts | Items.* tagged itemPart (928 on 2.31) |
Items.w_att_scope_short_01, Items.w_att_scope_long_02, Items.w_silencer_01, Items.w_muzzle_brake_01 |
Which slots a given weapon has comes from its blueprint, and components reads
it off the live item rather than guessing: a Lexington declares Scope,
PowerModule and the two Power_Handgun_WeaponMod slots beside its receiver,
barrel and magazine; a katana declares Blade_WeaponMod1/2 and Grip. The
full tables β 59 slots, 928 parts, every Items.* weapon's declared slots β are
in docs/generated/attachment-slots-2.31.csv, weapon-parts-2.31.csv and
weapon-part-slots-2.31.csv.
-- Client: a short scope on the rifle in slot 1, then read the slots back.
local requestId, reason = Open77.weapons.setComponent(1, "Items.w_att_scope_short_01")
assert(requestId, reason)
Open77.weapons.components(1)
AddEventHandler("open77:weapons:part", function(
requestId, slot, attachmentSlot, attachmentSlotId, taken, base, record, tweakDbId)
-- one row per attachment slot; `base == "true"` is the weapon's own receiver,
-- barrel or magazine, which is not a mod and cannot be removed
print(slot, attachmentSlot, taken, base, record)
end)
setComponent without options.attachmentSlot picks the first of the part's
own placement slots that this weapon declares, empty ones first β so a generic
mod finds the pistol slot on a pistol, where vanilla's GetPlacementSlot would
name the rifle one. Naming the slot is refused by name when it is not on the
weapon (slot_not_on_weapon), when the part cannot go there
(part_does_not_fit_slot), or when it holds a base part (base_part_slot). A
part the player does not own is created for the install, as the inventory
screen's is consumed by it; a part already in the slot is swapped out and
deleted, exactly as the vanilla screen does. The completion carries the changed
slot as result.part and a components request carries result.parts.
-- Server: the armoury fits a silencer to whatever is in slot 1, then lists it.
Open77.weapons.setComponent(playerId, 1, "Items.w_silencer_01",
{ attachmentSlot = "AttachmentSlots.PowerModule" })
Open77.weapons.requestComponents(playerId, 1)
Open77.weapons.removeComponent(playerId, 1, "AttachmentSlots.PowerModule")
-- The synchronous read, from the cache the owner keeps fed.
local fitted = Open77.weapons.components(playerId, 1)
for _, part in ipairs(fitted and fitted.parts or {}) do
print(part.attachmentSlot, part.record)
end
The owner's loadout report carries the installed, non-base parts of each slot,
so Open77.weapons.get(playerId).slots[n].parts and
Open77.weapons.components(playerId, n) answer without a round trip, dated by
the same loadoutAgeMs as the slot list.
What other players see. A part changes the owner's own weapon and the
server's cache. It does not change what other players see on the owner's
proxy: the proxy weapon is rebuilt from the ItemID the 20 Hz snapshot carries
(TweakDBID, seed, counter, structure), and parts are per-inventory-item data the
ItemID does not encode. Shipping that visual is a wire change and is not in
this release; a server that needs remote players to see the scope has no path
yet, and one that needs to know about it has the cache.
Gadgets: grenades in the quick slots
Grenades are EquipmentArea.QuickSlot gadgets, not weapon slots. giveGadget grants a counted stack and equips quick slot 1 unless equip = false. Use a grenade record such as Items.GrenadeFragRegular, Items.GrenadeEMPRegular or Items.Preset_Grenade_Smoke_Default; catalogue entries use category = grenade.
-- Client
Open77.weapons.giveGadget("Items.GrenadeFragRegular", 3) -- three, equipped, on RB
Open77.weapons.takeGadget("Items.GrenadeFragRegular", 1) -- one back
Open77.weapons.takeGadget() -- every one of the active gadget
Open77.weapons.gadgets()
AddEventHandler("open77:weapons:gadget", function(
requestId, quickSlot, record, tweakDbId, quantity, active, difference)
-- requestId "0": the engine consumed one (the player threw it); `difference`
-- is the change and `quantity` the count left
end)
-- Server: a medic's kit, counted.
Open77.weapons.giveGadget(playerId, "Items.GrenadeFragRegular", 2, { equip = true })
Open77.weapons.requestGadgets(playerId)
-- And the cache: the count after the last throw, with its own age.
local kit = Open77.weapons.gadgets(playerId)
if kit then print(kit.active, #kit.slots, kit.reportedAgeMs) end
AddEventHandler("onGadgetConsumed", function(playerId, record, remaining)
print(playerId .. " threw " .. record .. ", " .. remaining .. " left")
end)
The consume half, and what a server sees of the explosion. A throw is the
engine removing one unit from the stack. The bridge script registers an
inventory listener on the local player (the same InventoryScriptCallback the
vanilla currency and shard toasts use), reports the shrink with request id 0,
and the open77_weapons client resource β which knows whether one of its own
takeGadget requests is in flight β pushes it to the server on the reserved
open77:weapons:gadgets transport as a consumption. The host republishes it as
onGadgetConsumed(playerId, record, remaining[, tweakDbId]), a reserved name a
resource cannot forge, and refreshes the cache behind gadgets().
Native grenade explosions already replicate through the thrower's snapshot and report hits to the damage arbiter. Do not also call Open77.effects.explosion for the same grenade: that would apply damage twice. Each detonation a player's client observed is republished on the server as the reserved event onPlayerExplosion(playerId, sequence, x, y, z, count, bucket). It is the owner's report, not a verdict: a resource that must trust it pairs it with a consumed grenade, which is exactly what the blast relay does before it moves anything. Healing inhalers and other Consumable items are outside this API; apply healing through server-owned stats.
Explosions on every screen: the blast relay
Only the client that simulates a car can move it, and a vanilla explosion runs its physics on one
client: the grenade's thrower, the missile's shooter, the simulator of the car that blew up.
Without help, only that screen sees cars jump and NPCs fall. The server side of open77_weapons
relays every explosion the server can vouch for:
| Source | Accepted when |
|---|---|
Grenade: onPlayerExplosion |
paired with a frag, incendiary or cutting grenade the same player spent, reported by onConsumableUsed (a grenade charge) or onGadgetConsumed (a quick-slot stack that shrank): one detonation per grenade, within 80 m of where it was thrown (160 m for a sticky one) and 160 m of the thrower, in the thrower's bucket, three at once then one every two seconds |
Car: its canonical exploded flag rising |
once per explosion; a wreck created as a wreck is not one |
Mounted weapon: onVehicleWeaponExplosion |
the vehicle weapon ticket admitted the projectile's explosion (missiles, cannons and exploding rounds) |
exports.open77_weapons:relayBlast(request) |
another server resource admitted it |
For each accepted blast the relay sends open77_weapons:blast to the physics owner of every other
car in reach, listing that owner's cars (its client pushes them with
Open77.weapons.applyBlast, from its own poses),
grants an ownerless car to the source, and tells the nearest players within 150 m of the blast
radius to knock down their own copies of living network NPCs in range. The source's own cars and
NPC copies are not repeated: its engine already moved them. Radius and impulse come from server
tables per explosion family (frag: radius 5.5 m); no client supplies them, and the relay deals no
damage. A client without Open77.weapons.applyBlast ignores the relay.
Knocking players down is a gamemode rule and is off until a gamemode asks for it:
-- Server; publish again whenever this resource or open77_weapons (re)starts.
local function publishBlastPolicy()
local promise, reason = Open77.exports.call("open77_weapons", "setBlastPolicy", {
players = true, -- launch players in reach
safeZones = { { x = -1448, y = 96, z = 17, radius = 30 } },
buckets = { 0 }, -- optional
})
if not promise then print("blast policy not sent: " .. tostring(reason)); return end
local accepted, failure = promise:await()
if accepted ~= true then print("blast policy refused: " .. tostring(failure)) end
end
AddEventHandler("onResourceStart", function(name)
if name == GetCurrentResourceName() or name == "open77_weapons" then CreateThread(publishBlastPolicy) end
end)
The launch is decided twice: when the blast is admitted and again just before each launch, so a
player who reached a safe zone meanwhile, or a policy turned off meanwhile, is not launched.
Freeroam does this with its spawn safe zones and the street bucket (its combat.blastKnockdown
setting, on by default). A policy lapses when its resource stops; players = false also forbids
explicit launches requested through relayBlast.
| Control | Effect |
|---|---|
convar open77_blasts |
off (or 0, false) turns every source off; default on |
/weapon.blasts (ACL command.weapon.blasts) |
prints counters, blasts per kind and refusals by kind:reason |
exports.open77_weapons:blastStats() |
the same counters as a table |
relayBlast(request) lets another server resource relay a blast it admitted itself. The request is
{ kind = "tuned" | "scripted", source = playerId | 0, bucket (when source is 0), x, y, z, radius, push, lift, falloff?, serverSide?, players?, diagnostics?, sequence? }. It refuses anything outside
the client relay's own limits (radius above 0 and at most 60 m, push and lift 0β40 m/s with a
non-zero sum, falloff 0β4, default 1), a centre more than 250 m from the source, or a dead source
for tuned. It answers { vehicles, owners, characterHolders, accepted, rejected, reasons, closest, ... } or nil, reason (disabled, invalid_blast, invalid_source, invalid_bucket,
position_unavailable, body_unavailable, out_of_range, global_rate, source_rate). It is
rate-limited per source player, or per invoking resource for a world blast.
-- Server: a scripted blast at a fixed point in bucket 0, pushing cars and knocking down NPCs.
RegisterCommand("testblast", function(source)
local result, reason = exports.open77_weapons:relayBlast({
kind = "scripted", source = 0, bucket = 0,
x = -1442.2, y = 127.4, z = 18.0, radius = 8, push = 10, lift = 8,
})
if not result then print("blast refused: " .. tostring(reason)); return end
print(("blast reached %d cars across %d owners"):format(result.vehicles, result.owners))
end, true)
Reading a player's weapons from the server, synchronously
Everything above is a request: the answer comes back later, on an event. That is unusable inside a decision. An anticheat check, a shop validating a sale and an inventory sync reconciling a slot all have to conclude now, and an answer that arrives ten milliseconds later is an answer to a question nobody is still asking.
Open77.weapons.get(playerId) answers immediately:
-- manifest: permission "player.weapons.read"
local weapons, reason = Open77.weapons.get(playerId)
if weapons == nil then return end -- "weapons_unreported" when nothing is known
print(weapons.active, weapons.activeRecord, weapons.drawn, weapons.magazine)
for _, slot in ipairs(weapons.slots) do
print(slot.slot, slot.record, slot.active, slot.drawn, slot.ammo and slot.ammo.magazine)
end
| Field | Meaning |
|---|---|
slots |
one row per slot 1..3, in the shape the relay's snapshot already answers with |
active |
the selected slot, or absent when none is known |
activeRecord, activeTweakDbId |
the selected weapon; the id uses the same 0x%016X spelling as the relay |
drawn |
the weapon is in hand. Holstering clears this without clearing active |
magazine |
rounds in the drawn weapon; absent when the client did not report it |
reportedAgeMs |
how old active, drawn and magazine are |
loadoutAgeMs |
how old slots is |
fresh |
reportedAgeMs passes the same two-second rule Open77.players.get applies |
source |
report, snapshot, or the call fails |
It is a cache, and the staleness is yours to judge
Weapons are client-owned. They live in the client's REDengine equipment system; the server keeps no copy of that state and has no way to verify one. Every field above is as true as the owner's last report, and a client can lie about its own weapons exactly as it always could. Read this to decide β refuse a sale, flag an anomaly, reconcile an inventory β never to assert.
Which is why there are two ages rather than one, and why mixing them up is the first mistake a caller makes:
reportedAgeMsdates the drawn weapon, whether it is in hand, and the magazine. Those ride the ordinary 20 Hz player snapshot, which the client already sends β no extra traffic, no request. While a player is simulating this is tens of milliseconds; it stops advancing the moment they are not.loadoutAgeMsdates the slot list, which the owner pushes only when the loadout changes. An untouched loadout is legitimately minutes old and that is not a fault. Rejecting it for age would reject every player who has not swapped a gun since they connected.
source tells you which halves you have. report means the owner has described
its slots at least once. snapshot means only the packet's weapon block has
arrived, so activeTweakDbId and drawn are real but slots is empty and
active is unknown β the packet carries an id, not a slot number. A player nobody
has heard anything about at all fails with weapons_unreported, which is
deliberately different from a player carrying nothing.
-- The shape an anticheat actually wants.
local weapons = Open77.weapons.get(playerId)
if weapons and weapons.fresh and weapons.source == "report"
and (weapons.loadoutAgeMs or math.huge) < 60000 then
-- recent enough for this rule; otherwise ask, do not assume
end
onPlayerWeaponChanged
A host-wide server event, fired once per change and never for a report that changes nothing β a player standing still resends the same weapon block twenty times a second, and an event that repeated it would be unusable.
AddEventHandler("onPlayerWeaponChanged", function(playerId, slot, record, drawn)
playerId, slot = tonumber(playerId), tonumber(slot)
if drawn == "true" then print(playerId .. " drew " .. record) end
end)
Arguments are strings, like every other host event. It fires when a slot's
contents change, and when the selection or drawn changes β reported against the
slot concerned. The first report after a connection fires too, so a handler that
only ever hears about changes still learns the initial loadout.
How the cache is fed
Two sources, kept apart on purpose.
The drawn weapon, whether it is in hand, and the magazine come out of the
ordinary player snapshot: weaponTdbId, the WeaponValid / WeaponEquipped state
bits and ammoRemaining are already in that packet, so this half costs no extra
bandwidth and no request at all. WeaponValid clear means "the sender did not fill
this block", never "unarmed" β treating the two as the same is how an anticheat
starts accusing players who are in a loading screen.
Slot membership has no wire field, so it is pushed by the bundled
open77_weapons client resource, which reports after every accepted mutation and
once shortly after start. It does not poll the engine: a snapshot is a real
REDengine round trip, and the loadout changes a few times an hour. Instead it
compares the locally readable drawn weapon (Open77.character.weapon(), a field
read) against what it last reported, twice in a row, and only re-snapshots when
they genuinely disagree β which is what happens when a player loots a gun or swaps
one through the vanilla UI. An untouched loadout costs one table read a second and
zero traffic.
A disconnect forgets everything about that player, so a recycled player id never inherits the previous holder's loadout.
activate and unequip are server aliases. Results return only to the server
resource VM that created the namespaced request:
AddEventHandler("open77:weapons:completed", function(
playerId, requestId, operation, accepted, reason, result)
if not accepted then print(playerId, operation, reason) end
end)
Mutation result contains slot, record, tweakDbId, active, drawn, and
an ammo table with record, tweakDbId, total, reserve, magazine, and
capacity when applicable.
Snapshot result is an array of slot rows that additionally contain
equipped and locked. The relay matches the authenticated source, request
ID, and operation; unanswered calls complete with request_timeout after ten
seconds.
ACL-gated admin commands
The official resource registers:
/weapon.give <playerId|me> <template> [slot=1] [active=true]
/weapon.remove <playerId|me> <slot>
/weapon.ammo <playerId|me> <slot> <reserve> [magazine]
/weapon.clear <playerId|me>
/weapon.read <playerId|me>
They require command.weapon.give, command.weapon.remove,
command.weapon.ammo, command.weapon.clear and command.weapon.read
respectively; the built-in admin role's command.* includes all five.
/weapon.read is the odd one out: it asks the client nothing and prints the
server's cache, including both ages, which is the quickest way to see whether a
loadout report has arrived at all. me is available only
to an in-game issuer, slots are 1..3, and active defaults to true. The
command reports completion only after the authenticated target client verifies
the REDengine result. weapon.ammo draws the slot if needed, preserves the
magazine when omitted, and reports all verified counts. Removal clears the slot
without deleting inventory.
Authority and failures
addToInventory = true creates a local presentation item. It does not grant
or persist authoritative inventory. Server code must validate the record
against its own allowlist, commit ownership first, and only then project it to
the target client. Never forward a client-chosen arbitrary record.
Immediate failures include permission denials, invalid_weapon_template,
template_is_not_weapon, invalid_weapon_slot, player_unavailable,
script_bridge_unavailable, and queue_full. Open77.weapons.get adds
permission_denied (no player.weapons.read), invalid_player_id,
weapons_unavailable (an embedding that does not replicate) and
weapons_unreported; clear adds clear_in_progress from the owner when one is
already running. REDengine completion failures
include unsupported_weapon_area, weapon_slot_locked, weapon_not_owned,
item_creation_failed, equip_rejected, activation_rejected,
weapon_slot_empty, unequip_rejected, and holster_rejected.
Components add invalid_part_template, template_is_not_part,
invalid_attachment_slot (immediate) and slot_not_on_weapon,
part_does_not_fit_weapon, part_does_not_fit_slot, base_part_slot,
install_rejected, remove_rejected (from the owner). Gadgets add
invalid_gadget_template, template_is_not_gadget, invalid_gadget_count
(immediate) and unsupported_gadget_area, gadget_not_owned,
no_active_gadget, equip_rejected, hotkey_rejected,
inventory_update_rejected (from the owner). The synchronous reads add
gadgets_unreported, distinct from weapons_unreported: a loadout report says
nothing about the quick slots.
The admin package's weapons screen
open77_admin adds a second, curated front-end over the same API, reachable
from the compact /admin menu and from chat:
/admin.weap.give <playerId|me> <record|alias> [slot|auto] [reserve]
/admin.weap.ammo <playerId|me> [slot|all] [reserve]
/admin.weap.remove <playerId|me> [slot|all]
/admin.weap.holster <playerId|me>
/admin.weap.catalog
/admin.read.weapons [playerId|me] (alias: /weapons)
Three differences from the commands above are worth knowing before choosing one:
- It takes only catalogue records. 189 of them, filtered at build time to
EquipmentArea.Weaponso nothing in the list can answerunsupported_weapon_area./weapon.givetakes any TweakDB string. - It gives the weapon LOADED.
assign, thensetAmmowith a per-class full load, then one magazine top-up read off the capacity the engine reports back. autopicks the slot from a verified snapshot: the first empty one, and the active one only when all three are full.
resources/system/open77_admin/README.md carries the whole argument, including the
measured per-ammo-type carried ceiling β asking for more than it answers
ammo_update_rejected even though the weapon is equipped and loaded to the cap.
See the 2.31 game-data reference for curated weapon record
names. The exhaustive French catalogue and implementation research live in
docs/weapons.md and docs/research/weapons-and-item-records.md.
// For agents & LLMsthis page as Markdownllms.txtllms-full.txt
