Updated Docs #9

Closed
J.Schmidt92 wants to merge 8 commits from master into feature/surrealdb-storage
48 changed files with 2648 additions and 244 deletions

View File

@ -1,30 +1,46 @@
<h1 align="center">Forge Client</h1>
<p align="center">
<a href="https://gitea.innovativedevsolutions.org/IDSolutions/forge/releases/latest"><img src="https://img.shields.io/gitea/v/release/IDSolutions/forge?gitea_url=https%3A%2F%2Fgitea.innovativedevsolutions.org&label=Version" alt="Version"></a>
<a href="https://gitea.innovativedevsolutions.org/IDSolutions/forge/issues"><img src="https://img.shields.io/gitea/issues/open/IDSolutions/forge?gitea_url=https%3A%2F%2Fgitea.innovativedevsolutions.org&label=Issues" alt="Issues"></a>
<!-- <a href="https://steamcommunity.com/sharedfiles/filedetails/?id=MOD_ID"><img src="https://img.shields.io/steam/downloads/MOD_ID.svg?&label=Downloads" alt="Downloads"></a> -->
<a href="https://gitea.innovativedevsolutions.org/IDSolutions/forge/src/branch/master/arma/server/LICENSE.md"><img src="https://img.shields.io/badge/License-APL%20SA-red?label=License" alt="License"></a>
<br>
<img src="https://img.shields.io/github/v/release/brettmayson/hemtt?label=HEMTT" alt="HEMTT">
<img src="https://img.shields.io/github/v/release/cbateam/cba_a3?label=CBA%20A3" alt="CBA A3">
</p>
# Forge Client
<p align="center">
<b>Requires the latest version of <a href="https://github.com/CBATeam/CBA_A3/releases/latest">CBA A3</a></b>
</p>
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).

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,43 @@
# forge_client_garage
# Forge Client Garage
Description for this addon
## Overview
The garage addon provides player vehicle storage UI, vehicle store/retrieve
actions, selected nearby vehicle service requests, 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, 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.
## Browser Events
- `garage::ready`
- `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.

View File

@ -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", []];

View File

@ -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]]];

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

@ -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,
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,54 +1,47 @@
<!-- If you want to make changes to this README, you need to also modify the README.md in the docs folder as well -->
# Forge Client Documentation
<h1 align="center">forge-client</h1>
<p align="center">
<a href="https://github.com/IDSolutions/MOD_REPO/releases/latest">
<img src="https://img.shields.io/badge/Version-0.0.0-blue?style=flat-square" alt="forge-client Version">
</a>
<a href="https://github.com/IDSolutions/MOD_REPO/issues">
<img src="https://img.shields.io/github/issues-raw/IDSolutions/MOD_REPO.svg?style=flat-square&label=Issues" alt="forge-client Issues">
</a>
<a href="https://steamcommunity.com/sharedfiles/filedetails/?id=MOD_ID">
<img src="https://img.shields.io/steam/downloads/MOD_ID.svg?style=flat-square&label=Downloads" alt="forge-client Downloads">
</a>
<a href="https://github.com/IDSolutions/MOD_REPO/blob/master/LICENSE">
<img src="https://img.shields.io/badge/License-APL ND-red?style=flat-square" alt="forge-client License">
</a>
<br>
<img src="https://img.shields.io/github/actions/workflow/status/IDSolutions/MOD_REPO/check.yml?style=flat-square&label=HEMTT" alt="HEMTT">
</p>
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.
<p align="center">
<b>Requires the latest version of <a href="https://github.com/CBATeam/CBA_A3/releases/latest">CBA A3</a></b>
</p>
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)

View File

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

View File

@ -2,30 +2,93 @@
## 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, 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:
```sqf
[QEGVAR(economy,RepairService), [_target, _unit, _cost]] call CBA_fnc_serverEvent;
```
`_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.
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.
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.
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.

View File

@ -1,2 +1,3 @@
PREP(initFEconomyStore);
PREP(initMEconomyStore);
PREP(initSEconomyStore);

View File

@ -1,3 +1,5 @@
#include "script_component.hpp"
if !(isNil QGVAR(MEconomyStore)) then {
GVAR(MEconomyStore) call ["init", []];
};

View File

@ -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,16 @@ 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(RefuelService), {
params ["_target", "_unit"];
GVAR(FEconomyStore) call ["refuel", [_target, _unit]];
}] call CFUNC(addEventHandler);
[QGVAR(onKilled), {
params ["_unit"];
GVAR(MEconomyStore) call ["onKilled", [_unit]];

View File

@ -8,7 +8,8 @@ class CfgPatches {
name = COMPONENT_NAME;
requiredVersion = REQUIRED_VERSION;
requiredAddons[] = {
"forge_server_main"
"forge_server_main",
"forge_server_common"
};
units[] = {};
weapons[] = {};

View File

@ -4,20 +4,23 @@
* File: fnc_initFEconomyStore.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 fuel economy store. Active refueling sessions remain
* server-local; payment is routed through the organization extension hot
* cache. Garage service refuels use the same organization billing path
* and only fill the vehicle after the charge succeeds.
*
* 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 +39,103 @@ 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
}],
["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<br />Organization charged $%2.", _formattedTotalLiters, _formattedTotalCost]], _unit] call CFUNC(targetEvent);
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<br />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<br />Organization charged $%2.", _formattedTotalLiters, _formattedTotalCost]], _player] call CFUNC(targetEvent);
_fuelRegistry deleteAt _index;
}]
]];

View File

@ -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", {

View File

@ -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", {}]
]];

View File

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

View File

@ -63,8 +63,105 @@ 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. 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. 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.
### 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 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. 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,
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.
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;
@ -79,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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,95 @@
# Client Garage Usage Guide
The client garage addon provides player vehicle storage UI, vehicle
store/retrieve actions, selected nearby vehicle service requests, 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 and selected nearby vehicle refuel/repair request forwarding. |
| `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::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
| Event | Purpose |
| --- | --- |
| `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
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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

125
docs/CLIENT_USAGE_GUIDE.md Normal file
View File

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

View File

@ -0,0 +1,77 @@
# 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.
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.
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.
The client garage UI forwards selected nearby vehicle repair requests through
the same event.
## 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.

View File

@ -33,10 +33,11 @@ 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),
[Economy](./ECONOMY_USAGE_GUIDE.md),
[Garage](./GARAGE_USAGE_GUIDE.md),
[Locker](./LOCKER_USAGE_GUIDE.md),
[Organization](./ORG_USAGE_GUIDE.md),
@ -45,6 +46,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 |
@ -52,7 +67,7 @@ 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` |
@ -89,6 +104,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 +116,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 +146,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 +158,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 +172,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 +185,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 +196,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 |

View File

@ -14,11 +14,12 @@ 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)
- [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)
@ -27,6 +28,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)

View File

@ -128,6 +128,83 @@ 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. 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. 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.
## 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 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. 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
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,8 +232,12 @@ 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:
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
[
@ -167,9 +248,12 @@ 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.
`default` org. Create the BIS task separately if this task should appear in the
map task tab.
## Timer Semantics

View File

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

View File

@ -793,24 +793,69 @@ impl<R: OrgRepository, H: OrgHotRepository> OrgHotStateService<R, H> {
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<Option<Org>, 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<bool, String> {
Ok(false)
}
fn add_member(&self, _org_id: &str, _member_uid: &str) -> Result<(), String> {
Ok(())
}
fn get_members(&self, _org_id: &str) -> Result<Vec<MemberSummary>, 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<HashMap<String, HashMap<String, OrgAssetEntry>>, String> {
Ok(HashMap::new())
}
fn update_assets(
&self,
_org_id: &str,
_assets: &HashMap<String, HashMap<String, OrgAssetEntry>>,
) -> Result<(), String> {
Ok(())
}
fn get_fleet(&self, _org_id: &str) -> Result<HashMap<String, OrgFleetEntry>, String> {
Ok(HashMap::new())
}
fn update_fleet(
&self,
_org_id: &str,
_fleet: &HashMap<String, OrgFleetEntry>,
) -> 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<TestOrgRepository, InMemoryOrgHotRepository> {
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"));
}
}