From 9d950db890649336fdd4c1046fdbe0d257a4cbbe Mon Sep 17 00:00:00 2001 From: Jacob Schmidt Date: Fri, 17 Apr 2026 17:55:38 -0500 Subject: [PATCH] Expand framework usage guides --- arma/server/docs/README.md | 1 + arma/server/docs/api-reference.md | 13 ++ docs/ACTOR_USAGE_GUIDE.md | 127 +++++++++++ docs/BANK_USAGE_GUIDE.md | 169 +++++++++++++++ docs/CAD_USAGE_GUIDE.md | 183 ++++++++++++++++ docs/GARAGE_USAGE_GUIDE.md | 338 ++++++++++++------------------ docs/LOCKER_USAGE_GUIDE.md | 307 +++++++++++++-------------- docs/MODULE_REFERENCE.md | 16 +- docs/ORG_USAGE_GUIDE.md | 232 ++++++++++++++++++++ docs/OWNED_STORAGE_USAGE_GUIDE.md | 158 ++++++++++++++ docs/PHONE_USAGE_GUIDE.md | 136 ++++++++++++ docs/README.md | 8 + docs/STORE_USAGE_GUIDE.md | 135 ++++++++++++ docs/TASK_USAGE_GUIDE.md | 119 +++++++++++ 14 files changed, 1574 insertions(+), 368 deletions(-) create mode 100644 docs/ACTOR_USAGE_GUIDE.md create mode 100644 docs/BANK_USAGE_GUIDE.md create mode 100644 docs/CAD_USAGE_GUIDE.md create mode 100644 docs/ORG_USAGE_GUIDE.md create mode 100644 docs/OWNED_STORAGE_USAGE_GUIDE.md create mode 100644 docs/PHONE_USAGE_GUIDE.md create mode 100644 docs/STORE_USAGE_GUIDE.md create mode 100644 docs/TASK_USAGE_GUIDE.md diff --git a/arma/server/docs/README.md b/arma/server/docs/README.md index 41a834e..c59146c 100644 --- a/arma/server/docs/README.md +++ b/arma/server/docs/README.md @@ -37,3 +37,4 @@ connect_timeout_ms = 5000 - [API Reference](./api-reference.md) - [Usage Examples](./usage-examples.md) +- [Framework Module Guides](../../../docs/README.md) diff --git a/arma/server/docs/api-reference.md b/arma/server/docs/api-reference.md index 7ad1f62..b4844fd 100644 --- a/arma/server/docs/api-reference.md +++ b/arma/server/docs/api-reference.md @@ -33,3 +33,16 @@ Game systems should call the domain APIs instead of raw database operations: Large request and response payloads are routed through the transport layer when needed by `forge_server_addons_extension_fnc_extCall`. + +## Module Guides + +- [Actor](../../../docs/ACTOR_USAGE_GUIDE.md) +- [Bank](../../../docs/BANK_USAGE_GUIDE.md) +- [CAD](../../../docs/CAD_USAGE_GUIDE.md) +- [Garage](../../../docs/GARAGE_USAGE_GUIDE.md) +- [Locker](../../../docs/LOCKER_USAGE_GUIDE.md) +- [Organization](../../../docs/ORG_USAGE_GUIDE.md) +- [Owned Storage](../../../docs/OWNED_STORAGE_USAGE_GUIDE.md) +- [Phone](../../../docs/PHONE_USAGE_GUIDE.md) +- [Store](../../../docs/STORE_USAGE_GUIDE.md) +- [Task](../../../docs/TASK_USAGE_GUIDE.md) diff --git a/docs/ACTOR_USAGE_GUIDE.md b/docs/ACTOR_USAGE_GUIDE.md new file mode 100644 index 0000000..40eacd4 --- /dev/null +++ b/docs/ACTOR_USAGE_GUIDE.md @@ -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; +``` diff --git a/docs/BANK_USAGE_GUIDE.md b/docs/BANK_USAGE_GUIDE.md new file mode 100644 index 0000000..05cd332 --- /dev/null +++ b/docs/BANK_USAGE_GUIDE.md @@ -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; +``` diff --git a/docs/CAD_USAGE_GUIDE.md b/docs/CAD_USAGE_GUIDE.md new file mode 100644 index 0000000..44d64d7 --- /dev/null +++ b/docs/CAD_USAGE_GUIDE.md @@ -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:` +- Requests: `cad-request:` +- 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]; +}; +``` diff --git a/docs/GARAGE_USAGE_GUIDE.md b/docs/GARAGE_USAGE_GUIDE.md index 2afc4a7..de9ef66 100644 --- a/docs/GARAGE_USAGE_GUIDE.md +++ b/docs/GARAGE_USAGE_GUIDE.md @@ -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: "`. 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. diff --git a/docs/LOCKER_USAGE_GUIDE.md b/docs/LOCKER_USAGE_GUIDE.md index a29d495..4424596 100644 --- a/docs/LOCKER_USAGE_GUIDE.md +++ b/docs/LOCKER_USAGE_GUIDE.md @@ -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: "`. +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. diff --git a/docs/MODULE_REFERENCE.md b/docs/MODULE_REFERENCE.md index ed9c76f..bba9161 100644 --- a/docs/MODULE_REFERENCE.md +++ b/docs/MODULE_REFERENCE.md @@ -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 diff --git a/docs/ORG_USAGE_GUIDE.md b/docs/ORG_USAGE_GUIDE.md new file mode 100644 index 0000000..26ab7f1 --- /dev/null +++ b/docs/ORG_USAGE_GUIDE.md @@ -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]; +}; +``` diff --git a/docs/OWNED_STORAGE_USAGE_GUIDE.md b/docs/OWNED_STORAGE_USAGE_GUIDE.md new file mode 100644 index 0000000..1cc5223 --- /dev/null +++ b/docs/OWNED_STORAGE_USAGE_GUIDE.md @@ -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]; +}; +``` diff --git a/docs/PHONE_USAGE_GUIDE.md b/docs/PHONE_USAGE_GUIDE.md new file mode 100644 index 0000000..6cd0829 --- /dev/null +++ b/docs/PHONE_USAGE_GUIDE.md @@ -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]; +}; +``` diff --git a/docs/README.md b/docs/README.md index 06637f1..641e9ba 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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 diff --git a/docs/STORE_USAGE_GUIDE.md b/docs/STORE_USAGE_GUIDE.md new file mode 100644 index 0000000..befa519 --- /dev/null +++ b/docs/STORE_USAGE_GUIDE.md @@ -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; +``` diff --git a/docs/TASK_USAGE_GUIDE.md b/docs/TASK_USAGE_GUIDE.md new file mode 100644 index 0000000..446a0e8 --- /dev/null +++ b/docs/TASK_USAGE_GUIDE.md @@ -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]; +}; +```