From 586c3dcde80283a216ab2779cab3640012d2e8b0 Mon Sep 17 00:00:00 2001 From: Jacob Schmidt Date: Sat, 18 Apr 2026 12:03:54 -0500 Subject: [PATCH 1/7] Add usage guides for Actor, Bank, Org, Phone, CAD, Task, and Owned Storage modules --- docs/MODULE_REFERENCE.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/MODULE_REFERENCE.md b/docs/MODULE_REFERENCE.md index 1b9ec1e..1557846 100644 --- a/docs/MODULE_REFERENCE.md +++ b/docs/MODULE_REFERENCE.md @@ -89,6 +89,8 @@ Nested groups use additional `:` separators, for example | `actor:delete` | Delete actor data. | | `actor:hot:init`, `actor:hot:get`, `actor:hot:keys`, `actor:hot:override`, `actor:hot:save`, `actor:hot:remove` | Manage actor hot state. | +See [Actor Usage Guide](./ACTOR_USAGE_GUIDE.md) for examples. + ### Bank | Command | Purpose | @@ -99,6 +101,8 @@ Nested groups use additional `:` separators, for example | `bank:hot:charge_checkout` | Charge a checkout against hot bank state. | | `bank:hot:validate_pin` | Validate a PIN for bank operations. | +See [Bank Usage Guide](./BANK_USAGE_GUIDE.md) for examples. + ### Garage | Command | Purpose | @@ -127,6 +131,8 @@ See [Locker Usage Guide](./LOCKER_USAGE_GUIDE.md) for examples. | `org:members:get`, `org:members:add`, `org:members:remove` | Manage organization membership. | | `org:hot:*` | Runtime organization workflows including registration, invites, credit lines, checkout charging, assets, fleet, leave, disband, save, and remove. | +See [Org Usage Guide](./ORG_USAGE_GUIDE.md) for examples. + ### Phone | Command | Purpose | @@ -137,6 +143,8 @@ See [Locker Usage Guide](./LOCKER_USAGE_GUIDE.md) for examples. | `phone:emails:list`, `phone:emails:send`, `phone:emails:mark_read`, `phone:emails:delete` | Manage emails. | | `phone:remove` | Remove phone state for a UID. | +See [Phone Usage Guide](./PHONE_USAGE_GUIDE.md) for examples. + ### CAD | Command Group | Purpose | @@ -149,6 +157,8 @@ See [Locker Usage Guide](./LOCKER_USAGE_GUIDE.md) for examples. | `cad:groups:build` | Build grouped CAD state. | | `cad:view:hydrate` | Build the dispatcher view model. | +See [CAD Usage Guide](./CAD_USAGE_GUIDE.md) for examples. + ### Task | Command Group | Purpose | @@ -160,6 +170,8 @@ See [Locker Usage Guide](./LOCKER_USAGE_GUIDE.md) for examples. | `task:defuse:increment`, `task:defuse:get` | Manage defuse counters. | | `task:clear` | Clear task state. | +See [Task Usage Guide](./TASK_USAGE_GUIDE.md) for examples. + ### Owned Storage | Command Group | Purpose | @@ -169,6 +181,8 @@ See [Locker Usage Guide](./LOCKER_USAGE_GUIDE.md) for examples. | `owned:locker:create`, `owned:locker:fetch`, `owned:locker:get`, `owned:locker:add`, `owned:locker:remove`, `owned:locker:delete`, `owned:locker:exists` | Owner-scoped item storage. | | `owned:locker:hot:*` | Owner-scoped item hot state. | +See [Owned Storage Usage Guide](./OWNED_STORAGE_USAGE_GUIDE.md) for examples. + ### Other Extension Groups | Command Group | Purpose | -- 2.47.2 From 81baca90d3c7ea0c27f0a29d80ed86be0f86bb9c Mon Sep 17 00:00:00 2001 From: Jacob Schmidt Date: Sat, 18 Apr 2026 12:11:16 -0500 Subject: [PATCH 2/7] Enhance task module documentation with new task creation methods and usage examples --- arma/server/addons/task/README.md | 58 +++++++++++++++++++++++++++++++ docs/TASK_USAGE_GUIDE.md | 39 +++++++++++++++++++++ 2 files changed, 97 insertions(+) diff --git a/arma/server/addons/task/README.md b/arma/server/addons/task/README.md index 038449d..a989515 100644 --- a/arma/server/addons/task/README.md +++ b/arma/server/addons/task/README.md @@ -63,6 +63,64 @@ Task time limits use `0` for no limit on attack, destroy, delivery, hostage, and HVT tasks. Defuse IED timers are different: each IED must have a positive countdown value. +Mission designers can create tasks in four ways: + +- Eden modules for editor-authored tasks. +- `fnc_startTask.sqf` for script-authored tasks. +- `fnc_handler.sqf` for pre-registered entities with reputation gating and + ownership binding. +- Direct task function calls for server-owned or mission-authored flows that + intentionally fall back to the `default` org. + +The dynamic mission manager can also generate attack tasks from config. That is +system-generated content rather than a hand-authored task creation path. + +### Create With Eden Modules +Eden task modules are the normal designer-facing path. Place the module, +configure its attributes, and sync it to the relevant entities or grouping +modules. + +Available task modules: +- `FORGE_Module_Attack`: sync directly to target units or vehicles +- `FORGE_Module_Destroy`: sync directly to objects, vehicles, or units +- `FORGE_Module_Defuse`: sync to `FORGE_Module_Explosives` and optionally + `FORGE_Module_Protected` +- `FORGE_Module_Delivery`: sync to `FORGE_Module_Cargo`; the cargo module syncs + to cargo objects +- `FORGE_Module_Hostage`: sync to `FORGE_Module_Hostages` and + `FORGE_Module_Shooters` +- `FORGE_Module_HVT`: sync directly to HVT units +- `FORGE_Module_Defend`: configure the defense marker and wave settings + +These modules delegate to `fnc_startTask.sqf`. + +### Start Through `fnc_startTask.sqf` +Use `fnc_startTask.sqf` for script-authored tasks. It registers task entities, +creates the BIS task, stores the catalog entry, and dispatches through +`fnc_handler.sqf`. + +```sqf +[ + "attack", + "compound_attack_01", + getPosATL leader1, + "Attack: East Compound", + "Eliminate all hostile forces.", + createHashMapFromArray [["targets", [unit1, unit2, unit3]]], + createHashMapFromArray [ + ["limitFail", 0], + ["limitSuccess", 3], + ["funds", 50000], + ["ratingFail", -10], + ["ratingSuccess", 20], + ["timeLimit", 900] + ], + 0, + getPlayerUID player, + "script" +] call forge_server_task_fnc_startTask; +``` + ### Start Through The Handler Use the handler when you want reputation gating and task ownership binding. diff --git a/docs/TASK_USAGE_GUIDE.md b/docs/TASK_USAGE_GUIDE.md index 6003e55..60507dd 100644 --- a/docs/TASK_USAGE_GUIDE.md +++ b/docs/TASK_USAGE_GUIDE.md @@ -128,6 +128,41 @@ The task addon provides these server-owned task flows: - `hostage` - `hvt` +Mission designers can create tasks in four ways: + +- Eden modules for editor-authored tasks. +- `forge_server_task_fnc_startTask` for script-authored tasks. +- `forge_server_task_fnc_handler` for pre-registered entities with reputation + gating and ownership binding. +- Direct task function calls for server-owned or mission-authored flows that + intentionally fall back to the `default` org. + +The dynamic mission manager can also generate attack tasks from config. That is +system-generated content rather than a hand-authored task creation path. + +## Eden Modules + +Eden task modules are the normal designer-facing path. Place the module, +configure its attributes, and sync it to the relevant entities or grouping +modules. + +Available task modules: + +- `FORGE_Module_Attack`: sync directly to target units or vehicles. +- `FORGE_Module_Destroy`: sync directly to objects, vehicles, or units. +- `FORGE_Module_Defuse`: sync to `FORGE_Module_Explosives` and optionally + `FORGE_Module_Protected`. +- `FORGE_Module_Delivery`: sync to `FORGE_Module_Cargo`; the cargo module syncs + to cargo objects. +- `FORGE_Module_Hostage`: sync to `FORGE_Module_Hostages` and + `FORGE_Module_Shooters`. +- `FORGE_Module_HVT`: sync directly to HVT units. +- `FORGE_Module_Defend`: configure the defense marker and wave settings. + +These modules delegate to `forge_server_task_fnc_startTask`. + +## Scripted Start Task + Use `forge_server_task_fnc_startTask` when creating tasks from modules, mission scripts, or generated mission-manager content. It registers task entities, creates the BIS task, stores the catalog entry, then dispatches @@ -155,6 +190,8 @@ through `forge_server_task_fnc_handler`. ] call forge_server_task_fnc_startTask; ``` +## Handler Calls + Use `forge_server_task_fnc_handler` directly when the task entities are already registered and you want reputation gating plus ownership binding: @@ -167,6 +204,8 @@ registered and you want reputation gating plus ownership binding: ] call forge_server_task_fnc_handler; ``` +## Direct Task Calls + Direct task function calls still work for mission-authored or server-owned tasks, but they do not provide a requester UID. Ownership falls back to the `default` org. -- 2.47.2 From 0cfaec86d011b89eba58ebeb2ff8a1dd9a07d001 Mon Sep 17 00:00:00 2001 From: Jacob Schmidt Date: Sat, 18 Apr 2026 12:15:24 -0500 Subject: [PATCH 3/7] Add CAD compatibility section to task module documentation --- arma/server/addons/task/README.md | 19 +++++++++++++++++++ docs/TASK_USAGE_GUIDE.md | 23 +++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/arma/server/addons/task/README.md b/arma/server/addons/task/README.md index a989515..178b82c 100644 --- a/arma/server/addons/task/README.md +++ b/arma/server/addons/task/README.md @@ -75,6 +75,25 @@ Mission designers can create tasks in four ways: The dynamic mission manager can also generate attack tasks from config. That is system-generated content rather than a hand-authored task creation path. +### CAD Compatibility +CAD hydrates assignable tasks from `TaskStore.getActiveTaskCatalog`. A task must +have a catalog entry and active task status before CAD can show and assign it. + +CAD-compatible creation paths: +- Eden modules: compatible because they delegate to `fnc_startTask.sqf` +- `fnc_startTask.sqf`: compatible because it registers the catalog entry, + creates the BIS task, and dispatches through `fnc_handler.sqf` +- dynamic mission manager attack tasks: compatible because the mission manager + uses `fnc_startTask.sqf` + +Limited or incompatible paths: +- `fnc_handler.sqf`: only compatible if a catalog entry was already registered + elsewhere. The handler sets active status and ownership, but it does not + create the BIS task or upsert the catalog entry +- direct task function calls: not CAD-compatible by default. They bypass + `fnc_startTask.sqf` and usually do not register the task catalog entry or + active status that CAD hydrates from + ### Create With Eden Modules Eden task modules are the normal designer-facing path. Place the module, configure its attributes, and sync it to the relevant entities or grouping diff --git a/docs/TASK_USAGE_GUIDE.md b/docs/TASK_USAGE_GUIDE.md index 60507dd..05bc470 100644 --- a/docs/TASK_USAGE_GUIDE.md +++ b/docs/TASK_USAGE_GUIDE.md @@ -140,6 +140,29 @@ Mission designers can create tasks in four ways: The dynamic mission manager can also generate attack tasks from config. That is system-generated content rather than a hand-authored task creation path. +## CAD Compatibility + +CAD hydrates assignable tasks from `TaskStore.getActiveTaskCatalog`. A task must +have a catalog entry and active task status before CAD can show and assign it. + +CAD-compatible creation paths: + +- Eden modules: compatible because they delegate to + `forge_server_task_fnc_startTask`. +- `forge_server_task_fnc_startTask`: compatible because it registers the + catalog entry, creates the BIS task, and dispatches through the handler. +- Dynamic mission manager attack tasks: compatible because the mission manager + uses `forge_server_task_fnc_startTask`. + +Limited or incompatible paths: + +- `forge_server_task_fnc_handler`: only compatible if a catalog entry was + already registered elsewhere. The handler sets active status and ownership, + but it does not create the BIS task or upsert the catalog entry. +- Direct task function calls: not CAD-compatible by default. They bypass + `startTask` and usually do not register the task catalog entry or active + status that CAD hydrates from. + ## Eden Modules Eden task modules are the normal designer-facing path. Place the module, -- 2.47.2 From b8fef9be984e639cca48c3a0c3c2e1e1c9c98a5f Mon Sep 17 00:00:00 2001 From: Jacob Schmidt Date: Sat, 18 Apr 2026 12:32:50 -0500 Subject: [PATCH 4/7] Enhance task module documentation with CAD compatibility details and BIS task prerequisites --- arma/server/addons/task/README.md | 29 ++++++++++++++++++++++---- docs/TASK_USAGE_GUIDE.md | 34 +++++++++++++++++++++++++------ 2 files changed, 53 insertions(+), 10 deletions(-) diff --git a/arma/server/addons/task/README.md b/arma/server/addons/task/README.md index 178b82c..785002c 100644 --- a/arma/server/addons/task/README.md +++ b/arma/server/addons/task/README.md @@ -68,9 +68,11 @@ Mission designers can create tasks in four ways: - Eden modules for editor-authored tasks. - `fnc_startTask.sqf` for script-authored tasks. - `fnc_handler.sqf` for pre-registered entities with reputation gating and - ownership binding. + ownership binding. This path expects the BIS task and catalog entry to + already exist if map-task and CAD visibility are required. - Direct task function calls for server-owned or mission-authored flows that - intentionally fall back to the `default` org. + intentionally fall back to the `default` org. This path expects the BIS task + to already exist if map-task visibility is required. The dynamic mission manager can also generate attack tasks from config. That is system-generated content rather than a hand-authored task creation path. @@ -89,10 +91,26 @@ CAD-compatible creation paths: Limited or incompatible paths: - `fnc_handler.sqf`: only compatible if a catalog entry was already registered elsewhere. The handler sets active status and ownership, but it does not - create the BIS task or upsert the catalog entry + create the BIS task shown in the map task tab or upsert the catalog entry - direct task function calls: not CAD-compatible by default. They bypass `fnc_startTask.sqf` and usually do not register the task catalog entry or - active status that CAD hydrates from + active status that CAD hydrates from. They also only call + `BIS_fnc_taskSetState` at completion/failure; they do not create the BIS task + first + +### BIS Map Task Prerequisite +Only the Eden task modules and `fnc_startTask.sqf` create the BIS task +automatically through `BIS_fnc_taskCreate`. + +If a mission uses `fnc_handler.sqf` directly or calls a task flow function such +as `forge_server_task_fnc_attack`, the mission must create a BIS task with the +same task ID before the Forge task completes. Otherwise the success/failure +`BIS_fnc_taskSetState` call has no visible map task to update. + +That prerequisite can be satisfied with a vanilla Eden task creation module or +a scripted `BIS_fnc_taskCreate` call. `fnc_startTask.sqf` is the preferred Forge +path because it handles BIS task creation, Forge catalog registration, entity +registration, and handler dispatch together. ### Create With Eden Modules Eden task modules are the normal designer-facing path. Place the module, @@ -142,6 +160,8 @@ creates the BIS task, stores the catalog entry, and dispatches through ### Start Through The Handler Use the handler when you want reputation gating and task ownership binding. +Create the BIS task and catalog entry separately if this task should appear in +the map task tab or CAD. ```sqf ["attack", ["task_attack_1", 1, 2, 1500000, -75, 375, false, false], 250, getPlayerUID player] call forge_server_task_fnc_handler; @@ -156,6 +176,7 @@ Arguments: ### Start Task Functions Directly Direct task calls still work, but they do not provide a requester UID. That means task ownership falls back to the `default` org. +Create the BIS task separately if this task should appear in the map task tab. Use direct starts only when that behavior is intended, such as: - mission-authored tasks diff --git a/docs/TASK_USAGE_GUIDE.md b/docs/TASK_USAGE_GUIDE.md index 05bc470..0639dbb 100644 --- a/docs/TASK_USAGE_GUIDE.md +++ b/docs/TASK_USAGE_GUIDE.md @@ -133,9 +133,11 @@ Mission designers can create tasks in four ways: - Eden modules for editor-authored tasks. - `forge_server_task_fnc_startTask` for script-authored tasks. - `forge_server_task_fnc_handler` for pre-registered entities with reputation - gating and ownership binding. + gating and ownership binding. This path expects the BIS task and catalog + entry to already exist if map-task and CAD visibility are required. - Direct task function calls for server-owned or mission-authored flows that - intentionally fall back to the `default` org. + intentionally fall back to the `default` org. This path expects the BIS task + to already exist if map-task visibility is required. The dynamic mission manager can also generate attack tasks from config. That is system-generated content rather than a hand-authored task creation path. @@ -158,10 +160,27 @@ Limited or incompatible paths: - `forge_server_task_fnc_handler`: only compatible if a catalog entry was already registered elsewhere. The handler sets active status and ownership, - but it does not create the BIS task or upsert the catalog entry. + but it does not create the BIS task shown in the map task tab or upsert the + catalog entry. - Direct task function calls: not CAD-compatible by default. They bypass `startTask` and usually do not register the task catalog entry or active - status that CAD hydrates from. + status that CAD hydrates from. They also only call `BIS_fnc_taskSetState` at + completion/failure; they do not create the BIS task first. + +## BIS Map Task Prerequisite + +Only the Eden task modules and `forge_server_task_fnc_startTask` create the BIS +task automatically through `BIS_fnc_taskCreate`. + +If a mission uses `forge_server_task_fnc_handler` directly or calls a task flow +function such as `forge_server_task_fnc_attack`, the mission must create a BIS +task with the same task ID before the Forge task completes. Otherwise the +success/failure `BIS_fnc_taskSetState` call has no visible map task to update. + +That prerequisite can be satisfied with a vanilla Eden task creation module or +a scripted `BIS_fnc_taskCreate` call. `forge_server_task_fnc_startTask` is the +preferred Forge path because it handles BIS task creation, Forge catalog +registration, entity registration, and handler dispatch together. ## Eden Modules @@ -216,7 +235,9 @@ through `forge_server_task_fnc_handler`. ## Handler Calls Use `forge_server_task_fnc_handler` directly when the task entities are already -registered and you want reputation gating plus ownership binding: +registered and you want reputation gating plus ownership binding. Create the +BIS task and catalog entry separately if this task should appear in the map +task tab or CAD: ```sqf [ @@ -231,7 +252,8 @@ registered and you want reputation gating plus ownership binding: Direct task function calls still work for mission-authored or server-owned tasks, but they do not provide a requester UID. Ownership falls back to the -`default` org. +`default` org. Create the BIS task separately if this task should appear in the +map task tab. ## Timer Semantics -- 2.47.2 From 89169f1e846ceec407e0ba58961600443750f4b2 Mon Sep 17 00:00:00 2001 From: Jacob Schmidt Date: Sat, 18 Apr 2026 13:04:01 -0500 Subject: [PATCH 5/7] Update documentation and add new client usage guides - Revised README.md for the Forge Client Example Addon to clarify its purpose as a template. - Enhanced MODULE_REFERENCE.md to categorize guides into Server and Extension guides and Client guides. - Created detailed usage guides for various client addons including Actor, Bank, CAD, Garage, Locker, Notifications, Organization, Phone, and Store. - Added a Client Common Usage Guide to outline shared browser UI bridge patterns. - Introduced a Client Main Usage Guide to define the foundational elements for client addons. - Established authoritative state notes and usage rules across new guides to ensure clarity on server ownership and client responsibilities. --- arma/client/README.md | 56 +++++---- arma/client/addons/actor/README.md | 29 ++++- arma/client/addons/bank/README.md | 35 +++++- arma/client/addons/cad/README.md | 37 ++++++ arma/client/addons/common/README.md | 19 +++- arma/client/addons/garage/README.md | 36 +++++- arma/client/addons/locker/README.md | 28 ++++- arma/client/addons/main/README.md | 19 +++- arma/client/addons/notifications/README.md | 28 ++++- arma/client/addons/org/README.md | 108 +++++------------- arma/client/addons/phone/README.md | 31 ++++- arma/client/addons/store/README.md | 29 ++++- arma/client/docs/README.md | 91 +++++++-------- arma/client/extra/example_addon/README.md | 10 +- docs/CLIENT_ACTOR_USAGE_GUIDE.md | 98 ++++++++++++++++ docs/CLIENT_BANK_USAGE_GUIDE.md | 84 ++++++++++++++ docs/CLIENT_CAD_USAGE_GUIDE.md | 100 +++++++++++++++++ docs/CLIENT_COMMON_USAGE_GUIDE.md | 92 +++++++++++++++ docs/CLIENT_GARAGE_USAGE_GUIDE.md | 80 +++++++++++++ docs/CLIENT_LOCKER_USAGE_GUIDE.md | 87 ++++++++++++++ docs/CLIENT_MAIN_USAGE_GUIDE.md | 48 ++++++++ docs/CLIENT_NOTIFICATIONS_USAGE_GUIDE.md | 74 ++++++++++++ docs/CLIENT_ORG_USAGE_GUIDE.md | 106 +++++++++++++++++ docs/CLIENT_PHONE_USAGE_GUIDE.md | 107 ++++++++++++++++++ docs/CLIENT_STORE_USAGE_GUIDE.md | 92 +++++++++++++++ docs/CLIENT_USAGE_GUIDE.md | 125 +++++++++++++++++++++ docs/MODULE_REFERENCE.md | 16 ++- docs/README.md | 17 ++- 28 files changed, 1509 insertions(+), 173 deletions(-) create mode 100644 arma/client/addons/cad/README.md create mode 100644 docs/CLIENT_ACTOR_USAGE_GUIDE.md create mode 100644 docs/CLIENT_BANK_USAGE_GUIDE.md create mode 100644 docs/CLIENT_CAD_USAGE_GUIDE.md create mode 100644 docs/CLIENT_COMMON_USAGE_GUIDE.md create mode 100644 docs/CLIENT_GARAGE_USAGE_GUIDE.md create mode 100644 docs/CLIENT_LOCKER_USAGE_GUIDE.md create mode 100644 docs/CLIENT_MAIN_USAGE_GUIDE.md create mode 100644 docs/CLIENT_NOTIFICATIONS_USAGE_GUIDE.md create mode 100644 docs/CLIENT_ORG_USAGE_GUIDE.md create mode 100644 docs/CLIENT_PHONE_USAGE_GUIDE.md create mode 100644 docs/CLIENT_STORE_USAGE_GUIDE.md create mode 100644 docs/CLIENT_USAGE_GUIDE.md diff --git a/arma/client/README.md b/arma/client/README.md index 7d8932d..42176c4 100644 --- a/arma/client/README.md +++ b/arma/client/README.md @@ -1,30 +1,46 @@ -

Forge Client

-

- Version - Issues - - License -
- HEMTT - CBA A3 -

+# Forge Client -

- Requires the latest version of CBA A3 -

+Forge Client contains the Arma client-side addons for Forge. It owns player UI, +browser bridges, client repositories, local event handling, and client-to-server +CBA RPC requests. -**Forge Client** aims to... +The client mod pairs with `arma/server`: client addons collect player input and +render state, while server addons and the Rust extension own authoritative +state and persistence. -The project is entirely **open-source** and any contributions are welcome. +## Requirements +- CBA A3 +- ACE3 for features that use ACE interactions, arsenal, spectator, or medical + integrations +- Forge Server running the matching server-side addons -## Core Features +## Addons +- `main`: shared client mod config and macros +- `common`: shared browser UI bridge helpers +- `actor`: player interaction menu and actor repository +- `bank`: banking UI and account request bridge +- `cad`: map/CAD UI for dispatch, groups, tasks, and support requests +- `garage`: vehicle storage and virtual garage UI +- `locker`: locker and virtual arsenal repositories +- `notifications`: notification HUD and sounds +- `org`: organization portal UI +- `phone`: phone, contacts, messages, and email UI +- `store`: storefront catalog and checkout UI -- Feature +## UI Pattern +Most feature UIs use an Arma display with a `CT_WEBBROWSER` control. JavaScript +sends JSON events through A3API, SQF handles them in `fnc_handleUIEvents.sqf`, +and response events are sent back into the browser with `ctrlWebBrowserAction +["ExecJS", ...]`. -## Contributing +Client repositories cache the most recent state for display only. Server addons +and the extension remain authoritative. -For new contributers, see the [Contributing Setup & Guidelines](./.github/CONTRIBUTING.md). +## Documentation +- [Root client usage guide](../../docs/CLIENT_USAGE_GUIDE.md) +- [Client docs](./docs/README.md) +- [Common web UI framework notes](./addons/common/WEB_UI_FRAMEWORK.md) +- [CAD map integration notes](./addons/cad/MAP_README.md) ## License - Forge Client is licensed under [APL-SA](./LICENSE.md). diff --git a/arma/client/addons/actor/README.md b/arma/client/addons/actor/README.md index e83012b..2684cf8 100644 --- a/arma/client/addons/actor/README.md +++ b/arma/client/addons/actor/README.md @@ -1,3 +1,28 @@ -# forge_client_actor +# Forge Client Actor -Description for this addon +## Overview +The actor addon owns the player interaction menu and client-side actor +repository. It initializes actor state from the server, tracks client-visible +actor fields, and routes menu actions to other Forge UIs. + +## Dependencies +- `forge_client_main` +- server actor events from `forge_server_actor` +- runtime integrations with bank, CAD, garage, org, phone, store, locker, and + notifications addons + +## Main Components +- `fnc_initRepository.sqf` manages client actor state and server init/save + requests. +- `fnc_openUI.sqf` opens `RscActorMenu`. +- `fnc_handleUIEvents.sqf` handles browser menu actions. + +## Event Surface +The actor menu can open bank, ATM mode, CAD, garage, virtual garage, org, phone, +store, and ACE arsenal interactions. Client post-init also wires player killed +and respawn handlers into the server economy flow. + +## Runtime Notes +Actor state is loaded before dependent systems initialize. When the server sends +actor sync data, the repository updates local view state and clears the loading +screen. diff --git a/arma/client/addons/bank/README.md b/arma/client/addons/bank/README.md index a4b1503..bb1bf03 100644 --- a/arma/client/addons/bank/README.md +++ b/arma/client/addons/bank/README.md @@ -1,3 +1,34 @@ -# forge_client_bank +# Forge Client Bank -Description for this addon +## Overview +The bank addon provides the client banking UI and browser bridge for account +hydrate, deposits, withdrawals, transfers, PIN entry, earnings deposits, and +credit-line repayment. + +## Dependencies +- `forge_client_common` +- `forge_client_main` +- server bank events from `forge_server_bank` +- notifications for server-driven messages + +## Main Components +- `fnc_initRepository.sqf` tracks account load state. +- `fnc_initUIBridge.sqf` translates browser requests into server RPCs and sends + server responses back to the browser. +- `fnc_handleUIEvents.sqf` handles `bank::*` browser events. +- `fnc_openUI.sqf` opens `RscBank`; ATM mode is supported by passing `true`. + +## Browser Events +- `bank::ready` +- `bank::refresh` +- `bank::deposit::request` +- `bank::withdraw::request` +- `bank::transfer::request` +- `bank::depositEarnings::request` +- `bank::repayCreditLine::request` +- `bank::pin::request` +- `bank::close` + +## Runtime Notes +The client only displays and requests account changes. The server bank addon and +extension own validation, balances, authorization, and persistence. diff --git a/arma/client/addons/cad/README.md b/arma/client/addons/cad/README.md new file mode 100644 index 0000000..5f5d438 --- /dev/null +++ b/arma/client/addons/cad/README.md @@ -0,0 +1,37 @@ +# Forge Client CAD + +## Overview +The CAD addon provides the client map and dispatch interface for task +assignment, dispatch orders, support requests, group status, group roles, and +task acknowledge/decline actions. + +## Dependencies +- `forge_client_main` +- server CAD events from `forge_server_cad` +- server task catalog data exposed through CAD hydrate payloads + +## Main Components +- `fnc_initRepository.sqf` caches hydrated CAD view state. +- `fnc_initUI.sqf` wires the native map, top bar, bottom bar, side panel, and + dispatcher browser controls. +- `fnc_initUIBridge.sqf` sends browser actions to server CAD RPCs and pushes + state back to the UI. +- `fnc_handleUIEvents.sqf` handles `cad::*` browser events. +- `fnc_openUI.sqf` opens the CAD display. + +## Supported Actions +- hydrate CAD state +- assign active tasks to groups +- create and close dispatch orders +- submit and close support requests +- acknowledge or decline assigned tasks +- update group status, role, and profile +- focus map requests and toggle panels + +## Notes +CAD task visibility depends on server-side task catalog entries. Tasks created +through Forge task modules or `forge_server_task_fnc_startTask` are the normal +CAD-compatible task sources. + +See [MAP_README.md](./MAP_README.md) for details on the integrated native map +and browser layout. diff --git a/arma/client/addons/common/README.md b/arma/client/addons/common/README.md index 05fa06e..71c1190 100644 --- a/arma/client/addons/common/README.md +++ b/arma/client/addons/common/README.md @@ -1,5 +1,18 @@ -# forge_client_common +# Forge Client Common -Common functionality shared between addons. +## Overview +The common addon contains shared client-side UI bridge helpers and common +configuration used by browser-based feature addons. -See [WEB_UI_FRAMEWORK.md](./WEB_UI_FRAMEWORK.md) for the proposed shared `CT_WEBBROWSER` UI framework layout and API. +## Dependencies +- `forge_client_main` + +## Main Components +- `fnc_initWebUIBridge.sqf` provides shared bridge behavior for web browser UI + controls. +- `WEB_UI_FRAMEWORK.md` documents the proposed shared browser runtime and event + API for Forge web UIs. + +## Notes +Keep feature-specific behavior in the owning addon. Common should hold reusable +browser bridge patterns, not copied application logic. diff --git a/arma/client/addons/garage/README.md b/arma/client/addons/garage/README.md index 0442fa7..879df40 100644 --- a/arma/client/addons/garage/README.md +++ b/arma/client/addons/garage/README.md @@ -1,3 +1,35 @@ -# forge_client_garage +# Forge Client Garage -Description for this addon +## Overview +The garage addon provides player vehicle storage UI, vehicle store/retrieve +actions, and virtual garage state on the client. + +## Dependencies +- `forge_client_common` +- `forge_client_main` +- server garage events from `forge_server_garage` +- notifications for action feedback + +## Main Components +- `fnc_initRepository.sqf` manages player garage view state. +- `fnc_initVGRepository.sqf` manages virtual garage view state. +- `fnc_initHelperService.sqf` resolves vehicle names, hit points, and payload + details. +- `fnc_initContextService.sqf` gathers nearby/current vehicle context. +- `fnc_initPayloadService.sqf` builds browser hydrate payloads. +- `fnc_initActionService.sqf` sends store/retrieve requests and handles action + responses. +- `fnc_initUIBridge.sqf` pushes hydrate/sync events to the browser. +- `fnc_openUI.sqf` opens `RscGarage`. +- `fnc_openVG.sqf` opens the Arma garage-style virtual garage view. + +## Browser Events +- `garage::ready` +- `garage::refresh` +- `garage::vehicle::retrieve::request` +- `garage::vehicle::store::request` +- `garage::close` + +## Runtime Notes +The client builds vehicle context and sends requests. The server garage addon +and extension own stored vehicle state. diff --git a/arma/client/addons/locker/README.md b/arma/client/addons/locker/README.md index ccd333d..b5530cf 100644 --- a/arma/client/addons/locker/README.md +++ b/arma/client/addons/locker/README.md @@ -1,3 +1,27 @@ -# forge_client_locker +# Forge Client Locker -Description for this addon +## Overview +The locker addon manages client repositories for personal locker state and +virtual arsenal unlock state. It also integrates with ACE Arsenal display +behavior. + +## Dependencies +- `forge_client_main` +- ACE Arsenal +- server locker events from `forge_server_locker` + +## Main Components +- `fnc_initRepository.sqf` manages locker state, container open/close behavior, + and server sync requests. +- `fnc_initVARepository.sqf` manages virtual arsenal state. + +## Runtime Behavior +- Requests locker and virtual arsenal state after actor load. +- Syncs server responses into client repositories. +- Sends locker override data to the server when a managed locker container is + closed. +- Hides selected ACE Arsenal controls when the arsenal display opens. + +## Notes +The client repository is display/input state. The server locker addon and +extension own saved locker and virtual arsenal data. diff --git a/arma/client/addons/main/README.md b/arma/client/addons/main/README.md index 0f8cf55..52bdcff 100644 --- a/arma/client/addons/main/README.md +++ b/arma/client/addons/main/README.md @@ -1,3 +1,18 @@ -# forge_client_main +# Forge Client Main -Main Addon for forge-client +## Overview +The main addon provides shared mod metadata, macros, settings, and compile +infrastructure for Forge client addons. + +## Dependencies +- `cba_main` + +## Main Components +- `script_macros.hpp` defines shared function, RPC, path, variable, and compile + macros. +- `script_mod.hpp` and `script_version.hpp` define mod identity and version. +- `CfgSettings.hpp` contains client-side CBA settings. + +## Notes +Feature logic should live in the owning addon. Main is the shared foundation for +configuration, macros, and mod-level metadata. diff --git a/arma/client/addons/notifications/README.md b/arma/client/addons/notifications/README.md index ea21f89..5c3fe16 100644 --- a/arma/client/addons/notifications/README.md +++ b/arma/client/addons/notifications/README.md @@ -1,3 +1,27 @@ -# forge_client_notifications +# Forge Client Notifications -Description for this addon +## Overview +The notifications addon owns the client notification HUD, notification sound, +and local notification service used by other Forge client and server modules. + +## Dependencies +- `forge_client_main` + +## Main Components +- `fnc_initService.sqf` manages queued and visible notifications. +- `fnc_openUI.sqf` opens the notification HUD display. +- `fnc_handleUIEvents.sqf` handles browser/HUD events. +- `CfgSounds.hpp` defines the notification sound. + +## Event Surface +`forge_client_notifications_recieveNotification` accepts: + +```sqf +[_type, _title, _content, _duration] +``` + +The event plays the configured sound and adds the notification to the HUD. + +## Runtime Notes +The HUD opens after the virtual arsenal repository is loaded. Other addons +should use this notification event instead of creating their own transient UI. diff --git a/arma/client/addons/org/README.md b/arma/client/addons/org/README.md index a464e97..f1df4f2 100644 --- a/arma/client/addons/org/README.md +++ b/arma/client/addons/org/README.md @@ -1,85 +1,33 @@ -# forge_client_org +# Forge Client Organization -Player organization UI and client integration. +## Overview +The organization addon provides the client organization portal UI and bridge for +organization hydrate, registration, membership, invitations, credit lines, +leave/disband actions, assets, fleet, and treasury display. -## UI Login Contract +## Dependencies +- `forge_client_common` +- `forge_client_main` +- server organization events from `forge_server_org` +- notifications for user feedback -The web UI sends the following request through `A3API.SendAlert`: +## Main Components +- `fnc_initRepository.sqf` caches organization portal state. +- `fnc_initUIBridge.sqf` sends browser requests to server org RPCs and pushes + hydrate/sync events back to the browser. +- `fnc_handleUIEvents.sqf` handles `org::*` browser events. +- `fnc_openUI.sqf` opens `RscOrg`. -```json -{ - "event": "org::login::request", - "data": { - "email": "admin@spearnet.mil", - "password": "secret" - } -} -``` +## Browser Events +- `org::login::request` +- `org::create::request` +- `org::disband::request` +- `org::leave::request` +- `org::credit::request` +- `org::invite::request` +- `org::invite::accept` +- `org::invite::decline` -On success, SQF should call the browser bridge with: - -```sqf -private _payload = createHashMapFromArray [ - ["session", createHashMapFromArray [ - ["actorName", name player], - ["role", "Leader"] - ]], - ["portalData", createHashMapFromArray [ - ["org", createHashMapFromArray [ - ["name", "Black Rifle Company"], - ["tag", "BRC-0160566824"], - ["type", "Private Military Company"], - ["status", "Operational"], - ["headquarters", "Georgetown Command Annex"], - ["owner", "Jacob Schmidt"] - ]], - ["funds", 482750], - ["reputation", 72], - ["members", [ - createHashMapFromArray [["name", "Jacob Schmidt"]], - createHashMapFromArray [["name", "Mara Velez"]] - ]], - ["fleet", [ - createHashMapFromArray [ - ["name", "UH-80 Ghost Hawk"], - ["type", "helicopter"], - ["status", "Ready"], - ["damage", "16%"] - ] - ]], - ["assets", [ - createHashMapFromArray [ - ["name", "First Aid Kits"], - ["type", "items"], - ["quantity", "36"] - ] - ]], - ["activity", []], - ["roadmap", []] - ]] -]; - -_control ctrlWebBrowserAction [ - "ExecJS", - format ["OrgUIBridge.receiveLoginSuccess(%1)", toJSON _payload] -]; -``` - -On failure: - -```sqf -private _payload = createHashMapFromArray [ - ["message", "Invalid credentials."] -]; - -_control ctrlWebBrowserAction [ - "ExecJS", - format ["OrgUIBridge.receiveLoginFailure(%1)", toJSON _payload] -]; -``` - -Current implementation: - -- `fnc_handleUIEvents.sqf` now handles `org::login::request` -- success hydrates the portal with `session` + `portalData` -- failure returns a single `message` string for inline UI feedback +## Runtime Notes +The client portal is a view/controller. Organization state, funds, reputation, +credit lines, assets, fleet, and membership are authoritative on the server. diff --git a/arma/client/addons/phone/README.md b/arma/client/addons/phone/README.md index 756d2e4..d719a9e 100644 --- a/arma/client/addons/phone/README.md +++ b/arma/client/addons/phone/README.md @@ -1,4 +1,29 @@ -forge_client_phone -=================== +# Forge Client Phone -This addon provides the phone user interface and functionality for the in-game phone system. It handles all phone-related features including the UI display, interactions, and core phone operations. \ No newline at end of file +## Overview +The phone addon provides the in-game phone UI for contacts, SMS messages, and +email. It keeps a local `PhoneClass` facade for view state and sends all +authoritative operations to the server phone addon. + +## Dependencies +- `forge_client_main` +- server phone events from `forge_server_phone` +- notifications for contact/message/email feedback + +## Main Components +- `fnc_initClass.sqf` initializes the local phone facade. +- `fnc_handleUIEvents.sqf` translates browser events into server phone RPCs. +- `fnc_openUI.sqf` opens `RscPhone`. +- `ui/_site` contains the browser phone UI source. + +## Supported Operations +- initialize and sync phone state +- refresh contacts +- add/remove contacts by UID, phone number, or email +- send, read, and delete SMS messages +- send, read, and delete email +- push incoming message/email updates into the browser UI + +## Runtime Notes +Phone data is owned by the server extension. Client state is only used to render +the phone UI and provide immediate feedback. diff --git a/arma/client/addons/store/README.md b/arma/client/addons/store/README.md index 8a76830..f2693ad 100644 --- a/arma/client/addons/store/README.md +++ b/arma/client/addons/store/README.md @@ -1,3 +1,28 @@ -# forge_client_store +# Forge Client Store -Description for this addon +## Overview +The store addon provides the client storefront UI for catalog browsing, +category loading, payment-source display, cart handling, and checkout requests. + +## Dependencies +- `forge_client_common` +- `forge_client_main` +- server store events from `forge_server_store` +- bank/org/locker/garage server state through checkout results + +## Main Components +- `fnc_initUIBridge.sqf` handles browser readiness, category requests, checkout + requests, and server responses. +- `fnc_handleUIEvents.sqf` handles `store::*` browser events. +- `fnc_openUI.sqf` opens `RscStore`. + +## Browser Events +- `store::ready` +- `store::category::request` +- `store::checkout::request` +- `store::close` + +## Runtime Notes +The client never calculates authoritative checkout results. The server store +addon and extension validate prices, charge payment sources, grant assets, and +return patches for the UI. diff --git a/arma/client/docs/README.md b/arma/client/docs/README.md index c41b1e5..1fc922c 100644 --- a/arma/client/docs/README.md +++ b/arma/client/docs/README.md @@ -1,54 +1,47 @@ - +# Forge Client Documentation -

forge-client

-

- - forge-client Version - - - forge-client Issues - - - forge-client Downloads - - - forge-client License - -
- HEMTT -

+This folder documents the Arma client mod. The client side is responsible for +displaying UI, handling player input, caching client-visible state, and sending +CBA events to server addons. -

- Requires the latest version of CBA A3 -

+Authoritative gameplay state lives on the server side or in the Rust extension. +Client repositories should be treated as view state, not durable storage. -# Initial Project Setup! +## Architecture +- Each addon declares its own UI resources and CBA extended event handlers. +- `XEH_preStart.sqf`/`XEH_preInit.sqf` compile functions. +- `XEH_postInitClient.sqf` initializes client repositories, UI bridges, and + response event handlers. +- Browser UIs send JSON events through A3API. +- SQF handlers translate browser events into local actions or server RPCs. +- Server responses update repositories and push browser events back into the UI. -Delete this section after the project has been initially set up: +## Addon Docs +- [Main](../addons/main/README.md) +- [Common](../addons/common/README.md) +- [Actor](../addons/actor/README.md) +- [Bank](../addons/bank/README.md) +- [CAD](../addons/cad/README.md) +- [Garage](../addons/garage/README.md) +- [Locker](../addons/locker/README.md) +- [Notifications](../addons/notifications/README.md) +- [Organization](../addons/org/README.md) +- [Phone](../addons/phone/README.md) +- [Store](../addons/store/README.md) -1. Find and replace all instances of `forge-client` with the mod's name. -2. Find and replace all instances of `MOD_REPO` with the mod's name _and no spaces_. - - This should be the name of the repository on GitHub. -3. Find and replace all instances of `forge_client` with the mod's prefix. - - This should be all lowercase. -4. Find and replace all instances of `MOD_ACRONYM` with the mod's acronym. - - This should be all uppercase. -5. After the initial Steam upload, find and replace all instances of `MOD_ID` with the mod's Steam Workshop id. - -For third parties, make sure to also replace `IDSolutions` with your Github username / organization name, and to replace `DartRuffian` with your username. - -**forge-client** (MOD_ACRONYM) aims to... - -The project is entirely **open-source** and any contributions are welcome. - -## Core Features - -- Feature - -## Contributing - -For new contributers, see the [Contributing Setup & Guidelines](./.github/CONTRIBUTING.md). - -## License - -forge-client is licensed under [APL-ND](./LICENSE.md). +## Related Docs +- [Root Client Usage Guide](../../../docs/CLIENT_USAGE_GUIDE.md) +- [Root Client Main Usage Guide](../../../docs/CLIENT_MAIN_USAGE_GUIDE.md) +- [Root Client Common Usage Guide](../../../docs/CLIENT_COMMON_USAGE_GUIDE.md) +- [Root Client Actor Usage Guide](../../../docs/CLIENT_ACTOR_USAGE_GUIDE.md) +- [Root Client Bank Usage Guide](../../../docs/CLIENT_BANK_USAGE_GUIDE.md) +- [Root Client CAD Usage Guide](../../../docs/CLIENT_CAD_USAGE_GUIDE.md) +- [Root Client Garage Usage Guide](../../../docs/CLIENT_GARAGE_USAGE_GUIDE.md) +- [Root Client Locker Usage Guide](../../../docs/CLIENT_LOCKER_USAGE_GUIDE.md) +- [Root Client Notifications Usage Guide](../../../docs/CLIENT_NOTIFICATIONS_USAGE_GUIDE.md) +- [Root Client Organization Usage Guide](../../../docs/CLIENT_ORG_USAGE_GUIDE.md) +- [Root Client Phone Usage Guide](../../../docs/CLIENT_PHONE_USAGE_GUIDE.md) +- [Root Client Store Usage Guide](../../../docs/CLIENT_STORE_USAGE_GUIDE.md) +- [Shared web UI framework notes](../addons/common/WEB_UI_FRAMEWORK.md) +- [CAD map integration notes](../addons/cad/MAP_README.md) +- [Root framework docs](../../../docs/README.md) diff --git a/arma/client/extra/example_addon/README.md b/arma/client/extra/example_addon/README.md index 40e0345..8448397 100644 --- a/arma/client/extra/example_addon/README.md +++ b/arma/client/extra/example_addon/README.md @@ -1,3 +1,9 @@ -# forge_client_addonName +# Forge Client Example Addon -Description for this addon +This directory is a template for creating a new Forge client addon. + +Use it as a starting point for addon structure, config layout, event handler +files, and function preparation. Replace the component names, display strings, +and placeholder implementation with the new addon's real feature behavior. + +Do not ship this example addon as a gameplay module. diff --git a/docs/CLIENT_ACTOR_USAGE_GUIDE.md b/docs/CLIENT_ACTOR_USAGE_GUIDE.md new file mode 100644 index 0000000..bd514dd --- /dev/null +++ b/docs/CLIENT_ACTOR_USAGE_GUIDE.md @@ -0,0 +1,98 @@ +# Client Actor Usage Guide + +The client actor addon owns the player interaction menu and client-side actor +repository. It is the main launcher for nearby player actions and other Forge +client UIs. + +## Open the Actor Menu + +```sqf +call forge_client_actor_fnc_openUI; +``` + +The actor menu opens `RscActorMenu`, loads `ui/_site/index.html`, and routes +browser alerts through `forge_client_actor_fnc_handleUIEvents`. + +## Repository + +`forge_client_actor_fnc_initRepository` creates `GVAR(ActorRepository)`. + +The repository: + +- requests actor initialization from the server +- saves actor state through the server actor addon +- caches client-visible actor fields +- applies position, direction, stance, rank, and loadout on JIP sync when the + relevant settings allow it +- provides nearby interaction actions to the browser UI + +Initialize actor state through the repository: + +```sqf +GVAR(ActorRepository) call ["init", []]; +``` + +Save actor state through the server: + +```sqf +GVAR(ActorRepository) call ["save", [true]]; +``` + +## Nearby Actions + +The menu asks for nearby actions with: + +```text +actor::get::actions +``` + +The repository scans objects within 5 meters and returns actions based on +mission object variables: + +| Variable | Action | +| --- | --- | +| `storeType` | store | +| `isAtm` | ATM | +| `isBank` | bank | +| `isGarage` | garage | +| `garageType` | garage subtype | +| `isLocker` | virtual arsenal action when VA is enabled | +| `deviceType` | device action placeholder | +| nearby player unit | player interaction placeholder | + +The response is pushed into the browser with `updateAvailableActions(...)`. + +## Browser Events + +| Event | Client behavior | +| --- | --- | +| `actor::get::actions` | Refresh nearby actions. | +| `actor::close::menu` | Close actor menu. | +| `actor::open::atm` | Open bank UI in ATM mode. | +| `actor::open::bank` | Open bank UI in bank mode. | +| `actor::open::cad` | Open CAD UI. | +| `actor::open::garage` | Open garage UI. | +| `actor::open::vgarage` | Open virtual garage. | +| `actor::open::org` | Open organization UI. | +| `actor::open::vlocker` | Open ACE arsenal on `FORGE_Locker_Box`. | +| `actor::open::phone` | Open phone UI. | +| `actor::open::store` | Open store UI. | + +Device and player interaction events currently display placeholder feedback. + +## Authoritative State + +Actor persistence is server-owned. The client repository requests and displays +actor data, but actor creation, durable updates, and hot-state behavior are +handled by the server actor addon and extension. + +## Related Guides + +- [Actor Usage Guide](./ACTOR_USAGE_GUIDE.md) +- [Client Bank Usage Guide](./CLIENT_BANK_USAGE_GUIDE.md) +- [Client CAD Usage Guide](./CLIENT_CAD_USAGE_GUIDE.md) +- [Client Garage Usage Guide](./CLIENT_GARAGE_USAGE_GUIDE.md) +- [Client Locker Usage Guide](./CLIENT_LOCKER_USAGE_GUIDE.md) +- [Client Organization Usage Guide](./CLIENT_ORG_USAGE_GUIDE.md) +- [Client Phone Usage Guide](./CLIENT_PHONE_USAGE_GUIDE.md) +- [Client Store Usage Guide](./CLIENT_STORE_USAGE_GUIDE.md) diff --git a/docs/CLIENT_BANK_USAGE_GUIDE.md b/docs/CLIENT_BANK_USAGE_GUIDE.md new file mode 100644 index 0000000..4390f55 --- /dev/null +++ b/docs/CLIENT_BANK_USAGE_GUIDE.md @@ -0,0 +1,84 @@ +# Client Bank Usage Guide + +The client bank addon opens the bank and ATM browser UI, forwards banking +requests to the server bank addon, and pushes account updates back into the +browser. + +## Open Bank UI + +Open full bank mode: + +```sqf +call forge_client_bank_fnc_openUI; +``` + +Open ATM mode: + +```sqf +[true] call forge_client_bank_fnc_openUI; +``` + +The open function creates `RscBank`, sets the bridge mode to `bank` or `atm`, +loads `ui/_site/index.html`, and routes browser events through +`forge_client_bank_fnc_handleUIEvents`. + +## Bridge and Repository + +`forge_client_bank_fnc_initRepository` tracks account load and cached account +state. + +`forge_client_bank_fnc_initUIBridge` owns: + +- active browser control tracking +- bank/ATM mode +- browser ready handling +- account hydrate and sync responses +- deposit, withdrawal, transfer, earnings deposit, credit repayment, and PIN + requests +- browser notice delivery + +## Browser Events + +| Event | Client behavior | +| --- | --- | +| `bank::ready` | Mark browser ready and request hydrate from the server. | +| `bank::refresh` | Request fresh bank hydrate data. | +| `bank::deposit::request` | Forward deposit amount to the server. | +| `bank::withdraw::request` | Forward withdrawal amount to the server. | +| `bank::transfer::request` | Forward target, source field, and amount. | +| `bank::depositEarnings::request` | Request earnings deposit. | +| `bank::repayCreditLine::request` | Request credit-line repayment. | +| `bank::pin::request` | Forward PIN validation request. | +| `bank::close` | Dispose bridge screen state and close the display. | + +## Browser Response Events + +The bridge sends: + +| Event | Purpose | +| --- | --- | +| `bank::hydrate` | Full session/account payload. | +| `bank::sync` | Account patch or sync data. | +| `bank::notice` | UI-visible notice payload. | + +## Request Flow + +Example deposit flow: + +1. Browser sends `bank::deposit::request` with an `amount`. +2. Client bridge calls the server bank request event. +3. Server bank addon validates the request and calls bank hot-state logic. +4. Server response is caught by the client post-init event handlers. +5. Client bridge sends `bank::sync` or `bank::notice` back to the browser. + +## Authoritative State + +Balances, PIN authorization, transfers, checkout charges, credit lines, and +persistence are server-owned. The client should only display account data and +request mutations through server events. + +## Related Guides + +- [Bank Usage Guide](./BANK_USAGE_GUIDE.md) +- [Client Common Usage Guide](./CLIENT_COMMON_USAGE_GUIDE.md) +- [Client Store Usage Guide](./CLIENT_STORE_USAGE_GUIDE.md) diff --git a/docs/CLIENT_CAD_USAGE_GUIDE.md b/docs/CLIENT_CAD_USAGE_GUIDE.md new file mode 100644 index 0000000..654f23b --- /dev/null +++ b/docs/CLIENT_CAD_USAGE_GUIDE.md @@ -0,0 +1,100 @@ +# Client CAD Usage Guide + +The client CAD addon provides the map and dispatch UI for groups, active +tasks, task assignment, dispatch orders, support requests, and task +acknowledge/decline workflows. + +## Open CAD UI + +```sqf +call forge_client_cad_fnc_openUI; +``` + +The CAD UI opens `RscMapUI` and loads separate browser controls for: + +- top bar +- bottom bar +- side panel +- dispatcher board + +The native Arma map remains part of the same display. + +## Repository and Bridge + +`forge_client_cad_fnc_initRepository` caches the hydrated CAD payload, +selected mode, dispatch view, session data, groups, tasks, requests, and +assignments. + +`forge_client_cad_fnc_initUIBridge` owns: + +- ready state for side panel, top bar, and dispatcher board +- operations vs dispatch mode +- board vs map dispatch view +- hydrate requests +- task assignment, acknowledge, and decline requests +- dispatch order create/close requests +- support request submit/close requests +- group status, role, and profile requests +- map focus actions + +## Browser Events + +| Event | Client behavior | +| --- | --- | +| `cad::topbar::ready` | Mark top bar ready and push top bar state. | +| `cad::ready` | Mark side panel ready and request hydrate. | +| `cad::dispatcher::ready` | Mark dispatcher board ready and push hydrate data. | +| `cad::mode::set` | Switch between operations and dispatch mode. | +| `cad::dispatchView::set` | Switch dispatch board/map view. | +| `cad::refresh` | Request fresh CAD hydrate data. | +| `cad::tasks::assign` | Assign a task to a group. | +| `cad::tasks::acknowledge` | Acknowledge assigned task. | +| `cad::tasks::decline` | Decline assigned task. | +| `cad::dispatchOrder::create` | Create dispatch order. | +| `cad::dispatchOrder::close` | Close dispatch order. | +| `cad::supportRequest::submit` | Submit support request. | +| `cad::supportRequest::close` | Close support request. | +| `cad::groups::status` | Update group status. | +| `cad::groups::role` | Update group role. | +| `cad::groups::profile` | Update status and role together. | +| `cad::groups::focus` | Center map on a group. | +| `cad::tasks::focus` | Center map on a task. | +| `cad::requests::focus` | Center map on a support request. | +| `map::zoomIn` | Zoom native map in. | +| `map::zoomOut` | Zoom native map out. | +| `map::search` | Placeholder status update. | +| `map::close` | Dispose bridge state and close the display. | + +## Response Events + +The bridge pushes: + +| Event | Purpose | +| --- | --- | +| `cad::hydrate` | Full hydrated CAD payload to the side panel. | +| `cad::assignment::response` | Task assignment/acknowledge/decline result. | +| `cad::group::response` | Group status/role/profile result. | +| `cad::request::response` | Support request result. | + +Dispatcher board controls also receive direct `ExecJS` status and hydrate +calls. + +## Task Compatibility + +CAD task visibility depends on server-side task catalog entries. Tasks created +through Eden Forge task modules or `forge_server_task_fnc_startTask` are the +normal CAD-compatible task sources because they register task catalog data. + +Direct handler or task-function calls only work with CAD when the task catalog +entry already exists. + +## Authorization Notes + +Only dispatcher sessions can enter dispatch mode. If the hydrated session is +not a dispatcher, the bridge forces the UI back to operations mode. + +## Related Guides + +- [CAD Usage Guide](./CAD_USAGE_GUIDE.md) +- [Task Usage Guide](./TASK_USAGE_GUIDE.md) +- [Client Common Usage Guide](./CLIENT_COMMON_USAGE_GUIDE.md) diff --git a/docs/CLIENT_COMMON_USAGE_GUIDE.md b/docs/CLIENT_COMMON_USAGE_GUIDE.md new file mode 100644 index 0000000..754d6c8 --- /dev/null +++ b/docs/CLIENT_COMMON_USAGE_GUIDE.md @@ -0,0 +1,92 @@ +# Client Common Usage Guide + +The client `common` addon contains shared browser UI bridge declarations and +common client-side browser integration patterns. + +## Purpose + +Use `forge_client_common` when a browser-backed feature UI needs reusable +screen lifecycle behavior: + +- active browser control tracking +- browser ready state +- pending event queues +- `ExecJS` payload delivery +- shared bridge object inheritance through `createHashMapObject` + +Feature addons still own their app-specific events and server RPC mapping. + +## Shared Bridge + +Initialize the bridge declarations with: + +```sqf +private _webUIDeclarations = call forge_client_common_fnc_initWebUIBridge; +private _bridgeDeclaration = _webUIDeclarations get "bridgeDeclaration"; +``` + +Feature bridges can inherit from the shared declaration: + +```sqf +GVAR(MyUIBridgeBaseClass) = compileFinal createHashMapFromArray [ + ["#base", _bridgeDeclaration], + ["#type", "MyUIBridgeBaseClass"], + ["handleReady", compileFinal { + params [["_control", controlNull, [controlNull]]]; + + _self call ["setActiveBrowserControl", [_control]]; + _self call ["sendEvent", ["myAddon::hydrate", createHashMap, _control]]; + }] +]; +``` + +## Event Delivery + +`sendEvent` builds this payload: + +```json +{ + "event": "myAddon::event", + "data": {} +} +``` + +If the browser control is missing or not ready, the payload is queued on the +screen object. When the screen marks ready, `flushPendingEvents` delivers the +queue. + +## Screen Lifecycle + +The shared screen object tracks: + +| Field | Purpose | +| --- | --- | +| `control` | Active browser control. | +| `readyState` | Whether the browser app has sent its ready event. | +| `pendingEvents` | Outbound events waiting for a ready browser. | + +Call `handleClose` or `dispose` when a display closes so stale controls and +queued events are cleared. + +## Current Consumers + +The common bridge pattern is used by the newer bank, CAD, garage, and +organization client bridges. Store currently keeps its own bridge object and +browser bridge function names. + +## Usage Rules + +- Keep bridge inheritance in feature addons thin and explicit. +- Keep shared code generic; do not add bank, CAD, org, or store-specific logic + to `common`. +- Prefer namespaced events such as `garage::sync`. +- Send hash maps or arrays that can be safely serialized with `toJSON`. +- Avoid direct extension calls from the client bridge; send CBA server events. + +## Related Guides + +- [Client Usage Guide](./CLIENT_USAGE_GUIDE.md) +- [Client Bank Usage Guide](./CLIENT_BANK_USAGE_GUIDE.md) +- [Client CAD Usage Guide](./CLIENT_CAD_USAGE_GUIDE.md) +- [Client Garage Usage Guide](./CLIENT_GARAGE_USAGE_GUIDE.md) +- [Client Organization Usage Guide](./CLIENT_ORG_USAGE_GUIDE.md) diff --git a/docs/CLIENT_GARAGE_USAGE_GUIDE.md b/docs/CLIENT_GARAGE_USAGE_GUIDE.md new file mode 100644 index 0000000..991c34e --- /dev/null +++ b/docs/CLIENT_GARAGE_USAGE_GUIDE.md @@ -0,0 +1,80 @@ +# Client Garage Usage Guide + +The client garage addon provides player vehicle storage UI, vehicle +store/retrieve actions, vehicle context building, and the virtual garage view. + +## Open Garage UI + +```sqf +call forge_client_garage_fnc_openUI; +``` + +The garage UI opens `RscGarage`, loads `ui/_site/index.html`, and routes +browser events through `forge_client_garage_fnc_handleUIEvents`. + +## Open Virtual Garage + +```sqf +call forge_client_garage_fnc_openVG; +``` + +The virtual garage uses mission-configured `FORGE_CfgGarages` locations to set +the spawn/preview position, opens the BIS garage interface, and restricts the +available vehicle lists from the virtual garage repository. + +## Client Services + +| Service | Purpose | +| --- | --- | +| `GarageRepository` | Player garage view state. | +| `VGRepository` | Virtual garage unlock view state. | +| `GarageHelperService` | Vehicle names, hit points, and payload helpers. | +| `GarageContextService` | Nearby/current vehicle context. | +| `GaragePayloadService` | Browser hydrate payload construction. | +| `GarageActionService` | Store/retrieve request handling. | +| `GarageUIBridge` | Browser ready, hydrate, and sync delivery. | + +## Browser Events + +| Event | Client behavior | +| --- | --- | +| `garage::ready` | Mark browser ready and send `garage::hydrate`. | +| `garage::refresh` | Send current garage payload as `garage::sync`. | +| `garage::vehicle::retrieve::request` | Forward retrieve request through the action service. | +| `garage::vehicle::store::request` | Forward store request through the action service. | +| `garage::close` | Dispose bridge screen state and close the display. | + +## Browser Response Events + +| Event | Purpose | +| --- | --- | +| `garage::hydrate` | Initial vehicle and session payload. | +| `garage::sync` | Refreshed vehicle payload. | + +Server action responses are handled by the action service and notification +flow. + +## Mission Setup + +Garage interactions are normally surfaced through the actor menu when nearby +objects have garage variables such as: + +```sqf +_object setVariable ["isGarage", true, true]; +_object setVariable ["garageType", "cars", true]; +``` + +Virtual garage access also requires configured garage locations in mission +config so the preview/spawn position can be resolved. + +## Authoritative State + +The client gathers vehicle context and sends store/retrieve requests. Stored +vehicle state, validation, spawning, removal, and persistence are owned by the +server garage addon and extension. + +## Related Guides + +- [Garage Usage Guide](./GARAGE_USAGE_GUIDE.md) +- [Client Actor Usage Guide](./CLIENT_ACTOR_USAGE_GUIDE.md) +- [Client Notifications Usage Guide](./CLIENT_NOTIFICATIONS_USAGE_GUIDE.md) diff --git a/docs/CLIENT_LOCKER_USAGE_GUIDE.md b/docs/CLIENT_LOCKER_USAGE_GUIDE.md new file mode 100644 index 0000000..dc8d724 --- /dev/null +++ b/docs/CLIENT_LOCKER_USAGE_GUIDE.md @@ -0,0 +1,87 @@ +# Client Locker Usage Guide + +The client locker addon manages personal locker display state, local locker +container behavior, and virtual arsenal unlock state. + +## Repositories + +`forge_client_locker_fnc_initRepository` creates `GVAR(LockerRepository)`. + +`forge_client_locker_fnc_initVARepository` creates `GVAR(VARepository)`. + +Initialize locker state: + +```sqf +GVAR(LockerRepository) call ["init", []]; +GVAR(VARepository) call ["init", []]; +``` + +## Locker Container Flow + +The repository searches mission namespace variables whose names contain +`locker` and refer to objects. For each server/mission locker object, it creates +a local `Box_NATO_Equip_F` at the same position and attaches container event +handlers. + +On container open: + +- the local container is cleared +- cached locker items are inserted into the container +- over-capacity warnings are emitted when the item count is above 25 + +On container close: + +- cargo, nested container items, and weapon attachments are read back +- the new locker map is sent to the server with the override request +- the local repository cache is updated + +## Virtual Arsenal Flow + +The virtual arsenal repository creates a local `FORGE_Locker_Box` and requests +virtual arsenal unlocks from the server. + +As sync data arrives, it applies unlocks through ACE Arsenal: + +| Data key | Client behavior | +| --- | --- | +| `items` | Add virtual items. | +| `weapons` | Add virtual weapons. | +| `magazines` | Add virtual magazines. | +| `backpacks` | Add virtual backpacks. | + +The actor menu opens the virtual locker with: + +```sqf +[FORGE_Locker_Box, player, false] spawn ace_arsenal_fnc_openBox; +``` + +## Server Events + +The client repository sends requests for: + +- locker initialization +- locker save +- locker override after container close +- virtual arsenal initialization +- virtual arsenal save + +The server locker addon and extension own the saved locker and virtual arsenal +state. + +## Mission Setup + +Mission locker objects must be placed into `missionNamespace` with a variable +name containing `locker`. The client creates local interactive containers from +those authoritative mission objects. + +Example: + +```sqf +missionNamespace setVariable ["forge_locker_alpha", _lockerObject, true]; +``` + +## Related Guides + +- [Locker Usage Guide](./LOCKER_USAGE_GUIDE.md) +- [Owned Storage Usage Guide](./OWNED_STORAGE_USAGE_GUIDE.md) +- [Client Actor Usage Guide](./CLIENT_ACTOR_USAGE_GUIDE.md) diff --git a/docs/CLIENT_MAIN_USAGE_GUIDE.md b/docs/CLIENT_MAIN_USAGE_GUIDE.md new file mode 100644 index 0000000..0ac8fbf --- /dev/null +++ b/docs/CLIENT_MAIN_USAGE_GUIDE.md @@ -0,0 +1,48 @@ +# Client Main Usage Guide + +The client `main` addon provides the shared mod identity, version metadata, +CBA settings, and macro foundation used by the Forge client addons. + +## Purpose + +Use `forge_client_main` as the foundation dependency for client addons that +need Forge macros, function naming, settings, or mod-level configuration. + +Feature logic should stay in the owning addon. `main` should remain limited to +shared client configuration and compile infrastructure. + +## Key Files + +| File | Purpose | +| --- | --- | +| `script_mod.hpp` | Client mod identity. | +| `script_version.hpp` | Client mod version values. | +| `script_macros.hpp` | Shared client macros. | +| `CfgSettings.hpp` | Client CBA settings. | +| `config.cpp` | Addon config and mod wiring. | + +## Dependency Pattern + +Feature addons normally depend on `forge_client_main` in their `config.cpp`. + +```cpp +class forge_client_example { + requiredAddons[] = { + "forge_client_main" + }; +}; +``` + +## Usage Notes + +- Put domain UI, repositories, and event handling in feature addons. +- Put reusable browser bridge behavior in `forge_client_common`. +- Put server-only behavior in `arma/server/addons`. +- Keep settings in `CfgSettings.hpp` when they apply to the client mod as a + whole or to a client feature toggle. + +## Related Guides + +- [Client Usage Guide](./CLIENT_USAGE_GUIDE.md) +- [Client Common Usage Guide](./CLIENT_COMMON_USAGE_GUIDE.md) +- [Development Guide](./DEVELOPMENT_GUIDE.md) diff --git a/docs/CLIENT_NOTIFICATIONS_USAGE_GUIDE.md b/docs/CLIENT_NOTIFICATIONS_USAGE_GUIDE.md new file mode 100644 index 0000000..e102583 --- /dev/null +++ b/docs/CLIENT_NOTIFICATIONS_USAGE_GUIDE.md @@ -0,0 +1,74 @@ +# Client Notifications Usage Guide + +The client notifications addon owns the notification HUD, notification sound, +and local notification service used by Forge client and server modules. + +## Runtime Behavior + +The notification display is created during client initialization. The browser +HUD sends: + +```text +notifications::ready +``` + +When that event is received, `NotificationService` initializes and sends a +startup notification. + +## Create a Notification + +Use the notification service when available: + +```sqf +GVAR(NotificationService) call ["create", [ + "success", + "Title", + "Notification text.", + 4000 +]]; +``` + +Arguments: + +| Argument | Purpose | +| --- | --- | +| `_type` | Notification type, such as `success`, `info`, `warning`, or `error`. | +| `_title` | Notification title. | +| `_content` | Notification body text. | +| `_duration` | Display duration in milliseconds. | + +The service dispatches a browser `forge:notify` custom event. + +## CBA Event Surface + +Other addons can use the client notification event: + +```sqf +["forge_client_notifications_recieveNotification", [ + "warning", + "Garage", + "Vehicle spawn position is blocked.", + 3000 +]] call CBA_fnc_localEvent; +``` + +The event payload is: + +```sqf +[_type, _title, _content, _duration] +``` + +## Usage Rules + +- Use the shared notification service instead of opening separate transient + browser UIs. +- Keep server-driven player feedback short and actionable. +- Treat notification state as transient client UI state. +- Do not use notifications as the only record of durable domain changes. + +## Related Guides + +- [Client Usage Guide](./CLIENT_USAGE_GUIDE.md) +- [Client Garage Usage Guide](./CLIENT_GARAGE_USAGE_GUIDE.md) +- [Client Bank Usage Guide](./CLIENT_BANK_USAGE_GUIDE.md) +- [Client Store Usage Guide](./CLIENT_STORE_USAGE_GUIDE.md) diff --git a/docs/CLIENT_ORG_USAGE_GUIDE.md b/docs/CLIENT_ORG_USAGE_GUIDE.md new file mode 100644 index 0000000..5a5723b --- /dev/null +++ b/docs/CLIENT_ORG_USAGE_GUIDE.md @@ -0,0 +1,106 @@ +# Client Organization Usage Guide + +The client organization addon provides the organization portal UI and browser +bridge for login, registration, membership, invites, credit lines, leave and +disband flows, assets, fleet, and treasury display. + +## Open Organization UI + +```sqf +call forge_client_org_fnc_openUI; +``` + +The UI opens `RscOrg`, loads `ui/_site/index.html`, and routes browser alerts +through `forge_client_org_fnc_handleUIEvents`. + +## Repository and Bridge + +`forge_client_org_fnc_initRepository` caches organization portal state. + +`forge_client_org_fnc_initUIBridge` owns: + +- active browser control tracking +- portal hydrate requests +- create/login response routing +- leave and disband requests +- credit-line assignment requests +- invite, accept invite, and decline invite requests +- targeted browser response events + +## Browser Events + +| Event | Client behavior | +| --- | --- | +| `org::ready` | Mark browser ready and request `org::sync`. | +| `org::login::request` | Request portal hydrate as `org::login::success`. | +| `org::create::request` | Validate org name and request creation on server. | +| `org::disband::request` | Request disband on server. | +| `org::leave::request` | Request leave on server. | +| `org::credit::request` | Request credit-line assignment. | +| `org::invite::request` | Request member invite. | +| `org::invite::accept` | Accept invite by org ID. | +| `org::invite::decline` | Decline invite by org ID. | +| `org::close` | Close the display. | + +## Browser Response Events + +| Event | Purpose | +| --- | --- | +| `org::sync` | Full portal sync payload. | +| `org::login::success` | Login hydrate payload. | +| `org::create::success` | Creation hydrate payload. | +| `org::create::failure` | Creation validation or server failure. | +| `org::disband::success` | Requester disband success. | +| `org::disband::failure` | Disband failure. | +| `org::portal::revoked` | Portal state revoked by someone else's disband action. | +| `org::leave::success` | Leave success. | +| `org::leave::failure` | Leave failure. | +| `org::credit::success` | Credit-line request success. | +| `org::credit::failure` | Credit-line request failure. | +| `org::member::creditUpdated` | Targeted member credit-line patch. | +| `org::invite::success` | Invite success. | +| `org::invite::failure` | Invite failure. | +| `org::invite::decision::success` | Invite accept/decline success. | +| `org::invite::decision::failure` | Invite accept/decline failure. | + +## Request Examples + +Create organization request payload: + +```json +{ + "orgName": "Example Logistics" +} +``` + +Credit-line request payload: + +```json +{ + "memberUid": "76561198000000000", + "memberName": "Player Name", + "amount": 2500 +} +``` + +Invite request payload: + +```json +{ + "targetUid": "76561198000000000", + "targetName": "Player Name" +} +``` + +## Authoritative State + +Organization funds, reputation, membership, invites, credit lines, assets, +fleet, and persistence are server-owned. The client portal only displays and +requests changes. + +## Related Guides + +- [Organization Usage Guide](./ORG_USAGE_GUIDE.md) +- [Client Common Usage Guide](./CLIENT_COMMON_USAGE_GUIDE.md) +- [Client Bank Usage Guide](./CLIENT_BANK_USAGE_GUIDE.md) +- [Client Store Usage Guide](./CLIENT_STORE_USAGE_GUIDE.md) diff --git a/docs/CLIENT_PHONE_USAGE_GUIDE.md b/docs/CLIENT_PHONE_USAGE_GUIDE.md new file mode 100644 index 0000000..28832a1 --- /dev/null +++ b/docs/CLIENT_PHONE_USAGE_GUIDE.md @@ -0,0 +1,107 @@ +# Client Phone Usage Guide + +The client phone addon provides the in-game phone UI for contacts, SMS +messages, email, and local utility apps such as notes, calendar events, world +clocks, and alarms. + +## Open Phone UI + +```sqf +call forge_client_phone_fnc_openUI; +``` + +The phone UI creates `RscPhone`, loads `ui/_site/index.html`, and routes +browser alerts through `forge_client_phone_fnc_handleUIEvents`. + +## State Ownership + +Contacts, messages, and emails are server-owned and requested through the +server phone addon. + +Local utility app state is stored in `profileNamespace`: + +- notes +- calendar events +- world clocks +- alarms +- theme/preferences + +## Phone Class + +`forge_client_phone_fnc_initClass` creates `GVAR(PhoneClass)`. + +The phone class currently owns local notes, events, and settings helpers. +Contacts, messages, and emails continue to use server-backed request/response +events. + +## Browser Events + +### Session and Preferences + +| Event | Client behavior | +| --- | --- | +| `phone::get::player` | Send player UID to browser with `setPlayerUid`. | +| `phone::get::theme` | Send saved light/dark theme to browser. | +| `phone::set::theme` | Save theme preference to `profileNamespace`. | + +### Contacts + +| Event | Client behavior | +| --- | --- | +| `phone::get::contacts` | Load cached contacts and request server refresh. | +| `phone::refresh::contacts` | Request contacts from server. | +| `phone::add::contact` | Add contact by phone number. | +| `phone::add::contact::by::phone` | Add contact by phone number. | +| `phone::add::contact::by::email` | Add contact by email. | +| `phone::remove::contact` | Remove contact by UID. | + +### Messages + +| Event | Client behavior | +| --- | --- | +| `phone::get::messages` | Request messages from server. | +| `phone::get::message::thread` | Request thread with another UID. | +| `phone::send::message` | Send SMS through server. | +| `phone::mark::message::read` | Mark message read on server. | +| `phone::delete::message` | Delete message on server. | + +### Email + +| Event | Client behavior | +| --- | --- | +| `phone::get::emails` | Request emails from server. | +| `phone::send::email` | Send email through server. | +| `phone::mark::email::read` | Mark email read on server. | +| `phone::delete::email` | Delete email on server. | + +### Local Utility Apps + +| Event | Client behavior | +| --- | --- | +| `phone::get::notes` | Load local notes. | +| `phone::save::note` | Save local note. | +| `phone::delete::note` | Delete local note. | +| `phone::get::events` | Load local calendar events. | +| `phone::save::event` | Save local calendar event. | +| `phone::delete::event` | Delete local calendar event. | +| `phone::get::clocks` | Load local world clocks. | +| `phone::save::clock` | Save local world clock. | +| `phone::delete::clock` | Delete local world clock. | +| `phone::get::alarms` | Load local alarms. | +| `phone::save::alarm` | Save local alarm. | +| `phone::delete::alarm` | Delete local alarm. | +| `phone::toggle::alarm` | Toggle local alarm enabled state. | + +## Usage Rules + +- Send contact, message, and email mutations to the server phone addon. +- Keep local-only utility apps in `profileNamespace` until they are migrated to + server-backed storage. +- Do not treat local phone utility state as shared multiplayer state. +- Validate required UID, phone, email, subject, and message fields before + sending server requests. + +## Related Guides + +- [Phone Usage Guide](./PHONE_USAGE_GUIDE.md) +- [Client Notifications Usage Guide](./CLIENT_NOTIFICATIONS_USAGE_GUIDE.md) diff --git a/docs/CLIENT_STORE_USAGE_GUIDE.md b/docs/CLIENT_STORE_USAGE_GUIDE.md new file mode 100644 index 0000000..3078d3c --- /dev/null +++ b/docs/CLIENT_STORE_USAGE_GUIDE.md @@ -0,0 +1,92 @@ +# Client Store Usage Guide + +The client store addon provides the storefront browser UI for catalog browsing, +category hydration, payment source display, cart handling, and checkout +requests. + +## Open Store UI + +```sqf +call forge_client_store_fnc_openUI; +``` + +The UI opens `RscStore`, loads `ui/_site/index.html`, and routes browser alerts +through `forge_client_store_fnc_handleUIEvents`. + +## Bridge + +`forge_client_store_fnc_initUIBridge` owns: + +- browser control lookup +- store hydrate requests +- category requests +- checkout requests +- category hydrate/failure responses +- checkout success/failure responses +- store config refresh after successful checkout + +Store currently uses its own `StoreUIBridge.receive(...)` browser bridge rather +than the shared `ForgeBridge.receive(...)` delivery used by newer bridges. + +## Browser Events + +| Event | Client behavior | +| --- | --- | +| `store::ready` | Request store hydrate from the server. | +| `store::category::request` | Request catalog items for a category. | +| `store::checkout::request` | Forward checkout JSON to the server. | +| `store::close` | Close the display. | + +## Browser Response Events + +| Event | Purpose | +| --- | --- | +| `store::hydrate` | Initial storefront/session/config payload. | +| `store::config::hydrate` | Refreshed payment/source config. | +| `store::category::hydrate` | Category catalog payload. | +| `store::category::failure` | Category request failure. | +| `store::checkout::success` | Checkout success payload. | +| `store::checkout::failure` | Checkout failure payload. | + +## Category Requests + +Category requests require a non-empty category value. + +```json +{ + "category": "weapons" +} +``` + +The client lowercases the category before forwarding it to the server store +addon. + +## Checkout Requests + +Checkout requests send a serialized checkout payload: + +```json +{ + "checkoutJson": "{\"items\":[],\"paymentSource\":\"cash\"}" +} +``` + +The client only forwards the checkout data. The server store addon and +extension validate prices, inventory grants, payment source authorization, and +integration with bank, organization, locker, and garage state. + +After a successful checkout, the client asks the server for a fresh store config +payload so payment-source balances and permissions stay current. + +## Authoritative State + +Catalog data, prices, checkout validation, money movement, organization funds, +credit lines, locker grants, garage grants, and persistence are server-owned. + +## Related Guides + +- [Store Usage Guide](./STORE_USAGE_GUIDE.md) +- [Client Bank Usage Guide](./CLIENT_BANK_USAGE_GUIDE.md) +- [Client Organization Usage Guide](./CLIENT_ORG_USAGE_GUIDE.md) +- [Client Locker Usage Guide](./CLIENT_LOCKER_USAGE_GUIDE.md) +- [Client Garage Usage Guide](./CLIENT_GARAGE_USAGE_GUIDE.md) diff --git a/docs/CLIENT_USAGE_GUIDE.md b/docs/CLIENT_USAGE_GUIDE.md new file mode 100644 index 0000000..23f74f1 --- /dev/null +++ b/docs/CLIENT_USAGE_GUIDE.md @@ -0,0 +1,125 @@ +# Client Usage Guide + +Forge Client contains the Arma client-side addons that open player interfaces, +handle browser events, cache client-visible state, and forward authoritative +requests to the server addons. + +Use this guide as the entry point for client-side integration. Domain data, +validation, persistence, rewards, ownership, and checkout behavior remain +server-side responsibilities. + +## Client Responsibilities + +- Open Arma displays and `CT_WEBBROWSER` controls. +- Load browser UI assets from each addon's `ui/_site` folder. +- Receive browser alerts through `JSDialog` handlers. +- Translate browser events into local actions or CBA server events. +- Cache display state in client repositories. +- Push server responses back into browser UIs with `ExecJS`. +- Provide local-only utility state where the feature is intentionally local. + +## Authoritative Boundaries + +Client repositories are view state. They are useful for rendering, local UI +decisions, and short-lived session behavior, but they should not be treated as +durable state. + +Authoritative state lives in: + +- server SQF addons for mission and player workflow ownership +- the `forge_server` extension for durable and hot-state domain logic +- SurrealDB where the extension persists durable domain records + +## Common Runtime Flow + +Most browser-backed client addons follow this shape: + +1. The addon creates a display, finds a browser control, and registers a + `JSDialog` event handler. +2. The browser loads an HTML entrypoint from `ui/_site`. +3. The browser sends JSON alerts with an `event` name and `data` payload. +4. `fnc_handleUIEvents.sqf` parses the alert and routes the event. +5. A bridge object or repository sends a CBA server event when server data is + needed. +6. Server responses are caught in `XEH_postInitClient.sqf`. +7. The bridge sends browser update events back through `ExecJS`. + +Browser alert payload: + +```json +{ + "event": "module::action", + "data": {} +} +``` + +## Open UI Entry Points + +| UI | Entry point | +| --- | --- | +| Actor menu | `call forge_client_actor_fnc_openUI;` | +| Bank | `call forge_client_bank_fnc_openUI;` | +| ATM | `[true] call forge_client_bank_fnc_openUI;` | +| CAD | `call forge_client_cad_fnc_openUI;` | +| Garage | `call forge_client_garage_fnc_openUI;` | +| Virtual garage | `call forge_client_garage_fnc_openVG;` | +| Organization portal | `call forge_client_org_fnc_openUI;` | +| Phone | `call forge_client_phone_fnc_openUI;` | +| Store | `call forge_client_store_fnc_openUI;` | + +Notifications are normally opened during client initialization and then updated +through the notification event/service. + +## Addon Guides + +- [Client Main Usage Guide](./CLIENT_MAIN_USAGE_GUIDE.md) +- [Client Common Usage Guide](./CLIENT_COMMON_USAGE_GUIDE.md) +- [Client Actor Usage Guide](./CLIENT_ACTOR_USAGE_GUIDE.md) +- [Client Bank Usage Guide](./CLIENT_BANK_USAGE_GUIDE.md) +- [Client CAD Usage Guide](./CLIENT_CAD_USAGE_GUIDE.md) +- [Client Garage Usage Guide](./CLIENT_GARAGE_USAGE_GUIDE.md) +- [Client Locker Usage Guide](./CLIENT_LOCKER_USAGE_GUIDE.md) +- [Client Notifications Usage Guide](./CLIENT_NOTIFICATIONS_USAGE_GUIDE.md) +- [Client Organization Usage Guide](./CLIENT_ORG_USAGE_GUIDE.md) +- [Client Phone Usage Guide](./CLIENT_PHONE_USAGE_GUIDE.md) +- [Client Store Usage Guide](./CLIENT_STORE_USAGE_GUIDE.md) + +## Extension Calls + +Client addons should usually call server SQF events, not the `forge_server` +extension directly. The server addon owns validation context and converts the +request into extension commands. + +Example: + +```sqf +[SRPC(bank,requestDeposit), [getPlayerUID player, 100]] call CFUNC(serverEvent); +``` + +Direct extension calls from client code bypass server authorization boundaries +and should be avoided. + +## Browser Bridge Notes + +`forge_client_common_fnc_initWebUIBridge` provides reusable bridge and screen +objects for newer browser UIs. It queues outbound events until a browser screen +is ready, then delivers payloads through: + +```sqf +_control ctrlWebBrowserAction ["ExecJS", format ["ForgeBridge.receive(%1)", _json]]; +``` + +Feature addons still own their event names, request payloads, and response +mapping. + +## Development Checklist + +- Keep feature-specific behavior in the owning addon. +- Send authoritative changes to the server addon. +- Use namespaced browser events such as `bank::deposit::request`. +- Treat `profileNamespace` as local player preference or utility state only. +- Make browser-ready events request the current server state before rendering + stale data. +- Queue or ignore bridge responses when the display is closed. +- Keep mission object setup on the mission/server side and client display logic + on the client side. diff --git a/docs/MODULE_REFERENCE.md b/docs/MODULE_REFERENCE.md index 1557846..eb50fc8 100644 --- a/docs/MODULE_REFERENCE.md +++ b/docs/MODULE_REFERENCE.md @@ -33,7 +33,7 @@ docs/ Framework-level documentation | 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: +Server and extension guides: [Actor](./ACTOR_USAGE_GUIDE.md), [Bank](./BANK_USAGE_GUIDE.md), [CAD](./CAD_USAGE_GUIDE.md), @@ -45,6 +45,20 @@ Guides: [Store](./STORE_USAGE_GUIDE.md), [Task](./TASK_USAGE_GUIDE.md). +Client guides: +[Client Overview](./CLIENT_USAGE_GUIDE.md), +[Main](./CLIENT_MAIN_USAGE_GUIDE.md), +[Common](./CLIENT_COMMON_USAGE_GUIDE.md), +[Actor](./CLIENT_ACTOR_USAGE_GUIDE.md), +[Bank](./CLIENT_BANK_USAGE_GUIDE.md), +[CAD](./CLIENT_CAD_USAGE_GUIDE.md), +[Garage](./CLIENT_GARAGE_USAGE_GUIDE.md), +[Locker](./CLIENT_LOCKER_USAGE_GUIDE.md), +[Notifications](./CLIENT_NOTIFICATIONS_USAGE_GUIDE.md), +[Organization](./CLIENT_ORG_USAGE_GUIDE.md), +[Phone](./CLIENT_PHONE_USAGE_GUIDE.md), +[Store](./CLIENT_STORE_USAGE_GUIDE.md). + ## Infrastructure Modules | Module | Purpose | Location | diff --git a/docs/README.md b/docs/README.md index 641e9ba..81c25a7 100644 --- a/docs/README.md +++ b/docs/README.md @@ -14,7 +14,7 @@ collects framework-level documentation for those pieces. - [Development Guide](./DEVELOPMENT_GUIDE.md): how to add or change a module without breaking the framework boundaries. -## Existing Usage Guides +## Server and Extension Usage Guides - [Actor Usage Guide](./ACTOR_USAGE_GUIDE.md) - [Bank Usage Guide](./BANK_USAGE_GUIDE.md) @@ -27,6 +27,21 @@ collects framework-level documentation for those pieces. - [Store Usage Guide](./STORE_USAGE_GUIDE.md) - [Task Usage Guide](./TASK_USAGE_GUIDE.md) +## Client Usage Guides + +- [Client Usage Guide](./CLIENT_USAGE_GUIDE.md) +- [Client Main Usage Guide](./CLIENT_MAIN_USAGE_GUIDE.md) +- [Client Common Usage Guide](./CLIENT_COMMON_USAGE_GUIDE.md) +- [Client Actor Usage Guide](./CLIENT_ACTOR_USAGE_GUIDE.md) +- [Client Bank Usage Guide](./CLIENT_BANK_USAGE_GUIDE.md) +- [Client CAD Usage Guide](./CLIENT_CAD_USAGE_GUIDE.md) +- [Client Garage Usage Guide](./CLIENT_GARAGE_USAGE_GUIDE.md) +- [Client Locker Usage Guide](./CLIENT_LOCKER_USAGE_GUIDE.md) +- [Client Notifications Usage Guide](./CLIENT_NOTIFICATIONS_USAGE_GUIDE.md) +- [Client Organization Usage Guide](./CLIENT_ORG_USAGE_GUIDE.md) +- [Client Phone Usage Guide](./CLIENT_PHONE_USAGE_GUIDE.md) +- [Client Store Usage Guide](./CLIENT_STORE_USAGE_GUIDE.md) + ## Related Documentation - [Server Extension Docs](../arma/server/docs/README.md) -- 2.47.2 From 8117e6ffa6891cf25d635f14b5073b4ed1371267 Mon Sep 17 00:00:00 2001 From: Jacob Schmidt Date: Sat, 18 Apr 2026 13:37:09 -0500 Subject: [PATCH 6/7] feat(economy): Enhance economy system with service charges and medical billing - Expanded README.md to detail economy addon functionalities including refueling, medical services, and service charges. - Updated XEH_PREP.hpp to include initSEconomyStore preparation. - Modified XEH_postInit.sqf to ensure MEconomyStore initializes only if not nil. - Adjusted XEH_preInit.sqf to initialize SEconomyStore correctly. - Updated config.cpp to include forge_server_common as a required addon. - Enhanced fnc_initFEconomyStore.sqf to manage fuel refueling sessions and organization charges. - Improved fnc_initMEconomyStore.sqf to handle medical billing and fallback to organization funds. - Created fnc_initSEconomyStore.sqf for organization-funded service charges and repairs. - Updated org.rs and org.rs service layer to support member debt recording and organization fund charging. - Added ECONOMY_USAGE_GUIDE.md for comprehensive documentation on economy functionalities. - Updated MODULE_REFERENCE.md and README.md to include links to the new economy guide. --- arma/server/addons/economy/README.md | 80 +++++-- arma/server/addons/economy/XEH_PREP.hpp | 1 + arma/server/addons/economy/XEH_postInit.sqf | 4 +- arma/server/addons/economy/XEH_preInit.sqf | 7 +- arma/server/addons/economy/config.cpp | 3 +- .../functions/fnc_initFEconomyStore.sqf | 59 ++++- .../functions/fnc_initMEconomyStore.sqf | 115 ++++++++-- .../functions/fnc_initSEconomyStore.sqf | 115 +++++++++- .../addons/org/functions/fnc_initOrgStore.sqf | 17 +- docs/ECONOMY_USAGE_GUIDE.md | 69 ++++++ docs/MODULE_REFERENCE.md | 3 +- docs/README.md | 1 + lib/models/src/org.rs | 4 + lib/services/src/org.rs | 210 +++++++++++++++++- 14 files changed, 627 insertions(+), 61 deletions(-) create mode 100644 docs/ECONOMY_USAGE_GUIDE.md diff --git a/arma/server/addons/economy/README.md b/arma/server/addons/economy/README.md index 51adab8..f2e567c 100644 --- a/arma/server/addons/economy/README.md +++ b/arma/server/addons/economy/README.md @@ -2,30 +2,80 @@ ## Overview The economy addon contains server-side systems for world economic interactions -that are still implemented in SQF. +that are still implemented in SQF. It owns Arma-world behavior such as active +refueling sessions, medical spawn occupancy, respawn placement, and death +inventory handling. -Current stores cover fuel tracking, medical service behavior, and a placeholder -service economy store. +Current stores cover fuel tracking, medical service behavior, and service +charges such as repairs. ## Dependencies - `forge_server_main` -- `forge_server_common` at runtime for logging, formatting, and player lookup -- `forge_server_bank` at runtime for medical service charges +- `forge_server_common` for logging, formatting, and player lookup +- `forge_server_bank` for player-funded medical billing +- `forge_server_org` for extension-backed organization hot-cache charges - `forge_client_actor` and `forge_client_notifications` for response RPCs ## Main Components -- `fnc_initFEconomyStore.sqf` tracks active refueling sessions and reports fuel - totals. +- `fnc_initFEconomyStore.sqf` tracks active refueling sessions, calculates fuel + totals, charges the player's organization through `OrgStore`, syncs the org + patch, and rolls fuel back to the starting level when organization funds + cannot cover the refuel. - `fnc_initMEconomyStore.sqf` manages medical spawn occupancy, healing charges, - respawn placement, death inventory handling, and body-bag transfer. -- `fnc_initSEconomyStore.sqf` initializes the service economy placeholder. + respawn placement, death inventory handling, and body-bag transfer. Medical + charges use player bank/cash first, then organization funds with repayable + member debt only when the player cannot cover the service. +- `fnc_initSEconomyStore.sqf` handles organization-funded service charges and + repairs. Repairs only apply after the organization charge succeeds. The + shared org-charge helper can also record member debt for medical fallback. ## Event Surface -The addon registers CBA server events for fuel start/tick/stop, player killed, -player respawn, and healing. Medical store initialization runs after post-init -to discover configured medical spawn objects. +The addon registers CBA server events for fuel start/tick/stop, repair service, +player killed, player respawn, and healing. Medical store initialization runs +after post-init to discover configured medical spawn objects. + +Repair service requests use: + +```sqf +[QEGVAR(economy,RepairService), [_target, _unit, _cost]] call CBA_fnc_serverEvent; +``` + +`_cost` is optional. Passing `-1` uses the configured service repair cost. + +## Billing Rules +Economy does not own durable money state. It coordinates Arma-world effects +after the relevant hot-cache charge succeeds. + +Fuel and repair services are organization-funded: + +1. Resolve the player's organization from actor state. +2. Ensure the player is a member of that organization hot record. +3. Call `OrgStore chargeCheckout` with `source = "org_funds"`, + `commit = true`, and member service charging enabled. +4. Send the returned organization patch to online members. +5. If the charge fails, do not complete the service. Refueling rolls the target + back to its starting fuel level; repairs are not applied. + +Medical services are player-funded first: + +1. Load the player's bank hot state. +2. Charge the player's bank balance when it can cover the medical bill. +3. Otherwise charge the player's cash when it can cover the bill. +4. If neither personal balance can cover the bill, charge organization funds + and record the same amount as a debt on the player's organization credit + line. +5. If personal billing is unavailable, or both personal and organization funds + fail, do not complete the heal. + +The organization fallback reduces org funds immediately and adds the medical +cost to the player's credit-line balance due. Repayment uses the normal bank +credit-line repayment flow, which moves player bank funds back into the +organization treasury. + +This keeps money mutation rules in the extension-backed organization service +and bank service while leaving world interactions in SQF. ## Notes -The service economy store is currently a stub. Fuel and medical behavior should -stay server-authoritative because they mutate money, inventory, and respawn -state. +Fuel, medical, and service world behavior should stay server-authoritative +because it mutates inventory, vehicles, and respawn state. Money mutations +should continue to use extension-backed bank and organization hot state. diff --git a/arma/server/addons/economy/XEH_PREP.hpp b/arma/server/addons/economy/XEH_PREP.hpp index 6bbcd10..a377c3a 100644 --- a/arma/server/addons/economy/XEH_PREP.hpp +++ b/arma/server/addons/economy/XEH_PREP.hpp @@ -1,2 +1,3 @@ PREP(initFEconomyStore); PREP(initMEconomyStore); +PREP(initSEconomyStore); diff --git a/arma/server/addons/economy/XEH_postInit.sqf b/arma/server/addons/economy/XEH_postInit.sqf index b912379..96be61d 100644 --- a/arma/server/addons/economy/XEH_postInit.sqf +++ b/arma/server/addons/economy/XEH_postInit.sqf @@ -1,3 +1,5 @@ #include "script_component.hpp" -GVAR(MEconomyStore) call ["init", []]; +if !(isNil QGVAR(MEconomyStore)) then { + GVAR(MEconomyStore) call ["init", []]; +}; diff --git a/arma/server/addons/economy/XEH_preInit.sqf b/arma/server/addons/economy/XEH_preInit.sqf index 8a385c3..7b5bd48 100644 --- a/arma/server/addons/economy/XEH_preInit.sqf +++ b/arma/server/addons/economy/XEH_preInit.sqf @@ -8,7 +8,7 @@ PREP_RECOMPILE_END; if (isNil QGVAR(MEconomyStore)) then { call FUNC(initMEconomyStore); }; if (isNil QGVAR(FEconomyStore)) then { call FUNC(initFEconomyStore); }; -// if (isNil QGVAR(SEconomyStore)) then { call FUNC(initSEconomyStore); }; +if (isNil QGVAR(SEconomyStore)) then { call FUNC(initSEconomyStore); }; [QGVAR(FuelStart), { params ["_source", "_target", "_unit"]; @@ -28,6 +28,11 @@ if (isNil QGVAR(FEconomyStore)) then { call FUNC(initFEconomyStore); }; GVAR(FEconomyStore) call ["stop", [_source, _target]]; }] call CFUNC(addEventHandler); +[QGVAR(RepairService), { + params ["_target", "_unit", ["_cost", -1, [0]]]; + GVAR(SEconomyStore) call ["repair", [_target, _unit, _cost]]; +}] call CFUNC(addEventHandler); + [QGVAR(onKilled), { params ["_unit"]; GVAR(MEconomyStore) call ["onKilled", [_unit]]; diff --git a/arma/server/addons/economy/config.cpp b/arma/server/addons/economy/config.cpp index 05c825e..cbd0a75 100644 --- a/arma/server/addons/economy/config.cpp +++ b/arma/server/addons/economy/config.cpp @@ -8,7 +8,8 @@ class CfgPatches { name = COMPONENT_NAME; requiredVersion = REQUIRED_VERSION; requiredAddons[] = { - "forge_server_main" + "forge_server_main", + "forge_server_common" }; units[] = {}; weapons[] = {}; diff --git a/arma/server/addons/economy/functions/fnc_initFEconomyStore.sqf b/arma/server/addons/economy/functions/fnc_initFEconomyStore.sqf index 613ef18..eb820f9 100644 --- a/arma/server/addons/economy/functions/fnc_initFEconomyStore.sqf +++ b/arma/server/addons/economy/functions/fnc_initFEconomyStore.sqf @@ -8,16 +8,18 @@ * Public: No * * Description: - * No description added yet. + * Initializes the fuel economy store. Active refueling sessions remain + * server-local; payment is routed through the organization extension hot + * cache. * * Parameter(s): * N/A * * Returns: - * Something [BOOL] + * Fuel economy store object [HASHMAP OBJECT] * * Example(s): - * [parameter] call forge_x_component_fnc_myFunction + * call forge_server_economy_fnc_initFEconomyStore */ #pragma hemtt ignore_variables ["_self"] @@ -36,23 +38,66 @@ GVAR(FEconomyStore) = createHashMapObject [[ private _uid = getPlayerUID _unit; private _fuelRegistry = _self getOrDefault ["fuelRegistry", createHashMap]; - _fuelRegistry set [_index, _uid]; + _fuelRegistry set [_index, createHashMapFromArray [ + ["uid", _uid], + ["initialFuel", fuel _target] + ]]; SETVAR(_target,liters,0); }], + ["rollbackFuel", { + params [["_target", objNull, [objNull]], ["_initialFuel", 0, [0]]]; + + if (isNull _target) exitWith { false }; + + _target setFuel (_initialFuel max 0 min 1); + SETVAR(_target,liters,0); + true + }], ["stop", { params ["_source", "_target"]; private _index = netId _target; private _fuelRegistry = _self getOrDefault ["fuelRegistry", createHashMap]; - private _uid = _fuelRegistry get _index; + private _session = _fuelRegistry getOrDefault [_index, createHashMap]; + if (_session isEqualType "") then { + _session = createHashMapFromArray [["uid", _session], ["initialFuel", fuel _target]]; + }; + + private _uid = _session getOrDefault ["uid", ""]; + private _initialFuel = _session getOrDefault ["initialFuel", fuel _target]; private _player = [_uid] call EFUNC(common,getPlayer); private _totalLiters = GETVAR(_target,liters,0); - private _totalCost = _totalLiters * 5; + private _totalCost = _totalLiters * GVAR(FuelCost); private _formattedTotalCost = [_totalCost] call EFUNC(common,formatNumber); private _formattedTotalLiters = _totalLiters toFixed 2; - [CRPC(notifications,recieveNotification), ["info", "Refueling", format ["Refueling complete: %1L
Total Cost: $%2", _formattedTotalLiters, _formattedTotalCost]], _player] call CFUNC(targetEvent); + if (isNull _player || { _uid isEqualTo "" }) exitWith { + ["WARNING", format ["Unable to resolve refueling player for vehicle %1.", _index], nil, nil] call EFUNC(common,log); + _self call ["rollbackFuel", [_target, _initialFuel]]; + _fuelRegistry deleteAt _index; + }; + + if (_totalCost <= 0) exitWith { + [CRPC(notifications,recieveNotification), ["info", "Refueling", format ["Refueling complete: %1L", _formattedTotalLiters]], _player] call CFUNC(targetEvent); + _fuelRegistry deleteAt _index; + }; + + if (isNil QGVAR(SEconomyStore)) exitWith { + ["ERROR", "Service economy store unavailable for refueling charge.", nil, nil] call EFUNC(common,log); + [CRPC(notifications,recieveNotification), ["danger", "Refueling", "Organization billing is unavailable. Refueling was not completed."], _player] call CFUNC(targetEvent); + _self call ["rollbackFuel", [_target, _initialFuel]]; + _fuelRegistry deleteAt _index; + }; + + private _chargeResult = GVAR(SEconomyStore) call ["chargeOrg", [_player, _totalCost, "Refueling"]]; + if !(_chargeResult getOrDefault ["success", false]) exitWith { + [CRPC(notifications,recieveNotification), ["danger", "Refueling", _chargeResult getOrDefault ["message", "Organization funds cannot cover this refuel. Refueling was not completed."]], _player] call CFUNC(targetEvent); + _self call ["rollbackFuel", [_target, _initialFuel]]; + _fuelRegistry deleteAt _index; + }; + + [CRPC(notifications,recieveNotification), ["info", "Refueling", format ["Refueling complete: %1L
Organization charged $%2.", _formattedTotalLiters, _formattedTotalCost]], _player] call CFUNC(targetEvent); _fuelRegistry deleteAt _index; }] ]]; diff --git a/arma/server/addons/economy/functions/fnc_initMEconomyStore.sqf b/arma/server/addons/economy/functions/fnc_initMEconomyStore.sqf index d6edd84..217fe97 100644 --- a/arma/server/addons/economy/functions/fnc_initMEconomyStore.sqf +++ b/arma/server/addons/economy/functions/fnc_initMEconomyStore.sqf @@ -4,20 +4,23 @@ * File: fnc_initMEconomyStore.sqf * Author: IDSolutions * Date: 2025-12-20 - * Last Update: 2026-02-13 + * Last Update: 2026-04-18 * Public: No * * Description: - * No description added yet. + * Initializes the medical economy store. Respawn, body-bag, and spawn + * occupancy behavior remains server-local, while money mutations are + * routed through player bank hot state first, then organization hot state + * with a repayable member debt when personal funds cannot cover the bill. * * Parameter(s): * N/A * * Returns: - * Something [BOOL] + * Medical economy store object [HASHMAP OBJECT] * * Example(s): - * [parameter] call forge_x_component_fnc_myFunction + * call forge_server_economy_fnc_initMEconomyStore */ #pragma hemtt ignore_variables ["_self"] @@ -63,37 +66,105 @@ GVAR(MEconomyStore) = createHashMapObject [[ } forEach _mSpawns; }; }], + ["chargePlayer", { + params [["_uid", "", [""]], ["_amount", 0, [0]]]; + + private _result = createHashMapFromArray [ + ["success", false], + ["fallbackEligible", false], + ["source", ""], + ["message", "Unable to charge personal funds."] + ]; + + if (_uid isEqualTo "") exitWith { + _result set ["message", "A valid player UID is required for medical billing."]; + _result + }; + if (_amount <= 0) exitWith { + _result set ["success", true]; + _result + }; + if (isNil QEGVAR(bank,BankStore)) exitWith { + _result set ["message", "Personal billing is unavailable. Medical service cannot complete."]; + _result + }; + + private _account = EGVAR(bank,BankStore) call ["get", [_uid, ""]]; + if (_account isEqualTo createHashMap) exitWith { + _result set ["message", "Personal account could not be loaded for medical billing."]; + _result + }; + + private _source = ""; + if ((_account getOrDefault ["bank", 0]) >= _amount) then { + _source = "bank"; + } else { + if ((_account getOrDefault ["cash", 0]) >= _amount) then { + _source = "cash"; + }; + }; + + if (_source isEqualTo "") exitWith { + _result set ["fallbackEligible", true]; + _result set ["message", "Personal bank and cash balances cannot cover this medical service."]; + _result + }; + + private _charge = EGVAR(bank,BankStore) call ["chargeCheckout", [_uid, _source, _amount, true]]; + if !(_charge getOrDefault ["success", false]) exitWith { + _result set ["message", _charge getOrDefault ["message", "Personal funds could not be charged for medical service."]]; + _result + }; + + private _patch = _charge getOrDefault ["patch", createHashMap]; + if (_patch isNotEqualTo createHashMap && { !(isNil QEGVAR(bank,BankMessenger)) }) then { + EGVAR(bank,BankMessenger) call ["sendAccountSync", [_uid, _patch]]; + }; + + private _savedAccount = EGVAR(bank,BankStore) call ["save", [_uid]]; + if (_savedAccount isEqualTo createHashMap) then { + ["ERROR", format ["Medical charge for %1 succeeded in hot bank state, but durable bank save failed.", _uid]] call EFUNC(common,log); + }; + + _result set ["success", true]; + _result set ["source", _source]; + _result set ["message", ""]; + _result + }], ["onHealed", { params [["_unit", objNull, [objNull]]]; if (isNull _unit) exitWith { ["WARNING", format ["Invalid unit provided: %1", (name _unit)], nil, nil] call EFUNC(common,log); }; private _uid = getPlayerUID _unit; - private _account = EGVAR(bank,BankStore) call ["get", [_uid, ""]]; - if (_account isEqualTo createHashMap) then { - _account = EGVAR(bank,BankStore) call ["init", [_uid]]; - }; - - if (_account isEqualTo createHashMap) exitWith { ["ERROR", format ["No account found for %1. UID: %2", (name _unit), _uid], nil, nil] call EFUNC(common,log); }; - - private _bank = _account get "bank"; - private _cash = _account get "cash"; + if (_uid isEqualTo "") exitWith { ["WARNING", "Unable to charge medical service for unit without UID.", nil, nil] call EFUNC(common,log); }; private _healCost = 100; - private _newBalance = 0; - if (_bank < _healCost && _cash < _healCost) exitWith { - [CRPC(notifications,recieveNotification), ["danger", "Insufficient Funds", format ["Insufficient funds for %1. Bank: $%2, Cash: $%3, Required: $%4", (name _unit), [_bank] call EFUNC(common,formatNumber), [_cash] call EFUNC(common,formatNumber), [_healCost] call EFUNC(common,formatNumber)]], _unit] call CFUNC(targetEvent); + private _personalCharge = _self call ["chargePlayer", [_uid, _healCost]]; + if (_personalCharge getOrDefault ["success", false]) exitWith { + private _sourceLabel = ["cash", "bank"] select ((_personalCharge getOrDefault ["source", "bank"]) isEqualTo "bank"); + [CRPC(notifications,recieveNotification), ["info", "Medical Billing", format ["Medical service charged $%1 from your %2.", [_healCost] call EFUNC(common,formatNumber), _sourceLabel]], _unit] call CFUNC(targetEvent); + [CRPC(actor,onActorHealed), [], _unit] call CFUNC(targetEvent); }; - if (_bank >= _healCost) then { - _newBalance = _bank - _healCost; - _account set ["bank", _newBalance]; - } else { - _newBalance = _cash - _healCost; - _account set ["cash", _newBalance]; + if !(_personalCharge getOrDefault ["fallbackEligible", false]) exitWith { + private _message = _personalCharge getOrDefault ["message", "Personal funds could not be charged for medical service."]; + [CRPC(notifications,recieveNotification), ["danger", "Medical Billing", _message], _unit] call CFUNC(targetEvent); }; + if (isNil QGVAR(SEconomyStore)) exitWith { + ["ERROR", "Service economy store unavailable for medical organization fallback charge.", nil, nil] call EFUNC(common,log); + [CRPC(notifications,recieveNotification), ["danger", "Medical Billing", "Organization billing is unavailable. Medical service cannot complete."], _unit] call CFUNC(targetEvent); + }; + + private _chargeResult = GVAR(SEconomyStore) call ["chargeOrg", [_unit, _healCost, "Medical", true]]; + if !(_chargeResult getOrDefault ["success", false]) exitWith { + private _message = _chargeResult getOrDefault ["message", "Organization funds cannot cover this medical service."]; + [CRPC(notifications,recieveNotification), ["danger", "Medical Billing", _message], _unit] call CFUNC(targetEvent); + }; + + [CRPC(notifications,recieveNotification), ["info", "Medical Billing", format ["Personal funds could not cover medical service. Organization charged $%1; repay it through your organization credit line.", [_healCost] call EFUNC(common,formatNumber)]], _unit] call CFUNC(targetEvent); [CRPC(actor,onActorHealed), [], _unit] call CFUNC(targetEvent); }], ["onRespawn", { diff --git a/arma/server/addons/economy/functions/fnc_initSEconomyStore.sqf b/arma/server/addons/economy/functions/fnc_initSEconomyStore.sqf index 07a94dc..52c6454 100644 --- a/arma/server/addons/economy/functions/fnc_initSEconomyStore.sqf +++ b/arma/server/addons/economy/functions/fnc_initSEconomyStore.sqf @@ -1,31 +1,136 @@ #include "..\script_component.hpp" /* - * File: initSEconomyStore.sqf + * File: fnc_initSEconomyStore.sqf * Author: IDSolutions * Date: 2025-12-20 - * Last Update: 2026-01-03 + * Last Update: 2026-04-18 * Public: No * * Description: - * No description added yet. + * Initializes the service economy store for organization-funded world + * services such as repairs, with optional member debt recording for + * organization-covered medical fallback charges. * * Parameter(s): * N/A * * Returns: - * Something [BOOL] + * Service economy store object [HASHMAP OBJECT] * * Example(s): - * [parameter] call forge_x_component_fnc_myFunction + * call forge_server_economy_fnc_initSEconomyStore */ #pragma hemtt ignore_variables ["_self"] GVAR(SEconomyStore) = createHashMapObject [[ ["#type", "IServiceEconomy"], ["#create", { + GVAR(ServiceRepairCost) = 500; ["INFO", "Service Store Initialized!", nil, nil] call EFUNC(common,log); }], + ["notify", { + params [["_unit", objNull, [objNull]], ["_type", "info", [""]], ["_title", "Service", [""]], ["_message", "", [""]]]; + + if (isNull _unit || { _message isEqualTo "" }) exitWith { false }; + + [CRPC(notifications,recieveNotification), [_type, _title, _message], _unit] call CFUNC(targetEvent); + true + }], + ["syncOrgPatch", { + params [["_result", createHashMap, [createHashMap]]]; + + private _patch = _result getOrDefault ["patch", createHashMap]; + if ((keys _patch) isEqualTo []) exitWith { false }; + + { + private _memberPlayer = [_x] call EFUNC(common,getPlayer); + if (_memberPlayer isNotEqualTo objNull) then { + [CRPC(org,responseSyncOrg), [_patch], _memberPlayer] call CFUNC(targetEvent); + }; + } forEach (_result getOrDefault ["memberUids", []]); + + true + }], + ["chargeOrg", { + params [ + ["_unit", objNull, [objNull]], + ["_amount", 0, [0]], + ["_label", "Service", [""]], + ["_recordDebt", false, [false]] + ]; + + private _result = createHashMapFromArray [ + ["success", false], + ["message", "Unable to charge organization funds."], + ["patch", createHashMap], + ["memberUids", []], + ["persisted", false], + ["persistenceMessage", ""] + ]; + + if (isNull _unit) exitWith { + _result set ["message", "A valid player is required for organization billing."]; + _result + }; + + private _uid = getPlayerUID _unit; + if (_uid isEqualTo "") exitWith { + _result set ["message", "A valid player UID is required for organization billing."]; + _result + }; + + if (_amount <= 0) exitWith { + _result set ["success", true]; + _result set ["message", ""]; + _result + }; + + if (isNil QEGVAR(org,OrgStore)) exitWith { + _result set ["message", "Organization service is unavailable."]; + ["ERROR", format ["Org store unavailable for %1 charge.", _label], nil, nil] call EFUNC(common,log); + _result + }; + + private _orgID = EGVAR(org,OrgStore) call ["resolveOrgIdForUid", [_uid]]; + if (_orgID isEqualTo "") then { _orgID = "default"; }; + + private _actor = createHashMap; + if !(isNil QEGVAR(actor,ActorStore)) then { + _actor = EGVAR(actor,ActorStore) call ["load", [_uid]]; + }; + private _memberName = EGVAR(org,OrgStore) call ["resolveActorName", [_uid, _unit, _actor]]; + private _org = EGVAR(org,OrgStore) call ["ensureMember", [_orgID, _uid, _memberName]]; + if (_org isEqualTo createHashMap) exitWith { + _result set ["message", "Organization membership could not be verified."]; + _result + }; + + private _charge = EGVAR(org,OrgStore) call ["chargeCheckout", [_uid, _unit, "org_funds", _amount, true, true, _recordDebt]]; + if !(_charge getOrDefault ["success", false]) exitWith { + _result set ["message", _charge getOrDefault ["message", "Organization funds cannot cover this service."]]; + _result + }; + + _self call ["syncOrgPatch", [_charge]]; + _charge + }], + ["repair", { + params [["_target", objNull, [objNull]], ["_unit", objNull, [objNull]], ["_cost", -1, [0]]]; + + if (isNull _target || { isNull _unit }) exitWith { false }; + + private _repairCost = [_cost, GVAR(ServiceRepairCost)] select (_cost < 0); + private _charge = _self call ["chargeOrg", [_unit, _repairCost, "Repair"]]; + if !(_charge getOrDefault ["success", false]) exitWith { + _self call ["notify", [_unit, "danger", "Repair", _charge getOrDefault ["message", "Organization funds cannot cover this repair."]]]; + false + }; + + _target setDamage 0; + _self call ["notify", [_unit, "info", "Repair", format ["Repair complete. Organization charged $%1.", [_repairCost] call EFUNC(common,formatNumber)]]]; + true + }], ["init", {}] ]]; diff --git a/arma/server/addons/org/functions/fnc_initOrgStore.sqf b/arma/server/addons/org/functions/fnc_initOrgStore.sqf index e1a8dbd..cda2669 100644 --- a/arma/server/addons/org/functions/fnc_initOrgStore.sqf +++ b/arma/server/addons/org/functions/fnc_initOrgStore.sqf @@ -4,12 +4,13 @@ * File: fnc_initOrgStore.sqf * Author: IDSolutions * Date: 2026-02-13 - * Last Update: 2026-04-04 + * Last Update: 2026-04-18 * Public: Yes * * Description: * Initializes the org store for managing player organizations. - * Org hot state is owned by the extension; SQF acts as the bridge. + * Org hot state is owned by the extension; SQF acts as the bridge for + * treasury charges, credit lines, and service debt recording. * * Arguments: * None @@ -800,7 +801,15 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [ _result }], ["chargeCheckout", compileFinal { - params [["_requesterUid", "", [""]], ["_requesterPlayer", objNull, [objNull]], ["_source", "org_funds", [""]], ["_amount", 0, [0]], ["_commit", false, [false]]]; + params [ + ["_requesterUid", "", [""]], + ["_requesterPlayer", objNull, [objNull]], + ["_source", "org_funds", [""]], + ["_amount", 0, [0]], + ["_commit", false, [false]], + ["_allowMemberCharge", false, [false]], + ["_recordMemberDebt", false, [false]] + ]; private _result = createHashMapFromArray [ ["success", false], @@ -822,6 +831,8 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [ ["requesterUid", _requesterUid], ["orgId", _orgID], ["requesterIsDefaultOrgCeo", _requesterIsDefaultOrgCeo], + ["allowMemberCharge", _allowMemberCharge], + ["recordMemberDebt", _recordMemberDebt], ["source", _source], ["amount", _amount], ["commit", _commit] diff --git a/docs/ECONOMY_USAGE_GUIDE.md b/docs/ECONOMY_USAGE_GUIDE.md new file mode 100644 index 0000000..3552e15 --- /dev/null +++ b/docs/ECONOMY_USAGE_GUIDE.md @@ -0,0 +1,69 @@ +# Economy Usage Guide + +The economy server addon owns Arma-world service behavior for fuel, medical, +and repair interactions. It does not own money state. Money mutations go +through extension-backed bank and organization hot state before the world +effect is applied. + +## Dependencies + +- `forge_server_common` for logging, formatting, and player lookup. +- `forge_server_bank` for personal medical billing. +- `forge_server_org` for organization-funded services and medical fallback + debt. +- `forge_client_actor` and `forge_client_notifications` for targeted client + responses. + +## Fuel + +Fuel is organization-funded. + +When refueling stops, `fnc_initFEconomyStore.sqf` calculates the fuel delta and +cost, charges the player's organization through `OrgStore chargeCheckout`, and +syncs the organization patch to online members. If organization funds cannot +cover the refuel, the vehicle is rolled back to the fuel level it had when the +session started. + +## Repair + +Repair is organization-funded. + +Use the repair service event: + +```sqf +[QEGVAR(economy,RepairService), [_target, _unit, _cost]] call CBA_fnc_serverEvent; +``` + +`_cost` is optional. Passing `-1` uses the configured service repair cost. +The target is only repaired after the organization charge succeeds. + +## Medical + +Medical is player-funded first. + +When a heal is requested, `fnc_initMEconomyStore.sqf` uses this billing order: + +1. Charge the player's bank balance when it can cover the medical fee. +2. Otherwise charge the player's cash when it can cover the fee. +3. If neither personal balance can cover the fee, charge organization funds. +4. When organization funds cover the fallback charge, record the same amount as + debt on the player's organization credit line. + +The heal only completes after one of those charges succeeds. If personal +billing is unavailable, the heal does not fall back to organization funds +because the server cannot verify that the player is unable to cover the fee. + +## Medical Debt Repayment + +Medical fallback debt uses the existing organization credit-line repayment +flow. The organization treasury is reduced when the service is rendered, and +the player's credit-line `amount_due` increases by the medical fee. When the +player repays through the bank credit-line repayment action, player bank funds +are moved back into the organization treasury. + +## Hot-Cache Boundary + +The economy addon should stay server-authoritative for world effects such as +vehicle fuel, vehicle repair, healing, respawn placement, and death inventory +movement. Bank and organization balances should continue to mutate through the +extension-backed hot-cache services. diff --git a/docs/MODULE_REFERENCE.md b/docs/MODULE_REFERENCE.md index eb50fc8..9b16dc9 100644 --- a/docs/MODULE_REFERENCE.md +++ b/docs/MODULE_REFERENCE.md @@ -37,6 +37,7 @@ Server and extension guides: [Actor](./ACTOR_USAGE_GUIDE.md), [Bank](./BANK_USAGE_GUIDE.md), [CAD](./CAD_USAGE_GUIDE.md), +[Economy](./ECONOMY_USAGE_GUIDE.md), [Garage](./GARAGE_USAGE_GUIDE.md), [Locker](./LOCKER_USAGE_GUIDE.md), [Organization](./ORG_USAGE_GUIDE.md), @@ -66,7 +67,7 @@ Client guides: | `common` | Shared SQF helpers, base stores, utility functions, and shared UI bridge pieces. | `arma/client/addons/common`, `arma/server/addons/common` | | `extension` | Server SQF bridge around `forge_server` extension calls and chunked transport. | `arma/server/addons/extension` | | `main` | Mod-level configuration, pre-init wiring, and server/client startup glue. | `arma/client/addons/main`, `arma/server/addons/main` | -| `economy` | Server-side economy store initialization and economy-specific state helpers. | `arma/server/addons/economy` | +| `economy` | Server-side fuel, medical, and service economy helpers. Fuel and repair charge organization hot state; medical charges player bank/cash first, then organization funds with repayable member debt when personal funds cannot cover the bill. | `arma/server/addons/economy` | | `notifications` | Client notification UI, sounds, and UI event handling. | `arma/client/addons/notifications` | | `icom` | Rust helper for interprocess communication and event broadcasting. | `bin/icom`, `arma/server/extension/src/icom.rs` | | `terrain` | Extension-side terrain export helper. | `arma/server/extension/src/terrain.rs` | diff --git a/docs/README.md b/docs/README.md index 81c25a7..09210e6 100644 --- a/docs/README.md +++ b/docs/README.md @@ -19,6 +19,7 @@ collects framework-level documentation for those pieces. - [Actor Usage Guide](./ACTOR_USAGE_GUIDE.md) - [Bank Usage Guide](./BANK_USAGE_GUIDE.md) - [CAD Usage Guide](./CAD_USAGE_GUIDE.md) +- [Economy Usage Guide](./ECONOMY_USAGE_GUIDE.md) - [Garage Usage Guide](./GARAGE_USAGE_GUIDE.md) - [Locker Usage Guide](./LOCKER_USAGE_GUIDE.md) - [Organization Usage Guide](./ORG_USAGE_GUIDE.md) diff --git a/lib/models/src/org.rs b/lib/models/src/org.rs index 5d05592..266fec3 100644 --- a/lib/models/src/org.rs +++ b/lib/models/src/org.rs @@ -180,6 +180,10 @@ pub struct OrgCheckoutContext { pub requester_uid: String, pub org_id: String, pub requester_is_default_org_ceo: bool, + #[serde(default)] + pub allow_member_charge: bool, + #[serde(default)] + pub record_member_debt: bool, pub source: String, pub amount: f64, pub commit: bool, diff --git a/lib/services/src/org.rs b/lib/services/src/org.rs index 066d72b..f3e5ae0 100644 --- a/lib/services/src/org.rs +++ b/lib/services/src/org.rs @@ -793,24 +793,69 @@ impl OrgHotStateService { match context.source.trim().to_ascii_lowercase().as_str() { "org_funds" => { - if !can_manage_treasury( + let charged_amount = round_currency(context.amount); + let can_charge_org_funds = can_manage_treasury( &org, &context.requester_uid, context.requester_is_default_org_ceo, - ) { + ) || (context.allow_member_charge + && org.members.contains_key(&context.requester_uid)); + + if !can_charge_org_funds { return Err( "Only the organization leader or CEO can charge org funds.".to_string() ); } - if org.funds < context.amount { + if org.funds < charged_amount { return Err("Organization funds cannot cover this checkout.".to_string()); } - org.funds -= context.amount; + org.funds = round_currency(org.funds - charged_amount); + if context.record_member_debt { + let member_name = org + .members + .get(&context.requester_uid) + .map(|member| member.name.clone()) + .filter(|name| !name.trim().is_empty()) + .unwrap_or_else(|| "Unknown".to_string()); + let mut credit_line = org + .credit_lines + .get(&context.requester_uid) + .cloned() + .unwrap_or_else(|| CreditLineSummary { + uid: context.requester_uid.clone(), + name: member_name.clone(), + approved_amount: 0.0, + available_amount: 0.0, + outstanding_principal: 0.0, + interest_rate: DEFAULT_CREDIT_LINE_INTEREST_RATE, + amount_due: 0.0, + amount: 0.0, + }); + credit_line.normalize(); + credit_line.uid = context.requester_uid.clone(); + credit_line.name = member_name; + if credit_line.interest_rate <= 0.0 { + credit_line.interest_rate = DEFAULT_CREDIT_LINE_INTEREST_RATE; + } + credit_line.outstanding_principal = + round_currency(credit_line.outstanding_principal + charged_amount); + credit_line.amount_due = + round_currency(credit_line.amount_due + charged_amount); + credit_line.amount = credit_line.available_amount; + org.credit_lines + .insert(context.requester_uid.clone(), credit_line); + } self.repository.save(&org)?; + let patch_fields = if context.record_member_debt { + vec!["funds", "credit_lines"] + } else { + vec!["funds"] + }; + Ok(OrgMutationResult { - patch: build_org_patch(&org, &["funds"])?, + patch: build_org_patch(&org, &patch_fields)?, member_uids, message: String::new(), org, @@ -1220,3 +1265,158 @@ fn format_currency(amount: f64) -> String { fn round_currency(amount: f64) -> f64 { (amount.max(0.0) * 100.0).round() / 100.0 } + +#[cfg(test)] +mod tests { + use super::*; + use forge_repositories::InMemoryOrgHotRepository; + + #[derive(Clone, Default)] + struct TestOrgRepository; + + impl OrgRepository for TestOrgRepository { + fn create(&self, _org: &Org) -> Result<(), String> { + Ok(()) + } + + fn get_by_id(&self, _id: &str) -> Result, String> { + Ok(None) + } + + fn update(&self, _org: &Org) -> Result<(), String> { + Ok(()) + } + + fn delete(&self, _id: &str) -> Result<(), String> { + Ok(()) + } + + fn exists(&self, _id: &str) -> Result { + Ok(false) + } + + fn add_member(&self, _org_id: &str, _member_uid: &str) -> Result<(), String> { + Ok(()) + } + + fn get_members(&self, _org_id: &str) -> Result, String> { + Ok(Vec::new()) + } + + fn remove_member(&self, _org_id: &str, _member_uid: &str) -> Result<(), String> { + Ok(()) + } + + fn get_assets( + &self, + _org_id: &str, + ) -> Result>, String> { + Ok(HashMap::new()) + } + + fn update_assets( + &self, + _org_id: &str, + _assets: &HashMap>, + ) -> Result<(), String> { + Ok(()) + } + + fn get_fleet(&self, _org_id: &str) -> Result, String> { + Ok(HashMap::new()) + } + + fn update_fleet( + &self, + _org_id: &str, + _fleet: &HashMap, + ) -> Result<(), String> { + Ok(()) + } + } + + fn test_hot_org() -> HotOrgRecord { + let mut members = HashMap::new(); + members.insert( + "member".to_string(), + MemberSummary { + uid: "member".to_string(), + name: "Medic Patient".to_string(), + }, + ); + + HotOrgRecord { + id: "org".to_string(), + owner: "owner".to_string(), + name: "Test Org".to_string(), + funds: 500.0, + reputation: 0, + credit_lines: HashMap::new(), + assets: HashMap::new(), + fleet: HashMap::new(), + members, + pending_invites: HashMap::new(), + } + } + + fn test_service( + hot_repository: InMemoryOrgHotRepository, + ) -> OrgHotStateService { + OrgHotStateService::new(TestOrgRepository, hot_repository) + } + + #[test] + fn org_funds_checkout_without_member_debt_only_reduces_funds() { + let hot_repository = InMemoryOrgHotRepository::new(); + hot_repository.save(&test_hot_org()).unwrap(); + let service = test_service(hot_repository); + + let result = service + .charge_checkout(OrgCheckoutContext { + requester_uid: "member".to_string(), + org_id: "org".to_string(), + requester_is_default_org_ceo: false, + allow_member_charge: true, + record_member_debt: false, + source: "org_funds".to_string(), + amount: 125.0, + commit: true, + }) + .unwrap(); + + assert_eq!(result.org.funds, 375.0); + assert!(result.org.credit_lines.is_empty()); + assert!(result.patch.contains_key("funds")); + assert!(!result.patch.contains_key("credit_lines")); + } + + #[test] + fn org_funds_checkout_can_record_member_debt() { + let hot_repository = InMemoryOrgHotRepository::new(); + hot_repository.save(&test_hot_org()).unwrap(); + let service = test_service(hot_repository); + + let result = service + .charge_checkout(OrgCheckoutContext { + requester_uid: "member".to_string(), + org_id: "org".to_string(), + requester_is_default_org_ceo: false, + allow_member_charge: true, + record_member_debt: true, + source: "org_funds".to_string(), + amount: 100.0, + commit: true, + }) + .unwrap(); + + let credit_line = result.org.credit_lines.get("member").unwrap(); + assert_eq!(result.org.funds, 400.0); + assert_eq!(credit_line.uid, "member"); + assert_eq!(credit_line.name, "Medic Patient"); + assert_eq!(credit_line.outstanding_principal, 100.0); + assert_eq!(credit_line.amount_due, 100.0); + assert_eq!(credit_line.available_amount, 0.0); + assert!(result.patch.contains_key("funds")); + assert!(result.patch.contains_key("credit_lines")); + } +} -- 2.47.2 From ee7d1603ef3071533ded9d064432b53155b88e12 Mon Sep 17 00:00:00 2001 From: Jacob Schmidt Date: Sat, 18 Apr 2026 14:09:14 -0500 Subject: [PATCH 7/7] feat(garage): add refuel and repair service requests - Implemented requestRefuel and requestRepair functions in bridge.js to handle vehicle service requests. - Updated AppShell.js to include buttons for refueling and repairing nearby vehicles, with appropriate state management. - Added requestRefuelSelected and requestRepairSelected actions in events.js to validate and process service requests. - Enhanced economy README and usage guides to document new refuel and repair service functionalities. - Introduced server-side handling for refuel requests in FEconomyStore, ensuring organization billing and fuel management. --- arma/client/addons/garage/README.md | 14 +++- .../garage/functions/fnc_handleUIEvents.sqf | 12 ++- .../functions/fnc_initActionService.sqf | 82 ++++++++++++++++++- .../addons/garage/ui/_site/garage-ui.js | 2 +- arma/client/addons/garage/ui/src/bridge.js | 30 +++++++ .../garage/ui/src/components/AppShell.js | 41 +++++++++- .../addons/garage/ui/src/registry/events.js | 66 +++++++++++++++ arma/server/addons/economy/README.md | 19 ++++- arma/server/addons/economy/XEH_preInit.sqf | 5 ++ .../functions/fnc_initFEconomyStore.sqf | 42 +++++++++- docs/CLIENT_GARAGE_USAGE_GUIDE.md | 19 ++++- docs/ECONOMY_USAGE_GUIDE.md | 8 ++ 12 files changed, 323 insertions(+), 17 deletions(-) diff --git a/arma/client/addons/garage/README.md b/arma/client/addons/garage/README.md index 879df40..2950114 100644 --- a/arma/client/addons/garage/README.md +++ b/arma/client/addons/garage/README.md @@ -2,7 +2,8 @@ ## Overview The garage addon provides player vehicle storage UI, vehicle store/retrieve -actions, and virtual garage state on the client. +actions, selected nearby vehicle service requests, and virtual garage state on +the client. ## Dependencies - `forge_client_common` @@ -17,8 +18,8 @@ actions, and virtual garage state on the client. details. - `fnc_initContextService.sqf` gathers nearby/current vehicle context. - `fnc_initPayloadService.sqf` builds browser hydrate payloads. -- `fnc_initActionService.sqf` sends store/retrieve requests and handles action - responses. +- `fnc_initActionService.sqf` sends store/retrieve requests, forwards selected + nearby vehicle refuel/repair service requests, and handles action responses. - `fnc_initUIBridge.sqf` pushes hydrate/sync events to the browser. - `fnc_openUI.sqf` opens `RscGarage`. - `fnc_openVG.sqf` opens the Arma garage-style virtual garage view. @@ -28,8 +29,15 @@ actions, and virtual garage state on the client. - `garage::refresh` - `garage::vehicle::retrieve::request` - `garage::vehicle::store::request` +- `garage::vehicle::refuel::request` +- `garage::vehicle::repair::request` - `garage::close` ## Runtime Notes The client builds vehicle context and sends requests. The server garage addon and extension own stored vehicle state. + +Refuel and repair buttons are available from the selected vehicle detail panel +for nearby world vehicles. Stored records must be retrieved before they can be +serviced because fuel and repair operate on live vehicle objects. Service +billing is handled by the server economy addon and charges organization funds. diff --git a/arma/client/addons/garage/functions/fnc_handleUIEvents.sqf b/arma/client/addons/garage/functions/fnc_handleUIEvents.sqf index 94c1ad1..f437877 100644 --- a/arma/client/addons/garage/functions/fnc_handleUIEvents.sqf +++ b/arma/client/addons/garage/functions/fnc_handleUIEvents.sqf @@ -4,7 +4,7 @@ * File: fnc_handleUIEvents.sqf * Author: IDSolutions * Date: 2025-12-16 - * Last Update: 2026-01-30 + * Last Update: 2026-04-18 * Public: No * * Description: @@ -53,6 +53,16 @@ switch (_event) do { GVAR(GarageActionService) call ["handleStoreRequest", [_data]]; }; }; + case "garage::vehicle::refuel::request": { + if !(isNil QGVAR(GarageActionService)) then { + GVAR(GarageActionService) call ["handleRefuelRequest", [_data]]; + }; + }; + case "garage::vehicle::repair::request": { + if !(isNil QGVAR(GarageActionService)) then { + GVAR(GarageActionService) call ["handleRepairRequest", [_data]]; + }; + }; case "garage::refresh": { if !(isNil QGVAR(GarageUIBridge)) then { GVAR(GarageUIBridge) call ["refreshGarage", []]; diff --git a/arma/client/addons/garage/functions/fnc_initActionService.sqf b/arma/client/addons/garage/functions/fnc_initActionService.sqf index 408d5a8..bb4fd57 100644 --- a/arma/client/addons/garage/functions/fnc_initActionService.sqf +++ b/arma/client/addons/garage/functions/fnc_initActionService.sqf @@ -4,10 +4,12 @@ * File: fnc_initActionService.sqf * Author: IDSolutions * Date: 2026-03-27 + * Last Update: 2026-04-18 * Public: No * * Description: - * Initializes the garage action service for retrieve and store world actions. + * Initializes the garage action service for retrieve, store, refuel, and + * repair world actions. * * Arguments: * None @@ -26,6 +28,52 @@ GVAR(GarageActionServiceBaseClass) = compileFinal createHashMapFromArray [ _self set ["pendingStoreVehicle", objNull]; _self set ["pendingRetrieve", createHashMap]; }], + ["sendServiceResult", compileFinal { + params [["_action", "", [""]], ["_success", false, [false]], ["_message", "", [""]]]; + + private _event = ["garage::service::failure", "garage::service::success"] select _success; + GVAR(GarageUIBridge) call ["sendEvent", [_event, createHashMapFromArray [["action", _action], ["message", _message]]]]; + }], + ["refreshAfterService", compileFinal { + [] spawn { + sleep 0.75; + if !(isNil QGVAR(GarageUIBridge)) then { + GVAR(GarageUIBridge) call ["refreshGarage", []]; + }; + }; + }], + ["resolveServiceVehicle", compileFinal { + params [["_data", createHashMap, [createHashMap]], ["_action", "service", [""]]]; + + private _netId = _data getOrDefault ["netId", ""]; + if (_netId isEqualTo "") exitWith { + _self call ["sendServiceResult", [_action, false, "Select a nearby vehicle first."]]; + objNull + }; + + private _vehicle = objectFromNetId _netId; + if (isNull _vehicle) exitWith { + _self call ["sendServiceResult", [_action, false, "The selected vehicle is no longer available."]]; + objNull + }; + + if !(_vehicle isKindOf "Car" || { _vehicle isKindOf "Tank" } || { _vehicle isKindOf "Air" } || { _vehicle isKindOf "Ship" }) exitWith { + _self call ["sendServiceResult", [_action, false, "Selected object is not a serviceable vehicle."]]; + objNull + }; + + _vehicle + }], + ["vehicleNeedsRepair", compileFinal { + params [["_vehicle", objNull, [objNull]]]; + + if (isNull _vehicle) exitWith { false }; + if ((damage _vehicle) > 0.001) exitWith { true }; + + private _rawHitPoints = getAllHitPointsDamage _vehicle; + private _hitPointValues = if (_rawHitPoints isEqualType [] && { count _rawHitPoints >= 3 }) then { _rawHitPoints param [2, []] } else { [] }; + ({ _x > 0.001 } count _hitPointValues) > 0 + }], ["handleRetrieveRequest", compileFinal { params [["_data", createHashMap, [createHashMap]]]; @@ -97,6 +145,38 @@ GVAR(GarageActionServiceBaseClass) = compileFinal createHashMapFromArray [ _self set ["pendingStoreVehicle", _vehicle]; [SRPC(garage,requestStoreVehicle), [getPlayerUID player, typeOf _vehicle, fuel _vehicle, damage _vehicle, _hitPointsJson]] call CFUNC(serverEvent); }], + ["handleRefuelRequest", compileFinal { + params [["_data", createHashMap, [createHashMap]]]; + + private _vehicle = _self call ["resolveServiceVehicle", [_data, "refuel"]]; + if (isNull _vehicle) exitWith { false }; + + if ((fuel _vehicle) >= 0.999) exitWith { + _self call ["sendServiceResult", ["refuel", false, "Vehicle fuel tank is already full."]]; + false + }; + + [SRPC(economy,RefuelService), [_vehicle, player]] call CFUNC(serverEvent); + _self call ["sendServiceResult", ["refuel", true, "Refuel request sent. Billing result will appear as a notification."]]; + _self call ["refreshAfterService", []]; + true + }], + ["handleRepairRequest", compileFinal { + params [["_data", createHashMap, [createHashMap]]]; + + private _vehicle = _self call ["resolveServiceVehicle", [_data, "repair"]]; + if (isNull _vehicle) exitWith { false }; + + if !(_self call ["vehicleNeedsRepair", [_vehicle]]) exitWith { + _self call ["sendServiceResult", ["repair", false, "Vehicle has no reported damage."]]; + false + }; + + [SRPC(economy,RepairService), [_vehicle, player, -1]] call CFUNC(serverEvent); + _self call ["sendServiceResult", ["repair", true, "Repair request sent. Billing result will appear as a notification."]]; + _self call ["refreshAfterService", []]; + true + }], ["handleActionResponse", compileFinal { params [["_payload", createHashMap, [createHashMap]]]; diff --git a/arma/client/addons/garage/ui/_site/garage-ui.js b/arma/client/addons/garage/ui/_site/garage-ui.js index 2f7fb77..05c4afd 100644 --- a/arma/client/addons/garage/ui/_site/garage-ui.js +++ b/arma/client/addons/garage/ui/_site/garage-ui.js @@ -1 +1 @@ -!function(){const e=window.ForgeWebUI;(window.GarageApp=window.GarageApp||{}).runtime=e,window.AppRuntime=e}(),function(){const e=window.GarageApp=window.GarageApp||{},a={garageName:"Vehicle Garage",capacityUsed:0,capacityMax:5,nearbyCount:0,spawnBlocked:!1,spawnStatus:"Ready"},t={vehicles:[]},r={vehicles:[]};function s(e,a){var t;Object.keys(e).forEach(a=>delete e[a]),Object.assign(e,(t=a,JSON.parse(JSON.stringify(t))))}e.data={categories:[{id:"all",label:"All"},{id:"car",label:"Cars"},{id:"armor",label:"Armor"},{id:"air",label:"Air"},{id:"naval",label:"Naval"},{id:"other",label:"Other"}],session:Object.assign({},a),garage:Object.assign({},t),nearby:Object.assign({},r),applyHydratePayload(e){s(this.session,Object.assign({},a,e?.session||{})),s(this.garage,Object.assign({},t,e?.garage||{})),s(this.nearby,Object.assign({},r,e?.nearby||{}))}}}(),function(){const e=window.GarageApp=window.GarageApp||{},{createSignal:a}=e.runtime;e.store=new class{constructor(){[this.getSelectedKind,this.setSelectedKind]=a(""),[this.getSelectedId,this.setSelectedId]=a(""),[this.getSearchQuery,this.setSearchQuery]=a(""),[this.getCategoryFilter,this.setCategoryFilter]=a("all"),[this.getPendingAction,this.setPendingAction]=a(""),[this.getNotice,this.setNotice]=a({type:"",text:""})}getSelection(){return{id:this.getSelectedId(),kind:this.getSelectedKind()}}clearSelection(){this.setSelectedKind(""),this.setSelectedId("")}select(e,a){this.setSelectedKind(String(e||"")),this.setSelectedId(String(a||""))}startAction(e){this.setPendingAction(String(e||""))}finishAction(){this.setPendingAction("")}matchesSelection(e){if(!e||"object"!=typeof e)return!1;const a=this.getSelection();return!(!a.kind||!a.id)&&("stored"===a.kind?"stored"===e.entryKind&&String(e.plate||"")===a.id:"nearby"===a.kind&&("nearby"===e.entryKind&&String(e.netId||"")===a.id))}ensureSelection(){const a=Array.isArray(e.data?.garage?.vehicles)?e.data.garage.vehicles:[],t=Array.isArray(e.data?.nearby?.vehicles)?e.data.nearby.vehicles:[];if([...a,...t].some(e=>this.matchesSelection(e)))return;const r=a[0]||null;if(r)return void this.select("stored",r.plate||"");const s=t[0]||null;s?this.select("nearby",s.netId||""):this.clearSelection()}hydrateFromPayload(){this.finishAction(),this.ensureSelection()}}}(),function(){const e=window.GarageApp=window.GarageApp||{},a=e.store,t=window.ForgeWebUI.createBridge({closeEvent:"garage::close",globalName:"ForgeBridge",readyEvent:"garage::ready"});function r(t){e.data.applyHydratePayload(t),a.hydrateFromPayload(t)}t.on("garage::hydrate",r),t.on("garage::sync",r),t.on("garage::retrieve::success",t=>{a.finishAction(),e.actions&&e.actions.showNotice("success",t.message||"Vehicle retrieved from the garage.")}),t.on("garage::retrieve::failure",t=>{a.finishAction(),e.actions&&e.actions.showNotice("error",t.message||"Unable to retrieve vehicle.")}),t.on("garage::store::success",t=>{a.finishAction(),e.actions&&e.actions.showNotice("success",t.message||"Vehicle stored in the garage.")}),t.on("garage::store::failure",t=>{a.finishAction(),e.actions&&e.actions.showNotice("error",t.message||"Unable to store vehicle.")}),e.bridge={notifyReady:function(){return t.ready({loaded:!0})},receive:t.receive,requestClose:function(){return t.close({})},requestRefresh:function(){return t.send("garage::refresh",{})},requestRetrieve:function(e){return t.send("garage::vehicle::retrieve::request",e)},requestStore:function(e){return t.send("garage::vehicle::store::request",e)},sendEvent:t.send}}(),function(){const e=window.GarageApp=window.GarageApp||{},a=e.store;let t=null;function r(){const t=a.getSelection();return"stored"===t.kind?(Array.isArray(e.data?.garage?.vehicles)?e.data.garage.vehicles:[]).find(e=>String(e.plate||"")===t.id)||null:"nearby"===t.kind&&(Array.isArray(e.data?.nearby?.vehicles)?e.data.nearby.vehicles:[]).find(e=>String(e.netId||"")===t.id)||null}function s(e,r){a.setNotice({type:e,text:r}),t&&clearTimeout(t),t=setTimeout(()=>{a.setNotice({type:"",text:""}),t=null},3200)}e.actions={showNotice:s,closeGarage:function(){const a=e.bridge;if(a&&"function"==typeof a.requestClose){if(a.requestClose())return!0}return s("error","Garage bridge is unavailable."),!1},refreshGarage:function(){const a=e.bridge;if(a&&"function"==typeof a.requestRefresh){if(a.requestRefresh())return!0}return s("error","Garage refresh bridge is unavailable."),!1},applySearchQuery:function(e){a.setSearchQuery(String(e||"").trim())},clearSearch:function(){a.setSearchQuery("")},selectCategory:function(e){a.setCategoryFilter(String(e||"all").trim()||"all")},selectEntry:function(e,t){a.select(e,t)},getSelectedEntry:r,requestRetrieveSelected:function(){const t=r();if(!t||"stored"!==t.entryKind)return s("error","Select a stored vehicle to retrieve."),!1;if(e.data?.session?.spawnBlocked)return s("error","The garage spawn area is blocked."),!1;const i=e.bridge;return i&&"function"==typeof i.requestRetrieve?(a.startAction("retrieve"),!!i.requestRetrieve({plate:t.plate||""})||(a.finishAction(),s("error","Garage retrieve bridge is unavailable."),!1)):(s("error","Garage retrieve bridge is unavailable."),!1)},requestStoreSelected:function(){const t=r();if(!t||"nearby"!==t.entryKind)return s("error","Select a nearby vehicle to store."),!1;if(!1===t.isEmpty)return s("error","All crew must exit the vehicle before storing it."),!1;const i=e.bridge;return i&&"function"==typeof i.requestStore?(a.startAction("store"),!!i.requestStore({netId:t.netId||""})||(a.finishAction(),s("error","Garage store bridge is unavailable."),!1)):(s("error","Garage store bridge is unavailable."),!1)}}}(),function(){const e=window.GarageApp=window.GarageApp||{},{h:a}=e.runtime,t=window.SharedUI.componentFns.WindowTitleBar,r=e.store,s=e.actions,{categories:i,garage:n,nearby:c,session:l}=e.data;function o(e){return Math.max(0,Math.min(100,Math.round(100*Number(e||0))))}function g(e){const a=i.find(a=>a.id===String(e||"other").toLowerCase());return a?a.label:"Other"}function d(e){return`${Math.round(Number(e||0))} m`}function u(e){return String(e||"").trim()||"Untracked"}function m(e,a){return(e||[]).filter(e=>("all"===a.categoryFilter||String(e.category||"").toLowerCase()===a.categoryFilter)&&function(e,a){const t=String(e||"").trim().toLowerCase();return!t||a.some(e=>String(e||"").toLowerCase().includes(t))}(a.searchQuery,[e.displayName,e.classname,e.plate,e.netId,e.category]))}function p(e,t,r=""){return a("div",{className:r?`garage-stat-card is-${r}`:"garage-stat-card"},a("span",{className:"garage-stat-label"},e),a("span",{className:"garage-stat-value"},t))}function h(e,t,r){return a("div",{className:"garage-meter"},a("div",{className:"garage-meter-label-row"},a("span",{className:"garage-meter-label"},e),a("span",{className:"garage-meter-value"},`${t}%`)),a("div",{className:"garage-meter-track"},a("span",{className:`garage-meter-fill is-${r}`,style:{width:`${t}%`}})))}function y(e,t,r,i,n){return a("section",{className:"garage-card garage-list-card"},a("div",{className:"garage-card-header"},a("div",null,a("span",{className:"garage-eyebrow"},t),a("h2",{className:"garage-section-title"},e)),a("span",{className:"garage-pill"},`${i.length} ${1===i.length?"Vehicle":"Vehicles"}`)),a("div",{className:"garage-card-body garage-scroll-body","data-preserve-scroll-id":r},i.length>0?i.map(e=>function(e,t){const r="stored"===e.entryKind?String(e.plate||""):String(e.netId||""),i="nearby"===e.entryKind;return a("button",{type:"button",className:(n=e,c=t,n&&c&&String(n.entryKind||"")===String(c.entryKind||"")&&String(n.plate||"")===String(c.plate||"")&&String(n.netId||"")===String(c.netId||"")?"garage-vehicle-item is-selected":"garage-vehicle-item"),onClick:()=>s.selectEntry(e.entryKind,r)},a("div",{className:"garage-vehicle-item-head"},a("div",{className:"garage-vehicle-copy"},a("span",{className:"garage-vehicle-title"},e.displayName||e.classname||"Vehicle"),a("span",{className:"garage-vehicle-meta"},i?`Nearby ${d(e.distance)}`:`Plate ${u(e.plate)}`)),a("span",{className:i&&!1===e.isEmpty?"garage-badge is-warning":"garage-badge"},i?!1===e.isEmpty?"Crewed":"Empty":g(e.category))),a("div",{className:"garage-inline-meters"},h("Health",o(e.health),"health"),h("Fuel",o(e.fuel),"fuel")));var n,c}(e,n)):a("div",{className:"garage-empty-state"},a("h3",{className:"garage-empty-title"},"No matching vehicles"),a("p",{className:"garage-empty-copy"},"Adjust the current search or category filter to view more records."))))}function b(e){const t=(Array.isArray(e)?e:[]).slice().sort((e,a)=>Number(a.value||0)-Number(e.value||0)).slice(0,6).filter(e=>Number(e.value||0)>0);return 0===t.length?a("div",{className:"garage-empty-inline"},"No subsystem damage reported."):a("div",{className:"garage-hitpoint-grid"},t.map(e=>{return a("div",{className:"garage-hitpoint-row"},a("div",{className:"garage-hitpoint-copy"},a("span",{className:"garage-hitpoint-name"},(t=e.name,String(t||"").replace(/^Hit/i,"").replace(/([a-z])([A-Z])/g,"$1 $2").replace(/_/g," ").trim()||"Subsystem")),e.selection?a("span",{className:"garage-hitpoint-selection"},e.selection):null),a("span",{className:"garage-hitpoint-value"},`${Math.round(100*Number(e.value||0))}%`));var t}))}e.components=e.components||{},e.components.App=function(){const e={categoryFilter:r.getCategoryFilter(),notice:r.getNotice(),pendingAction:r.getPendingAction(),searchQuery:r.getSearchQuery(),selectedId:r.getSelectedId(),selectedKind:r.getSelectedKind()},v=function(e){return"stored"===e.selectedKind?(n.vehicles||[]).find(a=>String(a.plate||"")===e.selectedId)||null:"nearby"===e.selectedKind&&(c.vehicles||[]).find(a=>String(a.netId||"")===e.selectedId)||null}(e),N=m(n.vehicles||[],e),f=m(c.vehicles||[],e),S=e.searchQuery?`Search: ${e.searchQuery}`:"Live";return a("div",{className:"garage-shell"},t({kicker:"FORGE Logistics",title:"Vehicle Garage",onClose:()=>s.closeGarage(),closeLabel:"Close garage interface"}),e.notice.text?a("div",{className:"garage-toast-stack"},a("div",{className:"error"===e.notice.type?"garage-toast is-error":"garage-toast is-success"},e.notice.text)):null,a("div",{className:"garage-layout"},a("aside",{className:"garage-sidebar"},a("section",{className:"garage-module"},a("div",{className:"garage-module-header"},a("div",null,a("span",{className:"garage-eyebrow"},"Search"),a("h2",{className:"garage-section-title"},"Vehicle Records")),a("span",{className:"garage-pill"},S)),a("div",{className:"garage-search-form"},a("input",{id:"garage-search-input",type:"text",className:"garage-search-input",placeholder:"Search by name, plate, or category",value:e.searchQuery}),a("div",{className:"garage-search-actions"},a("button",{type:"button",className:"garage-btn garage-btn-primary",onClick:()=>s.applySearchQuery(document.getElementById("garage-search-input")?.value||"")},"Apply Search"),a("button",{type:"button",className:"garage-btn garage-btn-secondary",onClick:()=>s.clearSearch()},"Clear")))),a("section",{className:"garage-module"},a("div",{className:"garage-module-header"},a("div",null,a("span",{className:"garage-eyebrow"},"Filter"),a("h2",{className:"garage-section-title"},"Vehicle Categories"))),a("div",{className:"garage-category-grid"},i.map(t=>a("button",{type:"button",className:e.categoryFilter===t.id?"garage-chip is-active":"garage-chip",onClick:()=>s.selectCategory(t.id)},t.label)))),a("section",{className:"garage-module"},a("div",{className:"garage-module-header"},a("div",null,a("span",{className:"garage-eyebrow"},"Status"),a("h2",{className:"garage-section-title"},"Garage Summary")),a("button",{type:"button",className:"garage-btn garage-btn-secondary",disabled:Boolean(e.pendingAction),onClick:()=>s.refreshGarage()},"Refresh")),a("div",{className:"garage-summary-grid"},p("Stored",`${l.capacityUsed}/${l.capacityMax}`),p("Nearby",l.nearbyCount,"accent"),p("Spawn Lane",l.spawnStatus,l.spawnBlocked?"danger":"")))),a("main",{className:"garage-main"},a("section",{className:"garage-panel"},a("div",{className:"garage-panel-header"},a("div",null,a("span",{className:"garage-eyebrow"},"Operations Bay"),a("h1",{className:"garage-title"},l.garageName||"Vehicle Garage")),a("span",{className:"garage-pill"},`${l.capacityUsed}/${l.capacityMax} Stored`)),a("div",{className:"garage-panel-intro"},a("p",{className:"garage-copy"},"Retrieve stored vehicles into the active spawn lane or store nearby empty vehicles back into persistent ownership records.")),a("div",{className:"garage-dashboard"},y("Stored Vehicles","Persistent Records","garage-stored-list",N,v),y("Nearby Vehicles","Store Window","garage-nearby-list",f,v),function(e,t){if(!e)return a("section",{className:"garage-card garage-detail-card"},a("div",{className:"garage-card-header"},a("div",null,a("span",{className:"garage-eyebrow"},"Selection"),a("h2",{className:"garage-section-title"},"Vehicle Detail"))),a("div",{className:"garage-card-body garage-detail-empty"},a("h3",{className:"garage-empty-title"},"Select a vehicle"),a("p",{className:"garage-empty-copy"},"Choose a stored record to retrieve or a nearby vehicle to store.")));const r="stored"===e.entryKind,i=String(t.pendingAction||""),n="retrieve"===i||"store"===i,c=r&&!l.spawnBlocked&&!n,m=!r&&!1!==e.isEmpty&&!n;return a("section",{className:"garage-card garage-detail-card"},a("div",{className:"garage-card-header"},a("div",null,a("span",{className:"garage-eyebrow"},r?"Stored Record":"Nearby Vehicle"),a("h2",{className:"garage-section-title"},e.displayName||e.classname||"Vehicle")),a("span",{className:"nearby"===e.entryKind&&!1===e.isEmpty?"garage-badge is-warning":"garage-badge"},r?`Plate ${u(e.plate)}`:!1===e.isEmpty?"Crewed":"Ready")),a("div",{className:"garage-card-body garage-detail-body"},a("div",{className:"garage-detail-grid"},a("div",{className:"garage-detail-copy"},a("div",{className:"garage-detail-meta"},p("Category",g(e.category)),p("Status",(y=e)?"stored"===y.entryKind?"Stored":!1===y.isEmpty?"Crewed":"Ready":"-","nearby"===e.entryKind&&!1===e.isEmpty?"danger":""),p(r?"Record":"Distance",r?u(e.plate):d(e.distance),r?"":"accent")),a("div",{className:"garage-meter-stack"},h("Health",o(e.health),"health"),h("Fuel",o(e.fuel),"fuel")),a("div",{className:"garage-action-row"},r?a("button",{type:"button",className:"garage-btn garage-btn-primary",disabled:!c,onClick:()=>s.requestRetrieveSelected()},"retrieve"===i?"Retrieving...":"Retrieve Vehicle"):a("button",{type:"button",className:"garage-btn garage-btn-primary",disabled:!m,onClick:()=>s.requestStoreSelected()},"store"===i?"Storing...":"Store Vehicle"),a("button",{type:"button",className:"garage-btn garage-btn-secondary",disabled:n,onClick:()=>s.refreshGarage()},"Refresh")),a("p",{className:"garage-detail-note"},r?l.spawnBlocked?"The garage spawn lane is currently blocked.":"Retrieve this stored vehicle into the active spawn lane.":!1===e.isEmpty?"Only empty nearby vehicles can be stored.":"Store this nearby vehicle back into persistent garage storage.")),a("div",{className:"garage-detail-subsystems"},a("div",{className:"garage-subsystem-header"},a("span",{className:"garage-eyebrow"},"Subsystems"),a("span",{className:"garage-detail-caption"},"Highest damage first")),b(e.hitPoints)))));var y}(v,e))))),a("footer",{className:"garage-footer-bar"},a("div",{className:"garage-footer"},a("div",{className:"garage-footer-block"},a("span",{className:"garage-footer-title"},"Storage Capacity"),a("span",{className:"garage-footer-copy"},`${l.capacityUsed} of ${l.capacityMax} vehicle slot(s) are currently occupied.`)),a("div",{className:"garage-footer-block"},a("span",{className:"garage-footer-title"},"Retrieval Window"),a("span",{className:"garage-footer-copy"},l.spawnBlocked?"Spawn lane is blocked. Clear the bay before retrieving another vehicle.":"Spawn lane is clear. Stored vehicles can be retrieved immediately.")),a("div",{className:"garage-footer-block"},a("span",{className:"garage-footer-title"},"Store Rules"),a("span",{className:"garage-footer-copy"},"Only nearby empty vehicles can be stored. Nearby count updates from the live world state.")))))}}(),function(){const e=window.ForgeWebUI,a=window.GarageApp;e.createApp({name:"garage",root:"#app",setup({root:t}){e.mount(t,()=>a.components.App(),{preserveScroll:!0}),a.bridge&&a.bridge.notifyReady()}}).start()}(); \ No newline at end of file +!function(){const e=window.ForgeWebUI;(window.GarageApp=window.GarageApp||{}).runtime=e,window.AppRuntime=e}(),function(){const e=window.GarageApp=window.GarageApp||{},a={garageName:"Vehicle Garage",capacityUsed:0,capacityMax:5,nearbyCount:0,spawnBlocked:!1,spawnStatus:"Ready"},r={vehicles:[]},t={vehicles:[]};function s(e,a){var r;Object.keys(e).forEach(a=>delete e[a]),Object.assign(e,(r=a,JSON.parse(JSON.stringify(r))))}e.data={categories:[{id:"all",label:"All"},{id:"car",label:"Cars"},{id:"armor",label:"Armor"},{id:"air",label:"Air"},{id:"naval",label:"Naval"},{id:"other",label:"Other"}],session:Object.assign({},a),garage:Object.assign({},r),nearby:Object.assign({},t),applyHydratePayload(e){s(this.session,Object.assign({},a,e?.session||{})),s(this.garage,Object.assign({},r,e?.garage||{})),s(this.nearby,Object.assign({},t,e?.nearby||{}))}}}(),function(){const e=window.GarageApp=window.GarageApp||{},{createSignal:a}=e.runtime;e.store=new class{constructor(){[this.getSelectedKind,this.setSelectedKind]=a(""),[this.getSelectedId,this.setSelectedId]=a(""),[this.getSearchQuery,this.setSearchQuery]=a(""),[this.getCategoryFilter,this.setCategoryFilter]=a("all"),[this.getPendingAction,this.setPendingAction]=a(""),[this.getNotice,this.setNotice]=a({type:"",text:""})}getSelection(){return{id:this.getSelectedId(),kind:this.getSelectedKind()}}clearSelection(){this.setSelectedKind(""),this.setSelectedId("")}select(e,a){this.setSelectedKind(String(e||"")),this.setSelectedId(String(a||""))}startAction(e){this.setPendingAction(String(e||""))}finishAction(){this.setPendingAction("")}matchesSelection(e){if(!e||"object"!=typeof e)return!1;const a=this.getSelection();return!(!a.kind||!a.id)&&("stored"===a.kind?"stored"===e.entryKind&&String(e.plate||"")===a.id:"nearby"===a.kind&&("nearby"===e.entryKind&&String(e.netId||"")===a.id))}ensureSelection(){const a=Array.isArray(e.data?.garage?.vehicles)?e.data.garage.vehicles:[],r=Array.isArray(e.data?.nearby?.vehicles)?e.data.nearby.vehicles:[];if([...a,...r].some(e=>this.matchesSelection(e)))return;const t=a[0]||null;if(t)return void this.select("stored",t.plate||"");const s=r[0]||null;s?this.select("nearby",s.netId||""):this.clearSelection()}hydrateFromPayload(){this.finishAction(),this.ensureSelection()}}}(),function(){const e=window.GarageApp=window.GarageApp||{},a=e.store,r=window.ForgeWebUI.createBridge({closeEvent:"garage::close",globalName:"ForgeBridge",readyEvent:"garage::ready"});function t(r){e.data.applyHydratePayload(r),a.hydrateFromPayload(r)}r.on("garage::hydrate",t),r.on("garage::sync",t),r.on("garage::retrieve::success",r=>{a.finishAction(),e.actions&&e.actions.showNotice("success",r.message||"Vehicle retrieved from the garage.")}),r.on("garage::retrieve::failure",r=>{a.finishAction(),e.actions&&e.actions.showNotice("error",r.message||"Unable to retrieve vehicle.")}),r.on("garage::store::success",r=>{a.finishAction(),e.actions&&e.actions.showNotice("success",r.message||"Vehicle stored in the garage.")}),r.on("garage::store::failure",r=>{a.finishAction(),e.actions&&e.actions.showNotice("error",r.message||"Unable to store vehicle.")}),r.on("garage::service::success",r=>{a.finishAction(),e.actions&&e.actions.showNotice("success",r.message||"Service request sent.")}),r.on("garage::service::failure",r=>{a.finishAction(),e.actions&&e.actions.showNotice("error",r.message||"Unable to service vehicle.")}),e.bridge={notifyReady:function(){return r.ready({loaded:!0})},receive:r.receive,requestClose:function(){return r.close({})},requestRefresh:function(){return r.send("garage::refresh",{})},requestRefuel:function(e){return r.send("garage::vehicle::refuel::request",e)},requestRepair:function(e){return r.send("garage::vehicle::repair::request",e)},requestRetrieve:function(e){return r.send("garage::vehicle::retrieve::request",e)},requestStore:function(e){return r.send("garage::vehicle::store::request",e)},sendEvent:r.send}}(),function(){const e=window.GarageApp=window.GarageApp||{},a=e.store;let r=null;function t(){const r=a.getSelection();return"stored"===r.kind?(Array.isArray(e.data?.garage?.vehicles)?e.data.garage.vehicles:[]).find(e=>String(e.plate||"")===r.id)||null:"nearby"===r.kind&&(Array.isArray(e.data?.nearby?.vehicles)?e.data.nearby.vehicles:[]).find(e=>String(e.netId||"")===r.id)||null}function s(e,t){a.setNotice({type:e,text:t}),r&&clearTimeout(r),r=setTimeout(()=>{a.setNotice({type:"",text:""}),r=null},3200)}e.actions={showNotice:s,closeGarage:function(){const a=e.bridge;if(a&&"function"==typeof a.requestClose){if(a.requestClose())return!0}return s("error","Garage bridge is unavailable."),!1},refreshGarage:function(){const a=e.bridge;if(a&&"function"==typeof a.requestRefresh){if(a.requestRefresh())return!0}return s("error","Garage refresh bridge is unavailable."),!1},applySearchQuery:function(e){a.setSearchQuery(String(e||"").trim())},clearSearch:function(){a.setSearchQuery("")},selectCategory:function(e){a.setCategoryFilter(String(e||"all").trim()||"all")},selectEntry:function(e,r){a.select(e,r)},getSelectedEntry:t,requestRefuelSelected:function(){const r=t();if(!r||"nearby"!==r.entryKind)return s("error","Select a nearby vehicle to refuel."),!1;if(Number(r.fuel||0)>=.999)return s("error","Vehicle fuel tank is already full."),!1;const i=e.bridge;return i&&"function"==typeof i.requestRefuel?(a.startAction("refuel"),!!i.requestRefuel({netId:r.netId||""})||(a.finishAction(),s("error","Garage refuel bridge is unavailable."),!1)):(s("error","Garage refuel bridge is unavailable."),!1)},requestRepairSelected:function(){const r=t();if(!r||"nearby"!==r.entryKind)return s("error","Select a nearby vehicle to repair."),!1;if(Number(r.health||0)>=.999)return s("error","Vehicle has no reported damage."),!1;const i=e.bridge;return i&&"function"==typeof i.requestRepair?(a.startAction("repair"),!!i.requestRepair({netId:r.netId||""})||(a.finishAction(),s("error","Garage repair bridge is unavailable."),!1)):(s("error","Garage repair bridge is unavailable."),!1)},requestRetrieveSelected:function(){const r=t();if(!r||"stored"!==r.entryKind)return s("error","Select a stored vehicle to retrieve."),!1;if(e.data?.session?.spawnBlocked)return s("error","The garage spawn area is blocked."),!1;const i=e.bridge;return i&&"function"==typeof i.requestRetrieve?(a.startAction("retrieve"),!!i.requestRetrieve({plate:r.plate||""})||(a.finishAction(),s("error","Garage retrieve bridge is unavailable."),!1)):(s("error","Garage retrieve bridge is unavailable."),!1)},requestStoreSelected:function(){const r=t();if(!r||"nearby"!==r.entryKind)return s("error","Select a nearby vehicle to store."),!1;if(!1===r.isEmpty)return s("error","All crew must exit the vehicle before storing it."),!1;const i=e.bridge;return i&&"function"==typeof i.requestStore?(a.startAction("store"),!!i.requestStore({netId:r.netId||""})||(a.finishAction(),s("error","Garage store bridge is unavailable."),!1)):(s("error","Garage store bridge is unavailable."),!1)}}}(),function(){const e=window.GarageApp=window.GarageApp||{},{h:a}=e.runtime,r=window.SharedUI.componentFns.WindowTitleBar,t=e.store,s=e.actions,{categories:i,garage:n,nearby:c,session:l}=e.data;function o(e){return Math.max(0,Math.min(100,Math.round(100*Number(e||0))))}function g(e){const a=i.find(a=>a.id===String(e||"other").toLowerCase());return a?a.label:"Other"}function d(e){return`${Math.round(Number(e||0))} m`}function u(e){return String(e||"").trim()||"Untracked"}function p(e,a){return(e||[]).filter(e=>("all"===a.categoryFilter||String(e.category||"").toLowerCase()===a.categoryFilter)&&function(e,a){const r=String(e||"").trim().toLowerCase();return!r||a.some(e=>String(e||"").toLowerCase().includes(r))}(a.searchQuery,[e.displayName,e.classname,e.plate,e.netId,e.category]))}function m(e,r,t=""){return a("div",{className:t?`garage-stat-card is-${t}`:"garage-stat-card"},a("span",{className:"garage-stat-label"},e),a("span",{className:"garage-stat-value"},r))}function h(e,r,t){return a("div",{className:"garage-meter"},a("div",{className:"garage-meter-label-row"},a("span",{className:"garage-meter-label"},e),a("span",{className:"garage-meter-value"},`${r}%`)),a("div",{className:"garage-meter-track"},a("span",{className:`garage-meter-fill is-${t}`,style:{width:`${r}%`}})))}function y(e,r,t,i,n){return a("section",{className:"garage-card garage-list-card"},a("div",{className:"garage-card-header"},a("div",null,a("span",{className:"garage-eyebrow"},r),a("h2",{className:"garage-section-title"},e)),a("span",{className:"garage-pill"},`${i.length} ${1===i.length?"Vehicle":"Vehicles"}`)),a("div",{className:"garage-card-body garage-scroll-body","data-preserve-scroll-id":t},i.length>0?i.map(e=>function(e,r){const t="stored"===e.entryKind?String(e.plate||""):String(e.netId||""),i="nearby"===e.entryKind;return a("button",{type:"button",className:(n=e,c=r,n&&c&&String(n.entryKind||"")===String(c.entryKind||"")&&String(n.plate||"")===String(c.plate||"")&&String(n.netId||"")===String(c.netId||"")?"garage-vehicle-item is-selected":"garage-vehicle-item"),onClick:()=>s.selectEntry(e.entryKind,t)},a("div",{className:"garage-vehicle-item-head"},a("div",{className:"garage-vehicle-copy"},a("span",{className:"garage-vehicle-title"},e.displayName||e.classname||"Vehicle"),a("span",{className:"garage-vehicle-meta"},i?`Nearby ${d(e.distance)}`:`Plate ${u(e.plate)}`)),a("span",{className:i&&!1===e.isEmpty?"garage-badge is-warning":"garage-badge"},i?!1===e.isEmpty?"Crewed":"Empty":g(e.category))),a("div",{className:"garage-inline-meters"},h("Health",o(e.health),"health"),h("Fuel",o(e.fuel),"fuel")));var n,c}(e,n)):a("div",{className:"garage-empty-state"},a("h3",{className:"garage-empty-title"},"No matching vehicles"),a("p",{className:"garage-empty-copy"},"Adjust the current search or category filter to view more records."))))}function b(e){const r=(Array.isArray(e)?e:[]).slice().sort((e,a)=>Number(a.value||0)-Number(e.value||0)).slice(0,6).filter(e=>Number(e.value||0)>0);return 0===r.length?a("div",{className:"garage-empty-inline"},"No subsystem damage reported."):a("div",{className:"garage-hitpoint-grid"},r.map(e=>{return a("div",{className:"garage-hitpoint-row"},a("div",{className:"garage-hitpoint-copy"},a("span",{className:"garage-hitpoint-name"},(r=e.name,String(r||"").replace(/^Hit/i,"").replace(/([a-z])([A-Z])/g,"$1 $2").replace(/_/g," ").trim()||"Subsystem")),e.selection?a("span",{className:"garage-hitpoint-selection"},e.selection):null),a("span",{className:"garage-hitpoint-value"},`${Math.round(100*Number(e.value||0))}%`));var r}))}e.components=e.components||{},e.components.App=function(){const e={categoryFilter:t.getCategoryFilter(),notice:t.getNotice(),pendingAction:t.getPendingAction(),searchQuery:t.getSearchQuery(),selectedId:t.getSelectedId(),selectedKind:t.getSelectedKind()},v=function(e){return"stored"===e.selectedKind?(n.vehicles||[]).find(a=>String(a.plate||"")===e.selectedId)||null:"nearby"===e.selectedKind&&(c.vehicles||[]).find(a=>String(a.netId||"")===e.selectedId)||null}(e),f=p(n.vehicles||[],e),N=p(c.vehicles||[],e),S=e.searchQuery?`Search: ${e.searchQuery}`:"Live";return a("div",{className:"garage-shell"},r({kicker:"FORGE Logistics",title:"Vehicle Garage",onClose:()=>s.closeGarage(),closeLabel:"Close garage interface"}),e.notice.text?a("div",{className:"garage-toast-stack"},a("div",{className:"error"===e.notice.type?"garage-toast is-error":"garage-toast is-success"},e.notice.text)):null,a("div",{className:"garage-layout"},a("aside",{className:"garage-sidebar"},a("section",{className:"garage-module"},a("div",{className:"garage-module-header"},a("div",null,a("span",{className:"garage-eyebrow"},"Search"),a("h2",{className:"garage-section-title"},"Vehicle Records")),a("span",{className:"garage-pill"},S)),a("div",{className:"garage-search-form"},a("input",{id:"garage-search-input",type:"text",className:"garage-search-input",placeholder:"Search by name, plate, or category",value:e.searchQuery}),a("div",{className:"garage-search-actions"},a("button",{type:"button",className:"garage-btn garage-btn-primary",onClick:()=>s.applySearchQuery(document.getElementById("garage-search-input")?.value||"")},"Apply Search"),a("button",{type:"button",className:"garage-btn garage-btn-secondary",onClick:()=>s.clearSearch()},"Clear")))),a("section",{className:"garage-module"},a("div",{className:"garage-module-header"},a("div",null,a("span",{className:"garage-eyebrow"},"Filter"),a("h2",{className:"garage-section-title"},"Vehicle Categories"))),a("div",{className:"garage-category-grid"},i.map(r=>a("button",{type:"button",className:e.categoryFilter===r.id?"garage-chip is-active":"garage-chip",onClick:()=>s.selectCategory(r.id)},r.label)))),a("section",{className:"garage-module"},a("div",{className:"garage-module-header"},a("div",null,a("span",{className:"garage-eyebrow"},"Status"),a("h2",{className:"garage-section-title"},"Garage Summary")),a("button",{type:"button",className:"garage-btn garage-btn-secondary",disabled:Boolean(e.pendingAction),onClick:()=>s.refreshGarage()},"Refresh")),a("div",{className:"garage-summary-grid"},m("Stored",`${l.capacityUsed}/${l.capacityMax}`),m("Nearby",l.nearbyCount,"accent"),m("Spawn Lane",l.spawnStatus,l.spawnBlocked?"danger":"")))),a("main",{className:"garage-main"},a("section",{className:"garage-panel"},a("div",{className:"garage-panel-header"},a("div",null,a("span",{className:"garage-eyebrow"},"Operations Bay"),a("h1",{className:"garage-title"},l.garageName||"Vehicle Garage")),a("span",{className:"garage-pill"},`${l.capacityUsed}/${l.capacityMax} Stored`)),a("div",{className:"garage-panel-intro"},a("p",{className:"garage-copy"},"Retrieve stored vehicles into the active spawn lane or store nearby empty vehicles back into persistent ownership records.")),a("div",{className:"garage-dashboard"},y("Stored Vehicles","Persistent Records","garage-stored-list",f,v),y("Nearby Vehicles","Store Window","garage-nearby-list",N,v),function(e,r){if(!e)return a("section",{className:"garage-card garage-detail-card"},a("div",{className:"garage-card-header"},a("div",null,a("span",{className:"garage-eyebrow"},"Selection"),a("h2",{className:"garage-section-title"},"Vehicle Detail"))),a("div",{className:"garage-card-body garage-detail-empty"},a("h3",{className:"garage-empty-title"},"Select a vehicle"),a("p",{className:"garage-empty-copy"},"Choose a stored record to retrieve or a nearby vehicle to store.")));const t="stored"===e.entryKind,i=String(r.pendingAction||""),n=Boolean(i),c=t&&!l.spawnBlocked&&!n,p=!t&&!1!==e.isEmpty&&!n,y=!t&&Number(e.fuel||0)<.999&&!n,v=!t&&Number(e.health||0)<.999&&!n;return a("section",{className:"garage-card garage-detail-card"},a("div",{className:"garage-card-header"},a("div",null,a("span",{className:"garage-eyebrow"},t?"Stored Record":"Nearby Vehicle"),a("h2",{className:"garage-section-title"},e.displayName||e.classname||"Vehicle")),a("span",{className:"nearby"===e.entryKind&&!1===e.isEmpty?"garage-badge is-warning":"garage-badge"},t?`Plate ${u(e.plate)}`:!1===e.isEmpty?"Crewed":"Ready")),a("div",{className:"garage-card-body garage-detail-body"},a("div",{className:"garage-detail-grid"},a("div",{className:"garage-detail-copy"},a("div",{className:"garage-detail-meta"},m("Category",g(e.category)),m("Status",(f=e)?"stored"===f.entryKind?"Stored":!1===f.isEmpty?"Crewed":"Ready":"-","nearby"===e.entryKind&&!1===e.isEmpty?"danger":""),m(t?"Record":"Distance",t?u(e.plate):d(e.distance),t?"":"accent")),a("div",{className:"garage-meter-stack"},h("Health",o(e.health),"health"),h("Fuel",o(e.fuel),"fuel")),a("div",{className:"garage-action-row"},t?a("button",{type:"button",className:"garage-btn garage-btn-primary",disabled:!c,onClick:()=>s.requestRetrieveSelected()},"retrieve"===i?"Retrieving...":"Retrieve Vehicle"):a("button",{type:"button",className:"garage-btn garage-btn-primary",disabled:!p,onClick:()=>s.requestStoreSelected()},"store"===i?"Storing...":"Store Vehicle"),a("button",{type:"button",className:"garage-btn garage-btn-secondary",disabled:!y,onClick:()=>s.requestRefuelSelected()},"refuel"===i?"Refueling...":"Refuel"),a("button",{type:"button",className:"garage-btn garage-btn-secondary",disabled:!v,onClick:()=>s.requestRepairSelected()},"repair"===i?"Repairing...":"Repair"),a("button",{type:"button",className:"garage-btn garage-btn-secondary",disabled:n,onClick:()=>s.refreshGarage()},"Refresh")),a("p",{className:"garage-detail-note"},t?l.spawnBlocked?"The garage spawn lane is currently blocked.":"Retrieve this stored vehicle into the active spawn lane before refuel or repair service.":!1===e.isEmpty?"Only empty nearby vehicles can be stored.":"Store this nearby vehicle or request organization-billed refuel and repair service.")),a("div",{className:"garage-detail-subsystems"},a("div",{className:"garage-subsystem-header"},a("span",{className:"garage-eyebrow"},"Subsystems"),a("span",{className:"garage-detail-caption"},"Highest damage first")),b(e.hitPoints)))));var f}(v,e))))),a("footer",{className:"garage-footer-bar"},a("div",{className:"garage-footer"},a("div",{className:"garage-footer-block"},a("span",{className:"garage-footer-title"},"Storage Capacity"),a("span",{className:"garage-footer-copy"},`${l.capacityUsed} of ${l.capacityMax} vehicle slot(s) are currently occupied.`)),a("div",{className:"garage-footer-block"},a("span",{className:"garage-footer-title"},"Retrieval Window"),a("span",{className:"garage-footer-copy"},l.spawnBlocked?"Spawn lane is blocked. Clear the bay before retrieving another vehicle.":"Spawn lane is clear. Stored vehicles can be retrieved immediately.")),a("div",{className:"garage-footer-block"},a("span",{className:"garage-footer-title"},"Store Rules"),a("span",{className:"garage-footer-copy"},"Only nearby empty vehicles can be stored. Nearby count updates from the live world state.")))))}}(),function(){const e=window.ForgeWebUI,a=window.GarageApp;e.createApp({name:"garage",root:"#app",setup({root:r}){e.mount(r,()=>a.components.App(),{preserveScroll:!0}),a.bridge&&a.bridge.notifyReady()}}).start()}(); \ No newline at end of file diff --git a/arma/client/addons/garage/ui/src/bridge.js b/arma/client/addons/garage/ui/src/bridge.js index c86b282..0e1c9f8 100644 --- a/arma/client/addons/garage/ui/src/bridge.js +++ b/arma/client/addons/garage/ui/src/bridge.js @@ -23,6 +23,14 @@ return bridge.send("garage::vehicle::store::request", payload); } + function requestRefuel(payload) { + return bridge.send("garage::vehicle::refuel::request", payload); + } + + function requestRepair(payload) { + return bridge.send("garage::vehicle::repair::request", payload); + } + function notifyReady() { return bridge.ready({ loaded: true }); } @@ -75,11 +83,33 @@ } }); + bridge.on("garage::service::success", (payloadData) => { + store.finishAction(); + if (GarageApp.actions) { + GarageApp.actions.showNotice( + "success", + payloadData.message || "Service request sent.", + ); + } + }); + + bridge.on("garage::service::failure", (payloadData) => { + store.finishAction(); + if (GarageApp.actions) { + GarageApp.actions.showNotice( + "error", + payloadData.message || "Unable to service vehicle.", + ); + } + }); + GarageApp.bridge = { notifyReady, receive: bridge.receive, requestClose, requestRefresh, + requestRefuel, + requestRepair, requestRetrieve, requestStore, sendEvent: bridge.send, diff --git a/arma/client/addons/garage/ui/src/components/AppShell.js b/arma/client/addons/garage/ui/src/components/AppShell.js index 6d00c24..ed57875 100644 --- a/arma/client/addons/garage/ui/src/components/AppShell.js +++ b/arma/client/addons/garage/ui/src/components/AppShell.js @@ -343,11 +343,16 @@ const isStored = currentSelection.entryKind === "stored"; const pendingAction = String(state.pendingAction || ""); - const isBusy = - pendingAction === "retrieve" || pendingAction === "store"; + const isBusy = Boolean(pendingAction); const canRetrieve = isStored && !session.spawnBlocked && !isBusy; const canStore = !isStored && currentSelection.isEmpty !== false && !isBusy; + const canRefuel = + !isStored && Number(currentSelection.fuel || 0) < 0.999 && !isBusy; + const canRepair = + !isStored && + Number(currentSelection.health || 0) < 0.999 && + !isBusy; return h( "section", @@ -461,6 +466,34 @@ ? "Storing..." : "Store Vehicle", ), + h( + "button", + { + type: "button", + className: + "garage-btn garage-btn-secondary", + disabled: !canRefuel, + onClick: () => + actions.requestRefuelSelected(), + }, + pendingAction === "refuel" + ? "Refueling..." + : "Refuel", + ), + h( + "button", + { + type: "button", + className: + "garage-btn garage-btn-secondary", + disabled: !canRepair, + onClick: () => + actions.requestRepairSelected(), + }, + pendingAction === "repair" + ? "Repairing..." + : "Repair", + ), h( "button", { @@ -479,10 +512,10 @@ isStored ? session.spawnBlocked ? "The garage spawn lane is currently blocked." - : "Retrieve this stored vehicle into the active spawn lane." + : "Retrieve this stored vehicle into the active spawn lane before refuel or repair service." : currentSelection.isEmpty === false ? "Only empty nearby vehicles can be stored." - : "Store this nearby vehicle back into persistent garage storage.", + : "Store this nearby vehicle or request organization-billed refuel and repair service.", ), ), h( diff --git a/arma/client/addons/garage/ui/src/registry/events.js b/arma/client/addons/garage/ui/src/registry/events.js index 3ca41d3..07e47bc 100644 --- a/arma/client/addons/garage/ui/src/registry/events.js +++ b/arma/client/addons/garage/ui/src/registry/events.js @@ -159,6 +159,70 @@ return true; } + function requestRefuelSelected() { + const selectedEntry = getSelectedEntry(); + if (!selectedEntry || selectedEntry.entryKind !== "nearby") { + showNotice("error", "Select a nearby vehicle to refuel."); + return false; + } + + if (Number(selectedEntry.fuel || 0) >= 0.999) { + showNotice("error", "Vehicle fuel tank is already full."); + return false; + } + + const bridge = GarageApp.bridge; + if (!bridge || typeof bridge.requestRefuel !== "function") { + showNotice("error", "Garage refuel bridge is unavailable."); + return false; + } + + store.startAction("refuel"); + const sent = bridge.requestRefuel({ + netId: selectedEntry.netId || "", + }); + + if (!sent) { + store.finishAction(); + showNotice("error", "Garage refuel bridge is unavailable."); + return false; + } + + return true; + } + + function requestRepairSelected() { + const selectedEntry = getSelectedEntry(); + if (!selectedEntry || selectedEntry.entryKind !== "nearby") { + showNotice("error", "Select a nearby vehicle to repair."); + return false; + } + + if (Number(selectedEntry.health || 0) >= 0.999) { + showNotice("error", "Vehicle has no reported damage."); + return false; + } + + const bridge = GarageApp.bridge; + if (!bridge || typeof bridge.requestRepair !== "function") { + showNotice("error", "Garage repair bridge is unavailable."); + return false; + } + + store.startAction("repair"); + const sent = bridge.requestRepair({ + netId: selectedEntry.netId || "", + }); + + if (!sent) { + store.finishAction(); + showNotice("error", "Garage repair bridge is unavailable."); + return false; + } + + return true; + } + GarageApp.actions = { showNotice, closeGarage, @@ -168,6 +232,8 @@ selectCategory, selectEntry, getSelectedEntry, + requestRefuelSelected, + requestRepairSelected, requestRetrieveSelected, requestStoreSelected, }; diff --git a/arma/server/addons/economy/README.md b/arma/server/addons/economy/README.md index f2e567c..988ca10 100644 --- a/arma/server/addons/economy/README.md +++ b/arma/server/addons/economy/README.md @@ -30,9 +30,10 @@ charges such as repairs. shared org-charge helper can also record member debt for medical fallback. ## Event Surface -The addon registers CBA server events for fuel start/tick/stop, repair service, -player killed, player respawn, and healing. Medical store initialization runs -after post-init to discover configured medical spawn objects. +The addon registers CBA server events for fuel start/tick/stop, direct refuel +service, repair service, player killed, player respawn, and healing. Medical +store initialization runs after post-init to discover configured medical spawn +objects. Repair service requests use: @@ -42,6 +43,14 @@ Repair service requests use: `_cost` is optional. Passing `-1` uses the configured service repair cost. +Garage refuel service requests use: + +```sqf +[QEGVAR(economy,RefuelService), [_target, _unit]] call CBA_fnc_serverEvent; +``` + +This fills the selected live vehicle after organization billing succeeds. + ## Billing Rules Economy does not own durable money state. It coordinates Arma-world effects after the relevant hot-cache charge succeeds. @@ -56,6 +65,10 @@ Fuel and repair services are organization-funded: 5. If the charge fails, do not complete the service. Refueling rolls the target back to its starting fuel level; repairs are not applied. +Direct refuel service requests, such as those from the garage UI, calculate +the missing fuel from `fuelCapacity`, charge the organization, and fill the +vehicle only after the charge succeeds. + Medical services are player-funded first: 1. Load the player's bank hot state. diff --git a/arma/server/addons/economy/XEH_preInit.sqf b/arma/server/addons/economy/XEH_preInit.sqf index 7b5bd48..34e561b 100644 --- a/arma/server/addons/economy/XEH_preInit.sqf +++ b/arma/server/addons/economy/XEH_preInit.sqf @@ -33,6 +33,11 @@ if (isNil QGVAR(SEconomyStore)) then { call FUNC(initSEconomyStore); }; GVAR(SEconomyStore) call ["repair", [_target, _unit, _cost]]; }] call CFUNC(addEventHandler); +[QGVAR(RefuelService), { + params ["_target", "_unit"]; + GVAR(FEconomyStore) call ["refuel", [_target, _unit]]; +}] call CFUNC(addEventHandler); + [QGVAR(onKilled), { params ["_unit"]; GVAR(MEconomyStore) call ["onKilled", [_unit]]; diff --git a/arma/server/addons/economy/functions/fnc_initFEconomyStore.sqf b/arma/server/addons/economy/functions/fnc_initFEconomyStore.sqf index eb820f9..0806220 100644 --- a/arma/server/addons/economy/functions/fnc_initFEconomyStore.sqf +++ b/arma/server/addons/economy/functions/fnc_initFEconomyStore.sqf @@ -4,13 +4,14 @@ * File: fnc_initFEconomyStore.sqf * Author: IDSolutions * Date: 2025-12-20 - * Last Update: 2026-01-03 + * Last Update: 2026-04-18 * Public: No * * Description: * Initializes the fuel economy store. Active refueling sessions remain * server-local; payment is routed through the organization extension hot - * cache. + * cache. Garage service refuels use the same organization billing path + * and only fill the vehicle after the charge succeeds. * * Parameter(s): * N/A @@ -53,6 +54,43 @@ GVAR(FEconomyStore) = createHashMapObject [[ SETVAR(_target,liters,0); true }], + ["refuel", { + params [["_target", objNull, [objNull]], ["_unit", objNull, [objNull]]]; + + if (isNull _target || { isNull _unit }) exitWith { false }; + + private _currentFuel = fuel _target; + private _missingFuel = (1 - _currentFuel) max 0 min 1; + if (_missingFuel <= 0.001) exitWith { + [CRPC(notifications,recieveNotification), ["info", "Refueling", "Vehicle fuel tank is already full."], _unit] call CFUNC(targetEvent); + false + }; + + if (isNil QGVAR(SEconomyStore)) exitWith { + ["ERROR", "Service economy store unavailable for garage refueling charge.", nil, nil] call EFUNC(common,log); + [CRPC(notifications,recieveNotification), ["danger", "Refueling", "Organization billing is unavailable. Refueling was not completed."], _unit] call CFUNC(targetEvent); + false + }; + + private _fuelCapacity = getNumber (configOf _target >> "fuelCapacity"); + if (_fuelCapacity <= 0) then { _fuelCapacity = 100; }; + + private _totalLiters = _missingFuel * _fuelCapacity; + private _totalCost = _totalLiters * GVAR(FuelCost); + private _chargeResult = GVAR(SEconomyStore) call ["chargeOrg", [_unit, _totalCost, "Refueling"]]; + if !(_chargeResult getOrDefault ["success", false]) exitWith { + [CRPC(notifications,recieveNotification), ["danger", "Refueling", _chargeResult getOrDefault ["message", "Organization funds cannot cover this refuel. Refueling was not completed."]], _unit] call CFUNC(targetEvent); + false + }; + + _target setFuel 1; + SETVAR(_target,liters,0); + + private _formattedTotalCost = [_totalCost] call EFUNC(common,formatNumber); + private _formattedTotalLiters = _totalLiters toFixed 2; + [CRPC(notifications,recieveNotification), ["info", "Refueling", format ["Refueling complete: %1L
Organization charged $%2.", _formattedTotalLiters, _formattedTotalCost]], _unit] call CFUNC(targetEvent); + true + }], ["stop", { params ["_source", "_target"]; diff --git a/docs/CLIENT_GARAGE_USAGE_GUIDE.md b/docs/CLIENT_GARAGE_USAGE_GUIDE.md index 991c34e..c670ff0 100644 --- a/docs/CLIENT_GARAGE_USAGE_GUIDE.md +++ b/docs/CLIENT_GARAGE_USAGE_GUIDE.md @@ -1,7 +1,8 @@ # Client Garage Usage Guide The client garage addon provides player vehicle storage UI, vehicle -store/retrieve actions, vehicle context building, and the virtual garage view. +store/retrieve actions, selected nearby vehicle service requests, vehicle +context building, and the virtual garage view. ## Open Garage UI @@ -31,7 +32,7 @@ available vehicle lists from the virtual garage repository. | `GarageHelperService` | Vehicle names, hit points, and payload helpers. | | `GarageContextService` | Nearby/current vehicle context. | | `GaragePayloadService` | Browser hydrate payload construction. | -| `GarageActionService` | Store/retrieve request handling. | +| `GarageActionService` | Store/retrieve request handling and selected nearby vehicle refuel/repair request forwarding. | | `GarageUIBridge` | Browser ready, hydrate, and sync delivery. | ## Browser Events @@ -42,6 +43,8 @@ available vehicle lists from the virtual garage repository. | `garage::refresh` | Send current garage payload as `garage::sync`. | | `garage::vehicle::retrieve::request` | Forward retrieve request through the action service. | | `garage::vehicle::store::request` | Forward store request through the action service. | +| `garage::vehicle::refuel::request` | Forward selected nearby vehicle refuel request to the server economy service. | +| `garage::vehicle::repair::request` | Forward selected nearby vehicle repair request to the server economy service. | | `garage::close` | Dispose bridge screen state and close the display. | ## Browser Response Events @@ -50,10 +53,22 @@ available vehicle lists from the virtual garage repository. | --- | --- | | `garage::hydrate` | Initial vehicle and session payload. | | `garage::sync` | Refreshed vehicle payload. | +| `garage::service::success` | Browser notice for accepted refuel/repair requests. | +| `garage::service::failure` | Browser notice for rejected refuel/repair requests. | Server action responses are handled by the action service and notification flow. +## Vehicle Service + +The selected vehicle detail panel includes refuel and repair actions for nearby +world vehicles. Stored records must be retrieved first because server economy +services operate on live vehicle objects, not stored garage records. + +Refuel requests use the server economy `RefuelService` event. Repair requests +use the server economy `RepairService` event. Both services are billed by the +server economy addon through organization funds. + ## Mission Setup Garage interactions are normally surfaced through the actor menu when nearby diff --git a/docs/ECONOMY_USAGE_GUIDE.md b/docs/ECONOMY_USAGE_GUIDE.md index 3552e15..88b6181 100644 --- a/docs/ECONOMY_USAGE_GUIDE.md +++ b/docs/ECONOMY_USAGE_GUIDE.md @@ -24,6 +24,11 @@ syncs the organization patch to online members. If organization funds cannot cover the refuel, the vehicle is rolled back to the fuel level it had when the session started. +Garage UI refuel requests use the server `RefuelService` event. The fuel store +calculates missing fuel from the vehicle config `fuelCapacity`, charges the +player's organization, and fills the vehicle only after the organization charge +succeeds. + ## Repair Repair is organization-funded. @@ -37,6 +42,9 @@ Use the repair service event: `_cost` is optional. Passing `-1` uses the configured service repair cost. The target is only repaired after the organization charge succeeds. +The client garage UI forwards selected nearby vehicle repair requests through +the same event. + ## Medical Medical is player-funded first. -- 2.47.2