Compare commits

..

No commits in common. "b0b53eb28b3dc8333437f36c7a233b70e90d7060" and "9d17d6845396a060639191519ce65490f33047f2" have entirely different histories.

14 changed files with 368 additions and 1574 deletions

View File

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

View File

@ -33,16 +33,3 @@ 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)

View File

@ -1,127 +0,0 @@
# 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;
```

View File

@ -1,169 +0,0 @@
# 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;
```

View File

@ -1,183 +0,0 @@
# 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,212 +1,274 @@
# Garage Usage Guide
# Garage System Integration Guide
The garage module stores physical player vehicles. Each record keeps the
vehicle classname, generated plate UUID, fuel, overall damage, and detailed hit
point damage.
## Overview
## Storage Model
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.
Garage data is persisted through SurrealDB by the server extension.
## Data Storage
```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]
}
}
}
- 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)
```
Rules validated by the Rust service:
### Get 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.
Retrieves all vehicles in a player's garage.
```sqf
private _result = "forge_server" callExtension ["garage:get", [getPlayerUID player]];
private _payload = _result select 0;
if (_payload find "Error:" == 0) exitWith {
systemChat format ["Garage error: %1", _payload];
};
private _garage = fromJSON _payload;
private _garageMap = fromJSON (_result select 0);
// Returns: {"plate_uuid": {"plate":"plate_uuid","classname":"...","damage":0.0,"hit_points":{...}}, ...}
```
## Add a Vehicle
### Add Vehicle
`garage:add` requires `classname`, `fuel`, `damage`, and `hit_points`.
Adds a new vehicle to the garage. The system automatically generates a unique plate (UUID) for the vehicle.
```sqf
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 _data = createHashMapFromArray [
["classname", "B_Quadbike_01_F"],
["fuel", 1.0],
["damage", 0.0],
["hit_points", createHashMap] // Optional hit points map
];
private _result = "forge_server" callExtension ["garage:add", [
getPlayerUID player,
toJSON _vehicleData
toJSON _data
]];
private _payload = _result select 0;
if (_payload find "Error:" == 0) exitWith {
hint format ["Failed to store vehicle: %1", _payload];
};
private _garage = fromJSON _payload;
private _updatedGarage = fromJSON (_result select 0);
// Returns updated garage map
```
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.
### Update Garage (Sync)
Updates the entire garage state. Useful for syncing changes made locally.
```sqf
private _storedPlate = "";
{
private _vehicleRecord = _garage get _x;
if ((_vehicleRecord get "classname") == typeOf _vehicle) then {
_storedPlate = _x;
};
} forEach keys _garage;
// _garageMap is the local HashMap of vehicles
private _result = "forge_server" callExtension ["garage:update", [
getPlayerUID player,
toJSON _garageMap
]];
private _updatedGarage = fromJSON (_result select 0);
```
## Patch a Vehicle
### Patch Vehicle
`garage:patch` updates selected fields for one plate. The `plate` field is
required. `fuel`, `damage`, and `hit_points` are optional.
Updates specific fields of a vehicle without sending the entire garage. Useful for frequent updates like fuel or damage.
```sqf
private _patch = createHashMapFromArray [
["plate", _vehicle getVariable ["forge_garage_plate", ""]],
["fuel", fuel _vehicle],
["damage", damage _vehicle]
private _plate = "some-plate-uuid";
private _data = createHashMapFromArray [
["plate", _plate],
["fuel", 0.8],
["damage", 0.1],
// "hit_points" is optional
];
private _result = "forge_server" callExtension ["garage:patch", [
getPlayerUID player,
toJSON _patch
toJSON _data
]];
private _updatedGarage = fromJSON (_result select 0);
```
## Remove a Vehicle
### Remove Vehicle
`garage:remove` expects JSON with a `plate` field.
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.
```sqf
private _remove = createHashMapFromArray [
private _plate = "some-plate-uuid";
private _data = createHashMapFromArray [
["plate", _plate]
];
private _result = "forge_server" callExtension ["garage:remove", [
getPlayerUID player,
toJSON _remove
toJSON _data
]];
private _updatedGarage = fromJSON (_result select 0);
```
## Spawn a Stored Vehicle
### Delete Garage
Permanently deletes all vehicles from a player's garage.
```sqf
fnc_spawnGarageVehicle = {
params ["_plate"];
private _result = "forge_server" callExtension ["garage:delete", [getPlayerUID player]];
// Returns: "OK" or "Error: ..."
```
### 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 _payload = _result select 0;
private _garage = fromJSON (_result select 0);
if (_payload find "Error:" == 0) exitWith {
hint format ["Failed to load garage: %1", _payload];
// Validate index
if (_vehicleIndex >= count _garage) exitWith {
hint "Invalid vehicle index!";
objNull
};
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
};
// 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 _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];
// Spawn vehicle
private _spawnPos = player getPos [10, getDir player];
private _vehicle = _classname createVehicle _spawnPos;
private _hitPoints = _vehicleData getOrDefault ["hit_points", createHashMap];
private _names = _hitPoints getOrDefault ["names", []];
private _values = _hitPoints getOrDefault ["values", []];
// Apply damage
_vehicle setDamage _storedDamage;
// Apply hit point damage
private _names = _hitPoints get "names";
private _values = _hitPoints get "values";
{
_vehicle setHitPointDamage [_x, _values select _forEachIndex];
} forEach _names;
private _remove = createHashMapFromArray [["plate", _plate]];
"forge_server" callExtension ["garage:remove", [getPlayerUID player, toJSON _remove]];
// Store plate on vehicle for future updates
private _plate = _vehicleData get "plate";
_vehicle setVariable ["garagePlate", _plate];
// 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
};
```
## Hot State
## Hit Points Data Format
The `garage:hot:*` commands keep a runtime copy of a player's garage and write
it back only when `garage:hot:save` runs.
The hit points data matches Arma 3's `getAllHitPointsDamage` return format:
| 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`. |
```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, ...]
}
```
Use hot state for session-heavy vehicle workflows. Use the durable commands for
simple store/retrieve operations.
- **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;
};
```
## Best Practices
- 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. **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

View File

@ -1,203 +1,228 @@
# Locker Usage Guide
# Locker System Integration Guide
The locker module stores physical player inventory items by classname. It is
separate from the virtual arsenal unlock module documented in
[Owned Storage Usage Guide](./OWNED_STORAGE_USAGE_GUIDE.md).
## Overview
## Storage Model
The locker system provides persistent item storage for Arma 3 players. Each player can store multiple items (weapons, magazines, equipment) with quantity tracking.
Locker data is persisted through SurrealDB by the server extension.
## Data Storage
```json
{
"arifle_MX_F": {
"category": "weapon",
"classname": "arifle_MX_F",
"amount": 1
}
}
- 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
```
Rules validated by the Rust service:
### Get 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.
Retrieves all items in a player's locker.
```sqf
private _result = "forge_server" callExtension ["locker:get", [getPlayerUID player]];
private _payload = _result select 0;
if (_payload find "Error:" == 0) exitWith {
systemChat format ["Locker error: %1", _payload];
};
private _locker = fromJSON _payload;
private _lockerMap = fromJSON (_result select 0);
// Returns: {"arifle_MX_F": {"classname":"arifle_MX_F","category":"Weapon","amount":1}, ...}
```
## Add an Item
### Add Item
`locker:add` creates or overwrites one classname entry.
Adds a new item to the locker or updates the amount if it already exists.
**Checks capacity limit (25 items).**
```sqf
private _item = createHashMapFromArray [
["category", "weapon"],
private _data = createHashMapFromArray [
["classname", "arifle_MX_F"],
["category", "Weapon"],
["amount", 1]
];
private _result = "forge_server" callExtension ["locker:add", [
getPlayerUID player,
toJSON _item
toJSON _data
]];
private _payload = _result select 0;
if (_payload find "Error:" == 0) exitWith {
hint format ["Failed to store item: %1", _payload];
};
private _locker = fromJSON _payload;
private _updatedLocker = fromJSON (_result select 0);
// Returns updated locker map
```
## Patch an Amount
### Update Locker (Sync)
`locker:patch` currently patches the `amount` field for an existing classname.
Updates the entire locker state. Useful for syncing changes made locally (e.g., bulk moves).
**Replaces the entire locker content.**
```sqf
private _patch = createHashMapFromArray [
["classname", "arifle_MX_F"],
["amount", 5]
// _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 _result = "forge_server" callExtension ["locker:patch", [
getPlayerUID player,
toJSON _patch
toJSON _data
]];
private _updatedLocker = fromJSON (_result select 0);
```
## Remove an Item
### Remove Item
`locker:remove` takes the classname as the second argument.
Removes a specific item from the locker by classname.
```sqf
private _classname = "arifle_MX_F";
private _data = createHashMapFromArray [
["classname", _classname]
];
private _result = "forge_server" callExtension ["locker:remove", [
getPlayerUID player,
"arifle_MX_F"
toJSON _data
]];
private _updatedLocker = fromJSON (_result select 0);
```
## Retrieve an Item
### Delete Locker
Permanently deletes all items from a player's locker.
```sqf
fnc_retrieveLockerItem = {
params ["_classname"];
private _result = "forge_server" callExtension ["locker:delete", [getPlayerUID player]];
// Returns: "OK" or "Error: ..."
```
private _result = "forge_server" callExtension ["locker:get", [getPlayerUID player]];
private _payload = _result select 0;
### Check Existence
if (_payload find "Error:" == 0) exitWith {
hint format ["Failed to load locker: %1", _payload];
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];
false
};
private _locker = fromJSON _payload;
private _item = _locker getOrDefault [_classname, createHashMap];
if (_item isEqualTo createHashMap) exitWith {
hint "Item was not found in your locker.";
false
};
private _amount = _item getOrDefault ["amount", 0];
if (_amount <= 0) exitWith {
hint "Item is out of stock.";
false
};
if !(player canAdd _classname) exitWith {
hint "Not enough inventory space.";
false
};
player addItem _classname;
if (_amount > 1) then {
private _patch = createHashMapFromArray [
["classname", _classname],
["amount", _amount - 1]
];
"forge_server" callExtension ["locker:patch", [getPlayerUID player, toJSON _patch]];
} else {
"forge_server" callExtension ["locker:remove", [getPlayerUID player, _classname]];
};
// Remove weapon from player
player removeWeapon _weapon;
hint format ["Stored %1 in locker!", _weapon];
true
};
```
## Replace the Whole Locker
`locker:update` replaces the whole item map. Use it for explicit bulk syncs,
not single-item changes.
### Retrieving an Item
```sqf
private _items = createHashMapFromArray [
["arifle_MX_F", createHashMapFromArray [
["category", "weapon"],
["classname", "arifle_MX_F"],
["amount", 1]
]]
];
fnc_retrieveItem = {
params ["_classname"];
private _result = "forge_server" callExtension ["locker:update", [
getPlayerUID player,
toJSON _items
]];
// 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
};
};
```
## Hot State
## Error Handling
The `locker:hot:*` commands keep a runtime copy of a player's locker and write
it back only when `locker:hot:save` runs.
All commands return errors in the format `"Error: <message>"`.
| 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`. |
```sqf
private _result = "forge_server" callExtension ["locker:add", ...];
private _data = _result select 0;
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.
if (_data find "Error:" == 0) then {
systemChat format ["Locker error: %1", _data];
};
```

View File

@ -30,20 +30,8 @@ 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 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).
| 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:*` |
## Infrastructure Modules

View File

@ -1,232 +0,0 @@
# 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

@ -1,158 +0,0 @@
# 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];
};
```

View File

@ -1,136 +0,0 @@
# 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,16 +16,8 @@ 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

View File

@ -1,135 +0,0 @@
# 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;
```

View File

@ -1,119 +0,0 @@
# 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];
};
```