mirror of
https://github.com/acemod/ACE3.git
synced 2024-08-30 18:23:18 +00:00
Add Iron Dome interceptor API
This commit is contained in:
parent
b82484b73c
commit
980c3d0546
1
addons/iron_dome/$PBOPREFIX$
Normal file
1
addons/iron_dome/$PBOPREFIX$
Normal file
@ -0,0 +1 @@
|
||||
z\ace\addons\iron_dome
|
11
addons/iron_dome/CfgEventHandlers.hpp
Normal file
11
addons/iron_dome/CfgEventHandlers.hpp
Normal file
@ -0,0 +1,11 @@
|
||||
class Extended_PreStart_EventHandlers {
|
||||
class ADDON {
|
||||
init = QUOTE(call COMPILE_FILE(XEH_preStart));
|
||||
};
|
||||
};
|
||||
class Extended_PreInit_EventHandlers {
|
||||
class ADDON {
|
||||
init = QUOTE(call COMPILE_FILE(XEH_preInit));
|
||||
};
|
||||
};
|
||||
|
11
addons/iron_dome/README.md
Normal file
11
addons/iron_dome/README.md
Normal file
@ -0,0 +1,11 @@
|
||||
ace_iron_dome
|
||||
===================
|
||||
|
||||
Adds an Iron Dome projectile interceptor system
|
||||
|
||||
|
||||
## Maintainers
|
||||
|
||||
The people responsible for merging changes to this component or answering potential questions.
|
||||
|
||||
- [Brandon (TCVM)](https://github.com/TheCandianVendingMachine)
|
2
addons/iron_dome/XEH_PREP.hpp
Normal file
2
addons/iron_dome/XEH_PREP.hpp
Normal file
@ -0,0 +1,2 @@
|
||||
PREP(projectileTrackerPFH);
|
||||
PREP(proximityFusePFH);
|
79
addons/iron_dome/XEH_preInit.sqf
Normal file
79
addons/iron_dome/XEH_preInit.sqf
Normal file
@ -0,0 +1,79 @@
|
||||
#include "script_component.hpp"
|
||||
|
||||
ADDON = false;
|
||||
|
||||
PREP_RECOMPILE_START;
|
||||
#include "XEH_PREP.hpp"
|
||||
PREP_RECOMPILE_END;
|
||||
|
||||
ADDON = true;
|
||||
|
||||
// Server handles the tracking of all projectiles. It dispatches events to launchers to fire at specific targets
|
||||
if (isServer) then {
|
||||
GVAR(trackers) = [];
|
||||
GVAR(launchers) = [];
|
||||
GVAR(nonTrackingProjectiles) = [];
|
||||
GVAR(trackingProjectiles) = [];
|
||||
|
||||
GVAR(interceptors) = [];
|
||||
GVAR(toBeShot) = [];
|
||||
|
||||
[QGVAR(track), {
|
||||
params ["_projectile"];
|
||||
GVAR(nonTrackingProjectiles) pushBack _projectile;
|
||||
}] call CBA_fnc_addEventHandler;
|
||||
|
||||
[QGVAR(registerLauncher), {
|
||||
params ["_launcher"];
|
||||
GVAR(launchers) pushBackUnique _launcher;
|
||||
_launcher setVariable [QGVAR(targetList), []];
|
||||
_launcher setVariable [QGVAR(launchState), LAUNCH_STATE_IDLE];
|
||||
_launcher setVariable [QGVAR(lastLaunchTime), 0];
|
||||
_launcher setVariable [QGVAR(engagedTargets), [[], objNull] call CBA_fnc_hashCreate];
|
||||
_launcher setVariable [QEGVAR(missileguidance,target), objNull];
|
||||
}] call CBA_fnc_addEventHandler;
|
||||
|
||||
[QGVAR(registerTracker), {
|
||||
params ["_tracker", "_range"];
|
||||
GVAR(trackers) pushBack [_tracker, _range];
|
||||
}] call CBA_fnc_addEventHandler;
|
||||
|
||||
[QGVAR(registerInterceptor), {
|
||||
params ["_interceptor", "_target"];
|
||||
GVAR(interceptors) pushBack [_interceptor, _target, getPosASLVisual _interceptor];
|
||||
|
||||
GVAR(toBeShot) deleteAt (GVAR(toBeShot) find _target);
|
||||
}] call CBA_fnc_addEventHandler;
|
||||
|
||||
[LINKFUNC(projectileTrackerPFH)] call CBA_fnc_addPerFrameHandler;
|
||||
[LINKFUNC(proximityFusePFH)] call CBA_fnc_addPerFrameHandler;
|
||||
};
|
||||
|
||||
// duplicate event to add event handler
|
||||
[QGVAR(registerLauncher), {
|
||||
params ["_launcher"];
|
||||
if !(local _launcher) exitWith {};
|
||||
_launcher addEventHandler ["Fired", {
|
||||
params ["_launcher", "", "", "", "", "", "_projectile"];
|
||||
private _target = _launcher getVariable [QEGVAR(missileguidance,target), objNull];
|
||||
if !(isNull _target) then {
|
||||
[QGVAR(registerInterceptor), [_projectile, _target]] call CBA_fnc_serverEvent;
|
||||
};
|
||||
}];
|
||||
}] call CBA_fnc_addEventHandler;
|
||||
|
||||
// When something is fired, determine if we want to track it. If so, send it to the server for processing
|
||||
GVAR(projectilesToIntercept) = [];
|
||||
|
||||
[QGVAR(addProjectileToIntercept), {
|
||||
params ["_type"];
|
||||
GVAR(projectilesToIntercept) pushBackUnique _type;
|
||||
}] call CBA_fnc_addEventHandler;
|
||||
|
||||
["All", "fired", {
|
||||
params ["", "", "", "", "", "", "_projectile"];
|
||||
if (local _projectile && { (typeOf _projectile) in GVAR(projectilesToIntercept) }) then {
|
||||
[QGVAR(track), [_projectile]] call CBA_fnc_serverEvent;
|
||||
};
|
||||
}] call CBA_fnc_addClassEventHandler;
|
||||
|
3
addons/iron_dome/XEH_preStart.sqf
Normal file
3
addons/iron_dome/XEH_preStart.sqf
Normal file
@ -0,0 +1,3 @@
|
||||
#include "script_component.hpp"
|
||||
|
||||
#include "XEH_PREP.hpp"
|
17
addons/iron_dome/config.cpp
Normal file
17
addons/iron_dome/config.cpp
Normal file
@ -0,0 +1,17 @@
|
||||
#include "script_component.hpp"
|
||||
|
||||
class CfgPatches {
|
||||
class ADDON {
|
||||
name = COMPONENT_NAME;
|
||||
units[] = {};
|
||||
weapons[] = {};
|
||||
requiredVersion = REQUIRED_VERSION;
|
||||
requiredAddons[] = {"ace_common"};
|
||||
author = ECSTRING(common,ACETeam);
|
||||
authors[] = {"Brandon (TCVM)"};
|
||||
url = ECSTRING(main,URL);
|
||||
VERSION_CONFIG;
|
||||
};
|
||||
};
|
||||
|
||||
#include "CfgEventHandlers.hpp"
|
194
addons/iron_dome/functions/fnc_projectileTrackerPFH.sqf
Normal file
194
addons/iron_dome/functions/fnc_projectileTrackerPFH.sqf
Normal file
@ -0,0 +1,194 @@
|
||||
#include "script_component.hpp"
|
||||
/*
|
||||
* Author: Brandon (TCVM)
|
||||
* Handles tracking of incoming projectiles per frame
|
||||
*
|
||||
* Arguments:
|
||||
* 0: Args <ARRAY>
|
||||
* 1: Handle <NUMBER>
|
||||
*
|
||||
* Return Value:
|
||||
* None
|
||||
*
|
||||
* Example:
|
||||
* [ace_iron_dome_projectileTrackerPFH] call CBA_fnc_addPerFrameHandler
|
||||
*
|
||||
* Public: No
|
||||
*/
|
||||
|
||||
GVAR(trackers) = GVAR(trackers) select {
|
||||
_x params ["_tracker"];
|
||||
alive _tracker
|
||||
};
|
||||
|
||||
GVAR(launchers) = GVAR(launchers) select {
|
||||
alive _x
|
||||
};
|
||||
|
||||
GVAR(toBeShot) = GVAR(toBeShot) select {
|
||||
_x params ["", "_shotTime"];
|
||||
(CBA_missionTime - _shotTime) < RECYCLE_TIME
|
||||
};
|
||||
|
||||
GVAR(nonTrackingProjectiles) = GVAR(nonTrackingProjectiles) select {
|
||||
private _projectile = _x;
|
||||
private _keep = true;
|
||||
private _bestRange = 1e10;
|
||||
|
||||
{
|
||||
_x params ["_tracker", "_range"];
|
||||
_bestRange = _bestRange min (_projectile distanceSqr _tracker);
|
||||
if (_projectile distanceSqr _tracker <= _range * _range) exitWith {
|
||||
GVAR(trackingProjectiles) pushBack [_projectile, 0];
|
||||
_keep = false;
|
||||
};
|
||||
} forEach GVAR(trackers);
|
||||
|
||||
#ifdef DRAW_TRACKING_INFO
|
||||
drawIcon3D ["\a3\ui_f\data\IGUI\Cfg\Cursors\selectover_ca.paa", [1,0,0,1], getPos _projectile, 0.75, 0.75, 0, format ["%1 %2m", typeOf _projectile, sqrt _bestRange], 1, 0.025, "TahomaB"];
|
||||
#endif
|
||||
|
||||
_keep
|
||||
};
|
||||
|
||||
GVAR(trackingProjectiles) = GVAR(trackingProjectiles) select {
|
||||
_x params ["_projectile", "_lastFired"];
|
||||
|
||||
private _keep = false;
|
||||
if (alive _projectile) then {
|
||||
{
|
||||
_x params ["_tracker", "_range"];
|
||||
private _withinRange = _projectile distanceSqr _tracker <= _range * _range;
|
||||
|
||||
if (_withinRange) exitWith {
|
||||
_keep = true;
|
||||
};
|
||||
|
||||
} forEach GVAR(trackers);
|
||||
|
||||
if !(_keep) then {
|
||||
GVAR(nonTrackingProjectiles) pushBack _projectile;
|
||||
} else {
|
||||
private _bestLauncher = objNull;
|
||||
private _bestAmmo = 0;
|
||||
|
||||
private _engagedFuture = GVAR(toBeShot) findIf {
|
||||
_x params ["_target", "_timeShot"];
|
||||
_projectile isEqualTo _target
|
||||
};
|
||||
|
||||
private _engagedPast = GVAR(interceptors) findIf {
|
||||
_x params ["", "_target"];
|
||||
_projectile isEqualTo _target;
|
||||
};
|
||||
|
||||
private _engaged = (_engagedFuture != -1) || (_engagedPast != -1);
|
||||
if !(_engaged) then {
|
||||
// launch a missile
|
||||
{
|
||||
// try to get a launcher with the most ammo that is the closest to the incoming projectile
|
||||
private _state = _x getVariable QGVAR(launchState);
|
||||
private _ammo = parseNumber (((currentMagazineDetail _x) splitString "([ ]/:)") select 3);
|
||||
if (_state == LAUNCH_STATE_IDLE && { _ammo >= _bestAmmo }) then {
|
||||
_bestAmmo = _ammo;
|
||||
_bestLauncher = _x;
|
||||
};
|
||||
} forEach GVAR(launchers);
|
||||
|
||||
private _targetList = _bestLauncher getVariable QGVAR(targetList);
|
||||
_targetList pushBackUnique _projectile;
|
||||
_bestLauncher setVariable [QGVAR(targetList), _targetList];
|
||||
|
||||
GVAR(toBeShot) pushBackUnique [_projectile, CBA_missionTime];
|
||||
};
|
||||
|
||||
#ifdef DRAW_TRACKING_INFO
|
||||
drawIcon3D ["\a3\ui_f\data\IGUI\Cfg\Cursors\selectover_ca.paa", [0,1,0,1], getPos _projectile, 0.75, 0.75, 0, format ["%1 %2m %3s", typeOf _projectile, _bestLauncher distance _projectile], 1, 0.025, "TahomaB"];
|
||||
#endif
|
||||
};
|
||||
};
|
||||
|
||||
_keep
|
||||
};
|
||||
|
||||
{
|
||||
private _launcher = _x;
|
||||
private _state = _launcher getVariable QGVAR(launchState);
|
||||
|
||||
switch (_state) do {
|
||||
case LAUNCH_STATE_IDLE: {
|
||||
private _targetList = _x getVariable QGVAR(targetList);
|
||||
private _engagedTargets = _x getVariable QGVAR(engagedTargets);
|
||||
|
||||
_targetList = _targetList select {
|
||||
private _timeFiredAt = [_engagedTargets, _x, 0] call CBA_fnc_hashGet;
|
||||
alive _x && { (CBA_missionTime - _timeFiredAt) >= RE_ENGAGEMENT_TIME }
|
||||
};
|
||||
|
||||
private _bestTarget = objNull;
|
||||
private _bestDistance = 1e10;
|
||||
{
|
||||
if (_x distanceSqr _launcher < _bestDistance) then {
|
||||
_bestTarget = _x;
|
||||
_bestDistance = _x distanceSqr _launcher;
|
||||
};
|
||||
} forEach _targetList;
|
||||
|
||||
if !(isNull _bestTarget) then {
|
||||
_launcher setVariable [QEGVAR(missileguidance,target), _bestTarget];
|
||||
_launcher setVariable [QGVAR(launchState), LAUNCH_STATE_TRACKING];
|
||||
};
|
||||
|
||||
#ifdef DRAW_TRACKING_INFO
|
||||
drawIcon3D ["\a3\ui_f\data\IGUI\Cfg\Cursors\selectover_ca.paa", [1, 1, 1, 1], getPos _launcher, 0.75, 0.75, 0, format ["IDLE"], 1, 0.025, "TahomaB"];
|
||||
#endif
|
||||
};
|
||||
case LAUNCH_STATE_TRACKING: {
|
||||
private _target = _launcher getVariable QEGVAR(missileguidance,target);
|
||||
_launcher lookAt getPosVisual _target;
|
||||
|
||||
if (isNull _target) then {
|
||||
_launcher setVariable [QGVAR(launchState), LAUNCH_STATE_IDLE];
|
||||
} else {
|
||||
private _directionToTarget = (getPosASLVisual _launcher) vectorFromTo (getPosASLVisual _target);
|
||||
private _turretDirection = _launcher weaponDirection currentWeapon _launcher;
|
||||
private _angle = acos (_turretDirection vectorCos _directionToTarget);
|
||||
if (_angle <= LAUNCH_ACCEPTABLE_ANGLE) then {
|
||||
_launcher setVariable [QGVAR(launchState), LAUNCH_STATE_FIRING];
|
||||
};
|
||||
|
||||
#ifdef DRAW_TRACKING_INFO
|
||||
drawIcon3D ["\a3\ui_f\data\IGUI\Cfg\Cursors\selectover_ca.paa", [0, 0, 1, 1], getPos _launcher, 0.75, 0.75, 0, format ["TRACKING: %1", _angle], 1, 0.025, "TahomaB"];
|
||||
drawLine3D [getPos _launcher, getPos _target, [0, 0, 1, 1]];
|
||||
#endif
|
||||
};
|
||||
};
|
||||
case LAUNCH_STATE_FIRING: {
|
||||
private _turret = [_launcher, (crew _launcher) select 0] call CBA_fnc_turretPath;
|
||||
[_launcher, _launcher currentWeaponTurret _turret] call BIS_fnc_fire;
|
||||
|
||||
_launcher setVariable [QGVAR(lastLaunchTime), CBA_missionTime];
|
||||
_launcher setVariable [QGVAR(launchState), LAUNCH_STATE_COOLDOWN];
|
||||
|
||||
private _target = _launcher getVariable QEGVAR(missileguidance,target);
|
||||
private _engagedTargets = _x getVariable QGVAR(engagedTargets);
|
||||
[_engagedTargets, _target, CBA_missionTime] call CBA_fnc_hashSet;
|
||||
_x setVariable [QGVAR(engagedTargets), _engagedTargets];
|
||||
|
||||
#ifdef DRAW_TRACKING_INFO
|
||||
drawIcon3D ["\a3\ui_f\data\IGUI\Cfg\Cursors\selectover_ca.paa", [1, 0, 0, 1], getPos _launcher, 0.75, 0.75, 0, format ["FIRING"], 1, 0.025, "TahomaB"];
|
||||
#endif
|
||||
};
|
||||
case LAUNCH_STATE_COOLDOWN: {
|
||||
private _lastLaunchTime = _launcher getVariable QGVAR(lastLaunchTime);
|
||||
if (CBA_missionTime - _lastLaunchTime >= TIME_BETWEEN_LAUNCHES) then {
|
||||
_launcher setVariable [QGVAR(launchState), LAUNCH_STATE_IDLE];
|
||||
};
|
||||
|
||||
#ifdef DRAW_TRACKING_INFO
|
||||
drawIcon3D ["\a3\ui_f\data\IGUI\Cfg\Cursors\selectover_ca.paa", [0, 0, 1, 1], getPos _launcher, 0.75, 0.75, 0, format ["COOLDOWN %1", CBA_missionTime - _lastLaunchTime], 1, 0.025, "TahomaB"];
|
||||
#endif
|
||||
};
|
||||
};
|
||||
} forEach GVAR(launchers);
|
||||
|
49
addons/iron_dome/functions/fnc_proximityFusePFH.sqf
Normal file
49
addons/iron_dome/functions/fnc_proximityFusePFH.sqf
Normal file
@ -0,0 +1,49 @@
|
||||
#include "script_component.hpp"
|
||||
/*
|
||||
* Author: Brandon (TCVM)
|
||||
* Handles the fusing and detonation of any and all interceptors in the air
|
||||
*
|
||||
* Arguments:
|
||||
* None
|
||||
*
|
||||
* Return Value:
|
||||
* None
|
||||
*
|
||||
* Example:
|
||||
* [ace_iron_dome_proximityFusePFH] call CBA_fnc_addPerFrameHandler
|
||||
*
|
||||
* Public: No
|
||||
*/
|
||||
|
||||
GVAR(interceptors) = GVAR(interceptors) select {
|
||||
_x params ["_projectile", "_target", "_lastPosition"];
|
||||
// Sweep along path to ensure we don't overshoot target
|
||||
private _minDistance = 0;
|
||||
|
||||
private _currentPosition = getPosASLVisual _projectile;
|
||||
private _targetPosition = getPosASLVisual _target;
|
||||
|
||||
private _posDiff = (_currentPosition vectorDiff _lastPosition);
|
||||
private _lengthSqr = _posDiff vectorDotProduct _posDiff;
|
||||
if (_lengthSqr - 0.001 <= 0) then {
|
||||
_minDistance = _lastPosition vectorDistance _targetPosition
|
||||
} else {
|
||||
private _d = (_targetPosition vectorDiff _lastPosition) vectorDotProduct (_currentPosition vectorDiff _lastPosition);
|
||||
private _t = 0 max (1 min (_d / _lengthSqr));
|
||||
private _projection = _lastPosition vectorAdd ((_currentPosition vectorDiff _lastPosition) vectorMultiply _t);
|
||||
_minDistance = _projection vectorDistance _targetPosition;
|
||||
};
|
||||
|
||||
_x set [2, _currentPosition];
|
||||
|
||||
#ifdef DRAW_TRACKING_INFO
|
||||
drawIcon3D ["\a3\ui_f\data\IGUI\Cfg\Cursors\selectover_ca.paa", [0,0,1,1], (getPos _target) vectorAdd [0, 0, 0.5], 0.75, 0.75, 0, format ["%1m", _minDistance], 1, 0.025, "TahomaB"];
|
||||
#endif
|
||||
if (!alive _target || { _minDistance <= PROX_RANGE }) then {
|
||||
triggerAmmo _projectile;
|
||||
deleteVehicle _target;
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
};
|
1
addons/iron_dome/functions/script_component.hpp
Normal file
1
addons/iron_dome/functions/script_component.hpp
Normal file
@ -0,0 +1 @@
|
||||
#include "\z\ace\addons\iron_dome\script_component.hpp"
|
35
addons/iron_dome/script_component.hpp
Normal file
35
addons/iron_dome/script_component.hpp
Normal file
@ -0,0 +1,35 @@
|
||||
#define COMPONENT iron_dome
|
||||
#define COMPONENT_BEAUTIFIED Iron Dome
|
||||
#include "\z\ace\addons\main\script_mod.hpp"
|
||||
|
||||
// #define DRAW_TRACKING_INFO
|
||||
// #define DEBUG_MODE_FULL
|
||||
// #define DISABLE_COMPILE_CACHE
|
||||
// #define ENABLE_PERFORMANCE_COUNTERS
|
||||
|
||||
#ifdef DEBUG_ENABLED_IRON_DOME
|
||||
#define DEBUG_MODE_FULL
|
||||
#endif
|
||||
|
||||
#ifdef DEBUG_SETTINGS_IRON_DOME
|
||||
#define DEBUG_SETTINGS DEBUG_SETTINGS_IRON_DOME
|
||||
#endif
|
||||
|
||||
#define LAUNCH_STATE_IDLE 0
|
||||
#define LAUNCH_STATE_TRACKING 1
|
||||
#define LAUNCH_STATE_FIRING 2
|
||||
#define LAUNCH_STATE_COOLDOWN 3
|
||||
|
||||
// How long it takes to recycle a target
|
||||
#define RECYCLE_TIME 5
|
||||
// how many error degrees the launcher has to be pointing toward the target
|
||||
#define LAUNCH_ACCEPTABLE_ANGLE 30
|
||||
// How fast the launcher launches
|
||||
#define TIME_BETWEEN_LAUNCHES 1
|
||||
// how many seconds does a launcher have to wait before re-engaging the same target
|
||||
#define RE_ENGAGEMENT_TIME 5
|
||||
|
||||
// Proximity fuse range
|
||||
#define PROX_RANGE 10
|
||||
|
||||
#include "\z\ace\addons\main\script_macros.hpp"
|
@ -14,14 +14,16 @@
|
||||
*
|
||||
* Public: No
|
||||
*/
|
||||
params ["_firedEH", "", "", "", "_stateParams"];
|
||||
params ["_firedEH", "_launchParams", "", "", "_stateParams"];
|
||||
_firedEH params ["_shooter","_weapon","","","","","_projectile"];
|
||||
_stateParams params ["", "_seekerStateParams"];
|
||||
_launchParams params ["", "_targetLaunchParams"];
|
||||
_targetLaunchParams params ["_target"];
|
||||
|
||||
private _flareDistanceFilter = getNumber (configOf _projectile >> QUOTE(ADDON) >> "flareDistanceFilter");
|
||||
private _flareAngleFilter = getNumber (configOf _projectile >> QUOTE(ADDON) >> "flareAngleFilter");
|
||||
|
||||
_seekerStateParams set [0, _flareDistanceFilter];
|
||||
_seekerStateParams set [1, _flareAngleFilter];
|
||||
_seekerStateParams set [2, missileTarget _projectile];
|
||||
_seekerStateParams set [2, _target];
|
||||
|
||||
|
@ -24,7 +24,8 @@ _target = missileTarget _projectile;
|
||||
if (isNull _target && isVehicleRadarOn vehicle _shooter) then {
|
||||
_target = cursorTarget;
|
||||
};
|
||||
if !(_target isKindOf "AllVehicles") then {
|
||||
// always allow tracking of projectiles
|
||||
if !(_target isKindOf "AllVehicles" || { _target isKindOf ["Default", configFile >> "CfgAmmo"] }) then {
|
||||
_target = nil;
|
||||
};
|
||||
_launchParams set [0, _target];
|
||||
|
@ -2,10 +2,10 @@
|
||||
#define COMPONENT_BEAUTIFIED Missile Guidance
|
||||
#include "\z\ace\addons\main\script_mod.hpp"
|
||||
|
||||
#define DRAW_GUIDANCE_INFO
|
||||
// #define DRAW_GUIDANCE_INFO
|
||||
// #define ENABLE_PROJECTILE_CAMERA
|
||||
// #define DEBUG_MODE_FULL
|
||||
#define DISABLE_COMPILE_CACHE
|
||||
// #define DISABLE_COMPILE_CACHE
|
||||
// #define ENABLE_PERFORMANCE_COUNTERS
|
||||
|
||||
#ifdef DEBUG_ENABLED_MISSILEGUIDANCE
|
||||
|
@ -91,3 +91,7 @@ General To-Do:
|
||||
X Fix GBU drag
|
||||
X Make sure all applicable pylons can hold all applicable weapons
|
||||
X NLAW is busted: figure out PLOS navigation system
|
||||
Add 9m14 textures
|
||||
Add 9m14 animation state where missile doesn't exist
|
||||
Add 9m14 joystick view rotation (+- 5 deg vertical as well)
|
||||
Add 9m14 proxy
|
||||
|
Loading…
Reference in New Issue
Block a user