Expand framework usage guides #6
@ -37,3 +37,4 @@ connect_timeout_ms = 5000
|
||||
|
||||
- [API Reference](./api-reference.md)
|
||||
- [Usage Examples](./usage-examples.md)
|
||||
- [Framework Module Guides](../../../docs/README.md)
|
||||
|
||||
@ -33,3 +33,16 @@ Game systems should call the domain APIs instead of raw database operations:
|
||||
|
||||
Large request and response payloads are routed through the transport layer when
|
||||
needed by `forge_server_addons_extension_fnc_extCall`.
|
||||
|
||||
## Module Guides
|
||||
|
||||
- [Actor](../../../docs/ACTOR_USAGE_GUIDE.md)
|
||||
- [Bank](../../../docs/BANK_USAGE_GUIDE.md)
|
||||
- [CAD](../../../docs/CAD_USAGE_GUIDE.md)
|
||||
- [Garage](../../../docs/GARAGE_USAGE_GUIDE.md)
|
||||
- [Locker](../../../docs/LOCKER_USAGE_GUIDE.md)
|
||||
- [Organization](../../../docs/ORG_USAGE_GUIDE.md)
|
||||
- [Owned Storage](../../../docs/OWNED_STORAGE_USAGE_GUIDE.md)
|
||||
- [Phone](../../../docs/PHONE_USAGE_GUIDE.md)
|
||||
- [Store](../../../docs/STORE_USAGE_GUIDE.md)
|
||||
- [Task](../../../docs/TASK_USAGE_GUIDE.md)
|
||||
|
||||
127
docs/ACTOR_USAGE_GUIDE.md
Normal file
127
docs/ACTOR_USAGE_GUIDE.md
Normal file
@ -0,0 +1,127 @@
|
||||
# Actor Usage Guide
|
||||
|
||||
The actor module stores persistent player character data: identity, loadout,
|
||||
position, direction, stance, contact fields, state, holster status, rank, and
|
||||
organization.
|
||||
|
||||
## Storage Model
|
||||
|
||||
Actor data is persisted through SurrealDB by the server extension.
|
||||
|
||||
```json
|
||||
{
|
||||
"uid": "76561198000000000",
|
||||
"name": "Player Name",
|
||||
"loadout": {},
|
||||
"position": [1234.5, 6789.0, 0.0],
|
||||
"direction": 90.0,
|
||||
"stance": "STAND",
|
||||
"email": "0160000000@spearnet.mil",
|
||||
"phone_number": "0160000000",
|
||||
"state": "HEALTHY",
|
||||
"holster": true,
|
||||
"rank": null,
|
||||
"organization": "default"
|
||||
}
|
||||
```
|
||||
|
||||
Rules validated by the Rust service:
|
||||
|
||||
- `uid` is authoritative from the command argument and must be a 17-digit Steam
|
||||
UID.
|
||||
- `name` is optional, but cannot be empty when set and cannot exceed 50
|
||||
characters.
|
||||
- `position` must be three finite numbers when set.
|
||||
- `direction` must be in the `0.0 <= direction < 360.0` range.
|
||||
- `email` must contain `@` and end with `.mil` when set.
|
||||
- `phone_number` must start with `0160` and be 10 digits when set.
|
||||
- Empty `phone_number`, `email`, or `organization` fields are filled on create.
|
||||
|
||||
## Commands
|
||||
|
||||
All commands are called on the `actor` group.
|
||||
|
||||
| Command | Arguments | Returns |
|
||||
| --- | --- | --- |
|
||||
| `actor:get` | `uid` | Actor JSON. If no actor exists, returns a default actor but does not persist it. |
|
||||
| `actor:create` | `uid`, `actor_json` | Persisted actor JSON. |
|
||||
| `actor:update` | `uid`, `patch_json` | Updated actor JSON. |
|
||||
| `actor:exists` | `uid` | `true` or `false`. |
|
||||
| `actor:delete` | `uid` | `OK`. |
|
||||
|
||||
## Create an Actor
|
||||
|
||||
The `uid` field in the JSON is overwritten with the command UID.
|
||||
|
||||
```sqf
|
||||
private _actor = createHashMapFromArray [
|
||||
["uid", getPlayerUID player],
|
||||
["name", name player],
|
||||
["loadout", getUnitLoadout player],
|
||||
["position", getPosATL player],
|
||||
["direction", getDir player],
|
||||
["stance", stance player],
|
||||
["email", ""],
|
||||
["phone_number", ""],
|
||||
["state", "HEALTHY"],
|
||||
["holster", true],
|
||||
["organization", "default"]
|
||||
];
|
||||
|
||||
private _result = "forge_server" callExtension ["actor:create", [
|
||||
getPlayerUID player,
|
||||
toJSON _actor
|
||||
]];
|
||||
```
|
||||
|
||||
## Update an Actor
|
||||
|
||||
`actor:update` accepts a JSON object containing only fields to change.
|
||||
|
||||
```sqf
|
||||
private _patch = createHashMapFromArray [
|
||||
["position", getPosATL player],
|
||||
["direction", getDir player],
|
||||
["stance", stance player],
|
||||
["loadout", getUnitLoadout player]
|
||||
];
|
||||
|
||||
private _result = "forge_server" callExtension ["actor:update", [
|
||||
getPlayerUID player,
|
||||
toJSON _patch
|
||||
]];
|
||||
```
|
||||
|
||||
Supported patch fields are `name`, `position`, `direction`, `stance`, `email`,
|
||||
`phone_number`, `state`, `holster`, `rank`, `organization`, and `loadout`.
|
||||
`uid` is ignored.
|
||||
|
||||
## Hot State
|
||||
|
||||
The `actor:hot:*` commands keep a runtime copy of actor data and write it back
|
||||
only when `actor:hot:save` runs.
|
||||
|
||||
| Command | Arguments | Returns |
|
||||
| --- | --- | --- |
|
||||
| `actor:hot:init` | `uid` | Actor JSON from durable storage. |
|
||||
| `actor:hot:get` | `uid` | Actor JSON. |
|
||||
| `actor:hot:keys` | none | JSON array of hot actor UIDs. |
|
||||
| `actor:hot:override` | `uid`, `actor_json` | Actor JSON. |
|
||||
| `actor:hot:save` | `uid` | Current hot actor JSON and async durable save. |
|
||||
| `actor:hot:remove` | `uid` | `OK`. |
|
||||
|
||||
Use hot state for frequently updated session data such as position and loadout.
|
||||
Use durable commands for account creation and administrative changes.
|
||||
|
||||
## Error Handling
|
||||
|
||||
```sqf
|
||||
private _result = "forge_server" callExtension ["actor:get", [getPlayerUID player]];
|
||||
private _payload = _result select 0;
|
||||
|
||||
if (_payload find "Error:" == 0) exitWith {
|
||||
systemChat format ["Actor error: %1", _payload];
|
||||
};
|
||||
|
||||
private _actor = fromJSON _payload;
|
||||
```
|
||||
169
docs/BANK_USAGE_GUIDE.md
Normal file
169
docs/BANK_USAGE_GUIDE.md
Normal file
@ -0,0 +1,169 @@
|
||||
# Bank Usage Guide
|
||||
|
||||
The bank module stores player account balances, earnings, PINs, and transaction
|
||||
strings. The hot-state API also owns the active banking workflows used by the
|
||||
UI: deposit, withdraw, transfer, checkout charge, and PIN validation.
|
||||
|
||||
## Storage Model
|
||||
|
||||
Bank data is persisted through SurrealDB by the server extension.
|
||||
|
||||
```json
|
||||
{
|
||||
"uid": "76561198000000000",
|
||||
"name": "Player Name",
|
||||
"bank": 1000.0,
|
||||
"cash": 250.0,
|
||||
"earnings": 0.0,
|
||||
"pin": 1234,
|
||||
"transactions": []
|
||||
}
|
||||
```
|
||||
|
||||
Rules validated by the Rust service:
|
||||
|
||||
- `uid` is authoritative from the command argument.
|
||||
- `name` cannot be empty.
|
||||
- `bank` and `cash` cannot be negative.
|
||||
- `pin` must be a four-digit number.
|
||||
- Durable `bank:get` requires an existing bank account.
|
||||
|
||||
## Durable Commands
|
||||
|
||||
| Command | Arguments | Returns |
|
||||
| --- | --- | --- |
|
||||
| `bank:create` | `uid`, `bank_json` | Persisted bank JSON. |
|
||||
| `bank:get` | `uid` | Bank JSON. |
|
||||
| `bank:update` | `uid`, `patch_json` | Updated bank JSON. |
|
||||
| `bank:exists` | `uid` | `true` or `false`. |
|
||||
| `bank:delete` | `uid` | `OK`. |
|
||||
|
||||
## Create an Account
|
||||
|
||||
The `uid` field in the JSON is overwritten with the command UID.
|
||||
|
||||
```sqf
|
||||
private _account = createHashMapFromArray [
|
||||
["uid", getPlayerUID player],
|
||||
["name", name player],
|
||||
["bank", 0],
|
||||
["cash", 0],
|
||||
["earnings", 0],
|
||||
["pin", 1234],
|
||||
["transactions", []]
|
||||
];
|
||||
|
||||
private _result = "forge_server" callExtension ["bank:create", [
|
||||
getPlayerUID player,
|
||||
toJSON _account
|
||||
]];
|
||||
```
|
||||
|
||||
## Hot-State Commands
|
||||
|
||||
| Command | Arguments | Returns |
|
||||
| --- | --- | --- |
|
||||
| `bank:hot:init` | `uid` | Bank JSON loaded into hot state. |
|
||||
| `bank:hot:get` | `uid` | Bank JSON. |
|
||||
| `bank:hot:override` | `uid`, `bank_json` | Bank JSON. |
|
||||
| `bank:hot:patch` | `uid`, `patch_json` | `{ account, patch }`. |
|
||||
| `bank:hot:deposit` | `uid`, `amount`, `context_json` | `{ account, patch }`. |
|
||||
| `bank:hot:withdraw` | `uid`, `amount`, `context_json` | `{ account, patch }`. |
|
||||
| `bank:hot:deposit_earnings` | `uid`, `amount`, `context_json` | `{ account, patch }`. |
|
||||
| `bank:hot:transfer` | `source_uid`, `target_uid`, `amount`, `context_json` | Transfer result JSON. |
|
||||
| `bank:hot:charge_checkout` | `uid`, `amount`, `context_json` | `{ account, patch }`. |
|
||||
| `bank:hot:validate_pin` | `uid`, `pin`, `context_json` | `{}` on success. |
|
||||
| `bank:hot:save` | `uid` | Current hot bank JSON and async durable save. |
|
||||
| `bank:hot:remove` | `uid` | `OK`. |
|
||||
|
||||
Use hot-state commands for UI workflows. They return patch objects so the UI can
|
||||
update only changed fields.
|
||||
|
||||
## Deposit and Withdraw
|
||||
|
||||
ATM sessions require `atmAuthorized: true`. Full bank sessions can set
|
||||
`mode: "bank"`.
|
||||
|
||||
```sqf
|
||||
private _context = createHashMapFromArray [
|
||||
["mode", "atm"],
|
||||
["atmAuthorized", true]
|
||||
];
|
||||
|
||||
private _deposit = "forge_server" callExtension ["bank:hot:deposit", [
|
||||
getPlayerUID player,
|
||||
"100",
|
||||
toJSON _context
|
||||
]];
|
||||
|
||||
private _withdraw = "forge_server" callExtension ["bank:hot:withdraw", [
|
||||
getPlayerUID player,
|
||||
"50",
|
||||
toJSON _context
|
||||
]];
|
||||
```
|
||||
|
||||
## Transfer
|
||||
|
||||
Transfers are only available from the full bank interface. `fromField` can be
|
||||
`bank` or `cash`.
|
||||
|
||||
```sqf
|
||||
private _context = createHashMapFromArray [
|
||||
["mode", "bank"],
|
||||
["atmAuthorized", false],
|
||||
["fromField", "bank"]
|
||||
];
|
||||
|
||||
private _result = "forge_server" callExtension ["bank:hot:transfer", [
|
||||
getPlayerUID player,
|
||||
_targetUid,
|
||||
"250",
|
||||
toJSON _context
|
||||
]];
|
||||
```
|
||||
|
||||
## Checkout Charge
|
||||
|
||||
Checkout charging supports `sourceField: "cash"` or `sourceField: "bank"`.
|
||||
Set `commit` to `false` to preview the patch without saving.
|
||||
|
||||
```sqf
|
||||
private _context = createHashMapFromArray [
|
||||
["sourceField", "bank"],
|
||||
["commit", true]
|
||||
];
|
||||
|
||||
private _result = "forge_server" callExtension ["bank:hot:charge_checkout", [
|
||||
getPlayerUID player,
|
||||
"125",
|
||||
toJSON _context
|
||||
]];
|
||||
```
|
||||
|
||||
## PIN Validation
|
||||
|
||||
PIN entry is only valid in ATM mode.
|
||||
|
||||
```sqf
|
||||
private _context = createHashMapFromArray [["mode", "atm"]];
|
||||
|
||||
private _result = "forge_server" callExtension ["bank:hot:validate_pin", [
|
||||
getPlayerUID player,
|
||||
"1234",
|
||||
toJSON _context
|
||||
]];
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```sqf
|
||||
private _result = "forge_server" callExtension ["bank:hot:get", [getPlayerUID player]];
|
||||
private _payload = _result select 0;
|
||||
|
||||
if (_payload find "Error:" == 0) exitWith {
|
||||
systemChat format ["Bank error: %1", _payload];
|
||||
};
|
||||
|
||||
private _bank = fromJSON _payload;
|
||||
```
|
||||
183
docs/CAD_USAGE_GUIDE.md
Normal file
183
docs/CAD_USAGE_GUIDE.md
Normal file
@ -0,0 +1,183 @@
|
||||
# CAD Usage Guide
|
||||
|
||||
The CAD module stores transient operational state for dispatch activity,
|
||||
assignments, dispatch orders, support requests, group profiles, grouped views,
|
||||
and hydrated UI payloads. CAD state is in-memory and follows the active server
|
||||
or mission lifecycle.
|
||||
|
||||
## Data Model
|
||||
|
||||
Most CAD records are flexible JSON objects. The service normalizes important
|
||||
IDs and returns structured mutation results for higher-level workflows.
|
||||
|
||||
Common generated IDs:
|
||||
|
||||
- Orders: `cad-order:<sequence>`
|
||||
- Requests: `cad-request:<sequence>`
|
||||
- Assignments usually share a task ID or order ID.
|
||||
|
||||
## Commands
|
||||
|
||||
### Activity
|
||||
|
||||
| Command | Arguments | Returns |
|
||||
| --- | --- | --- |
|
||||
| `cad:activity:append` | `activity_json` | `OK`. |
|
||||
| `cad:activity:recent` | `limit` | Recent activity array JSON. |
|
||||
|
||||
### Assignments
|
||||
|
||||
| Command | Arguments | Returns |
|
||||
| --- | --- | --- |
|
||||
| `cad:assignments:list` | none | Assignment array JSON. |
|
||||
| `cad:assignments:assign` | `entry_id`, `assignment_json` | Assignment mutation result JSON. |
|
||||
| `cad:assignments:acknowledge` | `entry_id`, `patch_json` | Assignment mutation result JSON. |
|
||||
| `cad:assignments:decline` | `entry_id`, `patch_json` | Assignment mutation result JSON and removes assignment. |
|
||||
| `cad:assignments:upsert` | `entry_id`, `assignment_json` | `OK`. |
|
||||
| `cad:assignments:delete` | `entry_id` | `OK`. |
|
||||
|
||||
### Orders
|
||||
|
||||
| Command | Arguments | Returns |
|
||||
| --- | --- | --- |
|
||||
| `cad:orders:list` | none | Order array JSON. |
|
||||
| `cad:orders:create` | `order_seed_json` | Dispatch order mutation result JSON. |
|
||||
| `cad:orders:create_from_context` | `context_json` | Dispatch order mutation result JSON. |
|
||||
| `cad:orders:close` | `entry_id` | Dispatch order mutation result JSON and removes order/assignment. |
|
||||
| `cad:orders:upsert` | `entry_id`, `order_json` | `OK`. |
|
||||
| `cad:orders:delete` | `entry_id` | `OK`. |
|
||||
|
||||
### Requests
|
||||
|
||||
| Command | Arguments | Returns |
|
||||
| --- | --- | --- |
|
||||
| `cad:requests:list` | none | Request array JSON. |
|
||||
| `cad:requests:submit` | `request_json` | Request mutation result JSON. |
|
||||
| `cad:requests:submit_from_context` | `context_json` | Request mutation result JSON. |
|
||||
| `cad:requests:close` | `entry_id` | Request mutation result JSON and removes request. |
|
||||
| `cad:requests:upsert` | `entry_id`, `request_json` | `OK`. |
|
||||
| `cad:requests:delete` | `entry_id` | `OK`. |
|
||||
|
||||
### Profiles and Views
|
||||
|
||||
| Command | Arguments | Returns |
|
||||
| --- | --- | --- |
|
||||
| `cad:profiles:list` | none | Profile array JSON. |
|
||||
| `cad:profiles:update_from_context` | `context_json` | Profile mutation result JSON. |
|
||||
| `cad:profiles:upsert` | `entry_id`, `profile_json` | `OK`. |
|
||||
| `cad:profiles:delete` | `entry_id` | `OK`. |
|
||||
| `cad:groups:build` | `groups_seed_json` | Group array JSON. |
|
||||
| `cad:view:hydrate` | `hydrate_seed_json` | Hydrated CAD payload JSON. |
|
||||
|
||||
## Submit a Support Request
|
||||
|
||||
```sqf
|
||||
private _fields = createHashMapFromArray [
|
||||
["pickup_location", "Grid 123456"],
|
||||
["precedence", "urgent"],
|
||||
["security", "secure"]
|
||||
];
|
||||
|
||||
private _context = createHashMapFromArray [
|
||||
["type", "medevac_9line"],
|
||||
["fields", _fields],
|
||||
["groupId", "alpha"],
|
||||
["groupCallsign", "Alpha 1-1"],
|
||||
["submittedByUid", getPlayerUID player],
|
||||
["submittedByName", name player],
|
||||
["priority", "emergency"],
|
||||
["position", getPosATL player],
|
||||
["createdAt", diag_tickTime]
|
||||
];
|
||||
|
||||
private _result = "forge_server" callExtension ["cad:requests:submit_from_context", [
|
||||
toJSON _context
|
||||
]];
|
||||
```
|
||||
|
||||
Supported priority values are `routine`, `priority`, and `emergency`. Unknown
|
||||
values normalize to `priority`.
|
||||
|
||||
## Create a Dispatch Order
|
||||
|
||||
```sqf
|
||||
private _context = createHashMapFromArray [
|
||||
["assigneeGroupId", "bravo"],
|
||||
["assigneeGroupCallsign", "Bravo 1-1"],
|
||||
["targetGroupId", "alpha"],
|
||||
["targetGroupCallsign", "Alpha 1-1"],
|
||||
["targetPosition", getPosATL player],
|
||||
["createdByUid", getPlayerUID player],
|
||||
["createdByName", name player],
|
||||
["requestId", "cad-request:1"],
|
||||
["requestType", "logreq"],
|
||||
["requestTitle", "LOGREQ | Alpha 1-1"],
|
||||
["requestSummary", "Ammo resupply requested"],
|
||||
["requestFields", createHashMap],
|
||||
["note", "Support Alpha 1-1 at current position."],
|
||||
["priority", "priority"],
|
||||
["createdAt", diag_tickTime]
|
||||
];
|
||||
|
||||
private _result = "forge_server" callExtension ["cad:orders:create_from_context", [
|
||||
toJSON _context
|
||||
]];
|
||||
```
|
||||
|
||||
## Assignment Workflow
|
||||
|
||||
```sqf
|
||||
private _assignment = createHashMapFromArray [
|
||||
["groupId", "bravo"],
|
||||
["assigneeGroupCallsign", "Bravo 1-1"],
|
||||
["assignedByUid", getPlayerUID player],
|
||||
["assignedByName", name player],
|
||||
["assignedAt", diag_tickTime],
|
||||
["state", "assigned"]
|
||||
];
|
||||
|
||||
"forge_server" callExtension ["cad:assignments:assign", [
|
||||
"task-123",
|
||||
toJSON _assignment
|
||||
]];
|
||||
|
||||
private _ack = createHashMapFromArray [
|
||||
["state", "acknowledged"],
|
||||
["acknowledgedByUid", getPlayerUID player],
|
||||
["acknowledgedAt", diag_tickTime]
|
||||
];
|
||||
|
||||
"forge_server" callExtension ["cad:assignments:acknowledge", [
|
||||
"task-123",
|
||||
toJSON _ack
|
||||
]];
|
||||
```
|
||||
|
||||
## Hydrate the CAD UI
|
||||
|
||||
```sqf
|
||||
private _session = createHashMapFromArray [
|
||||
["uid", getPlayerUID player],
|
||||
["orgId", "default"],
|
||||
["isDispatcher", true],
|
||||
["groupId", "alpha"],
|
||||
["isLeader", true]
|
||||
];
|
||||
|
||||
private _seed = createHashMapFromArray [
|
||||
["groups", _liveGroups],
|
||||
["activeTasks", _activeTasks],
|
||||
["session", _session]
|
||||
];
|
||||
|
||||
private _result = "forge_server" callExtension ["cad:view:hydrate", [toJSON _seed]];
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```sqf
|
||||
private _payload = _result select 0;
|
||||
if (_payload find "Error:" == 0) exitWith {
|
||||
systemChat format ["CAD error: %1", _payload];
|
||||
};
|
||||
```
|
||||
@ -1,274 +1,212 @@
|
||||
# Garage System Integration Guide
|
||||
# Garage Usage Guide
|
||||
|
||||
## Overview
|
||||
The garage module stores physical player vehicles. Each record keeps the
|
||||
vehicle classname, generated plate UUID, fuel, overall damage, and detailed hit
|
||||
point damage.
|
||||
|
||||
The garage system provides complete vehicle storage, retrieval, and management for Arma 3 players. Each player can store multiple vehicles with full damage and hit point tracking.
|
||||
## Storage Model
|
||||
|
||||
## Data Storage
|
||||
Garage data is persisted through SurrealDB by the server extension.
|
||||
|
||||
- Each player's garage is persisted by the server extension through SurrealDB.
|
||||
- The map is keyed by the vehicle's unique plate (UUID)
|
||||
- Each vehicle tracks: plate (UUID), classname, overall damage, fuel, and detailed hit points
|
||||
- **Plates are auto-generated** when vehicles are added via `garage:add`
|
||||
- **Empty garages are auto-created** when a player first fetches their garage
|
||||
|
||||
## Extension Commands
|
||||
|
||||
All commands are accessed via the `garage` group:
|
||||
|
||||
### Create Garage
|
||||
|
||||
Creates a new empty garage for a player. Should be called when initializing a new player.
|
||||
|
||||
```sqf
|
||||
private _result = "forge_server" callExtension ["garage:create", [getPlayerUID player]];
|
||||
private _emptyGarage = fromJSON (_result select 0);
|
||||
// Returns: {} (empty map)
|
||||
```json
|
||||
{
|
||||
"plate-uuid": {
|
||||
"plate": "plate-uuid",
|
||||
"classname": "B_Quadbike_01_F",
|
||||
"fuel": 1.0,
|
||||
"damage": 0.0,
|
||||
"hit_points": {
|
||||
"names": ["hitengine"],
|
||||
"selections": ["engine_hitpoint"],
|
||||
"values": [0.0]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Get Garage
|
||||
Rules validated by the Rust service:
|
||||
|
||||
Retrieves all vehicles in a player's garage.
|
||||
- A player garage can contain up to 5 vehicles.
|
||||
- `garage:add` generates a UUID plate automatically.
|
||||
- `fuel`, `damage`, and every hit point value must be between `0.0` and `1.0`.
|
||||
- `hit_points.names`, `hit_points.selections`, and `hit_points.values` must have
|
||||
the same length.
|
||||
- `garage:get`, `garage:patch`, and `garage:remove` require an existing garage.
|
||||
- `garage:add` creates an empty garage automatically when one does not exist.
|
||||
|
||||
## Commands
|
||||
|
||||
All commands are called on the `garage` group.
|
||||
|
||||
| Command | Arguments | Returns |
|
||||
| --- | --- | --- |
|
||||
| `garage:create` | `uid` | Empty vehicle map as JSON. |
|
||||
| `garage:get` | `uid` | Vehicle map as JSON. |
|
||||
| `garage:add` | `uid`, `vehicle_json` | Updated vehicle map as JSON. |
|
||||
| `garage:update` | `uid`, `vehicles_json` | Replaced vehicle map as JSON. |
|
||||
| `garage:patch` | `uid`, `patch_json` | Updated vehicle map as JSON. |
|
||||
| `garage:remove` | `uid`, `remove_json` | Updated vehicle map as JSON. |
|
||||
| `garage:delete` | `uid` | `OK`. |
|
||||
| `garage: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 ["garage:get", [getPlayerUID player]];
|
||||
private _garageMap = fromJSON (_result select 0);
|
||||
// Returns: {"plate_uuid": {"plate":"plate_uuid","classname":"...","damage":0.0,"hit_points":{...}}, ...}
|
||||
private _payload = _result select 0;
|
||||
|
||||
if (_payload find "Error:" == 0) exitWith {
|
||||
systemChat format ["Garage error: %1", _payload];
|
||||
};
|
||||
|
||||
private _garage = fromJSON _payload;
|
||||
```
|
||||
|
||||
### Add Vehicle
|
||||
## Add a Vehicle
|
||||
|
||||
Adds a new vehicle to the garage. The system automatically generates a unique plate (UUID) for the vehicle.
|
||||
`garage:add` requires `classname`, `fuel`, `damage`, and `hit_points`.
|
||||
|
||||
```sqf
|
||||
private _data = createHashMapFromArray [
|
||||
["classname", "B_Quadbike_01_F"],
|
||||
["fuel", 1.0],
|
||||
["damage", 0.0],
|
||||
["hit_points", createHashMap] // Optional hit points map
|
||||
private _hitPointData = getAllHitPointsDamage _vehicle;
|
||||
private _hitPoints = createHashMapFromArray [
|
||||
["names", _hitPointData select 0],
|
||||
["selections", _hitPointData select 1],
|
||||
["values", _hitPointData select 2]
|
||||
];
|
||||
|
||||
private _vehicleData = createHashMapFromArray [
|
||||
["classname", typeOf _vehicle],
|
||||
["fuel", fuel _vehicle],
|
||||
["damage", damage _vehicle],
|
||||
["hit_points", _hitPoints]
|
||||
];
|
||||
|
||||
private _result = "forge_server" callExtension ["garage:add", [
|
||||
getPlayerUID player,
|
||||
toJSON _data
|
||||
toJSON _vehicleData
|
||||
]];
|
||||
|
||||
private _updatedGarage = fromJSON (_result select 0);
|
||||
// Returns updated garage map
|
||||
private _payload = _result select 0;
|
||||
if (_payload find "Error:" == 0) exitWith {
|
||||
hint format ["Failed to store vehicle: %1", _payload];
|
||||
};
|
||||
|
||||
private _garage = fromJSON _payload;
|
||||
```
|
||||
|
||||
### Update Garage (Sync)
|
||||
|
||||
Updates the entire garage state. Useful for syncing changes made locally.
|
||||
The returned value is a hash map keyed by generated plate. To find the newly
|
||||
stored vehicle, compare returned keys before and after the add, or search by
|
||||
classname if your workflow guarantees a unique pending vehicle.
|
||||
|
||||
```sqf
|
||||
// _garageMap is the local HashMap of vehicles
|
||||
private _result = "forge_server" callExtension ["garage:update", [
|
||||
getPlayerUID player,
|
||||
toJSON _garageMap
|
||||
]];
|
||||
|
||||
private _updatedGarage = fromJSON (_result select 0);
|
||||
private _storedPlate = "";
|
||||
{
|
||||
private _vehicleRecord = _garage get _x;
|
||||
if ((_vehicleRecord get "classname") == typeOf _vehicle) then {
|
||||
_storedPlate = _x;
|
||||
};
|
||||
} forEach keys _garage;
|
||||
```
|
||||
|
||||
### Patch Vehicle
|
||||
## Patch a Vehicle
|
||||
|
||||
Updates specific fields of a vehicle without sending the entire garage. Useful for frequent updates like fuel or damage.
|
||||
`garage:patch` updates selected fields for one plate. The `plate` field is
|
||||
required. `fuel`, `damage`, and `hit_points` are optional.
|
||||
|
||||
```sqf
|
||||
private _plate = "some-plate-uuid";
|
||||
private _data = createHashMapFromArray [
|
||||
["plate", _plate],
|
||||
["fuel", 0.8],
|
||||
["damage", 0.1],
|
||||
// "hit_points" is optional
|
||||
private _patch = createHashMapFromArray [
|
||||
["plate", _vehicle getVariable ["forge_garage_plate", ""]],
|
||||
["fuel", fuel _vehicle],
|
||||
["damage", damage _vehicle]
|
||||
];
|
||||
|
||||
private _result = "forge_server" callExtension ["garage:patch", [
|
||||
getPlayerUID player,
|
||||
toJSON _data
|
||||
toJSON _patch
|
||||
]];
|
||||
|
||||
private _updatedGarage = fromJSON (_result select 0);
|
||||
```
|
||||
|
||||
### Remove Vehicle
|
||||
## Remove a Vehicle
|
||||
|
||||
Removes a specific vehicle from the garage by plate number.
|
||||
Note: If using the GarageStore, removing a vehicle locally and saving/syncing will also remove it from the server.
|
||||
`garage:remove` expects JSON with a `plate` field.
|
||||
|
||||
```sqf
|
||||
private _plate = "some-plate-uuid";
|
||||
|
||||
private _data = createHashMapFromArray [
|
||||
private _remove = createHashMapFromArray [
|
||||
["plate", _plate]
|
||||
];
|
||||
|
||||
private _result = "forge_server" callExtension ["garage:remove", [
|
||||
getPlayerUID player,
|
||||
toJSON _data
|
||||
toJSON _remove
|
||||
]];
|
||||
|
||||
private _updatedGarage = fromJSON (_result select 0);
|
||||
```
|
||||
|
||||
### Delete Garage
|
||||
|
||||
Permanently deletes all vehicles from a player's garage.
|
||||
## Spawn a Stored Vehicle
|
||||
|
||||
```sqf
|
||||
private _result = "forge_server" callExtension ["garage:delete", [getPlayerUID player]];
|
||||
// Returns: "OK" or "Error: ..."
|
||||
```
|
||||
fnc_spawnGarageVehicle = {
|
||||
params ["_plate"];
|
||||
|
||||
### Check Existence
|
||||
|
||||
Checks if a player has any vehicles in their garage.
|
||||
|
||||
```sqf
|
||||
private _result = "forge_server" callExtension ["garage:exists", [getPlayerUID player]];
|
||||
private _exists = (_result select 0) == "true";
|
||||
```
|
||||
|
||||
## Complete Integration Example
|
||||
|
||||
### Storing a Vehicle
|
||||
|
||||
```sqf
|
||||
fnc_storeVehicle = {
|
||||
params ["_vehicle"];
|
||||
|
||||
// Get vehicle data
|
||||
private _hitPointsData = getAllHitPointsDamage _vehicle;
|
||||
private _hitPoints = createHashMapFromArray [
|
||||
["names", _hitPointsData select 0],
|
||||
["selections", _hitPointsData select 1],
|
||||
["values", _hitPointsData select 2]
|
||||
];
|
||||
|
||||
// Create data hashMap
|
||||
private _data = createHashMapFromArray [
|
||||
["classname", typeOf _vehicle],
|
||||
["damage", damage _vehicle],
|
||||
["hit_points", _hitPoints]
|
||||
];
|
||||
|
||||
// Add to garage (plate is auto-generated)
|
||||
private _result = "forge_server" callExtension ["garage:add", [
|
||||
getPlayerUID player,
|
||||
toJSON _data
|
||||
]];
|
||||
|
||||
// Check for error
|
||||
if ((_result select 0) find "Error:" == 0) exitWith {
|
||||
hint format ["Failed to store vehicle: %1", _result select 0];
|
||||
false
|
||||
};
|
||||
|
||||
// Parse result to get the new vehicle's plate
|
||||
private _updatedGarage = fromJSON (_result select 0);
|
||||
private _newVehicle = _updatedGarage select ((count _updatedGarage) - 1);
|
||||
private _assignedPlate = _newVehicle get "plate";
|
||||
|
||||
// Delete the actual vehicle from game world
|
||||
deleteVehicle _vehicle;
|
||||
|
||||
hint format ["Vehicle %1 stored with plate %2!", _data get "classname", _assignedPlate];
|
||||
true
|
||||
};
|
||||
```
|
||||
|
||||
### Retrieving and Spawning a Vehicle
|
||||
|
||||
```sqf
|
||||
fnc_spawnVehicleFromGarage = {
|
||||
params ["_vehicleIndex"];
|
||||
|
||||
// Get garage
|
||||
private _result = "forge_server" callExtension ["garage:get", [getPlayerUID player]];
|
||||
private _garage = fromJSON (_result select 0);
|
||||
private _payload = _result select 0;
|
||||
|
||||
// Validate index
|
||||
if (_vehicleIndex >= count _garage) exitWith {
|
||||
hint "Invalid vehicle index!";
|
||||
if (_payload find "Error:" == 0) exitWith {
|
||||
hint format ["Failed to load garage: %1", _payload];
|
||||
objNull
|
||||
};
|
||||
|
||||
// Get vehicle data
|
||||
private _vehicleData = _garage select _vehicleIndex;
|
||||
private _classname = _vehicleData get "classname";
|
||||
private _storedDamage = _vehicleData get "damage";
|
||||
private _hitPoints = _vehicleData get "hit_points";
|
||||
private _garage = fromJSON _payload;
|
||||
private _vehicleData = _garage getOrDefault [_plate, createHashMap];
|
||||
if (_vehicleData isEqualTo createHashMap) exitWith {
|
||||
hint "Vehicle plate was not found in your garage.";
|
||||
objNull
|
||||
};
|
||||
|
||||
// Spawn vehicle
|
||||
private _spawnPos = player getPos [10, getDir player];
|
||||
private _vehicle = _classname createVehicle _spawnPos;
|
||||
private _vehicle = (_vehicleData get "classname") createVehicle (player getPos [10, getDir player]);
|
||||
_vehicle setFuel (_vehicleData getOrDefault ["fuel", 1]);
|
||||
_vehicle setDamage (_vehicleData getOrDefault ["damage", 0]);
|
||||
_vehicle setVariable ["forge_garage_plate", _plate, true];
|
||||
|
||||
// Apply damage
|
||||
_vehicle setDamage _storedDamage;
|
||||
|
||||
// Apply hit point damage
|
||||
private _names = _hitPoints get "names";
|
||||
private _values = _hitPoints get "values";
|
||||
private _hitPoints = _vehicleData getOrDefault ["hit_points", createHashMap];
|
||||
private _names = _hitPoints getOrDefault ["names", []];
|
||||
private _values = _hitPoints getOrDefault ["values", []];
|
||||
|
||||
{
|
||||
_vehicle setHitPointDamage [_x, _values select _forEachIndex];
|
||||
} forEach _names;
|
||||
|
||||
// Store plate on vehicle for future updates
|
||||
private _plate = _vehicleData get "plate";
|
||||
_vehicle setVariable ["garagePlate", _plate];
|
||||
private _remove = createHashMapFromArray [["plate", _plate]];
|
||||
"forge_server" callExtension ["garage:remove", [getPlayerUID player, toJSON _remove]];
|
||||
|
||||
// Remove from garage
|
||||
private _removeData = createHashMapFromArray [["plate", _plate]];
|
||||
"forge_server" callExtension ["garage:remove", [getPlayerUID player, toJSON _removeData]];
|
||||
|
||||
hint format ["Vehicle %1 spawned with plate %2!", _classname, _plate];
|
||||
_vehicle
|
||||
};
|
||||
```
|
||||
|
||||
## Hit Points Data Format
|
||||
## Hot State
|
||||
|
||||
The hit points data matches Arma 3's `getAllHitPointsDamage` return format:
|
||||
The `garage:hot:*` commands keep a runtime copy of a player's garage and write
|
||||
it back only when `garage:hot:save` runs.
|
||||
|
||||
```json
|
||||
{
|
||||
"names": ["hitlfwheel", "hitlf2wheel", "hitfuel", "hitengine", "hitbody", ...],
|
||||
"selections": ["wheel_1_1_steering", "wheel_1_2_steering", "fuel_hitpoint", "engine_hitpoint", "body_hitpoint", ...],
|
||||
"values": [0, 0, 0.1, 0.2, 0, ...]
|
||||
}
|
||||
```
|
||||
| Command | Arguments | Returns |
|
||||
| --- | --- | --- |
|
||||
| `garage:hot:init` | `uid` | Vehicle map as JSON. |
|
||||
| `garage:hot:get` | `uid` | Vehicle map as JSON. |
|
||||
| `garage:hot:override` | `uid`, `vehicles_json` | Vehicle map as JSON. |
|
||||
| `garage:hot:add` | `uid`, `vehicle_json` | Vehicle map as JSON. |
|
||||
| `garage:hot:remove_vehicle` | `uid`, `remove_json` | Vehicle map as JSON. |
|
||||
| `garage:hot:save` | `uid` | Current hot vehicle map as JSON. |
|
||||
| `garage:hot:remove` | `uid` | `OK`. |
|
||||
|
||||
- **names**: Hit point identifiers
|
||||
- **selections**: Physical selection names (can be empty strings `""`)
|
||||
- **values**: Damage values from 0.0 (no damage) to 1.0 (destroyed)
|
||||
|
||||
## Error Handling
|
||||
|
||||
All commands return errors in the format `"Error: <message>"`. Always check for this:
|
||||
|
||||
```sqf
|
||||
private _result = "forge_server" callExtension ["garage:get", [getPlayerUID player]];
|
||||
private _data = _result select 0;
|
||||
|
||||
if (_data find "Error:" == 0) then {
|
||||
// Handle error
|
||||
systemChat format ["Garage error: %1", _data];
|
||||
} else {
|
||||
// Parse and use data
|
||||
private _garage = fromJSON _data;
|
||||
};
|
||||
```
|
||||
Use hot state for session-heavy vehicle workflows. Use the durable commands for
|
||||
simple store/retrieve operations.
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Track Vehicle Plates**: When spawning vehicles from the garage, store the plate (UUID) as a variable so you can update them later:
|
||||
```sqf
|
||||
_vehicle setVariable ["garagePlate", _plate];
|
||||
```
|
||||
2. **Auto-Creation**: The system automatically creates an empty garage for new players on first access
|
||||
3. **Validate Before Storage**: Check that vehicles are in good condition before allowing storage
|
||||
4. **Limit Garage Size**: Implement a maximum number of vehicles per player
|
||||
5. **Regular Updates**: Update vehicle damage periodically while in use
|
||||
6. **Clean Deleted Vehicles**: Remove vehicles from garage when they're destroyed or sold
|
||||
- Store the generated plate on spawned vehicles with `setVariable`.
|
||||
- Use `garage:patch` for frequent fuel and damage syncs.
|
||||
- Use `garage:update` only when replacing the whole vehicle map intentionally.
|
||||
- Do not delete the world vehicle until `garage:add` succeeds.
|
||||
- Treat vehicle maps as hash maps keyed by plate, not arrays.
|
||||
|
||||
@ -1,228 +1,203 @@
|
||||
# Locker System Integration Guide
|
||||
# Locker Usage Guide
|
||||
|
||||
## Overview
|
||||
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).
|
||||
|
||||
The locker system provides persistent item storage for Arma 3 players. Each player can store multiple items (weapons, magazines, equipment) with quantity tracking.
|
||||
## Storage Model
|
||||
|
||||
## Data Storage
|
||||
Locker data is persisted through SurrealDB by the server extension.
|
||||
|
||||
- Each player's locker is persisted by the server extension through SurrealDB.
|
||||
- The map is keyed by the item's **classname** (String)
|
||||
- Each item tracks: `category`, `classname`, and `amount`
|
||||
- **Maximum Capacity**: 25 unique items per locker
|
||||
- **Empty lockers are auto-created** when a player first fetches their locker if it doesn't exist
|
||||
|
||||
## Extension Commands
|
||||
|
||||
All commands are accessed via the `locker` group:
|
||||
|
||||
### Create Locker
|
||||
|
||||
Creates a new empty locker for a player. Should be called when initializing a new player if one doesn't exist.
|
||||
|
||||
```sqf
|
||||
private _result = "forge_server" callExtension ["locker:create", [getPlayerUID player]];
|
||||
// Returns: {} (empty map) or Error message
|
||||
```json
|
||||
{
|
||||
"arifle_MX_F": {
|
||||
"category": "weapon",
|
||||
"classname": "arifle_MX_F",
|
||||
"amount": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Get Locker
|
||||
Rules validated by the Rust service:
|
||||
|
||||
Retrieves all items in a player's locker.
|
||||
- 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.
|
||||
|
||||
## 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 _lockerMap = fromJSON (_result select 0);
|
||||
// Returns: {"arifle_MX_F": {"classname":"arifle_MX_F","category":"Weapon","amount":1}, ...}
|
||||
private _payload = _result select 0;
|
||||
|
||||
if (_payload find "Error:" == 0) exitWith {
|
||||
systemChat format ["Locker error: %1", _payload];
|
||||
};
|
||||
|
||||
private _locker = fromJSON _payload;
|
||||
```
|
||||
|
||||
### Add Item
|
||||
## Add an Item
|
||||
|
||||
Adds a new item to the locker or updates the amount if it already exists.
|
||||
**Checks capacity limit (25 items).**
|
||||
`locker:add` creates or overwrites one classname entry.
|
||||
|
||||
```sqf
|
||||
private _data = createHashMapFromArray [
|
||||
private _item = createHashMapFromArray [
|
||||
["category", "weapon"],
|
||||
["classname", "arifle_MX_F"],
|
||||
["category", "Weapon"],
|
||||
["amount", 1]
|
||||
];
|
||||
|
||||
private _result = "forge_server" callExtension ["locker:add", [
|
||||
getPlayerUID player,
|
||||
toJSON _data
|
||||
toJSON _item
|
||||
]];
|
||||
|
||||
private _updatedLocker = fromJSON (_result select 0);
|
||||
// Returns updated locker map
|
||||
private _payload = _result select 0;
|
||||
if (_payload find "Error:" == 0) exitWith {
|
||||
hint format ["Failed to store item: %1", _payload];
|
||||
};
|
||||
|
||||
private _locker = fromJSON _payload;
|
||||
```
|
||||
|
||||
### Update Locker (Sync)
|
||||
## Patch an Amount
|
||||
|
||||
Updates the entire locker state. Useful for syncing changes made locally (e.g., bulk moves).
|
||||
**Replaces the entire locker content.**
|
||||
`locker:patch` currently patches the `amount` field for an existing classname.
|
||||
|
||||
```sqf
|
||||
// _lockerMap is the local HashMap of items
|
||||
private _result = "forge_server" callExtension ["locker:update", [
|
||||
getPlayerUID player,
|
||||
toJSON _lockerMap
|
||||
]];
|
||||
|
||||
private _updatedLocker = fromJSON (_result select 0);
|
||||
```
|
||||
|
||||
### Patch Item
|
||||
|
||||
Updates specific fields (currently `amount`) of an existing item without sending the entire locker.
|
||||
**Efficient for quantity changes.**
|
||||
|
||||
```sqf
|
||||
private _classname = "arifle_MX_F";
|
||||
private _data = createHashMapFromArray [
|
||||
["classname", _classname],
|
||||
["amount", 5] // New amount
|
||||
private _patch = createHashMapFromArray [
|
||||
["classname", "arifle_MX_F"],
|
||||
["amount", 5]
|
||||
];
|
||||
|
||||
private _result = "forge_server" callExtension ["locker:patch", [
|
||||
getPlayerUID player,
|
||||
toJSON _data
|
||||
toJSON _patch
|
||||
]];
|
||||
|
||||
private _updatedLocker = fromJSON (_result select 0);
|
||||
```
|
||||
|
||||
### Remove Item
|
||||
## Remove an Item
|
||||
|
||||
Removes a specific item from the locker by classname.
|
||||
`locker:remove` takes the classname as the second argument.
|
||||
|
||||
```sqf
|
||||
private _classname = "arifle_MX_F";
|
||||
private _data = createHashMapFromArray [
|
||||
["classname", _classname]
|
||||
];
|
||||
|
||||
private _result = "forge_server" callExtension ["locker:remove", [
|
||||
getPlayerUID player,
|
||||
toJSON _data
|
||||
"arifle_MX_F"
|
||||
]];
|
||||
|
||||
private _updatedLocker = fromJSON (_result select 0);
|
||||
```
|
||||
|
||||
### Delete Locker
|
||||
|
||||
Permanently deletes all items from a player's locker.
|
||||
## Retrieve an Item
|
||||
|
||||
```sqf
|
||||
private _result = "forge_server" callExtension ["locker:delete", [getPlayerUID player]];
|
||||
// Returns: "OK" or "Error: ..."
|
||||
```
|
||||
fnc_retrieveLockerItem = {
|
||||
params ["_classname"];
|
||||
|
||||
### Check Existence
|
||||
private _result = "forge_server" callExtension ["locker:get", [getPlayerUID player]];
|
||||
private _payload = _result select 0;
|
||||
|
||||
Checks if a player has a locker created.
|
||||
|
||||
```sqf
|
||||
private _result = "forge_server" callExtension ["locker:exists", [getPlayerUID player]];
|
||||
private _exists = (_result select 0) == "true";
|
||||
```
|
||||
|
||||
## Complete Integration Example
|
||||
|
||||
### Storing an Item
|
||||
|
||||
```sqf
|
||||
fnc_storeCurrentWeapon = {
|
||||
private _weapon = currentWeapon player;
|
||||
if (_weapon == "") exitWith { hint "No weapon in hand!"; };
|
||||
|
||||
// Create item data
|
||||
private _data = createHashMapFromArray [
|
||||
["classname", _weapon],
|
||||
["category", "Weapon"],
|
||||
["amount", 1]
|
||||
];
|
||||
|
||||
// Add to locker
|
||||
private _result = "forge_server" callExtension ["locker:add", [
|
||||
getPlayerUID player,
|
||||
toJSON _data
|
||||
]];
|
||||
|
||||
// Check for error (e.g., full locker)
|
||||
if ((_result select 0) find "Error:" == 0) exitWith {
|
||||
hint format ["Failed to store item: %1", _result select 0];
|
||||
if (_payload find "Error:" == 0) exitWith {
|
||||
hint format ["Failed to load locker: %1", _payload];
|
||||
false
|
||||
};
|
||||
|
||||
// Remove weapon from player
|
||||
player removeWeapon _weapon;
|
||||
hint format ["Stored %1 in locker!", _weapon];
|
||||
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
|
||||
};
|
||||
```
|
||||
|
||||
### Retrieving an Item
|
||||
## Replace the Whole Locker
|
||||
|
||||
`locker:update` replaces the whole item map. Use it for explicit bulk syncs,
|
||||
not single-item changes.
|
||||
|
||||
```sqf
|
||||
fnc_retrieveItem = {
|
||||
params ["_classname"];
|
||||
private _items = createHashMapFromArray [
|
||||
["arifle_MX_F", createHashMapFromArray [
|
||||
["category", "weapon"],
|
||||
["classname", "arifle_MX_F"],
|
||||
["amount", 1]
|
||||
]]
|
||||
];
|
||||
|
||||
// Get locker
|
||||
private _result = "forge_server" callExtension ["locker:get", [getPlayerUID player]];
|
||||
private _locker = fromJSON (_result select 0);
|
||||
|
||||
// Check if item exists
|
||||
private _item = _locker getOrDefault [_classname, locationNull];
|
||||
if (_item isEqualTo locationNull) exitWith {
|
||||
hint "Item not found in locker!";
|
||||
false
|
||||
};
|
||||
|
||||
// Get amount
|
||||
private _amount = _item get "amount";
|
||||
if (_amount <= 0) exitWith {
|
||||
// Should not happen, but safe to handle
|
||||
hint "Item out of stock!";
|
||||
false
|
||||
};
|
||||
|
||||
// Add to player
|
||||
if (player canAdd _classname) then {
|
||||
player addItem _classname;
|
||||
|
||||
// Decrement amount or remove if 0
|
||||
if (_amount > 1) then {
|
||||
private _patchData = createHashMapFromArray [
|
||||
["classname", _classname],
|
||||
["amount", _amount - 1]
|
||||
];
|
||||
"forge_server" callExtension ["locker:patch", [getPlayerUID player, toJSON _patchData]];
|
||||
} else {
|
||||
private _removeData = createHashMapFromArray [
|
||||
["classname", _classname]
|
||||
];
|
||||
"forge_server" callExtension ["locker:remove", [getPlayerUID player, toJSON _removeData]];
|
||||
};
|
||||
|
||||
hint format ["Retrieved %1 from locker!", _classname];
|
||||
true
|
||||
} else {
|
||||
hint "Not enough space in inventory!";
|
||||
false
|
||||
};
|
||||
};
|
||||
private _result = "forge_server" callExtension ["locker:update", [
|
||||
getPlayerUID player,
|
||||
toJSON _items
|
||||
]];
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
## Hot State
|
||||
|
||||
All commands return errors in the format `"Error: <message>"`.
|
||||
The `locker:hot:*` commands keep a runtime copy of a player's locker and write
|
||||
it back only when `locker:hot:save` runs.
|
||||
|
||||
```sqf
|
||||
private _result = "forge_server" callExtension ["locker:add", ...];
|
||||
private _data = _result select 0;
|
||||
| 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`. |
|
||||
|
||||
if (_data find "Error:" == 0) then {
|
||||
systemChat format ["Locker error: %1", _data];
|
||||
};
|
||||
```
|
||||
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.
|
||||
|
||||
@ -30,8 +30,20 @@ docs/ Framework-level documentation
|
||||
| Phone | Contacts, messages, and email state. | `arma/client/addons/phone` | `arma/server/addons/phone` | `lib/models/src/phone.rs`, `lib/services/src/phone.rs` | `phone:*` |
|
||||
| Store | Store catalog checkout workflows and checkout charging integration. | `arma/client/addons/store` | `arma/server/addons/store` | `lib/models/src/store.rs`, `lib/services/src/store.rs` | `store:checkout` |
|
||||
| Task | Mission/task catalog, ownership, status, reward context, and task counters. | none | `arma/server/addons/task` | `lib/models/src/task.rs`, `lib/services/src/task.rs` | `task:*` |
|
||||
| Owned Garage | Organization or owner-scoped vehicle storage. | via garage/org UI | server extension only | `lib/models/src/v_garage.rs`, `lib/services/src/v_garage.rs` | `owned:garage:*` |
|
||||
| Owned Locker | Organization or owner-scoped item storage. | via locker/org UI | server extension only | `lib/models/src/v_locker.rs`, `lib/services/src/v_locker.rs` | `owned:locker:*` |
|
||||
| Owned Garage | Organization or owner-scoped vehicle unlock storage. | via garage/org UI | server extension only | `lib/models/src/v_garage.rs`, `lib/services/src/v_garage.rs` | `owned:garage:*` |
|
||||
| Owned Locker | Organization or owner-scoped arsenal unlock storage. | via locker/org UI | server extension only | `lib/models/src/v_locker.rs`, `lib/services/src/v_locker.rs` | `owned:locker:*` |
|
||||
|
||||
Guides:
|
||||
[Actor](./ACTOR_USAGE_GUIDE.md),
|
||||
[Bank](./BANK_USAGE_GUIDE.md),
|
||||
[CAD](./CAD_USAGE_GUIDE.md),
|
||||
[Garage](./GARAGE_USAGE_GUIDE.md),
|
||||
[Locker](./LOCKER_USAGE_GUIDE.md),
|
||||
[Organization](./ORG_USAGE_GUIDE.md),
|
||||
[Owned Storage](./OWNED_STORAGE_USAGE_GUIDE.md),
|
||||
[Phone](./PHONE_USAGE_GUIDE.md),
|
||||
[Store](./STORE_USAGE_GUIDE.md),
|
||||
[Task](./TASK_USAGE_GUIDE.md).
|
||||
|
||||
## Infrastructure Modules
|
||||
|
||||
|
||||
232
docs/ORG_USAGE_GUIDE.md
Normal file
232
docs/ORG_USAGE_GUIDE.md
Normal file
@ -0,0 +1,232 @@
|
||||
# Organization Usage Guide
|
||||
|
||||
The organization module stores organization records, members, assets, fleet
|
||||
entries, and credit lines. Durable commands manage persisted records directly.
|
||||
Hot-state commands support the active organization UI workflows.
|
||||
|
||||
## Storage Model
|
||||
|
||||
Core organization:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "default",
|
||||
"owner": "server",
|
||||
"name": "Default Organization",
|
||||
"funds": 0.0,
|
||||
"reputation": 0,
|
||||
"credit_lines": {}
|
||||
}
|
||||
```
|
||||
|
||||
Hot organization:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "default",
|
||||
"owner": "server",
|
||||
"name": "Default Organization",
|
||||
"funds": 0.0,
|
||||
"reputation": 0,
|
||||
"credit_lines": {},
|
||||
"assets": {},
|
||||
"fleet": {},
|
||||
"members": {},
|
||||
"pending_invites": {}
|
||||
}
|
||||
```
|
||||
|
||||
Rules validated by the Rust service:
|
||||
|
||||
- `id` must be non-empty and contain only alphanumeric characters or `_`.
|
||||
- `owner` must be `server` or a 17-digit Steam UID.
|
||||
- `name` cannot be empty, cannot exceed 100 characters, and cannot contain
|
||||
control characters.
|
||||
- `funds`, reputation, and credit line amounts cannot be negative.
|
||||
- Player registration is rejected when the player already belongs to a
|
||||
non-default organization.
|
||||
|
||||
## Durable Commands
|
||||
|
||||
| Command | Arguments | Returns |
|
||||
| --- | --- | --- |
|
||||
| `org:create` | `org_id`, `org_json` | Organization JSON. |
|
||||
| `org:get` | `org_id` | Organization JSON. |
|
||||
| `org:update` | `org_id`, `patch_json` | Updated organization JSON. |
|
||||
| `org:exists` | `org_id` | `true` or `false`. |
|
||||
| `org:delete` | `org_id` | `OK`. |
|
||||
| `org:assets:get` | `org_id` | Asset map JSON. |
|
||||
| `org:assets:update` | `org_id`, `assets_json` | Updated asset map JSON. |
|
||||
| `org:fleet:get` | `org_id` | Fleet map JSON. |
|
||||
| `org:fleet:update` | `org_id`, `fleet_json` | Updated fleet map JSON. |
|
||||
| `org:members:get` | `org_id` | Member array JSON. |
|
||||
| `org:members:add` | `org_id`, `member_uid` | `OK`. |
|
||||
| `org:members:remove` | `org_id`, `member_uid` | `OK`. |
|
||||
|
||||
## Create an Organization
|
||||
|
||||
The command key is authoritative for `id`.
|
||||
|
||||
```sqf
|
||||
private _org = createHashMapFromArray [
|
||||
["id", _orgId],
|
||||
["owner", getPlayerUID player],
|
||||
["name", "Spearnet Logistics"],
|
||||
["funds", 0],
|
||||
["reputation", 0],
|
||||
["credit_lines", createHashMap]
|
||||
];
|
||||
|
||||
private _result = "forge_server" callExtension ["org:create", [
|
||||
_orgId,
|
||||
toJSON _org
|
||||
]];
|
||||
```
|
||||
|
||||
## Update Organization Funds
|
||||
|
||||
```sqf
|
||||
private _patch = createHashMapFromArray [
|
||||
["funds", 5000],
|
||||
["reputation", 10]
|
||||
];
|
||||
|
||||
private _result = "forge_server" callExtension ["org:update", [
|
||||
_orgId,
|
||||
toJSON _patch
|
||||
]];
|
||||
```
|
||||
|
||||
Supported durable patch fields are `id`, `owner`, `name`, `funds`,
|
||||
`reputation`, and `credit_lines`.
|
||||
|
||||
## Assets and Fleet
|
||||
|
||||
Assets are grouped by category, then classname.
|
||||
|
||||
```sqf
|
||||
private _assets = createHashMapFromArray [
|
||||
["ammo", createHashMapFromArray [
|
||||
["ACE_30Rnd_65x39_caseless_mag", createHashMapFromArray [
|
||||
["classname", "ACE_30Rnd_65x39_caseless_mag"],
|
||||
["type", "ammo"],
|
||||
["quantity", 20]
|
||||
]]
|
||||
]]
|
||||
];
|
||||
|
||||
"forge_server" callExtension ["org:assets:update", [_orgId, toJSON _assets]];
|
||||
```
|
||||
|
||||
Fleet is keyed by an internal fleet entry ID.
|
||||
|
||||
```sqf
|
||||
private _fleet = createHashMapFromArray [
|
||||
["B_Truck_01_transport_F_0", createHashMapFromArray [
|
||||
["classname", "B_Truck_01_transport_F"],
|
||||
["name", "Transport Truck"],
|
||||
["type", "cars"],
|
||||
["status", "Ready"],
|
||||
["damage", "0%"]
|
||||
]]
|
||||
];
|
||||
|
||||
"forge_server" callExtension ["org:fleet:update", [_orgId, toJSON _fleet]];
|
||||
```
|
||||
|
||||
## Hot-State Commands
|
||||
|
||||
| Command | Arguments | Returns |
|
||||
| --- | --- | --- |
|
||||
| `org:hot:init` | `org_id` | Hot organization JSON. |
|
||||
| `org:hot:get` | `org_id` | Hot organization JSON. |
|
||||
| `org:hot:override` | `org_id`, `hot_org_json` | Hot organization JSON. |
|
||||
| `org:hot:ensure_member` | `context_json` | Hot organization JSON. |
|
||||
| `org:hot:member_invites` | `member_uid` | Invite array JSON. |
|
||||
| `org:hot:register` | `context_json` | Register result JSON. |
|
||||
| `org:hot:invite_member` | `context_json` | Invite result JSON. |
|
||||
| `org:hot:accept_invite` | `context_json` | Invite decision result JSON. |
|
||||
| `org:hot:decline_invite` | `context_json` | Invite decision result JSON. |
|
||||
| `org:hot:assign_credit_line` | `context_json` | Mutation result JSON. |
|
||||
| `org:hot:repay_credit_line` | `context_json` | Repayment result JSON. |
|
||||
| `org:hot:charge_checkout` | `context_json` | Mutation result JSON. |
|
||||
| `org:hot:add_assets` | `context_json`, `assets_json` | Mutation result JSON. |
|
||||
| `org:hot:add_fleet` | `context_json`, `fleet_json` | Mutation result JSON. |
|
||||
| `org:hot:leave` | `context_json` | Leave result JSON. |
|
||||
| `org:hot:disband` | `context_json` | Disband result JSON. |
|
||||
| `org:hot:save` | `org_id` | Current hot organization JSON and async durable save. |
|
||||
| `org:hot:remove` | `org_id` | `OK`. |
|
||||
|
||||
## Register from UI Context
|
||||
|
||||
```sqf
|
||||
private _context = createHashMapFromArray [
|
||||
["requesterUid", getPlayerUID player],
|
||||
["requesterName", name player],
|
||||
["orgId", _orgId],
|
||||
["orgName", "Spearnet Logistics"],
|
||||
["existingOrgId", "default"]
|
||||
];
|
||||
|
||||
private _result = "forge_server" callExtension ["org:hot:register", [toJSON _context]];
|
||||
```
|
||||
|
||||
## Invite and Accept
|
||||
|
||||
```sqf
|
||||
private _invite = createHashMapFromArray [
|
||||
["requesterUid", getPlayerUID player],
|
||||
["requesterName", name player],
|
||||
["orgId", _orgId],
|
||||
["requesterIsDefaultOrgCeo", false],
|
||||
["targetUid", _targetUid],
|
||||
["targetName", _targetName],
|
||||
["targetOrgId", "default"]
|
||||
];
|
||||
|
||||
"forge_server" callExtension ["org:hot:invite_member", [toJSON _invite]];
|
||||
|
||||
private _decision = createHashMapFromArray [
|
||||
["requesterUid", _targetUid],
|
||||
["requesterName", _targetName],
|
||||
["orgId", _orgId],
|
||||
["existingOrgId", "default"]
|
||||
];
|
||||
|
||||
"forge_server" callExtension ["org:hot:accept_invite", [toJSON _decision]];
|
||||
```
|
||||
|
||||
## Credit Line Checkout
|
||||
|
||||
```sqf
|
||||
private _credit = createHashMapFromArray [
|
||||
["requesterUid", getPlayerUID player],
|
||||
["orgId", _orgId],
|
||||
["requesterIsDefaultOrgCeo", false],
|
||||
["memberUid", _memberUid],
|
||||
["memberName", _memberName],
|
||||
["amount", 1000]
|
||||
];
|
||||
|
||||
"forge_server" callExtension ["org:hot:assign_credit_line", [toJSON _credit]];
|
||||
|
||||
private _charge = createHashMapFromArray [
|
||||
["requesterUid", _memberUid],
|
||||
["orgId", _orgId],
|
||||
["requesterIsDefaultOrgCeo", false],
|
||||
["source", "credit_line"],
|
||||
["amount", 250],
|
||||
["commit", true]
|
||||
];
|
||||
|
||||
"forge_server" callExtension ["org:hot:charge_checkout", [toJSON _charge]];
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```sqf
|
||||
private _payload = _result select 0;
|
||||
if (_payload find "Error:" == 0) exitWith {
|
||||
systemChat format ["Organization error: %1", _payload];
|
||||
};
|
||||
```
|
||||
158
docs/OWNED_STORAGE_USAGE_GUIDE.md
Normal file
158
docs/OWNED_STORAGE_USAGE_GUIDE.md
Normal file
@ -0,0 +1,158 @@
|
||||
# Owned Storage Usage Guide
|
||||
|
||||
Owned storage covers the `owned:locker` and `owned:garage` extension command
|
||||
groups. These modules store unlock lists rather than physical item or vehicle
|
||||
instances.
|
||||
|
||||
Use these modules for virtual arsenal and virtual garage unlocks. Use
|
||||
[Locker Usage Guide](./LOCKER_USAGE_GUIDE.md) and
|
||||
[Garage Usage Guide](./GARAGE_USAGE_GUIDE.md) for physical inventory and stored
|
||||
vehicle instances.
|
||||
|
||||
## Owned Locker Model
|
||||
|
||||
```json
|
||||
{
|
||||
"items": ["FirstAidKit"],
|
||||
"weapons": ["arifle_MX_F"],
|
||||
"magazines": ["30Rnd_65x39_caseless_black_mag"],
|
||||
"backpacks": ["B_AssaultPack_rgr"]
|
||||
}
|
||||
```
|
||||
|
||||
Supported owned locker categories:
|
||||
|
||||
- `items`
|
||||
- `weapons`
|
||||
- `magazines`
|
||||
- `backpacks`
|
||||
|
||||
New owned lockers are created with default unlocks from the Rust model.
|
||||
|
||||
## Owned Garage Model
|
||||
|
||||
```json
|
||||
{
|
||||
"cars": ["B_Quadbike_01_F"],
|
||||
"armor": [],
|
||||
"helis": [],
|
||||
"planes": [],
|
||||
"naval": [],
|
||||
"other": []
|
||||
}
|
||||
```
|
||||
|
||||
Supported owned garage categories:
|
||||
|
||||
- `cars`
|
||||
- `armor`
|
||||
- `helis`
|
||||
- `planes`
|
||||
- `naval`
|
||||
- `other`
|
||||
|
||||
The durable `owned:garage:remove` command currently accepts `heli` for the
|
||||
helicopter category. Add, get, and hot remove accept `helis`.
|
||||
|
||||
New owned garages are created with default unlocks from the Rust model.
|
||||
|
||||
## Owned Locker Commands
|
||||
|
||||
| Command | Arguments | Returns |
|
||||
| --- | --- | --- |
|
||||
| `owned:locker:create` | `uid` | Full owned locker JSON. |
|
||||
| `owned:locker:fetch` | `uid` | Full owned locker JSON. |
|
||||
| `owned:locker:get` | `uid`, `category` | Category classname array JSON. |
|
||||
| `owned:locker:add` | `uid`, `category`, `classnames_json` | Updated category array JSON. |
|
||||
| `owned:locker:remove` | `uid`, `category`, `classname` | Updated category array JSON. |
|
||||
| `owned:locker:delete` | `uid` | `OK`. |
|
||||
| `owned:locker:exists` | `uid` | `true` or `false`. |
|
||||
|
||||
## Owned Garage Commands
|
||||
|
||||
| Command | Arguments | Returns |
|
||||
| --- | --- | --- |
|
||||
| `owned:garage:create` | `uid` | Full owned garage JSON. |
|
||||
| `owned:garage:fetch` | `uid` | Full owned garage JSON. |
|
||||
| `owned:garage:get` | `uid`, `category` | Category classname array JSON. |
|
||||
| `owned:garage:add` | `uid`, `category`, `classnames_json` | Updated category array JSON. |
|
||||
| `owned:garage:remove` | `uid`, `category`, `classname` | Updated category array JSON. |
|
||||
| `owned:garage:delete` | `uid` | `OK`. |
|
||||
| `owned:garage:exists` | `uid` | `true` or `false`. |
|
||||
|
||||
## Add Virtual Arsenal Unlocks
|
||||
|
||||
```sqf
|
||||
private _classes = ["arifle_MX_F", "hgun_P07_F"];
|
||||
|
||||
private _result = "forge_server" callExtension ["owned:locker:add", [
|
||||
getPlayerUID player,
|
||||
"weapons",
|
||||
toJSON _classes
|
||||
]];
|
||||
```
|
||||
|
||||
## Add Virtual Garage Unlocks
|
||||
|
||||
```sqf
|
||||
private _classes = ["B_Quadbike_01_F", "B_MRAP_01_F"];
|
||||
|
||||
private _result = "forge_server" callExtension ["owned:garage:add", [
|
||||
getPlayerUID player,
|
||||
"cars",
|
||||
toJSON _classes
|
||||
]];
|
||||
```
|
||||
|
||||
## Remove an Unlock
|
||||
|
||||
```sqf
|
||||
"forge_server" callExtension ["owned:locker:remove", [
|
||||
getPlayerUID player,
|
||||
"weapons",
|
||||
"arifle_MX_F"
|
||||
]];
|
||||
|
||||
"forge_server" callExtension ["owned:garage:remove", [
|
||||
getPlayerUID player,
|
||||
"cars",
|
||||
"B_Quadbike_01_F"
|
||||
]];
|
||||
```
|
||||
|
||||
## Hot-State Commands
|
||||
|
||||
Both owned storage modules support hot state.
|
||||
|
||||
Owned locker:
|
||||
|
||||
| Command | Arguments | Returns |
|
||||
| --- | --- | --- |
|
||||
| `owned:locker:hot:init` | `uid` | Full owned locker JSON. |
|
||||
| `owned:locker:hot:fetch` | `uid` | Full owned locker JSON. |
|
||||
| `owned:locker:hot:get` | `uid`, `category` | Category array JSON. |
|
||||
| `owned:locker:hot:override` | `uid`, `locker_json` | Full owned locker JSON. |
|
||||
| `owned:locker:hot:save` | `uid` | Current hot owned locker JSON and async durable save. |
|
||||
| `owned:locker:hot:remove` | `uid` | `OK`. |
|
||||
|
||||
Owned garage:
|
||||
|
||||
| Command | Arguments | Returns |
|
||||
| --- | --- | --- |
|
||||
| `owned:garage:hot:init` | `uid` | Full owned garage JSON. |
|
||||
| `owned:garage:hot:fetch` | `uid` | Full owned garage JSON. |
|
||||
| `owned:garage:hot:get` | `uid`, `category` | Category array JSON. |
|
||||
| `owned:garage:hot:override` | `uid`, `garage_json` | Full owned garage JSON. |
|
||||
| `owned:garage:hot:add` | `uid`, `category`, `classnames_json` | Updated category array JSON. |
|
||||
| `owned:garage:hot:remove_item` | `uid`, `category`, `classname` | Updated category array JSON. |
|
||||
| `owned:garage:hot:save` | `uid` | Current hot owned garage JSON and async durable save. |
|
||||
| `owned:garage:hot:remove` | `uid` | `OK`. |
|
||||
|
||||
## Error Handling
|
||||
|
||||
```sqf
|
||||
private _payload = _result select 0;
|
||||
if (_payload find "Error:" == 0) exitWith {
|
||||
systemChat format ["Owned storage error: %1", _payload];
|
||||
};
|
||||
```
|
||||
136
docs/PHONE_USAGE_GUIDE.md
Normal file
136
docs/PHONE_USAGE_GUIDE.md
Normal file
@ -0,0 +1,136 @@
|
||||
# Phone Usage Guide
|
||||
|
||||
The phone module stores contacts, messages, and emails for each UID. It is a
|
||||
server-extension state module backed by SurrealDB.
|
||||
|
||||
## Storage Model
|
||||
|
||||
```json
|
||||
{
|
||||
"contacts": ["76561198000000000", "field_commander"],
|
||||
"messages": [
|
||||
{
|
||||
"id": "phone-message:sender:receiver:1",
|
||||
"from": "sender",
|
||||
"to": "receiver",
|
||||
"message": "Text body",
|
||||
"timestamp": 123.45,
|
||||
"read": false
|
||||
}
|
||||
],
|
||||
"emails": [
|
||||
{
|
||||
"id": "phone-email:sender:receiver:2",
|
||||
"from": "sender",
|
||||
"to": "receiver",
|
||||
"subject": "Subject",
|
||||
"body": "Email body",
|
||||
"timestamp": 123.45,
|
||||
"read": false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Rules validated by the Rust service:
|
||||
|
||||
- UID arguments cannot be empty.
|
||||
- Message and email bodies cannot be empty.
|
||||
- Empty email subjects become `No subject`.
|
||||
- Player messages and emails cannot target `field_commander`.
|
||||
- `field_commander` can send messages or emails to players.
|
||||
- Deleting a message or email removes it only from the requesting UID's index.
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Arguments | Returns |
|
||||
| --- | --- | --- |
|
||||
| `phone:init` | `uid` | Full phone payload. |
|
||||
| `phone:contacts:list` | `uid` | Contact UID array. |
|
||||
| `phone:contacts:add` | `uid`, `contact_uid` | `true` or `false`. |
|
||||
| `phone:contacts:remove` | `uid`, `contact_uid` | `true` or `false`. |
|
||||
| `phone:messages:list` | `uid` | Message array. |
|
||||
| `phone:messages:thread` | `uid`, `other_uid` | Message array for both participants. |
|
||||
| `phone:messages:send` | `from_uid`, `to_uid`, `message`, `timestamp` | Message JSON. |
|
||||
| `phone:messages:mark_read` | `uid`, `message_id` | `true` or `false`. |
|
||||
| `phone:messages:delete` | `uid`, `message_id` | `true` or `false`. |
|
||||
| `phone:emails:list` | `uid` | Email array. |
|
||||
| `phone:emails:send` | `from_uid`, `to_uid`, `subject`, `body`, `timestamp` | Email JSON. |
|
||||
| `phone:emails:mark_read` | `uid`, `email_id` | `true` or `false`. |
|
||||
| `phone:emails:delete` | `uid`, `email_id` | `true` or `false`. |
|
||||
| `phone:remove` | `uid` | `OK`. |
|
||||
|
||||
## Initialize Phone State
|
||||
|
||||
`phone:init` creates phone state if needed and seeds self-contact plus
|
||||
`field_commander`.
|
||||
|
||||
```sqf
|
||||
private _result = "forge_server" callExtension ["phone:init", [getPlayerUID player]];
|
||||
private _payload = _result select 0;
|
||||
|
||||
if (_payload find "Error:" == 0) exitWith {
|
||||
systemChat format ["Phone init failed: %1", _payload];
|
||||
};
|
||||
|
||||
private _phone = fromJSON _payload;
|
||||
```
|
||||
|
||||
## Send a Message
|
||||
|
||||
```sqf
|
||||
private _timestamp = str diag_tickTime;
|
||||
|
||||
private _result = "forge_server" callExtension ["phone:messages:send", [
|
||||
getPlayerUID player,
|
||||
_targetUid,
|
||||
"Move to checkpoint Alpha.",
|
||||
_timestamp
|
||||
]];
|
||||
```
|
||||
|
||||
## Read a Conversation
|
||||
|
||||
```sqf
|
||||
private _result = "forge_server" callExtension ["phone:messages:thread", [
|
||||
getPlayerUID player,
|
||||
_otherUid
|
||||
]];
|
||||
|
||||
private _messages = fromJSON (_result select 0);
|
||||
```
|
||||
|
||||
## Send an Email
|
||||
|
||||
```sqf
|
||||
private _result = "forge_server" callExtension ["phone:emails:send", [
|
||||
getPlayerUID player,
|
||||
_targetUid,
|
||||
"Supply Request",
|
||||
"Requesting resupply at grid 123456.",
|
||||
str diag_tickTime
|
||||
]];
|
||||
```
|
||||
|
||||
## Mark and Delete Records
|
||||
|
||||
```sqf
|
||||
"forge_server" callExtension ["phone:messages:mark_read", [
|
||||
getPlayerUID player,
|
||||
_messageId
|
||||
]];
|
||||
|
||||
"forge_server" callExtension ["phone:emails:delete", [
|
||||
getPlayerUID player,
|
||||
_emailId
|
||||
]];
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```sqf
|
||||
private _payload = (_result select 0);
|
||||
if (_payload find "Error:" == 0) then {
|
||||
systemChat format ["Phone error: %1", _payload];
|
||||
};
|
||||
```
|
||||
@ -16,8 +16,16 @@ collects framework-level documentation for those pieces.
|
||||
|
||||
## Existing Usage Guides
|
||||
|
||||
- [Actor Usage Guide](./ACTOR_USAGE_GUIDE.md)
|
||||
- [Bank Usage Guide](./BANK_USAGE_GUIDE.md)
|
||||
- [CAD Usage Guide](./CAD_USAGE_GUIDE.md)
|
||||
- [Garage Usage Guide](./GARAGE_USAGE_GUIDE.md)
|
||||
- [Locker Usage Guide](./LOCKER_USAGE_GUIDE.md)
|
||||
- [Organization Usage Guide](./ORG_USAGE_GUIDE.md)
|
||||
- [Owned Storage Usage Guide](./OWNED_STORAGE_USAGE_GUIDE.md)
|
||||
- [Phone Usage Guide](./PHONE_USAGE_GUIDE.md)
|
||||
- [Store Usage Guide](./STORE_USAGE_GUIDE.md)
|
||||
- [Task Usage Guide](./TASK_USAGE_GUIDE.md)
|
||||
|
||||
## Related Documentation
|
||||
|
||||
|
||||
135
docs/STORE_USAGE_GUIDE.md
Normal file
135
docs/STORE_USAGE_GUIDE.md
Normal file
@ -0,0 +1,135 @@
|
||||
# Store Usage Guide
|
||||
|
||||
The store module processes checkout requests. It charges a payment source and
|
||||
grants purchased items to the player locker, virtual arsenal locker, and
|
||||
virtual garage unlocks.
|
||||
|
||||
## Checkout Model
|
||||
|
||||
`store:checkout` accepts one JSON context.
|
||||
|
||||
```json
|
||||
{
|
||||
"requesterUid": "76561198000000000",
|
||||
"requesterName": "Player Name",
|
||||
"orgId": "default",
|
||||
"requesterIsDefaultOrgCeo": false,
|
||||
"paymentMethod": "bank",
|
||||
"items": [
|
||||
{
|
||||
"classname": "arifle_MX_F",
|
||||
"category": "weapon",
|
||||
"priceValue": 500,
|
||||
"quantity": 1
|
||||
}
|
||||
],
|
||||
"vehicles": [
|
||||
{
|
||||
"classname": "B_Quadbike_01_F",
|
||||
"category": "cars",
|
||||
"priceValue": 1500
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Rules validated by the Rust service:
|
||||
|
||||
- `requesterUid` is required.
|
||||
- At least one item or vehicle is required.
|
||||
- The checkout total must be greater than zero.
|
||||
- Item categories must be `item`, `attachment`, `weapon`, `magazine`, or
|
||||
`backpack`.
|
||||
- Vehicle categories must be `cars`, `armor`, `helis`, `planes`, `naval`, or
|
||||
`other`.
|
||||
- Payment method must be `cash`, `bank`, `org_funds`, or `credit_line`.
|
||||
- Player locker capacity cannot exceed 25 unique items after checkout.
|
||||
- Organization funds can only be charged by the org owner or the default org
|
||||
CEO flag.
|
||||
|
||||
## Command
|
||||
|
||||
| Command | Arguments | Returns |
|
||||
| --- | --- | --- |
|
||||
| `store:checkout` | `checkout_json` | Checkout result JSON. |
|
||||
|
||||
## Result Model
|
||||
|
||||
```json
|
||||
{
|
||||
"chargedTotal": 2000.0,
|
||||
"paymentMethod": "bank",
|
||||
"message": "Checkout completed. $2,000 charged, 1 locker grant(s), 1 vehicle unlock(s).",
|
||||
"lockerGranted": [],
|
||||
"vehicleGranted": [],
|
||||
"lockerPatch": {},
|
||||
"vaPatch": {},
|
||||
"vgaragePatch": {},
|
||||
"bankPatch": {},
|
||||
"orgPatch": {},
|
||||
"orgTargetUids": []
|
||||
}
|
||||
```
|
||||
|
||||
Patch fields are intended for UI updates after checkout. The service commits
|
||||
all grants and payment changes together, and attempts rollback if a later write
|
||||
fails.
|
||||
|
||||
## Player Bank Checkout
|
||||
|
||||
```sqf
|
||||
private _item = createHashMapFromArray [
|
||||
["classname", "arifle_MX_F"],
|
||||
["category", "weapon"],
|
||||
["priceValue", 500],
|
||||
["quantity", 1]
|
||||
];
|
||||
|
||||
private _checkout = createHashMapFromArray [
|
||||
["requesterUid", getPlayerUID player],
|
||||
["requesterName", name player],
|
||||
["orgId", "default"],
|
||||
["requesterIsDefaultOrgCeo", false],
|
||||
["paymentMethod", "bank"],
|
||||
["items", [_item]],
|
||||
["vehicles", []]
|
||||
];
|
||||
|
||||
private _result = "forge_server" callExtension ["store:checkout", [toJSON _checkout]];
|
||||
```
|
||||
|
||||
## Organization Funds Checkout
|
||||
|
||||
When `paymentMethod` is `org_funds`, vehicles are also added to the
|
||||
organization fleet patch.
|
||||
|
||||
```sqf
|
||||
private _vehicle = createHashMapFromArray [
|
||||
["classname", "B_Quadbike_01_F"],
|
||||
["category", "cars"],
|
||||
["priceValue", 1500]
|
||||
];
|
||||
|
||||
private _checkout = createHashMapFromArray [
|
||||
["requesterUid", getPlayerUID player],
|
||||
["requesterName", name player],
|
||||
["orgId", _orgId],
|
||||
["requesterIsDefaultOrgCeo", false],
|
||||
["paymentMethod", "org_funds"],
|
||||
["items", []],
|
||||
["vehicles", [_vehicle]]
|
||||
];
|
||||
|
||||
private _result = "forge_server" callExtension ["store:checkout", [toJSON _checkout]];
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```sqf
|
||||
private _payload = _result select 0;
|
||||
if (_payload find "Error:" == 0) exitWith {
|
||||
hint format ["Checkout failed: %1", _payload];
|
||||
};
|
||||
|
||||
private _checkoutResult = fromJSON _payload;
|
||||
```
|
||||
119
docs/TASK_USAGE_GUIDE.md
Normal file
119
docs/TASK_USAGE_GUIDE.md
Normal file
@ -0,0 +1,119 @@
|
||||
# Task Usage Guide
|
||||
|
||||
The task module stores transient mission task metadata for active server or
|
||||
mission lifecycle workflows. SQF still owns Arma-only runtime state such as
|
||||
objects and participants.
|
||||
|
||||
## Data Model
|
||||
|
||||
Catalog entries are flexible JSON objects. The service normalizes these fields
|
||||
when a catalog entry is inserted or ownership changes:
|
||||
|
||||
- `taskId`
|
||||
- `taskID`
|
||||
- `accepted`
|
||||
- `requesterUid`
|
||||
- `orgID`
|
||||
|
||||
Ownership context:
|
||||
|
||||
```json
|
||||
{
|
||||
"requesterUid": "76561198000000000",
|
||||
"orgId": "default"
|
||||
}
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Arguments | Returns |
|
||||
| --- | --- | --- |
|
||||
| `task:reset` | none | `true`. |
|
||||
| `task:catalog:active` | none | Active catalog entry array JSON. |
|
||||
| `task:catalog:get` | `task_id` | Catalog entry JSON or `null`. |
|
||||
| `task:catalog:upsert` | `task_id`, `entry_json` | Stored catalog entry JSON. |
|
||||
| `task:catalog:delete` | `task_id` | `true`. |
|
||||
| `task:ownership:bind` | `task_id`, `ownership_json` | Ownership mutation result JSON. |
|
||||
| `task:ownership:release` | `task_id` | Ownership mutation result JSON. |
|
||||
| `task:ownership:accept` | `task_id`, `ownership_json` | Ownership mutation result JSON. |
|
||||
| `task:ownership:reward_context` | `task_id` | Reward context JSON. |
|
||||
| `task:status:set` | `task_id`, `status` | `true`. |
|
||||
| `task:status:get` | `task_id` | Status string JSON. |
|
||||
| `task:status:clear` | `task_id` | `true`. |
|
||||
| `task:defuse:increment` | `task_id` | New counter value JSON. |
|
||||
| `task:defuse:get` | `task_id` | Counter value JSON. |
|
||||
| `task:clear` | `task_id` | `true`. |
|
||||
|
||||
## Upsert a Catalog Entry
|
||||
|
||||
```sqf
|
||||
private _entry = createHashMapFromArray [
|
||||
["title", "Destroy Cache"],
|
||||
["description", "Destroy the enemy supply cache."],
|
||||
["reward", 1500]
|
||||
];
|
||||
|
||||
private _result = "forge_server" callExtension ["task:catalog:upsert", [
|
||||
"task-cache-1",
|
||||
toJSON _entry
|
||||
]];
|
||||
```
|
||||
|
||||
## Mark a Task Active
|
||||
|
||||
```sqf
|
||||
"forge_server" callExtension ["task:status:set", [
|
||||
"task-cache-1",
|
||||
"active"
|
||||
]];
|
||||
|
||||
private _active = "forge_server" callExtension ["task:catalog:active", []];
|
||||
```
|
||||
|
||||
Completed statuses `succeeded` and `failed` are also stored as completed status
|
||||
fallbacks. Clearing status removes active and completed state.
|
||||
|
||||
## Accept a Task
|
||||
|
||||
```sqf
|
||||
private _ownership = createHashMapFromArray [
|
||||
["requesterUid", getPlayerUID player],
|
||||
["orgId", "default"]
|
||||
];
|
||||
|
||||
private _result = "forge_server" callExtension ["task:ownership:accept", [
|
||||
"task-cache-1",
|
||||
toJSON _ownership
|
||||
]];
|
||||
```
|
||||
|
||||
`task:ownership:accept` fails if the task is not active or another requester
|
||||
already accepted it.
|
||||
|
||||
## Rewards
|
||||
|
||||
```sqf
|
||||
private _result = "forge_server" callExtension ["task:ownership:reward_context", [
|
||||
"task-cache-1"
|
||||
]];
|
||||
|
||||
private _context = fromJSON (_result select 0);
|
||||
```
|
||||
|
||||
The reward context contains `requesterUid` and `orgId`.
|
||||
|
||||
## Defuse Counter
|
||||
|
||||
```sqf
|
||||
"forge_server" callExtension ["task:defuse:increment", ["task-cache-1"]];
|
||||
private _count = "forge_server" callExtension ["task:defuse:get", ["task-cache-1"]];
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```sqf
|
||||
private _payload = _result select 0;
|
||||
if (_payload find "Error:" == 0) exitWith {
|
||||
systemChat format ["Task error: %1", _payload];
|
||||
};
|
||||
```
|
||||
Loading…
x
Reference in New Issue
Block a user