240 lines
7.0 KiB
Markdown
240 lines
7.0 KiB
Markdown
# Locker Usage Guide
|
|
|
|
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](./OWNED_STORAGE_USAGE_GUIDE.md).
|
|
|
|
## 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.
|