feat(garage): enhance virtual garage functionality with spawn lane resolution and category handling

This commit is contained in:
Jacob Schmidt 2026-04-22 17:57:26 -05:00
parent 6be064fb15
commit f3247b8e54
8 changed files with 350 additions and 86 deletions

View File

@ -37,6 +37,11 @@ the client.
The client builds vehicle context and sends requests. The server garage addon
and extension own stored vehicle state.
Virtual garage spawning resolves the active garage context and category lane,
then finalizes only the vehicle selected in that BIS garage session. Nearby
world vehicles are ignored as spawn candidates and are only used for the spawn
blocking check at the resolved lane.
Refuel and repair buttons are available from the selected vehicle detail panel
for nearby world vehicles. Stored records must be retrieved before they can be
serviced because fuel and repair operate on live vehicle objects. Service

View File

@ -88,9 +88,21 @@ GVAR(GarageActionServiceBaseClass) = compileFinal createHashMapFromArray [
GVAR(GarageUIBridge) call ["sendEvent", ["garage::retrieve::failure", createHashMapFromArray [["message", "Stored vehicle record could not be found."]]]];
};
private _className = _vehicleData getOrDefault ["classname", ""];
if (_className isEqualTo "") exitWith {
GVAR(GarageUIBridge) call ["sendEvent", ["garage::retrieve::failure", createHashMapFromArray [["message", "Stored vehicle record is missing a classname."]]]];
};
private _context = GVAR(GarageContextService) call ["getContext", []];
private _spawnPosition = _context getOrDefault ["spawnPosition", getPosATL player];
private _spawnHeading = _context getOrDefault ["spawnHeading", getDir player];
private _vehicleCategory = GVAR(GarageHelperService) call ["resolveVGCategory", [_className]];
private _spawnLane = GVAR(GarageContextService) call ["getExactSpawnLane", [_vehicleCategory, _context]];
if (_spawnLane isEqualTo createHashMap) exitWith {
private _categoryLabel = GVAR(GarageHelperService) call ["resolveGarageCategoryLabel", [_vehicleCategory]];
GVAR(GarageUIBridge) call ["sendEvent", ["garage::retrieve::failure", createHashMapFromArray [["message", format ["This garage does not support spawning %1.", _categoryLabel]]]]];
};
private _spawnPosition = _spawnLane getOrDefault ["spawnPosition", _context getOrDefault ["spawnPosition", getPosATL player]];
private _spawnHeading = _spawnLane getOrDefault ["spawnHeading", _context getOrDefault ["spawnHeading", getDir player]];
private _spawnRadius = _context getOrDefault ["spawnRadius", 6];
private _blockingVehicles = [];
{ _blockingVehicles pushBackUnique _x; } forEach (_spawnPosition nearEntities [["Car", "Tank", "Air", "Ship"], _spawnRadius]);
@ -99,11 +111,6 @@ GVAR(GarageActionServiceBaseClass) = compileFinal createHashMapFromArray [
GVAR(GarageUIBridge) call ["sendEvent", ["garage::retrieve::failure", createHashMapFromArray [["message", "The garage spawn area is blocked."]]]];
};
private _className = _vehicleData getOrDefault ["classname", ""];
if (_className isEqualTo "") exitWith {
GVAR(GarageUIBridge) call ["sendEvent", ["garage::retrieve::failure", createHashMapFromArray [["message", "Stored vehicle record is missing a classname."]]]];
};
private _vehicle = createVehicle [_className, _spawnPosition, [], 0, "CAN_COLLIDE"];
_vehicle setDir _spawnHeading;
_vehicle setFuel (_vehicleData getOrDefault ["fuel", 0]);

View File

@ -29,81 +29,189 @@ GVAR(GarageContextServiceBaseClass) = compileFinal createHashMapFromArray [
["name", "Vehicle Garage"],
["anchorPosition", getPosATL player],
["sourceObject", objNull],
["garageType", ""],
["spawnHeading", getDir player],
["spawnPosition", player getPos [8, getDir player]],
["spawnLanes", createHashMap],
["spawnRadius", 6],
["nearbyRadius", 30]
["nearbyRadius", 30],
["laneRadius", 150]
]
}],
["scanEntryValues", compileFinal {
params [["_values", [], [[]]], ["_state", createHashMap, [createHashMap]]];
["findNearbyGarageObject", compileFinal {
private _nearestGarage = objNull;
private _nearestDistance = 1e10;
{
if (_x isEqualType "" && { (_state getOrDefault ["name", "Vehicle Garage"]) isEqualTo "Vehicle Garage" }) then { _state set ["name", _x]; };
if (_x isEqualType "") then {
private _resolvedObject = _state getOrDefault ["sourceObject", objNull];
if (isNull _resolvedObject) then {
private _namedObject = missionNamespace getVariable [_x, objNull];
if (!isNull _namedObject) then { _state set ["sourceObject", _namedObject]; };
};
if ((_state getOrDefault ["anchorPosition", []]) isEqualTo [] && { _x in allMapMarkers }) then { _state set ["anchorPosition", markerPos _x]; };
continue;
if (isNull _x || { !(_x getVariable ["isGarage", false]) }) then { continue; };
private _distance = player distance2D _x;
if (_distance < _nearestDistance) then {
_nearestDistance = _distance;
_nearestGarage = _x;
};
if (_x isEqualType objNull && { isNull (_state getOrDefault ["sourceObject", objNull]) }) then {
_state set ["sourceObject", _x];
if ((_state getOrDefault ["anchorPosition", []]) isEqualTo []) then { _state set ["anchorPosition", getPosATL _x]; };
continue;
};
if (_x isEqualType 0 && { (_state getOrDefault ["spawnHeading", -1]) < 0 }) then { _state set ["spawnHeading", _x]; continue; };
if (_x isEqualType [] && { count _x > 0 }) then {
if ({ _x isEqualType 0 } count _x >= 2 && { ((_state getOrDefault ["offset", []]) isEqualTo []) || ((_state getOrDefault ["anchorPosition", []]) isEqualTo []) }) then {
if ((_state getOrDefault ["anchorPosition", []]) isEqualTo []) then { _state set ["anchorPosition", _x]; } else { _state set ["offset", _x]; };
continue;
};
_self call ["scanEntryValues", [_x, _state]];
};
} forEach _values;
_state
} forEach (player nearObjects 12);
_nearestGarage
}],
["resolveEntry", compileFinal {
params [["_entry", [], [[]]]];
private _state = createHashMapFromArray [["name", "Vehicle Garage"], ["anchorPosition", []], ["sourceObject", objNull], ["offset", []], ["spawnHeading", -1]];
_self call ["scanEntryValues", [_entry, _state]];
private _anchorPosition = _state getOrDefault ["anchorPosition", []];
private _offset = _state getOrDefault ["offset", []];
private _spawnPosition = if (_anchorPosition isEqualTo []) then { [] } else { if (_offset isEqualTo []) then { _anchorPosition } else { _anchorPosition vectorAdd _offset } };
createHashMapFromArray [["name", _state getOrDefault ["name", "Vehicle Garage"]], ["anchorPosition", _anchorPosition], ["sourceObject", _state getOrDefault ["sourceObject", objNull]], ["spawnHeading", _state getOrDefault ["spawnHeading", -1]], ["spawnPosition", _spawnPosition]]
["resolveGarageName", compileFinal {
params [["_garageObject", objNull, [objNull]]];
if (isNull _garageObject) exitWith { "Vehicle Garage" };
private _displayName = _garageObject getVariable ["garageName", ""];
if (_displayName isNotEqualTo "") exitWith { _displayName };
private _varName = vehicleVarName _garageObject;
if (_varName isEqualTo "") exitWith { "Vehicle Garage" };
_varName
}],
["buildMarkerLane", compileFinal {
params [["_markerName", "", [""]], ["_garageObject", objNull, [objNull]]];
if (_markerName isEqualTo "" || { markerShape _markerName isEqualTo "" }) exitWith { createHashMap };
private _spawnCategory = GVAR(GarageHelperService) call ["inferGarageCategory", [_markerName]];
if (_spawnCategory isEqualTo "") exitWith { createHashMap };
private _spawnPosition = markerPos _markerName;
private _interactionPosition = if (isNull _garageObject) then { _spawnPosition } else { getPosATL _garageObject };
private _markerDistance = if (isNull _garageObject) then { player distance2D _spawnPosition } else { _garageObject distance2D _spawnPosition };
private _garageVarName = if (isNull _garageObject) then { "" } else { toLowerANSI (vehicleVarName _garageObject) };
private _markerKey = toLowerANSI _markerName;
private _nameScore = 0;
if (_garageVarName isNotEqualTo "" && { (_markerKey find _garageVarName) >= 0 }) then {
_nameScore = -50;
};
createHashMapFromArray [
["name", _markerName],
["interactionPosition", _interactionPosition],
["sourceObject", _garageObject],
["spawnCategory", _spawnCategory],
["spawnHeading", markerDir _markerName],
["spawnPosition", _spawnPosition],
["score", _markerDistance + _nameScore]
]
}],
["discoverSpawnLanes", compileFinal {
params [["_garageObject", objNull, [objNull]]];
private _laneRadius = (_self call ["createDefaultContext", []]) getOrDefault ["laneRadius", 150];
private _lanes = createHashMap;
{
private _markerName = _x;
if ((toLowerANSI _markerName find "garage") < 0) then { continue; };
private _entry = _self call ["buildMarkerLane", [_markerName, _garageObject]];
if (_entry isEqualTo createHashMap) then { continue; };
private _spawnPosition = _entry getOrDefault ["spawnPosition", []];
if (_spawnPosition isEqualTo []) then { continue; };
private _distance = if (isNull _garageObject) then { player distance2D _spawnPosition } else { _garageObject distance2D _spawnPosition };
if (_distance > _laneRadius) then { continue; };
private _spawnCategory = _entry getOrDefault ["spawnCategory", ""];
private _currentEntry = _lanes getOrDefault [_spawnCategory, createHashMap];
if (_currentEntry isEqualTo createHashMap || { (_entry getOrDefault ["score", 1e10]) < (_currentEntry getOrDefault ["score", 1e10]) }) then {
_lanes set [_spawnCategory, _entry];
};
} forEach allMapMarkers;
_lanes
}],
["selectSpawnLane", compileFinal {
params [
["_lanes", createHashMap, [createHashMap]],
["_preferredCategory", "", [""]],
["_defaultPosition", [], [[]]],
["_defaultHeading", 0, [0]]
];
private _normalizedCategory = GVAR(GarageHelperService) call ["normalizeGarageCategory", [_preferredCategory]];
private _lane = createHashMap;
if (_normalizedCategory isNotEqualTo "") then {
_lane = _lanes getOrDefault [_normalizedCategory, createHashMap];
};
if (_lane isEqualTo createHashMap) then {
{
private _candidate = _lanes getOrDefault [_x, createHashMap];
if (_candidate isNotEqualTo createHashMap) exitWith { _lane = _candidate; };
} forEach ["cars", "armor", "helis", "planes", "naval", "other"];
};
if (_lane isEqualTo createHashMap) then {
_lane = createHashMapFromArray [
["spawnCategory", _normalizedCategory],
["spawnHeading", _defaultHeading],
["spawnPosition", _defaultPosition]
];
};
_lane
}],
["getSpawnLane", compileFinal {
params [["_category", "", [""]], ["_context", createHashMap, [createHashMap]]];
private _resolvedContext = _context;
if (_resolvedContext isEqualTo createHashMap) then {
_resolvedContext = _self call ["getContext", []];
};
private _spawnLanes = _resolvedContext getOrDefault ["spawnLanes", createHashMap];
private _defaultPosition = _resolvedContext getOrDefault ["spawnPosition", getPosATL player];
private _defaultHeading = _resolvedContext getOrDefault ["spawnHeading", getDir player];
_self call ["selectSpawnLane", [_spawnLanes, _category, _defaultPosition, _defaultHeading]]
}],
["getExactSpawnLane", compileFinal {
params [["_category", "", [""]], ["_context", createHashMap, [createHashMap]]];
private _resolvedContext = _context;
if (_resolvedContext isEqualTo createHashMap) then {
_resolvedContext = _self call ["getContext", []];
};
private _normalizedCategory = GVAR(GarageHelperService) call ["normalizeGarageCategory", [_category]];
if (_normalizedCategory isEqualTo "") exitWith { createHashMap };
private _spawnLanes = _resolvedContext getOrDefault ["spawnLanes", createHashMap];
_spawnLanes getOrDefault [_normalizedCategory, createHashMap]
}],
["resolveContext", compileFinal {
private _context = _self call ["createDefaultContext", []];
private _locations = (missionConfigFile >> "FORGE_CfgGarages" >> "locations") call BFUNC(getCfgData);
if !(_locations isEqualType []) exitWith { _self set ["lastContext", _context]; _context };
private _garageObject = _self call ["findNearbyGarageObject", []];
private _garageName = _self call ["resolveGarageName", [_garageObject]];
private _garageType = "";
private _anchorPosition = getPosATL player;
private _spawnHeading = getDir player;
private _spawnPosition = player getPos [8, _spawnHeading];
private _spawnLanes = createHashMap;
private _nearestEntry = [];
private _nearestDistance = 1e10;
{
private _entry = _self call ["resolveEntry", [_x]];
private _anchorPosition = _entry getOrDefault ["anchorPosition", []];
if (_anchorPosition isEqualTo []) then { continue; };
private _distance = player distance2D _anchorPosition;
if (_distance < _nearestDistance) then { _nearestDistance = _distance; _nearestEntry = _entry; };
} forEach _locations;
if (!isNull _garageObject) then {
_garageType = GVAR(GarageHelperService) call ["normalizeGarageCategory", [_garageObject getVariable ["garageType", ""]]];
_anchorPosition = getPosATL _garageObject;
_spawnHeading = getDir _garageObject;
_spawnPosition = _garageObject getPos [8, _spawnHeading];
_spawnLanes = _self call ["discoverSpawnLanes", [_garageObject]];
};
if (_nearestEntry isEqualTo []) exitWith { _self set ["lastContext", _context]; _context };
private _anchorPosition = _nearestEntry getOrDefault ["anchorPosition", []];
private _garageObject = _nearestEntry getOrDefault ["sourceObject", objNull];
private _garageName = _nearestEntry getOrDefault ["name", "Vehicle Garage"];
private _spawnHeading = _nearestEntry getOrDefault ["spawnHeading", getDir player];
if (_spawnHeading < 0) then { _spawnHeading = if (!isNull _garageObject) then { getDir _garageObject } else { getDir player }; };
private _spawnPosition = _nearestEntry getOrDefault ["spawnPosition", []];
if (_spawnPosition isEqualTo []) then { _spawnPosition = if (_anchorPosition isEqualTo []) then { player getPos [8, _spawnHeading] } else { _anchorPosition }; };
private _selectedLane = _self call ["selectSpawnLane", [_spawnLanes, _garageType, _spawnPosition, _spawnHeading]];
_spawnHeading = _selectedLane getOrDefault ["spawnHeading", _spawnHeading];
_spawnPosition = _selectedLane getOrDefault ["spawnPosition", _spawnPosition];
_context set ["name", _garageName];
_context set ["anchorPosition", _anchorPosition];
_context set ["sourceObject", _garageObject];
_context set ["garageType", _garageType];
_context set ["spawnHeading", _spawnHeading];
_context set ["spawnPosition", _spawnPosition];
_context set ["spawnLanes", _spawnLanes];
_self set ["lastContext", _context];
_context
}],

View File

@ -22,6 +22,33 @@
#pragma hemtt ignore_variables ["_self"]
GVAR(GarageHelperServiceBaseClass) = compileFinal createHashMapFromArray [
["#type", "GarageHelperServiceBaseClass"],
["normalizeGarageCategory", compileFinal {
params [["_value", "", [""]]];
private _normalized = toLowerANSI (trim _value);
if (_normalized isEqualTo "") exitWith { "" };
if (_normalized in ["cars", "armor", "helis", "planes", "naval", "other"]) exitWith { _normalized };
""
}],
["inferGarageCategory", compileFinal {
params [["_value", "", [""]]];
private _normalized = toLowerANSI (trim _value);
if (_normalized isEqualTo "") exitWith { "" };
private _resolvedCategory = _self call ["normalizeGarageCategory", [_normalized]];
if (_resolvedCategory isNotEqualTo "") exitWith { _resolvedCategory };
switch (true) do {
case ((_normalized find "cars") >= 0): { "cars" };
case ((_normalized find "armor") >= 0): { "armor" };
case ((_normalized find "helis") >= 0): { "helis" };
case ((_normalized find "planes") >= 0): { "planes" };
case ((_normalized find "naval") >= 0): { "naval" };
case ((_normalized find "other") >= 0): { "other" };
default { "" };
}
}],
["resolveCategory", compileFinal {
params [["_className", "", [""]]];
@ -36,6 +63,33 @@ GVAR(GarageHelperServiceBaseClass) = compileFinal createHashMapFromArray [
default { "other" };
}
}],
["resolveVGCategory", compileFinal {
params [["_className", "", [""]]];
if (_className isEqualTo "") exitWith { "other" };
switch (true) do {
case (_className isKindOf ["Car", configFile >> "CfgVehicles"]): { "cars" };
case (_className isKindOf ["Tank", configFile >> "CfgVehicles"]): { "armor" };
case (_className isKindOf ["Helicopter", configFile >> "CfgVehicles"]): { "helis" };
case (_className isKindOf ["Plane", configFile >> "CfgVehicles"]): { "planes" };
case (_className isKindOf ["Ship", configFile >> "CfgVehicles"]): { "naval" };
default { "other" };
}
}],
["resolveGarageCategoryLabel", compileFinal {
params [["_category", "", [""]]];
switch (_category) do {
case "cars": { "cars" };
case "armor": { "armored vehicles" };
case "helis": { "helicopters" };
case "planes": { "planes" };
case "naval": { "naval vehicles" };
case "other": { "other vehicles" };
default { "this vehicle type" };
}
}],
["resolveDisplayName", compileFinal {
params [["_className", "", [""]]];

View File

@ -4,7 +4,7 @@
* File: fnc_openVG.sqf
* Author: IDSolutions
* Date: 2025-12-16
* Last Update: 2026-01-30
* Last Update: 2026-04-22
* Public: No
*
* Description:
@ -20,11 +20,12 @@
* call forge_client_garage_fnc_openVG
*/
private _locations = (missionConfigFile >> "FORGE_CfgGarages" >> "locations") call BFUNC(getCfgData);
{
FORGE_VehSpawnPos = (_x select 1) getPos [5, (_x select 2)];
true;
} count _locations;
private _context = GVAR(GarageContextService) call ["getContext", []];
private _spawnLane = GVAR(GarageContextService) call ["getSpawnLane", [_context getOrDefault ["garageType", ""], _context]];
FORGE_VehSpawnPos = _spawnLane getOrDefault ["spawnPosition", player getPos [8, getDir player]];
missionNamespace setVariable [QGVAR(activeVGContext), _context];
missionNamespace setVariable [QGVAR(activeVGNearbyVehicles), + (FORGE_VehSpawnPos nearEntities [["Car", "Tank", "Air", "Ship"], 15])];
BIS_fnc_garage_center = createVehicle ["Land_HelipadEmpty_F", FORGE_VehSpawnPos, [], 0, "NONE"];
BIS_fnc_garage_centerType = getText (configFile >> "CfgVehicles" >> "B_Quadbike_01_F" >> "model");
@ -53,16 +54,41 @@ if !(GVAR(isPreLoaded)) then {
}] call BFUNC(addScriptedEventHandler);
[missionNamespace, "garageClosed", {
private _nearestObjects = BIS_fnc_garage_center nearEntities [["Car","Tank","Air","Ship"], 15];
private _nearbyVehicles = BIS_fnc_garage_center nearEntities [["Car", "Tank", "Air", "Ship"], 15];
private _preExistingVehicles = missionNamespace getVariable [QGVAR(activeVGNearbyVehicles), []];
private _spawnedVehicles = _nearbyVehicles select { !(_x in _preExistingVehicles) };
if (_spawnedVehicles isNotEqualTo []) then {
private _spawnedVehiclePairs = _spawnedVehicles apply { [_x distance2D BIS_fnc_garage_center, _x] };
_spawnedVehiclePairs sort true;
private _obj = (_spawnedVehiclePairs select 0) param [1, objNull];
if (isNull _obj) exitWith {
missionNamespace setVariable [QGVAR(activeVGNearbyVehicles), nil];
missionNamespace setVariable [QGVAR(activeVGContext), nil];
};
if (!isNil "_nearestObjects") then {
private _obj = _nearestObjects select 0;
private _veh = typeOf _obj;
private _textures = getObjectTextures _obj;
private _animationNames = animationNames _obj;
private _context = missionNamespace getVariable [QGVAR(activeVGContext), createHashMap];
private _spawnCategory = GVAR(GarageHelperService) call ["resolveVGCategory", [_veh]];
private _spawnLane = GVAR(GarageContextService) call ["getExactSpawnLane", [_spawnCategory, _context]];
private _spawnLabel = GVAR(GarageHelperService) call ["resolveGarageCategoryLabel", [_spawnCategory]];
{ deleteVehicle _x } forEach _nearestObjects;
private _createVehicle = createVehicle [_veh, FORGE_VehSpawnPos, [], 0, "CAN_COLLIDE"];
{ deleteVehicle _x } forEach _spawnedVehicles;
if (_spawnLane isEqualTo createHashMap) exitWith {
missionNamespace setVariable [QGVAR(activeVGNearbyVehicles), nil];
missionNamespace setVariable [QGVAR(activeVGContext), nil];
private _params = ["warning", "Virtual Garage", format ["This garage does not support spawning %1.", _spawnLabel], 4000];
EGVAR(notifications,NotificationService) call ["create", _params];
};
private _spawnPosition = _spawnLane getOrDefault ["spawnPosition", FORGE_VehSpawnPos];
private _spawnHeading = _spawnLane getOrDefault ["spawnHeading", getDir _obj];
private _createVehicle = createVehicle [_veh, _spawnPosition, [], 0, "CAN_COLLIDE"];
_createVehicle setDir _spawnHeading;
if (_textures isNotEqualTo []) then {
private _count = 0;
@ -81,6 +107,9 @@ if !(GVAR(isPreLoaded)) then {
};
};
};
missionNamespace setVariable [QGVAR(activeVGNearbyVehicles), nil];
missionNamespace setVariable [QGVAR(activeVGContext), nil];
}] call BFUNC(addScriptedEventHandler);
GVAR(isPreLoaded) = true;

View File

@ -20,14 +20,37 @@
* call forge_server_garage_fnc_initGarage
*/
private _resolveGarageType = {
params [["_value", "", [""]]];
private _normalized = toLowerANSI (trim _value);
switch (true) do {
case ((_normalized find "cars") >= 0): { "cars" };
case ((_normalized find "armor") >= 0): { "armor" };
case ((_normalized find "helis") >= 0): { "helis" };
case ((_normalized find "planes") >= 0): { "planes" };
case ((_normalized find "naval") >= 0): { "naval" };
case ((_normalized find "other") >= 0): { "other" };
default { "" };
}
};
private _garages = (allVariables missionNamespace) select {
private _var = missionNamespace getVariable _x;
("garage" in _x) && { _var isEqualType objNull } && { !isNull _var }
((toLowerANSI _x) find "garage") >= 0 && { _var isEqualType objNull } && { !isNull _var }
};
if (_garages isEqualTo []) exitWith { ["INFO", "No editor-placed garages found."] call EFUNC(common,log) };
{
private _garage = missionNamespace getVariable _x;
private _garageName = _x;
private _garage = missionNamespace getVariable _garageName;
SETPVAR(_garage,isGarage,true);
if ((_garage getVariable ["garageType", ""]) isEqualTo "") then {
private _garageType = _garageName call _resolveGarageType;
if (_garageType isNotEqualTo "") then {
SETPVAR(_garage,garageType,_garageType);
};
};
} forEach _garages;

View File

@ -19,9 +19,12 @@ browser events through `forge_client_garage_fnc_handleUIEvents`.
call forge_client_garage_fnc_openVG;
```
The virtual garage uses mission-configured `FORGE_CfgGarages` locations to set
the spawn/preview position, opens the BIS garage interface, and restricts the
available vehicle lists from the virtual garage repository.
The virtual garage resolves the active interaction object near the player,
discovers nearby `garage*` markers placed in Eden, chooses the matching spawn
lane for the selected vehicle type, opens the BIS garage interface, and
restricts the available vehicle lists from the virtual garage repository. When
the BIS garage closes, only the vehicle selected in that virtual garage session
is finalized and spawned onto the resolved lane.
## Client Services
@ -79,8 +82,24 @@ _object setVariable ["isGarage", true, true];
_object setVariable ["garageType", "cars", true];
```
Virtual garage access also requires configured garage locations in mission
config so the preview/spawn position can be resolved.
When using the server garage auto-init flow, editor-placed objects whose
variable names contain `garage` are marked as garage interaction points and
their `garageType` can be inferred from the name.
Virtual garage spawn lanes are resolved from empty markers placed in Eden. The
marker name should contain `garage` and one of the six supported category names:
`cars`, `armor`, `helis`, `planes`, `naval`, or `other`. Markers are matched to
the nearby interaction object by proximity, and names that include the garage
object's variable name are preferred when multiple garages exist.
Vehicle spawning is strict by category. If the active garage site does not have
a matching local marker for the vehicle category being retrieved or spawned from
the virtual garage, the request is blocked and the player is shown a message.
Nearby world vehicles are not used as virtual garage spawn candidates. They are
only checked to determine whether the resolved spawn position is blocked. If
any vehicle is within 5 meters of the spawn marker when the virtual garage is
opened, the session is blocked and the player is shown a warning.
## Authoritative State

View File

@ -19,9 +19,12 @@ browser events through `forge_client_garage_fnc_handleUIEvents`.
call forge_client_garage_fnc_openVG;
```
The virtual garage uses mission-configured `FORGE_CfgGarages` locations to set
the spawn/preview position, opens the BIS garage interface, and restricts the
available vehicle lists from the virtual garage repository.
The virtual garage resolves the active interaction object near the player,
discovers nearby `garage*` markers placed in Eden, chooses the matching spawn
lane for the selected vehicle type, opens the BIS garage interface, and
restricts the available vehicle lists from the virtual garage repository. When
the BIS garage closes, only the vehicle selected in that virtual garage session
is finalized and spawned onto the resolved lane.
## Client Services
@ -79,8 +82,24 @@ _object setVariable ["isGarage", true, true];
_object setVariable ["garageType", "cars", true];
```
Virtual garage access also requires configured garage locations in mission
config so the preview/spawn position can be resolved.
When using the server garage auto-init flow, editor-placed objects whose
variable names contain `garage` are marked as garage interaction points and
their `garageType` can be inferred from the name.
Virtual garage spawn lanes are resolved from empty markers placed in Eden. The
marker name should contain `garage` and one of the six supported category names:
`cars`, `armor`, `helis`, `planes`, `naval`, or `other`. Markers are matched to
the nearby interaction object by proximity, and names that include the garage
object's variable name are preferred when multiple garages exist.
Vehicle spawning is strict by category. If the active garage site does not have
a matching local marker for the vehicle category being retrieved or spawned from
the virtual garage, the request is blocked and the player is shown a message.
Nearby world vehicles are not used as virtual garage spawn candidates. They are
only checked to determine whether the resolved spawn position is blocked. If
any vehicle is within 5 meters of the spawn marker when the virtual garage is
opened, the session is blocked and the player is shown a warning.
## Authoritative State