From c0dd7821032282f8c2bd650315d1cd902a24cfa9 Mon Sep 17 00:00:00 2001 From: Jacob Schmidt Date: Mon, 25 May 2026 13:27:34 -0500 Subject: [PATCH] Add framework transport service --- .../actor/functions/fnc_handleUIEvents.sqf | 31 ++ .../actor/functions/fnc_initRepository.sqf | 55 +++ arma/client/addons/actor/ui/_site/script.js | 45 ++ arma/server/addons/transport/$PBOPREFIX$ | 1 + .../addons/transport/CfgEventHandlers.hpp | 17 + arma/server/addons/transport/XEH_PREP.hpp | 2 + arma/server/addons/transport/XEH_postInit.sqf | 4 + arma/server/addons/transport/XEH_preInit.sqf | 22 + arma/server/addons/transport/XEH_preStart.sqf | 1 + arma/server/addons/transport/config.cpp | 22 + .../functions/fnc_initTransportService.sqf | 399 ++++++++++++++++++ .../functions/fnc_requestTransport.sqf | 33 ++ .../addons/transport/script_component.hpp | 9 + docs/LOCKER_USAGE_GUIDE.md | 36 ++ docs/MISSION_DESIGNER_GUIDE.md | 72 +++- docs/PLAYER_GUIDE.md | 30 ++ docs/TRANSPORT_SERVICE_GUIDE.md | 135 ++++++ docs/images/eden/transport_arrival_marker.svg | 9 + docs/images/eden/transport_node_obj.svg | 9 + docs/images/eden/transport_node_var.svg | 9 + docs/images/eden/transport_vehicle_var.svg | 9 + docs/images/player/transport_complete.svg | 9 + .../player/transport_destination_menu.svg | 9 + docs/images/player/transport_menu_action.svg | 9 + .../1.getting-started/3.development.md | 46 ++ .../1.getting-started/4.git-workflow.md | 158 +++++++ ...sion-designer.md => 5.mission-designer.md} | 72 +++- .../{5.player-guide.md => 6.player-guide.md} | 30 ++ ...urrealdb-setup.md => 7.surrealdb-setup.md} | 0 docus/content/3.server-modules/0.index.md | 10 + .../3.server-modules/12.transport-service.md | 134 ++++++ docus/content/3.server-modules/6.locker.md | 36 ++ .../images/eden/transport_arrival_marker.svg | 9 + .../public/images/eden/transport_node_obj.svg | 9 + .../public/images/eden/transport_node_var.svg | 9 + .../images/eden/transport_vehicle_var.svg | 9 + .../images/player/transport_complete.svg | 9 + .../player/transport_destination_menu.svg | 9 + .../images/player/transport_menu_action.svg | 9 + tools/sync-docus-docs.mjs | 24 +- 40 files changed, 1545 insertions(+), 5 deletions(-) create mode 100644 arma/server/addons/transport/$PBOPREFIX$ create mode 100644 arma/server/addons/transport/CfgEventHandlers.hpp create mode 100644 arma/server/addons/transport/XEH_PREP.hpp create mode 100644 arma/server/addons/transport/XEH_postInit.sqf create mode 100644 arma/server/addons/transport/XEH_preInit.sqf create mode 100644 arma/server/addons/transport/XEH_preStart.sqf create mode 100644 arma/server/addons/transport/config.cpp create mode 100644 arma/server/addons/transport/functions/fnc_initTransportService.sqf create mode 100644 arma/server/addons/transport/functions/fnc_requestTransport.sqf create mode 100644 arma/server/addons/transport/script_component.hpp create mode 100644 docs/TRANSPORT_SERVICE_GUIDE.md create mode 100644 docs/images/eden/transport_arrival_marker.svg create mode 100644 docs/images/eden/transport_node_obj.svg create mode 100644 docs/images/eden/transport_node_var.svg create mode 100644 docs/images/eden/transport_vehicle_var.svg create mode 100644 docs/images/player/transport_complete.svg create mode 100644 docs/images/player/transport_destination_menu.svg create mode 100644 docs/images/player/transport_menu_action.svg create mode 100644 docus/content/1.getting-started/4.git-workflow.md rename docus/content/1.getting-started/{4.mission-designer.md => 5.mission-designer.md} (92%) rename docus/content/1.getting-started/{5.player-guide.md => 6.player-guide.md} (89%) rename docus/content/1.getting-started/{6.surrealdb-setup.md => 7.surrealdb-setup.md} (100%) create mode 100644 docus/content/3.server-modules/12.transport-service.md create mode 100644 docus/public/images/eden/transport_arrival_marker.svg create mode 100644 docus/public/images/eden/transport_node_obj.svg create mode 100644 docus/public/images/eden/transport_node_var.svg create mode 100644 docus/public/images/eden/transport_vehicle_var.svg create mode 100644 docus/public/images/player/transport_complete.svg create mode 100644 docus/public/images/player/transport_destination_menu.svg create mode 100644 docus/public/images/player/transport_menu_action.svg diff --git a/arma/client/addons/actor/functions/fnc_handleUIEvents.sqf b/arma/client/addons/actor/functions/fnc_handleUIEvents.sqf index cf26ae7..384bd67 100644 --- a/arma/client/addons/actor/functions/fnc_handleUIEvents.sqf +++ b/arma/client/addons/actor/functions/fnc_handleUIEvents.sqf @@ -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]; }; }; diff --git a/arma/client/addons/actor/functions/fnc_initRepository.sqf b/arma/client/addons/actor/functions/fnc_initRepository.sqf index 9e1de3b..edb9284 100644 --- a/arma/client/addons/actor/functions/fnc_initRepository.sqf +++ b/arma/client/addons/actor/functions/fnc_initRepository.sqf @@ -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); diff --git a/arma/client/addons/actor/ui/_site/script.js b/arma/client/addons/actor/ui/_site/script.js index be33f4f..3696a2a 100644 --- a/arma/client/addons/actor/ui/_site/script.js +++ b/arma/client/addons/actor/ui/_site/script.js @@ -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 || {}, diff --git a/arma/server/addons/transport/$PBOPREFIX$ b/arma/server/addons/transport/$PBOPREFIX$ new file mode 100644 index 0000000..adb02f6 --- /dev/null +++ b/arma/server/addons/transport/$PBOPREFIX$ @@ -0,0 +1 @@ +forge\forge_server\addons\transport diff --git a/arma/server/addons/transport/CfgEventHandlers.hpp b/arma/server/addons/transport/CfgEventHandlers.hpp new file mode 100644 index 0000000..f6503c2 --- /dev/null +++ b/arma/server/addons/transport/CfgEventHandlers.hpp @@ -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)); + }; +}; diff --git a/arma/server/addons/transport/XEH_PREP.hpp b/arma/server/addons/transport/XEH_PREP.hpp new file mode 100644 index 0000000..66d8227 --- /dev/null +++ b/arma/server/addons/transport/XEH_PREP.hpp @@ -0,0 +1,2 @@ +PREP(initTransportService); +PREP(requestTransport); diff --git a/arma/server/addons/transport/XEH_postInit.sqf b/arma/server/addons/transport/XEH_postInit.sqf new file mode 100644 index 0000000..39b097f --- /dev/null +++ b/arma/server/addons/transport/XEH_postInit.sqf @@ -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); }; diff --git a/arma/server/addons/transport/XEH_preInit.sqf b/arma/server/addons/transport/XEH_preInit.sqf new file mode 100644 index 0000000..f288028 --- /dev/null +++ b/arma/server/addons/transport/XEH_preInit.sqf @@ -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); diff --git a/arma/server/addons/transport/XEH_preStart.sqf b/arma/server/addons/transport/XEH_preStart.sqf new file mode 100644 index 0000000..421c54b --- /dev/null +++ b/arma/server/addons/transport/XEH_preStart.sqf @@ -0,0 +1 @@ +#include "script_component.hpp" diff --git a/arma/server/addons/transport/config.cpp b/arma/server/addons/transport/config.cpp new file mode 100644 index 0000000..5440687 --- /dev/null +++ b/arma/server/addons/transport/config.cpp @@ -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" diff --git a/arma/server/addons/transport/functions/fnc_initTransportService.sqf b/arma/server/addons/transport/functions/fnc_initTransportService.sqf new file mode 100644 index 0000000..e6f086e --- /dev/null +++ b/arma/server/addons/transport/functions/fnc_initTransportService.sqf @@ -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) diff --git a/arma/server/addons/transport/functions/fnc_requestTransport.sqf b/arma/server/addons/transport/functions/fnc_requestTransport.sqf new file mode 100644 index 0000000..1e5c074 --- /dev/null +++ b/arma/server/addons/transport/functions/fnc_requestTransport.sqf @@ -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 + * 1: Origin node + * 2: Destination node + * 3: Options (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]] diff --git a/arma/server/addons/transport/script_component.hpp b/arma/server/addons/transport/script_component.hpp new file mode 100644 index 0000000..8afb3c4 --- /dev/null +++ b/arma/server/addons/transport/script_component.hpp @@ -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" diff --git a/docs/LOCKER_USAGE_GUIDE.md b/docs/LOCKER_USAGE_GUIDE.md index 4424596..f2fba71 100644 --- a/docs/LOCKER_USAGE_GUIDE.md +++ b/docs/LOCKER_USAGE_GUIDE.md @@ -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. diff --git a/docs/MISSION_DESIGNER_GUIDE.md b/docs/MISSION_DESIGNER_GUIDE.md index f659210..6be9a86 100644 --- a/docs/MISSION_DESIGNER_GUIDE.md +++ b/docs/MISSION_DESIGNER_GUIDE.md @@ -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 diff --git a/docs/PLAYER_GUIDE.md b/docs/PLAYER_GUIDE.md index ccfa656..277888d 100644 --- a/docs/PLAYER_GUIDE.md +++ b/docs/PLAYER_GUIDE.md @@ -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 diff --git a/docs/TRANSPORT_SERVICE_GUIDE.md b/docs/TRANSPORT_SERVICE_GUIDE.md new file mode 100644 index 0000000..67884c3 --- /dev/null +++ b/docs/TRANSPORT_SERVICE_GUIDE.md @@ -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. diff --git a/docs/images/eden/transport_arrival_marker.svg b/docs/images/eden/transport_arrival_marker.svg new file mode 100644 index 0000000..57d8bf6 --- /dev/null +++ b/docs/images/eden/transport_arrival_marker.svg @@ -0,0 +1,9 @@ + + Placeholder: Eden transport arrival marker placement + Capture an Eden Editor screenshot showing the arrival marker location. + + + PLACEHOLDER IMAGE + Eden: transport arrival marker + Show where players and cargo should spawn. + diff --git a/docs/images/eden/transport_node_obj.svg b/docs/images/eden/transport_node_obj.svg new file mode 100644 index 0000000..2cc6d13 --- /dev/null +++ b/docs/images/eden/transport_node_obj.svg @@ -0,0 +1,9 @@ + + Placeholder: Eden transport node object placement + Capture an Eden Editor screenshot showing the placed transport access object. + + + PLACEHOLDER IMAGE + Eden: transport node object placement + Show the object players interact with for transport. + diff --git a/docs/images/eden/transport_node_var.svg b/docs/images/eden/transport_node_var.svg new file mode 100644 index 0000000..e6ce554 --- /dev/null +++ b/docs/images/eden/transport_node_var.svg @@ -0,0 +1,9 @@ + + Placeholder: Eden transport node variable name + Capture the Eden object attributes panel with the transport variable name. + + + PLACEHOLDER IMAGE + Eden: transport variable name + Show transport, transport_1, through transport_10. + diff --git a/docs/images/eden/transport_vehicle_var.svg b/docs/images/eden/transport_vehicle_var.svg new file mode 100644 index 0000000..71866a0 --- /dev/null +++ b/docs/images/eden/transport_vehicle_var.svg @@ -0,0 +1,9 @@ + + Placeholder: Eden transport vehicle variable name + Capture the variable name for the transport vehicle that should be excluded from cargo scans. + + + PLACEHOLDER IMAGE + Eden: transport_vehicle variable + Show transport_vehicle, transport_vehicle_1, etc. + diff --git a/docs/images/player/transport_complete.svg b/docs/images/player/transport_complete.svg new file mode 100644 index 0000000..be12ffd --- /dev/null +++ b/docs/images/player/transport_complete.svg @@ -0,0 +1,9 @@ + + Placeholder: Player transport completion + Capture the player and cargo after a successful transport request. + + + PLACEHOLDER IMAGE + Player: transport complete + Show arrival at the destination with moved vehicles nearby. + diff --git a/docs/images/player/transport_destination_menu.svg b/docs/images/player/transport_destination_menu.svg new file mode 100644 index 0000000..8bbe5d9 --- /dev/null +++ b/docs/images/player/transport_destination_menu.svg @@ -0,0 +1,9 @@ + + Placeholder: Player transport destination menu + Capture the destination submenu after selecting Transport. + + + PLACEHOLDER IMAGE + Player: destination submenu + Show destination choices and the Back action. + diff --git a/docs/images/player/transport_menu_action.svg b/docs/images/player/transport_menu_action.svg new file mode 100644 index 0000000..8b193e5 --- /dev/null +++ b/docs/images/player/transport_menu_action.svg @@ -0,0 +1,9 @@ + + Placeholder: Player transport menu action + Capture the actor interaction menu showing the Transport action. + + + PLACEHOLDER IMAGE + Player: Transport action + Show the first interaction menu near a transport node. + diff --git a/docus/content/1.getting-started/3.development.md b/docus/content/1.getting-started/3.development.md index d9bd7e0..0c8a1b7 100644 --- a/docus/content/1.getting-started/3.development.md +++ b/docus/content/1.getting-started/3.development.md @@ -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 diff --git a/docus/content/1.getting-started/4.git-workflow.md b/docus/content/1.getting-started/4.git-workflow.md new file mode 100644 index 0000000..4f3e87b --- /dev/null +++ b/docus/content/1.getting-started/4.git-workflow.md @@ -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 +``` + diff --git a/docus/content/1.getting-started/4.mission-designer.md b/docus/content/1.getting-started/5.mission-designer.md similarity index 92% rename from docus/content/1.getting-started/4.mission-designer.md rename to docus/content/1.getting-started/5.mission-designer.md index 2db17c5..6293ad5 100644 --- a/docus/content/1.getting-started/4.mission-designer.md +++ b/docus/content/1.getting-started/5.mission-designer.md @@ -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 diff --git a/docus/content/1.getting-started/5.player-guide.md b/docus/content/1.getting-started/6.player-guide.md similarity index 89% rename from docus/content/1.getting-started/5.player-guide.md rename to docus/content/1.getting-started/6.player-guide.md index cba668d..0984bd6 100644 --- a/docus/content/1.getting-started/5.player-guide.md +++ b/docus/content/1.getting-started/6.player-guide.md @@ -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 diff --git a/docus/content/1.getting-started/6.surrealdb-setup.md b/docus/content/1.getting-started/7.surrealdb-setup.md similarity index 100% rename from docus/content/1.getting-started/6.surrealdb-setup.md rename to docus/content/1.getting-started/7.surrealdb-setup.md diff --git a/docus/content/3.server-modules/0.index.md b/docus/content/3.server-modules/0.index.md index ae13769..1e36c84 100644 --- a/docus/content/3.server-modules/0.index.md +++ b/docus/content/3.server-modules/0.index.md @@ -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. + ::: :: diff --git a/docus/content/3.server-modules/12.transport-service.md b/docus/content/3.server-modules/12.transport-service.md new file mode 100644 index 0000000..f6b8cb7 --- /dev/null +++ b/docus/content/3.server-modules/12.transport-service.md @@ -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. diff --git a/docus/content/3.server-modules/6.locker.md b/docus/content/3.server-modules/6.locker.md index bcda611..35b2192 100644 --- a/docus/content/3.server-modules/6.locker.md +++ b/docus/content/3.server-modules/6.locker.md @@ -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. diff --git a/docus/public/images/eden/transport_arrival_marker.svg b/docus/public/images/eden/transport_arrival_marker.svg new file mode 100644 index 0000000..57d8bf6 --- /dev/null +++ b/docus/public/images/eden/transport_arrival_marker.svg @@ -0,0 +1,9 @@ + + Placeholder: Eden transport arrival marker placement + Capture an Eden Editor screenshot showing the arrival marker location. + + + PLACEHOLDER IMAGE + Eden: transport arrival marker + Show where players and cargo should spawn. + diff --git a/docus/public/images/eden/transport_node_obj.svg b/docus/public/images/eden/transport_node_obj.svg new file mode 100644 index 0000000..2cc6d13 --- /dev/null +++ b/docus/public/images/eden/transport_node_obj.svg @@ -0,0 +1,9 @@ + + Placeholder: Eden transport node object placement + Capture an Eden Editor screenshot showing the placed transport access object. + + + PLACEHOLDER IMAGE + Eden: transport node object placement + Show the object players interact with for transport. + diff --git a/docus/public/images/eden/transport_node_var.svg b/docus/public/images/eden/transport_node_var.svg new file mode 100644 index 0000000..e6ce554 --- /dev/null +++ b/docus/public/images/eden/transport_node_var.svg @@ -0,0 +1,9 @@ + + Placeholder: Eden transport node variable name + Capture the Eden object attributes panel with the transport variable name. + + + PLACEHOLDER IMAGE + Eden: transport variable name + Show transport, transport_1, through transport_10. + diff --git a/docus/public/images/eden/transport_vehicle_var.svg b/docus/public/images/eden/transport_vehicle_var.svg new file mode 100644 index 0000000..71866a0 --- /dev/null +++ b/docus/public/images/eden/transport_vehicle_var.svg @@ -0,0 +1,9 @@ + + Placeholder: Eden transport vehicle variable name + Capture the variable name for the transport vehicle that should be excluded from cargo scans. + + + PLACEHOLDER IMAGE + Eden: transport_vehicle variable + Show transport_vehicle, transport_vehicle_1, etc. + diff --git a/docus/public/images/player/transport_complete.svg b/docus/public/images/player/transport_complete.svg new file mode 100644 index 0000000..be12ffd --- /dev/null +++ b/docus/public/images/player/transport_complete.svg @@ -0,0 +1,9 @@ + + Placeholder: Player transport completion + Capture the player and cargo after a successful transport request. + + + PLACEHOLDER IMAGE + Player: transport complete + Show arrival at the destination with moved vehicles nearby. + diff --git a/docus/public/images/player/transport_destination_menu.svg b/docus/public/images/player/transport_destination_menu.svg new file mode 100644 index 0000000..8bbe5d9 --- /dev/null +++ b/docus/public/images/player/transport_destination_menu.svg @@ -0,0 +1,9 @@ + + Placeholder: Player transport destination menu + Capture the destination submenu after selecting Transport. + + + PLACEHOLDER IMAGE + Player: destination submenu + Show destination choices and the Back action. + diff --git a/docus/public/images/player/transport_menu_action.svg b/docus/public/images/player/transport_menu_action.svg new file mode 100644 index 0000000..8b193e5 --- /dev/null +++ b/docus/public/images/player/transport_menu_action.svg @@ -0,0 +1,9 @@ + + Placeholder: Player transport menu action + Capture the actor interaction menu showing the Transport action. + + + PLACEHOLDER IMAGE + Player: Transport action + Show the first interaction menu near a transport node. + diff --git a/tools/sync-docus-docs.mjs b/tools/sync-docus-docs.mjs index c6c5228..171f8e0 100644 --- a/tools/sync-docus-docs.mjs +++ b/tools/sync-docus-docs.mjs @@ -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. + ::: :: ` },