Compare commits
3 Commits
f2ac9fcbe7
...
582dd39640
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
582dd39640 | ||
|
|
89e3f794dc | ||
|
|
c0dd782103 |
@ -58,6 +58,37 @@ switch (_event) do {
|
|||||||
case "actor::open::phone": { [] spawn EFUNC(phone,openUI); };
|
case "actor::open::phone": { [] spawn EFUNC(phone,openUI); };
|
||||||
case "actor::open::iplayer": { hint "Player interaction is not yet implemented." };
|
case "actor::open::iplayer": { hint "Player interaction is not yet implemented." };
|
||||||
case "actor::open::store": { [] spawn EFUNC(store,openUI); };
|
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]; };
|
default { hint format ["Unhandled UI event: %1", _event]; };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -121,6 +121,12 @@ GVAR(ActorRepositoryBaseClass) = compileFinal createHashMapFromArray [
|
|||||||
];
|
];
|
||||||
private _deviceType = _x getVariable ["deviceType", ""];
|
private _deviceType = _x getVariable ["deviceType", ""];
|
||||||
private _isPlayer = _x isKindOf "Man" && isPlayer _x;
|
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 (_isStore) then { _nearbyActions pushBack ["store", true]; };
|
||||||
if (_isAtm) then { _nearbyActions pushBack ["atm", 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) then { _nearbyActions pushBack ["garage", _garageContext]; };
|
||||||
if (_isGarage && GVAR(enableVG)) then { _nearbyActions pushBack ["vg", _garageContext]; };
|
if (_isGarage && GVAR(enableVG)) then { _nearbyActions pushBack ["vg", _garageContext]; };
|
||||||
if (_deviceType isNotEqualTo "") then { _nearbyActions pushBack ["device", _deviceType]; };
|
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]; };
|
if (_isPlayer && { _x isNotEqualTo player }) then { _nearbyActions pushBack ["player", name _x]; };
|
||||||
} forEach (player nearObjects 5);
|
} forEach (player nearObjects 5);
|
||||||
|
|
||||||
|
|||||||
@ -193,12 +193,19 @@ const actionDefinitions = {
|
|||||||
description: "Access your virtual garage",
|
description: "Access your virtual garage",
|
||||||
action: "actor::open::vgarage",
|
action: "actor::open::vgarage",
|
||||||
},
|
},
|
||||||
|
transport: {
|
||||||
|
id: "transport",
|
||||||
|
title: "Transport",
|
||||||
|
description: "Show available travel destinations",
|
||||||
|
action: "actor::show::transport",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
availableActions: [],
|
availableActions: [],
|
||||||
menuItems: [...baseMenuItems],
|
menuItems: [...baseMenuItems],
|
||||||
baseMenuItems: [...baseMenuItems],
|
baseMenuItems: [...baseMenuItems],
|
||||||
|
defaultMenuItems: [...baseMenuItems],
|
||||||
actionDefinitions: { ...actionDefinitions },
|
actionDefinitions: { ...actionDefinitions },
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -244,6 +251,7 @@ function actorReducer(state = initialState, action) {
|
|||||||
...state,
|
...state,
|
||||||
availableActions: action.payload,
|
availableActions: action.payload,
|
||||||
menuItems: newMenuItems,
|
menuItems: newMenuItems,
|
||||||
|
defaultMenuItems: newMenuItems,
|
||||||
};
|
};
|
||||||
|
|
||||||
case ActionTypes.SET_MENU_ITEMS:
|
case ActionTypes.SET_MENU_ITEMS:
|
||||||
@ -426,6 +434,43 @@ function RadialMenu() {
|
|||||||
|
|
||||||
const handleItemClick = (item) => {
|
const handleItemClick = (item) => {
|
||||||
console.log("Menu item clicked:", 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 = {
|
const alert = {
|
||||||
event: item.action,
|
event: item.action,
|
||||||
data: item.context || {},
|
data: item.context || {},
|
||||||
|
|||||||
1
arma/server/addons/transport/$PBOPREFIX$
Normal file
@ -0,0 +1 @@
|
|||||||
|
forge\forge_server\addons\transport
|
||||||
17
arma/server/addons/transport/CfgEventHandlers.hpp
Normal 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));
|
||||||
|
};
|
||||||
|
};
|
||||||
2
arma/server/addons/transport/XEH_PREP.hpp
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
PREP(initTransportService);
|
||||||
|
PREP(requestTransport);
|
||||||
4
arma/server/addons/transport/XEH_postInit.sqf
Normal 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); };
|
||||||
22
arma/server/addons/transport/XEH_preInit.sqf
Normal 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);
|
||||||
1
arma/server/addons/transport/XEH_preStart.sqf
Normal file
@ -0,0 +1 @@
|
|||||||
|
#include "script_component.hpp"
|
||||||
22
arma/server/addons/transport/config.cpp
Normal 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"
|
||||||
@ -0,0 +1,408 @@
|
|||||||
|
#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
|
||||||
|
};
|
||||||
|
|
||||||
|
private _personalSourceAttempted = false;
|
||||||
|
private _allowOrgFallback = false;
|
||||||
|
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 _bankBalance = _account getOrDefault ["bank", 0];
|
||||||
|
private _cashBalance = _account getOrDefault ["cash", 0];
|
||||||
|
private _source = ["", "bank"] select (_bankBalance >= _amount);
|
||||||
|
if (_source isEqualTo "" && { _cashBalance >= _amount }) then {
|
||||||
|
_source = "cash";
|
||||||
|
};
|
||||||
|
|
||||||
|
if (_source isNotEqualTo "") then {
|
||||||
|
_personalSourceAttempted = true;
|
||||||
|
private _charge = EGVAR(bank,BankStore) call ["chargeCheckout", [_uid, _source, _amount, true]];
|
||||||
|
if (_charge getOrDefault ["success", false]) then {
|
||||||
|
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]];
|
||||||
|
} else {
|
||||||
|
_result set ["message", _charge getOrDefault ["message", format ["Unable to charge %1 from your %2.", _label, _source]]];
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
_allowOrgFallback = true;
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
_result set ["message", "Bank account could not be loaded for transport billing."];
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
_result set ["message", "Bank service is unavailable for transport billing."];
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!(_result getOrDefault ["success", false]) && { !_personalSourceAttempted } && { _allowOrgFallback } && { !(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)
|
||||||
@ -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]]
|
||||||
9
arma/server/addons/transport/script_component.hpp
Normal 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"
|
||||||
@ -6,7 +6,7 @@ handlers, and optional browser UI.
|
|||||||
|
|
||||||
## Runtime Flow
|
## Runtime Flow
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
```text
|
```text
|
||||||
Arma client UI or SQF action
|
Arma client UI or SQF action
|
||||||
|
|||||||
@ -27,6 +27,42 @@ Rules validated by the Rust service:
|
|||||||
- `locker:get`, `locker:patch`, and `locker:remove` require an existing locker.
|
- `locker:get`, `locker:patch`, and `locker:remove` require an existing locker.
|
||||||
- `locker:remove` takes the classname directly, not a JSON object.
|
- `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
|
## Commands
|
||||||
|
|
||||||
All commands are called on the `locker` group.
|
All commands are called on the `locker` group.
|
||||||
|
|||||||
@ -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. |
|
| 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. |
|
| 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. |
|
| 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:
|
Recommended object names:
|
||||||
|
|
||||||
@ -38,6 +39,8 @@ atm
|
|||||||
bank
|
bank
|
||||||
store
|
store
|
||||||
locker
|
locker
|
||||||
|
transport
|
||||||
|
transport_1
|
||||||
garage_hq
|
garage_hq
|
||||||
garage_hq_2
|
garage_hq_2
|
||||||
```
|
```
|
||||||
@ -60,6 +63,7 @@ _atmTerminal setVariable ["isAtm", true, true];
|
|||||||
_storeCounter setVariable ["isStore", true, true];
|
_storeCounter setVariable ["isStore", true, true];
|
||||||
_garageTerminal setVariable ["isGarage", true, true];
|
_garageTerminal setVariable ["isGarage", true, true];
|
||||||
_garageTerminal setVariable ["garageType", "cars", true];
|
_garageTerminal setVariable ["garageType", "cars", true];
|
||||||
|
_transportNode setVariable ["isTransport", true, true];
|
||||||
```
|
```
|
||||||
|
|
||||||
Supported garage types are:
|
Supported garage types are:
|
||||||
@ -142,6 +146,75 @@ Minimum Eden setup:
|
|||||||
2. Set its Eden variable name to something containing `store`.
|
2. Set its Eden variable name to something containing `store`.
|
||||||
3. Test that the actor menu shows the store action within 5 meters.
|
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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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 Setup
|
||||||
|
|
||||||
Bank and ATM objects intentionally expose different workflows.
|
Bank and ATM objects intentionally expose different workflows.
|
||||||
@ -179,7 +252,7 @@ Minimum Eden setup:
|
|||||||
Locker objects are slightly different from other interaction objects. The
|
Locker objects are slightly different from other interaction objects. The
|
||||||
server finds editor-placed objects whose variable names contain `locker`, hides
|
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
|
those global objects, and each client creates a local locker object at the same
|
||||||
position.
|
position using the placed object's classname and orientation.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@ -192,6 +265,11 @@ Minimum Eden setup:
|
|||||||
3. Do not use `forge_locker_box`.
|
3. Do not use `forge_locker_box`.
|
||||||
4. Test that the local locker appears and opens the virtual arsenal action.
|
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
|
## Medical Spawn Setup
|
||||||
|
|
||||||
The medical economy store discovers up to eleven medical spawn objects by exact
|
The medical economy store discovers up to eleven medical spawn objects by exact
|
||||||
|
|||||||
@ -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
|
spawn an unlocked vehicle, and use the garage to store or retrieve live world
|
||||||
vehicles.
|
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.
|
||||||
|
|
||||||
|

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

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

|
||||||
|
|
||||||
## Locker and Virtual Arsenal
|
## Locker and Virtual Arsenal
|
||||||
|
|
||||||
The locker is personal item storage.
|
The locker is personal item storage.
|
||||||
@ -225,6 +251,10 @@ Locker rules:
|
|||||||
- Up to 25 items can be stored.
|
- Up to 25 items can be stored.
|
||||||
- The locker saves when the locker container is closed.
|
- The locker saves when the locker container is closed.
|
||||||
- Over-capacity storage can warn or fail depending on server handling.
|
- 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
|
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
|
granted or have unlocked through systems such as the store. The virtual arsenal
|
||||||
|
|||||||
143
docs/TRANSPORT_SERVICE_GUIDE.md
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
## Reference Images
|
||||||
|
|
||||||
|
These screenshots show the default transport setup and player workflow:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 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.
|
||||||
1
docs/images/architecture-flow.svg
Normal file
|
After Width: | Height: | Size: 21 KiB |
1
docs/images/eden/.gitkeep
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
||||||
BIN
docs/images/eden/atm_obj.jpg
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
docs/images/eden/atm_obj_var.jpg
Normal file
|
After Width: | Height: | Size: 618 KiB |
BIN
docs/images/eden/attack_task_mod.jpg
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
docs/images/eden/attack_task_mod_params.jpg
Normal file
|
After Width: | Height: | Size: 565 KiB |
BIN
docs/images/eden/attack_task_tgts.jpg
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
docs/images/eden/bank_obj.jpg
Normal file
|
After Width: | Height: | Size: 443 KiB |
BIN
docs/images/eden/bank_obj_var.jpg
Normal file
|
After Width: | Height: | Size: 307 KiB |
BIN
docs/images/eden/blacklist_mrkr.jpg
Normal file
|
After Width: | Height: | Size: 680 KiB |
BIN
docs/images/eden/blacklist_mrkr_var.jpg
Normal file
|
After Width: | Height: | Size: 477 KiB |
BIN
docs/images/eden/cad-visible-task.jpg
Normal file
|
After Width: | Height: | Size: 827 KiB |
BIN
docs/images/eden/ceo_unit.jpg
Normal file
|
After Width: | Height: | Size: 560 KiB |
BIN
docs/images/eden/ceo_unit_var.jpg
Normal file
|
After Width: | Height: | Size: 352 KiB |
BIN
docs/images/eden/create_task_mod.jpg
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
docs/images/eden/create_task_mod_params.jpg
Normal file
|
After Width: | Height: | Size: 564 KiB |
BIN
docs/images/eden/defend_task_mod.jpg
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
docs/images/eden/defend_task_mod_params.jpg
Normal file
|
After Width: | Height: | Size: 695 KiB |
BIN
docs/images/eden/defend_zone_mrkr.jpg
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
docs/images/eden/defend_zone_mrkr_var.jpg
Normal file
|
After Width: | Height: | Size: 662 KiB |
BIN
docs/images/eden/defuse_explosives_mod.jpg
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
docs/images/eden/defuse_protected_mod.jpg
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
docs/images/eden/defuse_task_mod.jpg
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
docs/images/eden/defuse_task_mod_params.jpg
Normal file
|
After Width: | Height: | Size: 714 KiB |
BIN
docs/images/eden/delivery_cargo_mod.jpg
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
docs/images/eden/delivery_task_mod.jpg
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
docs/images/eden/delivery_task_mod_params.jpg
Normal file
|
After Width: | Height: | Size: 644 KiB |
BIN
docs/images/eden/delivery_zone_mrkr.jpg
Normal file
|
After Width: | Height: | Size: 956 KiB |
BIN
docs/images/eden/delivery_zone_mrkr_var.jpg
Normal file
|
After Width: | Height: | Size: 603 KiB |
BIN
docs/images/eden/destroy_task_mod.jpg
Normal file
|
After Width: | Height: | Size: 1008 KiB |
BIN
docs/images/eden/destroy_task_mod_params.jpg
Normal file
|
After Width: | Height: | Size: 646 KiB |
BIN
docs/images/eden/destroy_task_tgts.jpg
Normal file
|
After Width: | Height: | Size: 1018 KiB |
BIN
docs/images/eden/dispatch_unit.jpg
Normal file
|
After Width: | Height: | Size: 453 KiB |
BIN
docs/images/eden/dispatch_unit_var.jpg
Normal file
|
After Width: | Height: | Size: 294 KiB |
BIN
docs/images/eden/garage_obj.jpg
Normal file
|
After Width: | Height: | Size: 824 KiB |
BIN
docs/images/eden/garage_obj_1.jpg
Normal file
|
After Width: | Height: | Size: 840 KiB |
BIN
docs/images/eden/garage_obj_1_var.jpg
Normal file
|
After Width: | Height: | Size: 461 KiB |
BIN
docs/images/eden/garage_obj_2.jpg
Normal file
|
After Width: | Height: | Size: 738 KiB |
BIN
docs/images/eden/garage_obj_2_var.jpg
Normal file
|
After Width: | Height: | Size: 430 KiB |
BIN
docs/images/eden/garage_obj_var.jpg
Normal file
|
After Width: | Height: | Size: 443 KiB |
BIN
docs/images/eden/garage_spawn_1_mrkr.jpg
Normal file
|
After Width: | Height: | Size: 853 KiB |
BIN
docs/images/eden/garage_spawn_1_mrkr_var.jpg
Normal file
|
After Width: | Height: | Size: 463 KiB |
BIN
docs/images/eden/garage_spawn_2_mrkrs.jpg
Normal file
|
After Width: | Height: | Size: 738 KiB |
BIN
docs/images/eden/garage_spawn_mrkrs.jpg
Normal file
|
After Width: | Height: | Size: 827 KiB |
BIN
docs/images/eden/hostage_entities_mod.jpg
Normal file
|
After Width: | Height: | Size: 1013 KiB |
BIN
docs/images/eden/hostage_ext_zone_mrkr.jpg
Normal file
|
After Width: | Height: | Size: 851 KiB |
BIN
docs/images/eden/hostage_ext_zone_mrkr_var.jpg
Normal file
|
After Width: | Height: | Size: 558 KiB |
BIN
docs/images/eden/hostage_shooters_mod.jpg
Normal file
|
After Width: | Height: | Size: 1012 KiB |
BIN
docs/images/eden/hostage_task_mod.jpg
Normal file
|
After Width: | Height: | Size: 1012 KiB |
BIN
docs/images/eden/hostage_task_mod_params.jpg
Normal file
|
After Width: | Height: | Size: 640 KiB |
BIN
docs/images/eden/hvt_capture_task_mod.jpg
Normal file
|
After Width: | Height: | Size: 691 KiB |
BIN
docs/images/eden/hvt_capture_task_mod_params.jpg
Normal file
|
After Width: | Height: | Size: 517 KiB |
BIN
docs/images/eden/hvt_ext_zone_mrkr.jpg
Normal file
|
After Width: | Height: | Size: 851 KiB |
BIN
docs/images/eden/hvt_ext_zone_mrkr_var.jpg
Normal file
|
After Width: | Height: | Size: 558 KiB |
BIN
docs/images/eden/hvt_task_mod.jpg
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
docs/images/eden/hvt_task_mod_params.jpg
Normal file
|
After Width: | Height: | Size: 688 KiB |
BIN
docs/images/eden/locker_obj.jpg
Normal file
|
After Width: | Height: | Size: 563 KiB |
BIN
docs/images/eden/locker_obj_var.jpg
Normal file
|
After Width: | Height: | Size: 366 KiB |
BIN
docs/images/eden/med_spawn_obj.jpg
Normal file
|
After Width: | Height: | Size: 695 KiB |
BIN
docs/images/eden/med_spawn_obj_var.jpg
Normal file
|
After Width: | Height: | Size: 412 KiB |
BIN
docs/images/eden/store_obj.jpg
Normal file
|
After Width: | Height: | Size: 588 KiB |
BIN
docs/images/eden/store_obj_var.jpg
Normal file
|
After Width: | Height: | Size: 393 KiB |
BIN
docs/images/eden/transport_arrival_mrkr.jpg
Normal file
|
After Width: | Height: | Size: 797 KiB |
BIN
docs/images/eden/transport_arrival_mrkr_var.jpg
Normal file
|
After Width: | Height: | Size: 498 KiB |
BIN
docs/images/eden/transport_loc_1.jpg
Normal file
|
After Width: | Height: | Size: 791 KiB |
BIN
docs/images/eden/transport_loc_2.jpg
Normal file
|
After Width: | Height: | Size: 762 KiB |
BIN
docs/images/eden/transport_obj_1.jpg
Normal file
|
After Width: | Height: | Size: 791 KiB |
BIN
docs/images/eden/transport_obj_1_var.jpg
Normal file
|
After Width: | Height: | Size: 495 KiB |
BIN
docs/images/eden/transport_veh_obj.jpg
Normal file
|
After Width: | Height: | Size: 793 KiB |
BIN
docs/images/eden/transport_veh_obj_var.jpg
Normal file
|
After Width: | Height: | Size: 512 KiB |
BIN
docs/images/player/atm_app_home.jpg
Normal file
|
After Width: | Height: | Size: 974 KiB |
BIN
docs/images/player/atm_app_pin.jpg
Normal file
|
After Width: | Height: | Size: 964 KiB |
BIN
docs/images/player/bank_app.jpg
Normal file
|
After Width: | Height: | Size: 183 KiB |
BIN
docs/images/player/cad_dispatch_board.jpg
Normal file
|
After Width: | Height: | Size: 450 KiB |
BIN
docs/images/player/cad_ops_board.jpg
Normal file
|
After Width: | Height: | Size: 569 KiB |
BIN
docs/images/player/garage.jpg
Normal file
|
After Width: | Height: | Size: 194 KiB |
BIN
docs/images/player/interaction_menu.jpg
Normal file
|
After Width: | Height: | Size: 417 KiB |
BIN
docs/images/player/locker.jpg
Normal file
|
After Width: | Height: | Size: 383 KiB |
BIN
docs/images/player/medical_respawn.jpg
Normal file
|
After Width: | Height: | Size: 759 KiB |
BIN
docs/images/player/org_dashboard.jpg
Normal file
|
After Width: | Height: | Size: 148 KiB |
BIN
docs/images/player/org_home.jpg
Normal file
|
After Width: | Height: | Size: 153 KiB |
BIN
docs/images/player/org_registration.jpg
Normal file
|
After Width: | Height: | Size: 164 KiB |