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 The client builds vehicle context and sends requests. The server garage addon
and extension own stored vehicle state. 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 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 for nearby world vehicles. Stored records must be retrieved before they can be
serviced because fuel and repair operate on live vehicle objects. Service 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."]]]]; 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 _context = GVAR(GarageContextService) call ["getContext", []];
private _spawnPosition = _context getOrDefault ["spawnPosition", getPosATL player]; private _vehicleCategory = GVAR(GarageHelperService) call ["resolveVGCategory", [_className]];
private _spawnHeading = _context getOrDefault ["spawnHeading", getDir player]; 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 _spawnRadius = _context getOrDefault ["spawnRadius", 6];
private _blockingVehicles = []; private _blockingVehicles = [];
{ _blockingVehicles pushBackUnique _x; } forEach (_spawnPosition nearEntities [["Car", "Tank", "Air", "Ship"], _spawnRadius]); { _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."]]]]; 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"]; private _vehicle = createVehicle [_className, _spawnPosition, [], 0, "CAN_COLLIDE"];
_vehicle setDir _spawnHeading; _vehicle setDir _spawnHeading;
_vehicle setFuel (_vehicleData getOrDefault ["fuel", 0]); _vehicle setFuel (_vehicleData getOrDefault ["fuel", 0]);

View File

@ -29,81 +29,189 @@ GVAR(GarageContextServiceBaseClass) = compileFinal createHashMapFromArray [
["name", "Vehicle Garage"], ["name", "Vehicle Garage"],
["anchorPosition", getPosATL player], ["anchorPosition", getPosATL player],
["sourceObject", objNull], ["sourceObject", objNull],
["garageType", ""],
["spawnHeading", getDir player], ["spawnHeading", getDir player],
["spawnPosition", player getPos [8, getDir player]], ["spawnPosition", player getPos [8, getDir player]],
["spawnLanes", createHashMap],
["spawnRadius", 6], ["spawnRadius", 6],
["nearbyRadius", 30] ["nearbyRadius", 30],
["laneRadius", 150]
] ]
}], }],
["scanEntryValues", compileFinal { ["findNearbyGarageObject", compileFinal {
params [["_values", [], [[]]], ["_state", createHashMap, [createHashMap]]]; private _nearestGarage = objNull;
private _nearestDistance = 1e10;
{ {
if (_x isEqualType "" && { (_state getOrDefault ["name", "Vehicle Garage"]) isEqualTo "Vehicle Garage" }) then { _state set ["name", _x]; }; if (isNull _x || { !(_x getVariable ["isGarage", false]) }) then { continue; };
if (_x isEqualType "") then { private _distance = player distance2D _x;
private _resolvedObject = _state getOrDefault ["sourceObject", objNull]; if (_distance < _nearestDistance) then {
if (isNull _resolvedObject) then { _nearestDistance = _distance;
private _namedObject = missionNamespace getVariable [_x, objNull]; _nearestGarage = _x;
if (!isNull _namedObject) then { _state set ["sourceObject", _namedObject]; };
};
if ((_state getOrDefault ["anchorPosition", []]) isEqualTo [] && { _x in allMapMarkers }) then { _state set ["anchorPosition", markerPos _x]; };
continue;
}; };
if (_x isEqualType objNull && { isNull (_state getOrDefault ["sourceObject", objNull]) }) then { } forEach (player nearObjects 12);
_state set ["sourceObject", _x];
if ((_state getOrDefault ["anchorPosition", []]) isEqualTo []) then { _state set ["anchorPosition", getPosATL _x]; }; _nearestGarage
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
}], }],
["resolveEntry", compileFinal { ["resolveGarageName", compileFinal {
params [["_entry", [], [[]]]]; params [["_garageObject", objNull, [objNull]]];
private _state = createHashMapFromArray [["name", "Vehicle Garage"], ["anchorPosition", []], ["sourceObject", objNull], ["offset", []], ["spawnHeading", -1]];
_self call ["scanEntryValues", [_entry, _state]]; if (isNull _garageObject) exitWith { "Vehicle Garage" };
private _anchorPosition = _state getOrDefault ["anchorPosition", []];
private _offset = _state getOrDefault ["offset", []]; private _displayName = _garageObject getVariable ["garageName", ""];
private _spawnPosition = if (_anchorPosition isEqualTo []) then { [] } else { if (_offset isEqualTo []) then { _anchorPosition } else { _anchorPosition vectorAdd _offset } }; if (_displayName isNotEqualTo "") exitWith { _displayName };
createHashMapFromArray [["name", _state getOrDefault ["name", "Vehicle Garage"]], ["anchorPosition", _anchorPosition], ["sourceObject", _state getOrDefault ["sourceObject", objNull]], ["spawnHeading", _state getOrDefault ["spawnHeading", -1]], ["spawnPosition", _spawnPosition]]
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 { ["resolveContext", compileFinal {
private _context = _self call ["createDefaultContext", []]; private _context = _self call ["createDefaultContext", []];
private _locations = (missionConfigFile >> "FORGE_CfgGarages" >> "locations") call BFUNC(getCfgData); private _garageObject = _self call ["findNearbyGarageObject", []];
if !(_locations isEqualType []) exitWith { _self set ["lastContext", _context]; _context }; 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 = []; if (!isNull _garageObject) then {
private _nearestDistance = 1e10; _garageType = GVAR(GarageHelperService) call ["normalizeGarageCategory", [_garageObject getVariable ["garageType", ""]]];
{ _anchorPosition = getPosATL _garageObject;
private _entry = _self call ["resolveEntry", [_x]]; _spawnHeading = getDir _garageObject;
private _anchorPosition = _entry getOrDefault ["anchorPosition", []]; _spawnPosition = _garageObject getPos [8, _spawnHeading];
if (_anchorPosition isEqualTo []) then { continue; }; _spawnLanes = _self call ["discoverSpawnLanes", [_garageObject]];
private _distance = player distance2D _anchorPosition; };
if (_distance < _nearestDistance) then { _nearestDistance = _distance; _nearestEntry = _entry; };
} forEach _locations;
if (_nearestEntry isEqualTo []) exitWith { _self set ["lastContext", _context]; _context }; private _selectedLane = _self call ["selectSpawnLane", [_spawnLanes, _garageType, _spawnPosition, _spawnHeading]];
_spawnHeading = _selectedLane getOrDefault ["spawnHeading", _spawnHeading];
private _anchorPosition = _nearestEntry getOrDefault ["anchorPosition", []]; _spawnPosition = _selectedLane getOrDefault ["spawnPosition", _spawnPosition];
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 }; };
_context set ["name", _garageName]; _context set ["name", _garageName];
_context set ["anchorPosition", _anchorPosition]; _context set ["anchorPosition", _anchorPosition];
_context set ["sourceObject", _garageObject]; _context set ["sourceObject", _garageObject];
_context set ["garageType", _garageType];
_context set ["spawnHeading", _spawnHeading]; _context set ["spawnHeading", _spawnHeading];
_context set ["spawnPosition", _spawnPosition]; _context set ["spawnPosition", _spawnPosition];
_context set ["spawnLanes", _spawnLanes];
_self set ["lastContext", _context]; _self set ["lastContext", _context];
_context _context
}], }],

View File

@ -22,6 +22,33 @@
#pragma hemtt ignore_variables ["_self"] #pragma hemtt ignore_variables ["_self"]
GVAR(GarageHelperServiceBaseClass) = compileFinal createHashMapFromArray [ GVAR(GarageHelperServiceBaseClass) = compileFinal createHashMapFromArray [
["#type", "GarageHelperServiceBaseClass"], ["#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 { ["resolveCategory", compileFinal {
params [["_className", "", [""]]]; params [["_className", "", [""]]];
@ -36,6 +63,33 @@ GVAR(GarageHelperServiceBaseClass) = compileFinal createHashMapFromArray [
default { "other" }; 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 { ["resolveDisplayName", compileFinal {
params [["_className", "", [""]]]; params [["_className", "", [""]]];

View File

@ -4,7 +4,7 @@
* File: fnc_openVG.sqf * File: fnc_openVG.sqf
* Author: IDSolutions * Author: IDSolutions
* Date: 2025-12-16 * Date: 2025-12-16
* Last Update: 2026-01-30 * Last Update: 2026-04-22
* Public: No * Public: No
* *
* Description: * Description:
@ -20,11 +20,12 @@
* call forge_client_garage_fnc_openVG * call forge_client_garage_fnc_openVG
*/ */
private _locations = (missionConfigFile >> "FORGE_CfgGarages" >> "locations") call BFUNC(getCfgData); private _context = GVAR(GarageContextService) call ["getContext", []];
{ private _spawnLane = GVAR(GarageContextService) call ["getSpawnLane", [_context getOrDefault ["garageType", ""], _context]];
FORGE_VehSpawnPos = (_x select 1) getPos [5, (_x select 2)];
true; FORGE_VehSpawnPos = _spawnLane getOrDefault ["spawnPosition", player getPos [8, getDir player]];
} count _locations; 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_center = createVehicle ["Land_HelipadEmpty_F", FORGE_VehSpawnPos, [], 0, "NONE"];
BIS_fnc_garage_centerType = getText (configFile >> "CfgVehicles" >> "B_Quadbike_01_F" >> "model"); BIS_fnc_garage_centerType = getText (configFile >> "CfgVehicles" >> "B_Quadbike_01_F" >> "model");
@ -53,16 +54,41 @@ if !(GVAR(isPreLoaded)) then {
}] call BFUNC(addScriptedEventHandler); }] call BFUNC(addScriptedEventHandler);
[missionNamespace, "garageClosed", { [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 _veh = typeOf _obj;
private _textures = getObjectTextures _obj; private _textures = getObjectTextures _obj;
private _animationNames = animationNames _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; { deleteVehicle _x } forEach _spawnedVehicles;
private _createVehicle = createVehicle [_veh, FORGE_VehSpawnPos, [], 0, "CAN_COLLIDE"];
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 { if (_textures isNotEqualTo []) then {
private _count = 0; private _count = 0;
@ -81,6 +107,9 @@ if !(GVAR(isPreLoaded)) then {
}; };
}; };
}; };
missionNamespace setVariable [QGVAR(activeVGNearbyVehicles), nil];
missionNamespace setVariable [QGVAR(activeVGContext), nil];
}] call BFUNC(addScriptedEventHandler); }] call BFUNC(addScriptedEventHandler);
GVAR(isPreLoaded) = true; GVAR(isPreLoaded) = true;

View File

@ -20,14 +20,37 @@
* call forge_server_garage_fnc_initGarage * 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 _garages = (allVariables missionNamespace) select {
private _var = missionNamespace getVariable _x; 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) }; 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); 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; } forEach _garages;

View File

@ -19,9 +19,12 @@ browser events through `forge_client_garage_fnc_handleUIEvents`.
call forge_client_garage_fnc_openVG; call forge_client_garage_fnc_openVG;
``` ```
The virtual garage uses mission-configured `FORGE_CfgGarages` locations to set The virtual garage resolves the active interaction object near the player,
the spawn/preview position, opens the BIS garage interface, and restricts the discovers nearby `garage*` markers placed in Eden, chooses the matching spawn
available vehicle lists from the virtual garage repository. 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 ## Client Services
@ -79,8 +82,24 @@ _object setVariable ["isGarage", true, true];
_object setVariable ["garageType", "cars", true]; _object setVariable ["garageType", "cars", true];
``` ```
Virtual garage access also requires configured garage locations in mission When using the server garage auto-init flow, editor-placed objects whose
config so the preview/spawn position can be resolved. 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 ## Authoritative State

View File

@ -19,9 +19,12 @@ browser events through `forge_client_garage_fnc_handleUIEvents`.
call forge_client_garage_fnc_openVG; call forge_client_garage_fnc_openVG;
``` ```
The virtual garage uses mission-configured `FORGE_CfgGarages` locations to set The virtual garage resolves the active interaction object near the player,
the spawn/preview position, opens the BIS garage interface, and restricts the discovers nearby `garage*` markers placed in Eden, chooses the matching spawn
available vehicle lists from the virtual garage repository. 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 ## Client Services
@ -79,8 +82,24 @@ _object setVariable ["isGarage", true, true];
_object setVariable ["garageType", "cars", true]; _object setVariable ["garageType", "cars", true];
``` ```
Virtual garage access also requires configured garage locations in mission When using the server garage auto-init flow, editor-placed objects whose
config so the preview/spawn position can be resolved. 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 ## Authoritative State