# Server loot and ground drops

Ground loot is server-authoritative. Native containers and drops display the server's items; Take and Take All send pickup requests checked against the drop, routing bucket and player distance. The local inventory changes only after authorization.

The official [`open77_loot`](https://github.com/Open2077/open77-resources/tree/main/system/open77_loot) resource must stay started. Its manifest asks for `network.events` and
`world.loot`, and its client is distributed with the server's resource set.

Vanilla loot suppression is a client session policy, independent of this resource. A server
without `open77_loot`, or one that stops it, does **not** enable solo loot: unmanaged choices and
local pickup/consume callbacks remain blocked. Managed drops keep their native presentation and
server pickup requests. Outside multiplayer, normal solo behavior remains the fallback unless
loot authority has been explicitly enabled for a local test.

Use this guide to place items in the world from a server resource, and to credit them to a player's
inventory when they are picked up.

## Server API

Server resources that declare `world.loot` get `Open77.loot`:

```lua
permissions { "network.events", "world.loot" }
```

```lua
local dropId, reason = Open77.loot.create({
    item = "Items.money",             -- TweakDB record
    quantity = 250,
    position = { x = -1450.2, y = 117.8, z = 12.4 },
    bucket = 0,                        -- routing bucket
    radius = 2.0,                      -- 0.5 to 10 metres
    label = "Eurodollars",
    visualItem = "Items.MoneyShard",  -- record used only for the 3D entity
    ttlMs = 300000                     -- optional, seven days maximum
})

assert(dropId, reason)

Open77.loot.update(dropId, {
    quantity = 300,
    position = { x = -1450.0, y = 118.1, z = 12.4 },
    radius = 2.5
})

local drop = Open77.loot.get(dropId)
local bucketDrops = Open77.loot.all(0)
Open77.loot.remove(dropId)
```

A player's authoritative position is available server-side too:

```lua
local position = Open77.players.position(playerId)
-- { x, y, z, bucket }, or nil when the player or snapshot is unavailable.
```

A drop belongs to the resource that created it. Another resource can neither change nor delete it,
and stopping the owning resource cleans up its drops. The registry is capped at 4,096 entries,
quantities at 1,000,000, and positions at the world bounds the protocol accepts.

`item` and `visualItem` play distinct roles. `item` is the authoritative content actually granted
once the pickup is validated. `visualItem` is a TweakDB record that owns a `dropObject` and serves
only to create the physical `gameItemDropObject`. For a weapon, leaving `visualItem` empty uses the
weapon's own record directly. Since `Items.money` is abstract currency, Open77 automatically picks
`Items.MoneyShard` (`smallItemDrop`, entity `money`) when no override is supplied.

After an accepted pickup, every server VM receives:

```lua
AddEventHandler("onLootPickup", function(playerId, dropId, item, quantity, ownerResource)
    if ownerResource ~= GetCurrentResourceName() then return end

    -- Write the authoritative persistent inventory here (database, weight,
    -- transaction log, and so on). The client never decides the outcome.
    print(playerId, dropId, item, quantity)
end)
```

The pickup is atomic within the registry: two players cannot consume the same id. Validation uses a
`PlayerSnapshot` received less than two seconds ago, requires the same routing bucket, and accepts
at most `radius + 0.75 m` to absorb network latency.

## Server events

```lua
AddEventHandler("onLootCreated", function(id, resource, item) end)
AddEventHandler("onLootRemoved", function(id, reason, resource) end)
```

Both require `world.loot` — the same string reading one costs, because a lifecycle event
that announced a loot to a resource that cannot list one would route around that capability.
`reason` is `removed`, `expired`, `resource_stopped`, `picked_up`, `moved_bucket`, or the free text (at most 64
characters) the caller passed to the remove call.

The same two transitions also reach the generic `onEntityCreated(kind, id, resource)` and
`onEntityRemoved(kind, id, reason)` with `kind` = `"loot"`, for a resource that additionally
declares `world.entities.observe`. The mirror is raised by the same statement, so the two feeds
cannot disagree; see [entity lifecycle events](server-api.md#entity-lifecycle-events) for the
authority rule and for why there is no `Updated` counterpart.

## Testing from the Open77 terminal

These commands are genuinely declared in `open77_loot/server/main.lua` with `RegisterCommand`, just
as in a FiveM resource:

```text
loot.create.player 1 Items.money 250 1.0 2.0 Items.MoneyShard
loot.create Items.money 250 -1450.2 117.8 12.4 0 2.0
loot.create.player 1 Items.Preset_Lexington_Default 1
loot.list 0
loot.remove 1
```

Type these lines into the developer terminal opened with `²` in Cyberpunk. `loot.list` is public.
The mutations `loot.create`, `loot.create.player`, and `loot.remove` are registered with
`restricted=true` and require the ACL permissions `command.loot.create`,
`command.loot.create.player`, and `command.loot.remove` respectively (or a suitable wildcard).

`loot.create` expects `<item> <quantity> <x> <y> <z> [bucket] [radius] [visualItem]`. Decimal numbers
use a dot. The drop is replicated to the bucket's players immediately; its prompt should disappear on
every client as soon as a pickup is accepted.

`loot.create.player` uses the player's latest fresh snapshot and adds one metre in Z by default, so
the item falls in front of them. Its full form is
`loot.create.player <playerId> <item> <quantity> [offsetZ] [radius] [visualItem]`.

The representation is a real native entity produced by `LootManager.SpawnItemDrop`: the record's
mesh, a `gameItemDropObject` wrapper, placement, physics, highlight, and REDengine UI. Open77 binds
the native `EntityID` to the server id when the child object appears, then fills the native inventory
with `item` and `quantity`. There is no separate Open77 anchor or prompt any more.

## Client API

Gameplay clients read the projection through `open77_loot`'s exports:

```lua
CreateThread(function()
    local promise, err = Open77.exports.call("open77_loot", "all")
    if not promise then return print(err) end
    local drops = assert(promise:await())
    for _, drop in ipairs(drops) do
        print(drop.id, drop.item, drop.quantity)
    end
end)

-- The native prompt already performs this. Useful for a custom UI.
CreateThread(function()
    local promise = Open77.exports.call("open77_loot", "requestPickup", 42)
    if promise then print(promise:await()) end
end)
```

Available exports:

- `get(id)` — returns the local drop, or `nil`;
- `all()` — snapshot sorted by id;
- `requestPickup(id)` — sends the request to the server.

This local event lets you show feedback without touching authority:

```lua
AddEventHandler("open77:loot:result", function(id, accepted, reason, item, quantity)
    if not accepted then
        print("Pickup refused: " .. tostring(reason))
    end
end)
```

Do not call `Open77.loot.upsert`, `acceptPickup`, or `setAuthorityEnabled` directly from a gameplay
resource. Those are the internal primitives of the distributed projection, guarded by `world.loot`.

## The boundary between native UI and server authority

- `Inventory` choices stay visible only for entities bound to a server drop;
- `Inventory.OnInteractionUsed` is intercepted before its local `RemoveItem` and becomes a
  `open77:loot:pickup` request;
- the native pickup condition is re-enabled only for bound `gameItemDropObject`s;
- legacy placed pickups (`HealthConsumable`, `VirtualItem_TEMP`) and the `Loot` choices of
  inspectable objects;
- the container role of bodies, bags, ground items, and static containers;
- the vanilla backpack's "drop" action;
- the normal held-weapon drop path when an NPC dies;
- drops already present when authority activates, purged before the server projection.

The global purge runs only when authority activates. After that, a pickup, a removal, or an update
empties only the inventory of the entity concerned. Cyberpunk then receives its normal
`OnInventoryEmptyEvent` and cleanly removes the HUD, the interaction, and the visual without
recreating the other ground items.

Open77 items therefore use the same presentation as vanilla: the physical mesh resolved by the
TweakDB record falls with the game's physics, and the `Inventory` component builds the list and the
item card. That entity holds no authority though: removal and granting only happen after the server
answers positively.

## Scope

**A drop holds one stack** — one `item` and one `quantity`. Containers holding several stacks, and
corpses carrying an authoritative inventory, belong to the container API and are not part of this
one.

**Persistent inventory is yours to write.** This subsystem decides *who may take what*, and stops
there. `onLootPickup` is where you credit an account, apply weight, or write a transaction log.

**Quest collectibles that bypass `Inventory`** are not intercepted. A handful of highly specialised
pickups take neither the `Inventory` path nor the standard loot conditions; those still grant
locally. If you meet one, its class can be identified with the Open77 inspector.
