diff --git a/addons/iron_dome/$PBOPREFIX$ b/addons/iron_dome/$PBOPREFIX$ new file mode 100644 index 0000000000..269c37fe0d --- /dev/null +++ b/addons/iron_dome/$PBOPREFIX$ @@ -0,0 +1 @@ +z\ace\addons\iron_dome \ No newline at end of file diff --git a/addons/iron_dome/CfgEventHandlers.hpp b/addons/iron_dome/CfgEventHandlers.hpp new file mode 100644 index 0000000000..03808bcdba --- /dev/null +++ b/addons/iron_dome/CfgEventHandlers.hpp @@ -0,0 +1,17 @@ +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)); + }; +}; + +class Extended_PostInit_EventHandlers { + class ADDON { + init = QUOTE(call COMPILE_SCRIPT(XEH_postInit)); + }; +}; + diff --git a/addons/iron_dome/README.md b/addons/iron_dome/README.md new file mode 100644 index 0000000000..a2ffc8b7a2 --- /dev/null +++ b/addons/iron_dome/README.md @@ -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. + +- [Dani (TCVM)](https://github.com/TheCandianVendingMachine) diff --git a/addons/iron_dome/XEH_PREP.hpp b/addons/iron_dome/XEH_PREP.hpp new file mode 100644 index 0000000000..1fce1ccdaf --- /dev/null +++ b/addons/iron_dome/XEH_PREP.hpp @@ -0,0 +1,2 @@ +PREP(projectileTrackerPFH); +PREP(proximityFusePFH); diff --git a/addons/iron_dome/XEH_postInit.sqf b/addons/iron_dome/XEH_postInit.sqf new file mode 100644 index 0000000000..0e50e4038c --- /dev/null +++ b/addons/iron_dome/XEH_postInit.sqf @@ -0,0 +1,7 @@ +#include "script_component.hpp" + +if (isServer && { GVAR(enable) }) then { + [LINKFUNC(projectileTrackerPFH)] call CBA_fnc_addPerFrameHandler; + [LINKFUNC(proximityFusePFH)] call CBA_fnc_addPerFrameHandler; +}; + diff --git a/addons/iron_dome/XEH_preInit.sqf b/addons/iron_dome/XEH_preInit.sqf new file mode 100644 index 0000000000..2e04ee4470 --- /dev/null +++ b/addons/iron_dome/XEH_preInit.sqf @@ -0,0 +1,97 @@ +#include "script_component.hpp" + +ADDON = false; + +PREP_RECOMPILE_START; +#include "XEH_PREP.hpp" +PREP_RECOMPILE_END; + +#include "initSettings.inc.sqf" + +// Server handles the tracking of all projectiles. It dispatches events to launchers to fire at specific targets +// The tracker and launcher array are global to allow for early-out if it is impossible to kill any projectiles to avoid wasting bandwidth +GVAR(trackers) = []; +GVAR(launchers) = []; + +if (isServer) then { + GVAR(nonTrackingProjectiles) = []; + GVAR(trackingProjectiles) = []; + + GVAR(interceptors) = []; + // Put these into hash table to avoid massive amounts of loops + GVAR(toBeShot) = call CBA_fnc_hashCreate; + + [QGVAR(track), { + params ["_projectile"]; + GVAR(nonTrackingProjectiles) pushBack _projectile; + }] call CBA_fnc_addEventHandler; + + [QGVAR(registerInterceptor), { + params ["_interceptor", "_target"]; + GVAR(interceptors) pushBack [_interceptor, _target, getPosASLVisual _interceptor, _interceptor distance _target]; + [GVAR(toBeShot), _target] call CBA_fnc_hashRem; + }] call CBA_fnc_addEventHandler; +}; + +[QGVAR(registerLaunchers), { + { + GVAR(launchers) pushBackUnique _x; + _x setVariable [QGVAR(targetList), []]; + _x setVariable [QGVAR(launchState), LAUNCH_STATE_IDLE]; + _x setVariable [QGVAR(lastLaunchTime), 0]; + _x setVariable [QGVAR(engagedTargets), [[], objNull] call CBA_fnc_hashCreate]; + _x setVariable [QEGVAR(missileguidance,target), objNull]; + + if (local _x) then { + _x 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; + }; + }]; + }; + } forEach _this; +}] call CBA_fnc_addEventHandler; + +[QGVAR(registerTrackers), { + { + _x params ["_tracker", "_range"]; + GVAR(trackers) pushBack [_tracker, _range]; + } forEach _this; +}] 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(addProjectilesToIntercept), { + { + GVAR(projectilesToIntercept) pushBackUnique _x; + } forEach _this; +}] call CBA_fnc_addEventHandler; + +["All", "fired", { + params ["", "", "", "", "", "", "_projectile"]; + if (local _projectile && { (typeOf _projectile) in GVAR(projectilesToIntercept) }) then { + // avoid extra bandwidth: don't make a call to the server if we don't have any systems up + GVAR(launchers) = GVAR(launchers) select { + alive _x + }; + GVAR(trackers) = GVAR(trackers) select { + _x params ["_tracker"]; + alive _tracker + }; + if !(GVAR(launchers) isEqualTo [] || { GVAR(trackers) isEqualTo [] }) then { + [QGVAR(track), [_projectile]] call CBA_fnc_serverEvent; + }; + }; +}] call CBA_fnc_addClassEventHandler; + +// Needed on all clients to properly destroy it. Despite the fact that deleteVehicle is AG EG, unless if you delete it on all clients there will still be missiles seen +[QGVAR(destroyProjectile), { + params ["_projectile"]; + deleteVehicle _projectile; +}] call CBA_fnc_addEventHandler; + +ADDON = true; + diff --git a/addons/iron_dome/XEH_preStart.sqf b/addons/iron_dome/XEH_preStart.sqf new file mode 100644 index 0000000000..022888575e --- /dev/null +++ b/addons/iron_dome/XEH_preStart.sqf @@ -0,0 +1,3 @@ +#include "script_component.hpp" + +#include "XEH_PREP.hpp" diff --git a/addons/iron_dome/config.cpp b/addons/iron_dome/config.cpp new file mode 100644 index 0000000000..bf30324096 --- /dev/null +++ b/addons/iron_dome/config.cpp @@ -0,0 +1,19 @@ +#include "script_component.hpp" + +class CfgPatches { + class ADDON { + name = COMPONENT_NAME; + units[] = {}; + weapons[] = {}; + requiredVersion = REQUIRED_VERSION; + // no point having this system without missile guidance: nothing would happen + requiredAddons[] = {"ace_common","ace_missileguidance"}; + author = ECSTRING(common,ACETeam); + authors[] = {"Dani (TCVM)"}; + url = ECSTRING(main,URL); + VERSION_CONFIG; + }; +}; + +#include "CfgEventHandlers.hpp" + diff --git a/addons/iron_dome/functions/fnc_projectileTrackerPFH.sqf b/addons/iron_dome/functions/fnc_projectileTrackerPFH.sqf new file mode 100644 index 0000000000..5482e74587 --- /dev/null +++ b/addons/iron_dome/functions/fnc_projectileTrackerPFH.sqf @@ -0,0 +1,204 @@ +#include "..\script_component.hpp" +/* + * Author: tcvm + * Handles tracking of incoming projectiles per frame + * + * Arguments: + * 0: Args + * 1: Handle + * + * Return Value: + * None + * + * Example: + * [ace_iron_dome_projectileTrackerPFH] call CBA_fnc_addPerFrameHandler + * + * Public: No + */ + +GVAR(trackers) = GVAR(trackers) select { + _x params ["_tracker", "_range"]; + #ifdef DRAW_TRACKING_INFO + drawIcon3D ["\a3\ui_f\data\IGUI\Cfg\Cursors\selectover_ca.paa", [1,1,0,1], getPos _tracker, 0.75, 0.75, 0, format ["TRACKER [%1m]", _range], 1, 0.025, "TahomaB"]; + #endif + alive _tracker +}; + +GVAR(launchers) = GVAR(launchers) select { + alive _x +}; + +[GVAR(toBeShot), { + (CBA_missionTime - _value) < GVAR(targetRecycleTime) +}] call CBA_fnc_hashFilter; + +private _idleLaunchers = GVAR(launchers) select { + (_x getVariable QGVAR(launchState)) isEqualTo LAUNCH_STATE_IDLE && { someAmmo _x } +}; + +// no point filtering if we don't have a launcher. Don't waste cycles +if (_idleLaunchers isNotEqualTo []) then { + + GVAR(nonTrackingProjectiles) = GVAR(nonTrackingProjectiles) select { + private _projectile = _x; + if (isNull _projectile) then {continueWith false}; + + 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; + [QGVAR(projectileInRange), [_projectile, _tracker]] call CBA_fnc_localEvent; + }; + } 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), _projectile] call CBA_fnc_hashHasKey; + + private _engagedPast = GVAR(interceptors) findIf { + _x params ["", "_target"]; + _projectile isEqualTo _target; + }; + + private _engaged = _engagedFuture || (_engagedPast != -1); + if !(_engaged) then { + // launch a missile + // pick first idle launcher. Could use a heuristic, but that would require O(k*l) operations, and that could be a lot + // 20 launchers * 100 projectiles = 2000 loops. Way too slow + private _bestLauncher = _idleLaunchers select 0; + _idleLaunchers deleteAt 0; + + private _targetList = _bestLauncher getVariable QGVAR(targetList); + _targetList pushBackUnique _projectile; + _bestLauncher setVariable [QGVAR(targetList), _targetList]; + + // avoid re-engaging same target + [GVAR(toBeShot), _projectile, CBA_missionTime] call CBA_fnc_hashSet; + }; + + #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 && (_timeFiredAt == 0 || { (CBA_missionTime - _timeFiredAt) >= GVAR(targetRecycleTime) }) + }; + + 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 [AMMO: %1]", someAmmo _launcher], 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 _localDirection = _launcher vectorWorldToModelVisual _turretDirection; + + private _elevation = 90 - ((_localDirection#1) atan2 (_localDirection#2)); + private _angle = acos (_turretDirection vectorCos _directionToTarget); + + if (_angle <= GVAR(launchAcceptableAngle) && _elevation >= GVAR(launchAcceptableElevation)) 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 %2", _angle, _elevation], 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 >= GVAR(timeBetweenLaunches)) 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); + diff --git a/addons/iron_dome/functions/fnc_proximityFusePFH.sqf b/addons/iron_dome/functions/fnc_proximityFusePFH.sqf new file mode 100644 index 0000000000..314d9ae891 --- /dev/null +++ b/addons/iron_dome/functions/fnc_proximityFusePFH.sqf @@ -0,0 +1,54 @@ +#include "..\script_component.hpp" +/* + * Author: 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", "_lastDistance"]; + // 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]; + _x set [3, _minDistance]; + + #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 <= GVAR(proximityFuseRange) } || { _minDistance > _lastDistance }) then { + triggerAmmo _projectile; + // if we overshot target, dont take out target + if (_minDistance <= _lastDistance && { GVAR(proximityFuseFailureChance) <= random 1 }) then { + private _explosion = createVehicle ["SmallSecondary", _target, [], 0, "CAN_COLLIDE"]; + [QGVAR(destroyProjectile), [_target]] call CBA_fnc_globalEvent; + }; + false + } else { + true + } +}; diff --git a/addons/iron_dome/initSettings.inc.sqf b/addons/iron_dome/initSettings.inc.sqf new file mode 100644 index 0000000000..cd9feed432 --- /dev/null +++ b/addons/iron_dome/initSettings.inc.sqf @@ -0,0 +1,70 @@ +[ + QGVAR(enable), "CHECKBOX", + [ELSTRING(common,Enabled), LSTRING(enable_description)], + LSTRING(category), + false, // default value + true, // isGlobal + {[QGVAR(enable), _this] call EFUNC(common,cbaSettings_settingChanged)}, + true // Needs mission restart +] call CBA_fnc_addSetting; + +[ + QGVAR(targetRecycleTime), "SLIDER", + [LSTRING(targetRecycleTime_setting), LSTRING(targetRecycleTime_description)], + LSTRING(category), + [0, 60, 15, 0, false], // default value + true, // isGlobal + {[QGVAR(targetRecycleTime), _this] call EFUNC(common,cbaSettings_settingChanged)}, + true // Needs mission restart +] call CBA_fnc_addSetting; + +[ + QGVAR(launchAcceptableAngle), "SLIDER", + [LSTRING(launchAcceptableAngle_setting), LSTRING(launchAcceptableAngle_description)], + LSTRING(category), + [1, 60, 10, 0, false], // default value + true, // isGlobal + {[QGVAR(launchAcceptableAngle), _this] call EFUNC(common,cbaSettings_settingChanged)}, + true // Needs mission restart +] call CBA_fnc_addSetting; + +[ + QGVAR(launchAcceptableElevation), "SLIDER", + [LSTRING(launchAcceptableElevation_setting), LSTRING(launchAcceptableElevation_description)], + LSTRING(category), + [-90, 90, 5, 0, false], // default value + true, // isGlobal + {[QGVAR(launchAcceptableElevation), _this] call EFUNC(common,cbaSettings_settingChanged)}, + true // Needs mission restart +] call CBA_fnc_addSetting; + +[ + QGVAR(timeBetweenLaunches), "SLIDER", + [LSTRING(timeBetweenLaunches_setting), LSTRING(timeBetweenLaunches_description)], + LSTRING(category), + [0, 60, 1, 0, false], // default value + true, // isGlobal + {[QGVAR(timeBetweenLaunches), _this] call EFUNC(common,cbaSettings_settingChanged)}, + true // Needs mission restart +] call CBA_fnc_addSetting; + +[ + QGVAR(proximityFuseRange), "SLIDER", + [LSTRING(proximityFuseRange_setting), LSTRING(proximityFuseRange_description)], + LSTRING(category), + [1, 50, 10, 0, false], // default value + true, // isGlobal + {[QGVAR(timeBetweenLaunches), _this] call EFUNC(common,cbaSettings_settingChanged)}, + true // Needs mission restart +] call CBA_fnc_addSetting; + +[ + QGVAR(proximityFuseFailureChance), "SLIDER", + [LSTRING(proximityFuseFailureChance_setting), LSTRING(proximityFuseFailureChance_description)], + LSTRING(category), + [0, 1, 0, 2, true], // default value + true, // isGlobal + {[QGVAR(proximityFuseFailureChance), _this] call EFUNC(common,cbaSettings_settingChanged)}, + true // Needs mission restart +] call CBA_fnc_addSetting; + diff --git a/addons/iron_dome/script_component.hpp b/addons/iron_dome/script_component.hpp new file mode 100644 index 0000000000..85a584d865 --- /dev/null +++ b/addons/iron_dome/script_component.hpp @@ -0,0 +1,23 @@ +#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 + +#include "\z\ace\addons\main\script_macros.hpp" diff --git a/addons/iron_dome/stringtable.xml b/addons/iron_dome/stringtable.xml new file mode 100644 index 0000000000..5ced0a510c --- /dev/null +++ b/addons/iron_dome/stringtable.xml @@ -0,0 +1,47 @@ + + + + + Enable the Iron Dome system + + + ACE Iron Dome + + + Target Recycle Time + + + How many seconds until another launcher can consider firing at the target + + + Launch Acceptable Angle + + + How many degrees offset the launcher can be before firing + + + Launch Acceptable Elevation + + + The minimum number of degrees the launcher has to be pointing up/down before firing + + + Time Between Launches + + + Minimum number of seconds between each launch of an interceptor for a single launcher + + + Proximity Fuse Range + + + How close the interceptor has to be to the target in order to detonate + + + Proximity Fuse Failure Chance + + + Chance for proximity fuse to fail to destroy target + + + \ No newline at end of file