| Scripts/telescope.js | this.name		 = "telescope";
this.author		 = "Norby, cag";
this.copyright	 = "2018 Norbert Nagy, cag";
this.license	 = "CC BY-NC-SA 4.0";
this.description = "Telescope mark all visible ships, show vitrual model, sniper ring and more.";
this.version	 = "2.0.2";
/* jshint elision: true, shadow: true, esnext: true, curly: false, maxerr: 1000, asi: true,
		  laxbreak: true, undef: true, unused: true, evil: true,  forin: true, eqnull: true,
		  noarg: true, eqeqeq: true, boss: true, loopfunc: true, strict: true, nonew: true, noempty: false
*/
/*jslint indent: 4, white: true, debug: true, continue: true, sub: true, css: false, todo: true,
		 on: false, fragment: false, vars: true, nomen: true, plusplus: true, bitwise: true,
		 regexp: true, newcap: true, unparam: true, sloppy: true, eqeq: true, stupid: true
*/
/* global addFrameCallback, clock, EquipmentInfo, isValidFrameCallback, log,
		  missionVariables, oolite, player, Quaternion, removeFrameCallback, SoundSource,
		  system,  Timer, worldScripts, Vector3D, Script
*/
		(function(){
/* validthis: true */
"use strict";
/*
 * customizable subset of property values also available in-game as primable equipment
 */
// NB: editting these values here WILL NOT take effect! Provided as illistration only.
//	   Use the in-station option facility (F4).	 If you insist on editing this
//	   file, make sure your changes are made in:
//			this._load_missionVariables()
//	   Default values get assigned there in the absence of missionVariables
// 'config' page
this.$AutoScan = true;					//check continually for new isVisible isPiloted target and scan if found
this.$AutoScanMaxRange = 1e6;			//meters, how far targets will be reported
this.$AutoLock = 1;						//degrees, if no target and something in crosshairs within this diff. from center, 1=lightball size, 0=off
this.$GravLock = 20;					//degrees, navigation scanner relock in this center cone ( 0-180, 20=about the screen height)
this.$IdentLock = 180;					//degrees, if ident pressed or target lost then lock in this center cone ( 0-180, 90=anything fwd,180=the whole sphere )
this.$IdentDelay = 4;					//(new) quarter seconds, time targeting is suspended following Ident unlock, default: 4 (1 sec)
										// otherwise it *could* immediately re-acquire same target!
										// - this comes down to a pilot's style, whether or not you move the aim point before/after unlocking
this.$FarStatus = false;				//red ball reveal pirates over normal scanner if true
this.$MaxTargets = 200;					//limitable to reduce FPS drop in systems with many ships, min. 4, max. 200
this.$RedAlertDist = 30000;				//meters, show lollipops in red alert within this distance only
this.$Steering = 0;						//auto steering if lock nearest or each step in the target list with activate, 2: each, 1: nearest only, 0 off;
this.$LightBalls = true;				//turn on or off all lightballs, but markes on the scanner will be remain
this.$ShipLightBalls = true;			//turn on or off the lightballs with scanner markers of the ships, but cargo, etc. remain
this.$LargeLightBalls = false;			//lightballs are increasing depending on the distance or remains small
this.$LightBallMinDist = 1000;			//meters, if target is inside then remove the lightball marker
this.$LightBallShipMinDist = 5000;		//meters, if target is ship and inside this range then remove the lightball marker
this.$DEFAULT_ML_RINGS = 23;			// as per 1.15, in green Alert or weapons off-line
this.$MassLockRings = 23;				//coloured circles around ships and planets in green alert or weapons off-line
										// - (new) now are bit flags for when to show
this.$MassLockViewDirn = 1;				//(new) bit flags for in which view masslock rings are shown
this.$BrightMassLockRings = false;		//brighter circles around ships and planets
this.$SniperRingSize = 2;				//size of the sniper ring ( between 1 and 5, default: 2 )
this.$SniperRingActive = 42;			//states when sniper ring is active (6: 3 alerts * 2 weapons states)
this.$SniperRange = 25600;				//meters, if the target is inside then show sniper ring
this.$SniperMinRange = 10000;			//meters, show sniper ring if the target is over this distance
this.$SniperRingColor = [0.3, 0.3, 0.3];//(new) colour of the sniper ring, default is lightGrayColor
this.$ShowVisualTarget = 0;				//show 3D model of target, 2: on, 1: only when weaps off-line, 0 off;
this.$VisualTargetNormalSize = 6;		//zoomed size of the visual target with off-line weapons ( between 0 and 8, default: 6 )
this.$VisualTargetCombatSize = 4;		//size of the visual target with online weapons ( between 0 and 8, default: 4 )
this.$VisualTargetRing = true;			//show a ring around the visual target
this.$ShowVisualStation = true;			//show or not show the 3D model of the targeted station
this.$ShowVisualQuestionMark = false;	//if a ship has no visual model in effecdata.plist show a big "?" model
this.$ModelRingColor = [0.3, 0.3, 0.3]; //(new) colour of ring around 3D model, default is lightGrayColor
this.$VTarget_HUD_shift = [0, 0, 0];	//position shift for your HUD's built-in visual target screen if any
// 'UI_and_docs' page
this.$ConsoleMsgDurn = 5;				// duration in sec for console messages
this.$GravScanMsgFreq = 3;				// bit flags for frequency of gravity scanner update msgs
this.$IdentMessages = true;				// flag for displaying/suppressing Ident key messages
this.$ShowSummary = true;				// display a summary of changes when exiting station options
										// - first conditionally displayed option
// 'experimental' page					// new options/features
this.$TargetOnlyHostile = false;		// ignore targeting cargo, pods and rocks in Red Alert
this.$RemoveInFlight = false;			// cut in half # of entries in equipment's mode cycle
// constants for MFD(s) filtering
this.$MFD_DYNAMIC_ALLSET = 127;			// highest bit = 64, Number('0x007f')
this.$MFD_STATIC_ALLSET = 4095;			// highest bit = 2048, Number('0x0fff')
this.$MFDFiltering = false;				// toggle for filtering MFD output
this.$MFDPrimaryStatic = this.$MFD_STATIC_ALLSET;// bit flags for filtering MFD using static properties
this.$MFDPrimaryDynamic = this.$MFD_DYNAMIC_ALLSET;// bit flags for filtering MFD using dynamic properties
this.$SeparateMFDs = false;				// toggle for adding an auxiliary MFD
this.$MFDAuxStatic = this.$MFD_STATIC_ALLSET; // bit flags for filtering MFD using static properties
this.$MFDAuxDynamic = this.$MFD_DYNAMIC_ALLSET;// bit flags for filtering MFD using dynamic properties
this.$Thargoids = false;				//you will get some aliens right after undock to test Telescope
// Beta licence feature in station options (testing dynamics)
this.$BetaLicence = '';					// choice for licence of experimental options
this.$BetaLicenceTimestamp = '';		// date of player accepting license agreement for experimental options
this.$BetaLicenceSystem = '';			// system where this occured; preserved in missionVariables (_reloadFromStn)
this.$DebugMessages = false;			// flag for logging debug messages
///////////////////////////////////////////////////////////////////////////////////////////////////
// internal properties, should not touch //////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////
this.$PrimaryMFD_name = 'TelescopeMFD';
this.$AuxilaryMFD_name = 'TelescopeAuxMFD';
this.$are_Steering = false;				// flag as to whether ship is auto-steering
this.$DamageMsg = true;					// flag to show messgage less frequently
this.$FixedTel = 0;						//cheaply fixed Telescope with drawbacks
this.$FixedGS = 0;						//cheaply fixed Gravity Scanner with drawbacks
this.$FixedSD = 0;						//cheaply fixed Small Dish with drawbacks
this.$FixedLD = 0;						//cheaply fixed Large Dish with drawbacks
this.$GravScanCount = 0;				//Gravity Scan counter to call aliens
this.$IdentKeyPress = 0;				// count for 'ident' key presses: 1st to lock target, 2nd to steer (if turned on), next press will unlock
// IdentKeyPress values
this.$IDENT_READY = 0;
this.$IDENT_LOCKED = 1;
this.$IDENT_STEERING = 2;
this.$IDENT_UNLOCK = 3;
this.$IDENT_STEER_DELAY = 4;
this.$IDENT_STEP_DELAY = 5;
this.$MaxRange = 1e15;					//10^15m, usable part of double precision for filteredEntities
this.$MASSLOCK_RING_SCALE = 41.8;		//masslock ring scale, used for .scale() calc's
										// to abort the resultant events shipTargetAcquired & shipTargetLost
this.$SoundScan = null;					//scan soundsource
this.$Timer_auto_updates = null;		//AutoScan timer get targets from normal scanner and do scan if new far target in the front view
this.$extenderActive = null;			// maintained for _condition statements in Station Options
// values from activate/mode in flight changes
//	 missionVariables - are VERY slow, so only use on load/save game
this.$TelescopeMenuSteering		= 1;	// defaults low so as to not overwhelm initiates
this.$TelescopeMenuLightballs	= 3;
this.$TelescopeMenuMasslockRings= 1;
this.$TelescopeMenuSniper		= 2;
this.$TelescopeMenuTargets		= 3;
this.$TelescopeMenuVisual		= 1;
this.$TelescopeMenuVisualSize	= 4;
this.$UserChangedSettings = 0;			// bit flags to signal changes in-game; see _SetLightballs et. al.
this.$SightingsMap = [];				// persistent array of Sightings that comprise all the telescope sees
this.$curr_Sighting = { map: null, ent: null, marker: null, marker_type: null, name: null };	// info on current Sighting
this.$Sighting_events_FCB = null;		// store frame callback for _Sighting_events()
this.$fps_closure = null;				// closure for fps_monitor
this.$Telescope_not_in_use = true;		// flag set in startUpComplete where it's determined if player has a telescope, used to stop event handlers
///////////////////////////////////////////////////////////////////////////////////////////////////
// legacy properties for oxp support //////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////
// new: a simpler way to get the entity a far target marker is refering to is a property
//      of the telescope marker: ps.target.$TelescopeTarget
//   eg. var target = ps.target;
//       if( target.dataKey === 'telescopemarker' )
//           target = target.$TelescopeTarget;
//   OR  var target = ps.target.dataKey === 'telescopemarker' ? ps.target.$TelescopeTarget : ps.target;
this.$fakeTelescopeList = function() {
	this[ 0 ] = null;
};
this.$fakeTelescopeList.prototype.length = 1;
this.$fakeTelescopeList.prototype.indexOf = function indexOf( ent ) {
	var that = indexOf;
	var ws = (that.ws = that.ws || worldScripts.telescope);
	var mapping = (that.mapping = that.mapping || ws.$SightingsMap);
	var index = ws._Sighting_index( ent );
	if( index < 0 ) return -1;
	this[ 0 ] = mapping[ index ].ent;
	return 0;
};
this.$TelescopeList = new this.$fakeTelescopeList();
this.$TelescopeListi = 0;
// changing variable & fn names breaks all external oxp references
// - external refs usually just ws.$TelescopeList[ ws.$TelescopeListi - 1 ], ie. current far target
// so we'll maintain TelescopeList as a one element array and set TelescopeListi to 0 or 1 accordingly
this.$TelescopeVPos = [0, 0, 0];		// maintain for Carriers oxp  (position of the visual effect)
this.$TelescopeVPosHUD = [0, 0, 0];		// maintain for Carriers oxp  (position shift for your HUD's built-in visual target screen if any)
this.$TelescopeSteerFCB = null;			// maintain for Towbar oxp
this.$TelescopeTargetSet = false;		// used by Telescope and EscortDeck
this.$TelescopeRing = null;				// maintain for VimanaHUD oxp
this.$TelescopeVSize = null;			// maintain for VimanaHUD oxp
this.$TelescopeVZoomSize = null;		// maintain for VimanaHUD oxp
//flag scanning to avoid double scan; it's like a write lock: when we set ps.target, we use this
///////////////////////////////////////////////////////////////////////////////////////////////////
// world script events ////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////
///
this.startUp = function startUp() {
	var ws = worldScripts.telescope;
///
if( worldScripts.NShields ) // too much logging
	worldScripts.NShields._logging = false;
///
	try {
		ws.$SoundScan = new SoundSource();
		ws.$SoundScan.sound = "ScanSound.ogg";						//sound of the Gravity Scanner
		ws.$SoundScan.loop = false;
		ws.$SoundScan.repeatCount = 1;
		var hud = worldScripts.hudselector;
		if( hud ) {
			//hud.startUpComplete = ws.$HUDStartUpComplete;
			//hud.$HUDSelectorSetMFDs = ws.$HUDSelectorSetMFDs;
			ws._registerHUDSelector();
		}
		// create sightings closure
		ws._init_Sightings_closure();
		ws._load_missionVariables();
		if( ws.$DebugMessages ) {
			ws._debug_Sightings_closure();
		}
		ws._reload_config( ws.$DebugMessages );						// if DebugMessages, will call _report_config
	} catch( err ) {
		log( ws.name, ws._reportError( err, startUp, ws.$SoundScan, 1 ) );
		if( ws.$DebugMessages )
			throw err;
	}
}
this.startUpComplete = function startUpComplete() {
	var ws = worldScripts.telescope;
	var ps = player && player.ship;
	var fps = worldScripts.telescope_fps_monitor;
	if( fps ) {
		var fm = ws.$fps_closure = fps._fps_monitor_closure;		// not called as it self-initiates
		//	_init_fps_monitor( oxp_name, paused, no_fcb )
		fm._init_fps_monitor(  'telescope', true );
		//	_setup_fps_report( minutes, shortterm, longterm, filelog, console, duration )
		fm._setup_fps_report(  1,		2,		   5,		 ws.$DebugMessages, false );
		//	_setup_fps_calc( cut_low, cut_high, harmonic, fps_only, median, mode,  mean,  high,	 low )
		fm._setup_fps_calc(	 2,		  0,		true,	  false,	false,	false, false, true,	 true );
	}
	ws._initOxpVars();												// moved from startUp as some oxp's will load after us
	// extenderActive is used in station options' _condition statements
	ws.$extenderActive = ps.equipmentStatus( 'EQ_TELESCOPEEXT' ) === 'EQUIPMENT_OK';
	ws._startStationOptions();
	if( ps.equipmentStatus( 'EQ_TELESCOPE' ) === 'EQUIPMENT_UNAVAILABLE' ) {
		ws.$Telescope_not_in_use = true;							// ship has no telescope, prevent world script handlers from running
	} else {
		ws.$Telescope_not_in_use = false;
	}
}
// load & save events /////////////////////////////////////////////////////////////////////////////
this._load_missionVariables = function _load_missionVariables() {
	var ws = worldScripts.telescope;
	var item = null, bool = [false, true];
	// remember menu values loaded so can switch 1.15 <-> 2.0 repeatedly
	var menuLightballs = false, menuSniper = false, menuSteering = false,
		menuTargets = false, menuVisual = false, menuVisualSize = false;
	// TelescopeVisualTargetRing is unique to ver.2
	var savedIsOrig = missionVariables.$TelescopeVisualTargetRing === null;
/// original telescope missionVariables
	// inflight options
	// - the original used these to store option values; here we store options on their own
	//   thus, we apply these first so inflight config is in sync
	if( savedIsOrig ) {
		item = missionVariables.$TelescopeMenuLightballs;
		if( item !== null ) {
			ws._oldSetLightballs( item );
			menuLightballs = true;									// preserve LightBalls, ShipLightBalls, MassLockRings, BrightMassLockRings & LargeLightBalls
		}
	} else {
		item = missionVariables.$TelescopeMenuLightballs;
		if( item !== null ) ws._SetLightballs( item );
		item = missionVariables.$TelescopeMenuMasslockRings;
		if( item !== null ) ws._SetMasslockRings( item );
	}
	item = missionVariables.$TelescopeMenuSniper;
	if( item !== null ) {
		ws._SetSniper( item );
		menuSniper = savedIsOrig;									// preserve SniperRange, SniperMinRange
	}
	item = missionVariables.$TelescopeMenuSteering;
	if( item !== null ) {
		ws._SetSteering( item );
		menuSteering = savedIsOrig;									// preserve Steering
	}
	item = missionVariables.$TelescopeMenuTargets;
	if( item !== null ) {
		ws._SetTargets( item );
		menuTargets = savedIsOrig;									// preserve MaxTargets
	}
	item = missionVariables.$TelescopeMenuVisual;
	if( item !== null ) {
		ws._SetVisual( item );
		menuVisual = savedIsOrig;									// preserve ShowVisualTarget, VisualTargetRing, TelescopeRing, ShowVisualStation, ShowVisualQuestionMark
	}
	item = missionVariables.$TelescopeMenuVisualSize;
	if( item !== null ) {
		ws._SetVisualSize( item );
		menuVisualSize = savedIsOrig;								// preserve VisualTargetCombatSize, TelescopeVSize, VisualTargetNormalSize, TelescopeVZoomSize
	}
	// state of equipment
	// - these missionVariables are common to both the original and this version
	item = missionVariables.$TelescopeFixedTel;
	ws.$FixedTel = item !== null ? item : 0;
	item = missionVariables.$TelescopeFixedGS;
	ws.$FixedGS = item !== null ? item : 0;
	item = missionVariables.$TelescopeFixedSD;
	ws.$FixedSD = item !== null ? item : 0;
	item = missionVariables.$TelescopeFixedLD;
	ws.$FixedLD = item !== null ? item : 0;
	// renamed from original $TelescopeGSC -> $TelescopeGravScanCount
	item = savedIsOrig ? missionVariables.$TelescopeGSC : missionVariables.$TelescopeGravScanCount;
	ws.$GravScanCount = item !== null ? item : 0;
/// new station options
	// light balls
	if( savedIsOrig ) {
		if( !menuLightballs ) {										// 1.15 defaults
			ws.$LightBalls = true;
			ws.$ShipLightBalls = true;
			ws.$MassLockRings = this.$DEFAULT_ML_RINGS;				// default green alert or weapons off-line
			ws.$BrightMassLockRings = false;
			ws.$LargeLightBalls = false;
		} // else set via _SetLightballs above
	} else {
		let isBetaSaveGame = !missionVariables.hasOwnProperty( '$TelescopeMassLockRings' );
		item = missionVariables.$TelescopeMassLockBorders;
		if( item !== null ) {										// renamed from beta
			delete missionVariables.$TelescopeMassLockBorders;
			if( isBetaSaveGame ) {
				missionVariables.TelescopeMassLockRings = item;
			}
		}
		item = missionVariables.$TelescopeBrightMassLockBorders;
		if( item !== null ) {										// renamed from beta
			delete missionVariables.$TelescopeBrightMassLockBorders;
			if( isBetaSaveGame ) {
				missionVariables.TelescopeBrightMassLockRings = item;
			}
		}
		item = missionVariables.$TelescopeLightBalls;
		ws.$LightBalls = item !== null ? bool[ item ] : true;		// default on
		item = missionVariables.$TelescopeShipLightBalls;
		ws.$ShipLightBalls = item !== null ? bool[ item ] : true;
		item = missionVariables.$TelescopeLargeLightBalls;
		ws.$LargeLightBalls = item !== null ? bool[ item ] : false;	// default off
		item = missionVariables.$TelescopeMassLockRings;
		ws.$MassLockRings = item !== null ? item : this.$DEFAULT_ML_RINGS; // default green alert or weapons off-line
		item = missionVariables.$TelescopeShowMassLock;
		if( item !== null ) {										// deprecated from beta (using MassLockRings flags only)
			delete missionVariables.$TelescopeShowMassLock;
			if( item === 0 && isBetaSaveGame ) {
				ws.$MassLockRings = 0;								// were turned off
			}
		}
		item = missionVariables.$TelescopeBrightMassLockRings;
		ws.$BrightMassLockRings = item !== null ? bool[ item ] : false;	// default off
	}
	item = savedIsOrig ? null : missionVariables.$TelescopeLightBallMinDist;
	ws.$LightBallMinDist = item !== null ? item : 1000;
	item = savedIsOrig ? null : missionVariables.$TelescopeLightBallShipMinDist;
	ws.$LightBallShipMinDist = item !== null ? item : 5000;
	item = savedIsOrig ? null : missionVariables.$TelescopeMassLockViewDirn;
	ws.$MassLockViewDirn = item !== null ? item : 1;				// default forward view only
	// sniper ring
	item = savedIsOrig ? null : missionVariables.$TelescopeSniperRingSize;
	ws.$SniperRingSize = item !== null ? item : 2;
	item = savedIsOrig ? null : missionVariables.$TelescopeSniperRingActive;
	ws.$SniperRingActive = item !== null ? item : 42;
	item = savedIsOrig ? null : missionVariables.$TelescopeSniperRingColor;
	ws.$SniperRingColor = item !== null ? JSON.parse( item ) : [0.3, 0.3, 0.3];
	if( savedIsOrig ) {
		if( !menuSniper ) {											// 1.15 defaults
			ws.$SniperRange = 25600;
			ws.$SniperMinRange = 10000;
		} // else set via _SetSniper above
	} else {
		item = missionVariables.$TelescopeSniperRange;
		ws.$SniperRange = item !== null ? item : 25600;
		item = missionVariables.$TelescopeSniperMinRange;
		ws.$SniperMinRange = item !== null ? item : 10000;
	}
	// visual target
	if( savedIsOrig ) {
		if( !menuVisual ) {											// 1.15 defaults
			ws.$ShowVisualTarget = 0;
			ws.$VisualTargetRing = true;
			ws.$TelescopeRing = true;								// maintain for oxps
			ws.$ShowVisualStation = true;
			ws.$ShowVisualQuestionMark = false;
		} // else set via _SetVisual above
	} else {
		item = missionVariables.$TelescopeShowVisualTarget;
		ws.$ShowVisualTarget = item !== null ? item : 0;			// default is choice 'Off'
		item = missionVariables.$TelescopeVisualTargetRing;
		ws.$VisualTargetRing = item !== null ? bool[ item ] : true;
		ws.$TelescopeRing = ws.$VisualTargetRing;					// maintain for oxps
		item = missionVariables.$TelescopeShowVisualStation;
		ws.$ShowVisualStation = item !== null ? bool[ item ] : true;
		item = missionVariables.$TelescopeShowVisualQuestionMark;
		ws.$ShowVisualQuestionMark = item !== null ? bool[ item ] : false;
	}
	if( savedIsOrig ) {
		if( !menuVisualSize ) {										// 1.15 defaults
			ws.$VisualTargetNormalSize = 6;
			ws.$TelescopeVZoomSize = 6;								// maintain for oxps
			ws.$VisualTargetCombatSize = 4;
			ws.$TelescopeVSize = 4;									// maintain for oxps
		} // else set via _SetVisualSize above
	} else {
		item = missionVariables.$TelescopeVisualTargetNormalSize;
		ws.$VisualTargetNormalSize	= item !== null ? item : 6;
		ws.$TelescopeVZoomSize = ws.$VisualTargetNormalSize;		// maintain for oxps
		item = missionVariables.$TelescopeVisualTargetCombatSize;
		ws.$VisualTargetCombatSize	= item !== null ? item : 4;
		ws.$TelescopeVSize = ws.$VisualTargetCombatSize;			// maintain for oxps
	}
	item = savedIsOrig ? null : missionVariables.$TelescopeModelRingColor;
	ws.$ModelRingColor = item !== null ? JSON.parse( item ) : [0.3, 0.3, 0.3];
	item = savedIsOrig ? null : missionVariables.$TelescopeVTarget_HUD_shift;
	ws.$VTarget_HUD_shift = item !== null ? JSON.parse( item ) : [0, 0, 0];
	ws.$TelescopeVPosHUD = ws.$VTarget_HUD_shift;					// maintain for oxps
	// miscellaneous
	if( savedIsOrig ) {
		if( !menuSteering ) {										// 1.15 defaults
			ws.$Steering = 0;
		} // else set via _SetSteering above
	} else {
		item = missionVariables.$TelescopeSteering;
		ws.$Steering = item !== null ? item : 0;					// default is choice 'Off'
	}
	if( savedIsOrig ) {
		if( !menuTargets ) {										// 1.15 defaults
			ws.$MaxTargets = 200;
		} // else set via _SetTargets above
	} else {
		item = missionVariables.$TelescopeMaxTargets;
		ws.$MaxTargets = item !== null ? item : 200;
	}
	item = savedIsOrig ? null : missionVariables.$TelescopeRemoveInFlight;
	ws.$RemoveInFlight = item !== null ? bool[ item ] : false;
	item = savedIsOrig ? null : missionVariables.$TelescopeAutoScan;
	ws.$AutoScan = item !== null ? bool[ item ] : true;
	item = savedIsOrig ? null : missionVariables.$TelescopeAutoScanMaxRange;
	ws.$AutoScanMaxRange = item !== null ? item : 1e6;
	item = savedIsOrig ? null : missionVariables.$TelescopeFarStatus;
	ws.$FarStatus = item !== null ? bool[ item ] : false;
	item = savedIsOrig ? null : missionVariables.$TelescopeAutoLock;
	ws.$AutoLock = item !== null ? item : 1;						// default cone of radius 1 degree
	item = savedIsOrig ? null : missionVariables.$TelescopeGravLock;
	ws.$GravLock = item !== null ? item : 20;						// default cone of radius 20 degrees
	item = savedIsOrig ? null : missionVariables.$TelescopeIdentLock;
	ws.$IdentLock = item !== null ? item : 180;						// default cone of radius 180 degrees, ie. whole sky
	item = savedIsOrig ? null : missionVariables.$TelescopeIdentDelay;
	ws.$IdentDelay = item !== null ? item : 4;						// time in 0.25 seconds, ie. 1 second
	item = savedIsOrig ? null : missionVariables.$TelescopeRedAlertDist;
	ws.$RedAlertDist = item !== null ? item : 30000;				// default to max range of military laser
	// - not a cheat, really, as just showing lollipop where shot came from, cannot target w/o extender
	// UI & docn
	item = savedIsOrig ? null : missionVariables.$TelescopeConsoleMsgDurn;
	ws.$ConsoleMsgDurn = item !== null ? item : 5;					// time in seconds
	item = savedIsOrig ? null : missionVariables.$TelescopeGravScanMsgFreq;
	ws.$GravScanMsgFreq = item !== null ? item : 3;					// default is choices 'progress endpoints' & 'progress quarterly update'
	item = savedIsOrig ? null : missionVariables.$TelescopeIdentMessages;
	ws.$IdentMessages = item !== null ? bool[ item ] : true;
	item = savedIsOrig ? null : missionVariables.$TelescopeShowSummary;
	ws.$ShowSummary = item !== null ? bool[ item ] : true;
	item = savedIsOrig ? null : missionVariables.$TelescopeDebugMessages;
	ws.$DebugMessages = item !== null ? bool[ item ] : false;
	// MFD(s) & filtering
	item = savedIsOrig ? null : missionVariables.$TelescopeMFDFiltering;
	ws.$MFDFiltering = item !== null ? bool[ item ] : false;
	item = savedIsOrig ? null : missionVariables.$TelescopeMFDfilterStatic;
	ws.$MFDPrimaryStatic = item !== null ? item : ws.$MFD_STATIC_ALLSET;
	item = savedIsOrig ? null : missionVariables.$TelescopeMFDfilterDynamic;
	ws.$MFDPrimaryDynamic = item !== null ? item : ws.$MFD_DYNAMIC_ALLSET;
	item = savedIsOrig ? null : missionVariables.$TelescopeSeparateMFDs;
	ws.$SeparateMFDs = item !== null ? bool[ item ] : false;
	item = savedIsOrig ? null : missionVariables.$TelescopeMFDAuxStatic;
	ws.$MFDAuxStatic = item !== null ? item : ws.$MFD_STATIC_ALLSET;
	item = savedIsOrig ? null : missionVariables.$TelescopeMFDAuxDynamic;
	ws.$MFDAuxDynamic = item !== null ? item : ws.$MFD_DYNAMIC_ALLSET;
	// $Thargoids was never saved in original so it remains a one-off switch from a station dock
	if( missionVariables.$TelescopeOptionsSaveGameReminder ) // never implemented
		delete missionVariables.$TelescopeOptionsSaveGameReminder;
	item = savedIsOrig ? null : missionVariables.$TelescopeBetaLicence;
	if( item !== null ) {
		ws.$BetaLicence = item;
		ws.$BetaLicenceTimestamp = missionVariables.$TelescopeBetaLicenceTimestamp;
		ws.$BetaLicenceSystem = missionVariables.$TelescopeBetaLicenceSystem;
	}
	ws.$UserChangedSettings = 0;									// clear flags as _initOxpVars will updateMenuVars
}
this.playerWillSaveGame = function playerWillSaveGame( /*message*/ ) {
	var that = playerWillSaveGame;
	var ws = (that.ws = that.ws || worldScripts.telescope);
	if( missionVariables.hasOwnProperty( '$TelescopeRedAlertLimiter' ) )// removed from beta
		delete missionVariables.$TelescopeRedAlertLimiter;
	// inflight options
	missionVariables.$TelescopeMenuLightballs			= ws._getOldLightballs();	// set to conform with 1.15
	missionVariables.$TelescopeMenuMasslockRings		= ws.$TelescopeMenuMasslockRings; // 1.15 will ignore
	missionVariables.$TelescopeMenuSniper				= ws.$TelescopeMenuSniper;
	missionVariables.$TelescopeMenuSteering				= ws.$TelescopeMenuSteering;
	missionVariables.$TelescopeMenuTargets				= ws.$TelescopeMenuTargets;
	missionVariables.$TelescopeMenuVisual				= ws.$TelescopeMenuVisual;
	missionVariables.$TelescopeMenuVisualSize			= ws.$TelescopeMenuVisualSize;
	// state of equipment
	missionVariables.$TelescopeGravScanCount			= ws.$GravScanCount;
	missionVariables.$TelescopeGSC						= ws.$GravScanCount;
	// - added to ensure reversion to original is complete
	missionVariables.$TelescopeFixedTel					= ws.$FixedTel;
	missionVariables.$TelescopeFixedGS					= ws.$FixedGS;
	missionVariables.$TelescopeFixedSD					= ws.$FixedSD
	missionVariables.$TelescopeFixedLD					= ws.$FixedLD;
	// NB: boolean values must be stored as 0 or 1, else loads false, true as strings(!) which are always true
	// light balls
	missionVariables.$TelescopeLightBalls				= ws.$LightBalls ? 1 : 0;
	missionVariables.$TelescopeShipLightBalls			= ws.$ShipLightBalls ? 1 : 0;
	missionVariables.$TelescopeLargeLightBalls			= ws.$LargeLightBalls ? 1 : 0;
	missionVariables.$TelescopeLightBallMinDist			= ws.$LightBallMinDist;
	missionVariables.$TelescopeLightBallShipMinDist		= ws.$LightBallShipMinDist;
	// masslock rings
	missionVariables.$TelescopeMassLockRings			= ws.$MassLockRings;
	missionVariables.$TelescopeBrightMassLockRings		= ws.$BrightMassLockRings ? 1 : 0;
	missionVariables.$TelescopeMassLockViewDirn			= ws.$MassLockViewDirn ? ws.$MassLockViewDirn : 1;
	// sniper ring
	missionVariables.$TelescopeSniperRingSize			= ws.$SniperRingSize;
	missionVariables.$TelescopeSniperRingActive			= ws.$SniperRingActive;
	missionVariables.$TelescopeSniperRange				= ws.$SniperRange;
	missionVariables.$TelescopeSniperMinRange			= ws.$SniperMinRange;
	missionVariables.$TelescopeSniperRingColor			= JSON.stringify( ws.$SniperRingColor );
	// visual target
	missionVariables.$TelescopeShowVisualTarget			= ws.$ShowVisualTarget;
	missionVariables.$TelescopeVisualTargetNormalSize	= ws.$VisualTargetNormalSize;
	missionVariables.$TelescopeVisualTargetCombatSize	= ws.$VisualTargetCombatSize;
	missionVariables.$TelescopeShowVisualStation		= ws.$ShowVisualStation ? 1 : 0;
	missionVariables.$TelescopeShowVisualQuestionMark	= ws.$ShowVisualQuestionMark ? 1 : 0;
	missionVariables.$TelescopeVisualTargetRing			= ws.$VisualTargetRing ? 1 : 0;
	missionVariables.$TelescopeModelRingColor			= JSON.stringify( ws.$ModelRingColor );
	missionVariables.$TelescopeVTarget_HUD_shift		= JSON.stringify( ws.$VTarget_HUD_shift );
	// option available on station
	missionVariables.$TelescopeRemoveInFlight			= ws.$RemoveInFlight ? 1 : 0;	// new
	missionVariables.$TelescopeSteering					= ws.$Steering;
	missionVariables.$TelescopeMaxTargets				= ws.$MaxTargets;
	missionVariables.$TelescopeAutoScan					= ws.$AutoScan ? 1 : 0;
	missionVariables.$TelescopeAutoScanMaxRange			= ws.$AutoScanMaxRange;
	missionVariables.$TelescopeFarStatus				= ws.$FarStatus ? 1 : 0;
	missionVariables.$TelescopeAutoLock					= ws.$AutoLock;
	missionVariables.$TelescopeGravLock					= ws.$GravLock;
	missionVariables.$TelescopeIdentLock				= ws.$IdentLock;
	missionVariables.$TelescopeIdentDelay				= ws.$IdentDelay;
	missionVariables.$TelescopeRedAlertDist				= ws.$RedAlertDist;
	// new options - UI_and_docs
	missionVariables.$TelescopeConsoleMsgDurn			= ws.$ConsoleMsgDurn;
	missionVariables.$TelescopeGravScanMsgFreq			= ws.$GravScanMsgFreq;
	missionVariables.$TelescopeIdentMessages			= ws.$IdentMessages ? 1 : 0;
	missionVariables.$TelescopeShowSummary				= ws.$ShowSummary ? 1 : 0;
	missionVariables.$TelescopeDebugMessages			= ws.$DebugMessages ? 1 : 0;
	// new options - experimental
	missionVariables.$TelescopeMFDFiltering				= ws.$MFDFiltering ? 1 : 0;
	missionVariables.$TelescopeMFDfilterStatic			= ws.$MFDPrimaryStatic;
	missionVariables.$TelescopeMFDfilterDynamic			= ws.$MFDPrimaryDynamic;
	missionVariables.$TelescopeSeparateMFDs				= ws.$SeparateMFDs ? 1 : 0;
	missionVariables.$TelescopeMFDAuxStatic				= ws.$MFDAuxStatic;
	missionVariables.$TelescopeMFDAuxDynamic			= ws.$MFDAuxDynamic;
	// Thargoids was never saved in original so it remains a one-off switch from a station dock
	// $TelescopeBetaLicenceTimestamp & $TelescopeBetaLicenceSystem are saved in _reloadFromStn(), maybe
	missionVariables.$TelescopeBetaLicence				= ws.$BetaLicence;
}
// station & witchspace events ////////////////////////////////////////////////////////////////////
this.shipWillLaunchFromStation = function shipWillLaunchFromStation( /*station*/ ) {
	var that = shipWillLaunchFromStation;
	var ws = (that.ws = that.ws || worldScripts.telescope);
	var ps = player && player.ship;
	if( ws.$Telescope_not_in_use ) return;							// no telescope, nothing to do
	ws._set_vShip_posn( ps.viewPositionForward, ws.$VTarget_HUD_shift );
	ws._AddShips();
}
this.shipExitedWitchspace =
this.shipLaunchedFromStation = function shipLaunchedFromStation( /*station*/ ) {
	var that = shipLaunchedFromStation;
	var ws = (that.ws = that.ws || worldScripts.telescope);
	if( ws.$DebugMessages && global.console && console.writeJSMemoryStats )
		console.writeJSMemoryStats();
	if( !ws.$GravScanCount ) ws.$GravScanCount = 0;					//start to count gravity scans (a missionVariables)
	if( !ws._init_player_vars() ) {									// equipment damaged, nothing to do
		ws._shutdown_Sightings();
		if( ws.$DebugMessages && player.ship.equipmentStatus( 'EQ_TELESCOPE' ) !== 'EQUIPMENT_UNAVAILABLE' )
			log(ws.name, 'shipLaunchedFromStation, _init_player_vars failed, quitting before making mapping or starting Timer!' );
		return;
	}
	ws._restart_after_shutdown();
	ws._create_Sightings();
	ws._StartTimer( 1 );											// delay to allow _create_Sightings to finish -curr'ly takes ~20 frames
}
this.shipWillDockWithStation = function shipWillDockWithStation( /*station*/ ) { // called by shipWillEnterWitchspace
	var that = shipWillDockWithStation;
	var ws = (that.ws = that.ws || worldScripts.telescope);
	ws._set_curr_Sighting( null, 'shipWillDockWithStation' );	// no parms clears it
	ws._StopTimer();
	ws._shutdown_Sightings();
	if( ws.$DebugMessages && global.console && console.writeJSMemoryStats )
		console.writeJSMemoryStats();
}
this.shipWillEnterWitchspace = function shipWillEnterWitchspace( /*cause, destination*/ ) {
	var that = shipWillEnterWitchspace;
	var ws = (that.ws = that.ws || worldScripts.telescope);
	var random = (that.random = that.random || Math.random);
	//	log(ws.name, 'in shipWillEnterWitchspace, arguments:'+arguments );
	var ps = player && player.ship;
	if( ws.$FixedGS === 1 && random() > 0.5 ) {
		ps.scriptedMisjump = true;									//meet Thargoids due to the cheap Grav.Sc. repair
		player.consoleMessage("Gravity Scanner caused misjump!");
	}
	if( ws.$FixedSD === 1 && random() > 0.2 ) {
		ps.setEquipmentStatus("EQ_SMALLDISH", "EQUIPMENT_DAMAGED");
		player.consoleMessage("Small Dish damaged during hyperjump!", 10);
	}
	if( ws.$FixedLD === 1 && random() > 0.2 ) {
		ps.setEquipmentStatus("EQ_LARGEDISH", "EQUIPMENT_DAMAGED");
		player.consoleMessage("Large Dish damaged during hyperjump!", 10);
	}
	ws.shipWillDockWithStation();
}
this.shipWillExitWitchspace = function shipWillExitWitchspace() {	//use this event due to shipExitedWitchspace is not working in v1.77
	var that = shipWillExitWitchspace;
	var ws = (that.ws = that.ws || worldScripts.telescope);
	if( ws.$Telescope_not_in_use ) return;							// no telescope, nothing to do
	ws._AddShips();													//do not call shipLaunchedFromStation() to avoid a bug
}
// ship events ////////////////////////////////////////////////////////////////////////////////////
this.shipBeingAttackedUnsuccessfully =
this.shipBeingAttacked = function shipBeingAttacked( whom ) {
	var that = shipBeingAttacked;
	var ws = (that.ws = that.ws || worldScripts.telescope);
	var mapping = (that.mapping = that.mapping || ws.$SightingsMap);
	if( ws.$Telescope_not_in_use ) return;							// no telescope, nothing to do
	if( !whom || !whom.isValid ) return;
	if( player.ship.equipmentStatus( 'EQ_TELESCOPE' ) !== 'EQUIPMENT_OK' ) return;
	var found = ws._Sighting_index( whom, 'shipBeingAttacked' );
	if( found < 0 ) {												// not registered
		found = ws._add_Sighting( whom, false, true, 'shipBeingAttacked' );
		if( found < 0 ) {
			log( ws.name, 'shipBeingAttacked, Yikes! _add_Sighting returned "'
				+ ws.$add_Sighting_errors[ found ] + '" trying to add ' + whom );
		}
	}
	if( found < 0 ) return;											// failed to add!
	var map = mapping[ found ];
	map.rank = 'bad';
}
this.shipDied =														// used instead of shipKilledOther, as catches more cases
this.shipScoopedOther = function shipScoopedOther( whom ) {			// NB: scooped objects become hostile, ie. they target ps (even splinters!)
	var that = shipScoopedOther;
	var ws = (that.ws = that.ws || worldScripts.telescope);
	if( ws.$Telescope_not_in_use ) return;							// no telescope, nothing to do
	ws._delete_Sighting( whom, 'shipScoopedOther' );				// will reset target lock, clear HUD, if it's player's target
}
this.shipSpawned = function shipSpawned( ship ) {					//detect missile launch immediately
	var that = shipSpawned;
	var ws = (that.ws = that.ws || worldScripts.telescope);
	if( ws.$Telescope_not_in_use ) return;							// no telescope, nothing to do
																	// - not testing scanClass, as conflicts w/ some oxp's
	if( ship.isVisualEffect ) return;
	if( ship.dataKey == 'telescopemarker' ) return;					// dataKey == 'telescope-shadow' caught by isVisualEffect
	var ps = player && player.ship;
	if( !ps || !ps.isValid || ps.alertCondition === 0 ) 			//player died or docked (alertCondition === 0)
		return;
	if( !ws.$Timer_auto_updates ) return;							//no timer means in witchspace
	if( ws._Sighting_index( ship ) >= 0 ) return;					// already in mapping! sometimes, check_if_new_targets can get there 1st
	let index = ws._add_Sighting( ship, false, false, 'shipSpawned' );
	if( index < -1 && index > -6 && ws.$DebugMessages ) {
		let reason = ws.$add_Sighting_errors[ index ];
		log(ws.name, 'shipSpawned, isVisible = ' + ship.isVisible
			+ ', distance = ' + ship.position.distanceTo( ps ).toFixed() + ', mass = ' + ship.mass
			+ ', w/ ship = ' + ship + '\n\t Yikes! _add_Sighting returned: ' + reason );
	}
/*
///testing if isVisible bug still present; 1.92 (Jan/22) set record: wreckage @ 31,733,066 m!
/// - problem is that .isVisible is always true when an ent is spawned and it may not be
///   properly set before we try to add it to the list of sightings
/// - new solution is to ignore anything spawned w/i last ??? second; here we try to get a feel
///   for what a good interval should be for use in grow_new_list, _add_Sighting: SPAWN_DELAY
if( ws.$DebugMessages ) {
	let dist = ps.position.distanceTo( ship ), spawn = ship.spawnTime;
	let now = clock.absoluteSeconds;
	if( dist > 5e6 && ship.isVisible && ship.scanClass !== 'CLASS_NO_DRAW'
			&& ship.status !== 'STATUS_LAUNCHING' ) {
		log(ws.name, 'shipSpawned, ship "' + ship.entityPersonality + '" at ' + dist.toFixed() + ' has isVisible true! ####, spawnTime: '
			+ spawn.toFixed(4) + ': , diff from now: ' + (spawn > 0 ? (now - spawn).toFixed(4) : 'n/a') + ', ship:' + ship );
		let timr = new Timer( ws, ws._isVisMonitor, 0.05, 0.25 );
		timr.$spawnedShip = ship;
		ws.$isVisTimers.push( timr );
	}
}
 */
}
///
this.$isVisTimers = [];
this._isVisMonitor = function _isVisMonitor() {
	function suicide() {
		if( timeRef.isRunning ) {
			timeRef.stop();
		}
		let idx = ws.$isVisTimers.indexOf( timeRef );
		if( idx < 0 ) {
			log('_isVisMonitor, timeRef ('+timeRef+') not found in $isVisTimers: ' + ws.$isVisTimers);
		} else {
			ws.$isVisTimers.splice( idx, 1 );	// modify array be removing 1 item at idx
		}
		timeRef = that.timeRef = null;
	}
	var that = _isVisMonitor;
	var ws = (that.ws = that.ws || worldScripts.telescope);
	var timeRef = (that.timeRef = that.timeRef || ws.$isVisTimers[ws.$isVisTimers.length - 1]);
	var ship = timeRef.$spawnedShip;
	if( !ship || ship.inValid ) {
		log('_isVisMonitor, failed to capture ship: ' + ship );
		suicide();
		return;
	}
	let now = clock.absoluteSeconds;
	if( ship.isVisible ) {
		log('_isVisMonitor, timeRef ('+timeRef+'), ship "' + ship.entityPersonality + '" still isVisible after ' + (now - ship.spawnTime).toFixed(4) + ': ' + ship );
	} else {
		log('_isVisMonitor, timeRef ('+timeRef+'), ship "' + ship.entityPersonality + '" NO LONGER isVisible after ' + (now - ship.spawnTime).toFixed(4) + ': ' + ship );
		suicide();
	}
}
///
this.$add_Sighting_errors = { '-1': '!mappingReady', '-2': 'maplen >= MaxTargets', '-3': '!ent.isValid',
							  '-4': 'player died or docked', '-5': 'already in mapping', '-6': '!_has_good_status',
							  '-7': '!notable_ent', '-8': 'rank === ukn', '-9': 'wreckage', '-10': 'younger than SPAWN_DELAY' };
this.shipTargetAcquired = function shipTargetAcquired( target ) {	//if locked target by hand then set as the actual item in the list
	var that = shipTargetAcquired;
	var ws = (that.ws = that.ws || worldScripts.telescope);
	var mapping = (that.mapping = that.mapping || ws.$SightingsMap);
	var curr_S = (that.curr_S = that.curr_S || ws.$curr_Sighting);
	var IDENT_READY = (that.IDENT_READY = that.IDENT_READY || ws.$IDENT_READY);
	var IDENT_STEER_DELAY = (that.IDENT_STEER_DELAY = that.IDENT_STEER_DELAY || ws.$IDENT_STEER_DELAY);
	if( ws.$Telescope_not_in_use ) return;							// no telescope, nothing to do
	if( !target || !target.isValid || ws.$TelescopeTargetSet )		//no target or we have just set ps.target
		return;
if( ws.$DebugMessages && target === curr_S.marker )
	log(ws.name, 'shipTargetAcquired, re-acquired same target, should we bail out?');
	if( target === curr_S.marker ) {								// marker for target outside scannerRange
		target = curr_S.ent || null;
	}
	var isNewTarget = target !== curr_S.ent;
	var index = ws._Sighting_index( target, 'shipTargetAcquired' ); // already scanned?
	if( index < 0 ) {												// try adding it (should only fail if $MaxTargets reached)
		index = ws._add_Sighting( target, false, false, 'shipTargetAcquired' );
		if( index === -2 ) {
			player.consoleMessage( (mapping.length >= ws.$MaxTargets
					? 'Telescope memory is full.' : 'Telescope unable to lock target.'), ws.$ConsoleMsgDurn );
			ws._set_curr_Sighting( null, 'shipTargetAcquired' );		// no parms resets
			if( ws.$DebugMessages )
				log(ws.name, 'shipTargetAcquired, maplen (' + mapping.length + ') >= MaxTargets (' + ws.$MaxTargets
					+ '), curr_Sighting being reset! ' +  target );
			return;
		}
	}
if( ws.$DebugMessages ) log(ws.name, 'shipTargetAcquired, new target (' + target.entityPersonality + '), index: ' + index
+ ', IdentKeyPress: ' + ws.$IdentKeyPress + ': ' + target );
	if( isNewTarget ) {
		let identKeyPress = ws.$IdentKeyPress;
		if( identKeyPress > IDENT_READY && identKeyPress < IDENT_STEER_DELAY ) {// it was 'locked', not in delay
			if( ws.$IdentMessages )
				player.consoleMessage( 'Telescope lock released', ws.$ConsoleMsgDurn );
			ws.$IdentKeyPress = IDENT_READY;
// if( ws.$DebugMessages ) log('shipTargetAcquired, identKeyPress = IDENT_READY');
		}
	}
	ws._manage_marker( mapping[ index ], false, 'shipTargetAcquired' );
}
this.shipTargetCloaked = function shipTargetCloaked() {
	var that = shipTargetCloaked;
	var ws = (that.ws = that.ws || worldScripts.telescope);
	var curr_S = (that.curr_S = that.curr_S || ws.$curr_Sighting);
	var IDENT_READY = (that.IDENT_READY = that.IDENT_READY || ws.$IDENT_READY);
	if( ws.$Telescope_not_in_use ) return;							// no telescope, nothing to do
	var target = curr_S.ent || null;
	if( target && target.isCloaked ) {
		ws._delete_Sighting( target, 'shipTargetCloaked' );
		if( ws.$IdentKeyPress > IDENT_READY ) {						// it was 'locked'
			if( ws.$IdentMessages )
				player.consoleMessage( 'Telescope lock released', ws.$ConsoleMsgDurn );
			ws.$IdentKeyPress = IDENT_READY;
// if( ws.$DebugMessages ) log('shipTargetCloaked, identKeyPress = IDENT_READY');
		}
	}
}
this.shipTargetLost = function shipTargetLost( target ) {			// used to re-purpose ident key fn
	var that = shipTargetLost;
	var ws = (that.ws = that.ws || worldScripts.telescope);
	var curr_S = (that.curr_S = that.curr_S || ws.$curr_Sighting);
	var _has_bad_status = (that._has_bad_status = that._has_bad_status || ws._has_bad_status);
	if( ws.$Telescope_not_in_use ) { 								// no telescope, nothing to do
		return;
	}
	var ps = player && player.ship;
	if( !ps || !ps.isValid || ps.alertCondition === 0 ) { 			//player died or docked
		return;
	}
	if( ws.$TelescopeTargetSet || ws.$IdentLock === 0 ) {			//set by script OR disabled by user
		return;
	}
// if( ws.$DebugMessages ) log('shipTargetLost, ws.$IdentKeyPress: ' + ws.$IdentKeyPress );
	if( target === curr_S.marker ) {								// telescopemarker was last target
		target = curr_S.ent;
	} else if( !target || target !== curr_S.ent ) {
		target = curr_S.ent || null;
	}
	var target_dead = !target										//target destroyed, jumped, docked else lost by ident key press
						|| _has_bad_status( target )				// _has_bad_status now checks .isValid, isWormhole
						|| target.energy <= 0;						// !isValid no longer enough, as not always set before this event
/*
	if( ws.$DebugMessages ) log(ws.name, 'shipTargetLost, ship target was '
		+ (target === curr_S.marker ? ' (' + curr_S.marker_type + ') ':'')
		+ (target ? ' @' + Math.floor(target.position.distanceTo(ps)) + ', ' + target : 'null' )
		+ '\n\t marker was ' + (curr_S.marker ? '@' + Math.floor(curr_S.marker.position.distanceTo(ps))
										+ (curr_S.marker_type === 'marker' ? ', a marker' : ', a shadow') : 'empty' )
		+'\n\t ent was '+ (target ? '@' + Math.floor(target.position.distanceTo(ps)) + ', ' + target : 'null')
		+ '\n\t IdentKeyPress = ' + ws.$IdentKeyPress
		+ ', target.isValid = ' + (target ? target.isValid : '<target is null>')
		+ ', target_dead = ' + target_dead + ', ps.target = ' + ps.target
		+ (curr_S.lightball ? '\nlightball (' + curr_S.map.ve_colour + ') @'
								+ Math.floor(curr_S.lightball.position.distanceTo(ps)) : '')
		);
	// if( ws.$DebugMessages && worldScripts.telescope_debug ) worldScripts.telescope_debug._curr_S_report();
 */
	if( ws.$IdentKeyPress < ws.$IDENT_STEER_DELAY )					// IdentDelay timer not running
		// when target_dead is true, _mostCentered won't allow an 'ident' lock
		ws._mostCentered( "ident", !target_dead );					//lock-steer?-unlock target
}
this.weaponsSystemsToggled = function weaponsSystemsToggled( /* state */ ) {
	var that = weaponsSystemsToggled;
	var ws = (that.ws = that.ws || worldScripts.telescope);
	if( player && player.ship ) {									// reset state (esp. for Navigation mode)
		ws.$IdentKeyPress = ws.$IDENT_READY;
// if( ws.$DebugMessages ) log('weaponsSystemsToggled, identKeyPress = IDENT_READY');
	}
}
// equipment events ///////////////////////////////////////////////////////////////////////////////
this.$telescopeEquipment = [
		'EQ_TELESCOPE', 'EQ_TELESCOPEEXT',
		'EQ_GRAVSCANNER', 'EQ_GRAVSCANNER2',
		'EQ_SMALLDISH', 'EQ_LARGEDISH' ];
this.equipmentDestroyed =
this.equipmentDamaged = function equipmentDamaged( equipment ) {
	var that = equipmentDamaged;
	var ws = (that.ws = that.ws || worldScripts.telescope);
	var telEq = (that.telEq = that.telEq || ws.$telescopeEquipment);
	if( telEq.indexOf( equipment ) === -1 ) {
		return;
	}
	if( equipment === 'EQ_TELESCOPE' ) {
		ws._StopTimer();
		ws._shutdown_Sightings();
		return;
	}
	if( equipment === 'EQ_TELESCOPEEXT' ) {
		ws.$extenderActive = false;									// only used in station options
	}
	ws._init_player_vars();											// update status of equipment vars
	ws._create_Sightings();											//remove lost targets, autoscan will scan again
}
this.equipmentRepaired = function equipmentRepaired( equipment ) {
	var that = equipmentRepaired;
	var ws = (that.ws = that.ws || worldScripts.telescope);
	var telEq = (that.telEq = that.telEq || ws.$telescopeEquipment);
	if( telEq.indexOf( equipment ) === -1 ) {						// not a relevant repair - thanks Milo
		return;
	}
	var ps = player && player.ship;
	if( equipment === 'EQ_TELESCOPE' ) {
		ws.$FixedTel = 0;
		if( ws._init_player_vars() ) {
			ws._restart_after_shutdown();
			if( ps && ps.isInSpace ) {								// emulate launch if fixed in space
				ws._StartTimer( 1 );								// delay to allow _create_Sightings to finish -curr'ly takes 20+ frames
			}
		} else {
			ws._StopTimer();
			ws._shutdown_Sightings();
			return;
		}
	} else if( equipment === 'EQ_TELESCOPEEXT' ) {
		ws.$extenderActive = true;
	} else if( equipment === 'EQ_GRAVSCANNER' ) {
		ws.$FixedGS = 0;
	} else if( equipment === 'EQ_GRAVSCANNER2' ) {
		ws.$FixedGS = 0;
	} else if( equipment === 'EQ_SMALLDISH' ) {
		ws.$FixedSD = 0;
	} else if( equipment === 'EQ_LARGEDISH' ) {
		ws.$FixedLD = 0;
	}
	ws._init_player_vars();											// update status of equipment vars
	ws._create_Sightings();											//remove lost targets, autoscan will scan again
}
this.playerBoughtEquipment = function playerBoughtEquipment( equipment ) {
	var that = playerBoughtEquipment;
	var ws = (that.ws = that.ws || worldScripts.telescope);
	var telEq = (that.telEq = that.telEq || ws.$telescopeEquipment);
	var random = (that.random = that.random || Math.random);
	var round = (that.round = that.round || Math.round);
	var floor = (that.floor = that.floor || Math.floor);
	var restock = (that.restock = that.restock || {});				// dictionary of stations' restocking fee
	var ps = player && player.ship;
	var actualEq = equipment,
		endsWith = '',
		parsed = equipment.split( '_' );
	if( parsed.length === 3 ) {
		actualEq = parsed[ 0 ] + '_' + parsed[ 1 ];
		endsWith = parsed[ 2 ];
	}
	if( telEq.indexOf( actualEq ) === -1 ) {
		return;
	}
	if( endsWith === '' ) {											// bought actual equipment
		if( equipment === 'EQ_TELESCOPE' ) {
			ws.$FixedTel = 0;
			ws.$Telescope_not_in_use = false;
			ws._registerHUDSelector();
			ps.setMultiFunctionText( ws.$PrimaryMFD_name, '' );		// make core aware now for other oxp's that play with MFDs
			ps.setMultiFunctionText( ws.$AuxilaryMFD_name, '' );
		} else if( equipment === 'EQ_TELESCOPEEXT' ) {
			ws.$extenderActive = true;
		} else if( equipment === 'EQ_GRAVSCANNER'
				|| equipment === 'EQ_GRAVSCANNER2' ) {
			ws.$FixedGS = 0;
		} else if( equipment === 'EQ_SMALLDISH' ) {
			ws.$FixedSD = 0;
		} else if( equipment === 'EQ_LARGEDISH' ) {
			ws.$FixedLD = 0;
		}
		return;
	}
	if( endsWith === 'REFUND' ) {									// sold actual equipment
		ps.removeEquipment( equipment );							//remove the 'bought' refund eq
		if( ps.equipmentStatus( actualEq ) === 'EQUIPMENT_OK' ) {
			if( actualEq === 'EQ_TELESCOPEEXT' ) {
				ws.$extenderActive = false;
			}
			ps.removeEquipment( actualEq );							// the refund voucher
			clock.addSeconds( ( actualEq[ 3 ] === 'G' ? 1800 : 4500 ) );// dish work takes longer
			let infoForKey = EquipmentInfo.infoForKey( actualEq );
			let rate, refund = infoForKey.price / 10;				// .plist price is in tenths of credits
			let station = player.dockedStation;
			if( restock.hasOwnProperty( station ) )					// fee cached to be consistent
				rate = restock[ station ];
			else													// random fee 1-5%
				rate = that.restock[ station ] =  floor( (random() * (0.051 - 0.01) + 0.01) * 100 );
				// rate = that.restock[ station ] = random() < 0.5 ? 0.05 : 0.1;
			let fee = round( refund * rate );
			refund -= fee;
			player.credits += refund;
			player.consoleMessage( 'Refunded ' + refund + ' credits (less '
									+ (rate * 100) + '% commission) for '
									+ infoForKey.name, ws.$ConsoleMsgDurn * 2 );
		}
	} else if( endsWith === 'REPAIR' || endsWith === 'FULLREPAIR') {
		// remainder deals with repairs (this function is called after equipmentRepaired)
		ps.setEquipmentStatus( actualEq, 'EQUIPMENT_OK' );
		ps.removeEquipment( equipment );							// the repair voucher
		clock.addSeconds( ( actualEq[ 3 ] === 'G' ? 3600 : 9000 ) );// dish work takes longer
		if( actualEq === 'EQ_TELESCOPE' ) {
			ws.$FixedTel = endsWith === 'REPAIR' ? 1 : 0;			//with drawback (lightballs only)
		} else if( equipment === 'EQ_TELESCOPEEXT' ) {
			ws.$extenderActive = true;								// has no cheap repair option
		} else if( actualEq === 'EQ_GRAVSCANNER'
			  || actualEq === 'EQ_GRAVSCANNER2' ) {
			ws.$FixedGS = endsWith === 'REPAIR' ? 1 : 0;			//with drawback (misjump)
		} else if( actualEq === 'EQ_SMALLDISH' ) {
			ws.$FixedSD = endsWith === 'REPAIR' ? 1 : 0;			//with drawback (break during jump)
		} else if( actualEq === 'EQ_LARGEDISH' ) {
			ws.$FixedLD = endsWith === 'REPAIR' ? 1 : 0;			//with drawback (break during jump)
		}
	}
}
///////////////////////////////////////////////////////////////////////////////////////////////////
// station options ////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////
this._startStationOptions = function _startStationOptions() {
	var ws = worldScripts.telescope;
	var so = worldScripts.station_options;
	try {
		if( so ) {													// pass callback functions and initialize station options
			let missionKeys;
			if( ws.$BetaLicence === 1 ) {							// 'experimental' page is active, set keys
				missionKeys = {
					'telescope_BetaLicenceTimestamp': ws.$BetaLicenceTimestamp,
					'telescope_BetaLicenceSystem': ws.$BetaLicenceSystem,
				};
			} else {												// replace 'experimental' page with licence agreement
				missionKeys = {
					'telescope_optionPages': "[telescope_optionPages_licence]",
					'telescope_optionTabStops': "[telescope_optionTabStops_licence]",
					'telescope_licence_summary': '[telescope_licence_undeclared]',
					'telescope_licence_short': '[telescope_licence_asking]',
					// set temporal strings to present tense in case player accepts
					'telescope_licenceAcceptance': expandDescription( '[telescope_experimental_accepts]' ),
					'telescope_licenceRegistered': expandDescription( '[telescope_experimental_registers]' ),
					// - not sure why but these won't expand otherwise (but telescope_licence_summary & telescope_licence_short do work???)
					// 'telescope_licenceAcceptance': '[telescope_experimental_accepts]',
					// 'telescope_licenceRegistered': '[telescope_experimental_registers]',
				};
			}
			// _initStationOptions( hostOxp, keyPrefix, optionsAllowedCallback, callPWSG, notifyCallback, suppressSummary, missionKeys	 )
			let okay = so.$O_initStationOptions( ws, 'telescope_', ws._stnOptionsAllowed, true, ws._reloadFromStn, false, missionKeys );
			if( !okay )
				return;
			if( so.$O_getReminder4Oxp ) { 							// absent from version 1.0
				let rmdr = so.$O_getReminder4Oxp( 'telescope_' );
				if( rmdr ) {
					ws.$ShowSummary = rmdr.reportSummary;
				} else {
					log( ws.name, '_startStationOptions, station_options _getReminder4Oxp returned: "' + rmdr + '"' );
				}
			}
		} else {
			log( ws.name, '_startStationOptions, station_options oxp is missing!' );
		}
	} catch( err ) {
		log( ws.name, ws._reportError( err, _startStationOptions ) );
		if( ws.$DebugMessages )
			throw err;
	}
}
this._BetaLicenceAnswered = function _BetaLicenceAnswered( response ) {		// _execute fn for station_options
	var ws = worldScripts.telescope;
	var so = worldScripts.station_options;
	// once licence is accepted, expiramental page is added.  The licence page remains
	// available until next station or next ship/equipment change or the player tries
	// to alter the acceptance.  These all result in a call to _setLicenceMissionVar()
	// which removes the licence page.
	var missionKeys;
	if( response === 1 ) {
		// preserve licence agreement info;  .length > 0 && !missionVariables => first time
		// - all subsequent values will be skipped, preserving the original
		// NB: here we're ASSUMING timestamp & system are set as a pair, ie. at the same time
		if( ws.$BetaLicenceTimestamp.length > 0 && !missionVariables.$TelescopeBetaLicenceTimestamp ) {
			missionVariables.$TelescopeBetaLicenceTimestamp = ws.$BetaLicenceTimestamp;
			missionVariables.$TelescopeBetaLicenceSystem = ws.$BetaLicenceSystem;
			// insert experimental page, update text
			missionKeys = {
				'telescope_BetaLicenceTimestamp': ws.$BetaLicenceTimestamp,
				'telescope_BetaLicenceSystem': ws.$BetaLicenceSystem,
				'telescope_optionPages': "[telescope_optionPages_licence_accepted]",
				'telescope_optionTabStops': "[telescope_optionTabStops_licence_accepted]",
				'telescope_licence_summary': '[telescope_licence_accept]',
				'telescope_licence_short': '[telescope_licence_answered]',
			};
		} else if( ws.$BetaLicenceTimestamp.length > 0 ) { // set a 2nd time??? reset missionKeys
			// player tries to undo licence acceptance, which we do not allow
			ws._setLicenceMissionVar()
			return;
		}
	} else {
		// rejection is only allowed when !$BetaLicence; once accepted, it cannot be changed
		// - see telescope_BetaLicence_assign in missiontext.plist
		missionKeys = {
			'telescope_licence_summary': "[telescope_licence_reject]",
		};
	}
	so.$O_updateMissionKeys( 'telescope_', missionKeys );
}
this._stnOptionsAllowed = function _stnOptionsAllowed() {			// callback fn for station_options
	var ws = worldScripts.telescope;
	var ps = player && player.ship;
	ws._setLicenceMissionVar()
	return ps && ps.equipmentStatus( 'EQ_TELESCOPE' ) === 'EQUIPMENT_OK';
}
this._setLicenceMissionVar = function _setLicenceMissionVar() {		// if accepted, change tense for environmental summary
	var ws = worldScripts.telescope;
	var so = worldScripts.station_options;
	// called from _BetaLicenceAnswered, _stnOptionsAllowed & _reloadFromStn, text will
	// (text will remain unchanged until this function is called)
	if( ws.$BetaLicence === 1										// licence accepted, ensure tense correct
			&& missionVariables.$TelescopeBetaLicence === null ) {
		// player entering station_options having accepted licence on previous visit
		so.$O_updateMissionKeys( 'telescope_',
			{
				'telescope_licenceAcceptance': expandDescription( '[telescope_experimental_has_accepted]' ),
				'telescope_licenceRegistered': expandDescription( '[telescope_experimental_has_registered]' ),
				// - not sure why but these won't expand otherwise (but telescope_licence_summary & telescope_licence_short do work???)
				// 'telescope_licenceAcceptance': '[telescope_experimental_has_accepted]',
				// 'telescope_licenceRegistered': '[telescope_experimental_has_registered]',
				'telescope_optionPages': '[telescope_optionPages_experimental]',
				'telescope_optionTabStops': '[telescope_optionTabStops_experimental]',
			} );
		missionVariables.$TelescopeBetaLicence = ws.$BetaLicence;
	}
}
this._reloadFromStn = function _reloadFromStn( names, pages ) {		// callback fn for station_options
	var ws = worldScripts.telescope;
	ws._setLicenceMissionVar()
	if( pages && pages.length > 0 ) {
		ws._reload_config();
		if( names.indexOf( 'DebugMessages' ) >= 0 ) {
			ws._debug_Sightings_closure();
		}
	}
	if( ws.$DebugMessages ) {
		log(ws.name, '_reloadFromStn, pages = ' + pages + '\n\t names = ' + names );
		ws._report_config();
	}
/* ShowSummary is now an option
	var so = worldScripts.station_options;
	// retrieve summary reporting status to support conditional option ShowSummary
	if( so && so.$O_getReminder4Oxp ) {
		var rmdr = so.$O_getReminder4Oxp( 'telescope_' );
		if( rmdr ) {
			ws.$ShowSummary = rmdr.reportSummary;
log('_reloadFromStn, saving  ShowSummary: ' + ws.$ShowSummary );
		}
	}
 */
}
///////////////////////////////////////////////////////////////////////////////////////////////////
// oxp support ////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////
this.$Telescope_List = function $Telescope_List( step ) {			// not used here, ?for other oxp's (was used in telescopeeq.js)
	var that = $Telescope_List;
	var ws = (that.ws = that.ws || worldScripts.telescope);
	if( step )
		ws._chg_curr_Sighting( step );								// user steps fwd/back through list of Sightings
	else
		ws._auto_updates( true );									// user performs 'rescan'
}
this.$Telescope_Scan = function _Scan() {							// not used here, ?for other oxp's
	var that = _Scan;
	var ws = (that.ws = that.ws || worldScripts.telescope);
if( ws.$DebugMessages ) log(ws.name, 'Telescope_Scan, FORCED new scan '	 );
	ws._auto_updates( true );										// true forces a completely new mapping to be built
}
this.$Telescope_Show = function _Show() {							// not used here, ?for other oxp's
	var that = _Show;
	var ws = (that.ws = that.ws || worldScripts.telescope);
	ws.$Telescope_Show2( true );
}
this.$Telescope_Show2 = function _Show2( showname ) {				 // not used here, for other oxp's: EscortDeck
	var that = _Show2;
	var ws = (that.ws = that.ws || worldScripts.telescope);
	var mapping = (that.mapping = that.mapping || ws.$SightingsMap);
	var curr_S = (that.curr_S = that.curr_S || ws.$curr_Sighting);
	var map = null,
		index = ws.$TelescopeListi;
	if( index === 0 ) {
		index = curr_S.index;
	} else {
		index -= 1;				// TelescopeListi is always index + 1
	}
	map = index >= 0 && index < mapping.length ? mapping[ index ] : null;
	ws._manage_marker( map, showname || false, '_Show' );
}
// HUDSelector ////////////////////////////////////////////////////////////////////////////////////
this.$HUDStartUpComplete = function() {
    if( !this.$HUDSelectorDefaultMFDs || this.$HUDSelectorDefaultMFDs.length === 0 ) {
        //set default MFDs first time in order of $HUDSelectorMFDs array
        this.$HUDSelectorDefaultMFDs = [];
        for(var i = 0; i < this.$HUDSelectorMFDs.length; i++) {
            if(this.$HUDSelectorMFDs[i] && this.$HUDSelectorMFDs[i][0]) {
                // var w = this.$HUDSelectorMFDs[i][0]; //worldScripts name
                // if( worldScripts[w] ) {
                    // this.$HUDSelectorDefaultMFDs[i] = w;
                // }
// $HUDSelectorSetMFDs assumes $HUDSelectorDefaultMFDs entries will be
//  [ worldScripts name, mfd name (maybe) ]
                let [w, m] = this.$HUDSelectorMFDs[i];
                if( worldScripts[w] ) {
                    this.$HUDSelectorDefaultMFDs[i] = m || w;
                }
            }
        }
    }
    log(this.name, "HUDs: "+this.$HUDSelectorHUDs);//debug
    if (this.$debug)
        for (var i=0; i<this.$HUDSelectorHUDs.length; i++)
            log(this.name, i+": "+this.$HUDSelectorHUDs[i]);
    this.$HUDSelectorRestoreHUD();
    this.$setInterface();
}
this.$HUDSelectorSetMFDs = function(h) {
	var dLen = h.$HUDSelectorDefaultMFDs.length,
		mLen = player.ship.multiFunctionDisplayList.length;
// must not exceed mLen else could fill empty slots when we wrap
//   for(var i = 0; i < h.$HUDSelectorDefaultMFDs.length; i++) {
    for(var i = 0; i < dLen && i < mLen; i++) {
//        log(h.name, i+". MFD: "+h.$HUDSelectorDefaultMFDs[i]);//debug
        if(h.$HUDSelectorDefaultMFDs[i]) {
            var w = h.$HUDSelectorDefaultMFDs[i]; //worldScripts or MFD name
            var mfd = -1;
            if( w && w.length > 0 && w != "undefined" ) {
                for(var j = 0; j < h.$HUDSelectorMFDs.length; j++) {
                    if( h.$HUDSelectorMFDs[j][0] == w
                        || h.$HUDSelectorMFDs[j][1] == w ) mfd = j;
                }
            }
            var m = null;
            if( mfd > -1) m = h.$HUDSelectorMFDs[mfd][1]; //MFD name
            if( !m ) m = w; //mfd name is equal with worldScripts name
            if( m && worldScripts[w] && w != "undefined" )
                player.ship.setMultiFunctionDisplay(i, m);
            else if( w && w.length > 0 ) player.ship.setMultiFunctionDisplay(i, w);
            else player.ship.setMultiFunctionDisplay(i, "");
//            log(h.name, i+". MFD: "+w+" "+m+" "+worldScripts[w]);//debug
        }
    }
}
/*
this.$HUDstartUpComplete = function() {
	var hud = worldScripts.hudselector,
		defaults = hud.$HUDSelectorDefaultMFDs,
		mfdDB = hud.$HUDSelectorMFDs;
	if( !defaults  ) {// should never happen
		hud.$HUDSelectorDefaultMFDs = defaults = [];
		log( this.name, '$HUDstartUpComplete, WARNING: hud.$HUDSelectorDefaultMFDs array got deleted!' )
	}
	if( defaults.length === 0 ) {
		//set default MFDs first time in order of $HUDSelectorMFDs array
		for( let idx = 0, len = mfdDB.length; idx < len; idx++ ) {
			let [wsName, mfdName] = mfdDB[ idx ];	// an array of [worldScripts.name, mfdName], mfdName may be absent
			if( wsName && worldScripts.hasOwnProperty( wsName ) ) {
				defaults[ idx ] = mfdName ? mfdName : wsName;
			}
		}
	}
	log( hud.name, "HUDs: "+hud.$HUDSelectorHUDs );//debug
	if( hud.$debug ) {
		for( let idx=0, len = hud.$HUDSelectorHUDs.length; idx < len; idx++ )
			log( hud.name, idx + ": " + hud.$HUDSelectorHUDs[ idx ] );
	}
	hud.$HUDSelectorRestoreHUD();
	hud.$setInterface();
log('HUDstartUpComplete,    exit, MFDs: ' + hud.$HUDSelectorMFDs );
log('HUDstartUpComplete,   DefaultMFDs: ' + hud.$HUDSelectorDefaultMFDs );
log('HUDstartUpComplete, MFDisplayList: ' + player.ship.multiFunctionDisplayList );
}
// : ' +  + '
this.$HUDSelectorSetMFDs = function( hud ) {
	if( !hud ) return;
	var mfdDB = hud.$HUDSelectorMFDs;								// nested array of MFDs registered with hudselector
	if( !mfdDB ) return;
	var ps = player && player.ship,
		defaults = hud.$HUDSelectorDefaultMFDs;
	var slot = 0, numSlots = ps.multiFunctionDisplays,
		dLen = defaults.length, mLen = mfdDB.length;
	for( let dx = 0; dx < dLen; dx++ ) { //  && slot < numSlots
		let wsName, mfdName,
			defName = defaults[ dx ]; 	//worldScripts name or MFD name
		if( defName && defName != "undefined" ) {
			wsName = mfdName = null;
			for( let mx = 0; mx < mLen; mx++ ) {
				[wsName, mfdName] = mfdDB[ mx ];
				if( mfdName == defName || wsName == defName ) {	// check MFD name first
					break;
				}
			}
			let key = mfdName || wsName;
			if( key && defName != "undefined" && worldScripts.hasOwnProperty( defName ) ) {
				ps.setMultiFunctionDisplay( slot++, key );
			} else if( defName && defName.length > 0 ) {
				ps.setMultiFunctionDisplay( slot++, defName );
			} else {
				ps.setMultiFunctionDisplay( slot, "" );
			}
		 log( hud.name, dx + ". MFD: " + defName + " " + key + " " + worldScripts[ defName ] );//debug
		}
	}
log('HUDSelectorSetMFDs,    exit, MFDs: ' + hud.$HUDSelectorMFDs );
log('HUDSelectorSetMFDs,   DefaultMFDs: ' + hud.$HUDSelectorDefaultMFDs );
log('HUDSelectorSetMFDs, MFDisplayList: ' + player.ship.multiFunctionDisplayList );
}
*/
// : ' +  + '
this._registerHUDSelector = function _registerHUDSelector() {		// called in startUp
	var ws = worldScripts.telescope;
	var hud = worldScripts.hudselector;
	if( !hud ) return;
	var mfdDB = hud.$HUDSelectorMFDs;								// nested array of MFDs registered with hudselector
	var telName = worldScripts.telescope.name;
	for( let idx = 0, len = mfdDB.length; idx < len; idx++ ) {
		// there is no $HUDSelectorRemoveMFD, so ...
		let [hudscript, mfdName] = mfdDB[ idx ];
		if( hudscript === telName && !mfdName ) {					// set for telescope <= 1.15
			for( let mvi = idx; mvi < len - 1; mvi++ ) {			// remove from mfdDB
				mfdDB[ mvi ] = mfdDB[ mvi + 1 ];
			}
			mfdDB.length = --len;
		}
	}
	hud.$HUDSelectorAddMFD( telName, ws.$PrimaryMFD_name )
	hud.$HUDSelectorAddMFD( telName, ws.$AuxilaryMFD_name )
}
/*
this._registerHUDSelector = function _registerHUDSelector() {		// called in startUp
	var ws = worldScripts.telescope;
	var hud = worldScripts.hudselector;
	if( !hud ) return;
	var mfdDB = hud.$HUDSelectorMFDs;								// nested array of MFDs registered with hudselector
	if( !mfdDB ) return;
	var telName = worldScripts.telescope.name;
	var isOld = false, ver = hud.version.split( '.' );
	if( ver.length > 1 && parseInt( ver[ 1 ], 10 ) < 18 )
		isOld = true;
	if( isOld ) {
		let changed = false, primary = false, auxilary = false;
		for( let idx = 0, len = mfdDB.length; idx < len; idx++ ) {
			let [hudscript, mfdName] = mfdDB[ idx ];
			if( hudscript === telName ) {
				if( !mfdName || mfdName === telName ) {					// old telescope in saved game
					mfdDB[ idx ] = [ telName, ws.$PrimaryMFD_name ];	// replace (keep initial position)
					primary = changed = true;
				} else if( mfdName === 'telescopeAux' ) {  				// old telescope in saved game
					mfdDB[ idx ] = [ telName, ws.$AuxilaryMFD_name ];	// replace (keep initial position)
					auxilary = changed = true;
				} else {												// detect new MFDs
					if( mfdName === ws.$PrimaryMFD_name )
						primary = true;
					else if( mfdName === ws.$AuxilaryMFD_name )
						auxilary = true;
				}
			}
		}
		if( !primary )
			mfdDB.push( [ telName, ws.$PrimaryMFD_name ] );
		if( !auxilary )
			mfdDB.push( [ telName, ws.$AuxilaryMFD_name ] );
		if( changed || !primary || !auxilary ) {
			hud.$HUDSelectorSetMFDs( hud );
		}
	} else {
/// once working, add some old version names & see what needs doing
		for( let idx = 0, len = mfdDB.length; idx < len; idx++ ) {
			let [hudscript, mfdName] = mfdDB[ idx ];
			if( hudscript === telName
					&& ( !mfdName || mfdName === hudscript 			// set for telescope <= 1.15
						|| mfdName === 'telescopeAux' ) ) {			// old telescope in saved game
				for( let mvi = idx; mvi < len - 1; mvi++ ) {		// remove from mfdDB
					mfdDB[ mvi ] = mfdDB[ mvi + 1 ];
				}
				mfdDB.length = --len;
			}
		}
		hud.$HUDSelectorAddMFD( telName, ws.$PrimaryMFD_name )
		hud.$HUDSelectorAddMFD( telName, ws.$AuxilaryMFD_name )
	}
log('_registerHUDSelector,    exit, MFDs: ' + hud.$HUDSelectorMFDs );
log('_registerHUDSelector,   DefaultMFDs: ' + hud.$HUDSelectorDefaultMFDs );
log('_registerHUDSelector, MFDisplayList: ' + player.ship.multiFunctionDisplayList );
}
*/
///////////////////////////////////////////////////////////////////////////////////////////////////
// Telescope methods //////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////
// inialization ///////////////////////////////////////////////////////////////////////////////////
this._init_Sightings_closure = function _init_Sightings_closure() {	// initialize closures & expose functions
	var ws = worldScripts.telescope;
	var sc = ws._Sightings_closure();
	ws.$Sighting_closure = sc;
	ws._initOxpVars = sc._initOxpVars;
	ws._init_player_vars = sc._init_player_vars;
	ws._reload_config = sc._reload_config;
	ws._adjustMLFlags = sc._adjustMLFlags;
	ws._getShowState = sc._getShowState;
	ws._getShowStateText = sc._getShowStateText;
	ws._currMLFlags = sc._currMLFlags;
	ws._shutdown_Sightings = sc._shutdown_Sightings;
	ws._restart_after_shutdown = sc._restart_after_shutdown;
	ws._has_bad_status = sc._has_bad_status;
	ws._Sighting_index = sc._Sighting_index;
	ws._set_curr_Sighting = sc._set_curr_Sighting;
	ws._add_Sighting = sc._add_Sighting;
	ws._delete_Sighting = sc._delete_Sighting;
	ws._nearest_Sighting = sc._nearest_Sighting;
	ws._chg_curr_Sighting = sc._chg_curr_Sighting;
	ws._reposition_effects = sc._reposition_effects;
	ws._update_Sightings = sc._update_Sightings;
	ws._newList = sc._newList;
	ws._call_pending = sc._call_pending;
	ws._create_Sightings = sc._create_Sightings;
	ws._update_target_marker = sc._update_target_marker;
	ws._manage_marker = sc._manage_marker;
	ws._mostCentered = sc._mostCentered;
	ws._auto_updates = sc._auto_updates;
	ws._resetIdentDelay = sc._resetIdentDelay;
	ws._steerFCB = sc._steerFCB;
	ws._clear_HUD_Effects = sc._clear_HUD_Effects;
	ws._showVShip = sc._showVShip;
	ws._set_vShip_posn = sc._set_vShip_posn;
	ws._hud_effects = sc._hud_effects;
	ws._relativeDirection = sc._relativeDirection;
	ws._report_config = sc._report_config;
	ws._report_autovars = sc._report_autovars;
}
this._debug_Sightings_closure = function _debug_Sightings_closure() {	// expose debug functions
	var ws = worldScripts.telescope;
	var sc = ws.$Sighting_closure;
	if( !sc || !ws.$DebugMessages ) return;
	ws.reset_common_vars = sc.reset_common_vars;
	ws.index_in_list = sc.index_in_list;
	ws.getDetected = sc.getDetected;
	ws.is_hostile = sc.is_hostile;
	ws.grav_scan_dist = sc.grav_scan_dist;
	ws.check_Sightings = sc.check_Sightings;
	ws.select_Sightings = sc.select_Sightings;
	ws.add_lt_ball = sc.add_lt_ball;
	ws.lb_effect_size = sc.lb_effect_size;
	ws.update_lt_ball = sc.update_lt_ball;
	ws.add_ml_ring = sc.add_ml_ring;
	ws.ml_effect_size = sc.ml_effect_size;
	ws.update_ml_ring = sc.update_ml_ring;
	ws.proc_stealthy = sc.proc_stealthy;
	ws.update_one_Sighting = sc.update_one_Sighting;
	ws.update_some = sc.refresh_Sightings;
	ws.classify_ship = sc.classify_ship;
	ws.is_ignored_ship = sc.is_ignored_ship;
	ws.process_new_targets = sc.process_new_targets;
	ws.fns_are_pending = sc.fns_are_pending;
	ws.set_fn_pending = sc.set_fn_pending;
	ws.clear_all_pending = sc.clear_all_pending;
	ws.show_pending = sc.show_pending;
	ws.grow_new_list = sc.grow_new_list;
	ws.notable_ent = sc.notable_ent;
	ws.check_if_new_targets = sc.check_if_new_targets;
	ws.update_MFDs = sc.update_MFDs;
	ws.qualifyMFD = sc.qualifyMFD;
	ws.set_displayName = sc.set_displayName;
	ws.showTargetName = sc.showTargetName;
	ws.showShipReport = sc.showShipReport;
	ws.entityIsNamed = sc.entityIsNamed;
	ws.planetIsNamed = sc.planetIsNamed;
	ws.sunName = sc.sunName;
	ws.orbName = sc.orbName;
	ws.planetNameString = sc.planetNameString;
	ws.report_scan_progress = sc.report_scan_progress;
}
this._StartTimer = function _StartTimer( delay ) {
	var that = _StartTimer;
	var ws = (that.ws = that.ws || worldScripts.telescope);
	var ps = player && player.ship;
	if( !ps ) return;
	if( ps.equipmentStatus('EQ_TELESCOPE') === 'EQUIPMENT_OK' ) {
		//AutoScan timer get targets from normal scanner and do scan if a new target is visible
		//need at least 1 sec delay when called from shipWillExitWitchspace to avoid many gray balls
		if( ws.$Timer_auto_updates || ws.$Sighting_events_FCB ) {
			ws._StopTimer();
		}
		ws.$Timer_auto_updates = new Timer( ws, ws._auto_updates, delay, 0.25 );
		ws.$Sighting_events_FCB = addFrameCallback( ws._Sighting_events.bind(ws) );
	}
}
this._StopTimer = function _StopTimer() {
	var that = _StopTimer;
	var ws = (that.ws = that.ws || worldScripts.telescope);
	let timer = ws.$Timer_auto_updates;
	if( timer ) {
		if( timer.isRunning ) {
			timer.stop();
		}
		ws.$Timer_auto_updates = null;								// discard Timer as timer == null => docked or in witchspace
	}
	let fcb = ws.$Sighting_events_FCB;
	if( fcb ) {
		if( isValidFrameCallback( fcb ) ) {
			removeFrameCallback( fcb );
		}
		ws.$Sighting_events_FCB = null;
	}
}
/*		(function () {	//	IIFE for re-loading _Sighting_events
	if( isValidFrameCallback( ws.$Sighting_events_FCB ) )
		removeFrameCallback( ws.$Sighting_events_FCB );
	ws.$Sighting_events_FCB = addFrameCallback( ws._Sighting_events.bind( ws ) );
})()
//*/
this._Sighting_events = function _Sighting_events( delta ) {		//delta is the time since the last frame
	function fps_missisng() { return -1 }
	var that = _Sighting_events;
	var ws = (that.ws = that.ws || worldScripts.telescope);
	var short_term_fps = (that.short_term_fps = that.short_term_fps || ws.$fps_closure ? ws.$fps_closure._short_term_fps : fps_missisng);
	var reset_fps_mon =	 (that.reset_fps_mon  = that.reset_fps_mon	|| ws.$fps_closure ? ws.$fps_closure._reset_fps_monitor : fps_missisng);
	if( that.speedup_tried === undefined ) that.speedup_tried = false;// persistent flag for speed-up attempt
	if( that.speedup_fps === undefined ) that.speedup_fps = -1;		// persistent variable for short_term_fps value
	if( that.tasks === undefined ) that.tasks = 1;					// persistent # of pending tasks processed each frame
	if( !ws._init_player_vars() ) return;							// equipment damaged, nothing to do
	var speedup_tried = that.speedup_tried,
		speedup_fps = that.speedup_fps,
		tasks = that.tasks;
	ws._update_target_marker();										// must be 1st, as set variables used by followng (eg. target_vector)
	if( ws.$are_Steering ) {										// steering must preceed _hud_effects
		ws._steerFCB( delta );
	}
	ws._hud_effects( delta );
	ws._reposition_effects();
	if( !speedup_tried ) {											// reduce overhead if tried & failed
		let fps = short_term_fps();
		if( fps > 0 ) {												// takes 2 minutes to get 1st report
			if( ws.$DebugMessages ) log(ws.name, '_Sighting_events, got fps of ' + fps );
			reset_fps_mon( delta, true );							// restart & wipe data to have another 2 minute wait
			if( speedup_fps < 0 ) {									// 1st time through
				if( fps > 65 ) {									// candidate for processing 2 tasks (2.38+ ms/frame diff)
					that.speedup_fps = fps;
					that.tasks = 2;									// try faster rate
					if( ws.$DebugMessages ) log(ws.name, '_Sighting_events, trying out '
											   + that.tasks + ' tasks/frame' );
				} else {											// 1st reading was low, shut down checking on slower machines
					that.tasks = 1;
					that.speedup_tried = true;
					if( ws.$DebugMessages ) log(ws.name, '_Sighting_events, running too slow (' + fps
											   + ') for trial of higher tasks/frame' );
				}
			} else {												// back after another 2 minutes
				if( fps <= 60 ) {									// extra task caused frame rate to fall too much
					that.tasks = 1;									// revert to single task each frame
					that.speedup_tried = true;
					if( ws.$DebugMessages ) log(ws.name, '_Sighting_events, fps cost too high('
											   + speedup_fps + ' -> ' + fps + '), reverting to '
											   + that.tasks + ' tasks/frame' );
				} else {											// still over 60, keep new rate & shut down checking
					that.speedup_tried = true;
					if( ws.$DebugMessages ) log(ws.name, '_Sighting_events, fps still over 60 (' + fps
											   + '), continuing with ' + that.tasks + ' tasks/frame' );
				}
			}
		}
	}
	ws._call_pending( tasks );
}
// mode/activate methods //////////////////////////////////////////////////////////////////////////
this.$SET_LIGHTBALLS = 1;											// bit flags to reinit cached $UserChangedSettings
this.$SET_MASSLOCKRINGS = 2;
this.$SET_SNIPER = 4;
this.$SET_STEERING = 8;
this.$SET_TARGETS = 16;
this.$SET_VISUAL = 32;
this.$SET_VISUAL_SIZE = 64;
this._getOldLightballs = function _getOldLightballs() { 			// return values expected by 1.15
	var that = _getOldLightballs;
	var ws = ( that.ws = that.ws || worldScripts.telescope );
/*
	subitem:			1		2				3				4				5						6
	version 1.15
	[ "Lightballs:", "off", "navigation only", "ships", "masslock borders", "bright masslock borders", "large" ],
	version 2
	[ "Lightballs:", "off", "navigation only", "include ships", "large" ],
	[ "Masslock rings:", "current alert/weapons state: off", "current alert/weapons state: on", "brighter" ],
 */
	var subitem = 1;	//off
	if( ws.$LightBalls ) 			subitem = 2;
	if( ws.$ShipLightBalls ) 		subitem = 3;
	if( ws.$MassLockRings && ws.$LightBalls && ws.$ShipLightBalls )	{// don't turn on LightBalls & ShipLightBalls if MassLockRings
		// lesser of evils: it's an inperfect mapping from 1.15's list of bools to v2's situational masslock rings
		subitem = 4;
	}
	if( ws.$BrightMassLockRings )	subitem = 5;
	if( ws.$LargeLightBalls ) 		subitem = 6;					// will turn on (bright) masslock rings <meh>
	return subitem;
}
// : ' +  + '
this._oldSetLightballs = function _oldSetLightballs( subitem ) { 	// handle as 1.15 would; 2.0 changes will overwrite
	var that = _oldSetLightballs;
	var ws = ( that.ws = that.ws || worldScripts.telescope );
//	if( subitem === 1 ) { //off
	ws.$LightBalls = 			subitem >= 2;						//ship off
	ws.$ShipLightBalls = 		subitem >= 3;						//small
	ws.$MassLockRings = 		subitem >= 4 ? this.$DEFAULT_ML_RINGS : 0;
	ws.$BrightMassLockRings = 	subitem >= 5;						//use brighter borders
	ws.$LargeLightBalls = 		subitem >= 6;						//large
}
this._SetLightballs = function _SetLightballs( subitem ) {			//set config variables from telescopeeq.js also
	var that = _SetLightballs;
	var ws = (that.ws = that.ws || worldScripts.telescope);
	ws.$UserChangedSettings |= ws.$SET_LIGHTBALLS;					// set bit flag to reinit _Sightings_closure cached vars
	ws.$DamageMsg = true;
	ws.$LightBalls =			subitem >= 2;						//ship off
	ws.$ShipLightBalls =		subitem >= 3;						//small
	ws.$LargeLightBalls =		subitem >= 4;						//large
	if( ws._update_Sightings )
		ws._update_Sightings( true );
}
this._SetMasslockRings = function _SetMasslockRings( subitem ) {	//set config variables from telescopeeq.js also
	var that = _SetMasslockRings;
	var ws = (that.ws = that.ws || worldScripts.telescope);
	ws.$UserChangedSettings |= ws.$SET_MASSLOCKRINGS;				// set bit flag to reinit _Sightings_closure cached vars
	ws.$DamageMsg = true;
	// ws.$MassLockRings =		subitem >= 2;
	// MassLockRings is no longer a boolean but a set of bitflags
	if( subitem >= 2 ) {											// enabling masslock rings
		ws._adjustMLFlags( true );									// add current state to flags
	} else {														// disabling masslock rings
		ws._adjustMLFlags( false );									// remove current state from flags
	}
	ws.$BrightMassLockRings = 	subitem >= 3;						//use brighter borders
	if( ws._update_Sightings )
		ws._update_Sightings( true );
}
this._SetSniper = function _SetSniper( subitem ) {
	var that = _SetSniper;
	var ws = (that.ws = that.ws || worldScripts.telescope);
	ws.$UserChangedSettings |= ws.$SET_SNIPER;						// set bit flag to reinit _Sightings_closure cached vars
	ws.$DamageMsg = true;
	if( subitem === 1 ) { //off
		ws.$SniperRange = 10000;
		ws.$SniperMinRange = 10000;
	} else {
		var minitem = subitem;
		if( subitem < 5 )
			ws.$SniperRange = 25600;
		else {
			minitem = subitem - 3;
			ws.$SniperRange = 30000;
		}
		ws.$SniperMinRange = 5000 * ( minitem - 1 );				//5, 10 or 15km
	}
}
this._SetSteering = function _SetSteering( subitem ) {
	var that = _SetSteering;
	var ws = (that.ws = that.ws || worldScripts.telescope);
	ws.$UserChangedSettings |= ws.$SET_STEERING;					// set bit flag to reinit _Sightings_closure cached vars
	ws.$DamageMsg = true;
	ws.$Steering = subitem - 1;
}
this._SetTargets = function _SetTargets( subitem ) {
	var that = _SetTargets;
	var ws = (that.ws = that.ws || worldScripts.telescope);
	ws.$UserChangedSettings |= ws.$SET_TARGETS;						// set bit flag to reinit _Sightings_closure cached vars
	ws.$DamageMsg = true;
	if( subitem === 1 ) {											//20 and limitation in red alert
		ws.$MaxTargets = 20;
	} else {
		if( subitem === 2 )
			ws.$MaxTargets = 50;
		else if( subitem === 3 )
			ws.$MaxTargets = 100;
		else
			ws.$MaxTargets = 200;
	}
}
this._SetVisual = function _SetVisual( subitem ) {
	var that = _SetVisual;
	var ws = (that.ws = that.ws || worldScripts.telescope);
	ws.$UserChangedSettings |= ws.$SET_VISUAL;						// set bit flag to reinit _hud_effects_closure cached vars
	ws.$DamageMsg = true;
	if( subitem === 1 ) {
		ws.$ShowVisualTarget = 0;									// always off
	} else if( subitem === 2 ) {
		ws.$ShowVisualTarget = 1;									// on only when weapons off-line
	} else {
		ws.$ShowVisualTarget = 2;									// always on
	}
	ws.$VisualTargetRing = subitem > 3;
	ws.$TelescopeRing = subitem > 3;								// maintain for oxps
	ws.$ShowVisualStation = subitem > 4;							//no station
	ws.$ShowVisualQuestionMark = subitem > 5;						//no "?"
}
this._SetVisualSize = function _SetVisualSize( subitem ) {
	var that = _SetVisualSize;
	var ws = (that.ws = that.ws || worldScripts.telescope);
	ws.$UserChangedSettings |= ws.$SET_VISUAL_SIZE;					// set bit flag to reinit _hud_effects_closure cached vars
	ws.$DamageMsg = true;
	if( ws.$VisualTargetCombatSize !== 0 ) {						// not disabled in station options
		ws.$VisualTargetCombatSize = subitem;						//1-8
		ws.$TelescopeVSize = subitem;								// maintain for oxps
	}
	if( ws.$VisualTargetNormalSize !== 0 ) {						// not disabled in station options
		ws.$VisualTargetNormalSize = subitem;						//1-8
		ws.$TelescopeVZoomSize = subitem;							// maintain for oxps
	}
}
// miscellaneous methods //////////////////////////////////////////////////////////////////////////////
this._AddShips = function _AddShips() {								// add ships/aliens for debugging
	var that = _AddShips;
	var ws = (that.ws = that.ws || worldScripts.telescope);
	var wop = (that.wop = that.wop || worldScripts[ "oolite-populator" ]);
	if( ws.$DebugMessages && !system.isInterstellarSpace ) { //need check due to called from shipWillExitWitchspace also
		//system.addShips("shuttle", 1, system.mainStation.position, 50000);//demo visible target
		//system.addShips("trader", 1, system.mainStation.position, 20000);//demo near target
		wop._addFreighter( system.mainStation );
		wop._addMediumHunterReturn( system.mainStation );
/* for specific ship model, enclose model name in [], eg. addShips('[vector_geek]', ...)
		system.addShips("asteroid", 10, system.mainStation.position, 20000);//test rock target
		system.addShips("rescue_station", 1, system.mainStation.position, 20000);//test custom station
		system.addShips("rescue_blackbox", 1, system.mainStation.position, 10000);//test custom ship
		system.addShips("rescue_blackbox_generic", 1, system.mainStation.position, 10000);//test custom ship
		system.addShips("stealth_base", 1, system.mainStation.position, 20000);//test custom station
		system.addShips("stealth_barracuda", 1, system.mainStation.position, 10000);//test stealth ship
		system.addShips("stealth_mine", 1, system.mainStation.position, 20000);//test stealth mine
		system.addShips("vector_areidisAlpha", 1, system.mainStation.position, 20000);//test custom station
		system.addShips("vector_arn", 1, system.mainStation.position, 10000);//test custom ship
		system.addShips("griff_NPC_prototype_boa_decals_from_red_channel",
			1, system.mainStation.position, 10000);//test visual effect shader uniforms, need Griff Boa OXP
 */
	}
	if( ws.$Thargoids ) { //test Telescope in instant action
		system.addShips("tharglet", 4, system.mainStation.position, 30000);
		system.addShips("thargoid", 4, system.mainStation.position, 30000);
//			system.addShips("police", 4, system.mainStation.position, 30000);
		player.ship.scriptedMisjump = true; //meet Thargoids in the next hyperjump also
	}
}
this._reportError = function _reportError( err, func, parms, depth, goDeep ) {
	// constants - adjust as needed
	var FILE_LEN = 100;		// cut-off len for file spec.
	var FNAME_LEN = 40;		// cut-off len for function name
	var ARGS_LEN = 60;		// cut-off len for arguments string
	var STRING_LEN = 120;	// cut-off for long strings
	var IPAD = ' ';			// inside padding, eg. after array open bracket, before close bracket
	function trim_str( str ) {
		var result, len = str.length;
		if( len === 0 )
			return '<empty string>';
		result = str.replace( /[\u180e\u2000-\u200a\u202f\u205f\u3000]+/g, ' ' );
		result = result.replace( /[\n]+/g, '\\n' ).replace( /[\t]+/g, '\\t' )
		result = '"' + (len > STRING_LEN ? result.substr(0, STRING_LEN) + ' ...' : result) + '"';
		return result
	}
	var padding = [];
	function mkSpacePad( count ) {
		if( typeof count === 'number' ) {
			padding.length = count + 1;
			return padding.join(' ');
		}
		return ' ';
	}
	function countObjKeys( obj, deep ) {	// Object.keys( obj ).length only counts hasOwnProperty ones
		var count = 0;						// deep overrides goDeep
		if( goDeep || deep ) {
			for( let prop in obj )
				if( prop )					// this is just to silence JSLint
					count++;
		} else {
			count = Object.keys( obj ).length;
		}
		return count;
	}
	function rptType( obj ) {
		if( Array.isArray( obj ) ) {
			let len = obj.length;
			return len > 0 ? '<array of ' + len + '>' : '[]';
		} else if( obj instanceof Script ) {
			return '[Script "' + obj.name + '" version ' + obj.version + ']';
		} else if( typeof obj === 'object' ) {
			let len = countObjKeys( obj, true );	// ignore goDeep when counting
			return len > 0 ? '<object of ' + len + '>' : '{}';
		} else {
			return obj;
		}
	}
	function hasComplex( obj ) {
		for( let prop in obj ) {
			if( goDeep || obj.hasOwnProperty( prop ) ) {
				let item = obj[ prop ];
				if( Array.isArray( item ) || (typeof item === 'object' && item !== null) )
					return true;
			}
		}
		return false;
	}
	function showComplex( obj, recurse ) {
		var isArray = Array.isArray( obj );
		var len = isArray ? obj.length : countObjKeys( obj );
		if( len === 0 ) return isArray ? '[]' : '{}';
		var index = 0,
			str = (isArray ? '[' : '{') + IPAD,
			strLen = str.length;
		var recursable = recurse > 0 && hasComplex( obj );
		for( let prop in obj ) {
			if( goDeep || obj.hasOwnProperty( prop ) ) {
				let item = obj[ prop ];
				let propStr = isArray ? '' :
							(goDeep && !obj.hasOwnProperty( prop ) ? '^' : '') + prop + ': ';
				let propLen = propStr.length;
				str += propStr;
				if( recursable ) {
					if( index === 0 ) {
						outStarts.push( (outStarts.length > 0
										? outStarts[outStarts.length-1] + propLen + strLen
										: strLen + propLen + strLen) );
					}
					str += fmt_parm( item, recurse );
					if( index < len - 1 ) {		// not the last one
						let inset = outStarts.length > 1 ? outStarts[outStarts.length-2] : strLen;
						str += ',\n' + mkSpacePad( indentLen + inset );
					} else {
						str += IPAD;
					}
				} else {
					str += hasComplex( item ) ? rptType( item ) : fmt_parm( item, 0 );
					str += index < len - 1 ? ', ' : IPAD;
				}
				index++;
			}
		}
		if( recursable && index ) outStarts.pop();
		return str + (isArray ? ']' : '}');
	}
	var outStarts = [];	// stack of running total of recursed insets
	var parents = [];	// check parm not in parents to avoid endless recursion
	function fmt_parm( parm, recurse ) {
		if( parents.indexOf( parm ) < 0 ) {
			parents.push( parm );
		} else  {
			return parm;
		}
		var type = typeof parm;
		var str = '';
		if( parm === null ) {
			str += 'null';
		} else if( type === 'undefined' ) {
			str += 'undefined';
		} else if( type === 'string' ) {
			str += trim_str( parm );
		} else if( type === 'boolean' ) {
			str += (parm ? 'true' : 'false');
		} else if( type === 'function' ) {
			str += 'function ' + parm.name + '()';
		} else if( parm instanceof Script ) {
			str += '[Script "' + parm.name + '" version ' + parm.version + ']';
		} else if( parm instanceof Vector3D ) {
			str += 'Vector3D: (' + parm.x.toFixed() + ', '
					+ parm.y.toFixed() + ', ' + parm.z.toFixed() + ')';
		} else if( parm instanceof Quaternion ) {
			str += 'Quaternion: (' + parm.w.toFixed() + ' + ' + parm.x.toFixed() + 'i + '
					+ parm.y.toFixed() + 'j + ' + parm.z.toFixed() + 'k)';
		} else if( Array.isArray( parm ) ) {
			str += showComplex( parm, recurse <= 1 ? 0 : recurse - 1 );
		} else if( type === 'object' && parm ) {
			str += showComplex( parm, recurse <= 1 ? 0 : recurse - 1 );
		} else {
			str += rptType( parm );
		}
		parents.pop();
		return str;
	}
	var funcProps = {};
	function propsNotName( obj ) {
		if( typeof obj !== 'function' ) return 0;	// backwards compatibity
		for( let key in funcProps ) {				// reset object
			if( funcProps.hasOwnProperty( key ) )
				delete funcProps[ key ];
		}
		for( let key in obj ) {
			if( key !== 'name' )
				funcProps[ key ] = obj[ key ];
		}
		return Object.keys( funcProps ).length;
	}
	var parmsLabel = '\n    parameters: ';
	var indentLen = parmsLabel.length - 1;	// -1 for \n
	var fnName = typeof func === 'function' ? func.name : func; // backwards compatibity
	var rpt, parmMax, propMax,
		bonus = Array.isArray( parms ) ? 1 : 0;			// don't count parms being an array as recursion (+ 1)
	if( Array.isArray( depth ) ) {
		parmMax = (depth.length > 0 && typeof depth[ 0 ] === 'number' ? ~~(depth[ 0 ]) : 1) + bonus;
		propMax = (depth.length > 1 && typeof depth[ 1 ] === 'number' ? ~~(depth[ 1 ]) : 1) + bonus;
	} else {
		parmMax = propMax = (typeof depth === 'number' ? ~~(depth) : 1) + bonus;
	}
	if( err instanceof Error ) {
		rpt = '\nfunction ' + fnName + '() \t caught: \t' + err.name + ': ' + err.message;
	} else {		// for thrown strings (user defined errors)
		rpt = '\nfunction ' + fnName + '() \t caught: \t' + err;
	}
	if( parms ) {
		rpt += parmsLabel + fmt_parm( parms, parmMax );
	}
	if( propsNotName( func ) ) {
		parmsLabel = '\n    properties: ';
		indentLen = parmsLabel.length - 1;	// -1 for \n
		rpt += parmsLabel + fmt_parm( funcProps, propMax + 1 );	// + 1 as funcProps is an object
	}
	// err is the stack object with properties: message, fileName, lineNumber, stack, name
	//  - stack is a long string containing <function call>@<filename>:<line #> separated by
	//    '\n' for each call in the stack
	if( err && err.stack ) {
		var lastFile, parsed, frame, fnCall, args, file, line, pad;
		var stk = err.stack.split( /[\n\r]+/ ); // split on line breaks
		for( let idx = 0, len = stk.length; idx < len; idx ++ ) {
			// stack line format: fn(parms)@../AddOns/.../script.js:123
			parsed = stk[ idx ].match( /^\s*(\w+)\((.*?)\)@(.*?):(.*?)$/ );
			if( !parsed || parsed.length < 5 ) break;
			[frame, fnCall, args, file, line] = parsed;
			if( file && file !== lastFile ) {	// suppress repeat of same filename
				if( file.length > FILE_LEN )
					file = file.substring( file.length - FILE_LEN ) + '...';
				rpt += '\n    file: ' + file;
				lastFile = file;
			}
			pad = line < 10 ? '   ' : line < 100 ? '  ' : line < 1000 ? ' ' : '' ;
			rpt += '\n        line: ' + pad + line + ',	';
			if( fnCall.length > FNAME_LEN ) fnCall = fnCall.substring(0, FNAME_LEN) + '...';
			if( args.length > ARGS_LEN ) args = args.substring(0, ARGS_LEN) + '...';
			if( args.length ) 					// add spaces inside function's parenthices
				args = ' ' + args.replace( /,/g, ', ' ) + ' ';
			rpt += fnCall + '(' + args + ')';
		}
	}
	return rpt;
}
/*	profiling in debug console
ws.set_profiling()
ws.clear_profiling()
ws._delete_Sighting( PS.target )
:time ws._delete_Sighting( PS.target )
ws._add_Sighting( PS.target )
:time ws._add_Sighting( PS.target )
ws._create_Sightings();
:time ws.grow_new_list( 'start', false	)
:time ws.update_some( 2 )
:time ws._call_pending( 1 )
ws.time_create()
ws._report_config()
ws._report_autovars()
*/
//	(function () {ws.$VisualTargetRing = true; ws.$UserChangedSettings = 63;})()
//	(function () {ws.$VisualTargetRing = false; ws.$UserChangedSettings = 63;})()
///////////////////////////////////////////////////////////////////////////////////////////////////
// Telescope closure //////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////
/*		(function () {	// telescope IIFE for reloading new _Sightings_closure
    var ws = worldScripts.telescope;
    console.clearConsole();
    ws._StopTimer();
    ws._shutdown_Sightings();
    ws._init_Sightings_closure();
    // for debugging  Sighting
    if (ws.$DebugMessages) {
        ws._debug_Sightings_closure();
    }
//ws.time_create = sc.time_create;					ws.time_update = sc.time_update;				//cagiife
//ws.time_refresh = sc.time_refresh;				ws.profile_create = sc.profile_create;		  //cagiife
//ws.profile_update = sc.profile_update;			ws.profile_refresh = sc.profile_refresh;		//cagiife
//ws.set_profiling = sc.set_profiling;				ws.clear_profiling = sc.clear_profiling;			//cagiife
	ws._initOxpVars();
	log("calling _set_vShip_posn(" + ps.viewPositionForward + ", " + ws.$VTarget_HUD_shift + ")");
	ws._set_vShip_posn( ps.viewPositionForward, ws.$VTarget_HUD_shift );
	log("after calling _set_vShip_posn(" + ps.viewPositionForward + ", " + ws.$VTarget_HUD_shift + ")");
// NB: these are basically shipLaunchedFromStation, so comment out if testing involves launch
	ws._init_player_vars( true );
	ws._restart_after_shutdown();
	ws._create_Sightings();
	ws._StartTimer(1);
})()	// */
// ^gui screen.*?[\w\s]+(?=[\n]^[^g])
// : ' +  + '
this._Sightings_closure = function _Sightings_closure() {
	// oxp 'constant' variables
	var ws = worldScripts.telescope;
	var AstroLibrary, Carriers, Combat_MFD, Escortdeck, FarPlanets,
		GalacticAlmanac, GalNavy, ILS, Navigation_MFD,
		SniperLock, SniperLockPlus, SpicyHermits, TorusToSun, Towbar,
		VariableMassLock, VimanaHUD, WarpDrive,
		add_Sighting_errors,
		short_term_fps, long_term_fps, current_fps,
		turn_off_fps_monitor, turn_on_fps_monitor, realtime_fps;
		// must be set after all oxp's loaded, ie. startUpComplete, not startUp where this closure is created
	// game 'constant' variables
	var gameSettings = oolite.gameSettings,
		gameWindow = gameSettings.gameWindow,						// can be changed via options menu
		fov = gameSettings.fovValue,								// can be changed via options menu
		sin_fov2, cos_fov2,
		strFontLen = defaultFont.measureString,
		SpaceLen = strFontLen( ' ' );
	// math function references
	var floor = Math.floor, round = Math.round, ceil = Math.ceil,
		sqrt = Math.sqrt, pow = Math.pow, ln = Math.log,
		sin = Math.sin, cos = Math.cos, acos = Math.acos,
		asin = Math.asin, random = Math.random, abs = Math.abs;
	const LOG10E = Math.LOG10E;
	function log10( num ) { 										// base 10 logarithm of num
		return ln( num ) * LOG10E;
	}
	// function references
	var entitiesWithScanClass = system.entitiesWithScanClass,
		addVisualEffect = system.addVisualEffect,
		addShips = system.addShips,
		consoleMessage = player.consoleMessage,
		isArray = Array.isArray;
	// user settable 'constant' variables
	var AutoScan = ws.$AutoScan,
		AutoScanMaxRange = ws.$AutoScanMaxRange,
		GravLock = ws.$GravLock,
		AutoLock = ws.$AutoLock,
		IdentLock = ws.$IdentLock,
		IdentDelay = ws.$IdentDelay,
		FarStatus = ws.$FarStatus,
		MaxTargets = ws.$MaxTargets,								// can config in flight
		RedAlertDist = ws.$RedAlertDist,
		Steering = ws.$Steering,									// can config in flight
		LightBalls = ws.$LightBalls,								// can config in flight
		ShipLightBalls = ws.$ShipLightBalls,						// can config in flight
		LargeLightBalls = ws.$LargeLightBalls,						// can config in flight
		LightBallMinDist = ws.$LightBallMinDist,
		LightBallShipMinDist = ws.$LightBallShipMinDist,
		MassLockRings = ws.$MassLockRings,							// can config in flight
		MassLockViewDirn = ws.$MassLockViewDirn,
		BrightMassLockRings = ws.$BrightMassLockRings,				// can config in flight
		SniperRingSize = ws.$SniperRingSize,
		SniperRingActive = ws.$SniperRingActive,					// new
		SniperRange = ws.$SniperRange,								// can config in flight
		SniperMinRange = ws.$SniperMinRange,						// can config in flight
		SniperRingColor = ws.$SniperRingColor,
		ShowVisualTarget = ws.$ShowVisualTarget,					// can config in flight
		VisualTargetNormalSize = ws.$VisualTargetNormalSize,		// can config in flight
		VisualTargetCombatSize = ws.$VisualTargetCombatSize,		// can config in flight
		VisualTargetRing = ws.$VisualTargetRing,					// can config in flight
		ShowVisualStation = ws.$ShowVisualStation,					// can config in flight
		ShowVisualQuestionMark = ws.$ShowVisualQuestionMark,		// can config in flight
		ModelRingColor = ws.$ModelRingColor,
		// VTarget_HUD_shift is not used in closure but passed in during shipWillLaunchFromStation
		// new/ui
		ConsoleMsgDurn = ws.$ConsoleMsgDurn,
		GravScanMsgFreq = ws.$GravScanMsgFreq,
		IdentMessages = ws.$IdentMessages,
		// ShowSummary is not used in closure; it's the reportSummary bool from station_options
		debug = ws.$DebugMessages,
		// new/experimental
		TargetOnlyHostile = ws.$TargetOnlyHostile,
		RemoveInFlight = ws.$RemoveInFlight,
		MFDFiltering = ws.$MFDFiltering,
		MFDPrimaryStatic = ws.$MFDPrimaryStatic,
		MFDPrimaryDynamic = ws.$MFDPrimaryDynamic,
		SeparateMFDs = ws.$SeparateMFDs,
		MFDAuxStatic = ws.$MFDAuxStatic,
		MFDAuxDynamic = ws.$MFDAuxDynamic;
	// flags for variables modifiable via activated event - see init_player_vars
	const SET_LIGHTBALLS = ws.$SET_LIGHTBALLS,
		  SET_MASSLOCKRINGS = ws.$SET_MASSLOCKRINGS,
		  SET_SNIPER = ws.$SET_SNIPER,
		  SET_STEERING = ws.$SET_STEERING,
		  SET_TARGETS = ws.$SET_TARGETS,
		  SET_VISUAL = ws.$SET_VISUAL,
		  SET_VISUAL_SIZE = ws.$SET_VISUAL_SIZE;
	// player's alertCondition
	const DOCKED = 0, GREEN_ALERT = 1, YELLOW_ALERT = 2, RED_ALERT = 3;
	// gravity scan state
	const GS_NONE = 0, GS_STOPPED = 1, GS_RUNNING = 2, GS_DEGRADING = 3, GS_COMPLETE = 4;
	// index used to calc bitflags for masslock view direction
	const VIEWS_LIST = [ 'VIEW_FORWARD', 'VIEW_AFT', 'VIEW_PORT', 'VIEW_STARBOARD' ];
	// identKeyPress values
	const IDENT_READY = 0, IDENT_LOCKED = 1, IDENT_STEERING = 2, IDENT_UNLOCK = 3, IDENT_STEER_DELAY = 4, IDENT_STEP_DELAY = 5;
	// MFD names
	const PrimaryMFD_name = ws.$PrimaryMFD_name,
		  AuxilaryMFD_name = ws.$AuxilaryMFD_name;
	// constant values (really!)
	const MASSLOCK_RING_SCALE = ws.$MASSLOCK_RING_SCALE,
		  PRECISION = 1E-8,											// standard for equality: a - b < 1E-8 => essentially equal
		  QUARTER_SECS_OF_4MIN = 1/960,
		  QUARTER_SECS_OF_2MIN = 1/480,
		  PI = Math.PI,
		  RADIANS_TO_DEGREES = 180 / PI,
		  //DEGREES_TO_RADIANS = PI / 180,
		  QUARTER_ARC = PI / 2,
		  FORTYFIVE_DEGREES = PI / 4,
		  ONE_DEGREE = PI / 180,
		  REL_DIR_HALF_PLUS =  QUARTER_ARC + ONE_DEGREE * 2,		// reduce ambiguity in some cases by excluding an axis close match
		  REL_DIR_HALF_MINUS = QUARTER_ARC - ONE_DEGREE * 2,		//	 for easier nav, eg. to port & up a small bit is 90 <, not 90<^
		  REL_DIR_STRESS = 2,										// ratio of horiz/vert angle to produce a double direction mark
		  VECTOR_ALL_ZEROS = [0, 0, 0],
		  VECTOR_ALL_ONES = [1, 1, 1];
	const SPAWN_DELAY = 0.25;										// fix (?) for .isVisible bug (freshly spawned ships have .isVisible == true)
																	// - ignore spawned ship until 1/2 second has passed
	// globally local variables, 'glocals'
	var TelescopeList = ws.$TelescopeList,							// cached ref to back-compatible telescope object for oxp support
		MaxRange = ws.$MaxRange,
		mapping = ws.$SightingsMap, maplen = 0, 	 				// persistent array of Sightings
		mappingReady = false,										// map of size 0 can exist in interstellar -thanks Milo
		curr_S = ws.$curr_Sighting,									// cached ref to permantent telescope object
		selected_Sightings = [],									// for return value of select_Sightings fn
		BuyMsg = true,												//flag to show the buy message once
		ps = player && player.ship,
		scannerRange, scannerRange_X_2, scannerRange_X_4, scannerRange_X_10;
	var curr_target, viewDirection, viewHasMLRings, identKeyPress,
		viewIsStandard,		// see _reposition_effects (used to decide if to alter masslock orientation)
		headingView = [],	// to calc heading for a view, need perpendicular horizontal vector
		eq_status, equip_ok, ext_ok, grav_eq_ok, grav_eq2_ok, large_ok, small_ok, scanFilter_ok,
		gravScanProgress, gs_mult, gs_state = GS_NONE, stationNearby,
		alertCondition, weaponsOnline, show_on_Alert, show_on_Weapons,
		ps_collisionRadius, ps_injectorsEngaged,ps_mass, ps_maxSpeed, ps_orientation,
		ps_position, prev_psp, ps_speed, ps_torusEngaged, ps_velocity, moving_fast,
		ps_vectorForward, ps_vectorRight, ps_vectorUp;
		// - these are all set in init_player_vars()
	var using_common_vars, hasAtmosphere, isBeacon, isBuoy, is_cargo, isCloaked, is_drone, isFrangible,
		isHostile, is_ignored, isJamming, is_minable, isPiloted, isPlanet, isStation, isSun, isThargoid,
		isVisible, isWormhole, dataKey, distance, mass, primaryRole, radius, scanClass,
		script_mass, shipClassName, status, collisionRadius, gs_curr, gs_max, lb_size,
		ml_size, rank, ve_colour, position = [], ent_vector = [], target_vector = [],
		bounty, has_targets, targeting_ps, in_ents_Targets, in_ps_Targets, dynamicMFD, staticMFD;
		// - these var.s are set as needed by local fns and are shared by all - see reset_common_vars
	var prevMFDTarget = null;										//support for Combat MFD
	var distanceUnits = 'm',										// support for navi_mfd, RandomStationNames & Stranger's world
		baseDistance = 1000;
	var cd = worldScripts.telescope_debug;
// : ' +  + '
//debug = ws.$DebugMessages = false; // for profiling
/* turn off for profiling!!*/
	function _initOxpVars() {										// closure is created in startUp but some values may not be know
																	//	 until startUpComplete (order of loading oxp's is unpredictible)
		try {
			AstroLibrary = worldScripts.AstroLibrary;
			Combat_MFD = worldScripts.combat_MFD;
			Carriers = worldScripts.carriers;
			Escortdeck = worldScripts.escortdeck;
			FarPlanets = worldScripts.farplanets;
			ILS = worldScripts.ils;
			GalacticAlmanac = worldScripts.RandomStationNames;
			GalNavy = worldScripts.GalNavy;
			Navigation_MFD = worldScripts.navi_mfd;
			PlanetaryCompass = worldScripts[ 'planetaryCompass_worldScript.js' ];
			PlanetNames = worldScripts.planetnames;
			SpicyHermits = worldScripts.spicy_hermits_abandoned;
			SniperLock = worldScripts.sniperlock;
			SniperLockPlus = worldScripts.sniperlock_plus;
			TorusToSun = worldScripts.torustosun;
			Towbar = worldScripts.towbar;
			VariableMassLock = worldScripts.variablemasslock;
			VimanaHUD = worldScripts.VimanaHUD;
			WarpDrive = worldScripts.WarpDrive;
			if( VimanaHUD ) {
				// VimanaHUD sets TelescopeVSize & TelescopeVZoomSize in its startUp
				VisualTargetCombatSize = ws.$VisualTargetCombatSize = ws.$TelescopeVSize;
				VisualTargetNormalSize = ws.$VisualTargetNormalSize = ws.$TelescopeVZoomSize;
				// vsizechanged = true;								// force update of current model
			}
			updateMenuVars();
			add_Sighting_errors = ws.$add_Sighting_errors;
			var fps = ws.$fps_closure;
			if( fps ) {
				short_term_fps = fps._short_term_fps;
				long_term_fps = fps._long_term_fps;
				current_fps = fps._current_fps;
				realtime_fps = fps._realtime_fps;
				turn_on_fps_monitor = fps._turn_on_fps_monitor;
				turn_off_fps_monitor = fps._turn_off_fps_monitor;
			}
		} catch( err ) {
			log( ws.name, ws._reportError( err, '_initOxpVars' ) );
			if( debug ) throw err;
		}
	}
	function init_player_statics( ps ) {							// init var that do not differ over frames
		ps_maxSpeed = ps.maxSpeed;
		ps_collisionRadius = ps.collisionRadius;
		scannerRange = ps.scannerRange;
		scannerRange_X_2 = scannerRange * 2;
		scannerRange_X_4 = scannerRange * 4;
		scannerRange_X_10 = scannerRange * 10;
		ws.$extenderActive = ps.equipmentStatus( 'EQ_TELESCOPEEXT' ) === 'EQUIPMENT_OK';
		// - other cases of changed status are handles in equipment world event handlers
	}
	function init_player_vars( report ) {
		try {
			return _init_player_vars( report );
		} catch( err ) {
			log( ws.name, ws._reportError( err, 'init_player_vars', report ) );
			if( debug ) throw err;
		}
	}
	function _init_player_vars( report ) {							// init var that may differ from one frame to the next
		let curr_ps = player && player.ship;
		if( !ps || ps !== curr_ps || !ps_maxSpeed )	{				// 1st time or diff ship
			init_player_statics( curr_ps );
		}
		ps = curr_ps;
		eq_status = ps.equipmentStatus( 'EQ_TELESCOPE' );
		equip_ok = eq_status === 'EQUIPMENT_OK';
		if( !equip_ok ) {
			_shutdown_Sightings()
			return false;
		}
		if( !ps_position ) {
			ps_position = alloc_array();
			prev_psp = alloc_array();
		} else { // ps_position = ps.position; is faster but ensuring it's an array now will streamline any future use
			if( ps_position.length )
				copy_vector( ps_position, prev_psp );
			copy_vector( ps.position, ps_position );
		}
		identKeyPress = ws.$IdentKeyPress;
		let ps_target = ps.target;
		if( identKeyPress === IDENT_READY ) {						// else target is 'locked' on curr_S, Steering or in IdentDelay
			curr_target = ps_target || null;
			if( curr_target ) {
				if( curr_target === curr_S.marker ) {				// far target, am targeting marker
					curr_target = curr_S.ent || null;				// fetch real target's ent
				}
			} else if( curr_S.ent ) {								// ensure kept in sync
				_set_curr_Sighting( null, '_init_player_vars (curr_S.ent but no target)' );
			}
		}
		ps_mass = ps.mass;
		ps_speed = ps.speed;
		if( WarpDrive ) {
			moving_fast = ps_speed > WarpDrive.$basicMaxSpeed;
		} else {
			moving_fast = ps_speed > ps_maxSpeed;					// injectors or torus
		}
		if( moving_fast && ShowVisualTarget !== 0) {
			/*
			 * NB: setting ps_torusEngaged & ps_torusEngaged only occurs when 3D model is used
			 *     as that's only place where they're used (this save 2 ship property get's/frame)
			 *     If used elsewhere, remove ShowVisualTarget test
			 */
			ps_torusEngaged = ps.torusEngaged;
			ps_injectorsEngaged = ps_torusEngaged ? false : ps.injectorsEngaged;
		} else {
			ps_injectorsEngaged = ps_torusEngaged = false;
		}
		if( !ps_velocity ) ps_velocity = alloc_array();
		copy_vector( ps.velocity, ps_velocity );
		if( !ps_orientation ) ps_orientation = alloc_array();
		copy_quaternion( ps.orientation, ps_orientation );
		basis_vectors_from_quaternion( ps_orientation )
		viewDirection = ps.viewDirection;
		let index = index_in_list( viewDirection, VIEWS_LIST );
		viewHasMLRings = index >= 0 && index < 4 					// VIEWS_LIST.length
				? MassLockViewDirn & pow( 2, index ) : false;		// bitflag is high
		viewIsStandard = false;										// used to decide orientation of masslock rings in _reposition_effects
		if( viewDirection === "VIEW_FORWARD" ) {
			copy_vector( ps_vectorForward, view_vector );
			viewIsStandard = true;
		} else if( viewDirection === "VIEW_AFT" ) {
			scale_vector( ps_vectorForward, -1, view_vector );
			viewIsStandard = true;
		} else if( viewDirection === "VIEW_STARBOARD" ) {
			copy_vector( ps_vectorRight, view_vector );
			viewIsStandard = true;
		} else if( viewDirection === "VIEW_PORT" ) {
			scale_vector( ps_vectorRight, -1, view_vector );
			viewIsStandard = true;
		}
		let ext_status = ps.equipmentStatus( 'EQ_TELESCOPEEXT' ) === 'EQUIPMENT_OK';
		if( ext_status !== ext_ok ) {								// only update when it changes
			ws.$extenderActive = ext_status;
		}
		ext_ok = ext_status;
		let grav_eq2 = ps.equipmentStatus( 'EQ_GRAVSCANNER2' );
		grav_eq2_ok = grav_eq2 === 'EQUIPMENT_OK';
		grav_eq_ok = ps.equipmentStatus( 'EQ_GRAVSCANNER' ) === 'EQUIPMENT_OK'
						&& grav_eq2 !== 'EQUIPMENT_DAMAGED'; 		// vs EQUIPMENT_OK || EQUIPMENT_UNKNOWN || EQUIPMENT_UNAVAILABLE
		small_ok = ps.equipmentStatus( 'EQ_SMALLDISH' ) === 'EQUIPMENT_OK';
		large_ok = ps.equipmentStatus( 'EQ_LARGEDISH' ) === 'EQUIPMENT_OK';
if( report && debug ) {
	log( ws.name, '_init_player_vars, ext_ok is ' + ext_ok + ', grav_eq_ok is ' + grav_eq_ok
				 + ', grav_eq2_ok is ' + grav_eq2_ok + ', small_ok is ' + small_ok  + ', large_ok is ' + large_ok
				 + '\n  player ship is ' + ps
				 + '\n  curr_target is ' + curr_target );
}
		let mult = 1;												// gravity scan + other equipment extends its range
		if( large_ok ) {
			mult = 2;
			if( 	 ps_mass > 1e8 ) mult = 8;						//baseship scan double range third time
			else if( ps_mass > 1e6 ) mult = 4;						//huge player ship double range another time
		} else if( small_ok ) {
			mult = 1.33333;
		}
		gs_mult = mult;
		scanFilter_ok = ps.equipmentStatus( 'EQ_MILITARY_SCANNER_FILTER' ) === 'EQUIPMENT_OK';
		alertCondition = player.alertCondition;
		weaponsOnline = ps.weaponsOnline;
		_set_GS_state(); 											// uses stationNearby, grav_eq_ok, weaponsOnline & gravScanProgress
		wide = gameWindow.height / gameWindow.width; ///widescreen correction
		fov = gameSettings.fovValue;								// player may change it
		sin_fov2 = sin( fov/2 );
		cos_fov2 = cos( fov/2 );
		setShowFlags();
		let userChanges = ws.$UserChangedSettings;
		if( userChanges === 0 ) return true;						// reload only when user's been busy w/ mode/activate fns
		if( userChanges & SET_LIGHTBALLS ) {
			LightBalls = ws.$LightBalls;
			ShipLightBalls = ws.$ShipLightBalls;
			LargeLightBalls = ws.$LargeLightBalls;
			ws.$UserChangedSettings &= ~SET_LIGHTBALLS;
		}
		if( userChanges & SET_MASSLOCKRINGS ) {
			MassLockRings = ws.$MassLockRings;
			BrightMassLockRings = ws.$BrightMassLockRings;
			ws.$UserChangedSettings &= ~SET_MASSLOCKRINGS;
		}
		if( userChanges & SET_SNIPER ) {
			SniperMinRange = ws.$SniperMinRange;
			SniperRange = ws.$SniperRange;
			ws.$UserChangedSettings &= ~SET_SNIPER;
		}
		if( userChanges & SET_STEERING ) {
			Steering = ws.$Steering;
			ws.$UserChangedSettings &= ~SET_STEERING;
		}
		if( userChanges & SET_TARGETS ) {
			MaxTargets = ws.$MaxTargets;
			ws.$UserChangedSettings &= ~SET_TARGETS;
		}
		if( userChanges & SET_VISUAL ) {
			ShowVisualTarget = ws.$ShowVisualTarget;
			VisualTargetRing = ws.$VisualTargetRing;
			ShowVisualStation = ws.$ShowVisualStation;
			ShowVisualQuestionMark = ws.$ShowVisualQuestionMark;
			VisualTargetNormalSize = ws.$VisualTargetNormalSize;
			VisualTargetCombatSize = ws.$VisualTargetCombatSize;
			vsizechanged = true;								// force update of current model
			ws.$UserChangedSettings &= ~SET_VISUAL;
		}
		if( userChanges & SET_VISUAL_SIZE ) {
			VisualTargetNormalSize = ws.$VisualTargetNormalSize;
			VisualTargetCombatSize = ws.$VisualTargetCombatSize;
			vsizechanged = true;								// force update of current model
			ws.$UserChangedSettings &= ~SET_VISUAL_SIZE;
		}
		updateMenuVars();
		return true;
	}
	function reset_common_vars() {									// reset all var's that are shared by various fn's
		// we don't know if we're set for curr. entity, so all set to -1 & 1st fn that needs it, sets it accordingly
		// we do this to minimize the # of property gets, which are a lot more expensive than testing local vars < 0
		bounty = -1;
		collisionRadius = -1;
		dataKey = -1;
		distance = -1;
		dynamicMFD = 0;
		ent_vector.length = 0;										// re-use array
		gs_curr = -1;
		gs_max = -1;
		has_targets = -1;
		hasAtmosphere = -1;
		in_ents_Targets = -1;
		in_ps_Targets = -1;
		isBeacon = -1;
		isBuoy = -1;
		is_cargo = -1;
		isCloaked = -1;
		is_drone = -1;
		isFrangible = -1;
		isHostile = -1;
		is_ignored = -1;
		isJamming = -1;
		is_minable = -1;
		isPiloted = -1;
		isPlanet = -1;
		isStation = -1;
		isSun = -1;
		isThargoid = -1;
		isVisible = -1;
		isWormhole = -1;
		lb_size = -1;
		mass = -1;
		ml_size = -1;
		position.length = 0;										// re-use array
		primaryRole = -1;
		radius = -1;
		rank = -1;
		scanClass = -1;
		script_mass = undefined;									// scriptInfo: telescope can be 0, 1, any +/- integer
		shipClassName = -1;
		staticMFD = 0;
		status = -1;
		targeting_ps = -1;
		target_vector.length = 0;									// re-use arrays
		target_direction.length = 0;
		ve_colour = -1;
		using_common_vars = true;
	}
	function _reload_config( report ) {								// reload config options chg'd on station
		try {
			AutoScan = ws.$AutoScan;
			AutoScanMaxRange = ws.$AutoScanMaxRange;
			AutoLock = ws.$AutoLock;
			GravLock = ws.$GravLock;
			IdentLock = ws.$IdentLock;
			IdentDelay = ws.$IdentDelay;
			FarStatus = ws.$FarStatus;
			MaxTargets = ws.$MaxTargets;
			RedAlertDist = ws.$RedAlertDist;
			Steering = ws.$Steering;
			LightBalls = ws.$LightBalls;
			ShipLightBalls = ws.$ShipLightBalls;
			LargeLightBalls = ws.$LargeLightBalls;
			LightBallMinDist = ws.$LightBallMinDist;
			LightBallShipMinDist = ws.$LightBallShipMinDist;
			MassLockRings = ws.$MassLockRings;
			MassLockViewDirn = ws.$MassLockViewDirn;
			BrightMassLockRings = ws.$BrightMassLockRings;
			SniperRingSize = ws.$SniperRingSize;
			SniperRingActive = ws.$SniperRingActive;
			SniperRange = ws.$SniperRange;
			SniperMinRange = ws.$SniperMinRange;
			SniperRingColor = ws.$SniperRingColor;
			ShowVisualTarget = ws.$ShowVisualTarget;
			VisualTargetNormalSize = ws.$VisualTargetNormalSize;
			VisualTargetCombatSize = ws.$VisualTargetCombatSize;
			VisualTargetRing = ws.$VisualTargetRing;
			ShowVisualStation = ws.$ShowVisualStation;
			ShowVisualQuestionMark = ws.$ShowVisualQuestionMark;
			ModelRingColor = ws.$ModelRingColor;
			//VTarget_HUD_shift = ws.$VTarget_HUD_shift; 			// not used in closure; gets applied in shipWillLaunchFromStation
			ws.$TelescopeVPosHUD = ws.$VTarget_HUD_shift;			// maintain for oxps
			updateMenuVars();
			// UI_and_docs
			ConsoleMsgDurn = ws.$ConsoleMsgDurn;
			GravScanMsgFreq = ws.$GravScanMsgFreq;
			IdentMessages = ws.$IdentMessages;
			// ShowSummary = ws.$ShowSummary;						// not used in closure
			debug = ws.$DebugMessages;
			// experimental
			TargetOnlyHostile = ws.$TargetOnlyHostile;
			RemoveInFlight = ws.$RemoveInFlight;
			MFDFiltering = ws.$MFDFiltering;
			MFDPrimaryStatic = ws.$MFDPrimaryStatic;
			MFDPrimaryDynamic = ws.$MFDPrimaryDynamic;
			SeparateMFDs = ws.$SeparateMFDs;
			MFDAuxStatic = ws.$MFDAuxStatic;
			MFDAuxDynamic = ws.$MFDAuxDynamic;
			// ws.$Thargoids		 // not used in closure; gets applied in shipWillLaunchFromStation
			if( report ) _report_config();
		} catch( err ) {
			log( ws.name, ws._reportError( err, '_reload_config', report ) );
			if( debug ) throw err;
		}
	}
	function updateMenuVars() {
		// update TelescopeMenu vars; values are 1-based index, as 0 used for description
		var menu = 1;												// off
		if( MaxTargets < 20 )
			MaxTargets = ws.$MaxTargets = 20;
		else if( MaxTargets > 200 )
			MaxTargets = ws.$MaxTargets = 200;
		menu += MaxTargets > 100 ? 3 : MaxTargets > 50 ? 2 : MaxTargets > 20 ? 1 : 0;
		ws.$TelescopeMenuTargets = menu;
		ws.$TelescopeMenuSteering = Steering + 1;
		menu = 1;													// off
		if( LightBalls ) menu = 2;
		if( ShipLightBalls ) menu = 3;
		if( LargeLightBalls ) menu = 4;
		ws.$TelescopeMenuLightballs = menu;
		menu = 1;													// off
		let state = ws._getShowState(),								// on/off for current alert/weaps state
			currFlags = ws._currMLFlags();
		if( currFlags & state ) {
			menu = 2;
			if( BrightMassLockRings )
				menu = 3;
		}
		ws.$TelescopeMenuMasslockRings = menu;
		menu = 1;													// off
		if( SniperMinRange !== SniperRange ) {
			let min = round(SniperMinRange / 5000);					// round( SniperMinRange / 5000 )
			menu = 1 + (min <= 1 ? 1 : min >= 3 ? 3 : min) + (SniperRange <= 25600 ? 0 : 3);
		}
		ws.$TelescopeMenuSniper = menu;
		menu = 6;													// all
		if( !ShowVisualQuestionMark )	  menu = 5;
		if( !ShowVisualStation )		  menu = 4;
		if( !VisualTargetRing )			  menu = 3;
		if( ShowVisualTarget === 1 )	  menu = 2;
		else if( ShowVisualTarget === 0 ) menu = 1;
		ws.$TelescopeMenuVisual = menu;
		menu = VisualTargetCombatSize < VisualTargetNormalSize
			 ? VisualTargetCombatSize : VisualTargetNormalSize;
		ws.$TelescopeMenuVisualSize = menu > 0 ? menu : 1;
/*
if( debug ) {
	log('updateMenuVars, TelescopeMenuTargets: ' + ws.$TelescopeMenuTargets
		+ ', TelescopeMenuSteering: ' + ws.$TelescopeMenuSteering
		+ ', TelescopeMenuLightballs: ' + ws.$TelescopeMenuLightballs
		+ ', TelescopeMenuMasslockRings: ' + ws.$TelescopeMenuMasslockRings
		+ ', \n\tTelescopeMenuSniper: ' + ws.$TelescopeMenuSniper
		+ ', TelescopeMenuVisual: ' + ws.$TelescopeMenuVisual
		+ ', TelescopeMenuVisualSize: ' + ws.$TelescopeMenuVisualSize
	);
}
 */
	}
	function report_config( limit ) {
		try {
			_report_config( limit );
		} catch( err ) {
			log( ws.name, ws._reportError( err, 'report_config' ) );
			if( debug ) throw err;
		}
	}
	function fmtStaticFlags( stat ) {
		var flag = '';
		if( stat & MFD_SALVAGE )	flag += '| SALVAGE ';
		if( stat & MFD_MINING )		flag += '| MINING ';
		if( stat & MFD_WEAPONS )	flag += '| WEAPONS ';
		if( stat & MFD_TRADERS )	flag += '| TRADERS ';
		if( stat & MFD_POLICE )		flag += '| POLICE ';
		if( stat & MFD_PIRATES )	flag += '| PIRATES ';
		if( stat & MFD_MILITARY )	flag += '| MILITARY ';
		if( stat & MFD_ALIENS )		flag += '| ALIENS ';
		if( stat & MFD_NEUTRAL )	flag += '| NEUTRAL ';
		if( stat & MFD_STATION )	flag += '| STATION ';
		if( stat & MFD_NAVIGATION )	flag += '| NAVIGATION ';
		if( stat & MFD_CELESTIAL )	flag += '| CELESTIAL ';
		return flag.length ? flag + '|' : '';
	}
	function fmtDynFlags( dyn ) {
		var flag = '';
		if( dyn & MFD_FRIENDLY )	flag += '| FRIENDLY ';
		if( dyn & MFD_UNSOCIABLE )	flag += '| UNSOCIABLE ';
		if( dyn & MFD_ACTIVE )		flag += '| ACTIVE ';
		if( dyn & MFD_HOSTILE )		flag += '| HOSTILE ';
		if( dyn & MFD_NEARBY )		flag += '| NEARBY ';
		if( dyn & MFD_PROTECTED )	flag += '| PROTECTED ';
		if( dyn & MFD_FARAWAY )		flag += '| FARAWAY ';
		return flag.length ? flag + '|' : '';
	}
	function fmtSteering() {
		return Steering > 1 ? "'Each in list'" : Steering > 0 ? "'Nearest'" : "'Off'";
	}
	function fmtMassLockViewDirn() {
		var flag = '';
		for( let num = 0; num <= 4; num++ ) {
			if( MassLockViewDirn & pow( 2, num ) )
				flag += '| ' + VIEWS_LIST[ num ].slice( 5 );
		}
		return flag.length ? flag + ' |' : '';
	}
	function fmtAlertWeapsState( bitflag ) {
		var flag = '';
		if( bitflag & SHOW_GREEN_WEAPS_OFF )	flag += '| GREEN_OFF ';
		if( bitflag & SHOW_GREEN_WEAPS_ON )	flag += '| GREEN_ON ';
		if( bitflag & SHOW_YELLOW_WEAPS_OFF )	flag += '| YELLOW_OFF ';
		if( bitflag & SHOW_YELLOW_WEAPS_ON )	flag += '| YELLOW_ON ';
		if( bitflag & SHOW_RED_WEAPS_OFF )	flag += '| RED_OFF ';
		if( bitflag & SHOW_RED_WEAPS_ON )		flag += '| RED_ON ';
		if( flag.length )
			return flag + '|';
		return '';
	}
	function fmtGSMsgFreq( stat ) {
		var flag = '';
		if( stat & 1 )	flag += '| ++ endpoints ';
		if( stat & 2 )	flag += '| ++ quarterly ';
		if( stat & 4 )	flag += '| ++ tenths ';
		if( stat & 8 )	flag += '| -- endpoints ';
		if( stat & 16 )	flag += '| -- quarterly ';
		if( stat & 32 )	flag += '| -- tenths ';
		return flag.length ? flag + '|' : '';
	}
	function _report_config( limit ) {								// also reports on experimental
		var rpt, idt = '    ', pad = ',' + idt,
			flags = '  ->  ', nlIdt = '\n' + idt;
		log( ws.name, '\n' );
		if( !limit || limit === 'config' ) {
			rpt = idt	+ 'AutoScan = ' + AutoScan
				+ pad	+ 'AutoScanMaxRange = ' + AutoScanMaxRange
				+ pad	+ 'AutoLock = ' + AutoLock + '°'
				+ pad	+ 'GravLock = ' + GravLock + '°'
				+ pad	+ 'IdentLock = ' + IdentLock + '°'
				+ nlIdt + 'IdentDelay = ' + IdentDelay
				+ pad	+ 'FarStatus = ' + FarStatus
				+ pad	+ 'MaxTargets = ' + MaxTargets
				+ pad	+ 'RedAlertDist = ' + RedAlertDist
				+ pad	+ 'Steering = ' + fmtSteering()
				+ nlIdt + 'LightBalls = ' + LightBalls
				+ pad	+ 'ShipLightBalls = ' + ShipLightBalls
				+ pad	+ 'LargeLightBalls = ' + LargeLightBalls
				+ pad	+ 'LightBallMinDist = ' + LightBallMinDist
				+ pad	+ 'LightBallShipMinDist = ' + LightBallShipMinDist
				+ nlIdt	+ 'MassLockRings = ' + MassLockRings
				+ flags + fmtAlertWeapsState( MassLockRings )
				+ nlIdt + 'MassLockViewDirn = ' + MassLockViewDirn
				+ flags + fmtMassLockViewDirn()
				+ nlIdt + 'BrightMassLockRings = ' + BrightMassLockRings
				+ nlIdt + 'SniperRingSize = ' + SniperRingSize
				+ pad	+ 'SniperRingActive = ' + SniperRingActive
				+ flags + fmtAlertWeapsState( SniperRingActive ) // vs binary: .toString(2)
				+ nlIdt + 'SniperRange = ' + SniperRange
				+ pad	+ 'SniperMinRange = ' + SniperMinRange
				+ pad	+ 'SniperRingColor = ' + SniperRingColor
				+ nlIdt + 'ShowVisualTarget = ' + ShowVisualTarget
				+ pad	+ 'VisualTargetNormalSize = ' + VisualTargetNormalSize
				+ pad	+ 'VisualTargetCombatSize = ' + VisualTargetCombatSize
				+ pad	+ 'VisualTargetRing = ' + VisualTargetRing
				+ nlIdt + 'ShowVisualStation = ' + ShowVisualStation
				+ pad	+ 'ShowVisualQuestionMark = ' + ShowVisualQuestionMark
				+ pad	+ 'ModelRingColor = ' + ModelRingColor
				+ pad	+ 'ws.$VTarget_HUD_shift = ' + ws.$VTarget_HUD_shift
				+ nlIdt;
			log( ws.name, 'config:\n' + rpt );
		}
		if( !limit || limit === 'UI_and_docs' ) {
			rpt = idt	+ 'ConsoleMsgDurn = ' + ConsoleMsgDurn
				+ pad	+ 'GravScanMsgFreq = ' + GravScanMsgFreq
				+ flags + fmtGSMsgFreq( GravScanMsgFreq )
				+ nlIdt + 'IdentMessages = ' + IdentMessages
				+ pad	+ 'ShowSummary = ' + ws.$ShowSummary			// not used in closure
				+ pad	+ 'DebugMessages = ' + debug
				+ nlIdt;
			log( ws.name, 'UI_and_docs:\n' + rpt );
		}
		if( !limit || limit === 'experimental' ) {
			rpt = idt	+ 'MFDFiltering = ' + MFDFiltering
				+ nlIdt + 'MFDPrimaryStatic = ' + MFDPrimaryStatic
				+ flags + fmtStaticFlags( MFDPrimaryStatic )
				+ nlIdt + 'MFDPrimaryDynamic = ' + MFDPrimaryDynamic
				+ flags + fmtDynFlags( MFDPrimaryDynamic )
				+ nlIdt + 'SeparateMFDs = ' + SeparateMFDs
				+ nlIdt + 'MFDAuxStatic = ' + MFDAuxStatic
				+ flags + fmtStaticFlags( MFDAuxStatic )
				+ nlIdt + 'MFDAuxDynamic = ' + MFDAuxDynamic
				+ flags + fmtDynFlags( MFDAuxDynamic )
				+ nlIdt + 'Thargoids = ' + ws.$Thargoids		 			// not used in closure; gets used in _AddShips
				+ nlIdt + 'BetaLicenceTimestamp = ' + ws.$BetaLicenceTimestamp
				+ pad	+ 'BetaLicenceSystem = ' + ws.$BetaLicenceSystem
				+ '\n';
			log( ws.name, 'experimental:\n' + rpt );
		}
		log( ws.name, '\n' );
	}
	var have_shutdown = false;
	function shutdown_Sightings() {
		try {
			_shutdown_Sightings();
		} catch( err ) {
			log( ws.name, ws._reportError( err, '_shutdown_Sightings' ) );
			if( debug ) throw err;
		}
	}
	function _shutdown_Sightings() {
		if( have_shutdown ) return;
		if( debug ) log( ws.name, '_shutdown_Sightings, shutting down...');
		ws.$Telescope_not_in_use = have_shutdown = true;
		clear_all_pending();
		if( ps ) {											// in case called from dock
			equip_ok = ps.equipmentStatus( 'EQ_TELESCOPE' ) === 'EQUIPMENT_OK';
			if( MFD_is_visible( PrimaryMFD_name ) )
				doClear_MFD( PrimaryMFD_name );
			if( MFD_is_visible( AuxilaryMFD_name ) )
				doClear_MFD( AuxilaryMFD_name )
		}
		_set_curr_Sighting( null, '_shutdown_Sightings' );	// no parms resets
		ws.$IdentKeyPress = identKeyPress = IDENT_READY;
		_newList();
		if( ps ) 											// in case called from dock
			_clear_HUD_Effects();
		// purge pools -happens either in witchspace or when docked before garbage is collected
		if( debug ) log(ws.name, '_shutdown_Sightings, used_Sightings = ' + used_Sightings.length
													  + ', used_arrays = ' + used_arrays.length
													  + ', used_pending = ' + used_pending.length );
		used_Sightings.length = 0;
		used_arrays.length = 0;
		used_pending.length = 0;
		if( turn_off_fps_monitor )
			turn_off_fps_monitor();
	}
	var system_sun = null;
	var system_name = null;
	var isInterstellarSpace = false;
	var mainPlanet = null;
	var system_planets = null;										// must be init'd after launch; see orbName
	var system_stations = null;
	function _restart_after_shutdown() {							// called only once _init_player_vars() succeeds
																	// - see shipLaunchedFromStation & shipExitedWitchspace
		try {
			if( debug ) log( ws.name, '_restart_after_shutdown, starting up...');
			ws.$Telescope_not_in_use = have_shutdown = false;
			clearNameCaches();
			system_sun = system.sun;
			if( system_sun && system_sun.hasGoneNova )				// thanks Milo
				system_sun = null;
			system_name = system.name;
			isInterstellarSpace = system.isInterstellarSpace;
			mainPlanet = system.mainPlanet;
			system_planets = system.planets;
			system_stations = system.stations;
			setDistanceUnits();
			buildEclipsers();
			doClear_MFD( PrimaryMFD_name );
			doClear_MFD( AuxilaryMFD_name );
			stationNearby = false;										//to send gravscanner message after launch
			gravScanProgress = 0;										//begin new gravity detection process
			ws.$IdentKeyPress = identKeyPress = IDENT_READY;			// reset target lock
			_resetIdentDelay();
			if( turn_on_fps_monitor )
				turn_on_fps_monitor();
		} catch( err ) {
			log( ws.name, ws._reportError( err, '_restart_after_shutdown' ) );
			if( debug ) throw err;
		}
	}
	function buildEclipsers() {
		if( !systemEclipsers ) {
			systemEclipsers = alloc_array();
		} else {
			systemEclipsers.length = 0;
			}
		// eclipsers checked from start of array, so put most likely ones first: stations, planets, ARHs, sun
		for( let idx = 0, len = system_stations.length; idx < len; idx++ ) {
			let ent = system_stations[ idx ];
			if( ent && ent.isValid ) {
				systemEclipsers.push( ent );
			}
		}
		for( let idx = 0, len = system_planets.length; idx < len; idx++ ) {
			let ent = system_planets[ idx ];
			if( ent && ent.isValid ) {
				systemEclipsers.push( ent );
			}
		}
		if( SpicyHermits ) {
			let abandonedRHs = entitiesWithScanClass( 'CLASS_ROCK' );
			for( let idx = 0, len = abandonedRHs.length; idx < len; idx++ ) {
				let ent = abandonedRHs[ idx ];
				if( ent && ent.isValid
						&& ent.isMinable && !ent.isFrangible ) {	// normal Rock Hermits caught in system_stations
					systemEclipsers.push( ent );
				}
			}
			free_array( abandonedRHs );
		}
		var sun = fetchSun();
		if( sun ) {
			systemEclipsers.push( sun );
		}
	}
/* Stranger's OU is system specific
    var mainOrbitVector = new Vector3D(system.sun.position.subtract(system.mainPlanet.position));
    var ouScale = mainOrbitVector.magnitude();  // OU in custom system
 */
/* navi_mfd  (NB: OU is static)
.$unitSetting is index into $distUnits = ["OU", "km", "m"] while $rounding = [6, 3, 0];
message += "Distance: " + dist.toFixed(this.$rounding[this.$unitSetting]) + " " + this.$distUnits[this.$unitSetting] +"\n";
from market inquirer:
	unitBase = worldScripts.navi_mfd.$ostronomicalUnits[worldScripts.navi_mfd.$unitSetting];
	unit = worldScripts.navi_mfd.$distUnits[worldScripts.navi_mfd.$unitSetting];
	rnd = worldScripts.navi_mfd.$rounding[worldScripts.navi_mfd.$unitSetting]
	...
	function dist(entity, rounding) {
		var distInKm = (player.ship.position.distanceTo(entity) - entity.collisionRadius)/unitBase;
		return (distInKm.toFixed(rounding) + " " + unit);
	};
 */
/* randomstationnames: extra units besides (static) OU (NB: strangers world overrides?)
  15800 TS "Torans" == 1000 KM "Kilometres" == 905520 OU "Orthodox Units" == 1609.34 MI "Miles" == 2.08641 CZ "Cavezzi"
 "Torans. One Unit is the distance travelled in one second under Torus Dilation.";
 "Kilometres. One Unit is the distance travelled by light in three microseconds.";
 "Orthodox Units. One Unit is the distance between the planet Lave and its Star.";
 "Miles. One Unit is equal to the combined hight of nine hundred Anciant Earthians.";
 "Cavezzi. One Unit is one twenty millionth the circumference of Ancient Earth.";
 - all .toFixed(3) except Cavezzi which is .toFixed(0)
!must be checked upon each launch as configurable in station's F4
if (missionVariables.random_station_names_units == "Torans") var unitBase = 15800;
if (missionVariables.random_station_names_units == "Torans") var unit = "TS";
if (missionVariables.random_station_names_units == "Kilometres") var unitBase = 1000;
if (missionVariables.random_station_names_units == "Kilometres") var unit = "KM";
if (missionVariables.random_station_names_units == "Orthodox Units") var unitBase = 905520;
if (missionVariables.random_station_names_units == "Orthodox Units") var unit = "OU";
if (missionVariables.random_station_names_units == "Miles") var unitBase = 1609.34;
if (missionVariables.random_station_names_units == "Miles") var unit = "MI";
if (missionVariables.random_station_names_units == "Cavezzi") var unitBase = 2.08641;
if (missionVariables.random_station_names_units == "Cavezzi") var unit = "CZ";
var rounding = missionVariables.random_station_names_mfd_rounding; // is 3 except for CZ where it's 0
var distranceinUnits = (player.ship.position.distanceTo(entity) - entity.collisionRadius)/missionVariables.random_station_names_mfd_unitBase;
var almanacDisplayDistance = distranceinUnits.toFixed(rounding);
if (almanacDisplayDistance <0) var almanacDisplayDistance = 0;
 */
	function setDistanceUnits() {
		distanceUnits = 'm';
		baseDistance = 1;
		if( GalacticAlmanac ) {
			let units = missionVariables.random_station_names_units;
			// baseDistance = missionVariables.random_station_names_mfd_unitBase;
			if( units === 'Torans' ) {
				distanceUnits = 'TS';
				baseDistance = 15800;
			} else if( units === 'Kilometres' ) {
				distanceUnits = 'KM';
				baseDistance = 1000;
			} else if( units === 'Orthodox Units' ) {
				distanceUnits = 'OU';
				baseDistance = 905520;
			} else if( units === 'Miles' ) {
				distanceUnits = 'MI';
				baseDistance = 1609.34;
			} else if( units === 'Cavezzi' ) {
				distanceUnits = 'CZ';
				baseDistance = 2.08641;
			}
		} else if( AstroLibrary ) {
			distanceUnits = 'OU';
			if( mainPlanet && system_sun ) {
				let pos = system_sun.position;
				if( pos )
					baseDistance = pos.subtract( mainPlanet ).magnitude();  // OU in custom system
			} // else continue using that from previous system
		} else if( Navigation_MFD ) {
			let unitSetting = Navigation_MFD.$unitSetting;
			distanceUnits = Navigation_MFD.$distUnits[ unitSetting ];
			baseDistance = Navigation_MFD.$ostronomicalUnits[ unitSetting ];
			// cache & update every system -> _restart_after_shutdown
		}
	}
	function index_in_list( item, list ) {							// for arrays only; faster than indexOf
		if( !list ) return -1;
		var len = list.length;
		while( len-- ) {
			if( list[ len ] === item )
				return len;
		}
		return -1;
	}
	function equal_value( a, b ) { return abs( a - b ) < PRECISION; }
//	function abs_diff( a, b ) { return abs( abs(a) - abs(b) ); }
	// vector functions & array pool //////////////////////////////////////////////////////////////
	function popArrayItem( arr, idx ) {								// garbage free alternative to array slice
		var popped = arr[ idx ], len = arr.length;
		for( var idx = idx; idx < len - 1; idx++ ) {
			arr[ idx ] = arr[ idx + 1 ];
		}
		arr.length = --len;
		return popped;
	}
	var used_arrays = [];
	function free_array( array ) {									// attempt to reduce garbage collection by managing used objects
		if( !array ) return;
		if( !isArray( array ) ) return;
		array.length = 0;											// scrub old data
		used_arrays.push( array );									// toss into recycle bin
		if( used_arrays.length >= 100 ) {							// build up over time
			used_arrays.length = 10;
if( debug ) log(ws.name, 'free_array, pool EXCEEDED 100, reduced to 10' );
		}
	}
	function alloc_array() {										// attempt to reduce garbage collection by managing used objects
		if( used_arrays.length > 0 ) {								// re-use old array
			return used_arrays.pop();
		}
		return [];
	}
	/* From Wikipedia
	Because the magnitude of the cross product goes by the sine of the angle between its arguments,
	the cross product can be thought of as a measure of perpendicularity in the same way that the
	dot product is a measure of parallelism. Given two unit vectors, their cross product has a
	magnitude of 1 if the two are perpendicular and a magnitude of zero if the two are parallel.
	The dot product of two unit vectors behaves just oppositely: it is zero when the unit vectors
	are perpendicular and 1 if the unit vectors are parallel.
	Unit vectors enable two convenient identities: the dot product of two unit vectors yields the
	cosine (which may be positive or negative) of the angle between the two unit vectors. The
	magnitude of the cross product of the two unit vectors yields the sine (which will always be positive).
	*/
	function chk_vparms( a, N, parm, testNaN ) { 					// a: vector, N: expected length, parm: callers parm #
		if( !a ) ///throw( 'chk_vparms, !a' );
			ws._reportError( '"a" is not defines', chk_vparms, [a, N, parm, testNaN] )
		var errMsg = '';
		if( isArray( a ) ) {		/// insignificant difference in profiling
			let len = a.length;
			if( len > 0 && len < N ) {								// can be 0 if re-used
				errMsg = 'isArray but length too short';
				// chk_vparms[ ('parm isArray but length too short') ].err = 0;
			}
		} else if( N === 3 && !(a instanceof Vector3D) ) {
			errMsg = '!isArray and !Vector3D';
			// chk_vparms[ 'parm !isArray and !Vector3D' ].err = 0;
		} else if( N === 4 && !(a instanceof Quaternion) ) {
			errMsg = '!isArray and !Quaternion';
			// chk_vparms[ 'parm !isArray and !Quaternion' ].err = 0;
		} else if( !isArray( a ) && !(a instanceof Vector3D) && !(a instanceof Quaternion) ) {
			errMsg = 'is invalid for a vector';
			// chk_vparms[ ('parm invalid for a vector: ' + a) ].err = 0;
		}
		if( errMsg ) {
			errMsg = 'caller\'s #' + parm + ' parm ' + errMsg + ': ' + a;
			ws._reportError( errMsg, chk_vparms, [a, N, parm, testNaN] )
			/// throw( errMsg );
			// chk_vparms[ errMsg ].err = 0
		}
		if( testNaN ) {
			let len = isArray( a ) ? a.length : a instanceof Vector3D ? 3 : a instanceof Quaternion ? 4 : 0;
			for( let idx = 0; len > 0 && idx < N; idx++ ) {
				if( isNaN( a[ idx ] ) ) {
					errMsg = 'caller\'s #' + parm + ' parm, ' +  'item ' + idx + ' isNaN: ' + a[ idx ] + ', parm: ' + a;
					ws._reportError( errMsg, chk_vparms, [a, N, parm, testNaN] )
					///throw( errMsg );
					// log( 'chk_vparms, item ' + idx + ' isNaN: ' + a[ idx ] );
					// chk_vparms[ 'an item isNaN' ].err = 0;
				}
			}
		}
	}
	// NEVER use copy_vector/copy_quaternion when setting a property (eg. ps.position/ps.orientation), as this
	//	 will generate 3/4 property gets/sets (& garbage) vs. just 1 by doing 'ps.position = my_var' (& no garbage)
	// Also, ALWAYS use copy_vector/copy_quaternion when getting a property, so all subsequent calculations are
	//	 local (ie. using arrays)
	// Yes, this does generate garbage (array object) but can't be helped until core gives us a method where we
	//	 provide the destination. Eg. ps.getPosition( my_var );
/*
	function describeVector( vect ) {
		var v = vect.direction();
		// .dot -1..0..1 spans PI radians; abs spans PI/2
		var dot2deg = RADIANS_TO_DEGREES * Math.PI / 2;
		var msg = ' -> vector points ', decimals = 3;
		var dotForward = v.dot( ps.vectorForward ), diffForward = Math.abs(dotForward) * dot2deg;
		var dotRight = v.dot( ps.vectorRight ), diffRight = Math.abs(dotRight) * dot2deg;
		var dotUp = v.dot( ps.vectorUp ), diffUp = Math.abs(dotUp) * dot2deg;
		var fwdMsg = false;
		if( equal_value( dotForward, 1 ) ) {
			msg += 'directly on heading';
			fwdMsg = true;
		} else if( !equal_value( dotForward, 0 ) ) {
			msg += diffForward.toFixed(decimals) + '° ';
			msg += dotForward > 0 ? 'fore ' : 'aft ';
			msg += dotUp > 0 ? 'of zenith' : 'of nadir';
			fwdMsg = true;
		}
		if( !equal_value( dotRight, 0 ) ) {
			if (fwdMsg) {
				msg += ", ";
			}
			msg += diffRight.toFixed(decimals) + '° ';
			msg += dotRight > 0 ? 'starboard' : 'port';
		}
		if( !equal_value( dotUp, 0 ) ) {
			if (fwdMsg) {
				msg += ", ";
			}
			msg += diffUp.toFixed(decimals) + '° ';
			msg += dotUp > 0 ? 'up' : 'down';
		}
		return msg;
	}
 */
	function copy_vector( a, b, skipChk ) {							// a -> b
		if( debug && !skipChk ) {									// skipChk for colors (arrays of length 4)
			chk_vparms( a, 3, 1, true );
			chk_vparms( b, 3, 2 );
		}
		b[0] = a[0];
		b[1] = a[1];
		b[2] = a[2];
	}
	function same_vectors( a, b ) {									// w/i limits of PRECISION
		if( debug ) {
			chk_vparms( a, 3, 1, true );
			chk_vparms( b, 3, 2, true );
		}
		if( !equal_value( b[0], a[0] ) ) return false;
		if( !equal_value( b[1], a[1] ) ) return false;
		if( !equal_value( b[2], a[2] ) ) return false;
		return true;
	}
	function exact_same_vectors( a, b ) {
		if( debug ) {
			chk_vparms( a, 3, 1, true );
			chk_vparms( b, 3, 2, true );
		}
		if( b[0] !== a[0] ) return false;
		if( b[1] !== a[1] ) return false;
		if( b[2] !== a[2] ) return false;
		return true;
	}
	function add_vectors( a, b, c ) {								// a + b -> c
		if( debug ) {
			chk_vparms( a, 3, 1, true );
			chk_vparms( b, 3, 2, true );
			chk_vparms( c, 3, 3 );
		}
		c[0] = a[0] + b[0];
		c[1] = a[1] + b[1];
		c[2] = a[2] + b[2];
	}
	function subtract_vectors( a, b, c ) {							// a - b -> c
		if( debug ) {
			chk_vparms( a, 3, 1, true );
			chk_vparms( b, 3, 2, true );
			chk_vparms( c, 3, 3 );
		}
		c[0] = a[0] - b[0];
		c[1] = a[1] - b[1];
		c[2] = a[2] - b[2];
	}
	function scale_vector( a, s, b ) {								// s * a -> b
		if( debug ) {
			chk_vparms( a, 3, 1, true );
			if( typeof s !== 'number' ) log( 'scale_vector, s = ' + s );
			if( typeof s !== 'number' ) scale_vector[ 'typeof s !== "number"' ].err = 0;
			chk_vparms( b, 3, 2 );
		}
		b[0] = a[0] * s;
		b[1] = a[1] * s;
		b[2] = a[2] * s;
	}
	function vector_magnitude( a ) {
		if( debug ) chk_vparms( a, 3, 1, true );
		return sqrt( a[0]*a[0]
				   + a[1]*a[1]
				   + a[2]*a[2] );
	}
	function unit_vector( a, b ) {									// |a| -> b
		if( debug ) {
			chk_vparms( a, 3, 1, true );
			chk_vparms( b, 3, 2 );
		}
		var magnitude = vector_magnitude( a );
		let abs_mag = abs( magnitude );
		if( abs_mag === 0 || abs_mag === 1 ) {
			copy_vector( a, b );									// return original vector
		} else {
			scale_vector( a, (1 / magnitude), b );
		}
	}
	function dot_product( a, b ) {
		if( debug ) {
			chk_vparms( a, 3, 1, true );
			chk_vparms( b, 3, 2, true );
		}
		return a[0]*b[0]
			 + a[1]*b[1]
			 + a[2]*b[2];
	}
	var vector = [];												// working vector available to functions
	var __vector = [];												// internal working vectors
/* normal_dot_product
	var __vector2 = [];												// internal working vectors
	function normal_dot_product( a, b ) {
		unit_vector( a, __vector );
		unit_vector( b, __vector2 );
		var dot = dot_product( __vector, __vector2 );
		if( dot > 1 )  dot = 1;		// for identical vectors the dot_product sometimes returns a value > 1.0 because of
		if( dot < -1 ) dot = -1;	// rounding errors, resulting in an undefined result for the acos (see angle_between).
		return dot;
	}
	function angle_between( a, b ) {
		return acos( normal_dot_product( a, b ) );
	}
 */
	function angle_between_unitV( a, b ) {							// faster version as 'a' is known to be a unit vector
		unit_vector( b, __vector );
		var dot = dot_product( a, __vector );
		if( dot > 1 )  dot = 1;		// for identical vectors the dot_product sometimes returns a value > 1.0 because of
		if( dot < -1 ) dot = -1;	// rounding errors, resulting in an undefined result for the acos (see angle_between).
		return acos( dot );
	}
	function angle_between_two_unitV( a, b ) {						// faster still as both are known to be a unit vector
		var dot = dot_product( a, b );
		if( dot > 1 )  dot = 1;		// for identical vectors the dot_product sometimes returns a value > 1.0 because of
		if( dot < -1 ) dot = -1;	// rounding errors, resulting in an undefined result for the acos (see angle_between).
		return acos( dot );
	}
	var cross = [];													// working vector available to functions
	function cross_product( a, b, c ) {								// a X b -> c
		if( debug ) {
			if( a === c || b === c ) cross_product[ '' ].err = 0;
			chk_vparms( a, 3, 1, true );
			chk_vparms( b, 3, 2, true );
			chk_vparms( c, 3, 3 );
		}
		c[0] = a[1]*b[2] - (a[2]*b[1]);
		c[1] = a[2]*b[0] - (a[0]*b[2]);
		c[2] = a[0]*b[1] - (a[1]*b[0]);
	}
	function copy_quaternion( a, b ) {								// a -> b
		if( debug ) {
			if( a === b ) copy_quaternion[ 'a === b' ].err = 0;
			chk_vparms( a, 4, 1, true );
			chk_vparms( b, 4, 2 );
		}
		b[0] = a[0];
		b[1] = a[1];
		b[2] = a[2];
		b[3] = a[3];
	}
	var quaternion = [];											// working quaternion available to functions
/*
	function quat_dot_product( a, b ) {
		if( debug ) {
			chk_vparms( a, 4, 1, true );
			chk_vparms( b, 4, 2, true );
		}
		return a[0]*b[0]
			 + a[1]*b[1]
			 + a[2]*b[2]
			 + a[3]*b[3];
	}
	function negate_quaternion( a, b ) {								// -a -> b
		if( debug ) {
			if( a === b ) copy_quaternion[ 'a === b' ].err = 0;
			chk_vparms( a, 4, 1, true );
			chk_vparms( b, 4, 2 );
		}
		b[0] = -a[0];
		b[1] = -a[1];
		b[2] = -a[2];
		b[3] = -a[3];
	}
 */
	function rotate_vector( vector, quat ) {						// rotate vector by quat (ala rotateBy)
		var that = rotate_vector;
		var qw = (that.qw = that.qw || []);							// working quaternion
		qw.length = 0;
		if( debug ) {
			chk_vparms( vector, 3, 1, true );
			chk_vparms( quat, 4, 2, true );
		}
		qw[0] = 0.0 - quat[1] * vector[0] - quat[2] * vector[1] - quat[3] * vector[2];
		qw[1] = -quat[0] * vector[0] + quat[2] * vector[2] - quat[3] * vector[1];
		qw[2] = -quat[0] * vector[1] + quat[3] * vector[0] - quat[1] * vector[2];
		qw[3] = -quat[0] * vector[2] + quat[1] * vector[1] - quat[2] * vector[0];
		vector[0] = qw[0] * -quat[1] + qw[1] * -quat[0] + qw[2] * -quat[3] - qw[3] * -quat[2];
		vector[1] = qw[0] * -quat[2] + qw[2] * -quat[0] + qw[3] * -quat[1] - qw[1] * -quat[3];
		vector[2] = qw[0] * -quat[3] + qw[3] * -quat[0] + qw[1] * -quat[2] - qw[2] * -quat[1];
	}
	function rotate_about_axis( quat, vector, angle, result ) {
		var that = rotate_about_axis;
		var rotn = (that.rotn = that.rotn || []);
		rotn.length = 0;
		if( debug ) {
			if( quat === result ) rotate_about_axis[ 'quat === result' ].err = 0;
			if( typeof angle !== 'number' ) log( ws.name, 'rotate_about_axis, angle = ' + angle );
			if( typeof angle !== 'number' ) rotate_about_axis[ 'typeof angle !== "number"' ].err = 0;
			chk_vparms( quat, 4, 1, true );
			chk_vparms( vector, 3, 2, true );
			chk_vparms( result, 4, 3 );
			if( !equal_value( 1, vector_magnitude( vector ) ) ) {
				if( debug ) {
					log('rotate_about_axis, NOT a unit vector, vector: ' + vector
						+ ' has magnitude: ' + vector_magnitude( vector ) );
					// rotate_about_axis[ 'vector is not normalized' ].err = 0;
				}
			}
		}
		var a = angle / 2;
		var c = cos(a);
		var s = sin(a);
		// rotation quaternion
		rotn[0] = c;
		rotn[1] = vector[0] * s;
		rotn[2] = vector[1] * s;
		rotn[3] = vector[2] * s;
		// multiply quaternions
		result[0] = quat[0]*rotn[0] - quat[1]*rotn[1] - quat[2]*rotn[2] - quat[3]*rotn[3];
		result[1] = quat[0]*rotn[1] + quat[1]*rotn[0] + quat[2]*rotn[3] - quat[3]*rotn[2];
		result[2] = quat[0]*rotn[2] + quat[2]*rotn[0] + quat[3]*rotn[1] - quat[1]*rotn[3];
		result[3] = quat[0]*rotn[3] + quat[3]*rotn[0] + quat[1]*rotn[2] - quat[2]*rotn[1];
	}
	function vector_forward_from_quaternion( quat ) {
		if( debug ) chk_vparms( quat, 4, 1, true );
		var w, wy, wx;
		var x, xz, xx;
		var y, yz, yy;
		var z, zz;
		var qx, qy, qz;
		w = quat[0];
		x = quat[1];
		y = quat[2];
		z = quat[3];
		xx = 2 * x; yy = 2 * y; zz = 2 * z;
		wx = w * xx; wy = w * yy;
		xx = x * xx; xz = x * zz;
		yy = y * yy; yz = y * zz;
		if( !ps_vectorForward ) ps_vectorForward = alloc_array();
		if( isArray( ps_vectorForward ) ) {
			qx = ps_vectorForward[0] = xz - wy;
			qy = ps_vectorForward[1] = yz + wx;
			qz = ps_vectorForward[2] = 1 - xx - yy;
			if( qx || qy || qz ) {
				unit_vector( ps_vectorForward, ps_vectorForward )
			} else {
				ps_vectorForward[0] = 0;
				ps_vectorForward[1] = 0;
				ps_vectorForward[2] = 1;
			}
		}
	}
	function basis_vectors_from_quaternion( quat ) {
		if( debug ) chk_vparms( quat, 4, 1, true );
		var w, wz, wy, wx;
		var x, xz, xy, xx;
		var y, yz, yy;
		var z, zz;
		var qx, qy, qz;
		w = quat[0];
		x = quat[1];
		y = quat[2];
		z = quat[3];
		xx = 2 * x;	 yy = 2 * y;  zz = 2 * z;
		wx = w * xx; wy = w * yy; wz = w * zz;
		xx = x * xx; xy = x * yy; xz = x * zz;
		yy = y * yy; yz = y * zz;
		zz = z * zz;
		if( !ps_vectorRight ) ps_vectorRight = alloc_array();
		if( isArray( ps_vectorRight ) ) {
			qx = ps_vectorRight[0] = 1 - yy - zz;
			qy = ps_vectorRight[1] = xy - wz;
			qz = ps_vectorRight[2] = xz + wy;
			if( qx || qy || qz ) {
				unit_vector( ps_vectorRight, ps_vectorRight )
			} else {
				ps_vectorRight[0] = 1;
				ps_vectorRight[1] = 0;
				ps_vectorRight[2] = 0;
			}
		}
		if( !ps_vectorUp ) ps_vectorUp = alloc_array();
		if( isArray( ps_vectorUp ) ) {
			qx = ps_vectorUp[0] = xy + wz;
			qy = ps_vectorUp[1] = 1 - xx - zz;
			qz = ps_vectorUp[2] = yz - wx;
			if( qx || qy || qz ) {
				unit_vector( ps_vectorUp, ps_vectorUp )
			} else {
				ps_vectorUp[0] = 0;
				ps_vectorUp[1] = 1;
				ps_vectorUp[2] = 0;
			}
		}
		if( !ps_vectorForward ) ps_vectorForward = alloc_array();
		if( isArray( ps_vectorForward ) ) {
			qx = ps_vectorForward[0] = xz - wy;
			qy = ps_vectorForward[1] = yz + wx;
			qz = ps_vectorForward[2] = 1 - xx - yy;
			if( qx || qy || qz ) {
				unit_vector( ps_vectorForward, ps_vectorForward )
			} else {
				ps_vectorForward[0] = 0;
				ps_vectorForward[1] = 0;
				ps_vectorForward[2] = 1;
			}
		}
	}
	// event call stack ///////////////////////////////////////////////////////////////////////////
	function Pending( fn, parm ) { this.fn = fn; this.parm = parm; }// constructor
	var tasks_pending = [];
	var tasks_deferred = [];										// tasks awaiting current cycle to complete
	var used_pending = [];
	function fns_are_pending() { return tasks_pending.length > 0; }
	function show_pending() {
		if( !debug ) return;
		if( tasks_pending.length > 0 ) {
			let rpt = ''
			for( let task in tasks_pending )
				if( tasks_pending.hasOwnProperty( task ) )
					rpt += '\n\t' + task + ': ' + tasks_pending[task].fn.name + '( ' + tasks_pending[task].parm + ' )';
			log(ws.name, 'tasks_pending = ' + rpt );
		} else {
			log(ws.name, 'tasks_pending is empty ' );
		}
		if( tasks_deferred.length > 0 ) {
			let rpt = ''
			for( let task in tasks_deferred )
				if( tasks_deferred.hasOwnProperty( task ) )
					rpt += '\n\t' + task + ': ' + tasks_deferred[task].fn.name + '( ' + tasks_deferred[task].parm + ' )';
			log(ws.name, 'tasks_deferred = ' + rpt );
		} else {
			log(ws.name, 'tasks_deferred is empty ' );
		}
	}
/* show_pending
function show_pending() { // debug
	if( fns_are_pending() )
		log(ws.name, 'show_pending, tasks_pending = \n' + cd._showProps( tasks_pending, 'tasks', false, 2 ) );
	else
		log(ws.name, 'show_pending, tasks_pending list is empty' );
	if( tasks_deferred.length > 0 )
		log(ws.name, 'show_pending, tasks_deferred = \n' + cd._showProps( tasks_deferred, 'tasks', false, 2 ) );
	else
		log(ws.name, 'show_pending, tasks_deferred list is empty' );
}
*/
	function free_pending( event ) {
		if( !event ) return;
		event.fn = null;
		event.parm = null;
		used_pending.push( event );
		if( used_pending.length >= 100 ) {							  // ?build up over time
			used_pending.length = 20;
			if( debug ) log(ws.name, 'free_pending, pool EXCEEDED 100, reduced to 20' );
		}
	}
	function alloc_pending( fn, parm ) {
		var event;
		if( used_pending.length > 0 ) {
			event = used_pending.pop();
			event.fn = fn;
			event.parm = parm;
		} else {
			event = new Pending( fn, parm );
		}
		return event;
	}
	function set_fn_pending( fn, parm, deferred ) {
		var passing = parm === undefined ? null : parm;				// parm could be zero
		var event, list = deferred ? tasks_deferred : tasks_pending, idx = list.length;
		while( idx-- ) {											// no dups in stack
			event = list[ idx ];
			if( event.fn === fn && event.parm === passing ) {
//if( debug ) log(ws.name, 'set_fn_pending, duplicate call back function "' + fn.name
//					+'", parm = '+passing+ ' ... DISCARDING.' );
				return;
			}
		}
		list.push( alloc_pending( fn, passing ) );
		if( tasks_pending.length > 10 || tasks_deferred.length > 10 ) {
			log(ws.name, 'set_fn_pending, stack has reached '+10+'! BAILING out by creating new Sightings ...' );
			_create_Sightings();
			return;
		}
	}
	function tasks_queued( func ) {
		var len = tasks_pending.length,
			fname = func.name;
		while( len-- > 0 ) {
			if( tasks_pending[ len ].fn.name === fname ) {
				return true;
			}
		}
		len = tasks_deferred.length;
		while( len-- > 0 ) {
			if( tasks_deferred[ len ].fn.name === fname ) {
				return true;
			}
		}
		return false;
	}
/*  purge_pending
	function purge_pending( func ) {
		var len = tasks_pending.length,
			fname = func.name;
		while( len-- > 0 ) {
			let fn = tasks_pending[ len ];
			if( fn.name === fname )
				free_pending( popArrayItem( tasks_pending, len ) );
		}
		len = tasks_deferred.length;
		while( len-- > 0 ) {
			let fn = tasks_deferred[ len ];
			if( fn.name === fname)
				free_pending( popArrayItem( tasks_deferred, len ) );
		}
	}
 */
	function clear_all_pending( keep_deferred ) {
		var len = tasks_pending.length;
		if( len > 0 ) {
			while( len-- )
				free_pending( tasks_pending.pop() );
			tasks_pending.length = 0;
		}
		if( keep_deferred ) return;
		len = tasks_deferred.length;
		if( len > 0 ) {
			while( len-- )
				free_pending( tasks_deferred.pop() );
			tasks_deferred.length = 0;
		}
	}
	function _call_pending( num ) {
		try{
			if( !equip_ok ) return;
			var list = tasks_pending;
			var len = list.length;
			if( len === 0 ) {
				list = tasks_deferred;
				len = list.length;
				if( len === 0 ) return;
			}
			var event, rtn;
			var count = ( num === undefined ? 2 : num );	// Sighting tasks limited to num, scan tasks to 2 arbitrarily
			while( len-- > 0 && count-- > 0 ) {
				event = list.shift();
				if( event === undefined ) throw 'list unexpectedly empty';
				try {
// log('_call_pending, list: ' + (list === tasks_pending ? 'tasks_pending' : 'tasks_deferred') + ', ' + event.fn.name + '(' + event.parm + ')' );
					rtn = event.fn( event.parm );
				} catch( err ) {
					log( ws.name, ws._reportError( err, event.fn.name, event.parm ) );
					if( debug ) throw err;
				} finally {
					free_pending( event );
				}
			}
		} catch( err ) {
			log( ws.name, ws._reportError( err, '_call_pending', num ) );
			if( debug ) throw err;
		}
	}
	// Sighting classification ////////////////////////////////////////////////////////////////////
	function is_hostile( ent, set_all ) {							// returns boolean as to whether an entity is hostile
																	// called in different places in notable_ent
																	//	 set_all is true on update_one_Sighting for all ents
																	// so, using_common_vars is *assumed* true
		var ent_defence, have_scanned = false;
		if( isHostile === true && !set_all ) return true;			// prevent repeat gets unless directed by set_all
		if( distance < 0 ) distance = _detect_distanceTo( ent );
		if( distance > scannerRange ) {
			if( FarStatus ) {										//reveal pirates over normal scanner if have already been in scannerRange
				let index = _Sighting_index( ent );
				if( index >= 0 ) {
					have_scanned = mapping[ index ].have_scanned;
					if( have_scanned !== true && have_scanned !== -1 ) {// have never been w/i scannerRange
						isHostile = false;
						if( set_all )
							bounty = has_targets = targeting_ps = in_ents_Targets = in_ps_Targets = false;
						return false;
					}
				}
			} else {												// w/o FarStatus, can only know status inside scannerRange
				isHostile = false;
				if( set_all )
					bounty = has_targets = targeting_ps = in_ents_Targets = in_ps_Targets = false;
				return false;
			}
		}
		if( ent.hasHostileTarget ) {
			isHostile = true;
			if( !set_all ) return true;
		}
		if( bounty < 0 ) bounty = ent.bounty > 0 || ent.markedForFines;
		if( bounty ) {
			isHostile = true;
			if( !set_all ) return true;
		}
		var ent_target = ent.target;
		if( has_targets < 0 ) {										// used to classify for MFD_ACTIVE
			if( ent_target ) has_targets = true;					// targeting itself is not hostile
			ent_defence = ent.defenseTargets;
			if( ent_defence && ent_defence.length > 0 )				// defending oneself is not hostile
				has_targets = true;
		}
		if( in_ents_Targets < 0 )
			in_ents_Targets = index_in_list( ps, ent_defence ) >= 0;
		if( in_ents_Targets ) {										// or in the defenseTargets of the other ship
			isHostile = true;
			if( !set_all ) return true;
		}
		if( in_ps_Targets < 0 )
			in_ps_Targets = index_in_list( ent, ps.defenseTargets ) >= 0;
		if( in_ps_Targets ) {										// or in player's defenseTargets
			isHostile = true;
			if( !set_all ) return true;
		}
		if( targeting_ps < 0 ) targeting_ps = ent_target === ps;
		if( alertCondition > YELLOW_ALERT && targeting_ps ) {		// target is hostile if targeting back during a fight
			isHostile = true;										// otherwise, he's just checking you over
			if( !set_all ) return true;
		}
		return isHostile < 0 ? false : isHostile;
	}
	function is_jamming( ent ) {
/*
http://oolite.aegidian.org/bb/viewtopic.php?f=4&t=3484#p35623
	The military jammer is a complement to the cloak, not a countermeasure. It makes a ship
	invisible to scanners, except to ships with a military scanner filter (who see it as a purple/orange flashing
	thing). has_military_scanner_filter seems to have fallen out of that list, it’s also a “fuzzy boolean”. The
	corresponding player equipment key is EQ_MILITARY_SCANNER_FILTER (and for has_military_jammer,
	EQ_MILITARY_JAMMER).
 */
		if( isJamming < 0 || !using_common_vars ) {
			isJamming = ent.isJamming || false;						// orbs lack a .isJamming prop
		}
		return !scanFilter_ok 										// player has working EQ_MILITARY_SCANNER_FILTER
				&& isJamming;
	}
	function is_cloaked( ent ) {
/*
OoRef _Ship.htm:
	isCloaked : Boolean (read/write)
	true if the ship has a cloaking device which is currently active false otherwise. If the ship
	has a cloaking device and sufficient energy to use it (energy > 0.75 * maxEnergy), you can
	activate it by setting isCloaked to true.
	isJamming : Boolean (read-only)
	true if the ship has a military scanner jammer which is currently active false otherwise.
 */
		if( isCloaked < 0 || !using_common_vars ) {
			isCloaked = ent.isCloaked;
		}
		return isCloaked;
	}
	function is_beacon( ent ) {
		if( isBeacon < 0 || !using_common_vars )
			isBeacon = ent.beaconCode || ent.isBeacon;
		return isBeacon;
	}
	function _has_good_status( ent, ent_status ) {
		if( status < 0 || !using_common_vars )						// ent_status optional, save a property get if already known
			status = ent_status || ent.status;
		if( status === 'STATUS_IN_FLIGHT'
				|| status === 'STATUS_ACTIVE'
				|| status === 'STATUS_EXITING_WITCHSPACE'
				|| status === 'STATUS_LAUNCHING' )
			return true;
		if( status === 'STATUS_EFFECT' ) {
			if( isWormhole < 0 || !using_common_vars )
				isWormhole = ent.isWormhole;
			if( isWormhole ) {
				if( collisionRadius < 0 || !using_common_vars )
					collisionRadius = ent.collisionRadius;
				if( collisionRadius > 0 )
					return true;
			}
		}
		return false;
	}
	function has_bad_status( ent, ent_status ) {
		try {
			return _has_bad_status( ent, ent_status );
		} catch( err ) {
			log( ws.name, ws._reportError( err, 'has_bad_status', [ ent, ent_status ] ) );
			if( debug ) throw err;
		}
	}
	function _has_bad_status( ent, ent_status ) {
		if( !ent || !ent.isValid ) return true;
		if( !using_common_vars || status < 0 )
			status = ent_status || ent.status;
		if( status === 'STATUS_ENTERING_WITCHSPACE'					// ship jumped; needed, as ship still around after this status is achieved
				|| status === 'STATUS_BEING_SCOOPED'
				|| status === 'STATUS_IN_HOLD'						// ditto, scooped entities stick around & will target you (slivers too!)
				|| status === 'STATUS_DOCKED'						//	 - also, if ps.target is set, it becomes null
				|| status === 'STATUS_DEAD' )
			return true;
		if( status === 'STATUS_EFFECT' ) {
			if( isWormhole < 0 || !using_common_vars )
				isWormhole = ent.isWormhole;
			if( isWormhole ) {
				if( collisionRadius < 0 || !using_common_vars )
					collisionRadius = ent.collisionRadius;
				if( collisionRadius === 0 )							// evaporated
					return true;
			}
		}
		return false;
	}
	function is_ignored_ship( ent ) {								// exclude from mapping marker, docked escorts & towed ship
		if( !ent ) return true;
		if( ent === curr_S.marker ) return true;					// is the 'telescopemarker'
		if( status < 0 || !using_common_vars ) status = ent.status;
		if( _has_bad_status( ent, status ) ) return true;
		if( Escortdeck ) {											// skip escorts if docked
			let index = index_in_list( ent, Escortdeck.$EscortDeckShip );
			if( index >= 0 && Escortdeck.$EscortDeckShipPos[ index ] ) {
				return true;										// ent is on deck so exclude it
			}
		}
		if( Towbar && ent === Towbar.$TowbarShip ) {
			return true;											//skip the towed ship
		}
		return false;
	}
	function read_scriptInfo( ent, map ) {							//read detection from the scriptInfo telescope entry & * set mass *
		if( !map || map.script_mass === undefined ) {
			let info = ent.scriptInfo;								// can give custom mass to the ship/station
			if( info && info.telescope ) {
				script_mass = parseInt( info.telescope, 10 );
			} else {
				script_mass = null;
				if( map ) map.script_mass = null;
				if( mass < 0 ) mass = ent.mass;
				return null;
			}
		} else {
			script_mass = map.script_mass;
		}
		// * Positive integer: give new mass to the ship in kg which can increase the gravity detection.
		// * Negative integer: will be substracted from the ship.mass in kg to reduce gravity detection.
		// * 0: detected within normal scanner only as without telescope.
		// * 1: detected in visible range only (due to gravity scanner can see a ship with 1kg mass from 2km only).
		if( script_mass > 0 ) {
			mass = script_mass;
			return script_mass;
		}
		if( mass < 0 ) mass = ent.mass;
		if( script_mass < 0 ) {										//substract the mass instead of overwrite it
			mass += script_mass;
		}
		return script_mass;
	}
	function getDetected( ent, restoring ) {						//read detection from the scriptInfo telescope entry; no need to
																	//	 check using_common_vars, as only called from notable_ent
		// a check for beacons is NOT done here for oxp's with ents
		// that are to remain hidden but insist on having beacons
		// - radio signals are pervasive except when ... \_O_/
		if( is_cloaked( ent ) ) return false;						// is_jamming ents can be seen, just not targetted
		if( distance < 0 ) distance = _detect_distanceTo( ent );
		if( scanClass === 'CLASS_CARGO'								// ent must 1st be detected w/i scannerRange
				&& (!mk_maps || restoring) ) {						// no grow_hidden_scanned array in update cycle
			let index = _Sighting_index( ent );						// need to test RFID range
			let limit = scannerRange;								// newly discovered must be inside scannerRange
			limit += !restoring && index < 0 ? 0					//	 updating & not in mapping
						: randomInt( 0, scannerRange >> 1 );		// while known ones detectable by RFID
			if( distance > limit )									// RFID not yet detected or lost to background noise
				return false;										//	 (telescope DB garbage collects data on lost RFID, so must re-aquire)
		}
		if( script_mass === undefined )								// has never been read
			script_mass = read_scriptInfo( ent );					//can give custom detection distance to the target ship
		if( script_mass === 0 )										// script_mass = 0: detected within normal scanner only as without telescope.
			return distance < scannerRange;							//	=> am assuming they're not to be detected beyond scannerRange, else mk work like cargo
			// "once detected then tracked over scanner range while in visible range until next scan (small help and save performance)"
		if( script_mass === 1 ) {									// script_mass = 1: detected in visible range only
			if( isVisible < 0 ) isVisible = ent.isVisible;
			if( !isVisible || (distance > scannerRange && !ext_ok) )
				return false;
		}
		if( script_mass === null ) {								// old code has any script_mass precluding station test
			if( isStation < 0 ) isStation = ent.isStation;
			if( isStation ) {										//hide custom station over 4x scanner range
				if( primaryRole < 0 ) primaryRole = ent.primaryRole;// 9 compares profiles twice as fast as using index_in_list
				if( primaryRole && primaryRole !== 'station' && primaryRole !== 'coriolis'	   && primaryRole !== 'dodo'
								&& primaryRole !== 'dodec'	 && primaryRole !== 'dodecahedron' && primaryRole !== 'ico'
								&& primaryRole !== 'icosa'	 && primaryRole !== 'icosahedron'  && primaryRole.substring(0, 10) !== 'rockhermit' )
					// rockhermit role can be "rockhermit", "rockhermit-chaotic", "rockhermit-pirate" and more in the future?
					if( distance > scannerRange_X_4 ) return false;
			}
		}
		//stealth ships and non-standard stations detected in normal scanner range only, requested by Svengali
		if( dataKey < 0 ) dataKey = ent.dataKey;
		if( dataKey ) {
			if( dataKey === 'vector_arn' || dataKey.indexOf( 'stealth' ) >= 0 ) {
				if( distance > scannerRange ) return false;			//mission ship in Vector OXP
			}
		}
		if( primaryRole < 0 ) primaryRole = ent.primaryRole;
		if( primaryRole ) {
			if( primaryRole.indexOf( 'stealth' ) >= 0 || primaryRole.indexOf( 'rescue_blackbox' ) >= 0 ) {
				if( distance > scannerRange ) return false;			//mission ships in Rescue Stations OXP
			}
		}
		return true;
	}
	// Sighting distance calculations /////////////////////////////////////////////////////////////
	function hullOffset( ent ) {
		var offset = 0;
		if( radius < 0 || !using_common_vars )
			radius = ent.radius || false;
		if( radius ) {												// distance to near surface
			offset = radius;
		} else {													// distance to near (hull) surface
			if( collisionRadius < 0 || !using_common_vars )
				collisionRadius = ent.collisionRadius;
			offset = collisionRadius;
		}
		return offset;
	}
	// dist for near vs. far targets is when .distanceTo === scannerRange, regardless of any radius/collisionRadius
	// - core crosshair shows .distanceTo less cr of target
	// => marker should read _detect_distanceTo, ie. position.distanceTo - ent.collisionRadius
	function _detect_distanceTo( ent ) {							// to x; distanceTo gives distance to centers, not hulls
		try {
			/// some of this function is duplicated in _reposition_effects for speed
			var that = _detect_distanceTo;
			var distanceTo = (that.distanceTo = that.distanceTo || []);
			distanceTo.length = 0;
			if( (ps_position && ps_position.length === 0) || !using_common_vars ) {	// set every frame
				copy_vector( ps.position, ps_position );
			}
			subtract_vectors( ps_position, ent.position, distanceTo );
			var distTo = vector_magnitude( distanceTo );			// dist from ship's hull to x's center; NB: core crosshairs give hull to hull
			distTo -= hullOffset( ent );							// distance to near surface
			return distTo < 0 ? 0 : distTo;
		} catch( err ) {
			log( ws.name, ', ent.position: ' + ent.position + ', distTo: ' + distTo
				+ ', radius: ' + radius + ', collisionRadius: ' + collisionRadius );
			ws._reportError( err, _detect_distanceTo, ent );
		}
	}
	function grav_scan_dist( ent, rtn_curr, map ) {					// return gravity scan distance for ent
		if( radius < 0 ) radius = ent.radius || false;
		if( radius )	 return -1;									// ignore planets, moons & sun
		if( script_mass === undefined )								// will set 'mass' variable if required, ie. calling read_scriptInfo() sets both
			script_mass = read_scriptInfo( ent, map );				//	  mass & script_mass, if had not already been called (if script_mass === undefined )
		if( mass === 0 ) return -1;									// ignore wormholes
		var dist;
		if( !rtn_curr || gravScanProgress === 1 ) {					// return max. gravity scan detection distance
			if( !map ) {											// "mass of the target in kg must be larger than d2*d2*d2/100 where d2 = distance*2 in km"
				dist = pow( 100 * mass, 1/3 )						//	 invert 'mass > d2*d2*d2/100' => 'd2 < cube root(mass * 100)'
					   * gs_mult * 500;								//	 '* 500' to cnv to meters: 'd2 = distance*2 in km' => 'distance = (1000 * d2)/2'
				return dist;
			}
			return map.gs_max_dist;									// skip calc if poss. (when updating)
		} else if( gravScanProgress > 0 && gravScanProgress < 1 ) { // grav. scan progress varies by mass, so distance varies as the cube root of mass,
			dist = pow( 100 * gravScanProgress * mass, 1/3 )		//	 thus the 2nd call of grav_scan_dist (can't just scale, ie. use gravScanProgress * max)
				   * gs_mult * 500;
			return dist;
		}
		return 0; // because gravScanProgress === 0
	}
	// Sighting creation & recycling pool /////////////////////////////////////////////////////////
	var used_Sightings = [];
	function free_Sighting( map ) {									// attempt to reduce garbage collection by managing used objects
		if( !map ) return;
		// scrub old data
		// these 3 set in init_Sighting
		map.ent = null;
		map.last_posn.length = 0;
		map.entityPersonality = -1;									// unique ID# for spreading updates across frames
																	//	 and generating random detection distance for cargos
		// these 11 set in mk_Sighting
		map.rank = -1;												// category used for sorting
		map.ent_dist = -1;											// distance to entity measured by whatever equipment is installed
		map.gs_curr_dist = -1;										// distance grows as grav. scan progresses
		map.gs_max_dist = -1;										// max. grav. scan distance, calc'd on creation
		map.script_mass = undefined;								// save scriptInfo so read_scriptInfo() only called once/ent (don't
																	//	 init to null, as set null when we know there's no scriptInfo
																	//	  => needs to be init'd as undefined
		map.dynamicMFD = 0;											// MFD flags for dynamic properties
		map.staticMFD = 0;											// MFD flags for static properties
		map.headingTo = 180;										// in degrees, offset from player's vectorForward; init behind so not immediately found
		map.ve_colour = '';											// visualEffect colour
		map.hasJammer = false;										// if true, name is not cached as different if on/off
		map.ml_radius = 0;											// for support of VariableMassLock
		map.have_scanned = false;									// for support of scriptInfo = { telescope = 0 }; => set to true
																	//	 also used for cargo RFID => set to a detection range > 0
																	//	 and FarStatus => set -1 when come w/i scannerRange
		// these may be set in update_one_Sighting et al
		map.lb_size = '';											// lightball size
		map.ml_size = '';											// masslock ring size
		let effect = map.lightball;									// visualEffect ref. if any
		if( effect ) {
			effect.remove();
			map.lightball = null;
		}
		effect = map.masslock;										// visualEffect ref. if any
		if( effect ) {
			effect.remove();
			map.masslock = null;
		}
		// toss into recycle bin
		used_Sightings.push( map );
		if( used_Sightings.length >= 300 ) {						  // ?build up over time
			used_Sightings.length = 50;
if( debug ) log(ws.name, 'free_Sighting, pool EXCEEDED 300, reduced to 50' );
		}
	}
	function alloc_Sighting() {										// attempt to reduce garbage collection by managing used objects
		if( used_Sightings.length > 0 ) {							// re-use old map
			return used_Sightings.pop();
		}
		return {};
	}
	function mkSighting( ent ) { // assumes 'rank' has been set before calling!
		var map = alloc_Sighting();
		map.ent = ent;
		if( position.length === 0 )								   // not already set
			copy_vector( ent.position, position );
		if( !map.last_posn ) map.last_posn = alloc_array();
		copy_vector( position, map.last_posn );						// position @ time of last scan/update -> $ListPos
		if( scanClass === 'CLASS_NO_DRAW' ) {						// celestial objects have no personality; [32768, 49151]
			map.entityPersonality = 32768 + floor((position[0] + position[1] + position[2]) % 16384);
		} else if( scanClass === 'CLASS_WORMHOLE' ) {				// don't use collisionRadius, as it varies; [49152, 65535]
			let spawnTime = ent.spawnTime;
			spawnTime -= floor(spawnTime);							// fractional part only; [0, 1]
			map.entityPersonality = 49152 + floor(spawnTime * 16384);
		} else {													// normal entities limited to [0, 32767]
			map.entityPersonality = ent.entityPersonality;
		}
		map.rank = rank;											// category used in sorting
		if( distance < 0 ) distance = _detect_distanceTo( ent );
		map.ent_dist = distance;
		if( gs_curr < 0 ) gs_curr = grav_scan_dist( ent, true );
		map.gs_curr_dist = gs_curr;
		if( gs_max < 0 ) gs_max = grav_scan_dist( ent );
		map.gs_max_dist = gs_max;
		if( script_mass === undefined )
			script_mass = read_scriptInfo( ent );					// also sets 'mass'
		map.script_mass = script_mass;
		map.dynamicMFD = dynamicMFD;
		map.staticMFD = staticMFD;
		map.headingTo = 180;
		if( radius < 0 ) radius = ent.radius || false;				// ents available to be swapped out for closer ones
		map.swapable = !radius && !is_beacon( ent );				//   are those that are not orbs and not beacons
		is_jamming( ent );											// sets isJamming; return includes scanFilter_ok which we ignore here
		map.hasJammer = isJamming;									// never gets set false, need to know ent has one, not if it's turned on
																	// - used to bypass naming cache, as becomes unknown-ship if it's on
		if( isWormhole < 0 ) isWormhole = ent.isWormhole;
		if( isWormhole && !ent.name ) ent.name = 'wormhole';
		if( ve_colour !== -1 ) map.ve_colour = ve_colour;
		if( VariableMassLock ) {
			let ml_radius = VariableMassLock.$Range( mass );		// mass * 0.02 + 17000 //small masslock radius of this ship
			// VariableMassLock scales masslock radius between	Adder (16 t) = 17 km -> Anaconda (438 t) = 26 km
			// - doesn't enforce an upper bound, so ships heavier than Anaconda will have even larger radius!
			//	 - he's using checkScanner, so never deals w/ ships beyond scannerRange
			map.ml_radius = ml_radius > scannerRange ? scannerRange // upper limit on ring size
													 : ml_radius;
		}
		map.have_scanned = false;
		if( script_mass === 0 ) {									// ents w/ scriptInfo telescope = 0 use .have_scanned
			map.have_scanned = true;								//	 to be remembered beyond scannerRange once detected within
			// for entities; with scriptInfo.telescope=0, entities are hidden until inside scanner
			// range, "but once detected then tracked over scanner range while in visible range until next scan"
		} else if( is_cargo === true ) {
			map.have_scanned = scannerRange + (map.entityPersonality >> 1);// pod's RFID range
			// .have_scanned used for cargo's extended range (RFID tracking) once it's entered scannerRange
		}
		return map;
	}
	// Sighting functions /////////////////////////////////////////////////////////////////////////
	function Sighting_index( ent ) {
		try {
			return _Sighting_index( ent );
		} catch( err ) {
			log( ws.name, ws._reportError( err, 'Sighting_index', ent ) );
			if( debug ) throw err;
		}
	}
	function _Sighting_index( ent /*,caller*/ ) {					// ent can be either a Sighting or entity
		if( !equip_ok ) return -1;
		if( !ent ) return -1;
		if( mapping === null ) return -1;							// mapping not yet initialized or empty list
		if( ent.ent_dist )											// ent is a Sighting
			return index_in_list( ent, mapping );
		var target = ent === curr_S.marker ? curr_S.ent : ent;
		if( !target || !target.isValid ) return -1;					// target died
		for( let idx = 0; idx < maplen; idx++ ) {
			let map = mapping[ idx ];
			if( target === map.ent ) return idx;
		}
		return -1;
	}
	function set_curr_Sighting( ent, caller ) {
		try {
			_set_curr_Sighting( ent, caller );
		} catch( err ) {
			log( ws.name, ws._reportError( err, 'set_curr_Sighting', ent ) );
			if( debug ) throw err;
		}
	}
	function _set_curr_Sighting( ent /*,caller */ ) {				// ent can be an entity or map to be located
		if( ent === undefined || ent === null ) {					// no parm is signal to reset
			curr_target = curr_S.ent = curr_S.map = null;
			curr_S.index = -1;
			if( curr_S.marker ) removeMarker();						// cannot hang onto it for next target, as still see lollipop
			curr_S.name = '';
			_clear_HUD_Effects();									// clear model, ring & sniper ring
			ws.$IdentKeyPress = identKeyPress = IDENT_READY;		// ensure it's reset
			TelescopeList[ 0 ] = null;
			ws.$TelescopeListi = 0;									// $TelescopeListi = 0 => not in $TelescopeList
// if( debug ) log('_set_curr_Sighting, curr_S cleared by ' + caller);
			return;
		}
		var index = _Sighting_index( ent, '_set_curr_Sighting' );
		if( index < 0 || index >= maplen ) {
			_set_curr_Sighting( null, '_set_curr_Sighting' );		// recurse to reset
			return;
		}
		var map = mapping[ index ];
		curr_S.map = map;
		curr_target = curr_S.ent = map.ent;
		curr_S.index = index;
		// curr_S.marker, curr_S.marker_type remain unchanged unless explicity changed by marker code
		if( curr_S.marker ) {										// oxp's can retrieve telescope target via marker.target
			curr_S.marker.$TelescopeTarget = curr_target;
		}
		TelescopeList[ 0 ] = curr_target;							// for oxp support, $TelescopeList is always an array of 1 entity
		ws.$TelescopeListi = 1;										// and $TelescopeListi is 1 if have a target, 0 otherwise
// if( debug ) log('_set_curr_Sighting, (IdentKeyPress='+identKeyPress +') curr_S set to '
				// + (map.name ? map.name : map.ent.displayName || map.ent.name) + ' by ' + caller);
	}
	function farthestToSwap( chkRank, chkDist ) {					// return map for a ship to swap out of mapping
		var maxDist = 0,
			farthest = null,
			secondBest = null,
			psTarget = curr_S.map;
		var saved_isBeacon = isBeacon,								// preserve so don't repeat property get
			was_using_common_vars = using_common_vars;				// (beaconCode is not a common var)
			isBeacon = -1;
			using_common_vars = false;
		for( let idx = 0; idx < maplen; idx++ ) {
			let map = mapping[ idx ];
			if( map === psTarget ) continue;						// is player's target
			if( map.rank < chkRank ) continue;						// cannot swap more important entity
			// - can use string compare as ranks are named to be alphabetically increasing
			if( !map.swapable ) continue;							// orbs and beacons are always maintained
																	// in mapping, ie. not available for swap
			let map_dist = map.ent_dist;
			if( map_dist < chkDist ) continue;						// cannot swap closer entity
			if( map_dist > maxDist ) {
				maxDist = map_dist;
				if( farthest )
					secondBest = farthest;							// also a candidate for deletion
				farthest = idx;
			}
		}
		isBeacon = saved_isBeacon;
		using_common_vars = was_using_common_vars;
		if( secondBest !== null ) {
			if( secondBest < farthest ) {
				farthest--;
			}
			_delete_Sighting( secondBest );
		}
		return farthest;
	}
	function numberSwapable() {
		var swapable = 0;
		for( let idx = 0, len = mapping.length; idx < len; idx++ ) {
			if( mapping[idx].swapable ) swapable++;
		}
		return swapable;
	}
	function add_Sighting( ent, is_notable, forced ) {
		try {
			return _add_Sighting( ent, is_notable, forced );
		} catch( err ) {
			log( ws.name, ws._reportError( err, 'add_Sighting', [ ent, is_notable ] ) );
			if( debug ) throw err;
		}
	}
	function _add_Sighting( ent, is_notable, forced /*, caller */ ) {
		if( !equip_ok ) return;
		if( !mappingReady ) return -1;								// launching/exiting witchspace & haven't made 1st _Scan()
		if( !ent || !ent.isValid ) return -3;						// ship died, docked or jumped
		if( !ps || !ps.isValid || alertCondition === DOCKED ) 		//if player died or docked
			return -4;
		if( _Sighting_index( ent, '_add_Sighting' ) >= 0 ) {		// already in mapping
			return -5;
		}
		let now = clock.absoluteSeconds;
		let spawned = ent.spawnTime;
		if( spawned > 0 && now - spawned < SPAWN_DELAY ) 			// too new; will be picked up as a new target
			return -10;
		scanClass = ent.scanClass;
		radius = ent.radius || false;
		if( scanClass === 'CLASS_NO_DRAW' && !radius ) {			// not an orb, probably wreckage
			return -9;
		}
		status = ent.status;
		if( !_has_good_status( ent, status ) ) {
			return -6;
		}
		if( !is_notable ) {											// else was done already in check_if_new_targets
			let save_status = status,
				save_scanClass = scanClass,
				save_radius = radius;								// preserve so don't repeat property get
			reset_common_vars();
			status = save_status;
			scanClass = save_scanClass;
			radius = save_radius;
			if( !notable_ent( ent ) ) {								// sets rank, ve_colour & (maybe) distance
				using_common_vars = false;
				return -7;
			}
		}
		if( rank === 'ukn' ) {										// must be detected before it can become lost
			return -8;												//  - rank may be set by caller
		}
		if( distance < 0 ) distance = _detect_distanceTo( ent );	// needed if call farthestToSwap (used in mkSighting)
		let swapable = numberSwapable();							// orbs & beacons excluded from MaxTargets
		if( !forced
				&& alertCondition !== RED_ALERT /// until someone complains, exclude RED_ALERT from MaxTargets restriction
				&& swapable >= MaxTargets ) {						// mapping is full, swap if ent is closer or a priority
			let swapIdx = farthestToSwap( rank, distance );
			if( swapIdx ) {											// found one futher out from ent
				free_Sighting( popArrayItem( mapping, swapIdx ) );
				maplen = mapping.length;
			} else {
				return swapable > 0 ? -8 : -2;
				// -2 used for 'memory full' message; -8 => something else stopped the insert
			}
		}
		found_new = true;
		var map = mkSighting( ent );
		mapping.push( map );
		maplen++;
		var insert_i = maplen - 1;
		update_one_Sighting( map, ent, insert_i );
		if( curr_target === ent && !profiling ) {
			_manage_marker( map, false, '_add_Sighting' );
		}
		if( !is_notable ) {
			using_common_vars = false;
		}
		return insert_i;
	}
	function delete_Sighting( ent, caller ) {
		try {
			_delete_Sighting( ent, caller );
		} catch( err ) {
			log( ws.name, ws._reportError( err, 'delete_Sighting' ) );
			if( debug ) throw err;
		}
	}
	function _delete_Sighting( ent /*, caller */) {					// ent can be an ent, Sighting or index (faster)
		if( !equip_ok ) return;
		if( !mappingReady || maplen === 0 ) return;					// mapping not yet initialized OR it's empty
		if( ent === null && ent === undefined ) return;
		var index = typeof( ent ) === 'number' ? ent : null;
		if( index !== null && (index < 0 || index >= maplen ) ) return;
		var found = -1, map = null;
		if( index !== null ) {										// were passed an index, will ignore ent arg
			found = index;
			map = mapping[ found ];
		} else if( ent.ent !== undefined ) {						// were passed a map
			map = ent;
			found = index_in_list( map, mapping );
			if( found < 0 ) {
				if( map === curr_S.map ) {
					_set_curr_Sighting( null, '_delete_Sighting' );
				}
				return;
			}
		} else {													// were passed an ent
			found = _Sighting_index( ent, '_delete_Sighting' );
			if( found < 0 ) {
				return;
			}
			map = mapping[ found ];
		}
		if( map === curr_S.map ) {									// is player's target
			_set_curr_Sighting( null, '_delete_Sighting2' );		// no parms resets $curr_Sighting
		}
		free_Sighting( popArrayItem( mapping, found ) );
		maplen = mapping.length;
	}
	const RANK_STRS = [ 'bad', 'isr', 'loc', 'mng', 'nsr', 'orb', 'ukn' ];
	// names for grouping hostiles(bad), ships in scannerRange(isr), stations & cargo(loc), mining stuff(mng),
	//					  far targets(nsr), sun/planet/moon(orb), lost targets(ukn)
	function select_Sightings( count, rank, compare ) {				// 'count' (0 => all); 'rank' (0 => all) & 'compare' are optional but need at least 1
																	//	 'rank' limits search to that group; 'compare' is a boolean function applied to each
																	// NB: return null or static array selected_Sightings
		if( !equip_ok ) return null;
		if( !mappingReady || maplen === 0 ) return null;			// launching/exiting witchspace & haven't made 1st mapping OR it's empty
		if( count === undefined ) return null;						// no point selecting entire list! (can be zero)
		do {
			if( compare ) break;
			if( rank === 0 ) break;									// all ranks
			if( index_in_list( rank, RANK_STRS ) >= 0 ) break;		// a valid rank
			return null;											// have to specify at least compare fn or some rank
		} while( false );
		selected_Sightings.length = 0;								// remove any previous results
		var num = count ? count : maplen;							// if no count, default to all
		for( let idx = 0; idx < maplen; idx++ ) {
			let map = mapping[ idx ];
			if( rank && rank !== map.rank ) continue;
			if( compare && !compare( map ) ) continue;
			selected_Sightings.push( map );
			num--;
			if( num <= 0 ) break;
		}
		return selected_Sightings;
	}
	function check_Sightings( parm ) {								// loop through Sightings & delete those no longer valid
		var that = check_Sightings;									// NB: neither update can delete ents that become !notable (expensive)
		if( that.blocked === undefined ) that.blocked = 0;			//	   so that must (eventually) happen here
		if( that.adjusted === undefined ) that.adjusted = 0;
		var adjusted = that.adjusted,
			blocked = that.blocked;
		if( !equip_ok ) return;
		if( !mappingReady || maplen === 0 ) return;					// mapping not yet initialized OR empty
		if( fns_are_pending() && parm === false ) {					// called in midst of creating a new mapping, abort!
			blocked = that.blocked = blocked + 1;					// count blocked full checks (can get blocked by update on slow PCs)
			if( blocked < 3 ) return;								// each called once/second, prevent blocks longer than 2 sec
		}															//	 wait for full, not quick check to unblock
		if( parm === false ) that.blocked = 0;						// checking, so reset counter
		var starting, index, quickly, fps, del;
		if( parm === true ) {
			quickly = that.quickly = true;
			fps = that.fps = current_fps ? current_fps() : -1;		// quickly is fast, so check fps/frame
			if( fps < 0 ) fps = that.fps = 30;						//	 current_fps returns -1 until 1st min. has passed
			starting = index = maplen;
		} else if( parm === false || parm === undefined ) {
			quickly = that.quickly = false;
			fps = current_fps ? current_fps() : -1;
			if( fps < 0 ) fps = 30;									//	  current_fps returns -1 until 1st min. has passed
			fps = that.fps = floor(fps / (5 - adjusted));			// store as fn prop for next frames' execution
			starting = index = maplen;								//	 - #/frame scales w/ framerate; override increases #/frame to reduce discarded calls
		} else {													// parm is an index # to resume
			quickly = that.quickly || true;
			fps = that.fps || 6;
			starting = maplen;
			index = parm;
		}
		using_common_vars = !quickly;
		while( index-- ) {											// work backwards thus list, so indices are simple
			let map = mapping[ index ];
			if( !map ) continue;
			if( index > 0 && index % fps === 0 ) {					// checking list can take more time than we'd like in a frame
				set_fn_pending( check_Sightings, index );			//	 @ 43 fps, 0.33 ms/Sighting; fps/5 = 8 => 2.67 ms
				return;												// so do a chunk each frame, its size a fn of fps
			}
			let ent = map.ent;
			if( quickly ) {
				scanClass = collisionRadius = isVisible = -1;
			} else {
				reset_common_vars();
			}
			isWormhole = ent ? ent.isWormhole : false;
			if( isWormhole ) _handle_wormhole( ent );				// keep an eye on clock to annouce destination
			del = true;
			do {
				if( !ent || !ent.isValid ) break;					// ship destroyed or wormhole expired
				status = ent.status;
				if( _has_bad_status( ent, status ) ) break;
				distance = map.ent_dist;							// needs to be set for notable_ent
				if( distance < scannerRange && is_cloaked( ent ) )	// range check limits calls to is_cloaked
					break;
				if( scanClass < 0 ) scanClass = ent.scanClass;
				if( scanClass === 'CLASS_CARGO' && is_ignored_ship( ent ) ) break;
				if( collisionRadius < 0 ) collisionRadius = ent.collisionRadius;
				if( isWormhole && collisionRadius === 0 ) break;	// wormhole expired
				let have_scanned = map.have_scanned;
				if( have_scanned === true && !ent.isVisible ) break;// lose sight of hidden (scriptInfo{ telescope= 0;})
				if( typeof have_scanned === 'number' && have_scanned !== -1
						&& distance > have_scanned )				// lose cargo RFID in background noise
					break;
				if( quickly ) {										// restrict check to the above for speed
					del = false;
					break;
				}
				if( notable_ent( ent, false, distance ) ) del = false;
			} while( false );
			if( del ) {
// if( debug ) log('check_Sightings, deleting (' + ent.entityPersonality + '): ' + ent.name );
				_delete_Sighting( index, 'check_Sightings' + (quickly ? ' -quickly' :'') ); // not ok, remove
			}
		}
		if( !quickly ) using_common_vars = false;
// if( debug && starting !== maplen ) {
// 	log('check_Sightings, started w/ ' + starting + ', ended w/ ' + maplen );
//		}
	}
	// user Sighting functions ////////////////////////////////////////////////////////////////////
	function chg_curr_Sighting( step ) {
		try {
			_chg_curr_Sighting( step );
		} catch( err ) {
			log( ws.name, ws._reportError( err, 'chg_curr_Sighting', step ) );
			if( debug ) throw err;
		}
	}
///
	function _chg_curr_Sighting( step ) {							// incr or decr used when user steps through the Sightings
		var that = _chg_curr_Sighting;
		if( that.last_curr_S === undefined ) that.last_curr_S = null;// to remember across calls
		var last_curr_S = that.last_curr_S;
		if( !mappingReady || maplen === 0 ) return;					// map not initialized OR empty
		check_Sightings( true );									// clear out any that died, jumped or scooped
		var map, ent, index, skip_scan = false;
		mapping.sort( map_sort_dist );								// match sort of mfd
		// mapping.sort( map_sort_rank_dist );
///??with filtering, must remember what's in mfd(s) and follow that
/// vs leave as in 1.15, stepping thru all
///?if !separate && !filtering, scroll primary MFD
///?elif !filtering, jump to aux & scroll it
		if( last_curr_S ) {											// resuming after we forced a rescan by wrapping end of list
			index = _Sighting_index( last_curr_S, '_chg_curr_Sighting' );
			last_curr_S = that.last_curr_S = null;
			skip_scan = true;
		} else {
			index = _Sighting_index( curr_S.map, '_chg_curr_Sighting' );
		}
		if( index < 0 ) {											// no target, start @ list end
			index = step > 0 ? maplen - 1 : 0;
			skip_scan = true;
		}
		map = index >= 0 && index < maplen ? mapping[ index ] : null;
		if( !map ) {												// no target, default to start of list (may have been removed by rescan)
			_set_curr_Sighting( maplen > 0 ? mapping[ 0 ] : null, '_chg_curr_Sighting' );
			last_curr_S = that.last_curr_S = null;					// ensure it's cleared before premature exit
			return;
		}
		do {
			index += step;
			if( index >= maplen || index < 0 ) {					// past an end of the mapping
				if( !skip_scan ) {									// so create a new one
					last_curr_S = that.last_curr_S = map.ent;		// remember which ent to resume at
					_auto_updates( true );							// true initiates create (which wipes deferred tasks), suppresses call to _manage_marker in _auto_updates
					set_fn_pending( _chg_curr_Sighting, step, true );// true sets a 'deferred' task for after update
					return;
				} else {											// back from creating a new one
					index = index < 0 ? maplen - 1 : 0;
				}
			}
			map = mapping[ index ];
			ent = map.ent;
		} while( ( ent && ent.isWormhole							// after wormhole scanner started, they become normal Sightings but
					&& ent.$TelescopeScanStart === undefined ) );	//	 not until manually targetted (ie. 'r' button), req'd by core
		_manage_marker( map, map.ent_dist > scannerRange,			// distance test prevents double msgs on near targets
						'_chg_curr_Sighting' );						//	 ours & ident msg (still get double @ transition, on telescope marker)
		if( Steering === 2 ) {
		//  ||( Steering === 1 && ent === findNearestEnt() ) ?is player expecting to steer to nearest when stepping through list
			start_Steering();										//turn to the target
		}
		// suspend _mostCentered while user is stepping
		// - there is no event to tell us player has stopped stepping, so a time limit is used
		// - delay_counter is set to double that of IdentDelay, which gets reset when no longer
		//   stepping
		// - mode() calls _resetIdentDelay if player switches to a different function
		// (all activated() does is call this fn)
		// NB: if weaponsOnline, the stepped ent remains current target (as _mostCentered not called unless have no target)
		//     else navigation mode reasserts itself using GravLock
		delay_counter = IdentDelay * 2;								// suspend mostCentered for twice IdentDelay
		ws.$IdentKeyPress = identKeyPress = IDENT_STEP_DELAY;		// this starts IdentDelay counter once steering complete
	}
	function targeting_player( map ) {
		// called from find_most_central & _nearest_Sighting (via select_Sightings) only in RED_ALERT
		// checks needed, as eg. scooped splinters/cargo targets player
		var ent = map.ent;
		var target = ent.target;
		if( target !== ps ) 				return false;
		if( !ent.isShip && !ent.isStation ) return false;
		if( ent.isDerelict ) 				return false;			// don't lock on derelicts when in combat (dybal)
		if( weaponsOnline && TargetOnlyHostile) {					// limit to hostile targetters (no cargo, pods, etc.)
/// mk an option so player can toggle this fn, default false (like prev. ver.s)
			let weap = target.currentWeapon;
			if( !weap || weap.equipmentKey === 'EQ_WEAPON_NONE' )
				return false;
			return true;
		}
		return true;
	}
	function findNearestEnt() {
		var list = mapping,
			len = maplen;
		if( alertCondition > YELLOW_ALERT && weaponsOnline ) {
			//in Red Alert lock the last attacker if any and weapons are online
			list = select_Sightings( 0, 0, targeting_player );		// first, target those targeting player; 0 => all, 0 => any rank
			len = list && list.length;
			if( !len ) {
				list = select_Sightings( 0, 'bad' );				// none, try any hostiles; 0 => all
				len = list && list.length;
			}
			if( !len ) {
				list = mapping;										// none found, search entire mapping
				len = maplen;
			}
		}
		var map, ent;
		var min_dist = MaxRange;
		var closest = null;
		while( len-- ) {
			map = list[ len ];
			ent = map.ent;
			if( ent && ent.isValid ) {
				let dist = map.ent_dist;
				if( dist < min_dist ) {
					closest = map;
					min_dist = dist;
				}
			}
		}
		return closest;
	}
	function nearest_Sighting() {
		try {
			_nearest_Sighting();
		} catch( err ) {
			log( ws.name, ws._reportError( err, 'nearest_Sighting' ) );
			if( debug ) throw err;
		}
	}
	function _nearest_Sighting() {									//lock the nearest target
		if( !ps || !ps.isValid || alertCondition === DOCKED )
			return;
		if( !mappingReady || maplen === 0 ) return;					// not yet built OR empty
		check_Sightings( true );									// clear out any that died, jumped or scooped
		var closest = findNearestEnt();
		if( closest ) {
			_manage_marker( closest, true, '_nearest_Sighting' );
			if( Steering > 0 ) start_Steering();					//turn to the target
		}
	}
	// Sighting updating - lightball //////////////////////////////////////////////////////////////
	function add_lt_ball( map, index ) {							// in order to appear on scanner, light ball effects are placed just inside
																	//	 scannerRange, not @ target, as with masslock rings
																	// also called via add_pending_lightballs so DO need to check !using_common_vars
		var lightball = map.lightball;
		if( lightball && !profiling ) {
			lightball.remove();
			lightball = map.lightball = null;
		}
		if( ve_colour < 0 ) ve_colour = map.ve_colour;
		if( lb_size < 0 ) lb_size = map.lb_size;
		if( !ve_colour || !lb_size ) return;						// must have both
		var ent = map.ent;
		if( scanClass < 0 || !using_common_vars ) scanClass = ent.scanClass;
		if( isFrangible < 0 || !using_common_vars ) isFrangible = ent.isFrangible;
		if( scanClass === 'CLASS_ROCK' && isFrangible ) return;		// Rock Hermits, even abandoned ones, but not asteroids, boulders
		var effect_key = 'telescope-' + ve_colour + lb_size;
		lb_position( map, index );
		if( profiling ) return; // else profiler goes BOOM!
		lightball = map.lightball = addVisualEffect( effect_key, lightball_posn );
		if( debug && !lightball )
			log(ws.name, 'add_lt_ball, add effect failed! effect_key = ' + effect_key + ', lightball_posn = ' + lightball_posn );
	}
	var target_direction = [];										// unit vector to target
	var lightball_posn = [];
	function lb_position( map, index, haveTD ) {					// calc lightball_posn; haveTD == true => target_direction already calc'd
		// is also called via add_pending_lightballs, so DO need to check !using_common_vars
		var lb_dist;
		if( distance < 0 || !using_common_vars )
			distance = map.ent_dist;								//distance to the target (or to last known pos)
		// light balls for far ships occupy ring inside scannerRange: -400..-600 (400+max_sightings)
		// - target marker lies at scannerRange - 600
		// while ships inside scannerRange have light balls outsice scannerRange: +300..+500 (300+max_sightings)
		if( distance > scannerRange ) {
			lb_dist = scannerRange - 400;
			//do not set closer to scannerRange so won't leave behind aft markers during torus travel
		} else {													// near ent, core supplies lollipop
			lb_dist = scannerRange + 400;							//show the lightball only without shadow lollipop
		}
		lb_dist -= index;
		// using index to prevent 2 light balls having same distance, so they don't interfere with each other when coincide in line of sight
		if( rank < 0 || !using_common_vars ) rank = map.rank;
		if( position.length === 0 || !using_common_vars )
			copy_vector( map.last_posn, position );					// if 'ukn', ball shouldn't move
		if( !haveTD ) {
			if( ent_vector.length === 0 || !using_common_vars ) {
				subtract_vectors( position, ps_position, ent_vector );
			}
			unit_vector( ent_vector, target_direction );			// unit vector to target (or to last known pos)
		} // else calling fn has already calc'd target_direction
		scale_vector( target_direction, lb_dist, vector );
		add_vectors( ps_position, vector, lightball_posn );
		if( moving_fast ) {
			apply_speed_adj( lightball_posn );
		}
	}
	function a_non_ship_colour( colour ) {
		// "non-ships with Blue, Cyan, Gray, Green and White colours will remain to help find these."
		return colour === 'green' || colour === 'white' || colour === 'cyan'
			|| colour === 'blue'  || colour === 'gray'	|| colour === 'lightgray';
	}
	function showing_lightball( ent ) {								// separate function to eliminate blinking lollipop
		let showing = true;
		do {
			if( !weaponsOnline ) break;								// show all
			if( curr_target === ent ) break;						// black lollipops except the current target
			if( collisionRadius < 0 || !using_common_vars )
				collisionRadius = ent.collisionRadius;
			let max = RedAlertDist > SniperRange + collisionRadius
					  ? RedAlertDist : SniperRange + collisionRadius;
			if( distance < max ) break;								// flag color relies on alertCondition for gray, black
			if( alertCondition < RED_ALERT ) {						// in Green or Yellow alert, weaponsOnline
				if( scanClass < 0 || !using_common_vars )
					scanClass = ent.scanClass;
				if( scanClass === 'CLASS_CARGO' ) break;			// RFID tag (are deleted when go out of range)
				if( is_beacon( ent ) ) break;
			}
			if( alertCondition < YELLOW_ALERT ) {					// in Green alert, weaponsOnline
				if( isVisible < 0 || !using_common_vars )
					isVisible = ent.isVisible;
				showing = isVisible;								// gravity scanner off-line, so visible only
			} else {
				showing = false;									// in Red alert, weaponsOnline & beyond RedAlertDist
			}
		} while( false );
		return showing;
	}
	var black_color = [ 0, 0, 0 ],
		darkgray_color = [ 0.1, 0.1, 0.1 ],
		lightgray_color = [ 2/3, 2/3, 2/3 ];
	function lb_showing_colour( ent, showing ) {					// separate function to eliminate blinking lollipop
		let newColor = null;										//original colour from effectdata.plist
		if( curr_target === ent ) {									// avoid flicker w/ shadow by setting same color
			newColor = lightgray_color;
		} else if( !showing ) {
			if( alertCondition > YELLOW_ALERT )
				newColor = black_color;								//black lollipops in red alert
			else if( alertCondition > GREEN_ALERT )
				newColor = darkgray_color;							//and very dark gray in yellow alert
		}
		return newColor;
	}
	function lb_effect_size( map, showing ) {						// only called via update_one_Sighting so no need to check !using_common_vars
	// "_largeball", "_ball", "_marker", "_smallmarker", "_tinymarker", "_dotmarker", "_flag"; lightgray also has _moon, _moonflag
		var ent = map.ent, collsn_rad = 0;
		if( ve_colour < 0 ) ve_colour = map.ve_colour;
		if( !ve_colour ) return '';
		if( radius < 0 ) radius = ent.radius || false;
		if( collisionRadius < 0 ) collisionRadius = ent.collisionRadius;
		collsn_rad = radius ? 0 : collisionRadius;					// 0 for planets, moons & sun
		if( distance < 0 ) distance = map.ent_dist;
		if( distance < LightBallMinDist + collsn_rad				//too near
			|| ( distance < LightBallShipMinDist + collsn_rad
				 && ve_colour !== 'cyan' && ve_colour !== 'white' && ve_colour !== 'pink' ) // ship too near
			|| redAlertOptimize() ) {
				//in red alert show ball marked targets only to save CPU and clean scanner (masslock rings are also removed)
			return '';
		}
		if( !collisionRadius ) { // planets, moons & sun
			if( isVisible < 0 ) isVisible = ent.isVisible;
			if( hasAtmosphere < 0 ) hasAtmosphere = ent.hasAtmosphere;
			if( isVisible && distance < 1e7 )
				return hasAtmosphere ? '_moonflag' : '_flag';  //no dot
			else
				return hasAtmosphere ? '_moon'	   : '_dotmarker';
		}
		if( !LightBalls || (!ShipLightBalls							// user set to off
							&& !a_non_ship_colour( ve_colour )) ) {	//if ship lightballs are disabled then show others only
			return '_flag';											//lollipop without lightball
		}
		if( !showing ) return '_flag';								//lollipop without lightball
		if( LargeLightBalls ) {//large balls
			if(	  distance < SniperMinRange + collsn_rad ) return '_largeball'; //xl size
			else if( distance < SniperRange + collsn_rad ) return '_ball';		//large size
			else if( distance > 1e6 )	return '_tinymarker'; //over 1000km show tiny ball
			else if( distance > 1e5 )	return '_smallmarker'; //over 100km show smaller ball
			return '_marker';	//average size
		}
		if( script_mass === undefined ) script_mass = read_scriptInfo( ent, map ); // also sets 'mass'
		if( distance > 1e6 )
			return '_dotmarker'; //over 1000km
		else if( mass < 4e5 )
			return '_tinymarker'; //escort ship -was hitting here w/ slivers
		else
			return '_smallmarker'; //large ship (Cobra3 and over)
	}
	function update_lt_ball( map, index ) {
		var lightball, new_size, ent;
		var non_ship_colour = a_non_ship_colour( ve_colour );
		var disallowed = !LightBalls								// turned off by user
					|| ( !ShipLightBalls && !non_ship_colour );		//if ship lightballs are disabled, show others only
		lightball = map.lightball;
		if( lb_size < 0 ) lb_size = map.lb_size;					// get current size
		if( disallowed ) {
			if( lightball && lb_size !== '_flag' && !profiling ) {	// need to check for existing, as (Ship)LightBalls are in-game settings
				lightball.remove();
				map.lightball = lightball = null;
			}
		}															// else this is not a ship ("navigation only" lightballs)
		ent = map.ent;
		if( distance < 0 ) distance = map.ent_dist;
		let showing = showing_lightball( ent );
		map.lb_size = new_size = lb_effect_size( map, showing );	// calc size to see if it's changed
		if( rank < 0 ) rank = map.rank;
		if( position.length === 0 ) {
			copy_vector( map.last_posn, position );				   // lightball doesn't move if 'ukn'
		}
		if( lightball && new_size === lb_size
					  && ve_colour === map.ve_colour )
			return;													// need only to reposition existing effect - now done in an fcb
		lb_size = new_size;
		if( lb_size !== '' ) {										// size chg => add new sized/coloured one
			add_lt_ball( map, index );
			if( map.lightball )
				map.lightball.scannerDisplayColor1 = lb_showing_colour( ent, showing ); // prevent blinking when cycle weapons
		} else if( lightball && !profiling ) {						// no ball, remove current one
			lightball.remove();
			map.lightball = null;
		}
	}
	// Sighting updating - masslock ring //////////////////////////////////////////////////////////
	const SHOW_GREEN_WEAPS_OFF  = 1,	/// default from 1.15 is
		  SHOW_GREEN_WEAPS_ON	= 2,	/// $TelescopeMassLockBorders && ( pla === 1 || !ps.weaponsOnline )
		  SHOW_YELLOW_WEAPS_OFF = 4,	/// so 1 | 2 | 4 | 16 = 23, aka $DEFAULT_ML_RINGS
		  SHOW_YELLOW_WEAPS_ON  = 8,
		  SHOW_RED_WEAPS_OFF	= 16,	///
		  SHOW_RED_WEAPS_ON		= 32,
		  SHOW_WEAPS_OFF		= SHOW_GREEN_WEAPS_OFF	| SHOW_YELLOW_WEAPS_OFF	| SHOW_RED_WEAPS_OFF,
		  SHOW_WEAPS_ON			= SHOW_GREEN_WEAPS_ON	| SHOW_YELLOW_WEAPS_ON	| SHOW_RED_WEAPS_ON,
		  SHOW_ALERT_GREEN		= SHOW_GREEN_WEAPS_OFF	| SHOW_GREEN_WEAPS_ON,
		  SHOW_ALERT_YELLOW		= SHOW_YELLOW_WEAPS_OFF | SHOW_YELLOW_WEAPS_ON,
		  SHOW_ALERT_RED		= SHOW_RED_WEAPS_OFF 	| SHOW_RED_WEAPS_ON;
	function setShowFlags() {
		show_on_Alert = alertCondition === GREEN_ALERT  ? SHOW_ALERT_GREEN :
						alertCondition === YELLOW_ALERT ? SHOW_ALERT_YELLOW :
						alertCondition === RED_ALERT    ? SHOW_ALERT_RED : 0;
		show_on_Weapons = weaponsOnline ? SHOW_WEAPS_ON
										: SHOW_WEAPS_OFF;
	}
	// on/off for masslock rings in current alertCondition/weaponsOnline state
	function _getShowState() { return show_on_Weapons & show_on_Alert; }
	function _getShowStateText() {									// text for dynamic activate messages
		var alert = alertCondition === GREEN_ALERT  ? 'green' :
					alertCondition === YELLOW_ALERT ? 'yellow' :
					alertCondition === RED_ALERT    ? 'red' : 'docked';
		var weapons = weaponsOnline ? 'online' : 'off-line';
		return [ alert, weapons ];
	}
	function _currMLFlags() {
		return ws.$MassLockRings === null
				? SHOW_ALERT_GREEN | SHOW_WEAPS_OFF 				// default, as player never set flags
				: ws.$MassLockRings;
	}
	function _currSniperRingFlags() {
		return SniperRingActive === null
				? SHOW_WEAPS_ON 									// default, as player never set flags
				: SniperRingActive;
	}
	function _adjustMLFlags( turnOn ) {
		// when traversing state of TelescopeMenuLightballs, player expects masslock rings
		// even if current state missing from MassLockRings, so we fold it in
		// - when turned off, we also remove state from MassLockRings
		var state = _getShowState(),
			currFlags = _currMLFlags();
		if( turnOn ) {												// ensure state is on
			MassLockRings = ws.$MassLockRings = currFlags | state;
		} else {													// turn state off, masslock rings off
			MassLockRings = ws.$MassLockRings = currFlags & ~state;
		}
	}
	function show_ml_ring() {
		if( !viewIsStandard ) return false;							// only shown from inside ship
		if( !viewHasMLRings ) return false;							// looking out wrong porthole
		return ( MassLockRings & show_on_Alert						// test alert state
								& show_on_Weapons ) > 0;			// test weapons state
	}
	var mlVector = [];												// working vector for add_ml_ring to call orientToFace
	function add_ml_ring( map ) {									// only called via update_one_Sighting so no need to check !using_common_vars
		var masslock = map.masslock;
		if( masslock && !profiling ) {
			masslock.remove();
			map.masslock = masslock = null;
		}
		if( ve_colour < 0 ) ve_colour = map.ve_colour;
		if( ml_size < 0 ) ml_size = map.ml_size;
		if( !ve_colour || !ml_size ) return;						// must have both
		var effect_key = 'telescope-' + ve_colour + ml_size;
		var ent = map.ent;
		if( script_mass === undefined ) script_mass = read_scriptInfo( ent, map ); // also sets 'mass'
		var ring_radius = VariableMassLock ? map.ml_radius : scannerRange;
		var ring_scale;
		if( isPlanet < 0 ) isPlanet = ent.isPlanet;
		if( isPlanet ) {											// planets (but not suns)
			if( radius	< 0 ) radius = ent.radius || false;
			ring_scale = ( radius + (radius > scannerRange ? radius : scannerRange) ) /* max(radius, scannerRange) */
							/ MASSLOCK_RING_SCALE;
		} else {
			ring_scale = ring_radius / MASSLOCK_RING_SCALE;
		}
		if( profiling ) return; // else profiler goes BOOM!
		if( position.length === 0 ) {
			copy_vector( ent.position, position );
		}
		map.masslock = masslock = addVisualEffect( effect_key, position );
		if( masslock ) {
			masslock.scale( ring_scale ); //ring radius == scanner range
			// not waiting to be orientated in reposition_effects as misaligned ring visible momentarily
			subtract_vectors( position, ps_position, vector );
			unit_vector( vector, mlVector );
			orientToFace( masslock, mlVector );						// orientToFace uses both 'vector' & 'cross'
		}
	}
	function ml_effect_size( map ) {								// only called via update_one_Sighting so no need to check !using_common_vars
		if( distance < 0 ) distance = map.ent_dist;
		if( distance < scannerRange )		return '';
		if( distance > 2.5e6 )				return '';				// current models are not visible beyond 2500 km
		var ent = map.ent;
		if( redAlertOptimize() )			return '';				// lightballs are also removed
		if( scanClass < 0 ) scanClass = ent.scanClass;
		if( scanClass === 'CLASS_CARGO' )	return '';
		// if( isFrangible < 0 ) isFrangible = ent.isFrangible;
		if( scanClass === 'CLASS_ROCK' )	return '';				// exclude all (abandoned) Rock Hermits, may have the mass but don't masslock
		if( scanClass === 'CLASS_BUOY' )	return '';
		if( isWormhole < 0 ) isWormhole = ent.isWormhole;
		if( isWormhole )					return '';
		if( is_cloaked( ent ) )				return '';
		// if( is_jamming( ent ) )				return '';
			// since beyond scannerRange, jammer not applicable
		var bright = BrightMassLockRings ? '2' : '';
		var size = '_ml';
		var fardist = 3e5;											//far masslock border over this distance (station or gravity scanner target)
		var farplanet = 30;											//far masslock border distance multiplier (an average 5000km planet over 1500km distance)
		if( isPlanet < 0 ) isPlanet = ent.isPlanet;
		if( distance > fardist ){									//far masslock border over this distance (station or gravity scanner target)
			if( isPlanet ) {
				if( radius < 0 ) radius = ent.radius || false;
				let save_radius = radius;
				if( map.planetRadius ) {							// it may be a moon
					radius = map.planetRadius;
				} else {
					if( hasAtmosphere < 0 ) hasAtmosphere = ent.hasAtmosphere;
					if( !hasAtmosphere ) {							// it's a moon, find it's planet
						let orb, oi, olen,
							orbs = entitiesWithScanClass( 'CLASS_NO_DRAW', ent, AutoScanMaxRange );
						for( oi = 0, olen = orbs.length; oi < olen; oi++ ) {
							orb = orbs[ oi ];
							if( orb.hasAtmosphere ) {
								radius = orb.radius;				// temporarily override, gets restored by save_radius
								map.planetRadius = radius;			// cache for later
								break;
							}
						}
					}
				}
				if( distance > farplanet * radius )
//							  (radius < 20000 ? 20000 : radius) )	// min. radius for small moons
// when based on radius, a moon gets mlf before its planet, looks wierd
					size = '_mlf';									//set far masslock border
				else if( distance < 3 * radius )
					size = '_mlt';									//set thin masslock border
				else
					size = '_ml';									//set normal masslock border (default)
				radius = save_radius;
			} else {				//is not planet but far
				size = '_mlf';		//set far masslock border
			}
		} else if( isPlanet ) {										// near a planet
			if( radius	< 0 ) radius = ent.radius || false;
			if( distance < 3 * radius )				size = '_mlt';	//set thin masslock border
		} else {
//			size = distance > scannerRange_X_2 ? '_ml' : '_mlt';
			size = '_ml';
			do {
				if( distance > scannerRange_X_2 )	break;			//check for thin border within 2x scanner range
				if( position.length === 0 ) {
					copy_vector( ent.position, position );
				}
				if( ent_vector.length === 0 ) subtract_vectors( position, ps_position, ent_vector );
				var angle = angle_between_unitV( ps_vectorForward, ent_vector );
				if( angle < FORTYFIVE_DEGREES )		break;			// under 45 degree mean ship is near and ring is out of sight?
				if( angle > QUARTER_ARC )			break;			// and less than 90 degree mean ship's ring is in front you (it's parallel to vectorRight)
				let run	 = sin( angle ) * distance;					// sin( angle ) = run / distance
				let rise = cos( angle ) * distance;					// cos( angle ) = rise / distance
				let ring_radius = VariableMassLock ? map.ml_radius : scannerRange;
				let center_dist = run - ring_radius;
				center_dist = center_dist < 0 ? -center_dist : center_dist;
				var fov_cutoff = center_dist / sin_fov2;
				if( cos_fov2 < rise / fov_cutoff ) break;
				size = '_mlt';										//so if ring is in screen then can be close so need thin border
			} while( false );
		}
		return bright + size;
	}
/*
				if( Math.sin( angle ) < scannerRange/dist )	break;	//and crosshairs points out of ring which is closer
			 \			  ^<--fov/2--> /	   |		  - we have angleTo & hypotenuse, and same orientation (ring parallels vectorRight)
	 |		   \		  |			 /		   |			  sin( angle ) = run / distance	  => run  = sin( angle ) * distance
	 |			 \		  |<-c_d->(--r_r---X   |  ^			  cos( angle ) = rise / distance  => rise = cos( angle ) * distance
	 |			   \	  |		 /			   |  |		  - masslock ring edge is at center_dist = run - ring_radius
	 |				 \	  |	   /			   |  | rise  - for fov at rise, sin( fov/2 ) = center_dist / hypotenuse
	 |				   \  |	 /				   |  |			 => hypotenuse = center_dist / sin( fov/2 ), called fov_cutoff
	 |---------|---------\_/---------|---------|  |		  - for ring to be visible, cos( fov/2 ) > rise / fov_cutoff
	 2	 scannerRange		   scannerRange		2
						  <----- run ---->
*/
	function update_ml_ring( map ) {								// only called from update_one_Sighting so no need to check !using_common_vars
		var new_size, ent, masslock = map.masslock;
		ent = map.ent;
		var showingMLRings = show_ml_ring();
		if( showingMLRings && !ext_ok ) {							// "Without a Telescope Extender, these are shown around
			do {													//	 planets and stations only"
				if( isStation < 0 ) isStation = ent.isStation;
				if( isStation ) break;
				if( radius < 0 ) radius = ent.radius || false;
				if( radius ) break;
				showingMLRings = false;
			} while( false );
		}
		if( !showingMLRings ) {
			if( masslock && !profiling ) {							// need to check for existing, as MassLockRings is an in-game setting
				if( debug ) log(ws.name, 'update_ml_ring, as !show_ml_ring, removing masslock ring ("'
											 + map.ml_size + '") for' + map.ent );
				masslock.remove();
				map.masslock = null;
			}
			return;
		}
		if( isSun < 0 ) isSun = ent.isSun;
		if( isSun ) return;											// no longer has one
		if( ml_size < 0 ) ml_size = map.ml_size;					// get current size
		map.ml_size = new_size = ml_effect_size( map );				// calc size to see if it's changed
		if( rank < 0 ) rank = map.rank;
		if( position.length === 0 ) {
			copy_vector( map.last_posn, position );					// ring stays w/ lightball, even if 'ukn'
		}
		if( masslock && new_size === ml_size
					 && ve_colour === map.ve_colour ) {				//move masslock ring
			return;													// need only to _reposition_effects
		}
		ml_size = new_size;
		if( ml_size !== '' && ve_colour !== 'gray' ) {				// size chg => add new sized
			add_ml_ring( map );
		} else if( masslock && !profiling ) {
			masslock.remove();
			map.masslock = null;
		}
	}
	// Sighting updating //////////////////////////////////////////////////////////////////////////
	function proc_stealthy( map, ent, have_scanned ) {
		if( have_scanned === true ) {								// ships using scriptInfo {...telescope = 0...}
//			if( map.rank === 'ukn' ) {								// lost contact of stealthy ent, wipe all info
			if( !ent.isVisible ) {									// lost contact of stealthy ent, wipe all info
				_delete_Sighting( map.ent, 'proc_stealthy' );		//  - threshold is visibility, not gravity
				return true;
			}
		} else if( distance > have_scanned ) {						// cargo pod's RFID signal lost in noise
			_delete_Sighting( map.ent, 'proc_stealthy cargo' );		// also deleted in check_Sightings
			return true;
		}
		return false;
	}
	// bitflags for dynamic MFD filtering
	const MFD_FRIENDLY = 1,		// bounty === 0 && !markedForFines
		  MFD_UNSOCIABLE = 2,	// bounty || markedForFines
		  MFD_ACTIVE = 4,		// has .target || defenseTargets.length > 0
		  MFD_HOSTILE = 8,		// in_ents_Targets || targeting_ps
		  	MFD_ATTITUDE = 15,	// those of 1st 4 flags used to choose targets
		  MFD_NEARBY = 16,		// distance < scannerRange
		  MFD_PROTECTED = 32,	// .withinStationAegis
		  MFD_FARAWAY = 64,		// distance > scannerRange
		  	MFD_RANGED = 112;	// those of prev. 3 flags used to limit those chosen
		  // if add more flags, be sure to update line 90: this.$MFD_DYNAMIC_ALLSET = 127;
	function update_one_Sighting( map, ent, index, check_notable ) {
		if( check_notable || status < 0 ) status = ent.status;
		if( _has_bad_status( ent, status ) )						// shipScoopedOther can fire & del before we get to it!
			return;
		if( check_notable || distance < 0 )
			distance = map.ent_dist;
		if( check_notable ) {										// false when called from include_ent, _add_Sighting, as already checked
			let save_status = status,								//	 but true from refresh_Sightings
				save_dist = distance;
			reset_common_vars();									//	 prepare for call to notable_ent
			status = save_status;
			distance = save_dist;
			if( !notable_ent( ent ) ) {								// sets rank, ve_colour and (maybe) distance
				if( index !== -1 )									//	 notable_ent checks if eclipsed
					_delete_Sighting( index, 'update_one_Sighting' + ' CHECK_NOTABLE' );
				return;
			}
			map.staticMFD = staticMFD;
		} else {
			isBeacon = is_beacon( ent );							// fn tests isBeacon < 0
			radius = ent.radius || false;
			let hidden = index < 0 ? false : eclipsed( ent, map );	// index = -1 => call from include_ent, eclipsed already checked
			if( !isBeacon && !radius && hidden ) {
				_delete_Sighting( (index >= 0 ? index : ent), 'update_one_Sighting' + ' !check_notable' );
				return;
			}
		}
		dynamicMFD = distance < scannerRange ? MFD_NEARBY			// clear all as all are reset here
											 : MFD_FARAWAY;
		if( scanClass < 0 ) scanClass = ent.scanClass;
		if( scanClass === 'CLASS_CARGO' ) {							// scannerRange exception for cargo
			if( check_notable ) {
				if( is_cargo === true )
					dynamicMFD = MFD_NEARBY;
			} else {
				if( is_ignored_ship( ent ) ) {
					_delete_Sighting( (index >= 0 ? index : ent), 'update_one_Sighting' + ' is_ignored_ship' );
					return;
				}
				if( shipClassName < 0 ) shipClassName = ent.shipClassName;
				if( shipClassName !== 'Splinter'
						 && shipClassName !== 'Boulder'
						 && shipClassName !== 'Metal fragment' )
					dynamicMFD = MFD_NEARBY;						// cargo, escape pods detectable until deleted
			}
		}
		if( gravScanProgress > 0 ) { // once gs stops running, this degrades over time (ie. not checking gs_state)
			if( gs_curr < 0 ) gs_curr = grav_scan_dist( ent, true, map );
			map.gs_curr_dist = gs_curr;
		}
		let have_scanned = map.have_scanned;
		if( rank < 0 )	rank = map.rank;
		else 			map.rank = rank;							// may have been altered in notable_ent
		if( ve_colour < 0 ) ve_colour = map.ve_colour;
		if( rank === 'ukn' ) {
			ve_colour = 'gray';										// becomes 'gray' if ent departs our equipment's range
			if( map.ve_colour !== 'gray' ) {						// just became 'ukn', remove masslock ring so a gray one will be created
				let masslock = map.masslock;
				if( masslock && !profiling ) {
					masslock.remove();
					map.masslock = null;
				}
			}
			isHostile = false;
			dynamicMFD = MFD_FARAWAY;								// lost contact, clear all dynamic MFD bitflags but Faraway
		} else {
			let moody = scanClass !== 'CLASS_BUOY'
					 && scanClass !== 'CLASS_CARGO'
					 && scanClass !== 'CLASS_ROCK'
					 && scanClass !== 'CLASS_NO_DRAW'
					 && scanClass !== 'CLASS_WORMHOLE';
			isHostile = moody ? is_hostile( ent, true ) : false;	// true to set all glocals
			if( isHostile ) {
				if( have_scanned !== true && have_scanned !== -1 ) {// must be explicit, as prop has multiple uses
					if( distance < scannerRange ) {
						map.have_scanned = -1;						// once scanned, offender status can be remembered (FarStatus)
					}
				}
				ve_colour = 'red';									// FarStatus & distance dealt with in is_hostile
				dynamicMFD |= MFD_UNSOCIABLE;
			} else if( moody ) {
				if( have_scanned === -1 ) {							// an offender has reformed?
					map.have_scanned = false;
				}
				dynamicMFD |= MFD_FRIENDLY;
			}
			if( has_targets )						  dynamicMFD |= MFD_ACTIVE;
			if( targeting_ps || in_ents_Targets )	  dynamicMFD |= MFD_HOSTILE;
			if( ent.withinStationAegis )			  dynamicMFD |= MFD_PROTECTED;
		}
		map.dynamicMFD = dynamicMFD;
		if( have_scanned === true || have_scanned > 0 ) {			// hidden ent or cargo
			if( proc_stealthy( map, ent, have_scanned ) ) {			// gets deleted if no longer detectable
				if( check_notable ) using_common_vars = false;		// premature exit, reset when necessary
				return;
			}
		}
		if( index >= 0 ) {											// when called by grow_new_list, has not been added to
			update_ml_ring( map );									//	 mapping, so wait for next update (index is req'd
			update_lt_ball( map, index );							//	 for update_lt_ball)
		}
		map.ve_colour = ve_colour;									// may have been altered
		if( check_notable ) using_common_vars = false;				// reset when necessary
	}
	var systemEclipsers = null;										// cache of system's orbs & stations
ws._eclipsed = eclipsed; // for debug
	function eclipsed( ent, map, dist ) {							// determine if it's behind orb or station (notable_ent only caller)
		var that = eclipsed;
		var eclipsed_ent = (that.eclipsed_ent = that.eclipsed_ent || []),// vector to ent we're checking
			orb_vector = (that.orb_vector = that.orb_vector || []);// vector to candidate eclipser
		eclipsed_ent.length = 0;									// reset array
		orb_vector.length = 0;										// reset array
		if( !systemEclipsers || systemEclipsers.length === 0 )		// cache not initialized
			return false;
		if( grav_eq_ok )
			return false;											// gravity scanner overcomes line of sight
		let eclipsing_dist = 0;										// its distance
		if( map )													// map optional, as is called by grow_list sequence
			eclipsing_dist = distance = map.ent_dist;
		else if( dist )												// dist optional, to save on call to _detect_distanceTo
			eclipsing_dist = distance = dist;
		else {
			if( distance < 0 ) distance = _detect_distanceTo( ent );
			eclipsing_dist = distance;
		}
		var threshold, tangentAngle, ecl_ent, ecl_map, dist,
			edge, opp, adj, msize, isOrb,
			len = systemEclipsers.length;
		if( !len ) return false;
		for( var idx = 0; idx < len; idx++ ) {
			ecl_ent = systemEclipsers[ idx ];
			if( ecl_ent === ent ) continue;
			if( ecl_ent && !ecl_ent.isValid ) continue;
			if( ecl_ent && !ecl_ent.position ) {
/// trap for dybal's bug where _detect_distanceTo call mks call to subtract_vectors w/ null 2nd parm
/// (may have fixed via fetchSun when building systemEclipsers)
if( debug ) {
	log( ws.name, 'eclipsed, stale systemEclipser w/o a position, idx = ' + idx );
	var tmp = _Sighting_index( ecl_ent );
	if( tmp < 0 ) {
		log( ws.name, 'eclipsed, NOT in _Sightings!!!' );
	} else {
		log( ws.name, 'eclipsed, _Sighting_index = ' + tmp );
		if( cd )
			cd._showProps( mapping[tmp], 'systemEclipser' );
	}
}
				continue;
			}
			let index = _Sighting_index( ecl_ent );
//			  if( index < 0 ) continue;								// not in map, cannot eclipse (ok, 1st create in system will thrash, a bit)
			if( index < 0 ) {
				ecl_map = null;
				let save_radius = radius,
					save_collRad = collisionRadius;					// preserve current ent's property gets
				radius = collisionRadius = -1;
try {
				dist = _detect_distanceTo( ecl_ent );				// sets radius, maybe collisionRadius
} catch( err ) {
	log( ws.name, 'ATTN: Dybal' );
	log( ws.name, 'eclipsed, failed to calculate distance to un-mapped ent: ' + ecl_ent );
	log( ws._reportError( err, eclipsed, [ent, map, dist], 2, true ) );
	if( !that.DybalMsg || that.DybalMsg < 3 ) {
		consoleMessage( '!! check log for error !!', ConsoleMsgDurn );
		that.DybalMsg = !that.DybalMsg ? 1 : that.DybalMsg + 1;
	}
	continue;
}
				edge = radius;
				radius = save_radius;								// restore current ent's property gets
				collisionRadius = save_collRad;
			} else {
				ecl_map = mapping[ index ];
				dist = ecl_map.ent_dist;
				edge = ecl_ent.radius;
			}
			if( eclipsing_dist < dist ) continue;					// is in front of ecl_map
			// calc dist from center to rim/outer hull
			isOrb = edge > 0;										// only they have a .radius property
			if( isOrb ) {
				opp = edge + (ecl_ent.hasAtmosphere === true ? 500 : 0);// 500 is default height of atmosphere
			} else if( ecl_ent.isStation || (SpicyHermits && ecl_ent.isRock && !ecl_ent.isFrangible) ) {
				if( dist > scannerRange_X_4 ) continue;				// don't bother with distant stations
				edge = ecl_ent.collisionRadius;
				opp = edge;
			} else continue;										// should never happen but, well, you know, bugs
			adj = dist + edge;										// is subtracted in _detect_distanceTo
			if( adj <= 0 ) continue;								// should never happen
			tangentAngle = asin( opp / adj ) * RADIANS_TO_DEGREES;	// angle from center to limb/hull
			copy_vector( (index < 0 ? ecl_ent.position
									: ecl_map.last_posn), vector );
			subtract_vectors( vector, ps_position, orb_vector );
			if( eclipsed_ent.length === 0 ) {						// 1st time here, calc ent's vector, etc., as now it's needed
				copy_vector( (map ? map.last_posn
								  : ent.position), vector );
				subtract_vectors( vector, ps_position, eclipsed_ent );
				unit_vector( eclipsed_ent, eclipsed_ent );
				if( isOrb ) {
					if( isStation < 0 ) isStation = ent.isStation;
					let marker = map ? map.lb_size : null;
					msize = !marker ? (LargeLightBalls ? 500 : 200) :// no lightball/map, use median size
							marker === '_largeball' ? 1000 :		// sizes vary a bit across colors; these are the largest values
							marker === '_ball' ? 800 :
							marker === '_marker' ? 600 :
							marker === '_smallmarker' ? 400 :
							marker === '_tinymarker' ? 300 : 150;	// '_dotmarker'
					msize = msize / 533 *							// in right ballpark, if _smallmarker is ~3/4 degree
							(isStation ? (1e6 - eclipsing_dist) / 1e6 : 1); // adj for station apparent size
				} else
					msize = 0;
			}
			threshold = tangentAngle - msize;						  // lightball has constant size
			let span = angle_between_unitV( eclipsed_ent, orb_vector ) * RADIANS_TO_DEGREES;
/*
if( debug && ent === curr_target )
	log(ws.name, 'eclipsed, ' + (span<threshold ? 'HIDDEN by ' + ecl_ent.name : '')
		+', span = ' + span.toFixed(2)
		+ ', threshold = ' + threshold.toFixed(2) + ', tangentAngle = ' + tangentAngle.toFixed(2) + ', msize = ' + msize.toFixed(2)
	);
player.ship.removeEquipment( 'EQ_GRAVSCANNER' )
player.ship.awardEquipment( 'EQ_GRAVSCANNER' )
*/
			if( span < threshold ) return ecl_ent;
		}
		return false;
	}
	function refresh_Sightings() {									// update existing ents a few at a time (1 update ~ 0.86 ms); use_map limits to known targets
		try {
			if( mk_maps ) return;									// have started creating new mapping, abort
			if( maplen <= 0 ) return;
			var index = map_update_index;
			var count = updates_per_frame;
			var map, ent;
			for( ; count > 0 && index < maplen; count--, index++ ) {
				if( index >= mapping.length ) break;				// scooped/died/jumped during update, which shortened mapping!
				map = mapping[ index ];
				if( !map ) continue;
				ent = map.ent;
				if( !ent || !ent.isValid ) continue;
				update_one_Sighting( map, ent, index, true );		// true directs call to notable_ent
			}
			map_update_index = index;
			if( index >= maplen ) {									// finished w/ list
				set_fn_pending( grow_new_list, 'refresh' );
			} else {												// more to process in next call
				set_fn_pending( refresh_Sightings );
			}
		} catch( err ) {
			if( debug ) log(ws.name, 'refresh_Sightings, map = '
						+ (cd ? cd._showProps( map, 'map' ) :'')
						+ '\nent = ' + ent + '\nindex = ' + index
						+ ', count = ' + count + ', maplen = ' + maplen );
			log( ws.name, ws._reportError( err, 'refresh_Sightings', updates_per_frame ) );
			if( debug ) throw err;
		}
	}
	var updates_per_frame = 2;										// # of ships updated in a single frame; adjusted to frame rate
	var map_update_index = 0;										// saves index across calls
	function update_Sightings( just_mapping ) {
		try {
			_update_Sightings( just_mapping );
		} catch( err ) {
			log( ws.name, ws._reportError( err, 'update_Sightings', just_mapping ) );
			if( debug ) throw err;
		}
	}
	function _update_Sightings( just_mapping ) {					// initiate update cycle; just_mapping limits update to known targets
		var that = _update_Sightings;
		if( that.fps === undefined ) that.fps = 0;
		if( !equip_ok ) return;
		if( !mappingReady || maplen === 0 ) return;					// mapping not yet initialized OR it's empty
		if( fns_are_pending() ) return;								// called in midst of creating a new mapping, abort!
		if( !ps || !ps.isValid || alertCondition === DOCKED ) 		//if player died or docked
			return;
		mk_maps = false;											// suppress creation of Sightings
		map_update_index = 0;
		if( just_mapping ) {										// update just known entities (ie. in mapping)
			set_fn_pending( refresh_Sightings );
			/*
			- a user setting (MaxTargets) deals with the max # of targets to consider.	At 200, processing
			  5 per frame, this completes in 40 frames but at a cost of 4.3 ms/frame (@~30 fps on my PC)
			- there is available, @ 60 fps 16.7 ms/frame,
								  @ 50	   20 ms
								  @ 40	   25 ms
								  @ 30	   33 ms
								  @ 20	   50 ms
			- so on my PC, I reduce my frame rate from 30 to 26.8 ( 1/33 -> 1/37.3 )
			- if I limit to 2 per frame, 200 targets will take 10 sec @20 fps, but only 3.33 sec @ 60 fps
			I'm sure the pilot @20 won't like the 10 seconds but all I can do is suggest (s)he reduce the max # of
			targets.  And the one @60 isn't thrilled either, but there I can do something:
			- start w/ a low rate until fps stats mature
			- then starting w/ an fps based target, incr # Sightings/frame
			- monitor the % diff in ms/Sighting and keep it below a threshold based on fps
			  (see init_growing; uses similar scheme & will decrease MaxTargets)
			*/
			var fps, base, target;
			if( !current_fps || !long_term_fps )
				return;
			fps = current_fps();
			base = long_term_fps();									// mean of last 5 min
			if( fps > 0 && fps !== that.fps ) {						// more than a minute has passed & got new value
				if( base > 0 ) {									// in flight for at least 5 min
					target = floor( 2 + fps / 30 );					// aim for twice min of 2 @ 60 Hz
					if( fps < base &&								// reduction in fps beyond threshold
						   percentDiffOver( fps, base, (fps / 8) ) )// 7.5% @ 60 (> 55.5 fps), 3.75% @ 30 (> 27.6 fps)
						target--;
					else
						target++;
				} else {
					target = floor( fps / 20 );						// start w/ a low rate
				}
				if( target > 2 && target !== updates_per_frame ) {	// adjust target every update based on fps stats from the last minute
					if( debug ) log(ws.name, '_update_Sightings, updates_per_frame changed from '
												 + updates_per_frame + ' to ' + target );
					updates_per_frame = target;
				}
				that.fps = fps;
			}
		} else {													// new entities detected
			init_growing( false );
			set_fn_pending( grow_new_list, 'add_orbs' );
		}
	}
	function percentDiffOver( a, b, p ) {							// for comparing fps readings; reciprocals compare time
		if( a === 0 || b === 0 || p === 0 ) return false;
		var diff = a > b ? (a - b) / a	// (1/b - 1/a)	=> a-b/ab; * b => a-b/a
						 : (b - a) / b; // (1/a - 1/b) * a => (b - a)/ab * a => (b - a) / b
		return diff > p / 100;
	}
	// Sighting effect positioning ////////////////////////////////////////////////////////////////
	var speed_adj = [];
	function apply_speed_adj( dst_posn ) {							// used in position calc for lightballs, marker
																	// prevDist is distance from last frame
		// travelling at high speed can distort positon calculations,
		// lightballs off-center or lose lock on far target marker
		//   eg. @reg. Torus of 12000 and fps < 30, travel > 400 m/frame
		// so we contract the range for positioning lightballs & the marker
		//   at rest, marker: scannerRange - 600, placed just in front of band of far lightballs
		//    far lightballs: scannerRange - 400 - index, a ring MaxTargets deep, ending @ 25200
		//   near lightballs: scannerRange + 400 - index, ie. a buffer of +- 200 around scannerRange
/*
		var that = apply_speed_adj;
		if( that.absMaxSpeed === undefined ) that.absMaxSpeed = 0;
		var absMaxSpeed = that.absMaxSpeed;
		var absMSShip = (that.absMSShip = that.absMSShip || {});
		let chkValue  = TorusToSun ? TorusToSun.$TorusToSunBonus :
						FarPlanets ? FarPlanets.$FarPlanetsBonus :
						WarpDrive  ? WarpDrive.$scanScale : ps_maxSpeed;
		if( !absMaxSpeed || !absMSShip[ ps ] || absMSShip[ ps ] !== chkValue ) {
			if( TorusToSun ) {										// using 31, not 32, as its chkValue is 1 based, ie. 1 => no bonus
				absMaxSpeed = ps_maxSpeed * 31 * chkValue;			// his code uses (b - 1) which doesn't work here
			} else if( FarPlanets ) {
				absMaxSpeed = ps_maxSpeed * 31 * chkValue;
			} else if( WarpDrive ) {
				absMaxSpeed = WarpDrive.$basicMaxSpeed * (WarpDrive.$warpFlag ? chkValue : 1);
			} else {
				absMaxSpeed = chkValue * 32;
			}
			that.absMaxSpeed = absMaxSpeed;
			that.absMSShip[ ps ] = chkValue;
		}
 */
		var travel = ps_speed * frame_delta;						// distance expect to travel this frame
		// - frame_delta is set by call to _hud_effects() which preceeds _reposition_effects()
		var dotP = dot_product( ps_vectorForward, target_direction );
		// - more sensitive to change in frame rate for ents parallel to heading, less so when perpendicular
		var contract = 250 + (250 * ps_speed/(ps_maxSpeed * 32));	// base amt for all directions
		if( dotP >= 0 ) {											// moving towards light ball
			contract += 0.5 * travel * dotP;
		} else {													// moving away from light ball
			contract += -1.5 * travel * dotP;
		}
		let adjust = -100 * ( floor(contract/100) ); 				// to hundreds to reduce jitter
		scale_vector( target_direction, adjust, speed_adj );
		add_vectors( dst_posn, speed_adj, dst_posn );
	}
	function redAlertOptimize() {
		// RedAlertDist: show lollipops in red alert within this distance only
		return RedAlertDist > 0 && distance > RedAlertDist && alertCondition > YELLOW_ALERT && weaponsOnline;
	}
	var view_vector = [],
		rotated_orient = [];	// working quaternion
		// "The Gravity scanner works only when you turn off your weapons with underscore ("_") button,
		//	otherwise only visible targets are displayed." (from readme)
	function orientToFace( ent, direction ) {
		// 'direction' is a vector pointing at 'ent'
		// orient entity so it faces 'direction', ie. its vectorForward is the negative of 'direction'
		copy_vector( ent.vectorForward, vector );					// 'vector' is a common working array
		cross_product( vector, direction, cross ); 					// axis of rotation ('cross' is the other working vector)
		unit_vector( cross, cross ); 								// must be normalized for rotation
		let angle = -angle_between_two_unitV( vector, direction );	// angle is negated to close the gap
		if( equal_value( angle, 0 ) ) 								// don't bother if w/i PRECISION, ie. close enough
			return;
		copy_quaternion( ent.orientation, quaternion );				// 'quaternion' is a common working array
		rotate_about_axis( quaternion, cross, angle, rotated_orient );
		ent.orientation = rotated_orient;							// 'rotated_orient' is another common working array
	}
	function _reposition_effects() {								// quick fcb that just updates effects
		try {
			if( !equip_ok ) return;
			if( !mappingReady || maplen === 0 ) return;				// mapping not yet initialized OR it's empty
			var index = maplen;
			var map, ent, distTo, lightball, masslock, redAlertOpt,
				showingMLRings = show_ml_ring();
			while( index-- ) {
				map = mapping[ index ];
				ent = map.ent;
				if( !ent || !ent.isValid ) continue;
				reset_common_vars();								// ensure no data carries over from last frame
				ve_colour = map.ve_colour;
				rank = map.rank;
				if( rank !== 'ukn' ) {								// if lost detection, position data goes stale (not updated)
					copy_vector( ent.position, map.last_posn );
				}
				copy_vector( map.last_posn, position );
				subtract_vectors( position, ps_position, ent_vector );
				unit_vector( ent_vector, target_direction );
			/// brought in from _detect_distanceTo for efficiency (share vector calc's)
				distTo = vector_magnitude( ent_vector );
				distTo -= hullOffset( ent );						// distance to near surface
				map.ent_dist = distance = distTo;
			/// end of _detect_distanceTo dup'd code
				redAlertOpt = redAlertOptimize();					// a fn of distance
				if( !redAlertOpt && viewIsStandard ) {
					map.headingTo = angle_between_two_unitV( view_vector, target_direction ) * RADIANS_TO_DEGREES;
				}
				lightball = map.lightball;
				lb_size = map.lb_size;
				let showingEffect = true;
				if( lightball )
					showingEffect = showing_lightball( ent );
				if( lightball && lb_size !== '_flag'
						&& ( redAlertOpt || !showingEffect ) ) {
					lightball.remove();
					map.lightball = null;
				} else if( lightball ) {
					lightball.scannerDisplayColor1 = lb_showing_colour( ent, showingEffect );
					lb_position( map, index, true );				// reposition existing effect, sets lightball_posn
						// uses distance, rank, position, ps_position, ent_vector, speed_adj
					lightball.position = lightball_posn;
				}
				masslock = map.masslock;
				if( masslock
						&& ( redAlertOpt || !showingMLRings )) {
					masslock.remove();
					map.masslock = null;
				} else if( masslock ) {
					masslock.position = position;					//move masslock ring
					if(	distance < scannerRange_X_2 || (radius		// set orientation of near ones for easier navigation
						&& distance < 2 * (scannerRange + radius)) ) {// including planets/moons (calc so masslock spheres don't touch)
						// setting to ps_orientation allows player to approach ring and steer
						// just outside masslock range
						masslock.orientation = ps_orientation;
					} else {										// orient ring to be perpendicular, face player.ship
						orientToFace( masslock, target_direction );
					}
				}
			}
			using_common_vars = false;
		} catch( err ) {
			log( ws.name, ws._reportError( err, '_reposition_effects' ) );
			if( debug ) throw err;
		}
	}
	// new Sightings //////////////////////////////////////////////////////////////////////////////
	var new_targets = [];											// list of new target entities for Combat_MFD
	function process_new_targets() {
		var index;
		if( new_targets && new_targets.length > 0 ) {
			var ent, idx, len;
			init_headingView();										// prep for showTargetName
			len = new_targets.length;
			for( idx = 0; idx < len; idx++ ) {						//search new target
				ent = new_targets[ idx ];
				if( ent.radius )
					continue;										// on launch, all targets are 'new' and system.planets may not be ready!
				index = _Sighting_index( ent, 'process_new_targets' );
				if( index < 0 )
					continue;										// should always be there but ...
				showTargetName( mapping[ index ] );					//but do not jump out of the cycle, keep to print all new name
			}
			new_targets.length = 0; 								// mk sure array ready for re-use
		} else {
			checkCombatMFD();										// update existing data
		}
	}
	function checkCombatMFD() {										// check health of target listed in Combat_MFD
		var index, map, ent;
		if( Combat_MFD ) {											//Combat_MFD support
			index = index_in_list( prevMFDTarget, mapping );
			if( index >= 0 ) {
				map = mapping[ index ];
				ent = map.ent;
				if( ent && ent.isValid ) {							// prev target is still ok
					showTargetName( map, true );					//update the direction (true suppresses console msg as this call 1/sec)
					return;											//	init_headingView called in _auto_updates
				}
			}
			prevMFDTarget = null;
			Combat_MFD.$TelescopeLine = '';							//clear the line in MFD
		}
	}
///////////////////////////////////////////////////////////////////////////////////////////////////
// mapping functions //////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////
	function newList( keep_deferred ) {
		try {
			_newList( keep_deferred );
		} catch( err ) {
			log( ws.name, ws._reportError( err, 'newList', keep_deferred ) );
			if( debug ) throw err;
		}
	}
	function _newList( keep_deferred ) {							// clears all Sightings & removes effects prior to docking/witchspace
		if( selected_Sightings )
			selected_Sightings.length = 0;							// remove any previous results
		clear_all_pending( keep_deferred );							// true will preserve deferred tasks queue
		if( !keep_deferred ) {										// are shutting down
			halt_steering();
		}
		while( maplen-- )
			free_Sighting( mapping[ maplen ] );
		mapping.length = maplen = 0;								// used to test when mapping not ready (launching, witchspace) - system var.s need re-init'g
		mappingReady = false;
		ws.$TelescopeList.length = 0;
		ws.$TelescopeListi = 0;										// $TelescopeListi = 0 => not in $TelescopeList
	}
	function map_sort_heading( a, b ) {								// sort by headingTo
		var a_heading = a.headingTo;								//  - to conform w/ find_most_central's logic, if 2 ships
		var b_heading = b.headingTo;								//    are within half a degree, choose the closer
		var diff = a_heading < b_heading ? b_heading - a_heading
										 : a_heading - b_heading;
		if( diff < 0.5 ) {											// w/i HALF_a_DEGREE of crosshairs,
			return a.ent_dist - b.ent_dist;							//	sort by distance
		} else {
			return a_heading - b_heading;							//	sort by heading
		}
	}
	function create_Sightings() {
		try {
			_create_Sightings();
		} catch( err ) {
			log( ws.name, ws._reportError( err, 'create_Sightings' ) );
			if( debug ) throw err;
		}
	}
	function _create_Sightings() {
		clear_all_pending();										// started in midst of _update_Sightings() or deferred tasks; shut that down!
		if( maplen === 0 )											// only needed when launching or exiting witchspace
			_init_player_vars();
		if( !equip_ok )
			return;
		grow_hidden_scanned.length = 0;
		var idx = maplen;
		while( idx-- ) {											// preserve list of 'hidden' entities that have been w/i scannerRange
			let map = mapping[ idx ];
			if( map.have_scanned !== false )						// for specials (hidden, cargo, FarStatus), should not be present otherwise
				grow_hidden_scanned.push( map.ent );				// preserve scan of specials (scriptInfo: telescope = 0), ie. once detected,
		}
		init_growing( true );
		set_fn_pending( grow_new_list, 'add_orbs' );
		// adj_update_MFDs.count = 0;									//	 "
	}
	var grow_maps = [];												// list of Sightings built to replace current mapping
	var grow_hidden_scanned = [];									// info carried over to new mapping; support for scriptInfo: telescope=0
	var max_sightings;												// counter during grow so don't exceed MaxTargets
	var mk_maps = true;												// flag as to whether (true) or not (false) we're creating a mapping
	var grow_list_index = 0;										// save index across calls
	var grow_start_count = 0;										// # of current iteration of 'start' step
	var grow_step_num = 12;											// arbitrary @start; scale it w/ fps_monitor
	var grow_target = 0;											// target step_num based on fps
	function init_growing( creating ) {
		var that = init_growing;
		if( that.fps === undefined ) that.fps = 0;
		if( that.base === undefined ) that.base = 0;
		mk_maps = creating;
		if( creating && current_fps && long_term_fps ) {			// adjust MaxTargets to prevent create cycle using too many frames
			var fps, base, limit;
			fps = current_fps();
			base = long_term_fps();									// mean of last 5 min
			if( fps > 0 && fps !== that.fps ) {						// more than a minute has passed/got new value
				that.fps = fps;
				if( base > 0 && base !== that.base ) {				// in flight for at least 5 min/got new 5 min baseline
					that.base = base;
					grow_target = floor( 6 + base / 5 );			// aim for 3 x's the minimum of 6 @ 60 Hz
					limit = grow_step_num;
					if( fps < base &&								// reduction in fps beyond threshold
						   percentDiffOver( fps, base, (fps / 8) ) )// 7.5% @ 60 (> 55.5 fps), 3.75% @ 30 (> 27.6 fps)
						limit--;
					else
						limit++;
					if( abs( grow_target - limit ) > 2 )			// it's a trend (occurs in 3/5)
						grow_target = limit;
/*
					let max_frames = floor( base / 5 );				// cycle must finish in 1/5 sec to allow time for others
					let total = grow_target * max_frames;
					if( debug && total !== MaxTargets ) {
						log(ws.name, 'init_growing, MaxTargets changed to ' + total
										 + ' due to frame rate of ' + base + ' fps' );
						MaxTargets = total;							// max # to finish in (arbitrary) 1/5  sec
						// Milo reports miss important new sightings; w/ various hardware/oxp config's, can creep too low
						// ? grow only, shrink to min of MaxTargets vs KISS
					}
 */
				} else {
					limit = floor( fps / 5 );						// initial rate, 12 per frame @ 60 fps, 6 @ 30
				}
				if( limit > 6 && limit !== grow_step_num ) {		// adjust limit every update based on fps stats from the last minute
																	//	 min of 6 is triple min in update because not all entities are kept,
																	//	 efficiency gains from immed. update (re-uses property get values)
																	//	 and 1st 4 frames limited to 1/2 limit (more are kept)
					if( debug ) log(ws.name, 'init_growing, grow_step_num changed from '
									+ grow_step_num + ' to ' + limit );
					grow_step_num = limit;
				}
			}
		}
		max_sightings = MaxTargets;									// orbs no longer counted in MaxTargets
		grow_list_index = grow_start_count = 0;
		allShips = system.allShips;									// array of all ships in system, sorted by distance
		past_range = 0;
		found_new = beacons_only = false;							// reset flag if full update of mapping started
	}
	function include_ent( ent, using_past_range, restoring ) {		// dual use: restoring is for adding from grow_hidden_scanned after create
		if( notable_ent( ent, using_past_range, null, restoring ) ) {
			var index = mk_maps && !restoring ? -1 : _Sighting_index( ent, 'include_ent' );
			var map = index < 0 ? mkSighting( ent ) : mapping[ index ];
			if( !map ) return false;
			if( mk_maps && !restoring ) {
				grow_maps.push( map );
			} else if( index < 0 ) {								// found new target or restoring
				mapping.push( map );
				maplen++;
				index = maplen - 1;
				if( !restoring ) {
					new_targets.push( ent );
				}
			}
			if( !restoring && map.swapable ) {						// orbs & beacons no longer counted in MaxTargets
				max_sightings--;
			}
			update_one_Sighting( map, ent, index );					// index can be -1, for those yet to be added into mapping
			return map;
		}
		return false;
	}
	var allShips, beacons_only;
	function fetchSun() {
		var ent = system_sun;
		if( isInterstellarSpace || !ent	|| !ent.isValid
				|| ent.hasGoneNova || ent.isGoingNova )	{			// sun is present and not nova
			ent = system_sun = null;
		}
		return ent;
	}
	function grow_new_list( step, testing ) {						// grow list over several frames
		if( testing !== undefined ) mk_maps = testing;
		var list, len, ent, map, idx;
		if( step === 'add_orbs' ) {									// add sun, planets & moons
			ent = fetchSun();
			len = system_planets.length;
			idx = 0;
			do {													// 1st loop adds sun, then ith loop add planet idx-1
				if( ent ) {
					reset_common_vars();							// sets using_common_vars true
					include_ent( ent );
				}
				if( idx >= len ) break;
				ent = system_planets[ idx++ ];
			} while( true );
			using_common_vars = false;
			set_fn_pending( grow_new_list, 'start' );
		} else if( step === 'start' ) {								// create or update cycle
			var stop, alllen = allShips.length;
			stop = grow_list_index + grow_step_num > alllen
				   ? alllen - grow_list_index : grow_step_num;
			if( grow_start_count <= 3 )								// to counter clusters near player, where high % are
				stop = stop >> 1;									//	 notable, 1st 4 loops are 1/2 grow_step_num
			grow_start_count++;
			// let psUnderAttack = ps.AIPrimaryAggressor;
			// psUnderAttack = psUnderAttack ? psUnderAttack.length !== 0 : false;
			let now = clock.absoluteSeconds;
			while( stop-- ) {
				ent = allShips[ grow_list_index++ ];
				if( !ent ) break;									// a ship died since start of loop, shortening list
				if( ent === ps ) continue;
				// newly spawned ships have .isVisible == true regardless of their distance
				let spawned = ent.spawnTime;
				if( spawned > 0 && now - spawned < SPAWN_DELAY ) 	// too new; will be picked up as a new target
					continue;
				reset_common_vars();								// clears distance
				if( beacons_only ) {								// once past AutoScanMaxRange, can only detect beacons
					isBeacon = is_beacon( ent );
					isStation = ent.isStation;
					if( !isBeacon ) continue;
				}
				include_ent( ent, true );							// sets distance
				if( !beacons_only && distance > scannerRange_X_2
						&& alertCondition > YELLOW_ALERT && weaponsOnline ) {
					beacons_only = true;
				}
				if( !beacons_only && distance > AutoScanMaxRange ) {
					beacons_only = true;
				}
				if( max_sightings === 0 ) {							// decr'd by include_ent
					beacons_only = true;
				}
			}
			using_common_vars = false;
			if( stop <= 0											// exit loop before finished this iteration
				&& grow_list_index < alllen - 1 )					// not at end of allShips
				set_fn_pending( grow_new_list, 'start' );			// still have work to do
			else if( mk_maps )										// new mapping created
				set_fn_pending( grow_new_list, 'create' );
			else													// list finished update cycle
				set_fn_pending( grow_new_list, 'update' );
		} else if( step === 'create' ) {							// creating new mapping finished
if( debug ) log(ws.name, 'grow_new_list, create, grow_maps.length = ' + grow_maps.length );
			for( idx = 0, len = grow_maps.length; idx < len; idx++ ) {	// transfer effects
				let new_map = grow_maps[ idx ];
				let index = _Sighting_index( new_map.ent );
				if( index < 0 ) continue;
				let curr_map = mapping[ index ];
				if( curr_map.ve_colour !== new_map.ve_colour )		// colour change => new effects needed
					continue;
				if( curr_map.lightball ) {
					new_map.lb_size = curr_map.lb_size;
					new_map.lightball = curr_map.lightball;
					curr_map.lightball = null;						// prevent removal in free_Sighting via _newList
				}
				if( curr_map.masslock ) {
					new_map.ml_size = curr_map.ml_size;
					new_map.masslock = curr_map.masslock;
					curr_map.masslock = null;						// prevent removal in free_Sighting via _newList
				}
			}
			_newList( true );										// true => don't clear deferred tasks
			for( idx = 0, len = grow_maps.length; idx < len; idx++ ) {	// to maintain external references, copy array
				mapping[ idx ] = grow_maps[ idx ];
			}
			maplen = mapping.length;
			mappingReady = true;
			grow_maps.length = 0;
			if( curr_S.ent )										// refresh current target
				_set_curr_Sighting( curr_S.ent, 'grow_new_list create' );
			list = grow_hidden_scanned;
			idx = list.length;
			while( idx-- ) {										// preserve list of 'hidden' entities that have been w/idx scannerRange
				ent = list[ idx ];
				reset_common_vars();
				map = include_ent( ent, false, true );				// true => checks it's still notable, update_one_Sighting (sets )
				if( !map ) continue;
				// when mkSighting, read_scriptInfo called, .script_mass is set set
				if( map.script_mass === 0 ) {						// ent hidden using scriptInfo {telescope = 0;}
					map.have_scanned = true;
				} else if( ent.scanClass === 'CLASS_CARGO' ) {		// cargo pod (restore its RFID range)
					map.have_scanned = scannerRange + (map.entityPersonality >> 1);
				} else {											// an offender, preserve detection for FarStatus
					map.have_scanned = -1;							//	 not using true, as offender status knowledge can
				}													//	 survive a scan whereas hidden does not
			}
			using_common_vars = false;
			set_fn_pending( grow_new_list, 'finish' );
		} else if( step === 'update' ) {							// update cycle finished
// if( debug ) log(ws.name, 'grow_new_list, update, mapping.length = ' + mapping.length );
			if( curr_target === null ) {
				ent = curr_S.ent || null;
				if( !ent || !ent.isValid || ent.radius )
					if( !testing && !profiling )					// sometimes doesn't return when profiling
						_clear_HUD_Effects();						//cleanup needed in some cases
			}
			set_fn_pending( grow_new_list, 'finish' );
		} else if( step === 'refresh' ) {							// finished updating existing mapping's ents
			mapping.sort( map_sort_heading );
		} else if( step === 'finish' ) {							// follow-up common to both create & update cycle
			process_new_targets();
			mapping.sort( map_sort_heading );
if( debug && mk_maps ) log( ws.name, 'grow_new_list, finished w/ ' + maplen + ' Sightings' );
			if( mk_maps ) {											// once mapping creation is done, update its ents
				grow_maps.length = 0;								// is now held in $SightingsMap (aka mapping)
				grow_hidden_scanned.length = 0;						// free up for garbage collection
			}														// - really only apparent on create cycle
		}
	}
	var past_range = 0;												// # of scannerRange's, to save calc's, as lists sorted by distance
	function set_range( ent, divide, using_past_range, dist ) {		// eliminate distance calc's where class is limited to
		if( using_past_range ) {									//	 a multiple of scannerRange
			if( past_range < divide && distance < 0 ) {				// have not reached divide, so must calc distance
				distance = _detect_distanceTo( ent );
			}
		} else {													// calc distance if not passed
			if( distance < 0 ) {
				distance = dist ? dist
								: _detect_distanceTo( ent );
			}
		}
		if( distance >= 0 ) {										 // have calc'd distance; never decrease past_range!
			if( past_range < divide && distance > scannerRange * divide ) {
				past_range = divide;
			}
		}
		return past_range >= divide;
	}
	// bitflags for static MFD filtering
	const MFD_SALVAGE = 1,		// cargo, escape pods, derelicts
		  MFD_MINING = 2,		// asteroids, boulders, splinters & metal fragments
		  MFD_WEAPONS = 4,		// mines & missiles
//		  	MFD_INANIMATE = 7,	// those of 1st 3 flags excluded from dynamic filtering
		  MFD_TRADERS = 8,		// ships .isTrader & escorts
		  MFD_POLICE = 16,		// scanClass === 'CLASS_POLICE'
		  MFD_PIRATES = 32,		// .isPirate & .isPirateVictim
		  MFD_MILITARY = 64,	// scanClass === 'CLASS_MILITARY'
		  MFD_ALIENS = 128,		// scanClass === 'CLASS_THARGOID'
		  MFD_NEUTRAL = 256,	// scanClass === 'CLASS_NEUTRAL' and not in any above category (e.g., miners, hunters, etc.)
//		  	MFD_ALLSHIPS = 504,	// all of the previous 6
		  MFD_STATION = 512,	// .isStation
		  MFD_NAVIGATION = 1024,// some stations & beacons (may include a ship if emitting a beacon)
		  MFD_CELESTIAL = 2048;	// sun, planets, moons
//		  	MFD_ORIENT = 3584;	// all of the previous 3
		  // if add more flags, be sure to update line 89: this.$MFD_STATIC_ALLSET = 4095;
	function classify_ship( ent, dist ) {							// police, military & alien flags set in notable_ent
		var markOffender = dist <= scannerRange;
		if( dist > scannerRange && FarStatus ) {
			// with FarStatus, once of an offender is seen w/i scannerRange, it's status is remembered when it leaves
			let idx, have_scanned = false;
			if( grow_hidden_scanned.length > 0 ) {
				idx = index_in_list( ent, grow_hidden_scanned );
				if( idx >= 0 )
					have_scanned = grow_hidden_scanned[ idx ].have_scanned;
			} else {
				idx = _Sighting_index( ent, 'classify_ship' );
				if( idx >= 0 )
					have_scanned = mapping[ idx ].have_scanned;
			}
			if( have_scanned === true || have_scanned === -1 ) {	// has been seen inside scannerRange
				markOffender = true;
			}
		}
		if( markOffender && ent.isPirate ) {
			staticMFD |= MFD_PIRATES;
		}
		if( markOffender && ent.isPirateVictim ) {					// include in pirate filter if under attack by pirates
			let defenseTargets = ent.defenseTargets;
			for( let idx = 0, len = defenseTargets.length; idx < len; idx ++ ) {
				if( defenseTargets[ idx ].isPirate ) {
					staticMFD |= MFD_PIRATES;
					break;
				}
			}
		}
		if( ent.isTrader ) {
			staticMFD |= MFD_TRADERS;
		}
		var group = ent.group,
			areTraders = false;
		if( group ) {
			let leader = group.leader;
			if( leader && leader.isTrader ) {
				staticMFD |= MFD_TRADERS;
				areTraders = true;									// having found group are traders, don't need to check escorts
			}
		}
		group = ent.escortGroup;
		if( group && !areTraders ) {
			let leader = group.leader;
			if( leader && leader.isTrader ) {
				staticMFD |= MFD_TRADERS;
			}
		}
		if( (staticMFD & MFD_TRADERS) || (staticMFD & MFD_POLICE)
				|| (staticMFD & MFD_PIRATES)
				|| (staticMFD & MFD_MILITARY)
				|| (staticMFD & MFD_ALIENS) ) {						// not NEUTRAL
			return;
		}
		staticMFD |= MFD_NEUTRAL; 									// all other ships that are not pirates or traders (e.g., miners, hunters, etc.)
	}
	function isGalactic_Navy( ent ) {
		var roles = ent.roles;
		for( let idx = 0, len = roles.length; idx < len; idx++ ) {
			let role = roles[ idx ];
			if( role === 'SeccomLocator' || role === 'seccom-medship'
					|| role === 'GN_sortie_target' || role ===  'navyradar'
					|| role === 'kurtz-pod' || role === 'nelly_crew' ) {
				return true;
			}
			let dash = role.indexOf( '-' );
			if( dash !== -1 ) {
				let pref = role.slice( 0, dash );
				if( pref === 'intercept' || pref === 'reserve'
						|| pref === 'picket' || pref ===  'patrol'
						|| pref === 'hofd' || pref === 'galNavy' ) {
					return true;
				} else if( pref === 'navy' ) {
					if( dataKey < 0 ) dataKey = ent.dataKey;
					if( dataKey === 'FA_Titan'
							|| dataKey === 'FA_Sunracer_N' ) { // Montana's Far_Arm_Ships.OXZ
						return false;
					}
					return true;
				}
			} else {
				let pref = role.slice( 0, 5 );
				if( pref === 'navyS' || pref === 'navys' ) {
					return true;
				}
			}
		}
	}
///
	function notable_ent( ent, using_past_range, dist, restoring ) {// ALL calls must be preceeded by reset_common_vars
																	//	- not done here as there may be some vars set before call
																	// 'using_past_range' only true in call from grow_new_list, to save distance
																	//	 calculations when batch processing ents sorted by distance
																	// 'restoring' is only used for ents in grow_hidden_scanned
		var is_past_range;
		if( !using_past_range ) past_range = 0;						// also being called from _add_Sighting, check_Sightings, update_one_Sighting
		if( scanClass < 0 ) scanClass = ent.scanClass;
		if( scanClass === 'CLASS_VISUAL_EFFECT'
				|| scanClass === 'CLASS_PLAYER' ) {
			return false;
		}
		if( status < 0 ) status = ent.status;
		if( _has_bad_status( ent, status ) )
			return false;
		switch( scanClass ) {
			case 'CLASS_BUOY':
				ve_colour = 'green';
				isBuoy = true;
				is_past_range = set_range( ent, 1, using_past_range, dist );
				isBeacon = is_beacon( ent );						// can't assume all buoys are beacons // fn tests isBeacon < 0
				if( isBeacon ) staticMFD |= MFD_NAVIGATION;
				if( !isBeacon && is_past_range ) return false;
				if( !isBeacon && eclipsed( ent, null, (distance < 0 ? dist : distance) ) )
					return false;
				rank = is_past_range ? 'nsr' : 'loc';
				break;
			case 'CLASS_CARGO':										// cargo, pods & scoopable minables; all have distance < scannerRange_X_2
				ve_colour = 'white';
				is_past_range = set_range( ent, 1, using_past_range, dist );
				if( !ext_ok && is_past_range ) return false;
				shipClassName = ent.shipClassName;
				if( shipClassName === 'Splinter' || shipClassName === 'Boulder'
						|| shipClassName === 'Metal fragment' ) {
					if( is_past_range ) return false;				// discard rocks past scannerRange
					if( eclipsed( ent, null, (distance < 0 ? dist : distance) ) )
						return false;
					rank = 'mng';
					is_minable = true;
					staticMFD |= MFD_MINING;
					return true;
				} else if( shipClassName === 'Thargoid Robot Fighter' ) {
					if( is_past_range ) return false;
					if( eclipsed( ent, null, (distance < 0 ? dist : distance) ) )
						return false;
					rank = 'loc';									//will be like cargo when inactive, ve_colour stays white
					staticMFD |= MFD_ALIENS;
					return true;
				}
				isBeacon = is_beacon( ent );						// some (escape?) pods may have a beacon // fn tests isBeacon < 0
				if( isBeacon ) staticMFD |= MFD_NAVIGATION;
				if( mk_maps && !restoring							// unkown cargo must enter scannerRange in order to obtain its
						&& is_past_range && !isBeacon )				//	 RFID frequency; existing preserved in grow_hidden_scanned
					return false;
				is_past_range = set_range( ent, 2, using_past_range, dist );// can 'see' cargo beyond scannerRange (RFID tags?)
				let isCargo = ent.isCargo;
				if( is_past_range ) {								// exclude all beyond 2 * scannerRange
					if( !isBeacon ) return false;
					if( isCargo ) return false;						// escortdeck ships are 'CLASS_CARGO' but !isCargo, have beacons
					if( isVisible < 1 ) isVisible = ent.isVisible;
					if( !isVisible ) return false;
				}
				if( !isBeacon && eclipsed( ent, null, (distance < 0 ? dist : distance) ) )
					return false;
				if( !getDetected( ent, restoring ) ) return false;
				if( !is_past_range && isBeacon && is_ignored_ship( ent ) )
// - cargo w/ beacons, mainly EscortDeckShip's
// - for escorts: beaconCode = "E"+pad+" "+ship.name;
// - for towed ships: beaconCode = "D"; //Derelict
					return false;									// limit is_ignored_ship calls (expensive) by doing them last
				is_cargo = isCargo;									// escortdeck ships are 'CLASS_CARGO' but !isCargo
				rank = 'loc';										// 'loc' rank, as distance < scannerRange_X_2
				staticMFD |= MFD_SALVAGE;
				break;
			case 'CLASS_MINE':
			case 'CLASS_MISSILE':
				is_past_range = set_range( ent, 1, using_past_range, dist );
				if( is_past_range ) return false;
				if( eclipsed( ent, null, (distance < 0 ? dist : distance) ) )
					return false;
				ve_colour = 'cyan';
				rank = 'isr';										// as distance < scannerRange
				staticMFD |= MFD_WEAPONS;
				break;
			case 'CLASS_THARGOID':
			case 'CLASS_POLICE':
			case 'CLASS_MILITARY':
			case 'CLASS_NEUTRAL':
				if( scanClass === 'CLASS_THARGOID' ) {
					dataKey = ent.dataKey;
					if( dataKey !== 'tharglet' )
						ve_colour = 'red';							//warship is red but tharglet is pink or white
					else
						ve_colour = 'pink';
					isHostile = true;
					staticMFD |= MFD_ALIENS;
				} else if( scanClass === 'CLASS_POLICE' ) {
					if( isGalactic_Navy( ent ) ) {
						staticMFD |= MFD_MILITARY;
					} else {
						staticMFD |= MFD_POLICE;
					}
					ve_colour = 'purple';
				} else if( scanClass === 'CLASS_MILITARY' ) {
					staticMFD |= MFD_MILITARY;
				}
				// pre-filtered by equipment: !ext_ok: < scannerRange, !grav_eq_ok: .isVisible
				if( is_cloaked( ent ) )
					return false;
				// jamming ents are still notable (can be seen, not locked); map.hasJammer gets set using isJamming in mkSighting
				// is_jamming( ent );									// sets isJamming, returns true if effective (ie. scanFilter_ok)
				if( isVisible < 0 ) isVisible = ent.isVisible;
				isBeacon = is_beacon( ent );						// fn tests isBeacon < 0
				if( isBeacon ) staticMFD |= MFD_NAVIGATION;
				if( !isBeacon && eclipsed( ent, null, (distance < 0 ? dist : distance) ) )
					return false;
				if( !getDetected( ent, restoring ) ) return false;
				if( dataKey < 0 ) dataKey = ent.dataKey;
				if( is_drone < 0 && dataKey )
					is_drone = dataKey.indexOf( 'drone' ) >= 0;		// allow drones from HardShips OXP
				if( is_drone ) {									// core 'sees' then over scannerRange, even when !isVisible
					if( distance < 0 ) distance = _detect_distanceTo( ent );
					if( distance > scannerRange ) {					//	 so are treated as special case (incompatable w/ code below)
if( debug ) log(ws.name, 'notable_ent, is_drone && distance > scannerRange, discarding ' + ent );
						return false;
					}
					if( isHostile < 0 ) isHostile = is_hostile( ent );// is_hostile accounts for distance & FarStatus
					ve_colour = isHostile ? 'red' : ve_colour < 0 ? 'yellow' : ve_colour;
if( debug && ve_colour === 'red' ) log(ws.name, 'notable_ent, ve_colour = ' + ve_colour + ', scanClass = ' + scanClass );
					rank = isHostile ? 'bad' : 'isr';				// undetectible beyond scannerRange
					if( scanClass === 'CLASS_NEUTRAL' ) {
						classify_ship( ent, distance );				// set MFD_'s for traders, pirates
					}
					return true;
				}
				is_past_range = set_range( ent, 1, using_past_range, dist );
				if( is_past_range && !ext_ok && !isBeacon ) return false;
				if( distance < 0 ) distance = _detect_distanceTo( ent );
				let in_mapping = -1, dist_visible = scannerRange, lost_target = false;
				if( !isBeacon && is_past_range && ext_ok ) {		// visible detection
					is_past_range = !isVisible;
					if( is_past_range && !grav_eq_ok ) {			// w/o gs, 'Lost target's hang around until beyond 10% of
						in_mapping = mk_maps ? false				//	 visible distance (or next scan)
											 : _Sighting_index( ent, 'notable_ent' ) >= 0;
						if( in_mapping ) {							// only ents in mapping get is_past_range extended, so only they can
							is_past_range = distance > dist_visible * 1.10;//	get assigned a rank of 'ukn' below, ie. become lost target
							lost_target = true;
						}
					}
				}
				if( !isBeacon && !isVisible && is_past_range && !grav_eq_ok )	   // discard unseen ships
					return false;
				if( !isBeacon && is_past_range && gravScanProgress > 0 ) {// gravity scanner detection
					gs_curr = grav_scan_dist( ent, true );			// true gets current range else max detection range
					isVisible = distance < gs_curr;
					is_past_range = !isVisible;
					if( is_past_range ) {
						if( in_mapping < 0 )
							in_mapping = mk_maps ? false
												 : _Sighting_index( ent, 'notable_ent #2' ) >= 0;
						if( in_mapping ) {							// only ents in mapping get is_past_range extended, so only they can
							gs_max = !stationNearby ? gs_curr * 1.10//	 get assigned a rank of 'ukn' below, ie. become lost target
													: grav_scan_dist( ent );
							is_past_range = distance > gs_max;		// w/ gs, 'Lost target's hang around until beyond detectable range,
							lost_target = distance < gs_max;
						}
					}
				}
				if( !isBeacon && !isVisible && is_past_range )		// discard undetectible ships
					return false;
				if( isHostile < 0 ) isHostile = is_hostile( ent );	// is_hostile accounts for distance & FarStatus
				if( isHostile )						rank = 'bad';
				else if( distance < scannerRange )	rank = 'isr';
				else if( isVisible || isBeacon )	rank = 'nsr';
				else if( lost_target )				rank = 'ukn';
				else return false;
				ve_colour = isHostile	  ? 'red' :					// colour police too if hostile
							ve_colour < 0 ? 'yellow' : ve_colour;	// preserve previously assigned colour
				if( ent.isDerelict ) {
					ve_colour = 'blue';
					staticMFD |= MFD_SALVAGE;
				} else if( scanClass === 'CLASS_NEUTRAL' ) {
					classify_ship( ent, distance );					// set MFD_'s for traders, pirates
				}
				if( !isVisible ) {
					if( rank === 'ukn' ) {
						ve_colour = 'gray';							// overrides all other colours
					} else {
						if( mass < 0 )								// mass set this way to catch any scriptInfo
							script_mass = read_scriptInfo( ent );	// sets mass
						ve_colour = mass < 130000 ? 'brown' : 'orange';
					}
				}
				break;
			case 'CLASS_ROCK':										// rocks are either minables or Rock Hermits
				if( dataKey < 0 ) dataKey = ent.dataKey;
				if( dataKey === 'telescopemarker' ) return false;
				is_past_range = set_range( ent, 1, using_past_range, dist );
				if( isFrangible < 0 ) isFrangible = ent.isFrangible;
				if( is_past_range && isFrangible ) return false;	// discard rocks beyond scannerRange; isFrangible works for asteroids
																	//	 and boulders, the smaller rocks (eg. splinters) are CLASS_CARGO
				if( isStation < 0 ) isStation = ent.isStation;		// abandoned rock hermits are !isStation
				isBeacon = is_beacon( ent );						// fn tests isBeacon < 0
				if( isBeacon ) staticMFD |= MFD_NAVIGATION;
				let hidden = -1;
				if( is_past_range ) {								// rock hermits
					if( !ext_ok ) return false;
					if( !isStation &&								// discard abandoned Rock Hermits beyond scannerRange
							!(grav_eq_ok && (small_ok || large_ok)))//	unless has a working dish
						return false;
					if( isVisible < 0 ) isVisible = ent.isVisible
					if( !isBeacon && !isVisible ) return false;
					hidden = !isBeacon && eclipsed( ent, null, (distance < 0 ? dist : distance) );
					if( hidden ) return false;
					if( isStation && !getDetected( ent, restoring ) ) {// rock hermit; abandoned ones are !isStation
						return false;
					}
				} // else isFrangible
				if( !isStation && !getDetected( ent, restoring ) )	// stealth mines are rocks
					return false;
				is_past_range = set_range( ent, 4, using_past_range, dist );
				if( hidden === -1 )
					hidden = !isBeacon && eclipsed( ent, null, (distance < 0 ? dist : distance) );
				if( hidden ) return false;
				isPiloted = ent.isPiloted;
				if( isFrangible && !isPiloted ) {					// only those < scannerRange or !isFrangible get here
					ve_colour = 'white';							//	 so distance calc not required
					rank = 'mng';
					is_minable = true;
					staticMFD |= MFD_MINING;
				} else if( isStation ) {							// abandoned Rock Hermits are !isStation but are isMinable
					staticMFD |= MFD_STATION;
					ve_colour = 'green';
					rank = is_past_range ? 'nsr' : 'loc';
				} else { // isFrangible
					if( isPiloted ) {								// lave.oxp has piloted rocks!
						ve_colour = 'white';						// since isFrangible, must be in scannerRange
						rank = 'loc';								// grouped w/ cargo
					} else if( grav_eq_ok && (small_ok || large_ok) &&
								ent.isMinable ) {					// allows 'Abandoned Rock Hermit' for working dishes
						staticMFD |= MFD_STATION;
						staticMFD |= MFD_MINING;
						rank = distance < scannerRange_X_4 ? 'loc' : 'nsr';
						ve_colour = 'pink';
					} else
						return false;								// fallback -> discard
				}
				break;
			case 'CLASS_STATION':
				ve_colour = 'green';
				isStation = true;
				staticMFD |= MFD_STATION;
				isBeacon = is_beacon( ent );						// fn tests isBeacon < 0
				if( isBeacon ) staticMFD |= MFD_NAVIGATION;
				if( isVisible < 0 ) isVisible = ent.isVisible;
				is_past_range = ext_ok ? !isVisible
									   : set_range( ent, 1, using_past_range, dist );
				// jamming ents are still notable (can be seen, not locked); map.hasJammer gets set using isJamming in mkSighting
				// is_jamming( ent );									// sets isJamming, returns true if effective (ie. scanFilter_ok)
				if( !isBeacon ) {
					if( is_past_range )
						return false;
					if( eclipsed( ent, null, (distance < 0 ? dist : distance) ) )
						return false;
					if( !getDetected( ent, restoring ) )
						return false;
				}
				if( distance <= scannerRange ) {
					rank = 'isr';
				} else {
					rank = 'nsr';
					if( is_past_range && !isBeacon
							&& _Sighting_index( ent, 'notable_ent' ) >= 0 ) {// only known (ie. existing) ents can become lost
						rank = 'ukn';
					}
				}
				break;
			case 'CLASS_NO_DRAW':
				if( radius < 0 ) radius = ent.radius || false;
				if( !radius ) return false;							// not an orb, probably wreckage
				if( ext_ok ) {
					if( isVisible < 0 ) isVisible = ent.isVisible;
					is_past_range = isVisible;						// sun, planets & moons always visible?
				} else
					is_past_range = set_range( ent, 1, using_past_range, dist );
				ve_colour = 'lightgray';
				rank = is_past_range ? 'orb' : 'loc';
				isSun = ent.isSun;
				isPlanet = !isSun;
				staticMFD |= MFD_CELESTIAL;
				break;
			case 'CLASS_WORMHOLE':
				is_past_range = set_range( ent, ext_ok ? 4 : 1, using_past_range, dist ); // scannerRange_X_4 = 102400, an arbitrary choice
				if( is_past_range ) return false;
				if( collisionRadius < 0 ) collisionRadius = ent.collisionRadius;
				if( collisionRadius === 0 ) return false;			// wormhole has evaporated
				if( eclipsed( ent, null, (distance < 0 ? dist : distance) ) )
					return false;
				ve_colour = 'blue';
				rank = distance < scannerRange ? 'loc' : 'nsr';
				isWormhole = true;
				staticMFD |= MFD_NAVIGATION;
				break;
			default:												// what slips thru
if( debug && scanClass !== undefined && scanClass !== 'CLASS_VISUAL_EFFECT' && scanClass !== 'CLASS_PLAYER' )
	log(ws.name, 'notable_ent, MISSING case for scanClass ' + scanClass );
				return false;
		}
		// no code here unless repl. 'return true' cases above
		return true;
	}
///
// profiling functions ////////////////////////////////////////////////////////////////////////////
var profiling = false;
/*	//!cagiife
function profile_create() {	 _profile_growing( true, false, false ); }
function profile_update() {	 _profile_growing( false, true, false ); }
function profile_refresh() { _profile_growing( false, false, true ); }
function _profile_growing( create, update, refresh ) {
	function profiled_code() { _call_pending( 1 ); }
	function profile_run( fn, result_array ) {
		var profile, fname, title, total, jstime, start, end, saved;
		set_fn_pending( fn );
		while( tasks_pending.length ) {
			title = fname = tasks_pending[0].fn.name;
			if( fname.length > 12 )
				fname = fname.substr( 0, 13 );
			else
				fname += '		  '.slice( fname.length - 13 );
			console.writeJSMemoryStats();
			profile = console.profile( profiled_code, worldScripts.telescope._Sightings_closure );
			start = profile.indexOf( ':' ) + 2; // ': '
			end = saved = profile.indexOf( ' ms');
			total = parseFloat( profile.slice( start, end ) );
			start = profile.indexOf( ':', end ) + 2; // ': '
			end = profile.indexOf( ' ms', start );
			jstime = parseFloat( profile.slice( start, end ) );
			result_array.push([ fname, total, jstime ]);
			log(ws.name, '\n\nprofiling ' + title +'():	 Total time: '	+ total
						   + ' ms\n			 ==' + '========================'.substr( 0, title.length )
						   + '				 =====' + (total < 10 ?	 '\n': '=\n'  ) + profile.substr( saved + 4 ) );
		}
	}
	clear_all_pending();
	let saved_debug = debug;
	debug = false;
	profiling = true;
	console.clearConsole();
	var creating = null, updating = null, refreshing = null;
	console.garbageCollect();
	if( create ) {
		creating = [];
		profile_run( _create_Sightings, creating );
		console.writeJSMemoryStats();
	}
	if( update ) {
		updating = [];
		profile_run( _update_Sightings, updating );
		console.writeJSMemoryStats();
	}
	if( refresh ) {
		refreshing = [];
		profile_run( refresh_Sightings, refreshing );
		console.writeJSMemoryStats();
	}
	report_timings( creating, updating, refreshing );
	profiling = false;
	debug = saved_debug;
}
*/	//!cagiife
/*
ws.time_create()
ws.profile_create()
JavaScript heap: 5.11 MiB (limit 32.00 MiB, 106 collections to date)
JavaScript heap: 5.11 MiB (limit 32.00 MiB, 106 collections to date)
JavaScript heap: 5.11 MiB (limit 32.00 MiB, 106 collections to date) => no garbage!
_create_Sightings (12)		_update_Sightings (12)		updating is:
======================		======================		============
_create_Sight = 0.9050		_update_Sight = 0.3110		65.6% faster (diff:	 0.5940, js:  0.5830)
grow_new_list = 1.8510		grow_new_list = 1.3360		27.8% faster (diff:	 0.5150, js:  0.4940)
grow_new_list = 2.0140		grow_new_list = 1.9240		 4.5% faster (diff:	 0.0900, js:  0.0740)
grow_new_list = 1.5700		grow_new_list = 1.7830	   -13.6% slower (diff: -0.2130, js: -0.1470)
grow_new_list = 1.3030		grow_new_list = 1.2590		 3.4% faster (diff:	 0.0440, js:  0.0530)
grow_new_list = 0.9010		grow_new_list = 0.8300		 7.9% faster (diff:	 0.0710, js:  0.0370)
grow_new_list = 2.2960		grow_new_list = 1.8980		17.3% faster (diff:	 0.3980, js:  0.3700)
grow_new_list = 2.7040		grow_new_list = 2.5930		 4.1% faster (diff:	 0.1110, js:  0.1280)
grow_new_list = 1.3190		grow_new_list = 1.0830		17.9% faster (diff:	 0.2360, js:  0.1510)
grow_new_list = 0.6150		grow_new_list = 0.4360		29.1% faster (diff:	 0.1790, js:  0.1510)
grow_new_list = 1.2130		grow_new_list = 0.1310		89.2% faster (diff:	 1.0820, js:  1.0860)
grow_new_list = 0.3260		grow_new_list = 0.2070		36.5% faster (diff:	 0.1190, js:  0.1190)
			   =======					   =======		======
	creating:  17.0170			 updating: 13.7910		updating is 18.96% faster
*/
/*	//!cagiife
function set_profiling() { profiling = true; } // debug access to glocal
function clear_profiling() { profiling = false; } // debug access to glocal
function time_create( solo ) { _time_growing( true, solo ? false : true, false ); }
function time_update( solo ) { _time_growing( false, true, solo ? false : true ); }
function time_refresh() { _time_growing( false, false, true ); }
function _time_growing( create, update, refresh ) {
	function profiled_code() { _call_pending( 1 ); }
	function profile_run( fn, result_array ) {
		var profile, fname, total, jstime, parm;
		set_fn_pending( fn );
		while( tasks_pending.length ) {
			fname = tasks_pending[0].fn.name;
			parm = tasks_pending[0].parm;
			profile = console.getProfile( profiled_code, worldScripts.telescope._Sightings_closure );
			total = ( profile.totalTime * 1000 );
			jstime = ( profile.javaScriptTime * 1000 );
			if( fname.length > 12 )
				fname = fname.substr( 0, 13 );
			else
				fname += '		  '.slice( fname.length - 13 );
			result_array.push([ fname, total, jstime, parm ]);
		}
	}
	try {
		clear_all_pending();
		let saved_debug = debug;
		debug = false;
		profiling = true;
		console.garbageCollect();
		console.writeJSMemoryStats();
		var creating = null, updating = null, refreshing = null;
		if( create && update && refresh ) {
			log(ws.name, '_time_growing, support for all 3 simultaneously NOT supported' );
			return;
		}
		if( !create && !update && !refresh ) create = update = true;
		if( create ) {
			creating = [];
			profile_run( _create_Sightings, creating );
			console.writeJSMemoryStats();
		}
		if( update ) {
			updating = [];
			profile_run( _update_Sightings, updating );
			console.writeJSMemoryStats();
		}
		if( refresh ) {
			refreshing = [];
			profile_run( refresh_Sightings, refreshing );
			console.writeJSMemoryStats();
		}
		report_timings( creating, updating, refreshing );
		profiling = false;
		debug = saved_debug;
	} catch( err ) {
		log( ws.name, ws._reportError( err, 'time_create' ) );
		if( debug ) throw err;
	}
}
function report_timings( creating, updating, refreshing ) {
	var first_col = null, second_col = null;
	if( creating ) {
		first_col = creating;
		if( updating ) second_col = updating;
		else if( refreshing ) second_col = refreshing;
	} else if( updating ) {
		first_col = updating;
		if( refreshing ) second_col = refreshing;
	} else if( refreshing ) {
		first_col = refreshing;
	}
	var ctotal, utotal, cstep, ustep, cjs, ujs, diff, last_c = false, last_u = false;
	var csum = 0, usum = 0, cjsum = 0, ujsum = 0, out_c = true, out_u = true;
	var out, ci, ui, jspercent, last_cr, last_up, parm, spacer = '		';
	var cr_len = first_col ? first_col.length : 0;
	var up_len = second_col ? second_col.length : 0;
	var both = first_col && second_col;
//	out = '_create_Sightings ('+cr_len+')	   _update_Sightings ('+up_len+')	   updating is:'
	out = '\n' + (creating ? '_create' : updating ? '_update' : 'refresh');
	//if( first_col )
		out += '_Sightings ('+cr_len+')';
	if( both ) out += spacer;
	if( second_col ) out += (creating ? (updating ? '_update' : 'refresh') : 'refresh') + '_Sightings ('+up_len+')';
	if( both ) out += spacer + (creating ? (updating ? 'updat' : 'refresh') : 'refresh') + 'ing is:';
//			 +'\n======================		 ======================		 ============';
	out += '\n';
	if( first_col ) out += '======================';
	if( both ) out += spacer;
	if( second_col ) out += '======================';
	if( both ) out += spacer + '============';
var count = 0;
	for( ci = 0, ui = 0; ci < cr_len || ui < up_len;  ) { // cr_len !== up_len
		out += '\n';
		if( out_c && ci < cr_len ) [ cstep, ctotal, cjs, parm ] = first_col[ ci ];
		if( out_u && ui < up_len ) [ ustep, utotal, ujs, parm ] = second_col[ ui ];
		if( both ) {
			out_c = last_c || ci < cr_len
								&& ( ci === 0 || cstep === ustep	// 1st row is always diff
									 || cstep === last_cr );		// complete run of same steps
			out_u = last_u || ui < up_len
								&& ( ui === 0 || ustep === cstep	// 1st row is always diff
									 || ustep === last_up );		// complete run of same steps
		} else {
			out_c = true;
			out_u = false;
		}
		if( !out_c && !out_u )
				 if( ci < cr_len && ci < ui ) out_c = true;			// allow shorter to catch up
			else if( ui < up_len && ui < ci ) out_u = true;
			else if( ci === ui ) {
				if( cr_len < up_len ) out_u = true;
				else				  out_c = true;
			} else if( ci < cr_len ) out_c = true;					// go w/ unfinished one
			else   if( ui < up_len ) out_u = true;
if( !out_c && !out_u ) { log(ws.name, 'time_create, stalled w/ cstep = ' + first_col[ ci ][0] + ', ustep = ' + second_col[ ui ][0] ); break; }
		if( !out_c && !out_u ) out_c = true;						// create has extra grow_new_list entries
		if( out_c && !last_c ) {
			ci++;
			csum += ctotal;
			cjsum += cjs;
			last_cr = cstep;
			let txt = cstep + ' = ' + ctotal.toFixed( 4 );
			if( both && ci === cr_len ) {
				last_c = txt;
				if( ui < up_len - 1 ) out += '						';
			} else out += txt;
		} else if( both && !last_c && !last_u ) {
			out += '					  ';
		}
		if( out_u && !last_u ) {
			ui++;
			usum += utotal;
			ujsum += ujs;
			last_up = ustep;
			let txt = (first_col ? spacer : '') + ustep + ' = ' + utotal.toFixed( 4 );
			if( both && ui === up_len ) {
				last_u = txt;
				if( ci < cr_len ) out += '							  ';
			} else out += txt;
		} else if( both && !last_c ) {
			out += '							';
		}
		if( out_c && out_u && !last_c && !last_u || (last_c && last_u)) {
			if( last_c && last_u ) out += last_c + last_u;
			diff = ((ctotal - utotal) / ctotal * 100).toFixed( 1 );
			diff = '		 '.slice( diff.length-8 ) + diff;
			let totdiff = (ctotal - utotal).toFixed(4);
			let jsdiff = (cjs - ujs).toFixed(4);
			out += '  ' +  diff + '% '+( diff < 0 ? 'slower' :'faster' )
				+' (diff: '+(totdiff > 0 ? ' '+totdiff : totdiff)
				+', js: '+(jsdiff > 0 ? ' '+jsdiff : jsdiff)+')';
		} else if( out_u && !last_c ) {
			jspercent = (ujs / utotal * 100).toFixed( 1 );
			out += spacer + 'native: '+ (utotal - ujs).toFixed(4) +', js: '+ ujs.toFixed(4)+' ('+jspercent+'%)';
		} else if( out_c && !last_u ) {
			jspercent = (cjs / ctotal * 100).toFixed( 1 );
			out += spacer + 'native: '+ (ctotal - cjs).toFixed(4) +', js: '+ cjs.toFixed(4)+' ('+jspercent+'%)';
		}
		if( parm ) out += '	   parm: ' + parm;
		if( last_c && (!both || last_u )) break;
if( ++count > 25 ) break;
	}
	if( both ) diff = ((csum - usum) / csum * 100).toFixed( 2 );
//	out += '\n				 =======					 =======	  ======';
	out += '\n';
	if( first_col ) out += '			   =======';
	if( both ) out += spacer;
	if( second_col ) out += '				=======';
	if( both ) out += spacer + '======';
//	out += '\n	  creating:	 ' +csum.toFixed( 4 )+ '		   updating: ' +usum.toFixed( 4 )+ '	  updating is '
//		+ diff + '% '+( diff < 0 ? 'slower' :'faster' );
	out += '\n';
	if( first_col ) out += (creating ? '	creating:  ' : updating ? '		updating: ' : '	  refreshing: ') +csum.toFixed( 4 );
	if( both ) out += spacer;
	if( second_col ) out += (updating ? '	  updating: ' : '	refreshing: ') +usum.toFixed( 4 );
	if( both ) out += spacer + (updating ? 'updating' : 'refreshing') + ' is '+ diff + '% '+( diff < 0 ? 'slower' :'faster' );
	log(ws.name, out );
	profiling = false;
}
*/	//!cagiife
///////////////////////////////////////////////////////////////////////////////////////////////////
// _target_marker_closure /////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////
	function _update_target_marker() {								// FCB to make and update shadow and vmarkship
		try {
			if( have_shutdown )
				return;
			var map = curr_S.map;
			if( !equip_ok ) {										// 1st FCB fn called, so responsible for orderly shutdown if equipment damaged
				if( map )
					_set_curr_Sighting( null, '_update_target_marker' );// no parms resets
				_newList();
				have_shutdown = true;
				return;
			}
			if( !map )
				return;
			var marker = curr_S.marker;
			if( marker && !marker.isValid ) {
				marker = removeMarker();							// convenience return of null
			}
			var ent = curr_S.ent;
			if( !ent || _has_bad_status( ent ) ) {
				_set_curr_Sighting( null, '_update_target_marker' );// no parms resets
				return;
			}
			reset_common_vars();
			// ensure target's distance is up to date so setting ps.target will always succeed
			// - when crossing boundary, must be exact if switching from far to near as cannot
			//   set ps.target to an ent that is scannerRange + 0.000000001 distant!
			distance = map.ent_dist = _detect_distanceTo( ent );
			radius = ent.radius || false;
			if( !marker
				|| crossing_boundary( map, distance + ent.collisionRadius,
									  curr_S.marker_type, radius, '_update_target_marker' ) ) {
				_manage_marker( map, false, '_update_target_marker (IdentKeyPress = '
											+ identKeyPress + ')' );
				if( !marker ) {
					marker = curr_S.marker;
					if( !marker ) {
						using_common_vars = false;
						return;
					}
				}
			} else {
				calc_marker_posn( map, distance, radius );
				marker.position = marker_posn;
				marker.velocity = ps_velocity;						//keep target over Torus speeds in FarPlanets OXP
			}
			if( curr_S.marker_type === 'marker' ) {  				// update km
				let displayName = set_displayName( map );
				let distAndUnits = distWithUnits( distance );
				marker.displayName = distAndUnits + ' ' + displayName;
			}
			using_common_vars = false;
		} catch( err ) {
			log( ws.name, ws._reportError( err, '_update_target_marker' ) );
			if( debug ) throw err;
		}
	}
	var marker_posn = [];
	var target_posn = [];
	function calc_marker_posn( map, map_ent_dist, is_an_orb ) {
		copy_vector( map.last_posn, position );
		copy_vector( position, target_posn );
		subtract_vectors( position, ps_position, target_vector );
		unit_vector( target_vector, target_direction );				// unit vector to target (or to last known pos)
		let marker_dist = scannerRange - 600;
		// - the shadow lollipop lies in front of the lightballs (sR - 400 in lb_position) to reduce flickering
		//do not set closer to scannerRange so won't leave behind aft markers during torus travel
		if( map_ent_dist < scannerRange && is_an_orb ) {			// close to a planet, moon or suns
			if( (map_ent_dist - 300) < marker_dist ) {
				marker_dist = map_ent_dist - 300;					// min( marker_dist, map_ent_dist - 299.6 ) from old code
			}
			if( marker_dist < ps_collisionRadius )					// so we don't collide with marker
				marker_dist = ps_collisionRadius;
		}
		scale_vector( target_direction, marker_dist, vector )
		add_vectors( ps_position, vector, marker_posn );
		if( moving_fast )
			apply_speed_adj( marker_posn );
	}
	function removeMarker() {
		if( curr_S.marker ) {
/*
			if( curr_S.marker_type === 'marker' ) {
				curr_S.marker.removeCollisionException(ps);
			} // not really necessary
 */
			curr_S.marker.$TelescopeTarget = null;
			curr_S.marker.remove();
		}
		curr_S.marker = null;
		curr_S.marker_type = '';
		return null;												// convenience return
	}
	function manage_marker( new_map, showName, caller ) {
		try {
			_manage_marker( new_map, showName, caller );
		} catch( err ) {
			log( ws.name, ws._reportError( err, 'manage_marker', [showName, caller] ) );
			if( debug ) throw err;
		}
	}
	function _manage_marker( new_map, showName, caller ) {
		if( !mappingReady || have_shutdown ) return;				// not yet built OR empty
/*
if( debug ) {
	log(ws.name, '\n_manage_marker, new_map'
		+ (new_map && new_map.ent ? ' (' + new_map.ent.entityPersonality + '): ' + new_map.ent.displayName : ': ' + new_map)
		+ ', showName: ' + showName + ', caller: ' + caller );
}
 */
		var ent, new_marker, map = null;
		var index = new_map === null ? -1 : _Sighting_index( new_map, '_manage_marker' );
		var actual = curr_S.map || false;							// ? may have died, docked, jumped, out of range, etc., ?must remove telescopemarker
		// check we have a valid target (current or new)
		var still_alive = actual && actual.ent && actual.ent.isValid;// if he's ok, may have to just update marker position
		if( index < 0 && still_alive ) {							// no new target, fetch existing one
			map = actual;
		} else {													// get ready to switch targets
			map = index >= 0 && index < maplen ? mapping[ index ] : null;
		}
/*
if( debug ) {
	log(ws.name, '  index: ' + index + ', actual'
		+ (actual && actual.ent ? ' (' + actual.ent.entityPersonality + '): ' + actual.ent.displayName : ': ' + actual)
		+ ', still_alive: ' + still_alive + ', map'
		+ (map && map.ent ? ' (' + map.ent.entityPersonality + '): ' + map.ent.displayName : ': ' + map) );
}
 */
		if( !map ) {												// nothing to do
			return;
		}
		ent = map.ent;
		if( _has_bad_status( ent ) ) {								// target died, jumped or docked, clean up
			if( actual && new_map === actual ) {					// ent is not a new target, reset curr_S
				_set_curr_Sighting( null, '_manage_marker, _has_bad_status, via ' + caller );
			} // else we just don't switch to new ent (ie. stay w/ curr_S.map)
			return;
		}
		if( is_jamming( ent ) ) {									// ent just started jamming
			if( actual && new_map === actual ) {					// jammer is current target, reset curr_S
				_set_curr_Sighting( null, '_manage_marker, is_jamming, via ' + caller );
			} // else we just don't switch to new ent (ie. stay w/ curr_S.map)
			return;
		}
 		if( is_cloaked( ent ) ) {									// ent cloaked before we got here!
			if( actual && new_map === actual ) {					// current target just cloaked, reset curr_S
				_set_curr_Sighting( null, '_manage_marker, is_cloaked, via ' + caller );
			} // else we just don't switch to new ent (ie. stay w/ curr_S.map)
			_delete_Sighting( map, '_manage_marker, via ' + caller );// telescope cannot 'see' cloaked ships
			return;
		}
		if( radius < 0 || !using_common_vars )
			radius = ent.radius || false;							// only orbs have .radius
		if( map !== actual ) {										// update curr_S.name
			set_displayName( map );
		}
		// determine if we keep current marker (reposition) or must create new one
		var marker = curr_S.marker || null;
		if( marker && !marker.isValid ) {
			marker = removeMarker();								// convenience return of null
		}
		if( ent.isWormhole ) {
			// core requires user to manually target wormhole (player hits 'u', 'r') for the wormhole scanner to work
			// we cannot target a wormhole, only the user can; setting ps.target = wormhole generates exception
			//	 "Exception: Error: Cannot set property target of instance of PlayerShip to invalid value"
			_handle_wormhole( ent );								// mimic core wormhole scanner
			if( (!curr_S || curr_S.ent !== ent) && ent.$TelescopeScanStart !== undefined ) {
				_set_curr_Sighting( ent, '_manage_marker wormhole, via ' + caller  );
			}
			return;
		}
		var mark_type = curr_S.marker_type || '';
		// edge for near/far targets is .distanceTo === scannerRange, regardless of any radius (core tries for 25k)
		// => marker should read _detect_distanceTo, ie. position.distanceTo - ent.collisionRadius
		let map_ent_dist = map.ent_dist;							// now updated every frame in reposition_effects
		new_marker = true;
		do {														// determine if we may need a new marker
			if( !marker || mark_type === '' ) break;				//	- launching or leaving witchspace
			if( curr_target === null ) break;						//	- had no target
			if( crossing_boundary( map, map_ent_dist, mark_type,	//	- ent has crossed scannerRange
						radius, '_manage_marker, via ' + caller ) )	//		will switch marker type
				break;
			new_marker = false;										// marker checks out ok, we can just move it
		} while( false );
		let ent_dist = map_ent_dist + hullOffset( ent );			// reverse adj from _detect_distanceTo, so it's .distanceTo
																	//	 collisionRadius << scannerRange
/*
if( debug ) {
	log(ws.name, '  for new_marker: ' + new_marker + ', mark_type: ' + mark_type
		+ ', ent_dist: ' + ent_dist.toFixed(2)
		+ ' (map_ent_dist: ' + map_ent_dist.toFixed(2) + ' + hullOffset( ent ): ' + hullOffset( ent ).toFixed(2)
		+ ' vs distanceTo: ' + ps.position.distanceTo( ent ).toFixed(2)
		+ '\n    marker: ' + marker + '\n    curr_target: ' + curr_target  );
}
 */
		mark_type = ent_dist < scannerRange && !radius				// sun/planet/moon must remain far targets as core does not
					? '-shadow' : 'marker';							//	 allow ship to target them; esp a prob w/ small moons with
																	//	 collisionRadius << scannerRange
		// do the actual updating of target & marker
		calc_marker_posn( map, map_ent_dist, radius );
		if( markerInsideOrb( map ) ) {
			_set_curr_Sighting( null, '_manage_marker, markerInsideOrb, via ' + caller );
			return;
		}
/*
if( debug ) {
	log(ws.name, '  new_marker: ' + new_marker + ', mark_type: ' + mark_type
		+ ', identKeyPress: ' + identKeyPress + ' (ws$: ' + ws.$IdentKeyPress
		+'), \n  curr_target' + (curr_target ? ' (' + curr_target.entityPersonality + '): ' + curr_target.displayName : ': ' + curr_target) );
}
 */
		if( !new_marker ) {											// just move the existing telescopemarker, so marker & marker_type stay the same
			marker.position = marker_posn;
			marker.velocity = ps_velocity;							//keep target over Torus speeds in FarPlanets OXP
			if( mark_type === '-shadow' ) {
				if( curr_target !== ent ) {							// switch from one near target to another near one
					switch_PS_target( ent, map, showName, '1_manage_marker, !new_marker, via ' + caller );
				} else if( identKeyPress === IDENT_LOCKED && !ps.target ) {
					switch_PS_target( ent, map, showName, '2_manage_marker, !ps.target but known marker, via ' + caller );
				}
			} else if( mark_type === 'marker' ) {					// have switched from a far target to a different far one, where
				if( curr_S.map !== map ) {							//	 _switch_PS_target is not called for far targets (it's always the target marker)
					if( !curr_S.ent.radius ) {						// not an orb
						marker.removeCollisionException( curr_S.ent ); // remove previous target
					}
					if( !radius ) {									// not an orb
						marker.addCollisionException( ent );		// avoid impacting target
					}
					marker.displayName = set_displayName( map );
					_set_curr_Sighting( map, '_manage_marker, !new_marker, via ' + caller );
					if( showName ) {
						init_headingView();
						showTargetName( map );
					}
					_showVShip( ent.dataKey );
				}
				if( !ps.target ) {	// added to support ident sequence (restore after target lost)
				// } else if( identKeyPress === IDENT_LOCKED && !ps.target ) {	// added to support ident sequence (restore after target lost)
					switch_PS_target( marker, map, showName, '3_manage_marker, !ps.target but known marker, via ' + caller );
				}
			}
// since re-using marker (& not chg'g ps.target), get no new 'ID locked' verbal msg
//	- no msg if browsing (!weaponsOnline) otherwise must hit ident key to chg far target, so do get verbal msg
		} else {													// create a new one (it gets remove()'d there)
			if( marker ) removeMarker();							// remove any existing marker/shadow
			curr_S.marker_type = mark_type;
			if( mark_type === 'marker' ) {							// addShips(role, count, position, radius);
				marker = addShips( 'telescopemarker', 1, marker_posn, 1 )[ 0 ];
				marker.addCollisionException( ps );					// avoid limiting Torus speed (Milo & dybal)
				if( !radius ) {										// not an orb
					marker.addCollisionException( ent );			// avoid impacting target
				}
			} else {												//replace the markership with visuall effect shadow lollipop
				marker = addVisualEffect( 'telescope-shadow', marker_posn );
			}
			if( marker ) {
				marker.velocity = ps_velocity;						//keep target over Torus speeds in FarPlanets OXP
				curr_S.marker = marker;
				if( mark_type === 'marker' ) {
					switch_PS_target( marker, map, showName, '4_manage_marker, new marker, via ' + caller );
				} else if( mark_type === '-shadow' ) {
					switch_PS_target( ent, map, showName, '5_manage_marker, new -shadow, via ' + caller );
				}
			} else {
				log(ws.name, '_manage_marker, unable to create marker; shutting down telescope operations!' );
				_shutdown_Sightings();
			}
		}
	}
/* when a user manually targets a wormhole, the wormhole scanner, if present, begins & after about 7 sec.s reports
   its findings. when user targets it, we add property $TelescopeScanStart to emulate the scanner and update its displayName
*/
	function _handle_wormhole( ent ) {								// mimic behavior of core game wormhole scanner
		if( !ent || !ent.isValid || ent.collisionRadius === 0 ) {	// expired wormholes eventually (!) become invalid
			_delete_Sighting( ent, '_handle_wormhole' );
			return;
		}
		if( ps.equipmentStatus( 'EQ_WORMHOLE_SCANNER' ) !== 'EQUIPMENT_OK' )
			return;
		if( !curr_target || curr_target !== ent )					// wait for it to be targetted before starting countdown
			return;
		if( ent.$TelescopeScanStart === undefined )	{				// save time acquired, to display destination after wormhole scanner (using 7 sec)
			ent.$TelescopeScanStart = clock.seconds;
			if( !ent.displayName ) {								// if not named by someone else
				ent.displayName = ent.$TelescopeName = 'Wormhole';	// name is 'wormhole' when Sighting created
			}
		} else if( clock.seconds - ent.$TelescopeScanStart >= 7 ) {	// once 7 seconds have expired, we'll update the displayName w/ the destination
			if( ent.$TelescopeName )								// if we named it
				ent.displayName = 'Wormhole to ' + System.systemNameForID( ent.destination );
		}
	}
// : ' +  + '
	function switch_PS_target( ent, map, showName, caller ) {
// if( debug ) log(ws.name, 'switch_PS_target, ent: ' + (ent ? ent.displayName : ent) + ', map: ' + (map && map.ent ? map.ent.displayName : map)
// 				+ ', showName: ' + showName + ', caller: ' + caller);
		if( ent === null ) {										// explicitly null target
			ws.$TelescopeTargetSet = true;							// prevent shipTargetAcquired and shipTargetLost treating
			ps.target = null;										//   this assignment as a new target
			ws.$TelescopeTargetSet = false;
			_set_curr_Sighting( null, '_switch_PS_target, ent is null, via ' + caller );
			return;
		}
		if( ps.target === ent ) {									// prevent double ident system verbal message on near targets
			_set_curr_Sighting( map, '_switch_PS_target #3-aborted, via ' + caller );
			_showVShip( map.ent.dataKey );
			return;
		}
		if( curr_S.marker_type === 'marker' ) {						//remove km from ident message
			ent.displayName = set_displayName( map );				// set displayName of telescopemarker
		}
		ws.$TelescopeTargetSet = true;								// prevent shipTargetAcquired and shipTargetLost treating
		ps.target = ent;											//   this assignment as a new target
		ws.$TelescopeTargetSet = false;
		if( ps.target !== ent ) {									// unlockable!
			let cloaked = is_cloaked( ent ),
				bad_status = _has_bad_status( ent );
log(ws.name, 'switch_PS_target, target lock FAILED, ' + caller + ', _has_bad_status: ' + bad_status
				+ ', is_cloaked: ' + cloaked + ', ps.target = ' + ps.target
				+ '\n    ship ent_dist = ' + map.ent_dist + ', .distanceTo( ent ): ' + ps.position.distanceTo( ent ) +': ' + ent );
			if( !bad_status && !cloaked ) {// can take time to register
log(ws.name, '  * UNABLE to lock * ' +  cd._showProps( map, 'map', 1,1,1 ) );
			}
		} else {
			_set_curr_Sighting( map, '_switch_PS_target #4, via ' + caller );// make sure in sync w/ ps.target, in case fn called from other than _manage_marker
			if( showName ) {
				init_headingView();
				showTargetName( map );
			}
			_showVShip( map.ent.dataKey );
		}
	}
	function crossing_boundary( map, dist, mark_type, targetRadius/*, caller*/ ) {// crossing the scannerRange threshold will generate a
																	// shipTargetLost event, we need to change marker type
		if( !map ) return false;
		let ent = map.ent;
		if( !ent || !ent.isValid ) return false;
		var marker_type = mark_type || curr_S.marker_type;
		let isanOrb = targetRadius === null ? ent.radius : targetRadius;// radius can be false
		if( isanOrb ) return marker_type === '-shadow';				// sun/planet/moon can never use shadow marker
		var targetDist = dist;
		if( dist === null ) {										// null dist forces distance update calc
			targetDist = map.ent_dist = _detect_distanceTo( ent );	// distance to hull/surface
		}
		targetDist += hullOffset( ent );							// reverse adj from _detect_distanceTo, so it's .distanceTo
		if( (marker_type === 'marker' && targetDist <= scannerRange)		// just came into scannerRange
			|| (marker_type === '-shadow' && targetDist > scannerRange) ) { // just left scannerRange
			return true;
		}
		return false;
	}
	function mostCentered( mode, chg_by_ident ) {
		try {
			_mostCentered( mode, chg_by_ident );
		} catch( err ) {
			log( ws.name, ws._reportError( err, 'mostCentered', [mode, chg_by_ident] ) );
			if( debug ) throw err;
		}
	}
// : ' +  + '
	function _mostCentered( mode, chg_by_ident ) {					// chg_by_ident is false (explicit in 2 calls), may be true in call from shipTargetLost
																	//	 where target lock lost but ship not destroyed, then ident key press is assumed
																	// called from _auto_updates only when not Steering and IdentKeyPress is IDENT_READY
		if( have_shutdown )
			return;
		var orig = curr_S.map,
			map = orig;
		reset_common_vars();										// ensure no data carries over to new ent
		using_common_vars = false; 									// not using glocals as code path complex & not in fcb
		// when called w/ null for distance, crossing_boundary will update map.ent_dist
		let crossed = crossing_boundary( map, null, null, null, '_mostCentered' );
		if( map && crossed ) {										// keep same target
			_manage_marker( map, false, '_mostCentered (scannerRange)' );
			return;
		}
		var ident_was_pressed = mode === 'ident' && chg_by_ident;	// called from shipTargetLost and target still alive
/*
if( debug && mode === 'ident' ) {
	log('\n_mostCentered, ident_was_pressed: ' + ident_was_pressed
		+ ' ==> mode (' + mode + ') === ident: ' + (mode === 'ident')
		+ ' && chg_by_ident: ' + (chg_by_ident || false) );
	log('\t\t  identKeyPress: ' + identKeyPress + ', orig'
		+ (orig && orig.ent ? ' (' + orig.ent.entityPersonality + '): ' + orig.ent.displayName : ': ' + orig) );
}
 */
		find_most_central( mode );									// sets found_map to a Sighting or null if it fails
		if( !ident_was_pressed || !map ) {
			if( !found_map ) {
// if( debug && mode === 'ident' ) log('  !found_map, bailing');
				return;
			}
			map = found_map;
		}
		if( !map ) {												// may have jumped/died
			_set_curr_Sighting( null, '_mostCentered (!map)' );		// sets identKeyPress to IDENT_READY
// if( debug && mode === 'ident' ) log('  !map, reset curr_S, bailing');
			return;
		}
		isWormhole = map.ent.isWormhole;
		if( orig !== map ) {										// changing to a new target
			ws.$IdentKeyPress = identKeyPress = IDENT_READY;		// reset lock
			if( !isWormhole ) {
				_manage_marker( map, false, '_mostCentered (new target)' );
// if( debug && mode === 'ident' ) log('\n_mostCentered, back from _manage_marker');
			}
/*
if( debug && mode === 'ident' ) {
	log('_mostCentered, orig !== map, ident found new target, identKeyPress := IDENT_READY');
	log('               bailing, new map'
		+ (map && map.ent ? ' (' + map.ent.entityPersonality + '): ' + map.ent.displayName : ': ' + map) );
}
 */
			return;
		}
		if( !ident_was_pressed ) {									// the rest of function is for ident only
			return;
		}
		if( identKeyPress !== IDENT_READY && found_map != orig ) {	// new most centered ship
/*
if( debug && mode === 'ident' ) {
	log('_mostCentered, identKeyPress := IDENT_READY because found_map: '
		+ (found_map && found_map.ent ? ' (' + found_map.ent.entityPersonality + '): ' + found_map.ent.displayName : ': ' + found_map)
		+ ' is != orig: '
		+ (orig && orig.ent ? ' (' + orig.ent.entityPersonality + '): ' + orig.ent.displayName : ': ' + orig) );
}
 */
			ws.$IdentKeyPress = identKeyPress = IDENT_READY;
			map = found_map;
		}
		if( identKeyPress === IDENT_READY ) {
			if( !isWormhole && orig && orig === found_map ) {		// wormhole need extra ident from player
			// if( !isWormhole || (orig && orig !== map) ) {			// wormhole need extra ident from player
				ws.$IdentKeyPress = identKeyPress = IDENT_LOCKED;	// really lock the target
/*
if( debug && mode === 'ident' ) {
	log('_mostCentered, lock the target, identKeyPress := IDENT_LOCKED, curr_target: '
		+ (curr_target ? ' (' + curr_target.entityPersonality + '): ' + curr_target.displayName : curr_target));
}
 */
				if( IdentMessages )
					consoleMessage( 'Telescope locked: ' + curr_S.name, ConsoleMsgDurn );
			}
			_manage_marker( map, false, '_mostCentered (IdentKeyPress = ' + (identKeyPress === 1 ? 'IDENT_LOCKED' : 'IDENT_READY') + ')' );
// if( debug && mode === 'ident' ) log('\n_mostCentered, back from _manage_marker2');
		} else {
			if( identKeyPress === IDENT_LOCKED ) {
				ws.$IdentKeyPress = identKeyPress = IDENT_STEERING;	//next time will do unlock
				_manage_marker( map, false, '_mostCentered2 (IdentKeyPress = IDENT_STEERING)' );
// if( debug && mode === 'ident' ) log('\n_mostCentered, back from _manage_marker3');
				if( Steering > 0 ) {
					if( ps_speed < ps_maxSpeed * 1.1 ) {			//prevent unwanted steer when lost marker at high speeds
						if( IdentMessages )
							consoleMessage( 'Telescope lock, auto-steering', ConsoleMsgDurn );
						start_Steering();							//turn to the target
/*
if( debug && mode === 'ident' ) {
	log('_mostCentered, steering, next time will do unlock, identKeyPress := IDENT_STEERING');
}
 */
						return;										// exit to allow steering to complete
					}
/*
} else {
if( debug && mode === 'ident' ) {
	log('_mostCentered, not steering, fall thru to unlock, identKeyPress := IDENT_STEERING');
}
 */
				}													// steering is turned off, so proceed to unlock now
			}
			// since not steering, proceed to unlock stage
			if( identKeyPress === IDENT_STEERING )  {				// manual unlock, as steering is off or failed to reach target
				ws.$IdentKeyPress = identKeyPress = IDENT_UNLOCK;	// need a delay, else relock immediately; see _auto_updates()
/*
if( debug && mode === 'ident' ) {
	log('_mostCentered, ' + (Steering > 0 ? 'finished' : 'not')  + ' steering, proceed to unlock stage, identKeyPress := IDENT_UNLOCK');
}
 */
				if( IdentMessages )
					consoleMessage( 'Telescope lock released', ConsoleMsgDurn );
			}
		}
	}
	var entHeading = [];											// working vector
	var vectorToOrb = [];											// working vector
	var vectorToPerp = [];											// working vector
	
	function markerInsideOrb( map ) {								// PlanetFall support: cannot mark distant target if marker
																	//   will be inside planet!
		// at rest, marker: scannerRange - 600, placed just in front of band of far lightballs
		var neededDist = scannerRange - 600;						// for head-on approach
		entHeading.length = 0;
		for( let mi = 0; mi < maplen; mi++ ) {
			let orb = mapping[ mi ];
			if( orb.rank !== 'orb' ) continue;
			let orbDist = orb.ent_dist;								 // distance to surface
			if( orbDist > neededDist ) continue;
			// the rest only executes if an orb is within scannerRange
			if( entHeading.length === 0 ) {							// delay as may not be needed
				subtract_vectors( map.last_posn, ps_position, entHeading );
				unit_vector( entHeading, entHeading );
			}
			let orbRadius = orb.ent.radius;
			subtract_vectors( orb.last_posn, ps_position, vectorToOrb );
			let distToOrb = vector_magnitude( vectorToOrb ) - orbRadius; // distance to surface
			unit_vector( vectorToOrb, vector );
			let angleCos = dot_product( entHeading, vector );		// only if both are unit vectors is dot_product the cosine
			if( angleCos < 0 ) continue;							// orb is > 90° from heading
			// projecting orb 'vector' onto entHeading is a quick way to catch most cases (unless quite near an orb)
			if( angleCos * distToOrb > neededDist ) 
				continue;
			// now check if entHeading intersects orb surface
			let distToPerp = dot_product( entHeading, vectorToOrb );// projecting entire vector to center onto unit vector gives
			scale_vector( entHeading, distToPerp, vectorToPerp );	//   portion of entHeading up to perpendicular from orb's center
			// find point along vector to target that meets the perpendicular
			add_vectors( ps_position, vectorToPerp, vector );
			subtract_vectors( orb.ent.position, vector, vector );
			if( vector_magnitude( vector ) < orbRadius ) {			// perpendicular clears the limb
				return true;
			}
		}
		return false;
	}
// : ' +  + '
	function closest_to() {
		var len = find_list.length;
		var min_a = find_radius;
		var best = null;											// ie. not init'd
		var best_d = MaxRange;
		var angle, diff, distance, map, ent, idx;
		var was_using_common_vars = using_common_vars;
		using_common_vars = false;
		find_list.sort( map_sort_heading );
		for( idx = 0; idx < len; idx++ ) {							//search target near the center
			map = find_list[ idx ];
			ent = map.ent;
			if( !ent || !ent.isValid ) continue;
			if( weaponsOnline
					&& !ent.isVisible								// cannot target !isVisible w/ grav. scanner off-line (see reposition_effects)
					&& ent.scanClass !== 'CLASS_CARGO'				// are deleted when go beyond RFID range
					&& !is_beacon( ent ) )							// radio always detectable
				continue;
			let ent_rank = ent.rank;
			if( ent_rank === 'ukn' ) continue;						// is lost (but recoverable) target
			if( find_rank >= 0 && find_rank !== ent_rank )			// wrong rank in limited rank search
				continue;
			if( is_cloaked( ent ) ) continue;
			if( weaponsOnline && alertCondition > YELLOW_ALERT
					&& ent.isDerelict )								// ignore when fighting
				continue;
			if( ent.isWormhole ) {
				if( find_mode !== 'ident' ) continue;				// wormholes must be targeted by player ('r')
				if( weaponsOnline && alertCondition > YELLOW_ALERT )// ignore when fighting
					continue;
				if( ent.collisionRadius === 0 ) continue;			// has closed
				if( ent.$TelescopeScanStart === undefined )
					continue;										// hasn't been manually ('ident' mode) targetted yet
			}
			if( redAlertOptimize() ) continue;						// headingTo is NOT being updated (see _reposition_effects)
			if( markerInsideOrb( map ) ) continue;
			angle = map.headingTo;
			if( find_mode !== 'ident' && angle > find_radius )		// halt search as remainings ents are outside specified cone
				break;
			diff = abs( angle - min_a );							// angle always >= 0, so abs(abs(angle) - abs(min_a)) not necessary
			if( best && diff > 0.5 )  // HALF_a_DEGREE				// found at least one and are beyond pt for distance chk
				break;												// now sorted by headingTo, so only need check 1st few ents
			if( angle > min_a )
				break;												// now sorted by headingTo, so only need check 1st few ents
			// if( angle > min_a ) continue;
			distance = map.ent_dist;
			if( distance <= scannerRange && is_jamming( ent ) ) 	// cannot target inside scannerRange
				continue;
			if( !best ) {											// 1st target found
				best_d = distance;
				min_a = angle;
				best = map;
				continue;
			}
			if( diff < 0.5 ) {// HALF_a_DEGREE						// for ships within a half degree, pick the closer one
				if( distance > best_d ) continue;
			}
			best_d = distance;
			min_a = angle;
			best = map;
		}
		using_common_vars = was_using_common_vars;
// if(find_mode === 'ident')
	// log('closest_to, returning best: ' + (best ? best.ent : best) + '\n');
		return best;
	}
	var find_mode, find_list, find_radius, find_rank, found_map;
	function find_most_central( mode ) {
		if( !mappingReady || maplen === 0 ) return;					// wait for mapping to be created OR it's empty
		if( !viewIsStandard )
			return;
		find_mode = mode;
		find_list = mapping;
		found_map = null;
		if( ILS && ILS.$L === ps ) { 								// suspend all autoscans while in ILS
			return;													// found_map being set to null should signal upstream
		}
		find_rank = -1;
		find_radius = IdentLock;
		var result = -1;
		// just in case you target something before it gets entered into the mapping
		if( curr_target && _Sighting_index( curr_target, '_find_most_central' ) === -1 ) {
			if( !_has_bad_status( curr_target ) 					// need check for when scooping target!
					&& !is_cloaked( curr_target )) {
				let index = _add_Sighting( curr_target, false, false, '_find_most_central' );
				if( index >= 0 ) {
					found_map = mapping[ index ];
					return;
				}
			}
		}
		if( alertCondition > YELLOW_ALERT && weaponsOnline ) {		//in red alert find most centered hostile if not supressed with off-line weapons
																	// in ident, "In Red Alert it will not narrow the locking to the attackers; it can lock any target."
			find_radius = 180;										//in Red Alert lock from the whole sphere who target you
			find_list = select_Sightings( 0, 0,						// 0 => all, 0 => any rank
										  targeting_player );		// first target those targeting you
			if( !find_list || find_list.length === 0 ) {			// no attackers identified
				find_list = mapping;
				find_rank = 'bad';									// limit to bad guys; if none in list yet, search it all
			}														//do not target asteroids before ships in red alert
			result = closest_to();									// try targeting bad guys 1st
			if( result && (mode !== 'ident' 						// ident mode switches from existing target
							|| result !== curr_S.map) ) {
				found_map = result;									//found, target it
				return;												//priority to targets in crosshairs for fighting/asteroid hunting
			}
			find_rank = -1;											// open up search to all targets, as none are targeting & none ranked 'bad'
			find_list = mapping;
			find_radius = AutoLock > 0 ? AutoLock : IdentLock;
		}															//if no bad guy in crosshairs then do normal ident to a ship in telescope list
		if( mode === "ident" ) {									//button "r" pressed or target lost
			find_radius = IdentLock;
		} else	if( mode === "auto" ) {								//lock in the crosshairs only; not called if AutoLock <= 0
			find_radius = AutoLock;
		} else	if( mode === "grav" ) {								//panorama targeting or lock in the crosshairs
			find_radius = GravLock;
		}
		result = closest_to();
		if( result ) {												//found, target it
			found_map = result;
			return;
		}
		return;
	}
///////////////////////////////////////////////////////////////////////////////////////////////////
// naming functions ///////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////
	sunName.sun_names = {};		//cache of names
	function sunName( ent ) {										// allow for multiple suns
		if( !system || !ent || !ent.isValid )
			return null;
		var name, key = ent.position.toString();
		if( sunName.sun_names.hasOwnProperty( key ) )				// use cached
			return sunName.sun_names[ key ];
		do {
			name = system.info.sun_name;
			if( name && name.length > 0 )
				break;
			name = system_sun.displayName;
			if( name && name.length > 0 )
				break;
			name = system.info.name;
			if( name && name.length > 0 ) {
				if( name === system_name )
					name += ' (Star)';
				break;
			}
			name = system_name + ' (Star)';
		} while( false );
		sunName.sun_names[ key ] = name;							// add to cache
		return name;
	}
	function entityIsNamed( ent ) {									// return name if someone else has named it
		var name;
		// copied property priority from GalacticAlmanac
		name = ent.displayName;
		if( name && name.length > 0 ) {
			return name;
		}
		name = ent.beaconLabel;
		if( name && name.length > 0 ) {
			return name;
		}
		name = ent.beacon;
		if( name && name.length > 0 ) {
			return name;
		}
		name = ent.beaconCode;
		if( name && name.length > 0 ) {
			return name;
		}
	}
	function planetIsNamed( ent ) {									// return planet name if named by another oxp_name
		var name, ent_name = entityIsNamed( ent );
		if( ent_name && ent_name.length > 0 ) {
			return( ent_name );
		}
		// farplanets oxz
		if( PlanetNames ) {		//	worldScripts.planetnames.$PlanetNames_GetPlanetName( cs.ent )
			name = PlanetNames.$PlanetNames_GetPlanetName( ent );
			if( name && name.length > 0 ) {
				return name;
			}
		}
		if( PlanetaryCompass ) {	//	worldScripts[ 'planetaryCompass_worldScript.js' ]
			// set beaconCode & beaconLabel (latter has type in parentheses)
			// - for mainPlanet, instead sets name & displayName (latter has type in parentheses)
			//   which is caught by entityIsNamed
			let pce = entitiesWithScanClass( "CLASS_VISUAL_EFFECT", ent, 10 );
			for( let idx = 0, len = pce.length; idx < len; idx++ ) {
				let first = pce[ idx ];
				if( !first || !first.isValid )
					continue;
				if( first.dataKey.indexOf( 'planetaryCompass' ) < 0 )
					continue;
				if( first.beaconLabel ) {
					name = first.beaconLabel;					// others
					break;
				}
			}
			free_array( pce );
			return name;
		}
	}
	orbName.planet_names = [];	//cache of names
	function orbName( ent ) {
		if( !ent ) return null;
		if( isSun < 0 ) isSun = ent.isSun;
		if( isPlanet < 0 ) isPlanet = ent.isPlanet;
		if( !isPlanet && !isSun ) return null;
		if( !system_planets || system_planets.length === 0 ) {
log(ws.name, 'orbName, SYSTEM_PLANETS INVALID: ' + system_planets );
			return null;
		}
		var name = '';
		if( isSun ) {
			return sunName( ent );
		} else {
			var idx = index_in_list( ent, system_planets );
			if( idx < 0 )
				return null;
			name = orbName.planet_names[ idx ];
			if( !name || name.length === 0 ) {
				name = planetNameString( ent );
				orbName.planet_names[ idx ] = name;
			}
		}
		return name;
	}
	planetNameString.orbList = null; // inventory of system's planets & moons generated when none exist
	planetNameString.ROMAN = [ 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X',
							   'XI', 'XII', 'XIII', 'XIV', 'XV', 'XVI', 'XVII', 'XVIII', 'XIX', 'XX' ];
	planetNameString.GREEK = [ 'Alpha', 'Beta', 'Gamma', 'Delta', 'Epsilon', 'Zeta', 'Eta',
							   'Theta', 'Iota', 'Kappa', 'Lambda', 'Mu', 'Nu', 'Xi', 'Omicron', 'Pi',
							   'Rho', 'Sigma', 'Tau', 'Upsilon', 'Phi', 'Chi', 'Psi', 'Omega' ];
	function planetNameString( ent ) {
		if( !system || !ent || !ent.isValid )
			return 'nada';
		if( !isPlanet )
			return( 'Non-Planet' );
		var name = planetIsNamed( ent );
		if( name && name.length > 0 ) {							// someone else has named it
			return name;
		}
		name = ent.name;
		if( name !== system_name ) {							// someone else has named it
			return name;
		}
		// create dictionary of default names, done once/system, taking 2.37 ms @ 32 fps
		let orbList = planetNameString.orbList;
		if( orbList === null ) {								// generate planet names
			planetNameString.orbList = orbList = {};
			let orbs = entitiesWithScanClass( "CLASS_NO_DRAW", system_sun );
				// all planets, moons ordered by distance from sun
			let pnum = 0, pstr, mnum = 0, mstr,
				ROMAN = planetNameString.ROMAN, romans = ROMAN.length,
				GREEK = planetNameString.GREEK, greeks = GREEK.length;
			for( let pl = 0, plen = orbs.length; pl < plen; pl++ ) {
				let orb = orbs[ pl ];
				if( !orb.hasOwnProperty( 'radius' ) ) continue;
				if( orb.hasAtmosphere ) {					// it's a planet
					pstr = pnum < romans ? ROMAN[ pnum ] : pnum;
					name = system_name + (orb.isMainPlanet ? ' Prime (Planet)'
														   : ' ' + pstr + ' (Planet)');
					orbList[ orb.position.toString() ] = name;
					pnum++;
					let moons = entitiesWithScanClass( "CLASS_NO_DRAW", orb, orb.collisionRadius * 10 );
						// all moons ordered by distance from orb
					for( let mn = 0, mlen = moons.length; mn < mlen; mn++ ) {
						let moon = moons[ mn ];
						if( moon.hasAtmosphere ) break;		// it's a planet, we're done (and have double counted!)
						mstr = mnum < greeks ? GREEK[ mnum ] : mnum;
						name = mnum < greeks ? mstr + ' Moon' : 'Moon ' + mstr;
						name += ' (' + pstr + ')';
						orbList[ moon.position.toString() ] = name;
						mnum++;
					}
					free_array( moons );
				}
			}
			free_array( orbs );
		}
		name = orbList[ ent.position.toString() ];
		return name;
	}
	function entityName( map ) {
		var ent = map.ent, name = '', ent_script = -1,
			ent_name = ent.name;
		do {
			if( map.rank === 'ukn' ) {
				name = lost_target_name;
				break;
			}
			if( ent_name && ( ent_name === 'Railgun Projectile'			//do not show launched bullets
							|| ent_name === 'Debris'
							|| ent_name.indexOf( 'customshields' ) >= 0 ) ) {
				return null;											//nor customshields parts
			}
			if( ent_name && ent_name.indexOf('Exhibition]' ) >= 0 ) {
				return null;											//do not show ships in exhibition of Gallery OXP
			}
			ent_script = ent.script;
			if( ent_script && ent_script.$Detectors_Origname ) {		// shipversion oxp
				name = ent_script.$Detectors_Origname;
				break;
			}
			name = entityIsNamed( ent );
			if( name && name.length > 0 ) {
				break;
			}
			if( ent_name && ent_name.length > 0 ) {
				name = ent_name;
				let unique = ent.shipUniqueName;
				if( unique && unique.length > 0 )
					name =+ ': ' + unique;
				break;
			}
			name = unknown_ship_name;
		} while( false );
		return name;
	}
	// had to remove cache for ship names for other oxp name changes
	// eg. cargoscanner sets .shipUniqueName, someone else applies it to .displayName
	//     and thus "Splinter" -> "Splinter: Minerals"
	function clearNameCaches() {									// reset name caches for each system
		var cache = sunName.sun_names;
		for( let prop in cache ) {
			if( cache.hasOwnProperty( prop ) ) {
				delete cache[ prop ];
			}
		}
		orbName.planet_names.length = 0;
		planetNameString.orbList = null;							// deliberate object -> garbage heap (once/system)
	}
	// const isoPrefix = ['', 'K', 'M', 'G', 'T', 'P'];
	function distWithUnits( distance ) {
/* 	telescope 1.15
		var range = floor( ent_dist / 1000 );
		if( range < 0 ) range = 0;
		if( range >= 1e6 ) range = floor( range / 1e6 ) + 'M';
		// ...
		let km = distance / 1000;
		if( km < 100 )
			name_with_dist = km.toFixed( 3 ) + ' km ' + displayName;
		else if( km < 1e6 )
			name_with_dist = floor( km ) + ' km ' + displayName;
		else
			name_with_dist = floor( km / 1e6 ) + ' Mkm ' + displayName;
*/
/*
		var displayDist = distance / baseDistance;
		var milles = floor( log10( displayDist ) / 3 ); // # of 1000's
		if( milles >= isoPrefix.length ) {
			return 'really far';
		}
		var prefix = 0;
		do {
			displayDist /= 1000;
			prefix++;
		} while( --milles >= 0 );
		var units = isoPrefix[ prefix ] + distanceUnits;
		if( displayDist >= 1000 ) {
			displayDist = displayDist.toFixed( 0 );
		} else if( displayDist >= 100 ) {
			displayDist = displayDist.toFixed( 1 );
		} else if( displayDist >= 10 ) {
			displayDist = displayDist.toFixed( 2 );
		} else {
			displayDist = displayDist.toFixed( 3 );
		}
// oxp's needing update:
//  Combat_MFD: has unit Mkm for 1000000 km, chg to Gm
//   - he does use Mm/s for speed
//  VimanaHUD: searches for 'km ' in telescopemarker's .displayName, ch to re /^(?:\d+[.]?\d*|[.]\d+)[kKMGTP]?m(.*)$/
//   - also fix for telescope v2 support; optimize distanceTo, repl filteredEntities w/ entitiesWithScanClass
 */
		// canon has Mkm as 1e9, not Gm; nothing defined for 1e6 (and Kkm is wierd)
		let units = ['m ', 'km ', 'Kkm ', 'Mkm ', 'Gkm ', 'Tkm ', 'Pkm '];
		let withUnits,
			fixed = floor( log10( distance ) / 3 ); // # of 1000's
		if( fixed >= units.length ) {
			withUnits = 'really far';
		} else {
			let signif = (distance / pow(1000, fixed) );
			if( fixed === 0 ) {
				// withUnits =	signif.toFixed( 3 - floor(log10( distance )) );
				// VimanaHUD searches for 'km ' to remove distance from displayName
				withUnits =	(distance / 1000).toFixed( 3 );
				withUnits += ' ' + units[ 1 ];
			} else if( fixed === 2 ) {						// skip Kkm (can't use Mm due to VimanaHUD)
				withUnits =	floor(distance / 1000);//.toFixed( 0 );
				withUnits += ' ' + units[ 1 ];
			} else {
				withUnits =	signif.toPrecision( 4 );
				withUnits += ' ' + units[ fixed ];
			}
		}
		return withUnits;
	}
// : ' +  + '
	function set_displayName( map ) {								// update curr_S.name and return new value (for marker.displayName)
		var that = set_displayName;
		var lastDisplayed = (that.lastDisplayed = that.lastDisplayed || null);
		// called by _update_target_marker, _manage_marker & switch_PS_target ie. target only
		var displayName = curr_S.name,								//remove km from ident message
			ent = map.ent;
		if( lastDisplayed !== ent ) {								// avoid repeating name construction as we cannot cache
			that.lastDisplayed = ent;								//  because some oxp's alter displayName dynamically (eg. cargoscanner)
			displayName = null;
		}
		if( map.rank === 'ukn' ) {
			displayName = lost_target_name;
		} else if( !displayName 									// save unaltered name for ident message
					|| displayName === lost_target_name ) {
			if( radius < 0 ) radius = ent.radius;
			if( radius ) {											// only orbs have .radius
				displayName = orbName( ent );
			} else if( is_jamming( ent ) ) {						// sets isJamming, returns true if effective (ie. scanFilter_ok)
				displayName = unknown_ship_name;					// can see jamming ships but not identify (thanks Milo)
			} else {
				displayName = entityName( map );
			}
		}
		if( !displayName ) {
			// Error: Cannot set property displayName of instance of Ship to invalid value null
			displayName = '';
		}
		curr_S.name = displayName;
		return displayName;
	}
	function showTargetName( map, combatMFDonly ) {
		if( !equip_ok ) return;
		if( _Sighting_index( map, 'showTargetName' ) < 0 ) return;
		reset_common_vars();										// required as done in groups of 3
		var msg = showShipReport( map );
		if( !msg || msg.length <= 0 ) return;
		if( Combat_MFD && index_in_list( 'combat_MFD', ps.multiFunctionDisplayList ) !== -1) {											//show in Combat MFD instead of console
			prevMFDTarget = map;									//store for Combat MFD
			Combat_MFD.$TelescopeLine = msg;
		} else if( !combatMFDonly ) {
			consoleMessage( msg, ConsoleMsgDurn );					//fallback to console
		}
		using_common_vars = false;
	}
	function showShipReport( map ) {								// format ship name/dist for format_line (MFD) & showTargetName
		if( !map ) return;
		var ent = map.ent;
		var name = '', cached = false,
			ent_script = -1,
			jamming = is_jamming( ent );							// sets isJamming, returns true if effective (ie. scanFilter_ok)
		copy_vector( map.last_posn, position ); 					// if 'ukn', can only report what was known
		while( name.length === 0 ) {
			if( radius < 0 ) radius = ent.radius;
			if( ent.radius ) {
				name = orbName( ent );
				if( name === null ) {
log(ws.name, 'showShipReport, orbName FAILED for ent: ' + ent );
					return;
				}
				break;
			}
			if( jamming ) {											// can see jamming ships but not identify (thanks Milo)
				name = unknown_ship_name;
				break;
			}
			name = entityName( map );
			if( name === null ) {
log(ws.name, 'showShipReport, entityName FAILED for ent: ' + ent );
				return;
			}
			break;
		}
		var ent_dist = map.ent_dist;
		var range = distWithUnits( ent_dist ) + ' ';
		var prefix = '';
		if( !ent.isWormhole ) {
			if( ent.isDerelict ) {
				if( ent_script < 0 ) ent_script = ent.script;
				if( Towbar && ent_script) { 						//Towbar status
					if( ent_script.$TowbarUsableShip ) 		prefix = 'Usable ';
					else if( ent_script.$TowbarMinedShip ) 	prefix = 'Mined ';
					else if( ent_script.$TowbarEmptyShip ) 	prefix = 'Empty ';
					else 									prefix = 'Derelict ';
				} else {
					prefix = 'Derelict ';
				}
			} else if( (FarStatus || ent_dist < scannerRange) && ent.target === ps ) {
				range = '! ' + range; 								//hostile
			} else if( map && map.rank === 'bad' ) {
				range = '* ' + range; 								//pirate
			}
		}
		_relative_direction( position, map );
// log(ws.name, 'showShipReport, ' + name+', d:'+relative_dirn+' p:'+position[0].toFixed(2)+', '
		// +position[1].toFixed(2)+', '+position[2].toFixed(2)); //debug
		var colon = name.indexOf( ': ' );
		if( !cached ) {												// it has a name
		// if( !cached && (colon = name.indexOf( ': ' )) >= 0 ) {		// it has a name
			let staticLen = strFontLen( range + prefix + ' ' + relative_dirn ),
				openLen = 18 - staticLen,
				nameLen = strFontLen( name );
			if( nameLen > openLen ) {								// replace 'Navigation Buoy' w/ 'Nav. Buoy'
				[ name, nameLen ] = subLongest( name, openLen,
						'Navigation Buoy',  ['Nav. Buoy', 'Buoy']);
			}
			if( nameLen > openLen ) {								// replace 'Station Buoy' w/ 'Stn. Buoy'
				[ name, nameLen ] = subLongest( name, openLen,
						'Station Buoy',  ['Stn. Buoy', 'Buoy']);
			}
			if( nameLen > openLen ) {								// replace 'Station' w/ 'Stn'
				[ name, nameLen ] = subLongest( name, openLen,
						'Station', ['Stn.']);
			}
			if( nameLen > openLen ) {
				name = shortenShipName( name, colon, nameLen, openLen );
			}
		}
		return range + prefix + name + ' ' + relative_dirn;
	}
	function subLongest( string, maxLen, target, candidates ) {
		// pass candidates in descending length
		var startLen = strFontLen( string ),
			newStr = string,
			len = candidates.length;
		if( len === 0 || !maxLen | startLen <= maxLen
				|| string.indexOf( target ) < 0 ) {
			return [ string, startLen ];
		}
		for( let idx = 0; idx < len; idx++ ) {
			newStr = string.replace( target, candidates[ idx ] );
			let fontLen = strFontLen( newStr );
			if( fontLen <= maxLen )
				return [ newStr, fontLen ];
		}
		return [ string, startLen ];
	}
	function shortenShipName( name, colon, startLen, targetLen ) {
		var that = shortenShipName;
		var name_breaks = that.name_breaks;							// const. props defined at end of function
		var name_suffix = that.name_suffix;							//	 "
		var name_shorten = (that.name_shorten = that.name_shorten || []);
		name_shorten.length = 0;									// prep to re-use array
		var idx, len, space, nameLen = startLen,
				diff, start, brk = -1;
		for( idx = 0, len = name.length; idx < len; idx++ )
			name_shorten[ idx ] = name[ idx ];
		space = name_shorten.lastIndexOf( ' ', colon );
		if( space >= 0 ) {											// try replacing superfluous tag
			let ship_tags = that.ship_tags,
				tags = ship_tags.length;
			for( idx = 0; idx < tags; idx++ ) {
				brk = findListInArray( ship_tags[ idx ], name_shorten, 0, colon );
				if( brk >= 0 ) break;
			}
			if( brk >= 0 ) {
				let shrink = colon - space;
				for( idx = colon, len = name_shorten.length; idx < len; idx++ )
					name_shorten[ idx - shrink ] = name_shorten[ idx ];
				name_shorten.length -= shrink;
				nameLen = strFontLen( name_shorten )				// strFontLen assumes spaces between elements
						  - (len - shrink - 1) * SpaceLen;			//	 so we subtract # commas * SpaceLen
			}
		}
		if( nameLen > targetLen ) {									// still too long, break on logical words
			diff = startLen - targetLen + colon * SpaceLen;			// 'colon * SpaceLen' => start earlier for long ship types
			start = floor(colon + (name.length - colon) / (1 + diff / 2));
			brk = -1; idx = 0; len = name_breaks.length;
			for( ; idx < len; idx++ ) {
				brk = findListInArray( name_breaks[ idx ], name_shorten, start );
				if( brk >= 0 ) break;
			}
			if( brk >= 0 ) {
				idx = brk;
				len = name_suffix.length;
				for( let nsi = 0; nsi < len; nsi++ )
					name_shorten[ idx++ ] = name_suffix[ nsi ];
				name_shorten.length = brk + len;
				nameLen = strFontLen( name_shorten ) - ( name_shorten.length - 1 ) * SpaceLen;
			}
		}
		if( nameLen > targetLen ){									// still too long, break on a space
			brk = name_shorten.indexOf( ' ', start );
			if( brk >= 0 ) {
				idx = brk;
				len = name_suffix.length;
				for( let nsi = 0; nsi < len; nsi++ )
					name_shorten[ idx++ ] = name_suffix[ nsi ];
				name_shorten.length = brk + len;
				nameLen = strFontLen( name_shorten ) - ( name_shorten.length - 1 ) * SpaceLen;
			}
		}
if( debug ) {
	let msg = 'name: "' + name + '", name_shorten: "' + name_shorten.join('') + '"';
	if( sShipNameRpt.indexOf < 0 ) {
		log(ws.name, 'shortenShipName, ' + msg );
		sShipNameRpt.push( msg );
	}
}
		return name_shorten.join('');
	}
	shortenShipName.ship_tags = [ ['S','h','i','p'], ['B','o','a','t'], ['S','h','u','t','t','l','e'] ];
	shortenShipName.name_suffix = [' ','.','.','.'];
	shortenShipName.name_breaks = [ [' ','a','n','d',' '], [' ','o','f',' '],	// must be lower case, will test upper
									[' ','t','h','e',' '], [' ','o','r',' '],
									[' ','o','n',' '],	   [' ','i','n',' '] ];
var sShipNameRpt = []; /// tmp4debug
	function findListInArray( list, array, start, end ) {			// return index of list in array
		var ar = start || 0,										//	 list is assumed lower case
			arLen = end || array.length,
			li, liLen = list.length;
		if( arLen < liLen ) return -1;
		for( ; ar < arLen; ar++ ) {
			for( li = 0; li < liLen; li++ ) {
				let achr = array[ ar + li ],
					lchar = list[ li ];
				if( achr === lchar ) continue;
				if( lchar === ' ' ) break;
				if( achr === lchar.toUpperCase() ) continue;
				break;
			}
			if( li >= liLen ) return ar;
			if( ar + liLen >= arLen ) return -1;
		}
	}
	function relativeDirection( position, map ) {					// stub for external call from telescope_debug._dump_map
		try {
			init_headingView();
			_relative_direction( position, map );
			return relative_dirn;
		} catch( err ) {
			log( ws.name, ws._reportError( err, 'relativeDirection', [position, map], 1 ) );
			if( debug ) throw err;
		}
	}
	var relative_dirn;												// external store for relative_direction's return
	function _relative_direction( position, map ) {					// init_headingView is called by calling fn(s) to reduce #
		if( !position || !map ) return;
		if( !viewIsStandard ) return;
		relative_dirn = '';
		if( !ps_position || ps_position.length === 0 ) {			// this fn can get called before FCB starts
			if( !ps_position ) ps_position = alloc_array();
			copy_vector( ps.position, ps_position );
		}
		subtract_vectors( position, ps_position, ent_vector );
		unit_vector( ent_vector, ent_vector );
		let right_v = angle_between_two_unitV( headingView, ent_vector );
		let delta_right = abs( QUARTER_ARC - right_v );
		let up_v = angle_between_two_unitV( ps_vectorUp, ent_vector );
		let delta_up = abs( QUARTER_ARC - up_v );
		let dirn_marks = '';
		if( right_v > REL_DIR_HALF_PLUS )
			dirn_marks += delta_right > (REL_DIR_STRESS * delta_up)
							? '<<' : '<';							// extra chr when heading is mostly in that direction
		if( right_v < REL_DIR_HALF_MINUS )
			dirn_marks += (delta_right > REL_DIR_STRESS * delta_up)
							? '>>' : '>';
		if( up_v < REL_DIR_HALF_MINUS )
			dirn_marks += (delta_up > REL_DIR_STRESS * delta_right)
							? '^^' : '^';
		if( up_v > REL_DIR_HALF_PLUS )
			dirn_marks += (delta_up > REL_DIR_STRESS * delta_right)
							? 'vv' : 'v';
		if( dirn_marks.length === 2 && dirn_marks[ 0 ] === dirn_marks[ 1 ] )
			dirn_marks = dirn_marks[ 0 ];							// remove doubled when near axis
		relative_dirn = round( map.headingTo ) + "° " + dirn_marks;
	}
	function init_headingView() {									// relativeDirection needs view vector
		if( viewDirection === "VIEW_FORWARD" ) {
			copy_vector( ps_vectorRight, headingView );				// right is right of fwd
		} else if( viewDirection === "VIEW_AFT" ) {
			scale_vector( ps_vectorRight, -1, headingView );		// -right is right of aft
		} else if( viewDirection === "VIEW_STARBOARD" ) {
			scale_vector( ps_vectorForward, -1, headingView );		// -fwd is right of starboard
		} else if( viewDirection === "VIEW_PORT" ) {
			copy_vector( ps_vectorForward, headingView );			// fwd is right of port
		}
	}
///////////////////////////////////////////////////////////////////////////////////////////////////
// _auto_update_closure ///////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////
	// 'constant' variables unique to _auto_update_closure
	var PlanetNames, PlanetaryCompass,
		lost_target_name = '(lost target)',
		unknown_ship_name = '(unknown ship)';
	// local variables unique to _auto_update_closure
	var quarter_sec_counter = 0,									//counter to make colour of the visual marks, used to do once in a second within a 0.25s timer
		report_status = false,										// flag to restrict over-reporting of msgs
		gravScanMsg = false,										// flag to show messgage less frequently
		found_new = false,											// flag for triggering action required when a new target is detected
		delay_counter = -1;											// countdown for IdentDelay (see also _chg_curr_Sighting)
	function _report_autovars() {
		let tmp = '_report_autovars ,stationNearby = ' + stationNearby + ', gravScanProgress = ' + gravScanProgress
			 + ', gs_state = ' + gs_state + ', gs_progress_report = ' + gs_progress_report
			 + '\nfound_new = ' + found_new + ', delay_counter = ' + delay_counter
			 + ', identKeyPress = ' + identKeyPress + '\n';
		var idx, len = selected_Sightings.length;
		tmp += 'selected_Sightings has ' + len + ' items\n'
		if( len > 0 ) {
			for( idx = 0, len = selected_Sightings.length; idx < len; idx++ ) {
				if( idx > 0 ) tmp += '\n'
				tmp += 'selected_Sightings['+idx+'] = \n';
				if( cd ) tmp += cd._showProps( selected_Sightings[idx], 'selected_S['+idx+']' );
			}
		}
		log( ws.name, tmp );
		debug = ws.$DebugMessages;
	}
	function _set_GS_state() {										//in the range of the Gravity Scanner if installed
		var available = stationNearby && !weaponsOnline 			//near a station or baseship & weaps off-line
			&& ext_ok && grav_eq_ok && gravScanProgress <= 1;		// scan underway or done( == 1 )
																	//grav scan without weapons only to need force it
		if( gravScanProgress === 1 ) {
			gs_state = GS_COMPLETE;
		} else if( gravScanProgress === 0 ) {
			gs_state = available ? GS_STOPPED : GS_NONE;
		} else {
			gs_state = available ? GS_RUNNING : GS_DEGRADING;
		}
	}
	function check_equip_ok() {
		if( eq_status === 'EQUIPMENT_DAMAGED' ) {
			if( ws.$DamageMsg ) {
				consoleMessage( 'Telescope damaged', ConsoleMsgDurn );
				ws.$DamageMsg = false;
			}
			return false;
		} else if( eq_status !== 'EQUIPMENT_OK' ) {
			if( BuyMsg ) {
				consoleMessage( 'Buy Telescope! (x)', ConsoleMsgDurn );
				BuyMsg = false;
			}
			ws.$DamageMsg = false;
			return false;
		}
		return true;
	}
	function chk_energy_gs_status( on_demand ) {					// on_demand = true for user directed scans (toggle weapons, Rescan, step thru end of list)
		if( ps.energy < 64 ) {
			if( on_demand )
				consoleMessage( 'Not enough energy for Telescope', ConsoleMsgDurn );
			return false;
		}
		if( on_demand ) {
			_create_Sightings();									// create list from scratch
			report_status = true;
		}
		var scanning = fns_are_pending();							// true => mapping creation/update is running
		if( !AutoScan && !on_demand && !scanning
			&& gs_state <= GS_STOPPED )
			return false;											// AutoScan turned off by user
		if( found_new || on_demand || scanning ) {
			ps.energy -= 2;											//use a little energy to scan the whole sky
		}
		if( gs_state === GS_RUNNING									// no energy drain during degradation
			&& alertCondition < RED_ALERT ) { 						// suspended during Red Alert
			ps.energy -= 6;											//need 4x energy with Gravity Scanner
		}
		return true;
	}
	function is_station_near( map ) {
		var ent = map.ent;
		if( !ent.isStation )			return false;				// not abandoned rock hermit, as has no power for its part of grav. scanner
		if( ent.mass <= 1e7 )			return false;				//skip ships with docking port (except baseships), rock hermit with 53508t must fit in
		if( ent.target === ps )			return false;				//target is hostile if targeting back
		var d = map.ent_dist;
		if( d > 5000 )					return false;				// not within 5 km
		if( index_in_list( ps, ent.defenseTargets ) >= 0 )
			return false;											// hostile
		return true;
	}
	function report_scan_progress( forced, set_gs_progress ) {
		if( forced ) report_status = true;							// record now in case fns_are_pending
		if( set_gs_progress === undefined && fns_are_pending() )
				return;												// report status when update is complete
		if( !forced && !report_status && !gravScanMsg ) return;		// already reported
		var msg, progress;
		if( ext_ok && grav_eq_ok ) {
			if( gravScanProgress === 1 ) {							// scan complete
				msg = 'Gravity scan found ';
				gravScanMsg = false;
			} else if( gravScanProgress <= 0 ) {					// scan totally degraded
				msg = 'Gravity scan off-line';
				gravScanMsg = false;
				consoleMessage( msg, ConsoleMsgDurn );
				report_status = false;
				return;
			} else {												// scan still active
				if( set_gs_progress !== undefined )					// ensure msg always an even amount
					gs_progress_report = progress = set_gs_progress;
				else
					progress = floor( gravScanProgress * 100 );
				if( !weaponsOnline ) {
					msg = 'Gravity scan ' + (stationNearby ? 'up to ' : 'down to ') + progress + '%, ' + (stationNearby ? 'found ' : 'has ');
					gravScanMsg = false;
				} else
					return;											// no progress msg when weaponsOnline
			}
		} else {													//send message about telescope scan
			msg = 'Telescope found ';
		}
		if(		 maplen === 0 ) msg += 'no targets';
		else if( maplen === 1 ) msg += '1 target';
		else					msg += maplen + ' targets';
		consoleMessage( msg, ConsoleMsgDurn );
		report_status = false;
	}
	const GSR_PROGRESS_ENDPTS = 1,									// gravity scan reporting frequency
		  GSR_PROGRESS_QUARTERS = 2,
		  GSR_PROGRESS_TENTHS = 4,
		  GSR_DEGRADE_ENDPTS = 8,
		  GSR_DEGRADE_QUARTERS = 16,
		  GSR_DEGRADE_TENTHS = 32;
	var gs_progress_report = 0;										// remember progress of last report
	function update_grav_scan() {
		// reporting is triggered in _hud_effects(): sets report_status = true when you turn weapons off-line
		if( !equip_ok || !ext_ok || !grav_eq_ok ) {					// check equipment ok
			gravScanMsg = false;									// suppress any reporting
			gravScanProgress = 0;
			return;
		}
		if( ps_mass >= 1e8 ) {
			stationNearby = true;									//baseships can perform gravity scan anywhere
		} else if( quarter_sec_counter <= 0 ) {						//check a station is nearby for gravity scanner every 4. call, ie. 1/sec
			var list = select_Sightings( 1, 'isr', is_station_near );
			if( list && list.length > 0 ) {
				if( !stationNearby && weaponsOnline )				//show when arrived near a station
					consoleMessage( 'Gravity scan needs weapons off-line', ConsoleMsgDurn );
				stationNearby = true;
			} else {												//too far or become hostile
				if( stationNearby )									// show when leave 5km radius or you pissed them off
					consoleMessage( 'Gravity scan needs a friendly station in 5km', ConsoleMsgDurn );
				stationNearby = false;
			}
		}
		if( stationNearby ) {										// incr gravity scan progress counter
			if( gravScanProgress === 0 )
				ws.$SoundScan.play();								//GS scan sound
			if( gravScanProgress < 1 ) {
				let halted = false;
				if( gravScanProgress > 0 && numberSwapable() >= MaxTargets ) {// orbs & beacons excluded from MaxTargets
					gravScanProgress = 1.1;							// terminate gravity scan, nothing to gains
					halted = true;									// suppress further reports
					consoleMessage( 'Gravity scan halted, memory full; '
									+ maplen + ' targets', ConsoleMsgDurn );
				} else {
					let gsm = 1;									//gravity scanner multiplyer
					if( ps_speed === 0 ) gsm = 4;					//4 times faster if stopped
					if( grav_eq2_ok )
						gsm *= 2;									//half time with 2 working grav.scanner
					gravScanProgress += gsm * QUARTER_SECS_OF_4MIN;	//normal gravity scan need 4 minutes
				}
				if( gravScanProgress > 1 ) {
					gravScanProgress = 1;
					gs_progress_report = 100;
					++ws.$GravScanCount;							//Gravity Scan counter to bring aliens
					if( GravScanMsgFreq & GSR_PROGRESS_ENDPTS ) {	// not turn off by user
						gravScanMsg = true;							// enable reporting
						if( weaponsOnline ) {
							consoleMessage( 'Gravity scan done, turn off weapons to see results', ConsoleMsgDurn );
						} else if( !halted ) {
							report_scan_progress();
						}
					}
					let p = ws.$FixedGS === 1 ? 100 : 200;			// every 100 scans if cheap fix, else every 200
					if( ws.$GravScanCount >= p ) {
						let num = ceil(pow( player.score, 0.5 ) / 10 ); //with 0 score do not get any
						if( num > 0 ) {
							consoleMessage( 'Aliens detected your Gravity Scan!', 10 );
							addShips( 'thargoid', num, ps_position, 50000 );
						}
						ws.$GravScanCount = 0;
					}
				} else if( GravScanMsgFreq > 0 ) {					// issue progress report
					let progress = floor(gravScanProgress * 100);
					let frequency = GravScanMsgFreq & GSR_PROGRESS_TENTHS	? 10 :
									GravScanMsgFreq & GSR_PROGRESS_QUARTERS ? 25 : 0;
					if( frequency > 0 ) {
						let div = floor(progress / frequency);
						let mark = progress % frequency;
						if( mark < 2 && div * frequency > gs_progress_report ) {
							report_scan_progress( true, div * frequency );
						}
					}
				}
			}
		} else if( gravScanProgress > 0 ) {							//degrading from 100% to 0% in 2 minute if away from stations
			gravScanProgress -= QUARTER_SECS_OF_2MIN *
								( 1 + ps_speed / ps_maxSpeed );		// faster if moving
			if( gravScanProgress < 0
					&& GravScanMsgFreq & GSR_DEGRADE_ENDPTS ) {
				report_scan_progress( true, 0 );
				gravScanProgress = 0;
			} else if( GravScanMsgFreq >= GSR_DEGRADE_ENDPTS ) { 	// issue progress report
				let frequency = GravScanMsgFreq & GSR_DEGRADE_TENTHS   ? 10 :
								GravScanMsgFreq & GSR_DEGRADE_QUARTERS ? 25 : 0;
				if( frequency > 0 ) {
					let progress = floor(gravScanProgress * 100);
					let rpt = floor(progress / frequency) * frequency;
					let mark = progress % frequency;
					if( mark < 2 && rpt < gs_progress_report ) {	// mark < 2 to ensure reported on slow machines
						report_scan_progress( true, rpt );
						if( rpt === frequency )						// last report, suppress report @ 0
							gs_progress_report = 0;
					}
				}
			}
		}
	}
// : ' +  + '
	function randomInt( min, max ) { return floor( random() * (max - min) ) + min; }
	doClear_MFD.orig_msg =	[ 'T','e','l','e','s','c','o','p','e',':',' ','N','o',' ','T','a','r','g','e','t','s' ];
	doClear_MFD.aux_msg =	[ 'T','e','l','e','s','c','o','p','e',' ','A','u','x','.',':',' ','N','o',' ','T','a','r','g','e','t','s' ];
	doClear_MFD.aux_not =	[ 'T','e','l','e','s','c','o','p','e',' ','A','u','x','.',':',' ','D','i','s','a','b','l','e','d' ];
	function doClear_MFD( MFDname, fully ) {						// ?completely empty MFD's vanish
		var that = doClear_MFD;
		var clear_msg = (that.clear_msg = that.clear_msg || []);
		var msg = MFDname === PrimaryMFD_name ? that.orig_msg :
				  SeparateMFDs ? that.aux_msg : that.aux_not;
		if( fully ) {
			ps.setMultiFunctionText( MFDname, '', false );
		} else if( equip_ok ) {
			ps.setMultiFunctionText( MFDname, msg.join(''), false );
		} else {
			let idx, msg_len;
			idx = msg_len = msg.length;
			clear_msg.length = 0;									// clear array
			while( idx-- ) clear_msg[ idx ] = msg[ idx ];			// set up working array
			let min, max, rand = randomInt( 4, 6 );					// distort msg by swapping a few, chg'g case
			while( rand ) {
				min = randomInt( 0, msg_len );
				max = randomInt( 0, msg_len );
				if( min === max ) continue;							// need diff #'s
				if( clear_msg[ min ] ===  clear_msg[ max ] )
					continue;										// want diff char's
				rand--;
				if( min > max ) {
					[ max, min ] = [ min, max ];
				}
				if( min % 2 === 0 || max - min > msg_len >> 2 ) {	// swap chars
					// let tmp = clear_msg[ min ];
					// clear_msg[ min ] = clear_msg[ max ];
					// clear_msg[ max ] = tmp;
					[ clear_msg[ min ], clear_msg[ max ] ] = [ clear_msg[ max ], clear_msg[ min ] ];
				} else {
					for( idx = min; idx < max; idx++ )
						clear_msg[ idx ] = clear_msg[ idx ].toUpperCase();
				}
			}
			ps.setMultiFunctionText( MFDname, clear_msg.join(''), false ); // '\n\n\n\n' +
		}
	}
	function format_line( map, list ) {
		let ent = map.ent;
		if( !ent || !ent.isValid ) return false;
		if( MFD_ents[ ent ] ) {
			return false;
		}
		reset_common_vars();										// required as done in groups of 3
		let rpt = showShipReport( map );
		using_common_vars = false;
		if( !rpt || rpt.length === 0 || rpt.indexOf( '(Lost ' ) === 0 )
			return false;
		if( curr_target ) {
			if( curr_target === ent || ent === curr_S.ent )
				rpt = '[ ' + rpt + ' ]';							//mark the current target
		}
		list.push( rpt );
		MFD_ents[ ent ] = true;
		return true;
	}
/*	"The Target list contains hostiles first, if any, then all ships in normal scanner (25.6km),
	 followed by Cargo and Escape Pods, then ending with ships which are not in the normal scanner."
	function map_sort_rank_dist( a, b ) {							// same as used in _chg_curr_Sighting
		let a_rank = a.rank, b_rank = b.rank;
		if( a_rank === b_rank )
			return a.ent_dist - b.ent_dist;
		else
			return a_rank > b_rank;
	}
*/
	function map_sort_dist( a, b ) {
		return  a.ent_dist - b.ent_dist;
	}
	function MFD_is_visible( name ) {
		var mfds = ps.multiFunctionDisplayList;
		if( !mfds || mfds.length ===0 )
			return false;											// ship damaged
		return index_in_list( name, mfds ) !== -1;
	}
	function qualifyMFD( map, staticFilter, dynamicFilter ) {
		var passStatic = staticFilter === 0 						// none specified OR matches static filter
							|| (map.staticMFD & staticFilter) > 0;
		var passAttitude = (dynamicFilter & MFD_ATTITUDE) === 0		// none specified OR matches attitude
							|| (map.dynamicMFD & dynamicFilter & MFD_ATTITUDE) > 0;
		var passRange = (dynamicFilter & MFD_RANGED) === 0 			// none specified OR matches range limit
							|| (map.dynamicMFD & dynamicFilter & MFD_RANGED) > 0;
		return passStatic && passAttitude && passRange;
	}
// : ' +  + '
	function bitsSet( map, stat, dyn ) {							// return # bits set for map in both stat & dyn
		var count = 0, 												//  - used as a crude determination of which list is better fit
			bits = map.staticMFD & stat;
		while( bits > 0 ) {
			if( bits & 1 )
				count++;
			bits >>>= 1;
		}
		bits = map.dynamicMFD & dyn;
		while( bits > 0 ) {
			if( bits & 1 )
				count++;
			bits >>>= 1;
		}
		return count;
	}
	var MFD_lines = [];												// formated lines for MFD
	var Aux_lines = [];
	var depthMFD, depthAUX;
	var MFD_ents = {};												// ents in MFD_lines, so don't repeat
	function update_MFDs( started, testing ) {
		var that = update_MFDs;
		var maps2rpt = (that.maps2rpt = that.maps2rpt || []);
		if( that.adjusted === undefined ) that.adjusted = 0;
		var adjusted = that.adjusted;
		var map, ent, rptlen, newLines, idx, primaryUp, auxiliaryUp;
		if( !mappingReady || maplen === 0 ) 						// not yet built OR empty
			return;
		primaryUp = MFD_is_visible( PrimaryMFD_name );
		auxiliaryUp = MFD_is_visible( AuxilaryMFD_name );
		if( !primaryUp && !auxiliaryUp ) {							// only update if being used (it's expensive)
			doClear_MFD( PrimaryMFD_name, true );
			doClear_MFD( AuxilaryMFD_name, true )
			return;
		}
		if( !started || testing ) {
			if( tasks_queued( update_MFDs ) ) 						// on a slow PC, _auto_updates may call before update cycle complete
				return;
			MFD_lines.length = 0;									// re-use arrays
			Aux_lines.length = 0;
			for( let ent in MFD_ents ) 								// re-use dictionaries
				if( MFD_ents.hasOwnProperty( ent ) )
					delete MFD_ents[ ent ];
			maps2rpt.length = 0;
			depthMFD = depthAUX = newLines = 0;
			mapping.sort( map_sort_dist );
			for( idx = 0; idx < maplen; idx++ ) {					// store maps to report so don't sort next frame
				maps2rpt[ idx ] = mapping[ idx ];
			}
			idx = 0;
		} else {													// continue from last frame
			depthMFD = MFD_lines.length;
			depthAUX = Aux_lines.length;
			newLines = 0;
			idx = started;
		}
		var continueMFD = SeparateMFDs								// aux is a continuation of primary when
						  && ( !MFDFiltering						// aux is active w/ no filter or same filter
							   || (MFDPrimaryDynamic === MFDAuxDynamic
								   && MFDPrimaryStatic === MFDAuxStatic) );
		rptlen = maps2rpt.length;
		for( ; idx < rptlen && ( (SeparateMFDs && (depthMFD < 10 || depthAUX < 10))
							|| (!SeparateMFDs && depthMFD < 10) ); idx++ ) {
			map = maps2rpt[ idx ];
			ent = map && map.ent;
			if( !map || !ent ) 										// was just deleted?
				continue;
			if( (newLines > 3 + adjusted) && !testing ) {			// split across frames as formatting takes time
				set_fn_pending( update_MFDs, idx, true );			// true is for tasks_deferred list
				return;
			}
			let qualifies, mfdBits = 0, auxBits = 0;
			if( !continueMFD && SeparateMFDs ) {					// decide which one gets it
				mfdBits = bitsSet( map, MFDPrimaryStatic, MFDPrimaryDynamic );
				auxBits = bitsSet( map, MFDAuxStatic, MFDAuxDynamic );
			}
			if( depthMFD < 10 ) {									// not full
				if( !MFDFiltering ) {								// just list the first 10
					qualifies = true;
				} else if( continueMFD								// just list the first qualified 10
							|| !SeparateMFDs						// no fit to worry about
							|| mfdBits >= auxBits ) {				// mfd list is a better fit (>= favours primary)
					qualifies = qualifyMFD( map, MFDPrimaryStatic,
												 MFDPrimaryDynamic );
				} else {											// - check that it passes filters
					qualifies = false;
				}
				if( qualifies ) {
					if( !primaryUp ) { 								// incr counter to keep aux aligned
						depthMFD++;
						if( !MFD_ents[ ent ] )						// add to reported ents dictionary
							MFD_ents[ ent ] = true;					//   so it won't appear in aux (should filters overlap)
					} else if( format_line( map, MFD_lines ) ) {	// only do expensive call when necessary
						newLines++;									// format_line adds to dictionary if map used
						depthMFD++;
					}
					continue;										// an ent cannot be in both MFDs, so move on to next
				}
			}
			if( depthAUX < 10 ) {									// not full
				if( !MFDFiltering ) {								// just list the first 10
					qualifies = true;
				} else if( continueMFD								// just list the first qualified 10
							|| !SeparateMFDs						// no fit to worry about
							|| auxBits > mfdBits					// aux list is a better fit (> excludes primary)
							|| (auxBits === mfdBits					// qualifies for both but didn't fit in primary
								&& !MFD_ents[ ent ]) ) {
							// || auxBits > mfdBits ) {				// aux list is a better fit (> excludes primary)
					qualifies = qualifyMFD( map, MFDAuxStatic,
												 MFDAuxDynamic );
				} else {											// - check that it passes filters
					qualifies = false;
				}
				if( qualifies ) {
					if( !auxiliaryUp ) { 							// incr counter to keep aux aligned
						depthAUX++;
						if( !MFD_ents[ ent ] )						// add to reported ents dictionary
							MFD_ents[ ent ] = true;					//   so it won't appear in main (should filters overlap)
					} else if( format_line( map, Aux_lines ) ) {	// only do expensive call when necessary
						newLines++;									// format_line adds to dictionary if map used
						depthAUX++;
					}
				}
			}
		}
		if( ps.setMultiFunctionText ) {
			if( depthMFD > 0 && primaryUp ) {
				ps.setMultiFunctionText( PrimaryMFD_name, MFD_lines.join( '\n' ), false );
			} else {
				doClear_MFD( PrimaryMFD_name, !primaryUp );
			}
			if( depthAUX > 0 && auxiliaryUp ) {
				ps.setMultiFunctionText( AuxilaryMFD_name, Aux_lines.join( '\n' ), false );
			} else {
				doClear_MFD( AuxilaryMFD_name, !auxiliaryUp );
			}
		}
	}
/*	"Telescope checks for new targets every second and performs an autoscan if it finds one: simple light sensors can see new dots
	 in the whole sky without using energy but must zoom with the main scope to determine the ship type which needs 2 energy points."
*/
	function check_if_new_targets() {								// detect when there are new target to consider
		if( found_new ) return;										// no need to check
		// "There are no passive gravity sensors so AutoScan will happen only if a new target arrives into the visible range."
		var ent, map, dist = -1, pastScannerRange = 0,
			isStn, stnsOnly = false, index = 0,
			allShips = system.allShips,								// array of all ships in system, sorted by distance
			alllen = allShips.length;								// - use this to suppress calc of distance, as exact value irrelevant
var numEnts = alllen - system_planets.length - 1;		// - 1 for sun
var breakEarly = MaxTargets <= 50 && numEnts > MaxTargets;// player has slow PC, so limit _create_Sightings() calls
		while( index < alllen ) {
			ent = allShips[ index++ ];
			if( ent === ps ) continue;
			let mapi = _Sighting_index( ent, 'check_if_new_targets' );
			if( mapi >= 0 ) {										// a known entity
				map = mapping[ mapi ];
				if( map.rank !== 'ukn' )							// ?is it an existing one that re-entered detection range
					 continue;
			} else {
				map = null;
			}
if( breakEarly && weaponsOnline && alertCondition === RED_ALERT ) {
	isBeacon = -1;									// force entity property get
	if( is_beacon( ent ) ) break;					// limit scanning while in battle
}
			if( pastScannerRange === 0 ) {
				dist = _detect_distanceTo( ent );
				if( dist > scannerRange ) {
					pastScannerRange = index - 1;					// last time in this if block
					if( !ext_ok ) break;
					if( !ent.isVisible	) continue;					// above all, it must be visible
				}
			} else {												// we use distance to know when to quit
				if( !stnsOnly && index > pastScannerRange
						&& (index - pastScannerRange) % 10 === 0 ) {// calc distance on every 10th ent
					dist = _detect_distanceTo( ent );
					if( dist > AutoScanMaxRange ) break;
					if( dist > scannerRange_X_10 )					// hard limit enough for largest ship?
						stnsOnly = true;
				} else {											// don't need to calc distance at all
					dist = -1;
				}
				if( !ent.isVisible ) continue;						// above all, it must be visible
				isStn = !stnsOnly ? false : ent.isStation;			// only check property once we're beyond limit
			}
if( breakEarly && weaponsOnline ) {
	if( alertCondition === YELLOW_ALERT && dist > scannerRange_X_2 ) {
		isBeacon = -1;								// force entity property get
		if( is_beacon( ent ) ) break;				// limit scanning of distant ents
	}
	if( alertCondition === GREEN_ALERT && dist > scannerRange_X_4 ) {
		isBeacon = -1;								// force entity property get
		if( is_beacon( ent ) ) break;				// limit scanning of distant ents
	}
}
			reset_common_vars();
			distance = dist > 0 ? dist : -1;						// restore known values if known
			isStation = stnsOnly ? isStn : -1;
			if( notable_ent( ent, false, (dist > 0 ? dist : null) ) ) {// found one missing or may need updating
				if( mapi < 0 ) {									// not in mapping
					let enti = _add_Sighting( ent, true, false, 'check_if_new_targets' );
					if( debug && (enti < -2 && enti > -8) ) {
						let reason = add_Sighting_errors[ enti ];
						log(ws.name, 'check_if_new_targets, _add_Sighting returned "' + reason
							+ '", distance: ' + distance + ': ' + ent );
					}
					new_targets.push( ent );
					///moved from below 2Cif--# repeat msgs
				} else {
					update_one_Sighting( map, ent, mapi, false );	// false suppresses call to notable_ent
				}
				///new_targets.push( ent );
				break;
			}
		}
		using_common_vars = false;
	}
	function auto_updates( forced_scan ) {
		try {
			_auto_updates( forced_scan );
		} catch( err ) {
			log( ws.name, ws._reportError( err, 'auto_updates', forced_scan ) );
			if( debug ) throw err;
		}
	}
	function _auto_updates( forced_scan ) { //check for most centered 4 times/second; new targets, update MFD once/second
		// forced_scan is true for user directed actions: weapons off-line, a Rescan or stepping past list boundary
		ps = player && player.ship;
		if( !ps || !ps.isValid || alertCondition === DOCKED )		//player died or docked
			return;
		if( !check_equip_ok() ) return;
		found_new = found_new
					|| ( new_targets && new_targets.length > 0 );	// check if any found in last scan
		if( forced_scan ) {
			if( report_status && chk_energy_gs_status( true ) ) 	// true will cause chk_energy_gs_status() to create new mapping
				consoleMessage( 'Starting new scan ...', ConsoleMsgDurn );
			return;													// must not incr. counter or select target
		}
		if( alertCondition < RED_ALERT )							// gravity scanner suspended during Red Alert
			update_grav_scan();										// invoked on ea. 1/4 sec; handles gravScanProgress
		if( AutoScan && quarter_sec_counter === 0
				&& !fns_are_pending() ) {							// create/update takes 20+ frames
			if( chk_energy_gs_status( false ) ) {					// false to suppress msgs; they only occur on user demand scans
				if( found_new )	{									// only when there are newly found
					_update_Sightings();							//	 (routine update calc's are in reposition_effects())
				} else {
					_update_Sightings( gravScanProgress === 0		// _update_Sightings( just_mapping )
									|| gravScanProgress === 1 );	// update only those ents in mapping, unless grav scan in flux
				}
			}
		} else if( quarter_sec_counter === 1 ) {
			check_Sightings( true );								// true -> 'quickly'; this just checks the health of ents in mapping
			 if( AutoScan ) {
				 check_if_new_targets();
			 }
		} else if( quarter_sec_counter === 2 ) {
			init_headingView();
			checkCombatMFD();
			update_MFDs();
		} else { // quarter_sec_counter === 3
			check_Sightings( false );								// update should be finished, so do proper check
		}
		if( !ws.$are_Steering ) {									//no retarget during autosteering
			if( identKeyPress >= IDENT_UNLOCK ) {					// just did an Ident unlock, manage delay counter, if any
				if( IdentDelay > 0 && delay_counter < 0 ) {			// initiate delay counter
					delay_counter = IdentDelay;						// it's specified in quarter seconds, so ...
				} else if( delay_counter > 0 ) {					// counter is running
					delay_counter--;								// ...decr on each call here
				}
				if( delay_counter === 0 ) {							// counter is finished
					_resetIdentDelay();
					if( identKeyPress === IDENT_STEER_DELAY )		// successful auto steer
						_manage_marker( curr_S.map, false, '_mostCentered (IdentKeyPress = IDENT_STEER_DELAY)' );
					ws.$IdentKeyPress = identKeyPress = IDENT_READY;// ...resume targeting
// if( debug ) log('_auto_updates, delay_counter === 0, identKeyPress := IDENT_READY');
				}
			} else if( identKeyPress === IDENT_READY ) {
				if( weaponsOnline ) {								//in auto mode
					if( curr_target === null && AutoLock !== 0 ) {	//no target and autolock not disabled
						_mostCentered( 'auto' );
					}
				} else if( GravLock !== 0 )	{						//in grav mode and not disabled
					_mostCentered( 'grav' );
				}
			}
		}
		quarter_sec_counter--;
		if( quarter_sec_counter < 0 )								// update counter for per sec tasks
			quarter_sec_counter = 3;								//do once in a second when reach 0
	}
	function _resetIdentDelay() {									// called from mode()
		delay_counter = -1;
	}
///////////////////////////////////////////////////////////////////////////////////////////////////
// steering ///////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////
	// local variable unique to steering
	var steer_map, steer_ent, ps_maxPitch, towbarShip, towShipmass;
	function halt_steering( angle_to_target ) {						// stop steering
		ws.$are_Steering = false;
		ws.$TelescopeSteerFCB = null;								// used by Towbar oxp
		if( SniperLock ) {											// improve compatibity (thanks Milo)
			SniperLock.deactivate = "FALSE";
		}
		if( SniperLockPlus ) {										// improve compatibity
			SniperLockPlus.$enabled = true;
		}
		steer_ent = steer_map = null;
		if( angle_to_target === undefined ) {
// if( debug ) log('halt_steering, angle_to_target === undefined' );
			return;
		}
		if( angle_to_target <= ONE_DEGREE * 1.01
				&& identKeyPress === IDENT_STEERING ) {				// auto steer reached target
			ws.$IdentKeyPress = identKeyPress = IDENT_STEER_DELAY;	// get ident delay but no clearing of target
// if( debug ) log('halt_steering, identKeyPress := IDENT_STEER_DELAY');
			consoleMessage( 'Telescope steering ended, lock released', ConsoleMsgDurn );
		}
	}
	function start_Steering() {										//turn to the target
		steer_map = curr_S.map;
		if( !steer_map ){
// if( debug ) log('start_Steering, !steer_map, bailing');
			return;
		}
		steer_ent = steer_map.ent;
		if( !steer_ent || !steer_ent.isValid ) {
// if( debug ) log('start_Steering, ' + (!steer_ent ? '!steer_ent' : '!steer_ent.isValid') + ', bailing');
			return;
		}
		if( viewDirection !== 'VIEW_FORWARD' ) { 					//working in forward view only
// if( debug ) log('start_Steering, viewDirection !== VIEW_FORWARD, bailing');
			return;
		}
		ps_maxPitch = ps.maxPitch;									// not set in _init_player_vars as only used for steering
		towbarShip = null;
/*
if( debug && (ws.$FixedTel === 1 || ws.$are_Steering) ) {
	log('start_Steering, cannot start as ' + (ws.$FixedTel === 1 ? 'ws.$FixedTel === ' + ws.$FixedTel : 'ws.$are_Steering is ' + ws.$are_Steering));
}
 */
		if( ws.$FixedTel !== 1 && !ws.$are_Steering ) {				// fully repaired & not already steering
			copy_vector( ps_vectorForward, prevHeading );
			if( Towbar ) {
				towbarShip = Towbar.$TowbarShip;
				towShipmass = towbarShip ? towbarShip.mass : 0;
			}
			if( SniperLock ) {										// improve compatibity (thanks Milo)
				SniperLock.deactivate = "TRUE";
			}
			if( SniperLockPlus ) {									// improve compatibity
				SniperLockPlus.$enabled = false;
			}
			ws.$are_Steering = true;
			ws.$TelescopeSteerFCB = ws.$Sighting_events_FCB;		// Towbar oxp checks isValidFrameCallback
		}
	}
	function _steerFCB( delta ) {
		try {
			var angle_to_target, angle_traversed;
			if( !steer_ent || _has_bad_status( steer_ent ) || !steer_map 	// _has_bad_status checks isValid
					|| !steer_map.last_posn || steer_map.last_posn.length === 0 ) {
/*
if( debug ) {
	if( !steer_ent )
		log('_steerFCB, !steer_ent, bailing');
	else if( _has_bad_status( steer_ent ) )
		log('_steerFCB, _has_bad_status( steer_ent ), bailing');
	else if( !steer_map )
		log('_steerFCB, !steer_map, bailing');
	else if( !steer_map.last_posn )
		log('_steerFCB, !steer_map.last_posn, bailing');
	else if( steer_map.last_posn.length === 0 )
		log('_steerFCB, steer_map.last_posn.length === 0, bailing');
	else
		log('_steerFCB, Yikes, do not know why, bailing');
}
 */
				if( steer_ent ) {
					halt_steering();								//end of steering (aborted)
				}
				return;
			}	// target still alive!
			copy_vector( steer_map.last_posn, position );			// if 'ukn', steer to last known position
			subtract_vectors( position, ps_position, vector );
			unit_vector( vector, vector );
			angle_to_target = angle_between_two_unitV( ps_vectorForward, vector );
			angle_traversed = angle_between_two_unitV( ps_vectorForward, prevHeading );
			if( angle_traversed < 0.005 && angle_to_target > ONE_DEGREE ) { //steer if no manual steering and not in 1 degree
				//if the above angle_traversed value lower then can not start steering in my intel atom netbook
				let opt1 = ps_maxPitch * delta;
				let opt2 = angle_to_target / 12;
				let angle = opt1 < opt2 ? opt1 : opt2;				//half max turn/step and not too accutate
				if( towbarShip && towbarShip.isValid ) {
					let tow_opt = ps_mass / towShipmass;
					let ma = tow_opt < 2 ? tow_opt : 2;				///small ship max. 2x
					angle = angle * ma / 3;							// 1/3 of the original rate with same mass, min. 1/5 max. 2/3
//					player.consoleMessage('Telescope slow steering with towed ship '+angle_traversed);//debug
				}
//log(ws.name, '_steerFCB, angle_to_target=' +angle_to_target*180 / Math.PI
//	+', angle_traversed=' +angle_traversed*180 / Math.PI +', a='+a*180 / Math.PI);
				cross_product( ps_vectorForward, vector, cross ); 	//set the plane where we should rotate in
				unit_vector( cross, cross );
				rotate_about_axis( ps_orientation, cross, -angle, quaternion );
				ps.orientation = quaternion;
				vector_forward_from_quaternion( quaternion );
				copy_vector( ps_vectorForward, prevHeading );
			} else {												//end of steering (normal)
				halt_steering( angle_to_target );
			}
		} catch( err ) {
			log( ws.name, ws._reportError( err, '_steerFCB', delta ) );
			if( debug ) throw err;
		}
	}
///////////////////////////////////////////////////////////////////////////////////////////////////
// _hud_effects_closure ///////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////
	// local variables unique to _HUD_effects_closure
	var prevHeading = [],		//heading vector of the player ship in the previous frame to detect manual steering
		viewPosition  = [0,0,0],//base position of the visual effect
		haveViewPosn = false,	// flag for view positioning calculations
		vShipShift = [],		// HUD specific shift of visual target
		haveHUDShift = false,	// flag for HUD shift calculations
		effect_start = [],		//starting point for all effects (50m in front of view position)
		effect_viewposn = [],	//initially calc'd posn common to both virtual ship & sniper ring
		vShip_posn = [],		//position of the virtual ship model (adjusted from viewPosition for scale, ring)
		weaps = false,			//the previous state of the player weapons
		vRing = null,			//a ring around the visual effect target
		vShip = null,			//visual effect to show the selected target
		vDataKey = null,		//key of the visual effect
		vDKsubst = null,		//key of the visual effect substituted for missing effectdata
		weaponPosition = [],	//sniper ring adjusts for offset to enhance accuracy
		weaponZOffset = null,	// - used to calculate sniper ring offsets
		sRingCorrection = [],	// - used to calculate sniper ring offsets
		haveWeaponPosn = false,	// flag for view positioning calculations
		sniper = null,			//if this ring guided around the crosshair then the far target is lined up correctly
		sRing_posn = [],		//position of the sniper ring (adjusted from viewPosition for scale, ring)
		vShipScale = 0,			// scaling factor to fit vShip effect into vRing
		vsize = !weaponsOnline ? VisualTargetNormalSize / 10 	// these options can range from 0-8
							   : VisualTargetCombatSize / 10,
		vsizechanged, wide;
	function _clear_HUD_Effects() {									//Clear Visual Effect Ship Model and Visual Marker also
		clear_SnipeRing();
		clearVShip();
	}
	function clear_SnipeRing() {									//Clear small sniper ring
		if( sniper ) {
			sniper.remove();
			sniper = null;
ws.$sniper = null; // debug
		}
	}
	function clearVShip() {											//Clear Visual Effect Ship Model and large visual ring
		if( vShip ) {
			vShip.remove();
			vShip = null;
			vDataKey = null;										//need to show again when reident
			vDKsubst = null;
		}
		if( vRing ) {
			vRing.remove();
			vRing = null;
		}
	}
/*
var fmt_position = function fmt_position(posn) {
	if( !posn || !Array.isArray(posn))
		return 'not a vector (' + posn + ')';
	var out = '(';
	for( let idx=0, len=posn.length; idx <len; idx++) {
		out += idx > 0 ? ', ' : '';
		out +=  posn[idx].toFixed(5);
	}
	return out + ')';
}
 */
	function _set_vShip_posn( viewposFwd, vShift ) {				// called from shipWillLaunchFromStation
													// 	ws._set_vShip_posn( ps.viewPositionForward, ws.$VTarget_HUD_shift );
		ps = player && player.ship;
		if( !ps ) return;
		haveViewPosn = haveWeaponPosn = haveHUDShift = false;
		var weaponPosnFwd = ps.weaponPositionForward;	// array of Vector3D
		var wPosnX = 0, wPosnY = 0, wPosnZ = 0;
		var idx = 0, len = weaponPosnFwd.length;
		for( ; idx < len; idx++ ) {
			wPosnX += weaponPosnFwd[idx][0];
			wPosnY += weaponPosnFwd[idx][1];
			wPosnZ += weaponPosnFwd[idx][2];
		} // average the positions of weapons (in some oxps, can be multiple)
		weaponPosnFwd[0] = wPosnX / len;
		weaponPosnFwd[1] = wPosnY / len;
		weaponPosnFwd[2] = wPosnZ / len;
		copy_vector( weaponPosnFwd, weaponPosition );
		haveWeaponPosn = !same_vectors( weaponPosition, VECTOR_ALL_ZEROS );
		if( weaponZOffset === null ) {
			weaponZOffset = weaponPosnFwd;							// reuse array
			weaponZOffset[0] = 0;
			weaponZOffset[1] = 0;
		} else {
			free_array(weaponPosnFwd);
		}
		if( !viewposFwd ) return;
		copy_vector( viewposFwd, viewPosition );					// save supplied position
		haveViewPosn = !same_vectors( viewPosition, VECTOR_ALL_ZEROS );
		if( !vShift ) return;
		copy_vector( vShift, vShipShift );							// save supplied HUD shift for 3D model
		haveHUDShift = !same_vectors( vShipShift, VECTOR_ALL_ZEROS );
	}
	var HUD_vars_init = false;	// flag to prevent duplicate calculations
	var grayLevels = [	[ 0.05, 0.05, 0.05, 1],
						[ 0.1, 0.1, 0.1, 1],
						[ 0.15, 0.15, 0.15, 1],
						[ 0.2, 0.2, 0.2, 1],
						[ 0.25, 0.25, 0.25, 1],
						[ 0.3, 0.3, 0.3, 1],
						[ 0.35, 0.35, 0.35, 1],						// ~darkGrayColor
						[ 0.4, 0.4, 0.4, 1],
					  //[ 0.5, 0.5, 0.5, 1]							// grayColor
					 ];
	var brightnessLevel = 0;										// level increased using clock.absoluteSeconds
	var lastGrayLevelTime = 0;
	function makeItBrighter( obj ) {
// log(ws.name, 'makeItBrighter, entry, parm obj: ' + obj);
// if(debug && obj.constructor !== VisualEffect) {
	// log(ws.name, 'makeItBrighter,  *** !VisualEffect ***,  parm obj: ' + obj);
	// if( obj && cd && !obj.position )
		// log(ws.name, cd._showProps( obj, 'obj' ));
// }
		var mats = obj.getMaterials();
		for( var prop in mats ) {
			if( mats.hasOwnProperty( prop ) ) {
				if( mats[ prop ].hasOwnProperty( 'illumination_map' )
						&& mats[ prop ].illumination_map !== 'telescope-illumination.png' )
					continue;										// it has its own, we didn't modify in collect_effectdata
				if( mats[ prop ].hasOwnProperty( 'illumination_modulate_color' ) ) {
					mats[ prop ].illumination_modulate_color = grayLevels[ brightnessLevel ];
					obj.setMaterials( mats );
				}
			}
		}
// log(ws.name, 'makeItBrighter, exit');
	}
	function illuminate() {
		var start = illuminate.start || 0;
// log(ws.name, 'illuminate, entry, .start = ' + start );
		if( !vShip ) {	/// thanks to Dybal
		   log(ws.name, 'illuminate, invalid vShip: ' + vShip + ', start: ' + start
				+ ', brightnessLevel: ' + brightnessLevel + ', lastGrayLevelTime: ' + lastGrayLevelTime );
// log(ws.name, 'illuminate, exit (false), .start = ' + illuminate.start );
		   return false;
		}
		makeItBrighter( vShip );
		var idx, list = vShip.subEntities,
			len = list && list.length || 0;
		for( idx = start; idx < len; idx++ ) {
			if( idx > start && idx % 3 === 0 ) {					// setMaterials calls in makeItBrighter are expensive
				illuminate.start = idx;								//	 so spread over several frames
// log(ws.name, 'illuminate, exit (false), .start = ' + illuminate.start );
				return false;
			}
			makeItBrighter( list[ idx ] );
		}
		illuminate.start = 0;
// log(ws.name, 'illuminate, exit (true), .start = ' + illuminate.start );
		return true;
	}
	function mk_vship( key, noset ) {								// by not setting vDataKey, prevent trashing
		if( dataKey || key ) {
			let add_key = key ? key : dataKey;
			vShip = addVisualEffect( add_key, ps_position );
			if( vShip ) {
				// cannot use effect's collisionRadius as it may lack subEntities! eg. ddtmanta's wings, fighter swarm group
				let vsCR = curr_target.collisionRadius;
				if( vsCR === 0 || !vShip.isValid ) {				// dataKey not in effecdata.plist
					vShip.remove();
					vShip = null;
					log(ws.name, 'mk_vship, removed ' +add_key+ ' as '
						+ (!vShip.isValid ? '!vShip.isValid' : 'vShip.collisionRadius === 0') );
					return false;
				}
ws.$vship = vShip; // debug
				lastGrayLevelTime = clock.absoluteSeconds;
				brightnessLevel = 0;
				if( noset ) {										// using a substitute model
					vDKsubst = add_key;
					vDataKey = dataKey;
				} else {
					vDataKey = vDKsubst = add_key;					// only set if using target's dataKey
				}
				if( vDKsubst === 'oolite-unknown-ship' ) {			// question mark size independent of target
					vsizechanged = true;							// signal for update of scaling, position
					return true;
				}
				vShip.scannerDisplayColor1 = null; 					//hide from the scanner
				vShip.scannerDisplayColor2 = null; 					//  "
				let bbox = curr_target.boundingBox;					// effects have no boundingBox <sigh>
				let bbRadius = sqrt( bbox[0]*bbox[0] + bbox[1]*bbox[1] + bbox[2]*bbox[2] ) / 2;
				// let z = bbRadius / 6; 							// all models scaled as a 6th for constant apparent size
				// - this assumes that the target & vship are close in size, not always true!	thanks Dybal & Milo
				// from the 'telescope-sniper.dat' file, model has an outer radius of 42, inner is 40
				// historically, it's placed @ 50 meters and scaled by 1/6
				// multiply by vsize in [0.1 .. 0.8] (a tenth of VisualTargetNormalSize or VisualTargetCombatSize)
				// yields its collisionRadius in [0.6667 .. 5.333]
				// => ensure vship is as large as possible while not exceeding ring's inner radius (whether shown or not)
				let ringRadius = (40/6) * vsize;
				let vShipBBRad;
				if( curr_target.isStation || (SpicyHermits && curr_target.isRock && !curr_target.isFrangible)) {
					// station effects have different collisionRadius than actual station
					vShipBBRad = bbRadius;
				} else {										
					vShipBBRad = bbRadius < vsCR ? vsCR : bbRadius; // use larger so subEntities will be enclosed by model's ring
				}
				let scaling = ringRadius / vShipBBRad;
				if( scaling > 0 ) {									// factor out vsize, as user can change on the fly
					vShipScale = scaling / vsize;					// - in position_and_orient, vShip.scale( vShipScale * vsize );
					vsizechanged = true;							// signal for update of scaling, position
					// return true;
					return false;
					// - delay illumination sequence until next frame
				} else {
					log(ws.name, 'mk_vship, unable to scale effect ... removing: ' + add_key );
				}
			}
		}
		return false;
	}
	var basic_models = [ "adder", "anaconda", "asp", "boa", "boa-mk2", 'buoy', "cobra", "cobra3", "cobramk1",
						 "ferdelance", "moray", "morayMED", "python", //original player ships
						 "base", "corvette", "cruiser", "drone", "freighter", "frigate",
						 "fighter", "gunship", "miner", "runner" ]; //custom ships
	function _showVShip( dkey ) {									//Show Visual Effect
		if( _showVShip.maxGrayLevel === undefined )					// store length so not checked every frame
			_showVShip.maxGrayLevel = grayLevels.length;			//	 (it's static)
		var maxGrayLevel = _showVShip.maxGrayLevel;
		HUD_vars_init = false;
		let clear_it = true;
		do {
			if( ShowVisualTarget === 0 ) break;						// turned off by user
			if( !weaponsOnline
				&& ( ShowVisualTarget < 1 || VisualTargetNormalSize === 0 ) )
				break;												// turned off by user
			if( weaponsOnline
				&& ( ShowVisualTarget < 2 || VisualTargetCombatSize === 0 ) )
				break;												// turned off by user
			if( moving_fast ) {
				vShipSuspended = ps_torusEngaged;					// turn off model until torus ends (too jittery)
				if( vShipSuspended )
					break;
			} else {
				vShipSuspended = false;
			}
			if( viewDirection !== 'VIEW_FORWARD' ) break;			// not facing forward
			if( ws.$FixedTel !== 0 ) break;							// cheap repair
			if( !curr_target ) break;
			if( !ShowVisualStation && curr_target.isStation ) break;  // don't show stations
			if( _has_bad_status( curr_target ) ) break;
			if( curr_S && curr_S.map && curr_S.map.rank === 'ukn' )
				break;												// lost target
			clear_it = false;										// thru gauntlet, ok to show!
		} while( false );
		if( clear_it ) {
			clearVShip();
			return;
		}
		dataKey = dkey ? dkey : curr_target && curr_target.dataKey;
		if( !dataKey || curr_target.radius							  // planet, moon or sun
					 || !curr_target.isValid ) {					  // nothing to show
			clearVShip();
			return;
		}
		if( !vShip || !vShip.isValid								 // no model or gone bad
				|| (vDataKey !== dataKey && vDKsubst !== dataKey )) {// OR different target
			if( vShip ) vShip.remove();
			vShip = null;
		}
		var made_ship = false;
//if( debug && !vShip ) log(ws.name, "_showVShip, dataKey = "+dataKey+", vShip "+vShip+", isStation "+curr_target.isStation+", ShowVisualStation "+ShowVisualStation);//debug
		if( !vShip ) {
			made_ship = mk_vship();									// 1st try ship's dataKey
		}
		let find_dk = '';
		if( !vShip ) {												// next, try basic model
			let idx = basic_models.length;
			while( idx-- ) {										// in reverse so longer names match 1st
				let model = basic_models[ idx ];
				if( dataKey.indexOf( model ) >= 0 ) {
					find_dk = model;
					break;
				}
			}
			if( find_dk !== '' ) {
				made_ship = mk_vship( find_dk, true );
			}
		}
		if( !vShip && ShowVisualQuestionMark ) {					 //fallback
			made_ship = mk_vship( 'oolite-unknown-ship', true );
		}
		if( !vShip ) {
			clearVShip(); //silent fallback
			return;
		}
		if( vDKsubst !== 'oolite-unknown-ship' && !curr_target.isRock && // do not illuminate the question mark/rocks
			!made_ship && brightnessLevel < maxGrayLevel ) {		// do not illuminate on same call as made_ship (both are expensive)
																	//	 as illuminate	doubles _showVShip execution time
			let call_it = illuminate.start !== 0;					// is current level done?
			if( !call_it ) {										// if it is, check if time for next
				let absSeconds = clock.absoluteSeconds;
				if( absSeconds - lastGrayLevelTime > 0.1 ) {		// time to do next level
					call_it = true;
					lastGrayLevelTime = absSeconds;
				}
			}
			if( call_it ) {
				if( illuminate() && realtime_fps ) {				// only incr when current level complete
					if( realtime_fps() > 50 ) {						// relatively fast PC
						brightnessLevel++;							// gradually increase brightness as telescope gathers light
					} else {										// PC is slow
						brightnessLevel = maxGrayLevel;				// display at maxGrayLevel immediately
					}
				}
			}
		}
		
		position_and_orient();
	}
	function calc_effects_vars( forSniper ) {						// 1st apply shift for viewPosition, then shift for vShip
																	// NB: sRing_posn is used as an intermediary; when we need to
																	//	   show sniper ring, calculations are ready (ie. cost nothing
		var shift;													//	   as were needed by _showVShip anyway)
		if( haveViewPosn ) {
			copy_vector( ps_position, effect_start );
			shift = viewPosition[0];
			if( shift ) {
				scale_vector( ps_vectorRight, shift, vector );
				add_vectors( vector, effect_start, effect_start );
			}
			shift = viewPosition[1];
			if( shift ) {
				scale_vector( ps_vectorUp, shift, vector );
				add_vectors( vector, effect_start, effect_start );
			}
			shift = viewPosition[2];
			shift = shift ? shift + 50 : 50;						// all effects are forward +50 from viewPosition
			scale_vector( ps_vectorForward, shift, vector );
			add_vectors( vector, effect_start, effect_start );
		} else {
			scale_vector( ps_vectorForward, 50, vector );			// all effects are forward +50 from viewPosition
			add_vectors( vector, ps_position, effect_start );
		}
		copy_vector( effect_start, sRing_posn );
		if( !forSniper ) {											// called for vShip (so calc's for sniper are free)
			copy_vector( effect_start, effect_viewposn );			// apply any vShipShift
			if( haveHUDShift ) {
				shift = vShipShift[0];
				if( shift ) {
					scale_vector( ps_vectorRight, shift, vector );
					add_vectors( vector, effect_viewposn, effect_viewposn );
				}
				shift = vShipShift[1];
				if( shift ) {
					scale_vector( ps_vectorUp, shift, vector );
					add_vectors( vector, effect_viewposn, effect_viewposn );
				}
				shift = vShipShift[2];
				if( shift ) {
					scale_vector( ps_vectorForward, shift, vector );
					add_vectors( vector, effect_viewposn, effect_viewposn );
				}
			}
		}
		if( target_vector.length === 0 ) {							// not initialized by _update_target_marker
			copy_vector( target_posn, vector );
			subtract_vectors( vector, ps_position, target_vector );
			unit_vector( target_vector, target_direction );
		}
		HUD_vars_init = true;										// prevents unnecessary call from sniper_ring()
	}
	var ring_color = [];											// working vector for position_and_orient, sniper_ring
	var vShipSuspended = false;										// stop showing vShip when speed gets too high, ie. Torus drive
	function position_and_orient() {								//orient vship model
		var that = position_and_orient;
		var model_orientation = (that.model_orientation = that.model_orientation || []);
		if( !vShip || !vShip.isValid || vShip.radius ) return;
		calc_effects_vars();
		// update model's position
		if( vsizechanged ) {
			if( vShip.dataKey === 'oolite-unknown-ship' ) {			// custom sized
				vShip.scale( 10*vsize - 2 );
			} else {
				let z = vShipScale * vsize;
				if( z > 0 ) {
					vShip.scale( z ); //shrink
// log('position_and_orient, scaling vShip by ' + (vShipScale * vsize).toFixed(4)+ ', not z: ' + z.toFixed(4));
				}
			}
		}
		var up = 18;	// arbitrarily chosen height; user can alter w/ viewPosition
		// calc alignment so top is constant: vsize*6 is radius of model/ring; 24*( 0.75 - wide ) carry over - needed for HUDs???
		copy_vector( effect_viewposn, vShip_posn );
		// wide < 1, === gameWindow.height / gameWindow.width; ///widescreen correction
		// is 0.75 for std screen, 0.625, 0.546875, etc. for wide screens
		scale_vector( ps_vectorUp, ( up - vsize*6 - 24*( 0.75 - wide ) ), vector );
		add_vectors( vector, vShip_posn, vShip_posn );
		vShip.position = vShip_posn;
/*
		// - frame_delta is set by call to _hud_effects() which preceeds _reposition_effects()
		var dotP = dot_product( ps_vectorForward, target_direction );
		// - more sensitive to change in frame rate for ents parallel to heading, less so when perpendicular
		var contract = 250 + (250 * ps_speed/(ps_maxSpeed * 32));	// base amt for all directions
		if( dotP >= 0 ) {											// moving towards light ball
			contract += 0.5 * travel * dotP;
		} else {													// moving away from light ball
			contract += -1.5 * travel * dotP;
		}
		let adjust = -100 * ( floor(contract/100) ); 				// to hundreds to reduce jitter
		scale_vector( target_direction, adjust, speed_adj );
		add_vectors( dst_posn, speed_adj, dst_posn );
 */
///
/*
if( debug && vsizechanged ) log(ws.name, 'position_and_orient, vShip.scaleX = ' + vShip.scaleX.toFixed(2)
	+ ', vShip.position = [ ' + vShip_posn[0].toFixed() + ', ' + vShip_posn[1].toFixed() + ', ' + vShip_posn[2].toFixed() + ' ]'
	+ ', dist from ps = ' + ps.position.distanceTo(vShip).toFixed(2)
	+ ', angled from Fwd, Up & Right: ' + (ps.vectorForward.angleTo(vShip) * 180/Math.PI).toFixed(1) + ', '
										+ (ps.vectorUp.angleTo(vShip) * 180/Math.PI).toFixed(1) + ', '
										+ (ps.vectorRight.angleTo(vShip) * 180/Math.PI).toFixed(1) );
 */
		// orientate 3D model
		if( curr_target.isVisible									//orientation is known only if visible (Grav.Scanner can give position only)
				|| curr_S.map.ent_dist <= scannerRange ) {			//	or in scannerRange (spec.case for small: missiles, drones, cargo, etc.)
																	//  which may be !isVisible well inside scannerRange
			let angle = angle_between_two_unitV( ps_vectorForward, target_direction );
			cross_product( ps_vectorForward, target_direction, cross );
			unit_vector( cross, cross );							// cross product of unit vectors not guaranteed to yield a unit vector
			copy_quaternion( curr_target.orientation, model_orientation );
			rotate_about_axis( model_orientation, cross, -angle, quaternion );
		} else {													//nonvisible, fixed view only
			let fixed = vShip.isStation ? PI + 0.22					//stations facing
										: QUARTER_ARC + 0.22;		///ships viewed from top (90 degree plus a bit)
			if( vShip.isMainStation ) {								//rotate to horizontal dock position
				rotate_about_axis( ps_orientation, ps_vectorRight, fixed, model_orientation );
				copy_vector( vShip.vectorForward, vector );
				rotate_about_axis( model_orientation, vector, QUARTER_ARC, quaternion );
			} else {
				rotate_about_axis( ps_orientation, ps_vectorRight, fixed, quaternion );
			}
		}
		vShip.orientation = quaternion;
		// model's ring
		let ring_scale = (vsize * 1.05)/ 6;	 ///shrink
		if( VisualTargetRing && ring_scale > 0 ) {					//large visual target ring
			if( !vRing ) {
				vRing = addVisualEffect( "telescope-sniper", vShip_posn );
				if( exact_same_vectors( ModelRingColor, VECTOR_ALL_ZEROS ) ) {		// special directives to match reticle color
					copy_vector( ps.reticleColorTarget, ring_color, true );			// cannot cache reticleColors, as hud could change
				} else if( exact_same_vectors( ModelRingColor, VECTOR_ALL_ONES ) ) {// special directives to match locking reticle color
					copy_vector( ps.reticleColorTargetSensitive, ring_color, true );
				} else {
					copy_vector( ModelRingColor, ring_color, true );
				}	// 3rd parm to copy_vector prevents parm validation
				vRing.setMaterials( { "telescope-ring.png": { emission_color: ring_color } } );
					// from effecdata: materials = { "telescope-ring.png" = { emission_color = darkGrayColor;};}
				vRing.scale( ring_scale );
			} else {
				vRing.position = vShip_posn;
				if( vsizechanged ) {								//if weapons switched on/off set new size (small/large)
					vRing.scale( ring_scale ); ///shrink
				}
			}
			vRing.orientation = ps_orientation;
ws.$vring = vRing;//debug
		} else if( vRing ) {
			vRing.remove();
			vRing = null;
		}
		vsizechanged = false;
	}
	function sniper_ring() {										//Visual FrameCallBack for the small sniper ring
		if( !curr_target 											// lost target
// 			|| SniperRange <= SniperMinRange						// turned off by user
// simplified options by having single toggle
			|| SniperRingSize === 0									// turned off by user
			|| (_getShowState() & _currSniperRingFlags()) === 0		// turned off by user in this state
			|| ws.$FixedTel !== 0 ) {								// no target or cheap repairs
			if( sniper )
				clear_SnipeRing();
			return;
		}
/*
		if( sniper && sniper.$TelescopeSniperTarget !== curr_target ) {
			clear_SnipeRing();
		}
 */
		var snipertarget = false;
		if( !HUD_vars_init ) calc_effects_vars( true );				// true limits calcs to those for sniper ring only
		distance = vector_magnitude( target_vector );
		let ctCR = curr_target.collisionRadius;
		if( distance - ctCR < SniperRange && distance - ctCR > SniperMinRange ) {
/*
			if( angle_between_two_unitV( ps_vectorForward, target_vector ) < QUARTER_ARC ) { //to exclude aft line-up
				let dratio = -2 * distance / ctCR;                   //distant and smaller target tracked with larger ring movement
				let ax = dratio * ( angle_between_two_unitV( ps_vectorRight, target_vector ) - QUARTER_ARC );
				let ay = dratio * ( angle_between_two_unitV( ps_vectorUp, target_vector ) - QUARTER_ARC );
				if( abs_v( ax ) < 5 &&  abs_v( ay ) < 6 * wide ) {  //ay<4 if 4:3, <3.4 if 16:9
 */			// new method yields same magnitude for ax, ay
			if( dot_product( target_direction, ps_vectorForward ) > 0 ) { //to exclude aft line-up
				/* core code shoots from weapon position, sets reticle using view position
				   w/o adjustment, separation of view & weapon will mk sniper ring innaccurate
					eg. DTT PE has viewPosition.y of 4.9 & weaponPosition.y of 13.5 (laser is almost 9m above view)
						which places sniper ring too low  */
				let dratio = 0;
				if( SniperLock && SniperLock.deactivate === "FALSE" && SniperLock.sniperlockchangeflag === "ON" ) {
					let slsTargetPosn = SniperLock.sniperlocktargetlastposition3;
					copy_vector( slsTargetPosn, vector );
					subtract_vectors( vector, ps_position, vector );// make new target_vector
					distance = vector_magnitude( vector );
					dratio = -distance / ctCR;
					unit_vector( vector, vector );					// make new target_direction
				} else if( SniperLockPlus && SniperLockPlus.$enabled && SniperLockPlus.$slpState === "ON" ) {
					let slpsTargetPosn = SniperLockPlus.$slpTargetPosition_3;
					copy_vector( slpsTargetPosn, vector );
					subtract_vectors( vector, ps_position, vector );// make new target_vector
					distance = vector_magnitude( vector );
					dratio = -distance / ctCR;
					unit_vector( vector, vector );					// make new target_direction
				} else if( haveWeaponPosn ) {						// correct for weapon position offset wrt viewPositionForward
					// dybal's solution from sniperlock_plus
					subtract_vectors( weaponZOffset, weaponPosition, sRingCorrection );
					rotate_vector( sRingCorrection, ps_orientation )
					add_vectors( target_posn, sRingCorrection, vector )// shift target position by sRingCorrection
					subtract_vectors( vector, ps_position, vector );// make new target_vector
					distance = vector_magnitude( vector );			// ok to clobber distance here as not used again
					dratio = -distance / ctCR;
					unit_vector( vector, vector );					// make new target_direction
				} else {
					dratio = -distance / ctCR;						//distant and smaller target tracked with larger ring movement
					copy_vector( target_direction, vector );
				}
				let ax = dratio * dot_product( vector, ps_vectorRight ),
					ay = dratio * dot_product( vector, ps_vectorUp );
				if( abs( ax ) < 5 && abs( ay ) < 6 * wide  ) {	//ay<4 if 4:3, <3.4 if 16:9
				// - restored original's criteria
				// if( abs( ax ) < 4 && abs( ay ) < 4  ) {	//ay<4 if 4:3, <3.4 if 16:9
					scale_vector( ps_vectorRight, ax, vector );	//show misalignment
					add_vectors( vector, sRing_posn, sRing_posn );
					scale_vector( ps_vectorUp, ay, vector );
					add_vectors( vector, sRing_posn, sRing_posn );
					if( sniper ) {
						sniper.position = sRing_posn;
					} else {
						sniper = addVisualEffect( "telescope-ring", sRing_posn);
						// sniper.$TelescopeSniperTarget = curr_target;
ws.$sniper = sniper; // debug
						sniper.scale( 0.01 * SniperRingSize );		//shrink
						if( exact_same_vectors( SniperRingColor, VECTOR_ALL_ZEROS ) ) {		// special directives to match reticle color
							copy_vector( ps.reticleColorTarget, ring_color, true );			// cannot cache reticleColors, as hud could change
						} else if( exact_same_vectors( SniperRingColor, VECTOR_ALL_ONES ) ) {// special directives to match locking reticle color
							copy_vector( ps.reticleColorTargetSensitive, ring_color, true );
						} else {
							copy_vector( SniperRingColor, ring_color, true );
						}	// 3rd parm to copy_vector prevents parm validation
						sniper.setMaterials( { "telescope-ring.png": { emission_color: ring_color, diffuse_color: ring_color } } );
					}
					sniper.orientation = ps_orientation;
					snipertarget = true;
				}
			}
		}
		if( sniper && !snipertarget ) {
			clear_SnipeRing();
		}
	}
	var frame_delta = 0;											// store this frame's delta; see apply_speed_adj
	var scanner_cooldown = 0;										// cool down period between consecutive scans
	function _hud_effects( delta ) {								//Visual FrameCallBack
		try {
			if( !ps_vectorRight || !curr_S.ent ) return;			// in case not set (eg. console load)
			// "If you turn off the weapons with the underscore button ("_") then a scan happens and you enter
			//	into "Navigation Mode", where autolock helps you see through targets (called Panorama targeting):
			//	continually relocks to the most centered target." (from readme)
			frame_delta = delta;
			scanner_cooldown = scanner_cooldown > delta ? scanner_cooldown - delta : 0;
			if( weaponsOnline ) {									//gravity scan if weapons turned off
				if( !weaps ) {										//state changed to on
					if( curr_S.marker_type === 'marker' ) {			// clear far target
						switch_PS_target( null );
					}
					weaps = true;									//save state
					let new_size = VisualTargetCombatSize / 10;
					vsizechanged = vsize !== new_size;
					vsize = new_size;
					_resetIdentDelay();								// reset counter for IdentDelay
					if( gs_state === GS_NONE ) {
						scanner_cooldown = 5;						// (sec.) delay to start next scan
					} else if( gs_state === GS_COMPLETE ) {
						scanner_cooldown = 10;						// (sec.) delay to start next gravity scan
					}
				}
			} else if( weaps ) {									//state changed to off
				weaps = false;										//save state
				let new_size = VisualTargetNormalSize / 10;
				vsizechanged = vsize !== new_size;
				vsize = new_size;
				_resetIdentDelay();									// reset counter for IdentDelay
				var scanning = fns_are_pending();					// true => mapping creation/update is running
				if( !scanning && scanner_cooldown === 0 )
					_auto_updates( gs_state <= GS_STOPPED );		// user starts a 'rescan' unless scan in progress
				report_status = true;
			}
			if( !mappingReady || maplen === 0 ) {					// wait for mapping to be built OR it's empty
				return;
			}
			if( viewDirection !== 'VIEW_FORWARD' ) {				//working in forward view only
				_clear_HUD_Effects();
				return;
			}
			dataKey = curr_target && curr_target.dataKey;
			var valid_target = dataKey && curr_target.isValid;		// check dataKey to exclude wormholes & orbs (have no dataKey)
			if( !valid_target || (vShip && !vShip.isValid) ) {		// no ship to update
				_clear_HUD_Effects();
				return;
			}
			if( curr_target.isStation ) {							// turn off hud effect when docking
				distance = _detect_distanceTo( curr_target );
				if( distance < 500 ) {
					copy_vector( curr_target.position, vector );
					subtract_vectors( vector, ps_position, vector );
					unit_vector( vector, vector );
					let dockDot = dot_product( vector, curr_target.vectorForward );
					let headingDot = dot_product( ps_vectorForward, curr_target.vectorForward );
					if( dockDot < -0.95 && headingDot < -0.95 ) {	// in front of dock, pointing in its direction
						_clear_HUD_Effects();
						return;
					}
				}
			}
			_showVShip( dataKey );
			sniper_ring();
		} catch( err ) {
			log( ws.name, 'vsize = ' + vsize + ', vShipScale = ' + vShipScale
					+ ', dataKey: ' + dataKey + ', valid_target: ' + valid_target + ', curr_target: ' + curr_target
					+ '\n\t vShip.isValid is ' + (vShip === null ? 'not available' : vShip.isValid)
					+ ', vRing.isValid is ' + (vRing === null ? 'not available' : vRing.isValid)
					+ ', sniper.isValid is ' + (sniper === null ? 'not available' : sniper.isValid) );
			log( ws.name, ws._reportError( err, 'hud_effects', delta ) );
			if( debug ) throw err;
		}
	}
	// All functions called from outside closure are wrapped in try..catch blocks.
	// In the key: value pairs below, values that do not start with '_' are
	// entry stubs containing try..catch blocks that call the underscored value, (so they're not nested)
	return {
					 _initOxpVars: _initOxpVars,
				_init_player_vars: init_player_vars,			// entry stub
				   _reload_config: _reload_config,
				   _adjustMLFlags: _adjustMLFlags,
					_getShowState: _getShowState,
				_getShowStateText: _getShowStateText,
					 _currMLFlags: _currMLFlags,
			  _shutdown_Sightings: shutdown_Sightings,			// entry stub
		  _restart_after_shutdown: _restart_after_shutdown,
				  _has_bad_status: has_bad_status,				// entry stub
				  _Sighting_index: Sighting_index,				// entry stub
			   _set_curr_Sighting: set_curr_Sighting,			// entry stub
					_add_Sighting: add_Sighting,				// entry stub
				 _delete_Sighting: delete_Sighting,				// entry stub
			   _chg_curr_Sighting: chg_curr_Sighting,			// entry stub
				_nearest_Sighting: nearest_Sighting,			// entry stub
			  _reposition_effects: _reposition_effects,
				_update_Sightings: update_Sightings,			// entry stub
						 _newList: newList,						// entry stub
					_call_pending: _call_pending,
				_create_Sightings: create_Sightings,			// entry stub
			_update_target_marker: _update_target_marker,
				   _manage_marker: manage_marker,				// entry stub
					_mostCentered: mostCentered,				// entry stub
					_auto_updates: auto_updates,				// entry stub
				 _resetIdentDelay: _resetIdentDelay,
						_steerFCB: _steerFCB,
			   _clear_HUD_Effects: _clear_HUD_Effects,
					   _showVShip: _showVShip,
				  _set_vShip_posn: _set_vShip_posn,
					 _hud_effects: _hud_effects,
			   _relativeDirection: relativeDirection,			// entry stub
				   _report_config: report_config,				// entry stub
				 _report_autovars: _report_autovars,
		// for debugging
				reset_common_vars: reset_common_vars,
					index_in_list: index_in_list,
					  getDetected: getDetected,
					   is_hostile: is_hostile,
				   grav_scan_dist: grav_scan_dist,
				  check_Sightings: check_Sightings,
				 select_Sightings: select_Sightings,
					  add_lt_ball: add_lt_ball,
				   lb_effect_size: lb_effect_size,
				   update_lt_ball: update_lt_ball,
					  add_ml_ring: add_ml_ring,
				   ml_effect_size: ml_effect_size,
				   update_ml_ring: update_ml_ring,
					proc_stealthy: proc_stealthy,
			  update_one_Sighting: update_one_Sighting,
					  update_some: refresh_Sightings,
					classify_ship: classify_ship,
				  is_ignored_ship: is_ignored_ship,
			  process_new_targets: process_new_targets,
				  fns_are_pending: fns_are_pending,
				   set_fn_pending: set_fn_pending,
				clear_all_pending: clear_all_pending,
					 show_pending: show_pending,
					grow_new_list: grow_new_list,
					  notable_ent: notable_ent,
			 check_if_new_targets: check_if_new_targets,
					  update_MFDs: update_MFDs,
					   qualifyMFD: qualifyMFD,
				  set_displayName: set_displayName,
				   showTargetName: showTargetName,
				   showShipReport: showShipReport,
					entityIsNamed: entityIsNamed,
					planetIsNamed: planetIsNamed,
						  sunName: sunName,
						  orbName: orbName,
				 planetNameString: planetNameString,
			 report_scan_progress: report_scan_progress,
/* for profiling (see also debug loading iife
			time_create: time_create,		 //cagiife
			time_update: time_update,		 //cagiife
			time_refresh: time_refresh,		   //cagiife
			profile_create: profile_create,		   //cagiife
			profile_update: profile_update,		   //cagiife
			profile_refresh: profile_refresh,		 //cagiife
			set_profiling:	set_profiling,		  //cagiife
			clear_profiling: clear_profiling,		 //cagiife
*/
	}
// };				// end of closure
}.bind(this); // get [native code] in debugger rather than entire function
}).call(this);	// end of strict function call
// }).call(worldScripts.telescope);
// run  ws._shutdown_Sightings()  BEFORE reloading entire script!
// run  ws.startUp() afterwards
// run  ws.startUpComplete() afterwards
 |