Quick navigation

Go to

Weapons & consumables

Weapon Lua API

Weapon templates, slots, holstering and server assignments.

15 min read3,261 wordsMarkdown
On this page10 sections

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:

  • reportedAgeMs dates 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.
  • loadoutAgeMs dates 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.Weapon so nothing in the list can answer unsupported_weapon_area. /weapon.give takes any TweakDB string.
  • It gives the weapon LOADED. assign, then setAmmo with a per-class full load, then one magazine top-up read off the capacity the engine reports back.
  • auto picks 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

// Guides Β· Lua API

Search the docs

1,905 guides and functions indexed

Try