Jump to content
Rydygier

[Release] Timid Icarus - Contour Flight Script

Recommended Posts

Timid-Icarus-1m.png

 

FILE

CFS 1.01 (Dropbox, APL-SA license)

 

 

INTRODUCTION

CFS script attempts to guide pointed helicopter along his waypoints using very low altitude similar to contour flight. CFS's control is paused as helicopter approaches current waypoint and resumed, when new waypoint become current. 

Testing run footage

 

 

Parameters description:

 

Helicopter - a chopper to be controlled.

Desired speed - speed in m/s, that heli will try to maintain most of the time. Default: max config speed. The faster, the more risky flight.

Desired altitude - AGL ceiling in meters: controlled heli will strive to maintain that height above the ground as much, as possible. Default: 10. Values below 10 not recommended.

Overfly buffer - additional buffer in meters added to calculated minimal height necessary to fly over an obstacle (map object etc.). Default: 2. Values below 2 not recommended. 

Obstacle detection mode - setting for obstacles detection method. 0 - less CPU hungry, pays attention only to the map objects, ignores editor-placed objects and very few exceptions amongst map objects too (example: Tanoa's sea bridge roadway). 1 - detects non-map objects as well, may cause a bit more bumpy ride, than necessary and recognize as an obstacle also objects, that should be ignored, can be more CPU-hungry. Default: 0 (recommended unless insuffice in certain scenarios).

Obstacle detection range - a multiplier of obstacles detection radius. Default: 1, which translates to the bigger of the two: (heli full length + 10) OR 20 meters. Too big radius will eat FPS fast, too low will tend to ignore big objects, that still can be collided with.

Emergency pull up - if this is enabled and code would detect, that heli gets dangerously close to the ground, emergency "pull up!" vertical velocity will be added, which often (not always) may help to avoid collision. In such situation it will override sharpness setting. Default: true.

Sharpness factor - the lower value, the more smoothed flight, but also more sluggish reactions which increase collision risk. Default: 0.2. Values 0.1-0.2 are risky. Values below 0.1 not recommended. Maximum: 1 (safest, but least elegant). Values above 1 or below 0 may cause weird behavior.  

Debug mode - added 3D visualisation markers and some numerical data on the screen. Default: false. 

 

KNOWN ISSUES & LIMITATIONS

CFS was tested with few helicopters (Ghost Hawk, Hummingbird and Huron) and settings along various, demanding paths, but further tests with various settings/on different terrains may still reveal exceptions, where collisions may occur.

Although I did, what I could to make it looking as natural, as possible, do not expect fully realistic flight model here. It's simplified, scripted makeshift. 

CFS will fail, if given map object has its shape bigger, than bounding box. One example found: sea bridges elements (pillars) on Tanoa - manual correction for them included.  

CFS will not try to pass obstacles sideways nor will try to guide the helicopter below any obstacle even, if possible (high bridges etc.). It will always try to overfly the obstacle instead.

CFS guides the helicopter towards current waypoint along the straight line, there's no path preplanning to use terrain, avoid hilltops etc.

The code is run per frame to make it fluid and reliable, so it may cause some FPS drops especially over areas with many objects, like forests etc. 

 

CODE

 

Initialization:

_handle = [heli1] spawn RYD_TI_TimidIcarus;//simple
_handle = [heli1,83.3,10,2,1,1,false,0.2,true] spawn RYD_TI_TimidIcarus;//advanced
//[helicopter,desired speed (m/s), desired altitude AGL (m), obstacle overfly buffer (m), obstacle detect mode (0/1), obstacle detect range (m), emergency pull up, smoothness factor, debug mode]

 

Ending:

removeMissionEventHandler ["EachFrame", RYD_TI_EFEH];

 

Source:

Spoiler

RYD_TI_TimidIcarus = 
	{
	private _heli = param [0,objNull,[objNull]];//helicopter to be controlled. Default : objNull (code exits)
	if (isNull _heli) exitWith {};
	
	private _desiredSpeed = param [1,((getNumber (configFile >> "CfgVehicles" >> (typeOf _heli) >> "maxSpeed"))/3.6),[0]];//speed in m/s, that heli will try to maintain most of the time. Default: max config speed. The faster, the more risky flight.
	private _desiredAltitude = param [2,10,[0]];//AGL ceiling in meters: controlled heli will strive to maintain that height abouve the ground as much, as possible. Default: 10. Values below 10 not recommended. 
	private _overflyBuffer = param [3,2,[0]];//additional buffer in meters added to calculated minimal height necessary to fly over an obstacle (map object etc.). Default: 2. Values below 2 not recommended. 
	private _obstacleDetectMode = param [4,0,[0]];//setting for obstacles detection method. 0 - less CPU hungry, pays attention only to the map objects, ignores editor-placed objects and very few exceptions amongst map objects. 1 - detects non-map objects as well, may cause a bit more bumpy ride, than necessary and recognize as an obstacle also objects, that should be ignored, can be more CPU-hungry. Default: 0 (recommended unless insuffice in certain scenarios)
	private _obstacleDetectRange = param [5,1,[0]];//a multiplier of obstacles detection radius. Default: 1, which translates to the bigger of the two: (heli full length + 10) OR 20 meters. Too big will eat FPS fast, too low will tend to ignore big objects, that still can be collided with.
	private _safetyPush = param [6,true,[true]];//if this is enabled and code would detect, that heli gets dangerously close to the ground, emergency "pull up!" vertical velocity will be added, which often may help to avoid collision. In such situation it will override sharpness setting. Default: true.
	private _sharpness = param [7,0.2,[0]];//sharpness factor. The lower value, the more smoothed flight, but also more sluggish reactions which increase collision risk. Default: 0.2. Values 0.1-0.2 are risky. Values below 0.1 not recommended. Maximum: 1. Values above 1 may cause crazy behavior.  
	private _debug = param [8,false,[true]];//Debug mode adding 3D visualisation markers and some numerical data on the screen. Default: false. 
	
	RYD_TI_Params = [_heli,_desiredSpeed,_desiredAltitude,_overflyBuffer,_obstacleDetectMode,_obstacleDetectRange,_safetyPush,_sharpness,_debug];
	
	RYD_TI_ContourFlight = 
		{
		if (isGamePaused) exitWith {};
		params ["_heli","_desiredSpeed","_desiredAltitude","_overflyBuffer","_obstacleDetectMode","_obstacleDetectRange","_safetyPush","_sharpness","_debug"];
		if (isNull _heli) exitWith {};

		RYD_TI_Control = false;
		if (_debug) then
			{			
			if (isNil "RYD_TI_a1") then
				{
				RYD_TI_a1 = createvehicle ["Sign_Arrow_Large_F",(_heli modelToWorld [0,0,10]),[],0,"CAN_COLLIDE"];
				RYD_TI_a2 = createvehicle ["Sign_Arrow_Large_Blue_F",(_heli modelToWorld [0,0,10]),[],0,"CAN_COLLIDE"];
				RYD_TI_a3 = createvehicle ["Sign_Arrow_Large_Green_F",(_heli modelToWorld [0,0,10]),[],0,"CAN_COLLIDE"];
				};			
			};
		
		_gp = group _heli;
		_bbH = boundingBoxReal _heli;
		_size = ((_bbH select 1) select 0) - ((_bbH select 0) select 0);
		_sizeL = ((_bbH select 1) select 1) - ((_bbH select 0) select 1);

		_addH = abs ((_bbH select 0) select 2);
		_overflyBuffer = _overflyBuffer + _addH;
		_finish = 0;
		_vel = _desiredSpeed;
		_desiredAlt = _desiredAltitude;

		if ((isNull _heli) or {not (alive _heli) or {(isNull _gp)}}) exitWith {true};

		_cP = currentPilot _heli;
		
		if (not (isTouchingGround _heli) and {(canMove _heli) and {((fuel _heli) > 0) and {not (isNull _cP) and {(alive _cP) and {not (isPlayer _cP) and {((count (waypoints _gp)) > 0)}}}}}}) then
			{
			_cw = currentWaypoint _gp;
			_cw = [_gp,_cw];
			_wPos = waypointPosition _cw;
			if (_wPos isEqualTo [0,0,0]) exitWith {};
			
		
			_hPos = getPosASL _heli;
			
			_cAlt = _hPos select 2;	
			_cVel = velocity _heli;
			_dst = _hPos distance2D _wPos;
			_cVelH = _cVel distance2D [0,0,0];
			
			_rAlt = (_heli modelToWorld [0,((_bbH select 1) select 1),((_bbH select 0) select 2)]) select 2;
			_rAlt2 = (_heli modelToWorldWorld [0,((_bbH select 1) select 1),((_bbH select 0) select 2)]) select 2;
			
			_rAltFront = _rAlt min _rAlt2;
			
			_rAlt3 = (_heli modelToWorld [0,((_bbH select 0) select 1),((_bbH select 0) select 2)]) select 2;
			_rAlt4 = (_heli modelToWorldWorld [0,((_bbH select 0) select 1),((_bbH select 0) select 2)]) select 2;
			
			_rAltRear = _rAlt3 min _rAlt4;
			
			_cAltATL = _rAltFront min _rAltRear;

			if ((_dst > 100) and {(_cAltATL > 5) or {(_cVelH > 10)}}) then
				{				
				_dirH = getDir _heli;
				_dir = _heli getDir _wPos;
				_diff0 = _dirH - _dir;
				_diff0 = (sin _diff0) atan2 (cos _diff0);
				_diff = abs _diff0;
				
				if ((_diff < 1) or {(_cVelH > 5) and {(_diff < 10)}}) then
					{
					RYD_TI_Control = true;
					if not (RYD_TI_wasControl) then
						{
						_heli setVariable ["RYD_TI_lastWP",_hPos]
						};
											
					_myLastWP = _heli getVariable ["RYD_TI_lastWP",[-1000,-1000,-1000]];
					_finish = (linearConversion [100,200,_dst,1,0,true]) max (linearConversion [0,100,(_heli distance2D _myLastWP),1,0,true]);
					
					_cVelN = _cVel distance [0,0,0];
					
					_range = _size + _cVelN + 10;
					_rad = ((_size + 10) max 20) * _obstacleDetectRange;
					_mplOH = 1;
					
					_passingO = _heli getVariable ["RYD_TI_PassingO",objNull];
					if not (isNull _passingO) then
						{
						_dirPO = _heli getDir _passingO;
						_diffPO = _dirH - _dirPO;
						_diffP0 = abs ((sin _diffPO) atan2 (cos _diffPO));
						
						if (_diffPO > 90) then
							{
							_bhd = _heli distance2D _passingO;
							if (_bhd > ((_sizeL/2) + 10)) then
								{
								_passingO = objNull;
								_heli setVariable ["RYD_TI_PassingO",objNull]
								}
							else
								{
								_mplOH = (1 - ((_bhd - (_sizeL/2))/((_sizeL/2) + 10))) min 1
								}
							}
						};
					
					_topPC = _passingO modelToWorld [0,0,(((boundingBoxReal _passingO) select 1) select 2)];				
					_obstacleHASL = (ATLtoASL _topPC) select 2;
					if ("bridgesea" in (toLower (str _passingO))) then
						{
						_obstacleHASL = _obstacleHASL + 5
						};	
						
					_obstacleHASL = _obstacleHASL * _mplOH;
					_corr = 0;
					_moDst = _range + _rad;
					_obstacle = objNull;
					_altASL = ((getTerrainHeightASL _hPos) max 0) + _desiredAltitude;

					for "_i" from (_size/2) to (_range + 50) step (_size/2) do
						{
						_pingPos = _heli getPos [_i,_dir];
						
						_objects = if (_obstacleDetectMode == 0) then
							{
							(nearestTerrainObjects [_pingPos, [], _rad, false, true])
							}
						else
							{
							_rem = [_heli];
							_rem appEnd (crew _heli);
							_birds = _pingPos nearObjects ["Bird",(_rad * 1.5)];
							_rem appEnd _birds;
							
							if (_debug) then
								{
								_rem appEnd [RYD_TI_a1,RYD_TI_a2,RYD_TI_a3]
								};
							
							((nearestObjects [_pingPos,[],_rad,true]) - _rem)
							};
						
							{
							_bbr = boundingBoxReal _x;
							_topC = _x modelToWorld [0,0,((_bbr select 1) select 2)];
							_top = _topC select 2;
							_topASL = (ATLtoASL _topC) select 2;
							
							if ("bridgesea" in (toLower (str _x))) then
								{
								_topASL = _topASL + 5
								};
							
							_aDst = (_heli distance2D _x) max 1;
													
							if ((_topASL > (_altASL - _overflyBuffer)) and {((_top * 4.5) > _aDst) or {(_aDst < _range)}}) then
								{
								if (_debug) then
									{
									drawIcon3D ["#(argb,8,8,3)color(0,0,0,0)",[1,1,1,1],(_x modelToWorld [0,0,((_bbr select 1) select 2)]),1,1,0,(str (round _topASL)),1,0.035,"PuristaSemibold"];
									};
								
								if (_topASL > _obstacleHASL) then
									{
									_obstacleHASL = _topASL;
									_obstacle = _x;
									_moDst = _moDst min _aDst;
									
									if (_debug) then
										{
										RYD_TI_a1 setPosATL (_x modelToWorld [0,0,((_bbr select 1) select 2)]);
										};
									};
								}
							}
						foreach _objects;
						};
						
					_heli setVariable ["RYD_TI_PassingO",_obstacle];
					_obsDst = _moDst;
					
					_desiredAlt = _altASL max (_obstacleHASL + _overflyBuffer);
					
					if (_finish > 0) then
						{
						_desiredAlt = linearConversion [0,1,_finish,_desiredAlt,((((getTerrainHeightASL _hPos) max 0) + 30) max _desiredAlt),true];
						};
															
					_diffAlt = _desiredAlt - _cAlt;
					_rPingPos = _hPos;
					_corrM = (_obstacleHASL + _diffAlt)/(sqrt _obsDst);
					
					for "_i" from (_size/2) to (_size + _cVelN) step (_size/8) do
						{
						_pingPos = _heli getPos [_i,_dir];
						_pingPos set [2,0];
						
						_corr = ((getTerrainHeightASL _pingPos) max 0) - ((getTerrainHeightASL _hPos) max 0) + _diffAlt;
						if (((abs _corr) > 0) and {(isNull _obstacle) or {(_corr > (_corrM * (sqrt _obsDst)))}}) then
							{
							_rPingPos = _pingPos;
							
							if (_debug) then
								{
								RYD_TI_a2 setPosATL _pingPos;
								};
								
							if ((_corr/(sqrt _i)) > _corrM) then
								{
								if (_debug) then
									{
									drawIcon3D ["#(argb,8,8,3)color(0,0,0,0)",[1,0.5,0,1],_pingPos,1,1,0,(str (round _corr)),1,0.035,"PuristaSemibold"];
									};
									
								_moDst = _moDst min (_heli distance2D _pingPos);
								_corrM = _corr/(sqrt _i)
								};
							};
						};

					_cAlt = _cAlt - (((getTerrainHeightASL _rPingPos) max 0) - ((getTerrainHeightASL _hPos) max 0));
					_diffAlt = _desiredAlt - _cAlt;
					
					_halfSpeed = (getNumber (configFile >> "CfgVehicles" >> (typeOf _heli) >> "maxSpeed"))/7.2;
					_vel = linearConversion [0,1,_finish,_desiredSpeed,(_halfSpeed min _vel),true];
													
					if ((_cVelN < _vel) or {((abs _diffAlt) > (10/(_cVelN max 10)))}) then
						{
						_altAm = (_diffAlt max 0)/(sqrt (_moDst max 1));
						_fPos = _hPos getPos [(_moDst max (_altAm * 3)),_dir];
						_fPos set [2,(_desiredAlt + _altAm)];

						if (_debug) then
							{
							RYD_TI_a3 setPosASL _fPos;
							};

						_safety = if (_safetyPush) then
							{
							(sqrt ((((_desiredAltitude/2) max 5) - _cAltATL) max 1))
							}
						else
							{
							1
							};
							
						_sharpnessF = if (_safety > 1) then
							{
							1
							}
						else
							{
							_sharpness
							};
						
						_vel2 = (_vel * (1 - _sharpnessF)) + (_vel * ((((_moDst max 1))/((_diffAlt max 1)^0.75)) min 1) * _sharpnessF);
						_vel = (_vel * (1 - _sharpnessF)) + (_vel * ((((_moDst max 1))/((_diffAlt max 1)^0.75)) min 1) * _sharpnessF);
						
						_dirV = _hPos vectorFromTo _fPos;
						_velV = [((((_dirV select 0) * _vel2) * _sharpnessF) + ((_cVel select 0) * (1 - _sharpnessF))),((((_dirV select 1) * _vel2) * _sharpnessF) + ((_cVel select 1) * (1 - _sharpnessF))),(((_dirV select 2) * (_vel * _safety * _sharpnessF)) + ((_cVel select 2) * (1 - _sharpnessF)))];
						_velF = _velV distance [0,0,0];

						_heli addTorque (_heli vectorModelToWorld [(((getMass _heli)^1.3)/16) * (sqrt (_velF/100)),0,0]);
						_heli setVelocity _velV;
						}
					}
				else
					{
					if (not (_diff < 5) and {(_cAlt > 1)}) then
						{
						if (_diff0 > 0) then
							{
							_heli addTorque (_heli vectorModelToWorld [0,0,-((((getMass _heli)^1.35)/25) * 3)]);
							}
						else
							{
							_heli addTorque (_heli vectorModelToWorld [0,0,((((getMass _heli)^1.35)/25) * 3)]);
							}
						};
					}
				}
			};
			
		RYD_TI_wasControl = RYD_TI_Control;
			
		if (_debug) then
			{
			hintSilent format ["spd: %1\nalt: %2\nfps: %3\nfn: %4\nctrl: %5",((velocity _heli) distance [0,0,0]),(((getPosATL _heli) select 2) min ((getPosASL _heli) select 2)),diag_fps,_finish,RYD_TI_Control];
			};
		};	

	if (_debug) then
		{
		systemChat format ["Params: %1",RYD_TI_Params];
		};

	RYD_TI_wasControl = false;	
	RYD_TI_Control = false;
	RYD_TI_EFEH = addMissionEventHandler ["EachFrame",{RYD_TI_Params call RYD_TI_ContourFlight}];
	};

sleep 1;

player allowDamage false;
player moveInCargo heli1;

_handle = [heli1] spawn RYD_TI_TimidIcarus;//simple
//_handle = [heli1,83.3,10,2,1,1,false,0.2,true] spawn RYD_TI_TimidIcarus;//advanced
//[helicopter,desired speed (m/s), desired altitude AGL (m), obstacle overfly buffer (m), obstacle detect mode (0/1), obstacle detect range (m), emergency pull up, smoothness factor, debug mode]

_wp = (group heli1) addWaypoint [(getMarkerPos "m1"),0];
_wp setWaypointType "MOVE";
_wp = (group heli1) addWaypoint [(getMarkerPos "m2"),0];
_wp setWaypointType "MOVE";
_wp = (group heli1) addWaypoint [(getMarkerPos "m3"),0];
_wp setWaypointType "MOVE";
_wp = (group heli1) addWaypoint [(getMarkerPos "m4"),0];
_wp setWaypointType "MOVE";

 

 

  • Like 7
  • Thanks 4

Share this post


Link to post
Share on other sites

Fantastic, and great news that you plan to add it to Hermes Airlift.  And love the name Timid Icarus!

  • Like 4

Share this post


Link to post
Share on other sites

Nice work Rydygier!

I'm wondering if its possible to add a tactical type flight path to how the ai flies like say through the valleys,

what im saying is, the lowest point of a valley is obviously at the bottom in between the two hills.

  

        Take Stratis map for example as seen in your two videos, they ai could seek the lowest point between two hills, or the lowest point of a ravine

or a path that is not only the shortest distance from its destination but the most concealed route.

      Concealed route would be ideal for transport of friendly ai as well as player.

 

Plans for an editor module too like Hermes?

  • Like 2

Share this post


Link to post
Share on other sites
Quote

Plans for an editor module too like Hermes?

 

I would rather first try to integrate this into HAS (depending on encountered difficulties...), say as lowest possible flight ceiling, that player can pick via mouse actions (maybe excluding slingloaded helis). Of course, if there will be a demand to turn also standalone CFS into module popular enough to justify additional work, I can try this too, but before I take this code anywhere else, let's just see some feedback, bug reports and requests. Speaking of which...

 

Quote

I'm wondering if its possible to add a tactical type flight path

 

Currently it works without any pre-planning. That means, it guides the heli along straight path and reacts dynamically on terrain shape and detected obstacles.

 

Adding such type of flight, with preliminary path choice along lowest terrain, is probably possible, something to think about. Also making the heli follow such path should be doable, perhaps using additional, dynamically added mid-waypoints along such path - that will maybe not ensure some terrific accuracy, but seems much simplier to implement than implementing CFS-controlled horizontal maneuvering, which would be a greater challenge. Not sure yet though. BTW IMO such addition would make things too complex for CFS inside HAS (the HAS logic is a maze already), so if I manage to put CFS in current form into HAS, it would rather stay in current, straight line, form while tactical form I would keep for standalone CFS. 

  • Like 3

Share this post


Link to post
Share on other sites

Good looking stuff!

 

Regarding this:

On 6/27/2020 at 6:55 PM, Rydygier said:

Heli during CFS-controlled flight doesn't tolerate well game pausing. After resuming does crazy movements for a while that depending on situation may lead to a crash. Seems, it's something between per frame object's physics manipulation and simulation pausing.

 

See note in link: https://community.bistudio.com/wiki/isGamePaused

 

In your function RYD_TI_ContourFlight put this...

RYD_TI_ContourFlight =
	{
		if (isGamePaused) exitWith {}; 
  
		/**** your code here ****/
	};

...or this...

RYD_TI_ContourFlight = 
	{
		if(not isGamePaused) then {
			/**** your code here ****/
		};
	};

...which ever floats your flying boat.

  • Like 3
  • Thanks 1

Share this post


Link to post
Share on other sites

Thanks for the info! Something, I didn't know. And pretty freshly introduced command as a solution, I see. Great. 🙂

 

EDIT: code updated accordingly.

  • Like 4

Share this post


Link to post
Share on other sites
7 hours ago, Rydygier said:

 

Quote

I'm wondering if its possible to add a tactical type flight path

 

Currently it works without any pre-planning. That means, it guides the heli along straight path and reacts dynamically on terrain shape and detected obstacles.

Can mission designer give a heli waypoints that this script will then use?  Then mission designers can define tactical paths using valleys, etc.  I'm guessing that sharp turns from one way point to another could be an issue though.

Share this post


Link to post
Share on other sites
Quote

Can mission designer give a heli waypoints that this script will then use?

 

Yes, that's how it is done now - CFS works basing on heli's waypoints and leads the heli from current position straight towards current waypoint etc, etc. 

 

Quote

 I'm guessing that sharp turns from one way point to another could be an issue though.

 

Yep, also the fact, CFS influence is gradually reduced to 0 as heli approaches current waypoint, which also could mean, heli will go up at each waypoint then back close to the ground when new waypoint become current. You can observe that in the demo mission. 

  • Like 1

Share this post


Link to post
Share on other sites
1 minute ago, Rydygier said:

Yes, that's how it is done now - CFS works basing on heli's waypoints and leads the heli from current position straight towards current waypoint etc, etc. 

That's great, as it makes it easy for mission makers to use.  It may not be dynamically calculating tactical paths, but that is really an unrelated task (that someone else could take on if its not on your priority list).  As a mission maker, I could use the editor to plot multiple paths using waypoints or markers, then dynamically/conditionally choose one of these heli paths during mission execution (so player feels like path is dynamic).

  • Like 2

Share this post


Link to post
Share on other sites

Hey @Rydygier, I am trying this script out in Prairie Fire. PF has 4 types of helis:  Cobra, Cayuse, Iriquois, and Stinger.  The script works great for Cobra and Cayuse helis.  But the Iriquois and Stingers fly badly with nose pointed straight down.  I'm guessing there is something different about the models that causes this.  It would be great if Iriquois worked since they are the troop transports.  Not sure you're supporting this, but just thought I should let you know.  Thanks for all you do sir!   The tests in the video were all executed using the simple version of your call:

_handle = [heli1] spawn RYD_TI_TimidIcarus;//simple

Edit:  On further testing I see the Cayuse has problems too.  It starts out flying good, but after a minute or two it has same nose-down problem.  Only the Cobra can fly the entire path and look good the whole way.

 

  • Thanks 1

Share this post


Link to post
Share on other sites

Yeah, one never can rely fully on custom assets, so I tend to not bother with them focusing on vanilla. Even if one day I would return to this, since I do not own PF, can't test those for debugging. 

  • Like 1

Share this post


Link to post
Share on other sites

To throw at least some guess work here, when TI moves heli forward, it looks like this:

 

						_heli addTorque (_heli vectorModelToWorld [(((getMass _heli)^1.3)/16) * (sqrt (_velF/100)),0,0]);
						_heli setVelocity _velV;

So the guess is, the torque added is too big. Formulas was tailored for vanilla assets, most like not universally correct for every model out there. Especially, if getMass is exotic/differs seriously. One could via trials and errors adjust torque value for desired models... Or figure out better formula, working with all stuff, assuming, that's the cause indeed. 

  • Thanks 1

Share this post


Link to post
Share on other sites
1 minute ago, Rydygier said:

To throw at least some guess work here, when TI moves heli forward, it looks like this:

 


						_heli addTorque (_heli vectorModelToWorld [(((getMass _heli)^1.3)/16) * (sqrt (_velF/100)),0,0]);
						_heli setVelocity _velV;

So the guess is, the torque added is too big. Formulas was tailored for vanilla assets, most like not universally correct for every model out there. Especially, ig getmass is exotic/differs seriously. 

I can try  tweaking it.  Any clue on which factor in your formula should be tweaked up or down?   1.3 or 16?

 

Does the fact that heli tips forward give you a clue on which variable to tweak?

Share this post


Link to post
Share on other sites

While I do not recall exact details right now, the basic rule is, the bigger will be whatever you put instead of (((getMass _heli)^1.3)/16) * (sqrt (_velF/100)), the stronger will be the tilt. So first I would put 0 there to see, if the issue is gone, to be sure, here's the right place to tweak. If so, the rest is about testing various values until the tilt is correct. You can work with both, 1.3 and 16, I guess...

Share this post


Link to post
Share on other sites
10 minutes ago, Rydygier said:

To throw at least some guess work here, when TI moves heli forward, it looks like this:

 


						_heli addTorque (_heli vectorModelToWorld [(((getMass _heli)^1.3)/16) * (sqrt (_velF/100)),0,0]);
						_heli setVelocity _velV;

Success!   I changed 1.3 to 1.2, and the Iriquois now flys beautifully.  I'll add some code to make the 1.3/1.2 a variable where 1.3 is default, but it sets to 1.2 if vehicle class is Iriquois.  I'll test the same for Cayuse and Stinger, and set the appropriate value for them as well.  Once it works, I will post the updated script here.

 

Thanks man!

  • Like 3

Share this post


Link to post
Share on other sites

Here's the updated script:

Spoiler

RYD_TI_TimidIcarus = 
	{
	private _heli = param [0,objNull,[objNull]];//helicopter to be controlled. Default : objNull (code exits)
	if (isNull _heli) exitWith {};
	
	private _desiredSpeed = param [1,((getNumber (configFile >> "CfgVehicles" >> (typeOf _heli) >> "maxSpeed"))/3.6),[0]];//speed in m/s, that heli will try to maintain most of the time. Default: max config speed. The faster, the more risky flight.
	private _desiredAltitude = param [2,10,[0]];//AGL ceiling in meters: controlled heli will strive to maintain that height abouve the ground as much, as possible. Default: 10. Values below 10 not recommended. 
	private _overflyBuffer = param [3,2,[0]];//additional buffer in meters added to calculated minimal height necessary to fly over an obstacle (map object etc.). Default: 2. Values below 2 not recommended. 
	private _obstacleDetectMode = param [4,0,[0]];//setting for obstacles detection method. 0 - less CPU hungry, pays attention only to the map objects, ignores editor-placed objects and very few exceptions amongst map objects. 1 - detects non-map objects as well, may cause a bit more bumpy ride, than necessary and recognize as an obstacle also objects, that should be ignored, can be more CPU-hungry. Default: 0 (recommended unless insuffice in certain scenarios)
	private _obstacleDetectRange = param [5,1,[0]];//a multiplier of obstacles detection radius. Default: 1, which translates to the bigger of the two: (heli full length + 10) OR 20 meters. Too big will eat FPS fast, too low will tend to ignore big objects, that still can be collided with.
	private _safetyPush = param [6,true,[true]];//if this is enabled and code would detect, that heli gets dangerously close to the ground, emergency "pull up!" vertical velocity will be added, which often may help to avoid collision. In such situation it will override sharpness setting. Default: true.
	private _sharpness = param [7,0.2,[0]];//sharpness factor. The lower value, the more smoothed flight, but also more sluggish reactions which increase collision risk. Default: 0.2. Values 0.1-0.2 are risky. Values below 0.1 not recommended. Maximum: 1. Values above 1 may cause crazy behavior.  
	private _debug = param [8,false,[true]];//Debug mode adding 3D visualisation markers and some numerical data on the screen. Default: false. 
	
	RYD_TI_Params = [_heli,_desiredSpeed,_desiredAltitude,_overflyBuffer,_obstacleDetectMode,_obstacleDetectRange,_safetyPush,_sharpness,_debug];
	
	RYD_TI_ContourFlight = 
		{
		if (isGamePaused) exitWith {};
		params ["_heli","_desiredSpeed","_desiredAltitude","_overflyBuffer","_obstacleDetectMode","_obstacleDetectRange","_safetyPush","_sharpness","_debug"];
		if (isNull _heli) exitWith {};

		RYD_TI_Control = false;
		if (_debug) then
			{			
			if (isNil "RYD_TI_a1") then
				{
				RYD_TI_a1 = createvehicle ["Sign_Arrow_Large_F",(_heli modelToWorld [0,0,10]),[],0,"CAN_COLLIDE"];
				RYD_TI_a2 = createvehicle ["Sign_Arrow_Large_Blue_F",(_heli modelToWorld [0,0,10]),[],0,"CAN_COLLIDE"];
				RYD_TI_a3 = createvehicle ["Sign_Arrow_Large_Green_F",(_heli modelToWorld [0,0,10]),[],0,"CAN_COLLIDE"];
				};			
			};
		
		_gp = group _heli;
		_bbH = boundingBoxReal _heli;
		_size = ((_bbH select 1) select 0) - ((_bbH select 0) select 0);
		_sizeL = ((_bbH select 1) select 1) - ((_bbH select 0) select 1);

		_addH = abs ((_bbH select 0) select 2);
		_overflyBuffer = _overflyBuffer + _addH;
		_finish = 0;
		_vel = _desiredSpeed;
		_desiredAlt = _desiredAltitude;
		
		_heliType = typeOf _heli;
		_torqueFactor = 1.3; // works for vanilla helis, plus Prairie Fire Cobras
		// Use different torqueFactor for Prairie Fire Iriquois, Cayuse and Stinger/SeaHorse helis
		if ("vn_b_air_oh6" in _heliType or "vn_b_air_ch34" in _heliType or "vn_b_air_uh1" in _heliType) then {_torqueFactor = 1.2;}; 

		if ((isNull _heli) or {not (alive _heli) or {(isNull _gp)}}) exitWith {true};

		_cP = currentPilot _heli;
		
		if (not (isTouchingGround _heli) and {(canMove _heli) and {((fuel _heli) > 0) and {not (isNull _cP) and {(alive _cP) and {not (isPlayer _cP) and {((count (waypoints _gp)) > 0)}}}}}}) then
			{
			_cw = currentWaypoint _gp;
			_cw = [_gp,_cw];
			_wPos = waypointPosition _cw;
			if (_wPos isEqualTo [0,0,0]) exitWith {};
			
		
			_hPos = getPosASL _heli;
			
			_cAlt = _hPos select 2;	
			_cVel = velocity _heli;
			_dst = _hPos distance2D _wPos;
			_cVelH = _cVel distance2D [0,0,0];
			
			_rAlt = (_heli modelToWorld [0,((_bbH select 1) select 1),((_bbH select 0) select 2)]) select 2;
			_rAlt2 = (_heli modelToWorldWorld [0,((_bbH select 1) select 1),((_bbH select 0) select 2)]) select 2;
			
			_rAltFront = _rAlt min _rAlt2;
			
			_rAlt3 = (_heli modelToWorld [0,((_bbH select 0) select 1),((_bbH select 0) select 2)]) select 2;
			_rAlt4 = (_heli modelToWorldWorld [0,((_bbH select 0) select 1),((_bbH select 0) select 2)]) select 2;
			
			_rAltRear = _rAlt3 min _rAlt4;
			
			_cAltATL = _rAltFront min _rAltRear;

			if ((_dst > 100) and {(_cAltATL > 5) or {(_cVelH > 10)}}) then
				{				
				_dirH = getDir _heli;
				_dir = _heli getDir _wPos;
				_diff0 = _dirH - _dir;
				_diff0 = (sin _diff0) atan2 (cos _diff0);
				_diff = abs _diff0;
				
				if ((_diff < 1) or {(_cVelH > 5) and {(_diff < 10)}}) then
					{
					RYD_TI_Control = true;
					if not (RYD_TI_wasControl) then
						{
						_heli setVariable ["RYD_TI_lastWP",_hPos]
						};
											
					_myLastWP = _heli getVariable ["RYD_TI_lastWP",[-1000,-1000,-1000]];
					_finish = (linearConversion [100,200,_dst,1,0,true]) max (linearConversion [0,100,(_heli distance2D _myLastWP),1,0,true]);
					
					_cVelN = _cVel distance [0,0,0];
					
					_range = _size + _cVelN + 10;
					_rad = ((_size + 10) max 20) * _obstacleDetectRange;
					_mplOH = 1;
					
					_passingO = _heli getVariable ["RYD_TI_PassingO",objNull];
					if not (isNull _passingO) then
						{
						_dirPO = _heli getDir _passingO;
						_diffPO = _dirH - _dirPO;
						_diffP0 = abs ((sin _diffPO) atan2 (cos _diffPO));
						
						if (_diffPO > 90) then
							{
							_bhd = _heli distance2D _passingO;
							if (_bhd > ((_sizeL/2) + 10)) then
								{
								_passingO = objNull;
								_heli setVariable ["RYD_TI_PassingO",objNull]
								}
							else
								{
								_mplOH = (1 - ((_bhd - (_sizeL/2))/((_sizeL/2) + 10))) min 1
								}
							}
						};
					
					_topPC = _passingO modelToWorld [0,0,(((boundingBoxReal _passingO) select 1) select 2)];				
					_obstacleHASL = (ATLtoASL _topPC) select 2;
					if ("bridgesea" in (toLower (str _passingO))) then
						{
						_obstacleHASL = _obstacleHASL + 5
						};	
						
					_obstacleHASL = _obstacleHASL * _mplOH;
					_corr = 0;
					_moDst = _range + _rad;
					_obstacle = objNull;
					_altASL = ((getTerrainHeightASL _hPos) max 0) + _desiredAltitude;

					for "_i" from (_size/2) to (_range + 50) step (_size/2) do
						{
						_pingPos = _heli getPos [_i,_dir];
						
						_objects = if (_obstacleDetectMode == 0) then
							{
							(nearestTerrainObjects [_pingPos, [], _rad, false, true])
							}
						else
							{
							_rem = [_heli];
							_rem appEnd (crew _heli);
							_birds = _pingPos nearObjects ["Bird",(_rad * 1.5)];
							_rem appEnd _birds;
							
							if (_debug) then
								{
								_rem appEnd [RYD_TI_a1,RYD_TI_a2,RYD_TI_a3]
								};
							
							((nearestObjects [_pingPos,[],_rad,true]) - _rem)
							};
						
							{
								_bbr = boundingBoxReal _x;
								_topC = _x modelToWorld [0,0,((_bbr select 1) select 2)];
								_top = _topC select 2;
								_topASL = (ATLtoASL _topC) select 2;
								
								if ("bridgesea" in (toLower (str _x))) then
									{
									_topASL = _topASL + 5
									};
								
								_aDst = (_heli distance2D _x) max 1;
														
								if ((_topASL > (_altASL - _overflyBuffer)) and {((_top * 4.5) > _aDst) or {(_aDst < _range)}}) then
									{
									if (_debug) then
										{
										drawIcon3D ["#(argb,8,8,3)color(0,0,0,0)",[1,1,1,1],(_x modelToWorld [0,0,((_bbr select 1) select 2)]),1,1,0,(str (round _topASL)),1,0.035,"PuristaSemibold"];
										};
									
									if (_topASL > _obstacleHASL) then
										{
										_obstacleHASL = _topASL;
										_obstacle = _x;
										_moDst = _moDst min _aDst;
										
										if (_debug) then
											{
											RYD_TI_a1 setPosATL (_x modelToWorld [0,0,((_bbr select 1) select 2)]);
											};
										};
									}
							}
							foreach _objects;
						};
						
					_heli setVariable ["RYD_TI_PassingO",_obstacle];
					_obsDst = _moDst;
					
					_desiredAlt = _altASL max (_obstacleHASL + _overflyBuffer);
					
					if (_finish > 0) then
						{
						_desiredAlt = linearConversion [0,1,_finish,_desiredAlt,((((getTerrainHeightASL _hPos) max 0) + 30) max _desiredAlt),true];
						};
															
					_diffAlt = _desiredAlt - _cAlt;
					_rPingPos = _hPos;
					_corrM = (_obstacleHASL + _diffAlt)/(sqrt _obsDst);
					
					for "_i" from (_size/2) to (_size + _cVelN) step (_size/8) do
						{
						_pingPos = _heli getPos [_i,_dir];
						_pingPos set [2,0];
						
						_corr = ((getTerrainHeightASL _pingPos) max 0) - ((getTerrainHeightASL _hPos) max 0) + _diffAlt;
						if (((abs _corr) > 0) and {(isNull _obstacle) or {(_corr > (_corrM * (sqrt _obsDst)))}}) then
							{
							_rPingPos = _pingPos;
							
							if (_debug) then
								{
								RYD_TI_a2 setPosATL _pingPos;
								};
								
							if ((_corr/(sqrt _i)) > _corrM) then
								{
								if (_debug) then
									{
									drawIcon3D ["#(argb,8,8,3)color(0,0,0,0)",[1,0.5,0,1],_pingPos,1,1,0,(str (round _corr)),1,0.035,"PuristaSemibold"];
									};
									
								_moDst = _moDst min (_heli distance2D _pingPos);
								_corrM = _corr/(sqrt _i)
								};
							};
						};

					_cAlt = _cAlt - (((getTerrainHeightASL _rPingPos) max 0) - ((getTerrainHeightASL _hPos) max 0));
					_diffAlt = _desiredAlt - _cAlt;
					
					_halfSpeed = (getNumber (configFile >> "CfgVehicles" >> (typeOf _heli) >> "maxSpeed"))/7.2;
					_vel = linearConversion [0,1,_finish,_desiredSpeed,(_halfSpeed min _vel),true];
													
					if ((_cVelN < _vel) or {((abs _diffAlt) > (10/(_cVelN max 10)))}) then
						{
						_altAm = (_diffAlt max 0)/(sqrt (_moDst max 1));
						_fPos = _hPos getPos [(_moDst max (_altAm * 3)),_dir];
						_fPos set [2,(_desiredAlt + _altAm)];

						if (_debug) then
							{
							RYD_TI_a3 setPosASL _fPos;
							};

						_safety = if (_safetyPush) then
							{
							(sqrt ((((_desiredAltitude/2) max 5) - _cAltATL) max 1))
							}
						else
							{
							1
							};
							
						_sharpnessF = if (_safety > 1) then
							{
							1
							}
						else
							{
							_sharpness
							};
						
						_vel2 = (_vel * (1 - _sharpnessF)) + (_vel * ((((_moDst max 1))/((_diffAlt max 1)^0.75)) min 1) * _sharpnessF);
						_vel = (_vel * (1 - _sharpnessF)) + (_vel * ((((_moDst max 1))/((_diffAlt max 1)^0.75)) min 1) * _sharpnessF);
						
						_dirV = _hPos vectorFromTo _fPos;
						_velV = [((((_dirV select 0) * _vel2) * _sharpnessF) + ((_cVel select 0) * (1 - _sharpnessF))),((((_dirV select 1) * _vel2) * _sharpnessF) + ((_cVel select 1) * (1 - _sharpnessF))),(((_dirV select 2) * (_vel * _safety * _sharpnessF)) + ((_cVel select 2) * (1 - _sharpnessF)))];
						_velF = _velV distance [0,0,0];

						//_heli addTorque (_heli vectorModelToWorld [(((getMass _heli)^1.3)/16) * (sqrt (_velF/100)),0,0]); // original
						_heli addTorque (_heli vectorModelToWorld [(((getMass _heli)^_torqueFactor)/16) * (sqrt (_velF/100)),0,0]);
						_heli setVelocity _velV;
						}
					}
				else
					{
					if (not (_diff < 5) and {(_cAlt > 1)}) then
						{
						if (_diff0 > 0) then
							{
							_heli addTorque (_heli vectorModelToWorld [0,0,-((((getMass _heli)^1.35)/25) * 3)]);
							}
						else
							{
							_heli addTorque (_heli vectorModelToWorld [0,0,((((getMass _heli)^1.35)/25) * 3)]);
							}
						};
					}
				}
			};
			
		RYD_TI_wasControl = RYD_TI_Control;
			
		if (_debug) then
			{
			hintSilent format ["spd: %1\nalt: %2\nfps: %3\nfn: %4\nctrl: %5\Desired Alt: %6",((velocity _heli) distance [0,0,0]),(((getPosATL _heli) select 2) min ((getPosASL _heli) select 2)),diag_fps,_finish,RYD_TI_Control, _desiredAlt];
			};
		};	

	if (_debug) then
		{
		systemChat format ["Params: %1",RYD_TI_Params];
		};

	RYD_TI_wasControl = false;	
	RYD_TI_Control = false;
	RYD_TI_EFEH = addMissionEventHandler ["EachFrame",{RYD_TI_Params call RYD_TI_ContourFlight}];
};

 

 

I created a new variable for _torqueFactor near top of script:

		_heliType = typeOf _heli;
		_torqueFactor = 1.3; // works for vanilla helis, plus Prairie Fire Cobras
		// Use different torqueFactor for Prairie Fire Iriquois, Cayuse and Stinger/SeaHorse helis
		if ("vn_b_air_oh6" in _heliType or "vn_b_air_ch34" in _heliType or "vn_b_air_uh1" in _heliType) then {_torqueFactor = 1.2;}; 

And then use this new var later in the script instead of hardcoding it:

						//_heli addTorque (_heli vectorModelToWorld [(((getMass _heli)^1.3)/16) * (sqrt (_velF/100)),0,0]); // original code
						_heli addTorque (_heli vectorModelToWorld [(((getMass _heli)^_torqueFactor)/16) * (sqrt (_velF/100)),0,0]);

Thanks Rydy, I love this script.

 

One thing I noticed is I think you can only have one heli at a time running this script.  I may look into making it work for multiple simultaneous helis (like my JBOY_Boat_Waypoints script allows).

 

And now I have to figure out how to make heli do a fast landing (instead of rising way up, and slowly lowering down to land...vanilla AI....sigh).

  • Like 2

Share this post


Link to post
Share on other sites
8 hours ago, johnnyboy said:

And now I have to figure out how to make heli do a fast landing

 

In Hermes Airlift Services for that purpose I use brute force, namely setVeloctiy. The trick is to ensure, it still looks convincing enough and does not end with crash. Ripped out of HAS context example of use:

 

			_frc = 0.9;
			waitUntil 
				{
				sleep 0.1;
				_alive = (alive RYD_HAS_Chopper) and {(canMove RYD_HAS_Chopper)};
				
				if not (_alive) exitWith {true};
				if not ([] call RYD_HAS_ifChopperReady) exitWith {_unable = true;true};
				if (RYD_HAS_CallCancelled) exitWith {true};
				
				if (RYD_HAS_AlternativeLanding) then
					{
					[RYD_HAS_Chopper,0.5,0,_pickPos,2,3,_frc] call RYD_HAS_AutoGuideB;
					_frc = (_frc - 0.025) max 0;
					};

				(((getPos RYD_HAS_Chopper) select 2) < _lvl)
				};	

and the function:

 

RYD_HAS_AutoGuideB = 
	{
	if (RYD_TI_ControlTI) exitWith {};
	
	params ["_heli","_lvl","_var","_refPos","_tol","_vMpl","_frc"];
		
	private _vel = velocity RYD_HAS_Chopper;
	private _lvl2 = (getPos RYD_HAS_Chopper) select 2;

	private _vel0 = random ((random (2 * _var)) - _var);
	private _vel1 = random ((random (2 * _var)) - _var);
	private _vel2 = random ((random (2 * _var)) - _var);
	
	private _dst = _heli distance2D _refPos;
	
	if (_dst > _tol) then
		{
		private _vect = (position _heli) vectorFromTo _refPos;
		private _spd = ((_dst - 1) max 1) min 10;
		_vel0 = _vel0 + ((_vect select 0) * _spd * _vMpl);
		_vel1 = _vel1 + ((_vect select 1) * _spd * _vMpl);
		};
		
	_heli setVelocity [((_vel select 0) * _frc) + (_vel0 * (1 - _frc)),((_vel select 1) * _frc) + (_vel1 * (1 - _frc)),((_vel select 2) * _frc) + ((_vel2 + ((((_lvl - _lvl2) max (-(_lvl2 * 0.5) max (-2 min -((_lvl2^0.6)/3)))) min (((_vel select 2) max 0) * 1.1)) * _vMpl)) * (1 - _frc))]	
	};

This should be used instead of (not together with) RYD_HAS_Chopper land 'GET IN';, when LZ waypoint is reached. "Get in" altitude must be same way maintained until ready to take off (so this is not for RTB). 

  • Thanks 1

Share this post


Link to post
Share on other sites
On 8/8/2021 at 7:09 PM, johnnyboy said:

And now I have to figure out how to make heli do a fast landing (instead of rising way up, and slowly lowering down to land...vanilla AI....sigh).

I've got another option for you @johnnyboy.  Making use of both setWindDir (AI land into the wind) and landing script I use in personal extraction scripts.  Makes use of vectorLinearConversion.

Don't want to hijack @Rydygier topic, so let me know if you'd like to see. 

Perhaps you could try out both, and let us know which works for you.  I may switch over to Rydygier's method if better (I have not tried myself)...

  • Like 1

Share this post


Link to post
Share on other sites
On 8/14/2021 at 8:14 PM, panther42 said:

I've got another option for you @johnnyboy.  Making use of both setWindDir (AI land into the wind) and landing script I use in personal extraction scripts.  Makes use of vectorLinearConversion.

Don't want to hijack @Rydygier topic, so let me know if you'd like to see. 

Perhaps you could try out both, and let us know which works for you.  I may switch over to Rydygier's method if better (I have not tried myself)...

I just tried your fast landing script, and it works great for me.  At some point I will likely combine Rydy's contour flight with your landing when I get to that point while building a Prairie Fire mission.  Thanks much to both of you!

  • Like 1

Share this post


Link to post
Share on other sites

This is quite a nice script, i would be glad to see it together with HAS, nice way to insert troops thru a canyon at night with out being detected by ground radar .

 

But i guess that it will take some ressources from the Pc to calculate the path from point A to point B.

 

it would be nice if it could be done so that the player have the option to decide witch route to take, if the script came up with like 3 routes that you can pick from.

 

I know it is some wild ideas but if it is to much to put inside HAS it would be great to have as standalone also..

 

Cheers 

Share this post


Link to post
Share on other sites
35 minutes ago, Play3r said:

This is quite a nice script, i would be glad to see it together with HAS

 

It was implemented in the newest (IIRC) scripted HAS wip. No official new version incoming for now, so that wip would be the best option to try the two together. HAS 1.96. Explanation in the scripted HAS thread:

 

Quote

There will be an option to make heli to fly in this manner except from slingloaded cargo delivery. In order to use it, keep in userConfig.sqf this true:

 

RYD_HAS_ContourFlightMode = true;//if enabled, new, lowest flight ceiling is added: "contour flight", where heli will try to fly as low, as possible.

 

and when ceiling change is possible at HAS call pending, set for the heli ceiling below usual minimum (for example use -250m when at default ceiling). It should be possible to switch back and forth on the fly, I hope.

 

I tested this a bit, but of course there's an ocean of circumstancies nad factors combinations to check out in order to be sure, CFS dances elegantly enough with HAS. Also CFS itself is still under testing...  

 

  • Like 1

Share this post


Link to post
Share on other sites
30 minutes ago, Rydygier said:

 

It was implemented in the newest (IIRC) scripted HAS wip. No official new version incoming for now, so that wip would be the best option to try the two together. HAS 1.96. Explanation in the scripted HAS thread:

 

 

Yes i know it is in the HAS.

i was thinking about the mark by Gunter :

 

qoute:

I'm wondering if its possible to add a tactical type flight path to how the ai flies like say through the valleys,

what im saying is, the lowest point of a valley is obviously at the bottom in between the two hills.

 

that was the one thing i was thinking about to work in the HAS. 

can see that it is not all clear in my first remark.

Share this post


Link to post
Share on other sites

Sorry, didn't read carefully enough. I wouldn't add such a thing to HAS, since in HAS player shapes the flight route using manually placed mid-waypoints. Adding some procedural system on the top of that would seriously complicate things already complicated, while seems redundant, if the player can achieve same or better results via own brain. I did some preliminary tests however if such stuff could be added to standalone CFS at least (as it was interesting scripting topic), but then other matters redirected my attention. Some day I possibly may return to this, who knows. 

  • Like 1
  • Thanks 1

Share this post


Link to post
Share on other sites

Please sign in to comment

You will be able to leave a comment after signing in



Sign In Now

×