Add framework transport service

This commit is contained in:
Jacob Schmidt 2026-05-25 13:27:34 -05:00
parent f2ac9fcbe7
commit c0dd782103
40 changed files with 1545 additions and 5 deletions

View File

@ -58,6 +58,37 @@ switch (_event) do {
case "actor::open::phone": { [] spawn EFUNC(phone,openUI); };
case "actor::open::iplayer": { hint "Player interaction is not yet implemented." };
case "actor::open::store": { [] spawn EFUNC(store,openUI); };
case "actor::request::transport": {
if !(_data isEqualType createHashMap) exitWith {
hint "Invalid transport request.";
};
private _destination = _data getOrDefault ["destination", createHashMap];
if !(_destination isEqualType createHashMap) exitWith {
hint "Invalid transport destination.";
};
private _fromNode = objectFromNetId (_data getOrDefault ["netId", ""]);
private _toNode = objectFromNetId (_destination getOrDefault ["netId", ""]);
if (isNull _fromNode || { isNull _toNode }) exitWith {
hint "Transport destination is no longer available.";
};
private _options = createHashMapFromArray [
["label", _data getOrDefault ["label", "Transport"]],
["nodePrefix", _data getOrDefault ["nodePrefix", "transport"]],
["vehiclePrefix", _data getOrDefault ["vehiclePrefix", "transport_vehicle"]],
["arrivalPrefix", _data getOrDefault ["arrivalPrefix", "transport_arrival"]],
["maxIndexedNodes", _data getOrDefault ["maxIndexedNodes", 10]],
["baseFare", _data getOrDefault ["baseFare", 100]],
["pricePerKm", _data getOrDefault ["pricePerKm", 50]],
["cargoRadius", _data getOrDefault ["cargoRadius", 25]],
["includeCargo", _data getOrDefault ["includeCargo", true]]
];
[SRPC(transport,requestTransport), [player, _fromNode, _toNode, _options]] call CFUNC(serverEvent);
};
default { hint format ["Unhandled UI event: %1", _event]; };
};

View File

@ -121,6 +121,12 @@ GVAR(ActorRepositoryBaseClass) = compileFinal createHashMapFromArray [
];
private _deviceType = _x getVariable ["deviceType", ""];
private _isPlayer = _x isKindOf "Man" && isPlayer _x;
private _objectName = vehicleVarName _x;
private _transportPrefix = _x getVariable ["transportNodePrefix", "transport"];
private _isTransport = _x getVariable ["isTransport", false];
if (!_isTransport && { _objectName isNotEqualTo "" }) then {
_isTransport = _objectName isEqualTo _transportPrefix || { (_objectName find format ["%1_", _transportPrefix]) == 0 };
};
if (_isStore) then { _nearbyActions pushBack ["store", true]; };
if (_isAtm) then { _nearbyActions pushBack ["atm", true]; };
@ -129,6 +135,55 @@ GVAR(ActorRepositoryBaseClass) = compileFinal createHashMapFromArray [
if (_isGarage) then { _nearbyActions pushBack ["garage", _garageContext]; };
if (_isGarage && GVAR(enableVG)) then { _nearbyActions pushBack ["vg", _garageContext]; };
if (_deviceType isNotEqualTo "") then { _nearbyActions pushBack ["device", _deviceType]; };
if (_isTransport) then {
private _fromTransportNode = _x;
private _maxIndexedNodes = _x getVariable ["transportMaxIndexedNodes", 10];
private _baseFare = _x getVariable ["transportBaseFare", 100];
private _pricePerKm = _x getVariable ["transportPricePerKm", 50];
private _vehiclePrefix = _x getVariable ["transportVehiclePrefix", format ["%1_vehicle", _transportPrefix]];
private _arrivalPrefix = _x getVariable ["transportArrivalPrefix", format ["%1_arrival", _transportPrefix]];
private _nodeNames = [_transportPrefix];
for "_i" from 1 to _maxIndexedNodes do {
_nodeNames pushBack format ["%1_%2", _transportPrefix, _i];
};
private _destinations = [];
{
private _node = missionNamespace getVariable [_x, objNull];
if (!isNull _node && { _node isNotEqualTo _fromTransportNode }) then {
private _nodeLabel = _node getVariable ["transportLabel", vehicleVarName _node];
if (_nodeLabel isEqualTo "") then { _nodeLabel = "Transport Point"; };
private _distanceMeters = _fromTransportNode distance2D _node;
private _cost = round (_baseFare + ((_distanceMeters / 1000) * _pricePerKm));
_destinations pushBack createHashMapFromArray [
["netId", netId _node],
["name", vehicleVarName _node],
["label", _nodeLabel],
["cost", _cost]
];
};
} forEach _nodeNames;
if (_destinations isNotEqualTo []) then {
private _transportContext = createHashMapFromArray [
["netId", netId _x],
["name", _objectName],
["label", _x getVariable ["transportLabel", "Transport"]],
["nodePrefix", _transportPrefix],
["vehiclePrefix", _vehiclePrefix],
["arrivalPrefix", _arrivalPrefix],
["maxIndexedNodes", _maxIndexedNodes],
["baseFare", _baseFare],
["pricePerKm", _pricePerKm],
["cargoRadius", _x getVariable ["transportCargoRadius", 25]],
["includeCargo", _x getVariable ["transportIncludeCargo", true]],
["destinations", _destinations]
];
_nearbyActions pushBack ["transport", _transportContext];
};
};
if (_isPlayer && { _x isNotEqualTo player }) then { _nearbyActions pushBack ["player", name _x]; };
} forEach (player nearObjects 5);

View File

@ -193,12 +193,19 @@ const actionDefinitions = {
description: "Access your virtual garage",
action: "actor::open::vgarage",
},
transport: {
id: "transport",
title: "Transport",
description: "Show available travel destinations",
action: "actor::show::transport",
},
};
const initialState = {
availableActions: [],
menuItems: [...baseMenuItems],
baseMenuItems: [...baseMenuItems],
defaultMenuItems: [...baseMenuItems],
actionDefinitions: { ...actionDefinitions },
};
@ -244,6 +251,7 @@ function actorReducer(state = initialState, action) {
...state,
availableActions: action.payload,
menuItems: newMenuItems,
defaultMenuItems: newMenuItems,
};
case ActionTypes.SET_MENU_ITEMS:
@ -426,6 +434,43 @@ function RadialMenu() {
const handleItemClick = (item) => {
console.log("Menu item clicked:", item);
if (item.action === "actor::show::default") {
store.dispatch(
actions.setMenuItems(state.defaultMenuItems || state.baseMenuItems),
);
return;
}
if (item.action === "actor::show::transport") {
const context = item.context || {};
const destinations = Array.isArray(context.destinations)
? context.destinations
: [];
const transportItems = [
{
id: "transport-back",
title: "Back",
description: "Return to the default interaction menu",
action: "actor::show::default",
},
...destinations.map((destination, index) => ({
id: `transport-destination-${index}`,
title: destination.cost
? `${destination.label || destination.name || "Destination"} - $${destination.cost}`
: destination.label || destination.name || "Destination",
description: "Request transport to this destination",
action: "actor::request::transport",
context: {
...context,
destination,
},
})),
];
store.dispatch(actions.setMenuItems(transportItems));
return;
}
const alert = {
event: item.action,
data: item.context || {},

View File

@ -0,0 +1 @@
forge\forge_server\addons\transport

View File

@ -0,0 +1,17 @@
class Extended_PreStart_EventHandlers {
class ADDON {
init = QUOTE(call COMPILE_SCRIPT(XEH_preStart));
};
};
class Extended_PreInit_EventHandlers {
class ADDON {
init = QUOTE(call COMPILE_SCRIPT(XEH_preInit));
};
};
class Extended_PostInit_EventHandlers {
class ADDON {
init = QUOTE(call COMPILE_SCRIPT(XEH_postInit));
};
};

View File

@ -0,0 +1,2 @@
PREP(initTransportService);
PREP(requestTransport);

View File

@ -0,0 +1,4 @@
#include "script_component.hpp"
if (isNil QEGVAR(common,EventBus)) then { call EFUNC(common,eventBus); };
if (isNil QGVAR(TransportService)) then { call FUNC(initTransportService); };

View File

@ -0,0 +1,22 @@
#include "script_component.hpp"
PREP_RECOMPILE_START;
#include "XEH_PREP.hpp"
PREP_RECOMPILE_END;
[QGVAR(requestTransport), {
params [
["_unit", objNull, [objNull]],
["_fromNode", objNull, [objNull]],
["_toNode", objNull, [objNull]],
["_options", createHashMap, [createHashMap]]
];
if (isNull _unit || { isNull _fromNode || { isNull _toNode } }) exitWith {};
if (isNil QGVAR(TransportService)) then {
call FUNC(initTransportService);
};
GVAR(TransportService) call ["requestTransport", [_unit, _fromNode, _toNode, _options]];
}] call CFUNC(addEventHandler);

View File

@ -0,0 +1 @@
#include "script_component.hpp"

View File

@ -0,0 +1,22 @@
#include "script_component.hpp"
class CfgPatches {
class ADDON {
author = AUTHOR;
authors[] = {"J.Schmidt"};
url = ECSTRING(main,url);
name = COMPONENT_NAME;
requiredVersion = REQUIRED_VERSION;
requiredAddons[] = {
"forge_server_main",
"forge_server_common",
"forge_server_bank",
"forge_server_economy"
};
units[] = {};
weapons[] = {};
VERSION_CONFIG;
};
};
#include "CfgEventHandlers.hpp"

View File

@ -0,0 +1,399 @@
#include "..\script_component.hpp"
/*
* File: fnc_initTransportService.sqf
* Author: IDSolutions
* Date: 2026-05-25
* Public: No
*
* Description:
* Initializes the server-side paid transport service for player and vehicle
* transfers between mission-placed transport nodes.
*
* Arguments:
* None
*
* Return Value:
* Transport service object [HASHMAP OBJECT]
*
* Example:
* call forge_server_transport_fnc_initTransportService
*/
if !(isServer) exitWith { objNull };
if !(isNil QGVAR(TransportService)) exitWith { GVAR(TransportService) };
if (isNil QEGVAR(common,EventBus)) then { call EFUNC(common,eventBus); };
#pragma hemtt ignore_variables ["_self"]
GVAR(TransportServiceBase) = compileFinal createHashMapFromArray [
["#type", "TransportService"],
["#create", compileFinal {
_self set ["baseFare", 100];
_self set ["pricePerKm", 50];
_self set ["cargoRadius", 25];
_self set ["nodePrefix", "transport"];
_self set ["vehiclePrefix", "transport_vehicle"];
_self set ["arrivalPrefix", "transport_arrival"];
_self set ["maxIndexedNodes", 10];
_self set ["eventTokens", []];
["INFO", "Transport Service Initialized!"] call EFUNC(common,log);
true
}],
["notify", compileFinal {
params [["_unit", objNull, [objNull]], ["_type", "info", [""]], ["_title", "Transport", [""]], ["_message", "", [""]]];
if (isNull _unit || { _message isEqualTo "" }) exitWith { false };
private _uid = getPlayerUID _unit;
if (_uid isEqualTo "") exitWith {
[_message] remoteExecCall ["systemChat", _unit];
true
};
if (isNil QEGVAR(common,EventBus)) exitWith {
[_message] remoteExecCall ["systemChat", _unit];
true
};
EGVAR(common,EventBus) call ["emit", [
"notification.requested",
createHashMapFromArray [
["uids", [_uid]],
["notificationType", _type],
["title", _title],
["message", _message]
],
createHashMapFromArray [["source", "transport"]]
]];
true
}],
["emit", compileFinal {
params [["_eventName", "", [""]], ["_payload", createHashMap, [createHashMap]]];
if (_eventName isEqualTo "" || { isNil QEGVAR(common,EventBus) }) exitWith { createHashMap };
EGVAR(common,EventBus) call ["emit", [
_eventName,
_payload,
createHashMapFromArray [["source", "transport"]]
]]
}],
["getIndexedNames", compileFinal {
params [["_prefix", "", [""]], ["_maxIndex", 10, [0]]];
private _names = [_prefix];
for "_i" from 1 to _maxIndex do {
_names pushBack format ["%1_%2", _prefix, _i];
};
_names
}],
["getNodes", compileFinal {
params [["_options", createHashMap, [createHashMap]]];
private _nodeNames = +(_options getOrDefault ["nodeNames", []]);
if (_nodeNames isEqualTo []) then {
private _prefix = _options getOrDefault ["nodePrefix", _self getOrDefault ["nodePrefix", "transport"]];
private _maxIndex = _options getOrDefault ["maxIndexedNodes", _self getOrDefault ["maxIndexedNodes", 10]];
_nodeNames = _self call ["getIndexedNames", [_prefix, _maxIndex]];
};
private _nodes = _nodeNames apply { missionNamespace getVariable [_x, objNull] };
_nodes select { !isNull _x }
}],
["getExclusionObjects", compileFinal {
params [["_options", createHashMap, [createHashMap]]];
private _excluded = +(_options getOrDefault ["excludedObjects", []]);
_excluded append (_self call ["getNodes", [_options]]);
private _vehicleNames = +(_options getOrDefault ["vehicleNames", []]);
if (_vehicleNames isEqualTo []) then {
private _prefix = _options getOrDefault ["vehiclePrefix", _self getOrDefault ["vehiclePrefix", "transport_vehicle"]];
private _maxIndex = _options getOrDefault ["maxIndexedNodes", _self getOrDefault ["maxIndexedNodes", 10]];
_vehicleNames = _self call ["getIndexedNames", [_prefix, _maxIndex]];
};
private _vehicles = _vehicleNames apply { missionNamespace getVariable [_x, objNull] };
_excluded append (_vehicles select { !isNull _x });
_excluded
}],
["getCost", compileFinal {
params [["_fromNode", objNull, [objNull]], ["_toNode", objNull, [objNull]], ["_options", createHashMap, [createHashMap]]];
private _baseFare = _options getOrDefault ["baseFare", _self getOrDefault ["baseFare", 100]];
private _pricePerKm = _options getOrDefault ["pricePerKm", _self getOrDefault ["pricePerKm", 50]];
private _distanceMeters = _fromNode distance2D _toNode;
round (_baseFare + ((_distanceMeters / 1000) * _pricePerKm))
}],
["getArrivalMarker", compileFinal {
params [["_toNode", objNull, [objNull]], ["_options", createHashMap, [createHashMap]]];
private _explicitMarker = _options getOrDefault ["arrivalMarker", ""];
if (_explicitMarker isNotEqualTo "") exitWith { _explicitMarker };
private _nodeName = vehicleVarName _toNode;
private _nodePrefix = _options getOrDefault ["nodePrefix", _self getOrDefault ["nodePrefix", "transport"]];
private _arrivalPrefix = _options getOrDefault ["arrivalPrefix", _self getOrDefault ["arrivalPrefix", "transport_arrival"]];
if (_nodeName isEqualTo _nodePrefix) exitWith { _arrivalPrefix };
private _prefixWithSeparator = format ["%1_", _nodePrefix];
if ((_nodeName find _prefixWithSeparator) != 0) exitWith { "" };
private _suffix = _nodeName select [count _prefixWithSeparator];
if (_suffix isEqualTo "") exitWith { "" };
format ["%1_%2", _arrivalPrefix, _suffix]
}],
["getArrivalPosition", compileFinal {
params [["_toNode", objNull, [objNull]], ["_index", -1, [0]], ["_options", createHashMap, [createHashMap]]];
private _marker = _self call ["getArrivalMarker", [_toNode, _options]];
private _basePos = if (_marker in allMapMarkers) then {
getMarkerPos _marker
} else {
ASLToATL (_toNode modelToWorldWorld [0, -8, 1.2])
};
if (_index < 0) exitWith { _basePos };
private _spacingX = _options getOrDefault ["cargoSpacingX", 5];
private _spacingY = _options getOrDefault ["cargoSpacingY", 7];
private _columns = _options getOrDefault ["cargoColumns", 3];
private _xOffset = ((_index % _columns) - floor (_columns / 2)) * _spacingX;
private _yOffset = floor (_index / _columns) * _spacingY;
_basePos vectorAdd [_xOffset, _yOffset, 0]
}],
["chargePassenger", compileFinal {
params [["_unit", objNull, [objNull]], ["_amount", 0, [0]], ["_label", "Transport", [""]]];
private _result = createHashMapFromArray [
["success", false],
["message", format ["Unable to charge %1 fare.", _label]],
["source", ""]
];
if (isNull _unit) exitWith { _result };
if (_amount <= 0) exitWith {
_result set ["success", true];
_result set ["message", ""];
_result
};
private _uid = getPlayerUID _unit;
if (_uid isEqualTo "") exitWith {
_result set ["message", "A valid player UID is required for transport billing."];
_result
};
if !(isNil QEGVAR(bank,BankStore)) then {
private _account = EGVAR(bank,BankStore) call ["get", [_uid, ""]];
if (_account isEqualTo createHashMap) then {
_account = EGVAR(bank,BankStore) call ["init", [_uid]];
};
if (_account isNotEqualTo createHashMap) then {
private _source = "";
if ((_account getOrDefault ["bank", 0]) >= _amount) then {
_source = "bank";
} else {
if ((_account getOrDefault ["cash", 0]) >= _amount) then {
_source = "cash";
};
};
if (_source isNotEqualTo "") then {
private _charge = EGVAR(bank,BankStore) call ["chargeCheckout", [_uid, _source, _amount, true]];
if (_charge getOrDefault ["success", false]) exitWith {
EGVAR(bank,BankStore) call ["save", [_uid]];
_result set ["success", true];
_result set ["source", _source];
_result set ["message", format ["%1 charged $%2 from your %3.", _label, [_amount] call EFUNC(common,formatNumber), _source]];
};
};
};
};
if !(isNil QEGVAR(economy,SEconomyStore)) then {
private _orgCharge = EGVAR(economy,SEconomyStore) call ["chargeOrg", [_unit, _amount, _label, true]];
if (_orgCharge getOrDefault ["success", false]) exitWith {
_result set ["success", true];
_result set ["source", "org_credit"];
_result set ["message", format [
"Personal funds could not cover %1. Organization charged $%2 and added it to your credit line.",
_label,
[_amount] call EFUNC(common,formatNumber)
]];
};
_result set ["message", _orgCharge getOrDefault ["message", format ["You cannot afford %1.", _label]]];
};
_result
}],
["getNearbyCargo", compileFinal {
params [
["_fromNode", objNull, [objNull]],
["_unit", objNull, [objNull]],
["_options", createHashMap, [createHashMap]]
];
private _radius = _options getOrDefault ["cargoRadius", _self getOrDefault ["cargoRadius", 25]];
if (_radius <= 0) exitWith { [] };
private _nearby = nearestObjects [
_fromNode,
["LandVehicle", "Air", "Ship", "CAManBase"],
_radius,
true
];
private _excluded = _self call ["getExclusionObjects", [_options]];
_nearby select {
!isNull _x
&& { _x isNotEqualTo _fromNode }
&& { !(_x in _excluded) }
&& { _x isNotEqualTo _unit }
&& { alive _x }
&& {
(_x isKindOf "LandVehicle")
|| { _x isKindOf "Air" }
|| { _x isKindOf "Ship" }
|| { _x isKindOf "CAManBase" && { isPlayer _x } }
}
}
}],
["moveCargo", compileFinal {
params [["_cargo", [], [[]]], ["_toNode", objNull, [objNull]], ["_options", createHashMap, [createHashMap]]];
private _moved = [];
{
private _entity = _x;
if (isNull _entity) then { continue; };
private _pos = _self call ["getArrivalPosition", [_toNode, _forEachIndex, _options]];
if (_entity isKindOf "CAManBase") then {
[_entity, _pos] remoteExecCall ["setPosATL", _entity];
} else {
_entity setPosATL _pos;
_entity setDir (getDir _toNode);
};
_moved pushBack _entity;
} forEach _cargo;
_moved
}],
["requestTransport", compileFinal {
params [
["_unit", objNull, [objNull]],
["_fromNode", objNull, [objNull]],
["_toNode", objNull, [objNull]],
["_options", createHashMap, [createHashMap]]
];
private _result = createHashMapFromArray [
["success", false],
["message", "Transport request failed."],
["cost", 0],
["movedCargo", []]
];
if (isNull _unit || { !isPlayer _unit }) exitWith { _result };
if (isNull _fromNode || { isNull _toNode }) exitWith { _result };
if (_fromNode isEqualTo _toNode) exitWith {
_result set ["message", "Origin and destination are the same."];
_result
};
private _nodes = _self call ["getNodes", [_options]];
if !(_fromNode in _nodes && { _toNode in _nodes }) exitWith {
_result set ["message", "Transport route is unavailable."];
_result
};
private _label = _options getOrDefault ["label", "Transport"];
private _cost = _self call ["getCost", [_fromNode, _toNode, _options]];
_result set ["cost", _cost];
_self call ["emit", [
"transport.requested",
createHashMapFromArray [
["unit", _unit],
["uid", getPlayerUID _unit],
["from", _fromNode],
["to", _toNode],
["cost", _cost],
["label", _label]
]
]];
private _charge = _self call ["chargePassenger", [_unit, _cost, _label]];
if !(_charge getOrDefault ["success", false]) exitWith {
private _message = _charge getOrDefault ["message", "Transport payment failed."];
_result set ["message", _message];
_self call ["notify", [_unit, "danger", _label, _message]];
_self call ["emit", ["transport.failed", +_result]];
_result
};
private _cargo = if (_options getOrDefault ["includeCargo", true]) then {
_self call ["getNearbyCargo", [_fromNode, _unit, _options]]
} else {
[]
};
private _destination = _self call ["getArrivalPosition", [_toNode, -1, _options]];
private _movedCargo = _self call ["moveCargo", [_cargo, _toNode, _options]];
[_unit, _destination] remoteExecCall ["setPosATL", _unit];
_self call ["notify", [_unit, "info", _label, _charge getOrDefault ["message", format ["%1 paid.", _label]]]];
if (_movedCargo isNotEqualTo []) then {
_self call ["notify", [_unit, "info", _label, format ["Moved %1 nearby passenger/vehicle item(s).", count _movedCargo]]];
};
_result set ["success", true];
_result set ["message", "Transport completed."];
_result set ["movedCargo", _movedCargo];
_result set ["paymentSource", _charge getOrDefault ["source", ""]];
_self call ["emit", ["transport.completed", +_result]];
_result
}],
["registerEventHandlers", compileFinal {
if (isNil QEGVAR(common,EventBus)) exitWith { false };
if ((_self getOrDefault ["eventTokens", []]) isNotEqualTo []) exitWith { true };
private _handleRequest = {
params ["_event"];
private _unit = _event getOrDefault ["unit", objNull];
private _from = _event getOrDefault ["from", objNull];
private _to = _event getOrDefault ["to", objNull];
private _options = _event getOrDefault ["options", createHashMap];
if (isNil QGVAR(TransportService)) exitWith {};
GVAR(TransportService) call ["requestTransport", [_unit, _from, _to, _options]];
};
_self set ["eventTokens", [
EGVAR(common,EventBus) call ["on", ["transport.request", _handleRequest, "transport.request"]]
]];
true
}],
["#delete", compileFinal {
if !(isNil QEGVAR(common,EventBus)) then {
{
EGVAR(common,EventBus) call ["off", [_x]];
} forEach (_self getOrDefault ["eventTokens", []]);
};
_self set ["eventTokens", []];
}]
];
GVAR(TransportService) = createHashMapObject [GVAR(TransportServiceBase), []];
GVAR(TransportService) call ["registerEventHandlers", []];
GVAR(TransportService)

View File

@ -0,0 +1,33 @@
#include "..\script_component.hpp"
/*
* File: fnc_requestTransport.sqf
* Author: IDSolutions
* Date: 2026-05-25
* Public: No
*
* Description:
* Requests a paid transport transfer for a player and nearby cargo.
*
* Arguments:
* 0: Player unit <OBJECT>
* 1: Origin node <OBJECT>
* 2: Destination node <OBJECT>
* 3: Options <HASHMAP> (optional)
*
* Return Value:
* Result [HASHMAP]
*
* Example:
* [player, transport, transport_1] call forge_server_transport_fnc_requestTransport
*/
params [
["_unit", objNull, [objNull]],
["_fromNode", objNull, [objNull]],
["_toNode", objNull, [objNull]],
["_options", createHashMap, [createHashMap]]
];
if (isNil QGVAR(TransportService)) then { call FUNC(initTransportService); };
GVAR(TransportService) call ["requestTransport", [_unit, _fromNode, _toNode, _options]]

View File

@ -0,0 +1,9 @@
#define COMPONENT transport
#define COMPONENT_BEAUTIFIED Transport
#include "\forge\forge_server\addons\main\script_mod.hpp"
// #define DEBUG_MODE_FULL
// #define DISABLE_COMPILE_CACHE
// #define ENABLE_PERFORMANCE_COUNTERS
#include "\forge\forge_server\addons\main\script_macros.hpp"

View File

@ -27,6 +27,42 @@ Rules validated by the Rust service:
- `locker:get`, `locker:patch`, and `locker:remove` require an existing locker.
- `locker:remove` takes the classname directly, not a JSON object.
## Multiple Locker Objects
Editor-placed locker objects are templates and access points, not separate
durable inventories. During server post-init, any mission namespace object
whose variable name contains `locker` is hidden globally. During client setup,
each client reads the hidden object's classname, ASL position, vector direction,
and vector up, then creates a matching local locker object with
`createVehicleLocal`.
The local clone is the object the player actually opens. On `ContainerOpened`,
the client clears the clone and fills it from the player's UID-owned locker
state. On `ContainerClosed`, the client reads the clone's cargo and sends a
full locker override back to the server.
There is no explicit maximum number of editor-placed locker access points. The
practical limit is mission performance and how many local container objects are
reasonable for the scenario.
All locker access points load and save the same player locker, keyed by player
UID. Opening `locker`, `locker_hq`, or `locker_outpost_1` does not create
separate persistent inventories; those objects are separate local access clones
for the same underlying player locker.
## Store Grants and Duplicate Inventory
The store checkout path grants items to the UID-owned locker hot state, not to a
specific placed locker object. Item grants are merged by classname:
- buying a new classname adds one new locker entry
- buying an existing classname increases that entry's amount
- checkout fails if the result would exceed 25 unique classnames
Having more than one locker object on the map does not duplicate store grants.
Duplicate quantities can only come from repeated checkout requests or repeated
manual locker writes, not from the number of placed locker access points.
## Commands
All commands are called on the `locker` group.

View File

@ -30,6 +30,7 @@ case-sensitive in some initializers, so use lower-case names.
| Store | name contains `store` | `isStore = true` | Store UI | Store catalog and checkout behavior are configured server-side. |
| Garage | name contains `garage` | `isGarage = true` | Garage UI and virtual garage | Include a garage category in the name or set `garageType` manually. |
| Locker | name contains `locker` | local `isLocker = true` | Virtual arsenal action | The server hides the editor object; each client creates a local locker at the same position. |
| Transport | `transport`, `transport_1` through `transport_10` | discovered by variable name or `isTransport = true` | Transport destination menu | Paid player and cargo transfer between named transport nodes. |
Recommended object names:
@ -38,6 +39,8 @@ atm
bank
store
locker
transport
transport_1
garage_hq
garage_hq_2
```
@ -60,6 +63,7 @@ _atmTerminal setVariable ["isAtm", true, true];
_storeCounter setVariable ["isStore", true, true];
_garageTerminal setVariable ["isGarage", true, true];
_garageTerminal setVariable ["garageType", "cars", true];
_transportNode setVariable ["isTransport", true, true];
```
Supported garage types are:
@ -142,6 +146,67 @@ Minimum Eden setup:
2. Set its Eden variable name to something containing `store`.
3. Test that the actor menu shows the store action within 5 meters.
## Transport Setup
Transport nodes are generic paid travel points. They can represent ferries,
airports, bus stops, teleport terminals, or any other mission transport system.
The framework owns the menu, billing, cargo scan, and movement logic. The
mission only needs placed objects and optional arrival markers.
![Placeholder: Eden transport node object placement](images/eden/transport_node_obj.svg)
![Placeholder: Eden transport node variable name](images/eden/transport_node_var.svg)
Place transport node objects with these variable names:
```text
transport
transport_1
transport_2
...
transport_10
```
Place optional arrival markers with matching suffixes:
```text
transport_arrival
transport_arrival_1
transport_arrival_2
...
transport_arrival_10
```
![Placeholder: Eden transport arrival marker placement](images/eden/transport_arrival_marker.svg)
Objects that should be excluded from the nearby cargo scan, such as the actual
boat or transport vehicle used as set dressing, should use:
```text
transport_vehicle
transport_vehicle_1
transport_vehicle_2
...
transport_vehicle_10
```
![Placeholder: Eden transport vehicle exclusion object variable name](images/eden/transport_vehicle_var.svg)
Minimum Eden setup:
1. Place at least two transport node objects.
2. Name them `transport`, `transport_1`, and so on.
3. Place matching `transport_arrival*` markers where players and cargo should
appear.
4. Name any set-dressing transport vehicles `transport_vehicle*` so they are
not moved as cargo.
5. Test that the actor menu shows Transport within 5 meters of a node.
The default fare is `$100 + distance in kilometers * $50`. The server charges
player bank first, player cash second, then organization credit line fallback.
See [Transport Service Guide](./TRANSPORT_SERVICE_GUIDE.md) for override
variables and implementation details.
## Bank and ATM Setup
Bank and ATM objects intentionally expose different workflows.
@ -179,7 +244,7 @@ Minimum Eden setup:
Locker objects are slightly different from other interaction objects. The
server finds editor-placed objects whose variable names contain `locker`, hides
those global objects, and each client creates a local locker object at the same
position.
position using the placed object's classname and orientation.
![Locker object placement](images/eden/locker_obj.jpg)
@ -192,6 +257,11 @@ Minimum Eden setup:
3. Do not use `forge_locker_box`.
4. Test that the local locker appears and opens the virtual arsenal action.
There is no editor-side maximum number of locker access points. Multiple locker
objects on a map create multiple local access clones, but all of those clones
load and save the same UID-owned player locker state. They do not create
separate persistent lockers or cause store grants to duplicate by themselves.
## Medical Spawn Setup
The medical economy store discovers up to eleven medical spawn objects by exact

View File

@ -214,6 +214,32 @@ physical vehicle into the player's 5-slot garage. Use the virtual garage to
spawn an unlocked vehicle, and use the garage to store or retrieve live world
vehicles.
## Transport
Transport points let players pay to travel between configured mission locations.
They may represent ferries, terminals, air shuttles, or other mission-specific
travel points.
![Placeholder: Actor menu Transport action](images/player/transport_menu_action.svg)
Player workflow:
1. Stand near a transport point.
2. Open the actor interaction menu.
3. Select Transport.
4. Select a destination from the transport submenu, or select Close to return
to the default interaction menu.
![Placeholder: Transport destination submenu](images/player/transport_destination_menu.svg)
The destination price is based on distance. The server charges player bank
first, player cash second, then organization credit line fallback when
available. If payment succeeds, the player is moved to the selected arrival
point. Nearby eligible vehicles or passengers may be moved with the player when
the mission has configured the transport point for cargo movement.
![Placeholder: Transport completion notification](images/player/transport_complete.svg)
## Locker and Virtual Arsenal
The locker is personal item storage.
@ -225,6 +251,10 @@ Locker rules:
- Up to 25 items can be stored.
- The locker saves when the locker container is closed.
- Over-capacity storage can warn or fail depending on server handling.
- Multiple locker access points on the map open local copies of the locker
object, but all of them use the same personal locker inventory.
- Store purchases merge granted items into the same personal locker by
classname; extra locker objects on the map do not duplicate store grants.
The virtual arsenal is locked down. Players only see gear they have been
granted or have unlocked through systems such as the store. The virtual arsenal

View File

@ -0,0 +1,135 @@
# Transport Service Guide
The transport service provides paid point-to-point travel for players and
nearby vehicles or passengers. It is framework-owned: missions only need placed
transport objects and optional arrival markers with the expected variable names.
## Mission Contract
By default the framework discovers transport nodes by exact mission namespace
variable name:
```text
transport
transport_1
transport_2
...
transport_10
```
Each node is an Eden-placed object players can stand near. When a player opens
the actor interaction menu within 5 meters of a node, the menu shows a
Transport action. Selecting Transport opens destination choices for the other
configured nodes.
Arrival markers use the same suffix:
```text
transport_arrival
transport_arrival_1
transport_arrival_2
...
transport_arrival_10
```
Object names used only to exclude parked ferry/transport vehicles from cargo
pickup scans use this convention:
```text
transport_vehicle
transport_vehicle_1
transport_vehicle_2
...
transport_vehicle_10
```
The suffix mapping is direct:
- `transport` arrives at `transport_arrival`
- `transport_1` arrives at `transport_arrival_1`
- `transport_10` arrives at `transport_arrival_10`
If an arrival marker is missing, the framework falls back to a position behind
the destination node object.
## Pricing and Payment
The default fare is:
```text
base fare + distance in kilometers * price per kilometer
```
Current defaults:
- base fare: `$100`
- price per kilometer: `$50`
- cargo scan radius: `25` meters
- max indexed nodes: `10`
Payment is server-authoritative. The transport service attempts payment in this
order:
1. Player bank balance.
2. Player cash.
3. Organization credit line fallback.
The player and cargo are moved only after payment succeeds.
## Cargo and Vehicle Transfer
When a player requests transport, the server scans near the origin node for
nearby vehicles, ships, aircraft, and player units. The scan ignores:
- the origin and destination transport nodes
- objects named with the `transport_vehicle` prefix
- the requesting player
- dead entities
Use `transport_vehicle*` names for the actual boat, ferry, aircraft, or set
dressing object that should not be moved as cargo.
## Optional Per-Node Overrides
The default naming convention should cover normal missions. If a specific
mission needs another prefix or different pricing, set variables on the
transport node object:
```sqf
this setVariable ["isTransport", true, true];
this setVariable ["transportLabel", "North Dock", true];
this setVariable ["transportNodePrefix", "dock", true];
this setVariable ["transportVehiclePrefix", "dock_vehicle", true];
this setVariable ["transportArrivalPrefix", "dock_arrival", true];
this setVariable ["transportMaxIndexedNodes", 4, true];
this setVariable ["transportBaseFare", 150, true];
this setVariable ["transportPricePerKm", 75, true];
this setVariable ["transportCargoRadius", 25, true];
this setVariable ["transportIncludeCargo", true, true];
```
Only use overrides when the default `transport*` convention is not appropriate.
## Image Checklist
Replace these placeholder image references after screenshots are captured:
![Placeholder: Eden transport node object placement](images/eden/transport_node_obj.svg)
![Placeholder: Eden transport node variable name field](images/eden/transport_node_var.svg)
![Placeholder: Eden transport arrival marker placement](images/eden/transport_arrival_marker.svg)
![Placeholder: Eden transport vehicle exclusion object variable name](images/eden/transport_vehicle_var.svg)
![Placeholder: Player actor menu transport action](images/player/transport_menu_action.svg)
![Placeholder: Player transport destination submenu](images/player/transport_destination_menu.svg)
![Placeholder: Player transport completion notification](images/player/transport_complete.svg)
## Mission-Side Code Requirement
No mission-side transport service, addAction script, or server event bridge is
required. The framework handles menu discovery, destination selection, pricing,
billing, cargo movement, and EventBus notifications.

View File

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="720" viewBox="0 0 1280 720" role="img" aria-labelledby="title desc">
<title id="title">Placeholder: Eden transport arrival marker placement</title>
<desc id="desc">Capture an Eden Editor screenshot showing the arrival marker location.</desc>
<rect width="1280" height="720" fill="#202733"/>
<rect x="80" y="70" width="1120" height="580" rx="8" fill="#2f3948" stroke="#7d8aa0" stroke-width="4" stroke-dasharray="18 14"/>
<text x="640" y="280" fill="#f3f5f7" font-family="Arial, sans-serif" font-size="48" text-anchor="middle">PLACEHOLDER IMAGE</text>
<text x="640" y="352" fill="#d7dde7" font-family="Arial, sans-serif" font-size="30" text-anchor="middle">Eden: transport arrival marker</text>
<text x="640" y="410" fill="#aeb9c9" font-family="Arial, sans-serif" font-size="24" text-anchor="middle">Show where players and cargo should spawn.</text>
</svg>

After

Width:  |  Height:  |  Size: 930 B

View File

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="720" viewBox="0 0 1280 720" role="img" aria-labelledby="title desc">
<title id="title">Placeholder: Eden transport node object placement</title>
<desc id="desc">Capture an Eden Editor screenshot showing the placed transport access object.</desc>
<rect width="1280" height="720" fill="#202733"/>
<rect x="80" y="70" width="1120" height="580" rx="8" fill="#2f3948" stroke="#7d8aa0" stroke-width="4" stroke-dasharray="18 14"/>
<text x="640" y="280" fill="#f3f5f7" font-family="Arial, sans-serif" font-size="48" text-anchor="middle">PLACEHOLDER IMAGE</text>
<text x="640" y="352" fill="#d7dde7" font-family="Arial, sans-serif" font-size="30" text-anchor="middle">Eden: transport node object placement</text>
<text x="640" y="410" fill="#aeb9c9" font-family="Arial, sans-serif" font-size="24" text-anchor="middle">Show the object players interact with for transport.</text>
</svg>

After

Width:  |  Height:  |  Size: 951 B

View File

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="720" viewBox="0 0 1280 720" role="img" aria-labelledby="title desc">
<title id="title">Placeholder: Eden transport node variable name</title>
<desc id="desc">Capture the Eden object attributes panel with the transport variable name.</desc>
<rect width="1280" height="720" fill="#202733"/>
<rect x="80" y="70" width="1120" height="580" rx="8" fill="#2f3948" stroke="#7d8aa0" stroke-width="4" stroke-dasharray="18 14"/>
<text x="640" y="280" fill="#f3f5f7" font-family="Arial, sans-serif" font-size="48" text-anchor="middle">PLACEHOLDER IMAGE</text>
<text x="640" y="352" fill="#d7dde7" font-family="Arial, sans-serif" font-size="30" text-anchor="middle">Eden: transport variable name</text>
<text x="640" y="410" fill="#aeb9c9" font-family="Arial, sans-serif" font-size="24" text-anchor="middle">Show transport, transport_1, through transport_10.</text>
</svg>

After

Width:  |  Height:  |  Size: 935 B

View File

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="720" viewBox="0 0 1280 720" role="img" aria-labelledby="title desc">
<title id="title">Placeholder: Eden transport vehicle variable name</title>
<desc id="desc">Capture the variable name for the transport vehicle that should be excluded from cargo scans.</desc>
<rect width="1280" height="720" fill="#202733"/>
<rect x="80" y="70" width="1120" height="580" rx="8" fill="#2f3948" stroke="#7d8aa0" stroke-width="4" stroke-dasharray="18 14"/>
<text x="640" y="280" fill="#f3f5f7" font-family="Arial, sans-serif" font-size="48" text-anchor="middle">PLACEHOLDER IMAGE</text>
<text x="640" y="352" fill="#d7dde7" font-family="Arial, sans-serif" font-size="30" text-anchor="middle">Eden: transport_vehicle variable</text>
<text x="640" y="410" fill="#aeb9c9" font-family="Arial, sans-serif" font-size="24" text-anchor="middle">Show transport_vehicle, transport_vehicle_1, etc.</text>
</svg>

After

Width:  |  Height:  |  Size: 959 B

View File

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="720" viewBox="0 0 1280 720" role="img" aria-labelledby="title desc">
<title id="title">Placeholder: Player transport completion</title>
<desc id="desc">Capture the player and cargo after a successful transport request.</desc>
<rect width="1280" height="720" fill="#202733"/>
<rect x="80" y="70" width="1120" height="580" rx="8" fill="#2f3948" stroke="#7d8aa0" stroke-width="4" stroke-dasharray="18 14"/>
<text x="640" y="280" fill="#f3f5f7" font-family="Arial, sans-serif" font-size="48" text-anchor="middle">PLACEHOLDER IMAGE</text>
<text x="640" y="352" fill="#d7dde7" font-family="Arial, sans-serif" font-size="30" text-anchor="middle">Player: transport complete</text>
<text x="640" y="410" fill="#aeb9c9" font-family="Arial, sans-serif" font-size="24" text-anchor="middle">Show arrival at the destination with moved vehicles nearby.</text>
</svg>

After

Width:  |  Height:  |  Size: 927 B

View File

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="720" viewBox="0 0 1280 720" role="img" aria-labelledby="title desc">
<title id="title">Placeholder: Player transport destination menu</title>
<desc id="desc">Capture the destination submenu after selecting Transport.</desc>
<rect width="1280" height="720" fill="#202733"/>
<rect x="80" y="70" width="1120" height="580" rx="8" fill="#2f3948" stroke="#7d8aa0" stroke-width="4" stroke-dasharray="18 14"/>
<text x="640" y="280" fill="#f3f5f7" font-family="Arial, sans-serif" font-size="48" text-anchor="middle">PLACEHOLDER IMAGE</text>
<text x="640" y="352" fill="#d7dde7" font-family="Arial, sans-serif" font-size="30" text-anchor="middle">Player: destination submenu</text>
<text x="640" y="410" fill="#aeb9c9" font-family="Arial, sans-serif" font-size="24" text-anchor="middle">Show destination choices and the Back action.</text>
</svg>

After

Width:  |  Height:  |  Size: 912 B

View File

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="720" viewBox="0 0 1280 720" role="img" aria-labelledby="title desc">
<title id="title">Placeholder: Player transport menu action</title>
<desc id="desc">Capture the actor interaction menu showing the Transport action.</desc>
<rect width="1280" height="720" fill="#202733"/>
<rect x="80" y="70" width="1120" height="580" rx="8" fill="#2f3948" stroke="#7d8aa0" stroke-width="4" stroke-dasharray="18 14"/>
<text x="640" y="280" fill="#f3f5f7" font-family="Arial, sans-serif" font-size="48" text-anchor="middle">PLACEHOLDER IMAGE</text>
<text x="640" y="352" fill="#d7dde7" font-family="Arial, sans-serif" font-size="30" text-anchor="middle">Player: Transport action</text>
<text x="640" y="410" fill="#aeb9c9" font-family="Arial, sans-serif" font-size="24" text-anchor="middle">Show the first interaction menu near a transport node.</text>
</svg>

After

Width:  |  Height:  |  Size: 919 B

View File

@ -3,6 +3,52 @@ title: "Development Guide"
description: "This guide covers the usual path for adding or changing a Forge module."
---
## Repository Workflow
Use [Git Workflow](/getting-started/git-workflow) as the source of truth for branch roles,
release tags, and mission branch handling. The short version is:
- Use `pre-v0.2` for framework development after the `v0.1.0` baseline.
- Keep `master` as the clean release baseline branch.
- Keep mission folders off `master`; mission work belongs on
`missions/local-mission-copies`.
- Keep `archive/pre-v0.1-history` read-only unless recovering old work.
- Bring reusable mission logic back to framework branches by copying only the
needed framework files or code, not by merging the mission branch.
Use the workflow helper for the routine checks:
```powershell
npm run workflow -- status
npm run workflow -- doctor
npm run workflow -- switch dev
npm run workflow -- switch missions
```
Example framework workflow:
```powershell
git switch pre-v0.2
git pull
git switch -c feature/cad-task-request
# make framework changes
git status --short --branch
git add arma/client/addons/cad arma/server/addons/cad
git commit -m "Add CAD task request workflow"
```
Example mission workflow:
```powershell
git switch missions/local-mission-copies
# make mission changes
git status --short --branch
git add arma/forge_pmc_simulator.Tanoa
git commit -m "Update PMC simulator mission setup"
```
## Local Checks
Before running storage-backed workflows locally, complete

View File

@ -0,0 +1,158 @@
---
title: "Git Workflow"
description: "This repository uses `master` as the clean framework branch. Mission folders are kept off `master` so the framework can be versioned without bundling local test missions or playable mission copies."
---
## Workflow Helper
The repository includes a small helper for the common branch checks and branch
switching commands:
```powershell
npm run workflow -- status
npm run workflow -- doctor
npm run workflow -- switch dev
npm run workflow -- switch missions
npm run workflow -- start-feature cad-task-request
npm run workflow -- release-check
```
The helper refuses branch switches and feature branch creation when the working
tree has uncommitted changes. Use the manual Git commands below when you need
more control.
## Branch Roles
- `master`: framework source, addon code, Rust extension code, docs, tooling,
and release tags.
- `missions/local-mission-copies`: local mission folders used for testing and
mission iteration. This branch is not pushed unless intentionally needed.
- `archive/pre-v0.1-history`: read-only archive of the previous full `master`
history before the `v0.1.0` baseline cleanup.
## Daily Framework Work
Start from the clean framework branch.
```powershell
git switch master
git pull
git status --short --branch
```
Create a short-lived feature branch for framework work.
```powershell
git switch -c feature/garage-marker-selection
```
Make the change, validate it, then commit.
```powershell
git status --short --branch
git add arma/client/addons/garage/functions/fnc_initContextService.sqf
git commit -m "Improve garage spawn marker selection"
```
Merge the work back into `master`. Squash merges keep future `master` history
compact.
```powershell
git switch master
git merge --squash feature/garage-marker-selection
git commit -m "Improve garage spawn marker selection"
git push
```
Remove the local feature branch when it is no longer needed.
```powershell
git branch -D feature/garage-marker-selection
```
## Mission Work
Switch to the local mission branch before editing mission folders.
```powershell
git switch missions/local-mission-copies
git status --short --branch
```
Mission folders currently tracked on that branch:
```text
arma/forge_framework.Malden
arma/forge_pmc_simulator.Tanoa
arma/forge_pmc_simulator_v2.Tanoa
```
Commit mission-only changes on the mission branch.
```powershell
git add arma/forge_pmc_simulator.Tanoa
git commit -m "Update PMC simulator mission setup"
```
Do not merge the mission branch into `master`. If a mission change becomes
framework code, copy only the reusable files or logic onto a framework feature
branch created from `master`.
Example:
```powershell
git switch master
git switch -c feature/cad-on-demand-task-request
# Bring over only the framework files needed from the mission branch.
git checkout missions/local-mission-copies -- arma/client/addons/cad/functions/fnc_initUIBridge.sqf
git checkout missions/local-mission-copies -- arma/server/addons/cad/XEH_preInit.sqf
git add arma/client/addons/cad/functions/fnc_initUIBridge.sqf arma/server/addons/cad/XEH_preInit.sqf
git commit -m "Add CAD on-demand mission task request bridge"
```
## Release Versioning
Use tags to mark framework releases.
Version guideline:
- Patch, such as `v0.1.1`: fixes and small compatible changes.
- Minor, such as `v0.2.0`: new modules or features.
- Major, such as `v1.0.0`: stable release line or breaking changes.
Create a release tag from `master`.
```powershell
git switch master
git pull
git status --short --branch
git tag -a v0.1.1 -m "v0.1.1"
git push origin master
git push origin v0.1.1
```
## Safety Checks
Before committing on `master`, check that no mission folders are staged.
```powershell
git status --short --branch
```
On `master`, these paths should not appear:
```text
arma/forge_framework.Malden
arma/forge_pmc_simulator.Tanoa
arma/forge_pmc_simulator_v2.Tanoa
```
If mission files appear while on `master`, stop and switch to the mission
branch before continuing.
```powershell
git switch missions/local-mission-copies
```

View File

@ -30,6 +30,7 @@ case-sensitive in some initializers, so use lower-case names.
| Store | name contains `store` | `isStore = true` | Store UI | Store catalog and checkout behavior are configured server-side. |
| Garage | name contains `garage` | `isGarage = true` | Garage UI and virtual garage | Include a garage category in the name or set `garageType` manually. |
| Locker | name contains `locker` | local `isLocker = true` | Virtual arsenal action | The server hides the editor object; each client creates a local locker at the same position. |
| Transport | `transport`, `transport_1` through `transport_10` | discovered by variable name or `isTransport = true` | Transport destination menu | Paid player and cargo transfer between named transport nodes. |
Recommended object names:
@ -38,6 +39,8 @@ atm
bank
store
locker
transport
transport_1
garage_hq
garage_hq_2
```
@ -60,6 +63,7 @@ _atmTerminal setVariable ["isAtm", true, true];
_storeCounter setVariable ["isStore", true, true];
_garageTerminal setVariable ["isGarage", true, true];
_garageTerminal setVariable ["garageType", "cars", true];
_transportNode setVariable ["isTransport", true, true];
```
Supported garage types are:
@ -142,6 +146,67 @@ Minimum Eden setup:
2. Set its Eden variable name to something containing `store`.
3. Test that the actor menu shows the store action within 5 meters.
## Transport Setup
Transport nodes are generic paid travel points. They can represent ferries,
airports, bus stops, teleport terminals, or any other mission transport system.
The framework owns the menu, billing, cargo scan, and movement logic. The
mission only needs placed objects and optional arrival markers.
![Placeholder: Eden transport node object placement](images/eden/transport_node_obj.svg)
![Placeholder: Eden transport node variable name](images/eden/transport_node_var.svg)
Place transport node objects with these variable names:
```text
transport
transport_1
transport_2
...
transport_10
```
Place optional arrival markers with matching suffixes:
```text
transport_arrival
transport_arrival_1
transport_arrival_2
...
transport_arrival_10
```
![Placeholder: Eden transport arrival marker placement](images/eden/transport_arrival_marker.svg)
Objects that should be excluded from the nearby cargo scan, such as the actual
boat or transport vehicle used as set dressing, should use:
```text
transport_vehicle
transport_vehicle_1
transport_vehicle_2
...
transport_vehicle_10
```
![Placeholder: Eden transport vehicle exclusion object variable name](images/eden/transport_vehicle_var.svg)
Minimum Eden setup:
1. Place at least two transport node objects.
2. Name them `transport`, `transport_1`, and so on.
3. Place matching `transport_arrival*` markers where players and cargo should
appear.
4. Name any set-dressing transport vehicles `transport_vehicle*` so they are
not moved as cargo.
5. Test that the actor menu shows Transport within 5 meters of a node.
The default fare is `$100 + distance in kilometers * $50`. The server charges
player bank first, player cash second, then organization credit line fallback.
See [Transport Service Guide](/server-modules/transport-service) for override
variables and implementation details.
## Bank and ATM Setup
Bank and ATM objects intentionally expose different workflows.
@ -179,7 +244,7 @@ Minimum Eden setup:
Locker objects are slightly different from other interaction objects. The
server finds editor-placed objects whose variable names contain `locker`, hides
those global objects, and each client creates a local locker object at the same
position.
position using the placed object's classname and orientation.
![Locker object placement](images/eden/locker_obj.jpg)
@ -192,6 +257,11 @@ Minimum Eden setup:
3. Do not use `forge_locker_box`.
4. Test that the local locker appears and opens the virtual arsenal action.
There is no editor-side maximum number of locker access points. Multiple locker
objects on a map create multiple local access clones, but all of those clones
load and save the same UID-owned player locker state. They do not create
separate persistent lockers or cause store grants to duplicate by themselves.
## Medical Spawn Setup
The medical economy store discovers up to eleven medical spawn objects by exact

View File

@ -213,6 +213,32 @@ physical vehicle into the player's 5-slot garage. Use the virtual garage to
spawn an unlocked vehicle, and use the garage to store or retrieve live world
vehicles.
## Transport
Transport points let players pay to travel between configured mission locations.
They may represent ferries, terminals, air shuttles, or other mission-specific
travel points.
![Placeholder: Actor menu Transport action](images/player/transport_menu_action.svg)
Player workflow:
1. Stand near a transport point.
2. Open the actor interaction menu.
3. Select Transport.
4. Select a destination from the transport submenu, or select Close to return
to the default interaction menu.
![Placeholder: Transport destination submenu](images/player/transport_destination_menu.svg)
The destination price is based on distance. The server charges player bank
first, player cash second, then organization credit line fallback when
available. If payment succeeds, the player is moved to the selected arrival
point. Nearby eligible vehicles or passengers may be moved with the player when
the mission has configured the transport point for cargo movement.
![Placeholder: Transport completion notification](images/player/transport_complete.svg)
## Locker and Virtual Arsenal
The locker is personal item storage.
@ -224,6 +250,10 @@ Locker rules:
- Up to 25 items can be stored.
- The locker saves when the locker container is closed.
- Over-capacity storage can warn or fail depending on server handling.
- Multiple locker access points on the map open local copies of the locker
object, but all of them use the same personal locker inventory.
- Store purchases merge granted items into the same personal locker by
classname; extra locker objects on the map do not duplicate store grants.
The virtual arsenal is locked down. Players only see gear they have been
granted or have unlocked through systems such as the store. The virtual arsenal

View File

@ -111,4 +111,14 @@ Most modules follow the same shape:
---
Task catalog, ownership, status transitions, defuse counters, and rewards.
:::
:::u-page-card
---
icon: i-lucide-route
title: Transport Service
to: /server-modules/transport-service
---
Paid point-to-point player and cargo transport configured through Eden
objects and arrival markers.
:::
::

View File

@ -0,0 +1,134 @@
---
title: "Transport Service Guide"
description: "The transport service provides paid point-to-point travel for players and nearby vehicles or passengers. It is framework-owned: missions only need placed transport objects and optional arrival markers with the expected variable names."
---
## Mission Contract
By default the framework discovers transport nodes by exact mission namespace
variable name:
```text
transport
transport_1
transport_2
...
transport_10
```
Each node is an Eden-placed object players can stand near. When a player opens
the actor interaction menu within 5 meters of a node, the menu shows a
Transport action. Selecting Transport opens destination choices for the other
configured nodes.
Arrival markers use the same suffix:
```text
transport_arrival
transport_arrival_1
transport_arrival_2
...
transport_arrival_10
```
Object names used only to exclude parked ferry/transport vehicles from cargo
pickup scans use this convention:
```text
transport_vehicle
transport_vehicle_1
transport_vehicle_2
...
transport_vehicle_10
```
The suffix mapping is direct:
- `transport` arrives at `transport_arrival`
- `transport_1` arrives at `transport_arrival_1`
- `transport_10` arrives at `transport_arrival_10`
If an arrival marker is missing, the framework falls back to a position behind
the destination node object.
## Pricing and Payment
The default fare is:
```text
base fare + distance in kilometers * price per kilometer
```
Current defaults:
- base fare: `$100`
- price per kilometer: `$50`
- cargo scan radius: `25` meters
- max indexed nodes: `10`
Payment is server-authoritative. The transport service attempts payment in this
order:
1. Player bank balance.
2. Player cash.
3. Organization credit line fallback.
The player and cargo are moved only after payment succeeds.
## Cargo and Vehicle Transfer
When a player requests transport, the server scans near the origin node for
nearby vehicles, ships, aircraft, and player units. The scan ignores:
- the origin and destination transport nodes
- objects named with the `transport_vehicle` prefix
- the requesting player
- dead entities
Use `transport_vehicle*` names for the actual boat, ferry, aircraft, or set
dressing object that should not be moved as cargo.
## Optional Per-Node Overrides
The default naming convention should cover normal missions. If a specific
mission needs another prefix or different pricing, set variables on the
transport node object:
```sqf
this setVariable ["isTransport", true, true];
this setVariable ["transportLabel", "North Dock", true];
this setVariable ["transportNodePrefix", "dock", true];
this setVariable ["transportVehiclePrefix", "dock_vehicle", true];
this setVariable ["transportArrivalPrefix", "dock_arrival", true];
this setVariable ["transportMaxIndexedNodes", 4, true];
this setVariable ["transportBaseFare", 150, true];
this setVariable ["transportPricePerKm", 75, true];
this setVariable ["transportCargoRadius", 25, true];
this setVariable ["transportIncludeCargo", true, true];
```
Only use overrides when the default `transport*` convention is not appropriate.
## Image Checklist
Replace these placeholder image references after screenshots are captured:
![Placeholder: Eden transport node object placement](images/eden/transport_node_obj.svg)
![Placeholder: Eden transport node variable name field](images/eden/transport_node_var.svg)
![Placeholder: Eden transport arrival marker placement](images/eden/transport_arrival_marker.svg)
![Placeholder: Eden transport vehicle exclusion object variable name](images/eden/transport_vehicle_var.svg)
![Placeholder: Player actor menu transport action](images/player/transport_menu_action.svg)
![Placeholder: Player transport destination submenu](images/player/transport_destination_menu.svg)
![Placeholder: Player transport completion notification](images/player/transport_complete.svg)
## Mission-Side Code Requirement
No mission-side transport service, addAction script, or server event bridge is
required. The framework handles menu discovery, destination selection, pricing,
billing, cargo movement, and EventBus notifications.

View File

@ -26,6 +26,42 @@ Rules validated by the Rust service:
- `locker:get`, `locker:patch`, and `locker:remove` require an existing locker.
- `locker:remove` takes the classname directly, not a JSON object.
## Multiple Locker Objects
Editor-placed locker objects are templates and access points, not separate
durable inventories. During server post-init, any mission namespace object
whose variable name contains `locker` is hidden globally. During client setup,
each client reads the hidden object's classname, ASL position, vector direction,
and vector up, then creates a matching local locker object with
`createVehicleLocal`.
The local clone is the object the player actually opens. On `ContainerOpened`,
the client clears the clone and fills it from the player's UID-owned locker
state. On `ContainerClosed`, the client reads the clone's cargo and sends a
full locker override back to the server.
There is no explicit maximum number of editor-placed locker access points. The
practical limit is mission performance and how many local container objects are
reasonable for the scenario.
All locker access points load and save the same player locker, keyed by player
UID. Opening `locker`, `locker_hq`, or `locker_outpost_1` does not create
separate persistent inventories; those objects are separate local access clones
for the same underlying player locker.
## Store Grants and Duplicate Inventory
The store checkout path grants items to the UID-owned locker hot state, not to a
specific placed locker object. Item grants are merged by classname:
- buying a new classname adds one new locker entry
- buying an existing classname increases that entry's amount
- checkout fails if the result would exceed 25 unique classnames
Having more than one locker object on the map does not duplicate store grants.
Duplicate quantities can only come from repeated checkout requests or repeated
manual locker writes, not from the number of placed locker access points.
## Commands
All commands are called on the `locker` group.

View File

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="720" viewBox="0 0 1280 720" role="img" aria-labelledby="title desc">
<title id="title">Placeholder: Eden transport arrival marker placement</title>
<desc id="desc">Capture an Eden Editor screenshot showing the arrival marker location.</desc>
<rect width="1280" height="720" fill="#202733"/>
<rect x="80" y="70" width="1120" height="580" rx="8" fill="#2f3948" stroke="#7d8aa0" stroke-width="4" stroke-dasharray="18 14"/>
<text x="640" y="280" fill="#f3f5f7" font-family="Arial, sans-serif" font-size="48" text-anchor="middle">PLACEHOLDER IMAGE</text>
<text x="640" y="352" fill="#d7dde7" font-family="Arial, sans-serif" font-size="30" text-anchor="middle">Eden: transport arrival marker</text>
<text x="640" y="410" fill="#aeb9c9" font-family="Arial, sans-serif" font-size="24" text-anchor="middle">Show where players and cargo should spawn.</text>
</svg>

After

Width:  |  Height:  |  Size: 930 B

View File

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="720" viewBox="0 0 1280 720" role="img" aria-labelledby="title desc">
<title id="title">Placeholder: Eden transport node object placement</title>
<desc id="desc">Capture an Eden Editor screenshot showing the placed transport access object.</desc>
<rect width="1280" height="720" fill="#202733"/>
<rect x="80" y="70" width="1120" height="580" rx="8" fill="#2f3948" stroke="#7d8aa0" stroke-width="4" stroke-dasharray="18 14"/>
<text x="640" y="280" fill="#f3f5f7" font-family="Arial, sans-serif" font-size="48" text-anchor="middle">PLACEHOLDER IMAGE</text>
<text x="640" y="352" fill="#d7dde7" font-family="Arial, sans-serif" font-size="30" text-anchor="middle">Eden: transport node object placement</text>
<text x="640" y="410" fill="#aeb9c9" font-family="Arial, sans-serif" font-size="24" text-anchor="middle">Show the object players interact with for transport.</text>
</svg>

After

Width:  |  Height:  |  Size: 951 B

View File

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="720" viewBox="0 0 1280 720" role="img" aria-labelledby="title desc">
<title id="title">Placeholder: Eden transport node variable name</title>
<desc id="desc">Capture the Eden object attributes panel with the transport variable name.</desc>
<rect width="1280" height="720" fill="#202733"/>
<rect x="80" y="70" width="1120" height="580" rx="8" fill="#2f3948" stroke="#7d8aa0" stroke-width="4" stroke-dasharray="18 14"/>
<text x="640" y="280" fill="#f3f5f7" font-family="Arial, sans-serif" font-size="48" text-anchor="middle">PLACEHOLDER IMAGE</text>
<text x="640" y="352" fill="#d7dde7" font-family="Arial, sans-serif" font-size="30" text-anchor="middle">Eden: transport variable name</text>
<text x="640" y="410" fill="#aeb9c9" font-family="Arial, sans-serif" font-size="24" text-anchor="middle">Show transport, transport_1, through transport_10.</text>
</svg>

After

Width:  |  Height:  |  Size: 935 B

View File

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="720" viewBox="0 0 1280 720" role="img" aria-labelledby="title desc">
<title id="title">Placeholder: Eden transport vehicle variable name</title>
<desc id="desc">Capture the variable name for the transport vehicle that should be excluded from cargo scans.</desc>
<rect width="1280" height="720" fill="#202733"/>
<rect x="80" y="70" width="1120" height="580" rx="8" fill="#2f3948" stroke="#7d8aa0" stroke-width="4" stroke-dasharray="18 14"/>
<text x="640" y="280" fill="#f3f5f7" font-family="Arial, sans-serif" font-size="48" text-anchor="middle">PLACEHOLDER IMAGE</text>
<text x="640" y="352" fill="#d7dde7" font-family="Arial, sans-serif" font-size="30" text-anchor="middle">Eden: transport_vehicle variable</text>
<text x="640" y="410" fill="#aeb9c9" font-family="Arial, sans-serif" font-size="24" text-anchor="middle">Show transport_vehicle, transport_vehicle_1, etc.</text>
</svg>

After

Width:  |  Height:  |  Size: 959 B

View File

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="720" viewBox="0 0 1280 720" role="img" aria-labelledby="title desc">
<title id="title">Placeholder: Player transport completion</title>
<desc id="desc">Capture the player and cargo after a successful transport request.</desc>
<rect width="1280" height="720" fill="#202733"/>
<rect x="80" y="70" width="1120" height="580" rx="8" fill="#2f3948" stroke="#7d8aa0" stroke-width="4" stroke-dasharray="18 14"/>
<text x="640" y="280" fill="#f3f5f7" font-family="Arial, sans-serif" font-size="48" text-anchor="middle">PLACEHOLDER IMAGE</text>
<text x="640" y="352" fill="#d7dde7" font-family="Arial, sans-serif" font-size="30" text-anchor="middle">Player: transport complete</text>
<text x="640" y="410" fill="#aeb9c9" font-family="Arial, sans-serif" font-size="24" text-anchor="middle">Show arrival at the destination with moved vehicles nearby.</text>
</svg>

After

Width:  |  Height:  |  Size: 927 B

View File

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="720" viewBox="0 0 1280 720" role="img" aria-labelledby="title desc">
<title id="title">Placeholder: Player transport destination menu</title>
<desc id="desc">Capture the destination submenu after selecting Transport.</desc>
<rect width="1280" height="720" fill="#202733"/>
<rect x="80" y="70" width="1120" height="580" rx="8" fill="#2f3948" stroke="#7d8aa0" stroke-width="4" stroke-dasharray="18 14"/>
<text x="640" y="280" fill="#f3f5f7" font-family="Arial, sans-serif" font-size="48" text-anchor="middle">PLACEHOLDER IMAGE</text>
<text x="640" y="352" fill="#d7dde7" font-family="Arial, sans-serif" font-size="30" text-anchor="middle">Player: destination submenu</text>
<text x="640" y="410" fill="#aeb9c9" font-family="Arial, sans-serif" font-size="24" text-anchor="middle">Show destination choices and the Back action.</text>
</svg>

After

Width:  |  Height:  |  Size: 912 B

View File

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="720" viewBox="0 0 1280 720" role="img" aria-labelledby="title desc">
<title id="title">Placeholder: Player transport menu action</title>
<desc id="desc">Capture the actor interaction menu showing the Transport action.</desc>
<rect width="1280" height="720" fill="#202733"/>
<rect x="80" y="70" width="1120" height="580" rx="8" fill="#2f3948" stroke="#7d8aa0" stroke-width="4" stroke-dasharray="18 14"/>
<text x="640" y="280" fill="#f3f5f7" font-family="Arial, sans-serif" font-size="48" text-anchor="middle">PLACEHOLDER IMAGE</text>
<text x="640" y="352" fill="#d7dde7" font-family="Arial, sans-serif" font-size="30" text-anchor="middle">Player: Transport action</text>
<text x="640" y="410" fill="#aeb9c9" font-family="Arial, sans-serif" font-size="24" text-anchor="middle">Show the first interaction menu near a transport node.</text>
</svg>

After

Width:  |  Height:  |  Size: 919 B

View File

@ -19,17 +19,21 @@ const generatedPages = [
source: 'docs/DEVELOPMENT_GUIDE.md',
target: '1.getting-started/3.development.md'
},
{
source: 'docs/GIT_WORKFLOW.md',
target: '1.getting-started/4.git-workflow.md'
},
{
source: 'docs/MISSION_DESIGNER_GUIDE.md',
target: '1.getting-started/4.mission-designer.md'
target: '1.getting-started/5.mission-designer.md'
},
{
source: 'docs/PLAYER_GUIDE.md',
target: '1.getting-started/5.player-guide.md'
target: '1.getting-started/6.player-guide.md'
},
{
source: 'docs/surrealdb-setup.md',
target: '1.getting-started/6.surrealdb-setup.md'
target: '1.getting-started/7.surrealdb-setup.md'
},
{
source: 'arma/server/docs/README.md',
@ -95,6 +99,10 @@ const generatedPages = [
source: 'docs/TASK_USAGE_GUIDE.md',
target: '3.server-modules/11.task.md'
},
{
source: 'docs/TRANSPORT_SERVICE_GUIDE.md',
target: '3.server-modules/12.transport-service.md'
},
{
source: 'docs/CLIENT_USAGE_GUIDE.md',
target: '4.client-addons/0.index.md'
@ -616,6 +624,16 @@ Most modules follow the same shape:
---
Task catalog, ownership, status transitions, defuse counters, and rewards.
:::
:::u-page-card
---
icon: i-lucide-route
title: Transport Service
to: /server-modules/transport-service
---
Paid point-to-point player and cargo transport configured through Eden
objects and arrival markers.
:::
::
`
},