Expand framework usage guides

This commit is contained in:
Jacob Schmidt 2026-04-17 17:55:38 -05:00
parent cec65381dd
commit 9d950db890
14 changed files with 1574 additions and 368 deletions

View File

@ -37,3 +37,4 @@ connect_timeout_ms = 5000
- [API Reference](./api-reference.md)
- [Usage Examples](./usage-examples.md)
- [Framework Module Guides](../../../docs/README.md)

View File

@ -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
View 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
View 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
View 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];
};
```

View File

@ -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.

View File

@ -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.

View File

@ -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
View 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];
};
```

View 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
View 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];
};
```

View File

@ -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
View 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
View 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];
};
```