2026-05-25 13:27:34 -05:00

239 lines
7.1 KiB
Markdown

---
title: "Locker Usage Guide"
description: "The locker module stores physical player inventory items by classname. It is separate from the virtual arsenal unlock module documented in [Owned Storage Usage Guide](/server-modules/owned-storage)."
---
## Storage Model
Locker data is persisted through SurrealDB by the server extension.
```json
{
"arifle_MX_F": {
"category": "weapon",
"classname": "arifle_MX_F",
"amount": 1
}
}
```
Rules validated by the Rust service:
- A locker can contain up to 25 unique classnames.
- `category` and `classname` cannot be empty.
- `amount` must be greater than `0`.
- `locker:add` creates an empty locker automatically when one does not exist.
- `locker:get`, `locker:patch`, and `locker:remove` require an existing locker.
- `locker:remove` takes the classname directly, not a JSON object.
## Multiple Locker Objects
Editor-placed locker objects are templates and access points, not separate
durable inventories. During server post-init, any mission namespace object
whose variable name contains `locker` is hidden globally. During client setup,
each client reads the hidden object's classname, ASL position, vector direction,
and vector up, then creates a matching local locker object with
`createVehicleLocal`.
The local clone is the object the player actually opens. On `ContainerOpened`,
the client clears the clone and fills it from the player's UID-owned locker
state. On `ContainerClosed`, the client reads the clone's cargo and sends a
full locker override back to the server.
There is no explicit maximum number of editor-placed locker access points. The
practical limit is mission performance and how many local container objects are
reasonable for the scenario.
All locker access points load and save the same player locker, keyed by player
UID. Opening `locker`, `locker_hq`, or `locker_outpost_1` does not create
separate persistent inventories; those objects are separate local access clones
for the same underlying player locker.
## Store Grants and Duplicate Inventory
The store checkout path grants items to the UID-owned locker hot state, not to a
specific placed locker object. Item grants are merged by classname:
- buying a new classname adds one new locker entry
- buying an existing classname increases that entry's amount
- checkout fails if the result would exceed 25 unique classnames
Having more than one locker object on the map does not duplicate store grants.
Duplicate quantities can only come from repeated checkout requests or repeated
manual locker writes, not from the number of placed locker access points.
## Commands
All commands are called on the `locker` group.
| Command | Arguments | Returns |
| --- | --- | --- |
| `locker:create` | `uid` | Empty item map as JSON. |
| `locker:get` | `uid` | Item map as JSON. |
| `locker:add` | `uid`, `item_json` | Updated item map as JSON. |
| `locker:update` | `uid`, `items_json` | Replaced item map as JSON. |
| `locker:patch` | `uid`, `patch_json` | Updated item map as JSON. |
| `locker:remove` | `uid`, `classname` | Updated item map as JSON. |
| `locker:delete` | `uid` | `OK`. |
| `locker:exists` | `uid` | `true` or `false`. |
## Error Handling
Every command returns a string payload. Always check for the `Error:` prefix
before parsing JSON.
```sqf
private _result = "forge_server" callExtension ["locker:get", [getPlayerUID player]];
private _payload = _result select 0;
if (_payload find "Error:" == 0) exitWith {
systemChat format ["Locker error: %1", _payload];
};
private _locker = fromJSON _payload;
```
## Add an Item
`locker:add` creates or overwrites one classname entry.
```sqf
private _item = createHashMapFromArray [
["category", "weapon"],
["classname", "arifle_MX_F"],
["amount", 1]
];
private _result = "forge_server" callExtension ["locker:add", [
getPlayerUID player,
toJSON _item
]];
private _payload = _result select 0;
if (_payload find "Error:" == 0) exitWith {
hint format ["Failed to store item: %1", _payload];
};
private _locker = fromJSON _payload;
```
## Patch an Amount
`locker:patch` currently patches the `amount` field for an existing classname.
```sqf
private _patch = createHashMapFromArray [
["classname", "arifle_MX_F"],
["amount", 5]
];
private _result = "forge_server" callExtension ["locker:patch", [
getPlayerUID player,
toJSON _patch
]];
```
## Remove an Item
`locker:remove` takes the classname as the second argument.
```sqf
private _result = "forge_server" callExtension ["locker:remove", [
getPlayerUID player,
"arifle_MX_F"
]];
```
## Retrieve an Item
```sqf
fnc_retrieveLockerItem = {
params ["_classname"];
private _result = "forge_server" callExtension ["locker:get", [getPlayerUID player]];
private _payload = _result select 0;
if (_payload find "Error:" == 0) exitWith {
hint format ["Failed to load locker: %1", _payload];
false
};
private _locker = fromJSON _payload;
private _item = _locker getOrDefault [_classname, createHashMap];
if (_item isEqualTo createHashMap) exitWith {
hint "Item was not found in your locker.";
false
};
private _amount = _item getOrDefault ["amount", 0];
if (_amount <= 0) exitWith {
hint "Item is out of stock.";
false
};
if !(player canAdd _classname) exitWith {
hint "Not enough inventory space.";
false
};
player addItem _classname;
if (_amount > 1) then {
private _patch = createHashMapFromArray [
["classname", _classname],
["amount", _amount - 1]
];
"forge_server" callExtension ["locker:patch", [getPlayerUID player, toJSON _patch]];
} else {
"forge_server" callExtension ["locker:remove", [getPlayerUID player, _classname]];
};
true
};
```
## Replace the Whole Locker
`locker:update` replaces the whole item map. Use it for explicit bulk syncs,
not single-item changes.
```sqf
private _items = createHashMapFromArray [
["arifle_MX_F", createHashMapFromArray [
["category", "weapon"],
["classname", "arifle_MX_F"],
["amount", 1]
]]
];
private _result = "forge_server" callExtension ["locker:update", [
getPlayerUID player,
toJSON _items
]];
```
## Hot State
The `locker:hot:*` commands keep a runtime copy of a player's locker and write
it back only when `locker:hot:save` runs.
| Command | Arguments | Returns |
| --- | --- | --- |
| `locker:hot:init` | `uid` | Item map as JSON. |
| `locker:hot:get` | `uid` | Item map as JSON. |
| `locker:hot:override` | `uid`, `items_json` | Item map as JSON. |
| `locker:hot:save` | `uid` | Current hot item map as JSON. |
| `locker:hot:remove` | `uid` | `OK`. |
Use hot state for session-heavy locker workflows. Use the durable commands for
simple item deposits and withdrawals.
## Best Practices
- Keep categories normalized, for example `weapon`, `magazine`, `item`, or
`backpack`.
- Use `locker:patch` for quantity changes.
- Use `locker:remove` when quantity reaches zero.
- Treat the locker response as a hash map keyed by classname.
- Check capacity before bulk operations that may exceed 25 unique items.