Updated Docs #9

Closed
J.Schmidt92 wants to merge 8 commits from master into feature/surrealdb-storage
12 changed files with 323 additions and 17 deletions
Showing only changes of commit ee7d1603ef - Show all commits

View File

@ -2,7 +2,8 @@
## Overview
The garage addon provides player vehicle storage UI, vehicle store/retrieve
actions, and virtual garage state on the client.
actions, selected nearby vehicle service requests, and virtual garage state on
the client.
## Dependencies
- `forge_client_common`
@ -17,8 +18,8 @@ actions, and virtual garage state on the client.
details.
- `fnc_initContextService.sqf` gathers nearby/current vehicle context.
- `fnc_initPayloadService.sqf` builds browser hydrate payloads.
- `fnc_initActionService.sqf` sends store/retrieve requests and handles action
responses.
- `fnc_initActionService.sqf` sends store/retrieve requests, forwards selected
nearby vehicle refuel/repair service requests, and handles action responses.
- `fnc_initUIBridge.sqf` pushes hydrate/sync events to the browser.
- `fnc_openUI.sqf` opens `RscGarage`.
- `fnc_openVG.sqf` opens the Arma garage-style virtual garage view.
@ -28,8 +29,15 @@ actions, and virtual garage state on the client.
- `garage::refresh`
- `garage::vehicle::retrieve::request`
- `garage::vehicle::store::request`
- `garage::vehicle::refuel::request`
- `garage::vehicle::repair::request`
- `garage::close`
## Runtime Notes
The client builds vehicle context and sends requests. The server garage addon
and extension own stored vehicle state.
Refuel and repair buttons are available from the selected vehicle detail panel
for nearby world vehicles. Stored records must be retrieved before they can be
serviced because fuel and repair operate on live vehicle objects. Service
billing is handled by the server economy addon and charges organization funds.

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

@ -30,9 +30,10 @@ charges such as repairs.
shared org-charge helper can also record member debt for medical fallback.
## Event Surface
The addon registers CBA server events for fuel start/tick/stop, repair service,
player killed, player respawn, and healing. Medical store initialization runs
after post-init to discover configured medical spawn objects.
The addon registers CBA server events for fuel start/tick/stop, direct refuel
service, repair service, player killed, player respawn, and healing. Medical
store initialization runs after post-init to discover configured medical spawn
objects.
Repair service requests use:
@ -42,6 +43,14 @@ Repair service requests use:
`_cost` is optional. Passing `-1` uses the configured service repair cost.
Garage refuel service requests use:
```sqf
[QEGVAR(economy,RefuelService), [_target, _unit]] call CBA_fnc_serverEvent;
```
This fills the selected live vehicle after organization billing succeeds.
## Billing Rules
Economy does not own durable money state. It coordinates Arma-world effects
after the relevant hot-cache charge succeeds.
@ -56,6 +65,10 @@ Fuel and repair services are organization-funded:
5. If the charge fails, do not complete the service. Refueling rolls the target
back to its starting fuel level; repairs are not applied.
Direct refuel service requests, such as those from the garage UI, calculate
the missing fuel from `fuelCapacity`, charge the organization, and fill the
vehicle only after the charge succeeds.
Medical services are player-funded first:
1. Load the player's bank hot state.

View File

@ -33,6 +33,11 @@ if (isNil QGVAR(SEconomyStore)) then { call FUNC(initSEconomyStore); };
GVAR(SEconomyStore) call ["repair", [_target, _unit, _cost]];
}] call CFUNC(addEventHandler);
[QGVAR(RefuelService), {
params ["_target", "_unit"];
GVAR(FEconomyStore) call ["refuel", [_target, _unit]];
}] call CFUNC(addEventHandler);
[QGVAR(onKilled), {
params ["_unit"];
GVAR(MEconomyStore) call ["onKilled", [_unit]];

View File

@ -4,13 +4,14 @@
* File: fnc_initFEconomyStore.sqf
* Author: IDSolutions
* Date: 2025-12-20
* Last Update: 2026-01-03
* Last Update: 2026-04-18
* Public: No
*
* Description:
* Initializes the fuel economy store. Active refueling sessions remain
* server-local; payment is routed through the organization extension hot
* cache.
* cache. Garage service refuels use the same organization billing path
* and only fill the vehicle after the charge succeeds.
*
* Parameter(s):
* N/A
@ -53,6 +54,43 @@ GVAR(FEconomyStore) = createHashMapObject [[
SETVAR(_target,liters,0);
true
}],
["refuel", {
params [["_target", objNull, [objNull]], ["_unit", objNull, [objNull]]];
if (isNull _target || { isNull _unit }) exitWith { false };
private _currentFuel = fuel _target;
private _missingFuel = (1 - _currentFuel) max 0 min 1;
if (_missingFuel <= 0.001) exitWith {
[CRPC(notifications,recieveNotification), ["info", "Refueling", "Vehicle fuel tank is already full."], _unit] call CFUNC(targetEvent);
false
};
if (isNil QGVAR(SEconomyStore)) exitWith {
["ERROR", "Service economy store unavailable for garage refueling charge.", nil, nil] call EFUNC(common,log);
[CRPC(notifications,recieveNotification), ["danger", "Refueling", "Organization billing is unavailable. Refueling was not completed."], _unit] call CFUNC(targetEvent);
false
};
private _fuelCapacity = getNumber (configOf _target >> "fuelCapacity");
if (_fuelCapacity <= 0) then { _fuelCapacity = 100; };
private _totalLiters = _missingFuel * _fuelCapacity;
private _totalCost = _totalLiters * GVAR(FuelCost);
private _chargeResult = GVAR(SEconomyStore) call ["chargeOrg", [_unit, _totalCost, "Refueling"]];
if !(_chargeResult getOrDefault ["success", false]) exitWith {
[CRPC(notifications,recieveNotification), ["danger", "Refueling", _chargeResult getOrDefault ["message", "Organization funds cannot cover this refuel. Refueling was not completed."]], _unit] call CFUNC(targetEvent);
false
};
_target setFuel 1;
SETVAR(_target,liters,0);
private _formattedTotalCost = [_totalCost] call EFUNC(common,formatNumber);
private _formattedTotalLiters = _totalLiters toFixed 2;
[CRPC(notifications,recieveNotification), ["info", "Refueling", format ["Refueling complete: %1L<br />Organization charged $%2.", _formattedTotalLiters, _formattedTotalCost]], _unit] call CFUNC(targetEvent);
true
}],
["stop", {
params ["_source", "_target"];

View File

@ -1,7 +1,8 @@
# Client Garage Usage Guide
The client garage addon provides player vehicle storage UI, vehicle
store/retrieve actions, vehicle context building, and the virtual garage view.
store/retrieve actions, selected nearby vehicle service requests, vehicle
context building, and the virtual garage view.
## Open Garage UI
@ -31,7 +32,7 @@ available vehicle lists from the virtual garage repository.
| `GarageHelperService` | Vehicle names, hit points, and payload helpers. |
| `GarageContextService` | Nearby/current vehicle context. |
| `GaragePayloadService` | Browser hydrate payload construction. |
| `GarageActionService` | Store/retrieve request handling. |
| `GarageActionService` | Store/retrieve request handling and selected nearby vehicle refuel/repair request forwarding. |
| `GarageUIBridge` | Browser ready, hydrate, and sync delivery. |
## Browser Events
@ -42,6 +43,8 @@ available vehicle lists from the virtual garage repository.
| `garage::refresh` | Send current garage payload as `garage::sync`. |
| `garage::vehicle::retrieve::request` | Forward retrieve request through the action service. |
| `garage::vehicle::store::request` | Forward store request through the action service. |
| `garage::vehicle::refuel::request` | Forward selected nearby vehicle refuel request to the server economy service. |
| `garage::vehicle::repair::request` | Forward selected nearby vehicle repair request to the server economy service. |
| `garage::close` | Dispose bridge screen state and close the display. |
## Browser Response Events
@ -50,10 +53,22 @@ available vehicle lists from the virtual garage repository.
| --- | --- |
| `garage::hydrate` | Initial vehicle and session payload. |
| `garage::sync` | Refreshed vehicle payload. |
| `garage::service::success` | Browser notice for accepted refuel/repair requests. |
| `garage::service::failure` | Browser notice for rejected refuel/repair requests. |
Server action responses are handled by the action service and notification
flow.
## Vehicle Service
The selected vehicle detail panel includes refuel and repair actions for nearby
world vehicles. Stored records must be retrieved first because server economy
services operate on live vehicle objects, not stored garage records.
Refuel requests use the server economy `RefuelService` event. Repair requests
use the server economy `RepairService` event. Both services are billed by the
server economy addon through organization funds.
## Mission Setup
Garage interactions are normally surfaced through the actor menu when nearby

View File

@ -24,6 +24,11 @@ syncs the organization patch to online members. If organization funds cannot
cover the refuel, the vehicle is rolled back to the fuel level it had when the
session started.
Garage UI refuel requests use the server `RefuelService` event. The fuel store
calculates missing fuel from the vehicle config `fuelCapacity`, charges the
player's organization, and fills the vehicle only after the organization charge
succeeds.
## Repair
Repair is organization-funded.
@ -37,6 +42,9 @@ Use the repair service event:
`_cost` is optional. Passing `-1` uses the configured service repair cost.
The target is only repaired after the organization charge succeeds.
The client garage UI forwards selected nearby vehicle repair requests through
the same event.
## Medical
Medical is player-funded first.