Back to Index Page generated: May 8, 2024, 6:16:03 AM

Expansion Telescope v2

Content

Warnings

  1. http://wiki.alioth.net/index.php/Telescope%20v2 -> 404 Not Found
  2. Low hanging fuit: Information URL exists...
  3. No version in dependency reference to oolite.oxp.Norby.Telescope:null
  4. No version in dependency reference to oolite.oxp.Norby.Telescope_Extender:null
  5. No version in dependency reference to oolite.oxp.Norby.HUDSelector:null
  6. Conflict Expansions mismatch between OXP Manifest and Expansion Manager at character position 0060 (DIGIT ZERO vs LATIN SMALL LETTER N)
  7. Unknown key 'upload_date' at https://wiki.alioth.net/img_auth.php/d/d2/Telescope_2.1.4.oxz!manifest.plist

Manifest

from Expansion Manager's OXP list from Expansion Manifest
Description Extended targeting and scanning features, masslock borders and sniper ring. Extended targeting and scanning features, masslock borders and sniper ring.
Identifier oolite.oxp.Norby.cag.Telescope oolite.oxp.Norby.cag.Telescope
Title Telescope v2 Telescope v2
Category Equipment Equipment
Author Norby, cag Norby, cag
Version 2.1.4 2.1.4
Tags
Required Oolite Version
Maximum Oolite Version
Required Expansions
  • oolite.oxp.cag.station_options:1.1.1
  • oolite.oxp.cag.station_options:1.1.1
  • Optional Expansions
  • oolite.oxp.Norby.cag.Telescope_Extender:2.1
  • oolite.oxp.Norby.CombatMFD:1.9
  • oolite.oxp.Norby.cag.Telescope_Extender:2.1
  • oolite.oxp.Norby.CombatMFD:1.9
  • Conflict Expansions
  • oolite.oxp.Norby.Telescope:0
  • oolite.oxp.Norby.Telescope_Extender:0
  • oolite.oxp.Norby.HUDSelector:0
  • oolite.oxp.Norby.Telescope:
  • oolite.oxp.Norby.Telescope_Extender:
  • oolite.oxp.Norby.HUDSelector:
  • Information URL http://wiki.alioth.net/index.php/Telescope n/a
    Download URL https://wiki.alioth.net/img_auth.php/d/d2/Telescope_2.1.4.oxz https://wiki.alioth.net/img_auth.php/a/a2/Norby.cag.Telescope.oxz
    License CC BY-NC-SA 4.0 CC BY-NC-SA 4.0
    File Size n/a
    Upload date 1701399518

    Documentation

    Telescope readme.txt

    Telescope forum topic: http://bb.aegidian.org/viewtopic.php?f=4&t=14961
    
    Quick start as a Hunter
    =======================
    
    * Copy the .oxp folder to the AddOns directory, hold down the Shift when starting the game the first time after copying.
    * Buy Telescope with Extender equipment or load the included savegame ("Telescope demo.oolite-save"). Injectors, ECM System, Fuel Scoop, Military Laser and Scanner Targeting Enhancement are used also in this guide.
    * After undock (F1) you can see the magnified target in top center position.
    * If you bought Gravity Scanner then stop near the station, turn off your weapons with underscore (_) and wait for the scan results.
    * Turn your ship where you see lollipops at the edge of your IFF scanner. The Panorama targeting will countinually change the box to the most centered target.
    * Choose your enemy for the strength of your ship: start with a small ship flying alone.
    * Use Injectors (i) to fly into scanner range where you can see a Red ball which means visible ship with bounty. Do not hunt Yellow traders and Purple police ships if you want to remain in clean status.
    * Use Ctrl+arrow keys to put the target into the middle of the crosshairs to turn on the sniper ring which magnify the difference from the correct line-up.
    * Turn on your weapons (_) and fire (a). You can try to hit with Military laser over normal scanner range but within 30km and a stopped target will turn when successful, but if your target is too small then fly inside 25.6km where the red target box indicate the correct line-up.
    * Press ident (r) to lock the most centered target if there are more, press again to start auto steering or to lock another if it's more centered than the current target.
    * Telescope can be primed (Shift+N) then activated (n) to lock the nearest target, mode (b) change the function of the activate key.
    * If a missile is coming then a cyan ball warns you: start ECM (e), target it (t), try to avoid (i) or shoot it (a).
    * If you win and the pilot ejected then shot down the derelict ship to get the bounty, then fly after the small White balls and scoop the Escape Pod, metal fragments and cargo pods.
    * Fly back to the station and dock to get the reward for the pilot and sell the cargo (F8). Congratulations, you are richer. :)
    
    
    Telescope Equipment
    ===================
    
    This OXP realizes some requests for an extended scanner described in this topic:
    http://bb.aegidian.org/viewtopic.php?f=6&t=13274
    Can determine the direction, distance, orientation and legal status of all visible ships.
    
    The detection range is equal with the scanner range if you use it without the Telescope Extender (still or willfully), but you can use the following features:
    
    Visual targeting
    ================
    
    The virtual model of the target ship is displayed and a console message shows the name, range and direction.
    The direction is marked by <^ or v> symbols to read faster than the Port Up or Down Starboard words.
    The ranges and directions are calculated continually to help turn in line and see the decreasing range while traveling to the target.
    The orientation of the model is equal with the target if you centered it and you can spin the model if turn around your ship.
    You can set the size of the model with the $VisualTargetNormalSize & $VisualTargetCombatSize variables when docked at the station or in the Scripts/telescope.js.
    
    Lightballs
    ==========
    
    Far targets get coloured lightball markers in the view and lollipops with darker colours near the edge of the scanner.
    
    Red: Hostile ship with bounty (pirates and thargoids),
    Pink: Tharglet until active and Drones with bounty in HardShips OXP,
    Yellow: Neutral ship with clean status (traders and co),
    Purple: Police,
    Cyan: Missile or Mine,
    Green: Buoy or Station,
    Blue: Derelict (Wormhole also from Oolite v1.79),
    White: Cargo, Escape Pod or Sun,
    Gray: last known position of a lost target (moved out of telescope range),
    Lightgray: Planet or Moon (a bit darker than Planet),
    Orange: Gravity Scanner detected ship too far to be visible,
    Brown: Gravity Scanner detected ship under 130t mass (less dangerous, especially with ShipVersion OXP).
    Black: lollipops in red alert over 30km to help focus on near targets.
    
    Beacons are identified in the whole system due to the transmitted radio signals.
    If a ship transmits a beaconcode then it's detected anywhere but coloured as a ship and turns to orange if too far to be visible.
    
    The current target is also marked with a gray "shadow" lollipop at the edge of the IFF scanner to help determine the direction, especially at close range, to avoid zooming the scanner. 
    If the target is closer than 1000m, the lightball will be removed as the ship almost covers the ball and cargo pods are large enough large to detect with your eyes, but the shadow lollipop still remain.
    You can set the minimum distance with the $LightBallMinDist variable when docked at the station or in the file Scripts/telescope.js.
    
    Targets within the range of the Military Laser (30km) are marked with a larger ball and a normal (not darker) coloured lollipop.
    You can use this feature to determine exactly where you can open fire on the target so it's large enough to stand a chance of being hit.
    
    You can disable the lighballs of the ships by setting $LightBalls to false, but non-ships with Blue, Cyan, Gray, Green and White colours will remain to help find these. Auto targeting functions can still lock far targets; there seems to be an empty box but the ships are in there.
    
    Sniper ring
    ===========
    
    If you have almost lined up with your target who is between 10 and 30km then a ring will appear.
    The movement of this ring magnifies the variance to the correct line-up, to help fine tune your aim.
    You can hit if the target box of the Scanner Targeting Enhancement switches to red.
    
    Snipers can use the distances between 25.6 and 30km to fire before the enemy can see the attacker on its scanner.
    This was possible without Telescope, but the size of the ball gives another aid to knowing when the target runs out of range; and the ring can guide your aim without red box, which only works in the normal scanner range.
    
    If you think this is a cheat then fire only when the target can also see you (red box) or set the $SniperRange variable to 25600 which will shorten the appearance of the ring and the largest ball (but there's no way to shorten the range of the Military Laser).
    
    Auto steering
    =============
    
    With the Telescope primed (Shift+N) the activate button ("n") can steer to the nearest lightball-marked target, which is useful in picking up cargo from the near field, rather than zooming the scanner to find them. Now you can just press activate instead of hitting zoom, locate, turn and unzoom.
    
    Auto-steering starts only if your ship flys in a line (not turning) and stops instantly if you touch the controls. It will stop steering before a perfect line-up so as not to aim for you.
    
    In Red Alert the activate button steers to the nearest attacker. You can disable this by turning off the weapons, in which case you can lock and steer to any near target regardless of the alert condition.
    
    Auto scanning
    =============
    
    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.
    The autoscan can be turned off if you set $AutoScan false in the telescope.js file but it's usually worth the cost to get new information sooner.
    
    Far target locking
    ==================
    
    You can lock far targets also which is not in the normal scanner (beacons and planets are always lockable). The distance is shown before the name of the ship with Scanner Targeting Enhancement, due to the fact that the second line shows the range of the virtual marker (which is always near the edge of the IFF Scanner).
    We must use this workaround because the core game does not allow locking onto a target outside of normal scanner range. So the locked marker coinsides with the lock of the target (can only fix this in the core game).
    
    If you cannot see a ship then it may have been destroyed, jumped out or flew farther than the visible range; or it has a Military Scanner Jammer and you do not have a Jammer Filter so you cannot get target lock.
    
    When a target flys too far and is lost, the telescope renames it to 'Lost target' until the next scan.
    
    Masslock borders
    ================
    
    In green alert you will see circles around detected targets where these ships can block your Torus Drive.
    Without a Telescope Extender, these are shown around planets and stations only, but if you buy one, it allows for detection of ships that you can  see with your Mark-1 eyeball (escort ships at 2x, traders near 4x scanner range) and you will then get circles around all the ships before you.
    
    Until you enter these circles you will stay in condition green, but cloaked ships can cause surprises.
    
    The colour of a masslock border is the same as the lightball in the middle (see above).
    
    To get the shortest route, you can safely go forward near the largest circle without touching it or going inside. Imagine this is the border of a sphere so if you fly direct to the center then you will be masslocked sooner than if you flew to a point of the circle. If you steer just a little outside the circle, you will fly forward as near as possible without masslock.
    
    These are also a good indicator of the distance to the ships because the radius of the circles represent exactly one scanner range for the ship at its center.
    Planets have rings that are double its radius as they can masslock you within this range.
    The sun also has a large masslock field but this is not displayed by rings - it's less important and mainly a distraction.
    
    You can also turn on masslock borders in non-green alerts if you turn off your weapons.
    You can turn off all circles completely with the Telescope primed to the Lightballs menu (see below).
    
    Keypress functions
    ==================
    
    The ident button ("r") gets a new feature from Telescope: it can lock the most centered target even if it's not shown on the screen.
    The next press will start auto steering to the target if the most centered target is the same, otherwise it will lock onto the new target.  If the auto steer completes successfully, the lock is automatically released, otherwise a third ident press will unlock it.
    In Red Alert it will not narrow the locking to the attackers; it can lock any target.
    
    (Works only if there was a locked target beforehand, due to the triggering of the shipTargetLost event, so if there's no target it won't get called. Press "r" again or get something into the crosshair or use equipment buttons to lock a target.  Usually only an issue when leaving a station, exitting witchspace or Torus drive.)
    
    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, continually relocks to the most centered target.
    This button is choosed to avoid unwanted fire if you have turrets.
    In Red Alert you can lock any target and see far targets if you turn this mode on.
    
    With the Telescope primed (Shift+N), the mode button ("b") cycles through the functions of the activate ("n") button:
    
     Nearest target
     Rescan
     Step forward in the target list
     Step back in the target list
     Steering: off / nearest target only / both nearest and step in the list
     Lightballs: off / navigation only / ships / masslock borders / large
     Sniper ring km: off / 5-25.6 / 10-25.6 / 15-25.6 / 5-30 / 10-30 / 15-30
     Targets: 20 and limitation in red alert / 50 / 100 / 200
     Visual target: off / weapons off / no ring / no station / no question mark / all
     Visual target size: 1-8
    
    The first 4 functions are commands which happen instantly when activated.
    The "Step forward" and "Step back" will rescan if you step over the end of the target list.
    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.
    
    The last 6 functions are some of customizable properties in the telescope.js file, which you can during flight.
    Your settings are stored into missionVariables and saved when you save your game after docking.
    
    [When the core game provides more equipment buttons, a back button could step back to the previous function (maybe Ctrl+"b").
    With a second equipment button (maybe Ctrl+"n") settings could be separated from the commands.]
    
     Cost: 500.0 Cr.
     Techlevel: 5
    
    
    Telescope Extender
    ==================
    
    You must install "Telescope Extender and Gravity Scanner" OXZ package and buy this equipment separatedly to confirm you want step over the rules of the standard game.
    This will increase the detection range based on the size of the target so you can lock onto it when the core game sets it to isVisible and shows at least a dot in the sky.
    For example you can see:
    -Adder at 32 km,
    -Viper and escort ships around 50 km (2x scanner range),
    -Anaconda, Boa, Cobra MkIII and Python about 100 km (4x scanner range),
    -Rock Hermit in almost 500 km (if not visible from the Main Station then fly around),
    -Coriolis Station at 1000 km (right from the witchpoint).
    
    (new) Once a cargo/escape pod comes within scanner range, its RFID frequency is isolated and stored along with its data, backscatter analysis and distortion characteristics of the local environs. Although originally designed for short-range, ship to ship transfer, this feature allows it to be tracked beyond scanner range.  Due to the vagarious and ever present background radio noise, this added range varies widely from 0 to 16 km.  Once beyond this extended range, the telescope purges all relevant data, as it is quite large and quickly becomes obsolete if not constantly updated.
    
     Cost: 500.0 Cr.
     Techlevel: 5
    
    
    Gravity Scanner Equipment
    =========================
    
    You must install the separated "Telescope Extender and Gravity Scanner" OXZ package to use this and the following uber-ranged equipments.
    
    If the mass of your ship more than 130t (Cobra MkIII and above) and there is a station within 5km, you can then extend the detection range of Telescope using a mass detector, which scales by third power of distance:
    the mass of the target in kg must be larger than d2*d2*d2/100 where d2 = distance*2 in km.  
    (new) And, as gravity waves are not affected by mass, the scanner can detect ships behind planets and moons.
    
    Detection of the player ships and the Hard versions in HardShips OXP:
                     t    km   Hard t  km
    Adder           11    52    23     66
    Moray           40    79    81     100
    Cobra Mk I      47    84    94     106
    Fer-de-Lance    51    86    102    108
    Asp             59    90    118    114
    Cobra Mk III    186   132   371    167
    Boa             192   134   385    169
    Python          222   141   445    177
    Anaconda        430   175   1289   253
    
    Beacons are detected in the whole system and Rock Hermits usually as well (from 900km).
    
    A station needs to be near as the largest parts of the gravity scanner system is fitted into the stations, which broadcast some important data and it needs a large mass (at least 10.000t, the station itself) nearby as a reference to refine the results.
    Mobile bases can scan anywhere.
    
    It takes 4 minutes from undock or hyperjump to reach the maximal detection range when your ship is on the move.
    The detection progresses 4 times faster when your ship is stopped, needing only 60 seconds to finish.
    
    The mass is scaled with the elapsed time: an Anaconda is detected at half time as its half mass (215t) would, which means 140km, and needs more time to detect it from the maximal 175km.
    
    The Gravity scanner works only when you turn off your weapons with underscore ("_") button, otherwise only visible targets are displayed.
    You can define a more comfortable key in the Oolite/oolite.app/Resources/Config/keyconfig.plist file.
    
    The auto-relock feature will contiunually change your target to the most centered one, so the box will jump during your turn; your ship helps browse the many targets. The ident ("r") button can lock the same target only in this mode, it cannot step to the second centered target.
    
    Gravity scan consume 8 energy points (visual scan uses only 2).
    You can hear the sound of the Gravity Scanner at work, which is to remind you of the energy usage.
    
    Orange and Brown lollipops and lightballs mark the detected targets out of the visible range.
    Brown if the ship is under 130t mass which means small ships, usually escorts - avoid Orange ones and target single Browns while your ship is not very strong.
    The mass usually cannot tell if a ship is pirate or not, coloured identification need visual contact.
    
    If you see a lighter ball then there are two or more ship in the same place.
    The smallest orange and brown ball means the target is farther than 150km, these get dark orange and dark brown lollipops.
    
    The Gravity Scanner cannot determine the orientation. If the target is not visible then the view position of the virtual model will be a fixed view from the top.
    
    There are no passive gravity sensors so AutoScan will happen only if a new target arrives into the visible range.
    When you undock or arrive at a new system, the telescope scan is performed automatically but if you want to scan the final frontier then you must turn off your weapons.
    
    Beware: aliens can sometimes detect the signal of a Gravity Scan, so if your ship is not strong then do not use it often and pay for the full repair if it's damaged.
    
     Cost: 10000.0 Cr.
     Techlevel: 5
    
    
    Secondary Gravity Scanner Equipment
    ===================================
    
    Cuts in half the time to get the full detection range and works as a single scanner if the primary one is damaged.
    Can only fit into ships over 400t mass (Anaconda and heavy OXP ships).
    
     Cost: 20000.0 Cr.
     Techlevel: 5
    
    
    Small Dish Equipment
    ====================
    
    Detect ships from one third farther out (for example an Asp at 120km) but can only fit onto ships over 130t mass (from Cobra MkIII) due to the size.
    
    Needs Gravity Scanner, it is a just piece of metal.
    
    Fast CPUs can draw 200 targets without relevant FPS drop, slow systems draw it also but with some drop (tested on Intel Atom netbook). In this case scan for Gravity targets only when needed and turn it off when Telescope range is reached.
    
    This is not a primable equipment (there are no buttons on the pure alloy) and a passive extension so will not increase the energy usage of the scan.
    
    (new) dish will allow detecton of abandoned Rock Hermits.
    
     Cost: 5000.0 Cr.
     Techlevel: 5
    
    
    Large Dish Equipment
    ====================
    
    Detect ships from double the range (an Asp at 180km) but can only fit onto ships over 400t mass (Anaconda and over) due to the size.
    
    Small Dish will not increase this range further so you can refund it when you buy a Large Dish.
    
    Ships over 1000t (Hard Anaconda and big OXP ships) can use the hull mass to refine the gravity signals to reach 4 times range than without Large Dish.
    
    Will only show the nearest 200 targets to save CPU and avoid serious FPS drops in systems with many ships.
    
    (new) dish will allow detecton of abandoned Rock Hermits.
    
     Cost: 10000.0 Cr.
     Techlevel: 5
    
    
    
    Cheap Repairs
    =============
    
    You can buy small fixes for 1/10 the cost of the new equipment instead of the normal 1/2 price but the following drawbacks will be applied:
    
    Telescope: get back the lightballs & masslock rings but will not fix the virtual model display, auto steering and sniper ring, and
               lose ability to adjust those settings when docked at station.
    Gravity Scanner: the cheap spare parts can interfere with the hyperdrive and often cause misjumps.
    Small and Large Dish: usees less durable alloys and can break during hyperjump.
    
    After a cheap fix you can buy the full repair which costs the difference between cheap and normal repair: 2/5 of the full price.
    You can only refund fully repaired equipment.
    
    
    Dependencies:
    =============
    
    Oolite v1.77 or later. No shaders needed.
    
    Instructions:
    =============
    
    Copy the ".oxz" file into ManagedAddOns folder if not obtained via the in-game manager.
    Or, unzip the file, and then move the folder name ending in ".oxp" into the AddOns directory of your Oolite installation.
    Savegame included: put the "Telescope demo.oolite-save" file into the oolite-saves directory to load it.
    
    
    Settings in Scripts/telescope.js: (deprecated)
    =================================
    
    All user variables are now available when docked:  F4 - Telescope Options
    
    The station options take precedence over the following (as it's driven by the missiontext!).  
    Of special note is that most options default to 'off', so as not to overwhelm initiates.  Values in previously saved games will be respected.
    
    Available via primable equipment:
    ---------------------------------
    $LightBalls = false; //turn on or off all lightballs, but marks on the scanner will remain
    
    $ShipLightBalls = false; //turn on or off the lightballs with scanner markers of the ships, but cargo, etc. remain
    Non-ships with Blue, Cyan, Gray, Green and White colours will remain to help find these. Auto targeting functions still can lock far targets, the targeted ship are in an empty box.
    
    $LargeLightBalls = false; //lightballs are increasing depending on the distance or remains small
    
    $MassLockBorders = false; //coloured circles around ships and planets in green alert
    
    $BrightMassLockBorders = false; //brighter coloured circles around ships and planets in green alert
    
    $SniperMinRange = 10000; //meters, show sniper ring if the target is over this distance
    
    $SniperRange = 25600; //meters, if the target is inside then show sniper ring and large lightball, max 30000 (military laser range)
    Set it to to 25600 if you want to shorten the appearance of the sniper ring in the name of the fair play.
    
    $Steering = 0; //auto steering if lock nearest or step in the target list with activate, 0: off, 1: nearest only, 2: each in step
    A setting of 1 or 2 will enable the auto steer function of the ident key.
    
    $MaxTargets = 100; //limitable to reduce FPS drop in systems with many ships, min. 20, max. 200
    
    $Ring = true; //show a ring around the visual target
    
    $ShowVisualStation = true; //show or not show the 3D model of the targeted station
    
    $ShowVisualQuestionMark = false; //if a ship has no virtual model in effecdata.plist show a big "?" model
    
    $ShowVisualTarget = 1; //0 to turn the model off, 1 to show only when weapons are off-line, 2 to always show the model.  
    
    $VisualTargetCombatSize = 4; //size of the visual target with online weapons (between 0 and 10, default: 4)
    If size is 0 then the visual target is not shown at all with weapons online.
    
    $VisualTargetNormalSize = 6; //zoomed size of the visual target with offline weapons (between 0 and 8, default: 6)
    If size is 0 then the visual target is not shown at all with weapons offline.
    
    Available at station only:
    --------------------------
    $AutoScan = true; //check continually for new isVisible isPiloted target and scan if found
    Scanning use a very little energy if a new target arrived but you strongly need this to avoid often scan manually.
    
    $AutoScanMaxRange = 1000000; //meters, how far targets will be reported
    
    $FarStatus = false; //red ball reveal pirates over normal scanner if true
    If false then bounty detected within 25.6km only which is more real and exciting but Thargoids get red ball at any range and a red ship will not change back to yellow if fly over the normal scanner range.
    
    $AutoLock = 1; //if no target and something in crosshairs with max. this degree diff., 1=lightball size, 0=off
    AutoLock is set to 1 degree (size of a lightball) and lock only if no current target to make it similar with the original ident function plus can lock far targets also. Disabled if set to 0 but in this case you can lock targets in 25.6km only.
    
    $GravLock = 20; //gravity scanner relock in this degree cone (0-180, 20=about the screen)
    Panorama targeting offer auto-relock to the most centered target during manual turning and can be switched on or off in-game first with the weapons and second with the ident button when you see gravity targets. Disabled if set it to 0 but it is not recommended due to in this case you can not lock far targets, so set it to 1 degree at least, or simply use the ident button in-game to reduce it to the level of the AutoLock and leave the possibility to turn it on again.
    
    $IdentLock = 180; //if ident pressed or target lost then lock in this degree (0-180, 180=the whole sphere)
    Can lock the most centered target even if behind you if set it to 180 and the second press will unlock it so you can clear the virtual model when not needed. If set it to 1 then the locking radius reduced to the size of a lightball so can do unlock only. Note the AutoLock will relock in 0.25 second if the target centered exactly, in this case turn a bit before unlock.
    Red alert is an exception where the IdentLock target hostiles only (who target you) to help find attackers. Can be override with AutoLock and GravLock, so if you unlock the current target and point exactly to another then the AutoLock will target it regardless of the hostile's status, or if you point it and turn weapons quickly off and on again then lock it regardless from you has lock on another target (but not too quickly, the timed function need max. 0.25 second to lock).
    
    $IdentDelay = 4; //(new) quarter seconds; targeting is suspended following the unlock mentioned above.  I.e. it prevents immediately locking the target just unlocked. This comes down to a pilot's style, whether or not you move the aim point before/after unlocking.
    
    $LightBallMinDist = 1000; //meters, if target is inside then remove the lightball marker
    
    $LightBallShipMinDist = 5000; //meters, if target is ship and inside then remove the lightball marker
    
    $MassLockFwdOnly = false; //(new) limits masslock borders to the forward view
    
    $RedAlertDist = 30000; //show lollipops in red alert within this distance only
    
    $SniperRingSize = 2; //size of the sniper ring (between 1 and 5, default: 2)
    
    $ModelRingColor = [0.33, 0.33, 0.33]; //(new) colour of ring around 3D model, default is a light gray.  [0,0,0] sets it to match the reticle's color, [1,1,1] the reticle's locking color.
    
    $Thargoids = false; //you will get aliens right after undock to test Telescope
    
    $VTarget_HUD_shift = [0, 0, 0]; //position shift for your HUD's built-in visual target screen if any
    
    
    Script_info support:
    ====================
    
    OXP makers can alter the detection of any objects in shipdata.plist to avoid revealing mission secrets.
    
    Stations with non-standard roles and ships with the word "stealth" within their dataKey or role are detected in normal scanner range only. Must specify "telescope" script_info key to detect it farther to stay compatible with the existing OXPs, for example Rescue Stations.oxp, Stealth.oxp and Vector.oxp.
    
    Standard Station roles: "station", "coriolis", "dodo", "dodec", "dodecahedron", "ico", "icosa", "icosahedron" and "rockhermit".
    
    Hiding ships need "stealth" role or set telescope = 0; in script_info.
    
     script_info = {
     	telescope = 0;
     };
    
    * 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).
    * 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.
    
    If this key is not placed at all then get the normal detection.
    Objects with disabled detection (0) are not detected when arrive into visible range (need scanner range), but once detected then tracked over scanner range while in visible range until next scan (small help and save performance).
    
    
    Problems:
    =========
    
    If you do not see the model of a ship (or see a question mark if enabled) then you probably installed a custom ship OXP.
    The core game currently does not support making visual effects from ships directly, but there is a workaround: copy the Config/shipdata.plist files in your OXPs to effectdata.plist or insert into the full contents if exists to avoid overwrite.
    
    If the model still not appear and the definition of the ship using like_ship then copy the model = "filename.dat"; from the original ship into the section of this ship. Original cobras arrived after I do this only.
    
    If a custom ship use shaders with uniforms then you may see errors in the log but the visual target usually appear with less detail (need several core improvements to fix properly). To avoid the errors you can try commenting out the referred lines with // from the effectdata.plist or replace the inputs with fix numbers based on the wiki. For example the Griff Boa ( http://wiki.alioth.net/index.php/Griff_Boa ) is included in the effectdata.plist file of the Telescope OXP.
    
    If you ride a custom ship and the visual target is misaligned (not in the top center position) then copy the view_position values from your shipdata.plist into the $ShipLibViewPosition array in Scripts/shiplib.js .
    From Oolite v1.79 the player.ship.viewPositionForward property solve this.
    
    Alternatively, the oxp ships with a Python script to generate a custom effectdata.plist for the telescope by reading all the shipdata.plist files of installed oxp/oxz files.
    
    ---
    
    Opened wormholes are not auto targetable. You can put a box around with the standard ident but the script receive an empty target only. Can not get a pointer from the list of allShips nor allVisualEffects in v1.77, but from v1.79 the isWormhole flag will solve this.
    The core games requires a manual ident of a wormhole, so it won't be recognized by Telescope until you do that.
    
    ---
    
    If a ship flys over a station then the lightball hides behind the station. To solve this need resizeable flashers which will be available from Oolite v1.79 only.
    
    ---
    
    Right after undock or hyperjump the first ident press can not target the most centered ship, only the second press. The first does not call any event, so you must press "r" again.
    This is only an issue if your AutoLock and GravLock settings preclude an auto lock. For example, launching from a main station with default values will lock on the beaon immediately.  But exiting witchspace with no ships in front of you leaves you with no target, so a 2nd key press is needed (we've hijacked the shipTargetLost event and you have to have one before you can lose it!).
    
    ---
    
    Distant targets writes double message when locked, sometimes the first shows the name of the previous target. This seems to be a problem in the core game (tried to log it but only one log line created). Simply ignore the first message.
    
    
    License:
    ========
    
    This work is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike License version 4.0.
    If you are re-using any piece of this OXP, please let me know by sending an e-mail to norbylite@gmail.com.
    ScanSound source: http://soundbible.com/878-Martian-Scanner.html
    
    Changelog:
    ==========
     2023.12.01. v2.1.4 Added code to make the mode of the primable equipment visible to 3rd parties.
     2023.10.11. v2.1.3 Removed monkey-patches for HUD Selector, as it was conflicting with updated version (phkb).
     2018.04.01. v2.0  BETA: rewritten to optimize for speed and reduce garbage generated, using many of the ideas from
                       http://aegidian.org/bb/viewtopic.php?f=4&t=18837&sid=3f1a12295fd26646177719d96d41b0e9 (OXP Performance tips)
     2017.06.24. v1.15 Optimized for speed by enclosing frame callbacks in closures.
     2015.05.25. v1.13 Asteroids are lockable with ident press not in the crosshairs also.
                       Selectable bright masslock borders.
     2014.10.28. v1.12 Masslock borders with very bright textures for fluxxx.
     2014.10.28. v1.11 Masslock border brightness is adjusted.
                       Default lightball size is smaller, except for large ships over 400t mass.
                       Very far targets over 1000km show a dot only.
                       Show hostile/offender/derelict flag in CombatMFD.
     2014.10.12. v1.10 A fix to prevent sudden target changes reported by Bogatyr.
     2014.10.11. v1.9  Show the list of the nearest 10 targets in a MFD.
                       The newest detected target is displayed in the CombatMFD if installed.
                       A small fix if Visual target:off and size is changed, thanks to Anthony.
     2014.07.06. v1.8  Fixed the restore of lightball settings after load game, thanks to Anthony.
                       Dark far lollipops in yellow alert except if weapons are offline.
                       Virtual target model is within a ring by default with offline weapons also.
                       Range extenders are separated into Telescope Extender and Gravity Scanner OXZ.
     2014.01.28. v1.7  Targets marked with dots from far distances for FarPlanets OXP.
                       Lock the nearest from overlapping targets within 0.5 degree.
                       $TelescopeRedAlertDist change lollipops over 30km to black in red alert.
                       Reduced chanche of timeLimit.
     2014.01.16. v1.6  Fixed reorientation during Asteroid hunting, thanks to Duggan.
                       Improvements for FarPlanets OXP.
                       Sun is added into the target list and got orange lollipop.
                       Planet lollipop color is changed to lightgray, masslockborder also.
                       Targets over $TelescopeSniperRange (30km) get tiny lightballs.
     2014.01.04. v1.5  Planets are targetable within scanner range also.
     2014.01.02. v1.4  Polished masslock borders and planet targets.
     2013.12.29. v1.3  Masslock borders: coloured circles around ships and planets.
                       Planets are included into the target list.
                       In Oolite 1.79 show visual target models of new ship graphics.
                       Thinner ring around visual target, smaller sniper ring.
     2013.11.02. v1.2  Gravity Scanner can fit from 130t mass again but restricted to use near stations.
                       Small Dish introduced, Large Dish need at least an Anaconda.
                       Ship lightballs minimal distance raised to 10km, cargo lightballs stay at 1km.
                       Lightball size of ships under 30t mass reduced to tiny.
                       Fixed infinite lost messages caused by piloted rocks in Lave.oxp.
     2013.10.22. v1.1  Double ident press will start auto steering to the target.
                       Need Telescope Extender to see over scanner range.
                       Gravity Scanner need ship with at least 400t mass.
                       Custom stations are lockable from 4x scanner range.
     2013.08.06. v1.0  Telescope configurable during fly: press mode key to cycle, activate to choose.
                       Settings stored into missionVariables and restored with load game.
                       Gravity Scanner is not primable anymore due to functions merged into Telescope.
     2013.08.03. v0.92 FCB speed almost doubled by Svengali, many thanks to him!
                       Maximal handled Telescope targets increased to 200.
                       False $TelescopeFarStatus to do not reveal pirates over normal scanner range.
                       Gravity scan need some time to calculate.
                       Added Secondary Gravity Scanner Equipment for faster scan in large ships.
                       $TelescopeLargeLightBalls set to false by default for small balls.
                       $TelescopeLightBalls can turn off lightballs but leave lollipops on the scanner.
                       $TelescopeShipLightBalls turn off ship lightballs only and show non-ships only.
     2013.07.23. v0.91 Cheap repairs get nice drawbacks.
                       Gravity scan sometimes detected by aliens.
                       FCB speed improvements to prevent Timelimit in trunk.
                       Fixed rescan bug if target has Military Jammer awarded by ShipVersion OXP.
                       Changed $TelescopeShowVisualTarget to always show the model in weapons off mode.
     2013.07.20. v0.90 Activate steer to the nearest target or to the nearest attacker in Red Alert.
                       Mode lock and steer to the most centered target or attacker in Red Alert.
                       Large lightballs added, show within $TelescopeSniperMinRange (10km).
                       Added telescope script_info key and hide custom Stations, thanks to Svengali.
                       Added $TelescopeThargoids if you want a test in instant action.
                       Fixed $TelescopeRedAlertLimiter, thanks to Solonar.
     2013.07.17. v0.86 Gravity Scanner cost increased, repair discounted.
                       Tharglet get small pink lightball until active.
     2013.07.16. v0.85 Debug version to Duggan and Solonar with many Tharglets and without crash.
     2013.07.15. v0.84 Internal updates to avoid timeLimit.
     2013.07.15. v0.83 Gravity Scanner range reduced and made another performance improvements.
     2013.07.14. v0.82 Added $TelescopeGravLock and $TelescopeIdentLock sensitivity in degree.
                       Added $TelescopeShowVisualStation and $TelescopeShowVisualQuestionMark.
                       Added is_external_dependency = yes; to griff boa, thanks to Svengali.
                       Bugfixes and preformance improvements.
                       Can lock asteroids in crosshairs with ident press.
     2013.07.11. v0.81 Added $TelescopeAutoLock, if 0 then must manually lock targets as before.
     2013.06.17. v0.8  Lightballs and shadow lollipop added.
     2013.06.10. v0.7  Visual targeting and Auto steering added.
     2013.05.05. v0.6  Minor fixes.
     2013.04.08. v0.5  First working version.
     2013.03.31. v0.1  First test files.
    

    effect data/effectdata readme.txt

    In order for telescope's 3D close-up to work, the oxp requires effects data
    on the ship targetted.  Unfortunately, oolite cannot create effects
    from 'shipdata.plist' files, so any ships that are absent from telescope's
    'effectdata.plist' file will not show up.
    
    You have some options if you should encounter this:
       
     * run the included Python script to create a custom 'effectdata.plist' 
       file from all of your installed oxp's,
       
     * just live with it by adjusting its options,
    
     * manually edit telescope's 'effectdata.plist' file and add the contents 
       of the problem ship's 'shipdata.plist' file
       
    [telescope's 'effect data' folder also contains 'effectdata.required.plist',
    data essential to telescope, for those of you who'd like to roll your own.
    Do not edit it - the Python script relies on it! Use a copy.]
    
    Note: if any ships listed in 'effectdata.plist' are not loaded 
          (ie. you're running with a subset of those ships), you will get 
    	  numerous errors in the log file.  These may be ignored as there is
    	  no problem loading effect data for absent ships.  The converse,
    	  lacking effect data for loaded ships, will result in a big '?' 
    	  displayed instead of the 3-D model (if you have the 
    	  ShowVisualQuestionMark option turned on).
    
    	  If these log errors are a problem for you, they can be suppressed
    	  by copying the file
    	    logcontrol.plist
    	  from 'oolite.app/Resources/Config' to your 'AddOns' folder.  
    	  Edit your copy and search for 'shipData.load.error' and change its 
    	  value to 'no'.
    	  This will stop ALL shipdata load errors.
    	  (if you're writing an oxp that deals with shipdata, leave it alone
    	   to catch any of your errors)
    
    
    Python Script
    =============
       
    In telescope's 'effect data' folder is the script 'collect_shipdata.py',
    a Python (2 or 3) script.  It can be run from anywhere unless you have 
    multiple Oolite installations.  In that case, run it from any folder within
    the Oolite installation you're updating.  It will generate a custom
    'effectdata.plist' file for all the oxp's that Oolite will load and update 
    your telescope, be it oxp or oxz.
        
    Windows users: there is an executalbe (.exe) version of this script.
     Due to its size (4.15 MB), it is a separate download found at:
     https://www.dropbox.com/s/i2diyvg240hr5jy/collect_shipdata.exe?dl=0
    
    Be sure to hold down the Shift key the next time you start oolite, so the 
    new effects are loaded.
    
    
    Live With It
    ============
    
    This situation is not expected to be resolved soon if ever.  It would entail 
    major recoding of the core game to be able to create effects from shipdata.
    
    The 3D close-up is but one feature of the telescope oxp and should you 
    encounter ships that don't resolve, you can:
    
     * turn on 'ShowVisualQuestionMark', which will display a big '?' where
       a ship would normally appear, so you're not nose to screen trying to 
       see a ship that's not there
       
     * turn off 'ShowVisualStation' if those are the problem
     
     * turn off the close-up's border with the 'Ring' option, so you're not 
       staring at an empty ring
       
     * turn off this feature altogether with the 'ShowVisualTarget' option
     
    
    Manual Editing
    ==============
    
    This is not the preferred option obviously, but if you must, start with a 
    copy of the current 'effectdata.plist' file, if it's only a few problem ships. 
    
     * add in the contents of the 'shipdata.plist' file(s)
     
      * rename all 'like_ship' keys to 'like_effect'
      
      * delete any 'shaders' keys (they are not used)
      
    As for 'flashers', that's your call. The script output has them removed.
    
    For those of you who'd like to roll your own, data essential to telescope can 
    be found in telescope's 'effect data' folder, in the 'effectdata.required.plist'
    file.  Using a COPY, add the shipdata from you ships, make the above changes, 
    save it as a replacement of telescope's 'effectdata.plist' in its Config folder 
    and you're good to go.
    
    Be sure to hold down the Shift key the next time you start oolite, so the new effects are loaded.
    
    

    Equipment

    Name Visible Cost [deci-credits] Tech-Level
    Telescope yes 5000 5+
    Finish Repair: Telescope yes 2000 5+
    Refund Telescope yes 0 5+
    Repair: Telescope (targeting only) yes 500 5+

    Ships

    Name
    Telescope marker

    Models

    This expansion declares no models. This may be related to warnings.

    Scripts

    Path
    Scripts/largeonly.js
    "use strict";
    this.name        = "largeonly";
    this.author      = "Norby";
    this.copyright   = "2013 Norbert Nagy";
    this.licence     = "CC BY-NC-SA 3.0";
    this.description = "This equipment is usable only for ships over 130t like the Cobra III or Rocket Miner.";
    this.version     = "1.0";
    
    this.allowAwardEquipment = function(eqKey, ship, context)
    {
    //	player.consoleMessage( eqKey+" "+ship+" "+context );//debug
    	if( ship.mass >= 130000 ) return true;
    	else return false;
    }
    
    Scripts/notforsmall.js
    "use strict";
    this.name        = "notforsmall";
    this.author      = "Norby";
    this.copyright   = "2013 Norbert Nagy";
    this.licence     = "CC BY-NC-SA 3.0";
    this.description = "This equipment can not fit into small ships like the Adder or Rocket Fighter.";
    this.version     = "1.0";
    
    this.allowAwardEquipment = function(eqKey, ship, context)
    {
    //bugfix for eqs with conditions scripts: need double replaceShip() to work!
    //player.replaceShip(shipname, pers); player.replaceShip(shipname, pers);
    //call twice! the first always got false for allow without run this script.
    //Proof: if you uncomment the following line, only the 2nd call will write into the log.
    //log("notforsmall", eqKey+" "+ship.name+" "+ship.mass+" "+context );
    
    	if( ship.mass >= 30000 ) return true;
    	else return false;
    }
    
    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
    
    
    
    
    
    
    
    
    
    
    
    
    
    Scripts/telescope_debug.js
    this.name		 = "telescope_debug";
    this.author		 = "cag";
    this.copyright	 = "2017 cag";
    this.licence	 = "CC BY-NC-SA 3.0";
    this.description = "debugging helper fns";
    this.version	 = "1.0";
    
    /* jshint elision:true, shadow:false, esnext: true, curly: false, maxerr: 1000, asi: true, laxbreak: true, undef: true, unused:true
    */
    /* global log, worldScripts, Script, Vector3D, Quaternion
    */
    
    (function(){
    /* validthis: true
    */
    "use strict";
    
    // false prevents output, though functions are still tracked
    // - except with start's header, if it's forced by a child fn's msg or by its stop's msg.
    // So putting text in a stop will force output of start's header when there's no regular
    // output from add, begin or end
    // - useful for exceptional situations, where a fn quits early: put a stop w/ msg before the return
    //	 and output start's header as well; only see the fn output when something interesting happens
    this.$fns_watched = {
    'shipSpawned':				false,
    'shipScoopedOther':			false,
    'update_one_Sighting':		true,
    '_update_some_Sighting':	true,
    '_update_Sightings':		false,
    '_delete_Sighting':			true,
    '_add_Sighting':			true
    };
    
    // worldScripts.telescope.$cag.init();
    
    this.$project_name = 'telescope';
    
    this.$DEBUG_CONSOLE_WIDTH = 200;
    this.$DEBUG_OFF = true;
    //this.$DEBUG_OFF = false;
    
    this._display_map = function _display_map( map ) {
    	var that = _display_map;
    	var _showProps = ( that._showProps = that._showProps || worldScripts.telescope_debug._showProps );
    	
    	if( map.hasOwnProperty( 'map' ) ) {
    		map = map.map;
    	}
    	var tmp = _showProps( map, 'map', false, true, true );
    	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
    	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
    
    	tmp += '\ndynamic flags';
    	var flags = map.dynamicMFD;
    	if( flags ) {
    		if( flags & MFD_FRIENDLY ) tmp += ':  FRIENDLY';
    		if( flags & MFD_UNSOCIABLE ) tmp += ':	UNSOCIABLE';
    		if( flags & MFD_ACTIVE ) tmp += ':	ACTIVE';
    		if( flags & MFD_HOSTILE ) tmp += ':	 HOSTILE';
    		if( flags & MFD_NEARBY ) tmp += ':	NEARBY';
    		if( flags & MFD_FARAWAY ) tmp += ':	 FARAWAY';
    		if( flags & MFD_PROTECTED ) tmp += ':  PROTECTED';
    	}
    	tmp += '\nstatic flags';
    	flags = map.staticMFD;
    	if( flags ) {
    		if( flags & MFD_SALVAGE ) tmp += ':	 SALVAGE';
    		if( flags & MFD_MINING ) tmp += ':	MINING';
    		if( flags & MFD_WEAPONS ) tmp += ':	 WEAPONS';
    		if( flags & MFD_TRADERS ) tmp += ':	 TRADERS';
    		if( flags & MFD_POLICE ) tmp += ':	POLICE';
    		if( flags & MFD_PIRATES ) tmp += ':	 PIRATES';
    		if( flags & MFD_MILITARY ) tmp += ':  MILITARY';
    		if( flags & MFD_ALIENS ) tmp += ':	ALIENS';
    		if( flags & MFD_STATION ) tmp += ':	 STATION';
    		if( flags & MFD_NAVIGATION ) tmp += ':	NAVIGATION';
    		if( flags & MFD_CELESTIAL ) tmp += ':  CELESTIAL';
    	}
    	log( tmp );
    }
    
    this._dump_map = function _dump_map() {		// dump Sightings list for telescope
    	var that = _dump_map;
    	var cd = ( that.cd = that.cd || worldScripts.telescope_debug );
    	var ws = ( that.ws = that.ws || worldScripts.telescope );
    	var curr_S = ( that.curr_S = that.curr_S || ws.$curr_Sighting );
    	
    	var pst = curr_S.ent || null;
    	var mapping = ws.$SightingsMap, maplen = mapping.length;
    	var map, ent, target, thargoids = '';
    	var out = '____ent_dist.rank   index	 rel. dir.n		 person	   [entity description minus position, scanClass & status labels ...]	 [ve_colour,  lb_size,	ml_size]\n';
    	// var out = '____ent_dist.rank   index	 rel. dir.n	   curr.grav.D	 person	   [entity description minus position, scanClass & status labels ...]	 [ve_colour,  lb_size,	ml_size]\n';
    	var i, padding = '________________';
    	var	 ent_dist, rank, index, rel_dir, gs_curr, entID, descrn, gs_maxed;
    	for( i = 0; i < maplen; i++ ) {
    		map = mapping[i]; ent = map.ent;
    		gs_curr = map.gs_curr_dist;
    		gs_maxed = gs_curr > 0 && gs_curr === map.gs_max_dist;
    		target = pst && pst === ent;
    		if(ent.isThargoid) { out += ent+'\n';
    			thargoids += '.	   dataKey = '+map.dataKey+', last_posn = '+map.last_posn+' roles = '+map.roles+'\n';
    		}
    		ent_dist = map.ent_dist;
    		if( ent_dist >= 1e15 )
    			ent_dist = cd._number_str( ent_dist / 1e15 ) + ' e15';
    		else if( ent_dist >= 1e12 )
    			ent_dist = cd._number_str( ent_dist / 1e12, 1 ) + ' e12';
    		else if( ent_dist >= 1e9 )
    			ent_dist = cd._number_str( ent_dist / 1e9, 2 ) + ' e9';
    		else if( ent_dist >= 1e6 )
    			ent_dist = cd._number_str( ent_dist / 1e6, 3 ) + ' e6';
    		else
    			ent_dist = cd._number_str( ent_dist );
    		ent_dist = padding.slice( ent_dist.length - 11 ) + ent_dist; // max column width is 10 ('123.456 e6') + 5 ('_ISR_')
    		if( target ) {
    			// ent_dist = ent_dist.replace(/^_*/, '==>');
    			ent_dist = ent_dist[1] == '_' ? '=>' + ent_dist.substring(2) :
    					   ent_dist[0] == '_' ? '|>' + ent_dist.substring(1) :
    					   '>' + ent_dist;
    		}
    		rank = map.rank.toUpperCase();
    		index = i < 10 ? '__' + i : i > 99 ? i : '_' + i;
    		rel_dir = ws._relativeDirection( ent.position, map );		
    		rel_dir = rel_dir ? padding.slice( rel_dir.length-9 ) + rel_dir : '???';
    		// gs_curr = cd._number_str( gs_curr );							
    		// gs_curr = padding.slice( gs_curr.length-(gs_maxed ? 10 : 11) ) + gs_curr;
    		entID = ent.entityPersonality;
    		entID = entID === undefined ? 'xxxxx' : entID.toString();	
    		entID = padding.slice( entID.length-6 ) + entID;
    		descrn = ent.toString();
    		descrn = descrn.replace(/position\: /, '' );
    		descrn = descrn.replace(/scanClass\: /, '__' );
    		descrn = descrn.replace(/CLASS_/, '' );
    		descrn = descrn.replace(/status\: /, '__' );
    		descrn = descrn.replace(/STATUS_/, '' );
    		out += ent_dist+'_'+rank
    		out += map.swapable ? '%' : '_';
    		out += '	['+index+']'+(target ? '|>' : '  ')+'['+rel_dir+']'
    			+(target ? '|>' : '  ')
    			// +'['+gs_curr+(gs_maxed ? ' M' : ']')
    			+' #'+entID+'  '
    			+descrn+'	['+map.ve_colour+', '+map.lb_size+', '+map.ml_size+']'+(target ? '<==' : '')
    //			  +' Ms: '+ map.staticMFD.toString(2)+' Md: '+map.dynamicMFD.toString(2)
    			+' stat: '+ cd._number_str( map.staticMFD, 0, 2 ) + ' dyn: '+ cd._number_str( map.dynamicMFD, 0, 2)
    			+ (map.have_scanned ? ' h_s: ' + map.have_scanned :'')
    			+'\n';
    	}
    	out += 'ws.$SightingsMap.length = ' + mapping.length;
    	log(out);
    	if( thargoids ) log( 'Thargoids:\n' + thargoids );
    }
    
    this._rel2PS = function rel2PS( name, position, distance ) {
    	var cd = worldScripts.telescope_debug;
    	var ps = player && player.ship;
    	var ps_position = ps.position;
    	let posn =	position.constructor === Vector3D ? position :
    				Array.isArray(position) ? new Vector3D( position ) :
    				position.position || null;
    	if( !posn ) {
    		return 'missing position for ' + name;
    	}
    	let dist = distance || posn.distanceTo( ps_position );
    	let padding = '________________';
    	let pname = padding.slice( name.length - 16 ) + name;
    	if( name === 'lightball' ) pname = '__' + pname;
    	let vector = posn.subtract( ps_position );
    	let fwd, right, up, rpt = '';
    	fwd =	( ps.vectorForward.angleTo( vector ) *180/3.1415927 ).toFixed();
    	right =	( ps.vectorRight.angleTo( vector ) *180/3.1415927 ).toFixed();
    	up =	( ps.vectorUp.angleTo( vector ) *180/3.1415927 ).toFixed();
    	rpt += pname + ' is ' + ( fwd <= 90 ? 'forward ' + fwd : 'astern ' + (fwd - 90) ) + '°, '
    							+ ( right <= 90 ? '	 starboard ' + right : '		port ' + (right - 90)) + '°, '
    							+ ( up <= 90 ? '	above '+ up : '	   below ' + (up - 90)) 
    							+ '° at ' + cd._number_str( dist ) + ' m';
    	// vector = vector.direction();
    	// fwd =	ps.vectorForward.dot( vector ).toFixed(5);
    	// right =	ps.vectorRight.dot( vector ).toFixed(5);
    	// up =	ps.vectorUp.dot( vector ).toFixed(5);
    	// rpt += '\n\t' + pname + ' is ' + ( fwd >= 0 ? 'forward ' : 'astern ' ) + ( Math.acos(fwd) *180/3.1415927 ).toFixed() + '°, '
    							// + ( right >= 0 ? '  starboard ' : '		  port ') + ( Math.acos(right) *180/3.1415927 ).toFixed() + '°, '
    							// + ( up >= 0 ? '	  above ' : '	 below ') +	 ( Math.acos(up) *180/3.1415927 ).toFixed()
    							// + '° at ' + cd._number_str( dist ) + ' m';
    	return rpt;
    }
    
    this._curr_S_report = function _curr_S_report() {
    	var cd = worldScripts.telescope_debug;
    	var cs = worldScripts.telescope.$curr_Sighting;
    	var ps = player && player.ship;
    	var scannerRange = ps.scannerRange;
    
    	let map = cs.map;
    	if( !map ) {
    		log('telescope', '_curr_S_report, $curr_Sighting.map is ' + map );
    		return;
    	}
    	let ent = cs.ent;
    	let map_ent_dist = cs.map.ent_dist;
    	let marker = cs.marker;
    	let lightball = cs.map.lightball;
    	let tmp = 'ps.speed = ' + ps.speed + ',	   viewDirection = ' + ps.viewDirection + ',  for ' + ent;
    	tmp += '\n ' + cd._rel2PS( 'ent', ent, map.ent_dist );
    	if( marker )
    		tmp += '\n ' + cd._rel2PS( 'marker', marker	 );
    	if( lightball )
    		tmp += '\n ' + cd._rel2PS( 'lightball', lightball );
    //	let ent_posn = map.rank === 'ukn'  ? map.last_posn : ent.position;
    
    	let marker_dist = scannerRange - 499.6;
    	if( map_ent_dist < scannerRange ) {
    		if( map_ent_dist < marker_dist )
    			marker_dist = map_ent_dist;
    	}
    //	let targ_dir = ent_posn.subtract( ps_position ).direction();
    //	let targ_posn = ps_position.add( targ_dir.multiply( marker_dist ) );//.add( speed_adj );
    //	tmp += '\n ' + cd._rel2PS( 'new marker', targ_posn, marker_dist );
    
    	log('telescope', tmp );
    }
    
    // debugger ///////////////////////////////////////////////////////////////////////////////////////
    
    /*	(function () {
    WS.telescope.$cag = worldScripts.telescope_debug._debug_msgs();
    WS.telescope.$cag.init();
    })()	// */
    
    this._debug_msgs = function _debug_msgs() {
    	function break_line( str, limit ) {			// break str into lines, avoiding splitting on an '=' if poss.
    		function trim( x ) {											// return index of 1st non-whitespace char.
    			var space;
    			var index = x;											// never modify arguments
    			do {
    				space = str[ index ];
    				if( space !== ' ' && space !== '\t' ) break;
    				index++;
    			} while( index < str.length );
    			return index;
    		}
    		function output( x ) {										// push indices for next line; return true if finished w/ str
    			var index = x;											// never modify arguments
    			strptr = trim( strptr );
    			lines.push( [ strptr, index ] );
    			index = trim( index );
    			if( index + limit >= str.length ) {						// just tail end remains
    				lines.push( [ index, -1 ] );
    				return true;										// finished w/ str
    			}
    			strptr = index;		i = index + limit;		first = -1;		newline = -1;
    			return false;											// still more to output
    		}
    		function _seek( dir ) {										// walk str in direction dir across whitespace; return presence of an '='
    			var equals = false;
    			do {													// scan in direction dir for end of whitespace or an '='
    				if( i + dir < 0 || i + dir >= str.length ) break;	// protect limits
    				i += dir;
    				letter = str[ i ];
    				if( letter === '=' ) equals = true;
    			} while( letter === ' ' || letter === '\t' || letter === '=' );
    			i -= dir;
    			return equals;											// return presence of an '='
    		}
    		var first = -1;		var letter, i;		var newline = -1;
    		var lines = [];		var strptr = 0;
    		for( i = strptr + limit; i > strptr; i-- ) {
    			if( newline === -1 ) {
    				newline = str.indexOf( '\n', strptr );
    				if( newline >= strptr && newline <= i )				// respect imbedded newlines
    					if( output( newline + 1 ) ) break;
    			} else
    				newline = 0;										// only check once per output()
    			letter = str[ i ];
    			if( letter === ' ' || letter === '\t' ) {
    				if( _seek( 1 ) || _seek( -1 ) ) {					// look fwd & back for '='s; found a bad break, ie. has '='
    					if( first < 0 ) first = i;						// save the first, just in case
    				} else {
    					if( output( i ) ) break;						// i pts to 1st letter in break
    					continue;
    				}
    			}
    			if( i - strptr <= 20 ) {								// no good break bound; reasonable lower limit on line length
    				if( first > -1 ) {									// use previously found (bad) break
    					if( output( first ) ) break;
    				} else {											// no break found, arbitrary cut using limit
    					if( output( strptr + limit ) ) break;
    				}
    			}
    		}
    		return lines;
    	}
    
    	function insert( str, frame ) {				// insert str into frame's msg buffer
    		var msg_len = frame.msg.length;
    		var pad_len = frame.pad.length;
    		var str_len = str.length;
    		if( msg_len === 0 ) {										// pad the 1st line
    			frame.len = pad_len;									// will be added on output but need to account for its length now
    		}
    		if( str_len + frame.len	 > DEBUG_CONSOLE_WIDTH ) {			// begin a new line
    			var lines = break_line( str, DEBUG_CONSOLE_WIDTH - pad_len );
    			var start, end, i;
    			for( i = 0; i < lines.length; i ++ ) {
    				[start, end] = lines[ i ];
    				frame.msg += str.substring( start, ( end === -1 ? str_len : end ) ) + '\n';
    			}
    		} else {
    			frame.msg += ( msg_len > 0 ? '	' : '' ) + str;
    		}
    		var nl_index = frame.msg.lastIndexOf( '\n', msg_len );
    		if( nl_index > -1 ) {										// calc length of last line in msg
    			frame.len = msg_len - nl_index;							// '\n' has length of 1
    		} else {
    			frame.len = msg_len;
    		}
    	}
    
    	function is_watched_fn( dbgfn, str ) {		// check user supplied $fns_watched to determine if msg should proceed
    		var caller = frame.caller;
    		if( fns_watched.length === 0 ) return true;					// fn limiting not used
    		if( frame === null ) {
    			log( project_name, 'ERR: _debug, null frame: debug cmd outside start...stop sequence.  is_watched_fn:  '+dbgfn+':  str = '+str+'\n############');
    			return true; // allow in hopes of locating errant cmd
    		}
    		if( !fns_watched.hasOwnProperty( caller ) ) return false;	// fn limiting is in play but fn missing -> deny by default
    		if( fns_watched[ caller ] !== true )						// its in use, fn found but turned off
    			return false;
    		return true;												// fn in list & set true!
    	}
    
    	function trim_space( str ) {				// trim white space at start & end of line; -can't use .trim() as it removes line breaks!
    		return str.replace(/^[ \t\u1680\u180e\u2000-\u200a\u202f\u205f\u3000\ufeff]+|[ \t\u1680\u180e\u2000-\u200a\u202f\u205f\u3000\ufeff]+$/g, '');
    	}
    
    	function change_breaks( line ) {			// collapse & convert all line breaks
    		return line.replace(/[\n\r\f\v\u00a0\u2028\u2029]+/g, '<BR>');
    	}
    
    	function output() {							// called by stop to output or start to flush pre-existing msg
    		if( DEBUG_OFF ) return;
    		var incl_hd = frame.incl_hd;
    		var msg = frame.msg;
    		if( !incl_hd && !msg )										// unless directed to output hd, only log if there's a msg
    			return;													// excl. header when no msg
    		if( incl_hd && frame.out_len > 0 )							// a force_hd not necess, as frame already has output
    			return;
    		var head = frame.head;
    		var out = curr_pad + '	' + frame.caller + ':: ';			// 1st line of output always starts w/ the caller (.len incr'd in .add)
    		if( head ) out += head;										// .head contains a newline char
    		out += msg;
    		out = trim_space( out );
    		out = change_breaks( out );
    		out = out.replace( /(<BR>)+$/g, '' );						// trim any trailing line break
    		out = out.replace( /(<BR>)+/g, '\n' + curr_pad + '	' );	// add padding to start of each line
    		if( out.slice( - curr_pad.length ) === curr_pad )			// out ends w/ just a pad
    			out = out.slice( 0, - curr_pad.length );
    		if( frame.logging ) log( project_name, out );				// writes to console and log file
    		else				log( out );								// writes only to console
    		frame.out_len += out.length;
    		frame.head = '';	msg = '';	frame.len = 0;
    		frame.incl_hd = false;										// reset for next time
    	}
    
    	function add_msg( via, str, format ) {		// insert format str into msg buffer
    		if( !is_watched_fn( via, str ) ) return;
    		if( !str ) return;
    		if( stack.length === 0 )
    			log( project_name, 'ERR: debug, no debug frame: check for premature ".stop()" or add a ".start()" statement. .' + via + ':	str = ' + str +'\n############');
    		insert( format, frame );
    	}
    
    	// add( str ) - append str to output msg
    	function add( str ) {						// user fn to append str to msg
    		if( DEBUG_OFF ) return;
    		add_msg( 'add', str, str );
    	}
    
    	// begin( str ) - output str at start of a new line (prepends a newline)
    	function begin( str ) {						// user fn to begin a new line w/ str
    		if( DEBUG_OFF ) return;
    		add_msg( 'begin', str, (frame.len > 0 ? '\n' + str : str) );
    	}
    
    	// end( str ) - output str at end of curr. line (appends a newline)
    	function end( str ) {						// user fn to end the current line w/ str
    		if( DEBUG_OFF ) return;
    		add_msg( 'end', str, str +'\n' );
    	}
    
    	// log_file( str, [con_log], [caller] )	 - stand-alone user fn to add output to log file
    	//										 - it ignores $DEBUG_OFF flag but terminates any further action if true
    	//										 - con_log flag can be used to output to console as well, in curr. frame
    	//										 - caller may be from any previous frame; if not found & con_log is true, output added to current frame
    	function log_file( str, con_log, caller ) {	// output data to log file independent of the current frame
    		log( project_name, caller + ', ' + str );					// writes to log file
    		if( DEBUG_OFF ) return;										// debugger if off, so just log msg
    		if( stack.length === 0 )	return;							// no active frame
    		if( con_log === undefined ) return;
    		if( caller === undefined ) {								// add str to curr. frame
    			add( str );
    			return;
    		}
    		var prev;
    		var len = stack.length;
    		for( var i = 0; i < len; i ++ ) {
    			prev = stack[ i ].caller;
    			if( prev === caller ) {
    				insert( str, prev )									// add str to caller's frame
    				return;
    			}
    		}
    		add( str );													// failed to locate caller, add to curr. frame
    	}
    
    	function Frame( c ) {	this.caller = c;		this.head = '';			this.msg = '';
    							this.pad = '';			this.out_len = 0;		this.len = 0;
    							this.incl_hd = false;	this.force_hd = 1;		this.logging = false;		}
    	var used_frames = [];
    	var used_frame_num = 0;
    	function free_frame( frame ) {
    //log('telescope', 'free_frame, for caller = ' + frame.caller + ', pool totals ' + (used_frame_num + 1)+ ', on stack: ' + (stack.length - 1) );
    		frame.caller = null;	frame.head = '';		frame.msg = '';
    		frame.pad = '';			frame.out_len = 0;		frame.len = 0;
    		frame.incl_hd = false;	frame.force_hd = 1;		frame.logging = false;
    		used_frames.push( frame );
    		used_frame_num++;
    	}
    	function alloc_frame( caller ) {
    		var frame;
    		if( used_frame_num > 0 ) {
    			frame = used_frames.pop();
    			used_frame_num--
    			frame.caller = caller;
    //log('telescope', 'alloc_frame for caller = ' + caller + ', allocating recycled frame, pool totals ' + used_frame_num + ', on stack: ' + (stack.length + 1) );
    		} else {
    			frame = new Frame( caller );
    log('telescope', 'alloc_frame, for caller = ' + caller + ', allocating NEW frame, pool totals ' + used_frame_num + ', on stack: ' + (stack.length + 1) );
    		}
    		stack.push( frame );
    		return frame;
    	}
    
    	// start( caller, [hd], [force_hd], [logging] )	 - open new debug level; caller is function's name (output toggled in $fns_watched)
    	//												 - hd is str (heading) that is output only if more str's comes in
    	//												 - force_hd, an int, over-rules this (default = 1; see Frame object).
    	//												   this give some context when a normally silent fn outputs a msg,
    	//												   by showing who called it. The higher the number, the farther up the frame stack
    	//												   headers are forced to output
    	//												 - logging is a boolean to also write to log file (vs. debug console only)
    	function start( caller, hd,					// user fn to init a msg frame, start's hd string is not counted in len, so if
    					force_hd, logging ) {		// no fn msg's between start & stop, no output
    		if( DEBUG_OFF ) return;
    		var len = stack.length;
    		if( len > 0 ) {												// not the root frame
    			output();												// flush any pending output
    			var fn_name = caller !== undefined ? caller
    											   : 'Anonomous' + stack.length;
    			var prev;
    			for( var i = len -1; i >= 0; i-- ) {					// check not already on stack (fix for GDERM bug)
    				prev = stack[ i ].caller;
    				if( prev === fn_name ) {
    					free_frame( stack.splice( i, 1 )[0] );
    					curr_pad = curr_pad.slice( 0, -PAD_LEN );		// just remove that one entry, as we know we're not recursing
    				}
    			}
    		}
    		frame = alloc_frame( caller );
    		var tmp_pad;
    		if( stack.length % 2 === 0 ) tmp_pad = '=========';			// alternate padding char's
    		else						 tmp_pad = '+++++++++';
    		frame.pad = tmp_pad.substr( 0, PAD_LEN );
    		curr_pad += frame.pad;
    		if( caller !== undefined ) {
    			frame.caller = caller;
    			frame.len += frame.caller.length;
    		} else {
    			log( project_name, 'ERR: _debug, invalid caller: function name needed for output to be formatted correctly. .start: str = ' + hd +'\n############');
    		}
    		if( frame === null ) {
    			log( project_name, 'ERR: _debug, internal error: unable to create frame; stack len = ' + len + ', .start:  caller: '+caller+'(),  str = ' + hd +'\n############');
    		}
    		if( logging !== undefined )	 frame.logging = logging;
    		if( force_hd !== undefined ) frame.force_hd = force_hd;
    		if( hd !== undefined ) {
    			tmp_pad = curr_pad;										// save current padding str and clear curr_pad
    			curr_pad = '';											//	 so we don't put any padding on the header
    			insert( hd + '\n', frame );								// a start header always ends its output line
    			curr_pad = tmp_pad;										// restore current padding
    		}
    		frame.head = trim_space( frame.msg );						// move to head
    		frame.msg = '';
    		frame.len = 0;												// reset len as header doesn't count
    	}
    
    	// _stop( caller, [str], [nolog] ) - close curr. level or regress to it if events interruped sequence; nolog suppresses log file output,
    	//									 over-ruling logging flag in start
    	function stop( caller, str, nolog ) {		// user fn to terminate a frame, ouputting any pending messages
    		if( DEBUG_OFF ) return;
    var have_str = str && str.length > 0; // debug
    		if( caller === undefined )
    			log( project_name, 'ERR: _debug, missing caller: function name needed for output to be formatted correctly. .stop:	str = ' + str +'\n############');
    		if( frame === null )
    			log( project_name, 'ERR: _debug, no debug frame: add a "start()" statement. .stop:	caller: '+caller+'(), str = ' + str +'\n############');
    		if( str && caller === frame.caller ) {
    			insert( '\n' + str, frame );
    have_str = false;
    		}
    		if( nolog !== undefined ) frame.logging = !nolog;			// nolog over-rules frame.logging
    		if( stack.length > 0 ) {
    			var msg_len = frame.msg.length;
    			output();												// flush any pending output
    			if( stack.length > 1 ) {								// not the root frame
    				var force_hd = ( msg_len > 0 ? frame.force_hd : 0 );
    				if( force_hd > 0 ) {								// set higher frame(s) to output
    					var i = stack.length;
    					while( i-- && force_hd > 0 ) {
    						stack[ i ].incl_hd = true;
    						force_hd--;
    					}
    				}
    			}
    			free_frame( frame );
    			frame = stack.pop();
    			if( str && caller === frame.caller ) {				// for when a .stop is skipped
    				log( project_name, 'ERR: _debug, missed .stop(): ' + frame.caller + '() stopped while ' + caller + '() still active. .stop:	 str = ' + str +'\n############');
    				insert( '\n' + str, frame );					// still output msg
    have_str = false;
    			}
    			curr_pad = curr_pad.slice( 0, -PAD_LEN );
    		} else {
    			frame = alloc_frame( caller );							// re-init safely after having lost track!
    		}
    if( have_str ) log('telescope', 'debug.stop, failed to output: ' + str );
    	}
    
    	function reset() {							// reset msg stack; meant for debug console but is also a user fn
    		var len;
    		len = stack.length;
    		while( len-- ) {
    			free_frame( stack[ len ] );
    		}
    		stack.length = 0;						// this will clear all ref's to frame objects
    		frame = null;
    		PAD_LEN = 3;
    		curr_pad = '';
    	}
    	function init( project ) {					// import local var's at runtime by calling this fn
    		var wc = worldScripts.telescope_debug;
    		if( project ) {
    			project_name = wc.$project_name = project;
    		} else {
    			project_name = wc.$project_name;
    		}
    		fns_watched = wc.$fns_watched;
    		DEBUG_CONSOLE_WIDTH = wc.$DEBUG_CONSOLE_WIDTH;
    		DEBUG_OFF = wc.$DEBUG_OFF;
    	}
    
    	var frame = null;
    	var stack = [];		// stack built of Frame's when debugging fns are nested
    	var PAD_LEN = 3;	// max 9 unless you enlarge 2 padding str's in start
    	var curr_pad = '';
    	var project_name, fns_watched, DEBUG_CONSOLE_WIDTH, DEBUG_OFF;
    
    	return {	 init:	init,
    				  add:	add,
    				begin:	begin,
    				  end:	end,
    			 log_file:	log_file,
    				reset:	reset,
    				start:	start,
    				 stop:	stop
    	};
    }
    
    // debugger ends //////////////////////////////////////////////////////////////////////////////////
    
    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 = 80;	// cut-off for long strings
    	var IPAD = ' ';			// inside padding, eg. after array open bracket, before close bracket
    /*	
    	err		(required) value passed in the catch statement
    	
    	func	(required) function that caught the error
    			- can also be a string with the function's name if the
    			  function is not named
    			  eg. this.startUp = function() { ...			-> pass 'startUp'
    			      this.startUp = function startUp() { ...	-> pass startUp
    			- using a named function has the advantage of reporting 
    			  any function properties  
    				
    	parms	(optional) parameters passed to func
    			- if there are multiple parms, pass them in an array
    			- can also include function variables if you want more 
    			  information dumped
    			  eg. this.shipSpawned = function shipSpawned( ship ) { 
    					var dist = player.ship.position.distanceTo( ship ); ...
    			      -> pass as parms: [ship, dist] to have both displayed
    				  
    	depth	(optional) # levels to expose contents of parameters/properties
    			- default is 1, use 0 to suppress overly long output
    			  eg. if a parm is an array of 3 items, 0 will print "<array of 3>",
    				  1 will print "[ item1, item2, item3 ]"
    				  - if item3 is an object: "[ item1, item2, <object of ..> ]"
    				  2 will print the contents of each item, in this case, 
    				    expanding the object
    				  3 will expand the object's properties, etc
    				  (ditto for nested arrays)
    			- NB: depth > 2 will likely produce large output, eg. ships
    			- will accept a 2 item array if you want different depths for 
    			  parameters vs. properties, ie. [ parm depth, prop depth ]
    			  eg. [ 2, 1 ]
    			
    	goDeep	(optional) boolean indicating whether or not to test hasOwnProperty
    			when exposing properties
    			- default is false meaning inherited properties won't be shown
    			- eg. if you dump player.ship, false shows 63 properties, while true 
    				  shows 226! (player properties + ship properties + entity properties)
    			- inherited properties are prefixed by a caret '^'
    				
    	example usage:
    	=============
    	
    	this.relativeDirection = function relativeDirection( position, map ) {
    		var that = relativeDirection;
    		var ws = ( that.ws = that.ws || worldScripts.telescope );
    		var ps = player && player.ship;
    		
    		try {
    			var dist = map.ent.positon.distanceTo( ps );			<== position mis-spelled
    			...
    			return relative_dirn;
    		} catch( err ) {
    			log( 'telescope', ws._reportError( err, relativeDirection, [position, map], 1 ) );
    			if( debug ) 
    				throw err;									
    		}
    	}
    
    	Latest.log output:
    	=================
    		
    	18:48:27.293 [LogEvents]: Asteroid 28897 spawned at 413 km
    	18:48:27.898 [telescope]:
    	function relativeDirection() 	 caught: 	TypeError: map.ent.positon is undefined
    		parameters: [ (70839.2, -96302.7, 664477), <object of 6> ]
    		properties: { ws: <object of 169> }
    		file: ../AddOns/Norby.cag.Telescope.oxp/Scripts/telescope.js
    			line: 6204,	relativeDirection( [object Vector3D], [object Object] )
    		file: ../AddOns/Norby.cag.Telescope.oxp/Scripts/telescope_debug.js
    			line:   47,	_display_map( [object Object] )
    	18:48:27.899 [script.javaScript.exception.unexpectedType]: ***** JavaScript exception (oolite-debug-console 1.89): TypeError: map.ent.positon is undefined
    	18:48:27.899 [script.javaScript.exception.unexpectedType]:       ../AddOns/Basic-debug.oxp/Scripts/oolite-debug-console.js, line 1117.
    */
    		
    	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 i = 0, len = stk.length; i < len; i ++ ) {
    			// stack line format: fn(parms)@../AddOns/.../script.js:123
    			parsed = stk[ i ].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;
    }
    	
    this._number_str = function _number_str( n, fixed, base ) {
    	var that = _number_str;
    	var round = ( that.round = that.round || Math.round );
    	var working = ( that.working = that.working || [] );
    
    	var comma, index, wk, len, str;
    	str = typeof n === 'string' ? parseFloat( n ) : n;
    	if( !isFinite( str ) ) return n.toString();
    	str = fixed > 0 ? str.toFixed( fixed ).toString( base || 10 )
    					: round( str ).toString( base || 10 );
    	len = str.length;
    	working.length = wk = 0;
    	for( index = 0; index < len; index++ ) working[ index ] = str[ index ];
    	index = str.indexOf( '.' );
    	comma = index >=0 ? (index > 0 ? index - 1 : 0) : len - 1;
    	for( ; comma > 0 ; comma -= 3 ) {
    		index = comma - 2;
    		if( (n < 0 ? index-1 : index) <= 0 ) break;
    		for( wk = len - 1; wk >= index; wk-- )
    			working[ wk + 1 ] = working[ wk ];
    		working[ index ] = ',';
    		len = working.length;
    	}
    	return working.join( '' );
    }
    
    //	worldScripts.telescope_debug
    //	worldScripts.telescope_debug._showProps( cs.map, 'map' )
    //	worldScripts.telescope_debug._showProps( cs.map, 'map', false,	false,	true )
    //	worldScripts.telescope_debug._showProps( cs.map, 'map', true )
    //	worldScripts.telescope_debug._showProps( cs.map, 'map', true,		false,	true )
    //	worldScripts.telescope_debug._showProps( cs.map, 'map', false,	true )
    //	worldScripts.telescope_debug._showProps( cs.map, 'map', false,	true,	true )
    //	worldScripts.telescope_debug._showProps( cs.map, 'map', true,		true,	true )
    //	worldScripts.telescope_debug._showProps( cs.map, 'map', false,	2,		true )
    
    //	worldScripts.telescope_debug._showProps =
    
    this._showProps =	function _showProps( obj, objName, newLine, show_deep, expand_arrays, show_type ) {
    	//										default:	true	true (1)		true			false
    	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( /[ ]{3:}/g, '  ' );
    		result = result.replace( /[\n]+/g, '\\n' ).replace( /[\t]+/g, '\\t' )
    		// result = str.replace( /[\s]+/g, ' . ' );
    		result = '"' + (len > 90 ? result.substr(0, 90) + ' ...' : result) + '"';
    		return result
    	}
    
    	function mkPad( i, suppress ) {
    		if( i <= 0 || suppress ) return '';
    		padding.length = newLine ? i : 1;
    		return (newLine ? '.' :'') + padding.join( '    ' ); // '.' needed for console, as leading space trimmed
    	}
    	 
    	function rptType( obj, showIt ) {
    		if( Array.isArray( obj ) )
    			return show_type || showIt ? ' <array: ' + obj.length + ' elements> ' : ''; 
    		else if( typeof obj === 'object' )
    			return show_type || showIt ? ' <object: ' + Object.keys( obj ).length + ' keys> ' : ''; 
    		else
    			return ' <' + typeof obj + '> ';
    	}
     
    	function show_array( array, recurse ) {
    		if( array.length === 0 ) return '[ ]';
    		ilevel++;
    		var str = '[ ';
    		var pad = mkPad( ilevel, !expand_arrays );
    		var len = array.length;
    		for( var index = 0; index < len; index ++ ) {
    			str += !expand_arrays || !newLine ? (index > 0 ? ', ' : '') : '\n';
    			str += pad;
    			if( expand_arrays )
    				str += index + ': ' + fmt_prop( array[ index ], recurse );
    			else
    				str += fmt_prop( array[ index ], (recurse <= 1 ? 0 : recurse - 1) );
    		}
    		ilevel--;
    		if( len > 0 && expand_arrays && newLine ) str += '\n';
    		str += mkPad( ilevel, !len || !expand_arrays ) + ' ]' + rptType( array );
    		return str;
    	}
    
    	function show_obj( obj, recurse ) {
    		ilevel++;
    		var str = '{ ';
    		var pad = mkPad( ilevel );
    		var len = Object.keys( obj ).length;
    		for( var item in obj ) {
    			str += newLine ? '\n' : '    ';
    			str += pad;
    			if( Array.isArray( obj ) ) {
    				str += show_array( obj[ item ], recurse );
    			} else {
    				str += (obj.hasOwnProperty( item ) ? '' : '^') + item + ': ';
    				str += fmt_prop( obj[ item ], recurse );
    			}
    			str += ';';
    		}
    		ilevel--;
    		if( len > 0 && newLine ) str += '\n';
    		str += mkPad( ilevel, !len ) + ' }' + rptType( obj );
    		return str;
    	}
    	
    	function fmt_prop( prop, recurse ) {
    		if( parents.indexOf( prop ) < 0 )
    			parents.push( prop );
    		else 
    			return prop;
    		var type = typeof prop;
    		var str = '';
    		if( prop === null ) {
    			str += 'null';
    		} else if( type === 'undefined' ) {
    			str += 'undefined';
    		} else if( type === 'string' ) {
    			str += trim_str( prop ) + (show_type ? ' <string: ' + prop.length + ' char.s>' : '');
    		} else if( type === 'number' ) {
    			str +=	cd._number_str( prop, prop % 1 === 0 ? 0 : expand_arrays ? 3 : 1 )
    					+ (show_type ? ' <number>' : '');
    		} else if( type === 'boolean' ) {
    			str += prop + (show_type ? ' <boolean>' : '');
    		} else if( type === 'function' ) {
    			str += ' function ' + prop.name + '()';
    		} else if( Array.isArray( prop ) ) {
    			if( expand_arrays ) {
    				str += show_array( prop, (recurse <= 1 ? 0 : recurse - 1) );
    			} else {
    				str += rptType( prop, true );
    			}
    		} else if( type === 'object' && prop ) {
    			if( recurse > 0 ) {
    				str += show_obj( prop, (recurse <= 1 ? 0 : recurse - 1) );
    			} else {
    				str += rptType( prop, true );
    			}
    		} else {
    			str += prop + (show_type ? ' <'+type+'>' : '');
    		}
    		parents.pop();
    		return str;
    	}
    	
    	var that = _showProps;
    	var cd = ( that.cd = that.cd || worldScripts.telescope_debug );
    	
    	var padding = [], parents = [];
    	if( show_type === undefined ) 		show_type = false;
    	if( expand_arrays === undefined )	expand_arrays = true;	
    	if( show_deep === undefined ) 		show_deep = 1;
    	if( newLine === undefined ) 		newLine = true;
    	var rmax = !show_deep ? 0 : show_deep === true ? 1 : ~~(show_deep);
    	var ilevel = 1;
    
    	return (newLine ? '\n' : ' ') + objName + ': ' + fmt_prop( obj, rmax ) + (newLine ? '\n' : '');
    }
    
    }).call(this);
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    Scripts/telescope_fps_monitor.js
    this.name        = "telescope_fps_monitor";
    this.author      = "cag";
    this.copyright   = "2017 cag";
    this.licence     = "CC BY-NC-SA 4.0";
    this.description = "Monitor and report frames/sec, and optionally, various other stats, plus callback support.";
    this.version     = "1.2";
    
    /* global addFrameCallback, isValidFrameCallback, log, player, removeFrameCallback, Uint8Array
     */
     
    /// stripped down version of fps_monitor for running with Telescope 
    
    this._fps_monitor_closure = (function _fps_monitor_closure() { 
        "use strict";
    	// closure is self-initiating, doing so when it loads, so there no need to call it
    
    	var that = _fps_monitor_closure;
        
        /*  user settings  */
        
        var OXP_NAME = '<<your oxp>>'; // required! used as 1st parm in call to log(); can also be set 
                                       //   if you call _init_fps_monitor( '<<your oxp>>' )
        // reporting
        var N_MINUTES = 1;             // minutes, frequency of reporting on last minute's stats (default 1, 0 to turn off)
        var SHORT_TERM = 15;           // minutes, > 1, time interval for short term report (default 15, 0 to turn off)
        var LONG_TERM = 60;            // minutes, > 1, time interval for long term report (default 60, 0 to turn off)
        var FPS_ONLY = false;          // stores and reports only fps value
        var FILE_LOG = true;           // write to the log file results for 3 time periods just mentioned
        var CONSOLE_LOG = false;       // echo the reports to in-game console
        var CONSOLE_TIME = 5;          // seconds, 1-10, duration of in-game console message (default 5)
    
        // calculations
        var TRUNC_LOW = 10;            // seconds, > 0, for discarding low outliers before calculating fps (default 10, 0 to turn off)
        var TRUNC_HIGH = 5;            // seconds, > 0, for discarding high outliers before calculating fps (default 5, 0 to turn off)
        var HARMONIC_MEAN = false;     // when true, calc harmonic rather than arithmetic mean, for less sensitivity to outliers
    
        // these next 5 booleans turn on extra values for reports and return values for data fetch functions
        // - computed every minute together w/ fps; relative order is always:  median, mode, mean, high, low
        
        var MEDIAN = true;             // fps rate that has equal # of values before/after it in the minute
                                       // - averaged over SHORT_TERM and LONG_TERM
        var MODE = false;              // fps rate with the most hits in the minute
                                       // - averaged over SHORT_TERM and LONG_TERM
        var MEAN = false;              // arithmetic mean (simple average), not very useful, included for completeness
                                       // - averaged over SHORT_TERM and LONG_TERM
        var HIGH = true;               // highest frame count in a second in the term
        var LOW = true;                // lowest frame count in a second in the term
    
        /*  user functions  */
        
        function _setup_fps_report( minutes, shortterm, longterm, filelog, console, duration ) { // set reporting variables
            // set values of N_MINUTES, SHORT_TERM, LONG_TERM, FILE_LOG, CONSOLE_LOG, CONSOLE_TIME
            //               int,       int,        int,       bool,     bool,        int
    		N_MINUTES = get_term( minutes, 1, 1 );
    		SHORT_TERM = get_term( shortterm, 1, 15 );
    		LONG_TERM = get_term( longterm, 1, 60 );
    		if( filelog !== undefined ) FILE_LOG = !!filelog;
    		if( console !== undefined ) CONSOLE_LOG = !!console;
    		if( N_MINUTES === 0 && SHORT_TERM === 0 && LONG_TERM === 0 ) {
    			FILE_LOG = false;
    			CONSOLE_LOG = false;
    		}
    		if( duration ) {						// cannot be set to 0
    			let dur = get_term( duration, 1, 3 );
    			CONSOLE_TIME = dur > 10 ? 10 : dur;	// as per the wiki
    		}
    	}
    	function _setup_fps_calc( cut_low, cut_high, harmonic, fps_only, median, mode, mean, high, low ) { // set calculation variables
            // set values of TRUNC_LOW, TRUNC_HIGH, HARMONIC_MEAN, FPS_ONLY, MEDIAN, MODE, MEAN, HIGH, LOW
            //               int,       int,        bool,          bool,   	 bool,   bool, bool, bool, bool
    		TRUNC_LOW = get_term( cut_low, 1, 5 );
    		TRUNC_HIGH = get_term( cut_high, 1, 5 );
    		if( harmonic !== undefined )HARMONIC_MEAN = !!harmonic;
    		if( fps_only ) FPS_ONLY = !!fps_only;
    		if( FPS_ONLY ) MEDIAN = MODE = MEAN = HIGH = LOW = false;
    		if( median !== undefined )	MEDIAN = !!median;
    		if( mode !== undefined )	MODE = !!mode;
    		if( mean !== undefined )	MEAN = !!mean;
    		if( high !== undefined )	HIGH = !!high;
    		if( low !== undefined )		LOW = !!low;
    		if( FPS_ONLY && ( MEDIAN || MODE || MEAN || HIGH || LOW ) ) {
    			let num = (MEDIAN ? 1 : 0 ) + (MODE ? 1 : 0 ) + (MEAN ? 1 : 0 )
    						+ (HIGH ? 1 : 0 ) + (LOW ? 1 : 0 );
    			let count = num;
    			log('fps_monitor', '_setup_fps_calc, warning: fps_only setting (true) conflicts with ' 
    					+ (MEDIAN ? 'median' + (num-- > 1 ? ', ' : '') : '') 
    					+ (MODE ? 'mode'     + (num-- > 1 ? ', ' : '') : '') 
    					+ (MEAN ? 'mean'     + (num-- > 1 ? ', ' : '') : '')
    					+ (HIGH ? 'high'     + (num-- > 1 ? ', ' : '') : '') 
    					+ (LOW ? 'low'       + (num-- > 1 ? ', ' : '') : '') 
    					+ '. '+(count>1 ? 'These values are' : 'This value is') +' NOT being stored.' ); 
    		}
    		if( FPS_ONLY ) MEDIAN = MODE = MEAN = HIGH = LOW = false;
    	}
    	function _init_fps_monitor( oxp_name, paused, no_fcb ) {// str, [bool, bool]; one-time setup; sets 'oxp_name' used when calling log(OXP_NAME,"...")
    															//  - 'paused' prevents it from starting until you call _turn_on_fps_monitor()
    															//  - 'no_fcb' prevents it adding a frame callback, so you either call _turn_on_fps_monitor()
    															//	  later or one of your fcb's calls _fps_monitor() and passes along 'delta'
    		if( oxp_name )
    			OXP_NAME = typeof oxp_name === 'string'	// or, you can just edit this file above
    							? oxp_name
    							: oxp_name.toString();
            else
                OXP_NAME = 'FPS_MONITOR';
    		if( isValidFrameCallback( FCB_handle ) ) 
    			removeFrameCallback(  FCB_handle );
    		if( !no_fcb && !isValidFrameCallback( FCB_handle ) )
    			FCB_handle = addFrameCallback( _fps_monitor.bind( that ) );
    			// FCB_handle = addFrameCallback( _fps_monitor.bind( this ) );
    		if( paused !== undefined )
    			is_running = !paused;
    		else
    			is_running = true;
    	}
    	function _turn_on_fps_monitor( no_fcb, stutter, thresh, wipe ) {// [bool[,bool[,float[,bool]]]]; resume fps monitoring; set 'no_fcb' to true to prevent adding a frame callback (see above)
    																// - set 'stutter' to true to enable stutter checking (NB: this will force LOW to be true!)
    																// - 'thresh' is a number > 0 & < 1 that defines what a stutter is, default 0.8.
    																//   So if you encounter a frame count that's less than thresh * fps, a report is written, at the end of that minute,
    																//   to your log file. (w/ default 0.8, any second w/ < 80% of mean frames gets reported)
    																// - 'wipe' set true wipes all data arrays for a clean start
    		if( !no_fcb && !isValidFrameCallback( FCB_handle ) )
    			FCB_handle = addFrameCallback( _fps_monitor.bind( that ) );
    			// FCB_handle = addFrameCallback( _fps_monitor.bind( this ) );
    		_reset_fps_monitor( 0, wipe );
    		is_running = true; 
    		if( stutter === undefined ) return;
    		chk_stutter = !!stutter;
    		if( chk_stutter && !stutter_mon && !LOW ) 
    			set_calc_vars();					// need to adjust for required LOW values
    		stutter_mon = chk_stutter;
    		if( thresh === undefined ) return;
    		if( thresh > 0 && thresh < 1 )
    			threshold = thresh;
    		else
    			log( OXP_NAME, 'fps_monitor, _turn_on_fps_monitor, invalid threshold of ' + thresh + ', using default of ' + threshold );
    	}
    	function _turn_off_fps_monitor( remove ) {	// [bool]; suspend fps monitoring; set 'remove' to true to remove frame callback
    		if( remove && isValidFrameCallback( FCB_handle ) ) {
    			removeFrameCallback( FCB_handle );
    			FCB_handle = null;
    		}
    		is_running = false; 
    	}
    	function _set_callback( fn, period ) {	// function, [int]; returns bool; set a callback function to be invoked every 'period' seconds (default is 60)
    											// - for performance reasons, callbacks only occur on second boundary. If you want more, search for the string
    											//   "per frame resolution" and uncomment the line
    		if( typeof fn !== 'function' ) 	return false;
    		if( period <= 0 ) 				return false;
    		callback = fn;
    		callback_period = period > 0 ? period >>> 0 : 60;
    		var max = SHORT_TERM > LONG_TERM ? SHORT_TERM : LONG_TERM;
    		if( N_MINUTES > max ) max = N_MINUTES;
    		max *= 60;
    		if( callback_period > max ) {
    			callback_period = max;
    			log( OXP_NAME, 'fps_monitor, _set_callback, period exceeds your largest time interval, reset to ' + max );
    		}
    		return true;
    	}
    	function _clear_callback() {				// clear a callback function set in _set_callback
    		callback = null;
    		callback_period = 0;
    		return true;
    	}
    	function _realtime_fps() {					// returns int; result of the last second's frame count
    												//  - returns -1 in the event of no data (not started)
    												//  - not real useful (?), but trivial to implement
    		if( frames_per_sec[ 0 ] === 0 			// has not started
    			|| !is_running )					// is not running
    			return -1;	
    		let i = seconds - 1;					// seconds is post-incremented
    		if( i < 0 ) i = 59;
    		return frames_per_sec[ i ];				// return last frame's count
    	}
    	function _current_fps( all_stats, result ) {// [bool[,array]]; returns int or array; result(s) of the last minute's measured data
    												//  - returns -1 in the event of no data (not started, not running long enough)
    												//  - when 'all_stats' is true, returns an array of up to 6 values.  I deliberately left 
    												//    this out of the _setup_... functions, so it would be more apparent what return to expect
    												//    - array values have relative order: fps, median, mode, mean, high, low
    												//  - when 'result' is present, data is copied into the array (ie. not creating new one to return)
    												//    - will append to any existing array data, so set its length = 0 if you want to empty it first
    		return return_data( minute_data, all_stats, result );
    	}
    	function _short_term_fps( all_stats, result ) {// [bool[,array]]; returns int or array; result(s) of the last SHORT_TERM's *reported* data
    												//  - if you want SHORT_TERM data from period ending when you call, use _get_fps_data()
    												//  - otherwise, works the same as _current_fps()
    		return return_data( short_data, all_stats, result );
    	}
    	function _long_term_fps( all_stats, result ) {// [bool[,array]]; returns int or array; result(s) of the last LONG_TERM's *reported* data
    												//  - if you want LONG_TERM data from period ending when you call, use _get_fps_data()
    												//  - otherwise, works the same as _current_fps()
    		return return_data( long_data, all_stats, result );
    	}
    	function _get_frames( count, result ) {		// [int[,array]]; returns an array integer frame counts; 'count' specifies # of seconds of 
    												// data to return; if no count supplied, all are returned, to a max. of 60 (if it's been running 
    												// less than a minute, the array's length will reflect that)
    												//  - returns -1 in the event of no data (not started, not running long enough)
    												//  - when 'result' is present, data is copied into the array (ie. not creating new one to return)
    												//    - will append to any existing array data, so set its length = 0 if you want to empty it first
    		var len, asking, data, dst, src;
    		if( !is_running 						// monitor is not running
    			|| frames_per_sec[ 0 ] === 0 )	
    			return -1;		
    		len = frames_per_sec[ seconds ] === 0 	// buffer not full, strip unused
    				? seconds : 60;
    		asking = 60;							// max. # to return
    		if( count !== undefined )
    			asking = typeof count === 'number' 
    						? round( count )
    						: parseInt( count, 10 );
    		dst = asking < len ? asking : len;		// index for destination
    		asking = dst;
    		if( result && Array.isArray(result) ) { 
    			data = result;						// copy data into supplied array
    			dst += result.length;
    		} else {
    			data = new Array( dst );
    		}
    		src = seconds;							// index for source
    		while( asking-- ) {
    			if( src <= 0 ) src = 60;
    			data[ --dst ] = frames_per_sec[ --src ];
    		}
    		return data;
    	}
    	function _get_fps_data( all_stats, count, result ) {// [bool[,int[,array]]]; returns an array of integers of all it's collected for further processing
    												//  - returns -1 in the event of no data (not started, not running long enough)
    												//  - by default, returns an array of fps values
    												//    - set 'all_stats' true for data to be an array of: fps, median, ..., depending on what
    												//      flags you set in _setup_fps_calc() (or by editing the script)
    												//      NB: not an array of [ fps, median, ... ] entries, just a stream of int's
    												//  - length of return array will be max( SHORT_TERM, LONG_TERM ) if !all_stats
    												//    - if all_stats is true, each min. will have 1 - 6 int's, depending on flags set previously 
    												//  - count is some # of minutes <=  max( SHORT_TERM, LONG_TERM )
    												//  NB: _short_term_fps() returns data from short term report, as such, only changes every SHORT_TERM minutes.
    												//      If you want data from the previous SHORT_TERM minutes, call this fn w/ count = SHORT_TERM. Ditto for long term.
    												//  - when 'result' is present, data is copied into the array (ie. not creating new one to return)
    												//    - will append to any existing array data, so set its length = 0 if you want to empty it first
    		var len, asking, data, fps_only, maxm, dst, src;
    		if( !is_running || !fps_stats			// monitor is not running
    			|| fps_stats[ 0 ] === 0 )			// too soon, have not measured anything
    			return -1;		
    		fps_only = all_stats !== undefined
    					? !all_stats : true;
    		if( fps_stats[ index ] === 0 ) {		// buffer not full, strip unused
    			maxm = asking = minutes;			// default for when !count
    			len = index;
    		} else {
    			maxm = asking = max_minutes;		// default for when !count
    			len = array_size;					// max. fps_stats index
    		}
    		if( count ) {
    			asking = typeof count === 'number' 
    						? round( count )
    						: parseInt( count, 10 );
    		}
    		if( asking > maxm ) asking = maxm;
    		if( fps_only ) {
    			dst = asking;						// index for destination			
    		} else {
    			let dsize = asking * DATA_SIZE;			
    			dst = dsize < len ? dsize : len;	// index for destination
    		}
    		if( result && Array.isArray(result) ) {
    			data = result;						// copy data into supplied array
    			dst += result.length;
    		} else {
    			data = new Array( dst );
    		}
    		src = index;							// index for source
    		while( asking-- ) {
    			if( src <= 0 ) src = array_size;
    			if( fps_only ) {
    				src -= DATA_SIZE - 1;			// discard everything but fps
    			} else {
    				if( LOW )    data[ --dst ] = fps_stats[ --src ];
    				if( HIGH )   data[ --dst ] = fps_stats[ --src ];
    				if( MEAN )   data[ --dst ] = fps_stats[ --src ];
    				if( MODE  )  data[ --dst ] = fps_stats[ --src ];
    				if( MEDIAN ) data[ --dst ] = fps_stats[ --src ];
    			}
    			data[ --dst ] = fps_stats[ --src ];	// copy fps
    		}
    		return data;
    	}
    
    	/*  internal variables - you break it, you own it!  */
    
    	// function references
    	var round = Math.round;
    	var callback = null;						// function to be called at end of each callback_period minutes
    	// local variables
    	var chk_stutter = false;					// user toggle for stutter monitor
    	var stutter_mon = false;					// internal state for stutter monitor
    	var threshold = 0.8;						// amount below average fps that defines what's a stutter (eg. < 80%)
    	var stutters = [];							// array (re-used) to store stutters while reporting
    	var callback_period = 0;					// # of seconds between each callback
    	var game_paused = false;					// flag so when games resumes, can continue accuratly
    	var FCB_handle = null;						// reference for frame callback
    	var is_running = false;						// on - off switch available via _turn_on_fps_monitor() & _turn_off_fps_monitor()
    	var frames = 0;								// # of frames
    	var seconds = 0;							// # of seconds, index into frames_per_sec array
    	var minutes = 0;							// # of minutes; not an index, so keeps incrementing (unles _reset_fps_monitor( .., true ))
    	var start_delta = -1;						// delta value @ start of ea. sec. (can be any value, depending on when 1st fcb called)
    	var running_total = 0;						// running total of delta values passed to frame callback
    	var index = 0;								// index into fps_stats, max is array_size
    	var frames_per_sec = new Uint8Array( 60 );	// typed array used for storing frame counts, wraps every minute
    	var counted = new Uint8Array( 256 ); 		// internal working area for counting_sort; frame counts limited to 254!
    	var counted_most, counted_mid;				// when sorting, save most popular -> mode, middle -> median
    
    	var DIVISOR = 60 - TRUNC_HIGH - TRUNC_LOW;	// divisor for (trucated) mean or dividend for harmonic mean
    	var DATA_SIZE, array_size, max_minutes, fps_stats, long_data, short_data, minute_data, mid_data, frames_sorted;
    	// NB: data store relative order: fps, median, mode, mean, high, low
    	//     - some may be missing due to user settings
    	function set_calc_vars() {
    		DIVISOR = 60 - TRUNC_HIGH - TRUNC_LOW;
    		max_minutes = LONG_TERM > SHORT_TERM	// the larger of the two
    				   ? LONG_TERM : SHORT_TERM;
    		var old_data_size = DATA_SIZE;
    		if( FPS_ONLY ) {
    			frames_sorted = frames_per_sec;		// sort not called, pts to frames_per_sec for code following sort
    			DATA_SIZE = (stutter_mon || chk_stutter) ? 2 : 1;
    		} else {
    			frames_sorted = new Uint8Array( 60 );// typed array used for calc'g average fps, high & low values, reset every minute
    			DATA_SIZE = 1 + (MEDIAN ? 1 : 0 )	// saving 1 data point + upto 5 more
    						  + (MODE   ? 1 : 0 )
    						  + (MEAN   ? 1 : 0 )
    						  + (HIGH   ? 1 : 0 )
    						  + (LOW || stutter_mon || chk_stutter ? 1 : 0 );
    		}
    		if( old_data_size === DATA_SIZE ) 	 	// subsequent call but size not changed
    			return;	
    		fps_stats = null;
    		array_size = max_minutes * DATA_SIZE;
    		if( array_size > 0 )
    			fps_stats = new Uint8Array( array_size );// a 2nd typed array of each minute's stats as an int or run of ints
    		minute_data = new Uint8Array( DATA_SIZE );// if both SHORT_TERM & LONG_TERM shut off, still needed for minutes
    		short_data = long_data = mid_data = null;
    		if( SHORT_TERM === 0 && LONG_TERM === 0 ) {
    			return;
    		} else if( SHORT_TERM === 0 ) {
    			long_data = new Uint8Array( DATA_SIZE );// save latest data for get fns
    		} else if( LONG_TERM === 0 ) { 
    			short_data = new Uint8Array( DATA_SIZE );
    		} else {
    			short_data = new Uint8Array( DATA_SIZE );
    			long_data = new Uint8Array( DATA_SIZE );	// save latest data for get fns
    			mid_data = new Uint8Array( DATA_SIZE );	// internal for case when long & short reports coincide
    		}			
    	}
    	
    	var rpt_begin = '*** FPS report ***: over the last ';
    	var minutes_str = [ 'minute', '1 min'];
    	var short_term_str, long_term_str;
    	function set_time_strs() {
    		short_term_str = [ (SHORT_TERM % 15 === 0
    							? (SHORT_TERM / 15) + '/4 hour'
    								+ (SHORT_TERM > 15 ? 's.' : '.') 
    							: SHORT_TERM + ' minutes'),
    							SHORT_TERM + ' min' ];
    		long_term_str = [ (LONG_TERM  % 60 === 0 
    							? (LONG_TERM / 60) +' hour' 
    								+ (LONG_TERM > 60 ? 's.' : '.') 
    							: LONG_TERM + ' minutes'),
    							LONG_TERM + ' min' ];
    	}
    
    	/*  internal functions - any modification will void you warrantee  */
    	
    	function get_term( term, minimum, defawlt ) {
    		var num;
    		if( !term ) return 0;					// no parms shuts it down
    		num = typeof term === 'number' 
    				? round( term ) : parseInt( term, 10 );
    		num = num >= minimum ? num : defawlt;
    		return num;
    	}
    	function return_data( data, all_stats, result ) {
    		if( !data || data[ 0 ] === 0  			// too soon, have not measured 1st minute/term
    				  || !is_running )				// is not running
    			return -1;	
    		var fps_only, rtn, len, i;
    		fps_only = all_stats === undefined
    						? true : !all_stats;
    		if( !all_stats && !result )
    			return data[ 0 ];					// return fps
    		
    		if( result && Array.isArray(result) ) {
    			i = result.length;
    			rtn = result;
    		} else {
    			i = 0;
    			rtn = [];
    		}					
    		len = fps_only ? 1 : DATA_SIZE;
    		while( len-- ) {
    			rtn[i] = data[i];					// copy data into (supplied) array		
    			i++;
    		}
    		return rtn;								// return [ fps, ... ]
    	}
    	function wipe_data( list ) {				// for typed Arrays, setting length = 0 doesn't clear, .fill() not supported
    		if( list ) for( var i = list.length-1; i >= 0; i-- ) list[i] = 0;
    	}
    	function _reset_fps_monitor( delta, restart ) {	// [float[,bool]]: reset the monitor; internal function, useful for limiting monitoring (eg. dockside vs in flight)
    													//  - not strictly required, as invoking the closure initializes these
    													//  - if using this, it's not necessary to supply 'delta' (counting will start on next frame)
    													//  - 'restart' sets minutes to 0 & wipes existing data; for calls w/o 'delta', use _reset_fps_monitor( 0, true )
    		if( restart === true ) {
    			wipe_data( minute_data );
    			wipe_data( fps_stats );
    			wipe_data( short_data );
    			wipe_data( long_data );
    			wipe_data( mid_data );
    			minutes = 0;
    		}
    		start_delta = 	!delta ? -1 : running_total;
    		running_total = !delta ? 0  : running_total + delta;
    		frames =		!delta ? 0  : 1;							
    		seconds = 0;							// indices must be set to 0	every minute
    		index = 0;								//    "                     every 'array_size'
    	}
    	function rpt_to_console( fps, median, mode, mean, high, low, time_str ) {
    		let msg = 'FPS: ' + fps;
    		if( !FPS_ONLY )  {
    			if( MEDIAN ) msg += ', median: ' + median;
    			if( MODE )   msg += ', mode: ' + mode;
    			if( MEAN )   msg += ', mean: ' + mean;
    			if( HIGH )   msg += ', hi: ' + high;
    			if( LOW )    msg += ', lo: ' + low;
    		}
    		msg += ' (' + time_str[ 1 ] + ')';
    		player.consoleMessage( msg, CONSOLE_TIME );
    	}
    	function rpt_to_log( fps, median, mode, mean, high, low, time_str ) {
    		var type = '';
    		if( HARMONIC_MEAN ) type = 'harmonic';
    		if( TRUNC_HIGH === 25 && TRUNC_LOW === 25 ) 
    			type += 'interquartile';
    		else if( TRUNC_HIGH || TRUNC_LOW ) type = 'truncated' + (type ? ' ' + type: '');
    		let report = rpt_begin + time_str[ 0 ] + ', '+type+' mean fps was '+ fps;
    		if( !FPS_ONLY ) {
    			if( MEDIAN ) report += ', median: ' + median;
    			if( MODE )   report += ', mode: ' + mode;
    			if( MEAN )   report += ', mean: ' + mean;
    			if( HIGH )   report += ', high: ' + high;
    			if( LOW )    report += ', low: ' + low;
    		}
    		log( OXP_NAME, report );
    	}
    	function rpt_results( data, time_str ) {
    		var fps, median, mode, mean, high, low;
    		var i = 0;
    		fps = data[ i++ ];
    		median = mode = mean = high = low = 0;
    		if( MEDIAN ) median = data[ i++ ];
    		if( MODE )   mode =   data[ i++ ];
    		if( MEAN )   mean =   data[ i++ ];
    		if( HIGH )   high =   data[ i++ ];
    		if( LOW )    low =    data[ i++ ];
    		if( FILE_LOG )
    			rpt_to_log( fps, median, mode, mean, high, low, time_str );
    		if( CONSOLE_LOG ) 
    			rpt_to_console( fps, median, mode, mean, high, low, time_str );
    	}
    	function check_stutter() {
    		var fps = minute_data[ 0 ];
    		if( !fps ) return;	// not ready
    		var low = minute_data[ DATA_SIZE - 1 ];
    		var thresh = fps * threshold;
    		if( low >= thresh ) return; // ignore any diff above threshold
    		var ref = '', all = '[ ', rpt = '';
    		var hic_len = 0, run = 0, slot, found = false;
    		var i, frames;
    		for( i = 0; i < 60; i++ ) { // always called at end of a full minute
    			frames = frames_per_sec[ i ];
    			slot = i + 1;
    			if( i < 15 ) ref += slot > 9 ? '   ' + slot : '    ' + slot;
    			all += (frames < 100 ? ' ' : '') + frames + (i === 59 ? ' ]' : (i < 59 && slot % 15 === 0) ? ',\n  ' : ', ');
    			if( frames < thresh ) {
    				stutters.push( frames );
    				found = true;
    				continue;
    			}
    			if( found ) { 
    				if( run > 0 ) rpt += ' ..[' + run + ' s].. ';
    				hic_len = stutters.length;
    				while( hic_len-- ) 
    					rpt += stutters.shift() + (hic_len > 0 ? ', ' : ' ');
    				stutters.length = hic_len = run = 0;
    				found = false;
    			}
    			run++;
    		}
    		rpt += run > 0 ? '..[' + run + ' s]..' : '';
    		if( found ) { 
    			hic_len = stutters.length;
    			while( hic_len-- ) 
    				rpt += stutters.shift() + (hic_len > 0 ? ', ' : ' ');
    			stutters.length = 0;
    		}
    		ref += '\n';
    		rpt = 'check_stutter, w/ threshold = ' + thresh.toFixed(1) + ' frames:  ->  ' + rpt;
    		log( OXP_NAME, rpt + '\nfps = ' + fps + ', low = ' + low + ', last 60 seconds:\n' + ref + all );
    	}
    /*
    *** FPS report ***: over the last minute, harmonic mean fps was 140, median: 139, high: 205, low: 130
    
    check_stutter, w/ threshold = 133.0 frames:  ->   ..[39 s].. 130  ..[7 s].. 132 ..[12 s]..
    fps = 140, low = 130, last 60 seconds:
        1    2    3    4    5    6    7    8    9   10   11   12   13   14   15
    [ 146, 137, 140, 134, 137, 140, 135, 137, 139, 137, 145, 139, 146, 138, 137,
      133, 135, 145, 138, 139, 139, 143, 142, 141, 141, 139, 145, 140, 138, 142,
      133, 139, 134, 138, 149, 145, 138, 139, 136, 130, 146, 136, 138, 139, 138,
      137, 144, 132, 140, 142, 137, 137, 135, 134, 133, 137, 137, 152, 205, 205 ]
    */
    	function counting_sort() {
    		var i, cs, max = 0, hh, ll, diff;
    		cs = 255; 
    		while( cs-- ) counted[ cs ] = 0;		// clear counts array
    		i = 60;
    		while( i-- ) {
    			cs = frames_per_sec[ i ];			// record count of each value
    			counted[ cs ]++;
    		}
    		cs = 255; i = 60;
    		while( cs-- ) {
    			let count = counted[ cs ];			// re-build frames_per_sec sorted by counts
    			if( MODE && count > max ) { 
    				max = count;
    				counted_most = cs;
    			}
    			while( count-- ) {
    				frames_sorted[ --i ] = cs;
    				if( MEDIAN && i > 28 && i < 31 ) {
    					if( i === 30 ) hh = cs;
    					else if( i === 29 ) {
    						ll = cs;
    						diff = hh - ll;
    						if( diff % 2 === 0 ) 	// ll + diff/2
    							counted_mid = ll + (diff >>> 1);
    						else  					// round( ll + diff/2 )
    							counted_mid = ll + ((diff + 1) >>> 1);
    					}
    				}
    			}
    		}
    	}
    	function do_callback() {
    		if( !callback ) return;
    		if( callback_period <= 0 ) return;
    		var elapsed = callback_period <= 60 ? seconds : minutes * 60 + seconds;
    		if( elapsed % callback_period === 0 ) {
    			try {
    				callback();
    			} catch( err ) {
    				log( OXP_NAME, 'callback function encountered an error: ' + err );
    				_clear_callback();
    				log( OXP_NAME, 'callback function has been cleared.' ); 
    			}
    		}
    	}
    	function _fps_monitor( delta ) {
    		if( delta === 0 && game_paused ) {		// game paused, subsequent frames
    			return;
    		} else if( delta === 0 ) {				// game paused, first frame
    			game_paused = true;
    			return;
    		} else if( delta > 0 && game_paused ) {	// game resumed
    			game_paused = false;
    		}
    		var fps, high, low, highest, lowest, short_rpt, long_rpt,
    			count, sum, total, i, j, len, mid, mean, mode, median, 
    			mean_sum, mode_sum, median_sum, div, data, mid_div, mid_data;
    		if( !is_running ) return;
    		if( !frames_sorted ) set_calc_vars();	// just-in-time init'n
    		if( index >= array_size ) index = 0;	// must be @ top of 'loop' due to various return statements
    		if( start_delta === -1 ) {				// 1st time thru
    			running_total = start_delta = delta;
    			frames = 0;							// start counting next frame
    			return;
    		}
    		running_total += delta;
    		frames++;
    		if( running_total - start_delta < 1 ) {
    ///			do_callback();						// remove comment for per frame resolution
    			return;
    		}										// every second
    		frames_per_sec[ seconds++ ] = frames > 254 ? 254 : frames;
    		frames = 0;
    		start_delta = running_total;
    		if( seconds < 60 ) {
    			do_callback();
    			return;	
    		}										// every minute	
    		seconds = 0;
    		minutes++;
    		if( !FPS_ONLY )
    			counting_sort();					// req'd for truncated mean, median & mode
    		median = mode = mean = high = sum = total = 0;
    		low = 254; i = 60;
    		while( i-- ) {
    			count = frames_sorted[ i ];
    			total += count;
    			if( i >= TRUNC_LOW && i < (60 - TRUNC_HIGH) ) {
    				if( HARMONIC_MEAN ) {
    					if( count > 0 )sum += 1/count;
    				} else
    					sum += count;
    			}
    			if( HIGH && count > high ) high = count;
    			if( (LOW || stutter_mon) && count < low ) low = count;
    		}
    		fps = HARMONIC_MEAN ? round( DIVISOR / sum )
    							: round( sum / DIVISOR );
    		i = 0;
    		minute_data[ i++ ] = fps;
    		if( MEDIAN ) minute_data[ i++ ] = counted_mid;
    		if( MODE )   minute_data[ i++ ] = counted_most;
    		if( MEAN )   minute_data[ i++ ] = round( total/ 60 );
    		if( HIGH )   minute_data[ i++ ] = high;
    		if( LOW || stutter_mon ) minute_data[ i++ ] = low;
    		if( N_MINUTES > 0 && (N_MINUTES === 1	// not turned off by user
    				|| minutes % N_MINUTES === 0) ) // on Nth minute
    			rpt_results( minute_data, minutes_str );
    		if( stutter_mon ) check_stutter();
    		if( array_size === 0 ) {
    			do_callback();
    			return;								// turned off by user
    		}
    		i = 0; // store data for short & long term reports
    		fps_stats[ index++ ] = minute_data[ i++ ]; // fps
    		if( MEDIAN ) fps_stats[ index++ ] = minute_data[ i++ ];
    		if( MODE )   fps_stats[ index++ ] = minute_data[ i++ ];
    		if( MEAN )   fps_stats[ index++ ] = minute_data[ i++ ];
    		if( HIGH )   fps_stats[ index++ ] = minute_data[ i++ ];
    		if( LOW || stutter_mon ) fps_stats[ index++ ] = minute_data[ i++ ];
    						
    		short_rpt = SHORT_TERM > 0 && minutes > 1 
    						? minutes % SHORT_TERM : -1;
    		long_rpt =  LONG_TERM > 0 && minutes > 1 
    						? minutes % LONG_TERM  : -1;
    		if( short_rpt !== 0 && long_rpt !== 0 ) {// not ready to report either benchmark
    			do_callback();
    			return;							
    		}
    		if( short_rpt === long_rpt ) {			// reporting both
    			if( SHORT_TERM < LONG_TERM ) {
    				len = j = LONG_TERM * DATA_SIZE;
    				data = long_data;
    				div = LONG_TERM;
    				mid = SHORT_TERM * DATA_SIZE;
    				mid_data = short_data;
    				mid_div = SHORT_TERM;
    			} else {
    				len = j = SHORT_TERM * DATA_SIZE;
    				data = short_data;
    				div = SHORT_TERM;
    				mid = LONG_TERM * DATA_SIZE;
    				mid_data = long_data;
    				mid_div = LONG_TERM;
    			}
    		} else {								// reporting which one?
    			if( long_rpt === 0  ) {
    				len = j = LONG_TERM * DATA_SIZE;
    				data = long_data;
    				div = LONG_TERM;
    			} else {
    				len = j = SHORT_TERM * DATA_SIZE;
    				data = short_data;
    				div = SHORT_TERM;
    			}
    			mid = -1;							// not used for single, act as flag
    		}
    		lowest = low = 254; 	highest = high = 0;		i = index;
    		mean_sum = mean = mode_sum = mode = median_sum = median = sum = 0;
    		while( j ) {							// walk back from end gathering stats
    			if( LOW || stutter_mon ) {
    				low = fps_stats[ --i ];
    				if( low < lowest ) lowest = low;
    			}
    			if( HIGH ) {
    				high = fps_stats[ --i ];
    				if( high > highest ) highest = high;
    			}	
    			if( MEAN )   mean_sum   += fps_stats[ --i ];
    			if( MODE )   mode_sum   += fps_stats[ --i ];
    			if( MEDIAN ) median_sum += fps_stats[ --i ];
    			sum += fps_stats[ --i ];
    			if( i <= 0 ) i = array_size;
    			j -= DATA_SIZE;
    			if( mid > 0 && len - j === mid ) {
    				// both are reporting, store data for shorter one
    				let m = 0;
    				mid_data[ m++ ] = round( sum / mid_div ); // fps
    				if( MEDIAN ) mid_data[ m++ ] = round( median_sum / mid_div );
    				if( MODE )   mid_data[ m++ ] = round( mode_sum   / mid_div );
    				if( MEAN )   mid_data[ m++ ] = round( mean_sum   / mid_div );
    				if( HIGH )   mid_data[ m++ ] = highest;
    				if( LOW || stutter_mon ) mid_data[ m++ ] = lowest;
    			}
    		}
    		data[ i++ ] = round( sum / div ); 		// fps
    		i = 0;
    		if( MEDIAN ) data[ i++ ] = round( median_sum / div );
    		if( MODE )   data[ i++ ] = round( mode_sum   / div );
    		if( MEAN )   data[ i++ ] = round( mean_sum   / div );
    		if( HIGH )   data[ i++ ] = highest;
    		if( LOW || stutter_mon ) data[ i++ ] = lowest;
    		do_callback();
    		if( !short_rpt ) set_time_strs();			// just-in-time init'n
    		if( short_rpt === 0 ) 
    			rpt_results( short_data, short_term_str );
    		if( long_rpt === 0 ) 
    			rpt_results( long_data, long_term_str );
    		if( index === array_size ) 
    			_reset_fps_monitor( delta );			// memory full, reset counters
    	}
    
    	return {	
    				  _setup_fps_calc: _setup_fps_calc,
    			    _setup_fps_report: _setup_fps_report, 
    				_init_fps_monitor: _init_fps_monitor,
    			 _turn_on_fps_monitor: _turn_on_fps_monitor,
    			_turn_off_fps_monitor: _turn_off_fps_monitor,
    			   _reset_fps_monitor: _reset_fps_monitor,
    					_set_callback: _set_callback,
    				  _clear_callback: _clear_callback,
    					_realtime_fps: _realtime_fps, 
    					 _current_fps: _current_fps, 
    				  _short_term_fps: _short_term_fps, 
    				   _long_term_fps: _long_term_fps, 
    					  _get_frames: _get_frames, 
    				    _get_fps_data: _get_fps_data, 
    					 _fps_monitor: _fps_monitor
    			};
    			
    }).call(this);
    
    
    
    Scripts/telescope_refundeq.js
    this.name        = "telescope_refundeq";
    this.author      = "Norby";
    this.copyright   = "2013 Norbert Nagy";
    this.licence     = "CC BY-NC-SA 3.0";
    this.description = "Refund an equipment if undamaged and not cheaply repaired only.";
    this.version     = "1.0";
    
    this.allowAwardEquipment = function( eqKey, ship, context )
    {   
        "use strict";
        if( context !== "purchase" ) 	return false;
        var ps = player && player.ship;
        if( ship !== ps )            	return false;
        var actualEq = eqKey,
            endsWith = '',
            parsed = eqKey.split( '_' );
        if( parsed.length === 3 ) {
            actualEq = parsed[ 0 ] + '_' + parsed[ 1 ];
            endsWith = parsed[ 2 ];
        }
        if( !endsWith ) 				return false;   // will be '' if there's only 1 '_'
        let status = ps.equipmentStatus( actualEq );
        if( status !== 'EQUIPMENT_OK' ) return false;  // must repair before you sell 
        var ws = worldScripts.telescope;
        if(      eqKey === "EQ_GRAVSCANNER2_REFUND" )   return ws.$FixedGS !== 1; // these have no equipment that
        else if( eqKey === "EQ_SMALLDISH_REFUND" )      return ws.$FixedSD !== 1; // requires them, so we are free
        else if( eqKey === "EQ_LARGEDISH_REFUND" )      return ws.$FixedLD !== 1; // to sell them if fully repaired
        else if( eqKey === "EQ_TELESCOPE_REFUND" ) {
            if( ps.equipmentStatus( "EQ_TELESCOPEEXT" ) === "EQUIPMENT_UNAVAILABLE" ) // must sell ext first
                return ws.$FixedTel !== 1;	// can sell only if fully repaired
            return false;
        } else if( eqKey === "EQ_TELESCOPEEXT_REFUND" ) {
            if( ps.equipmentStatus( "EQ_GRAVSCANNER" ) === "EQUIPMENT_UNAVAILABLE" )  // must sell gravity scanner first
    			return ws.$FixedTel !== 1;	// can sell only if fully repaired
            return false;
        } else if( eqKey === "EQ_GRAVSCANNER_REFUND" ) {							// must sell add-ons first
            if( ps.equipmentStatus( "EQ_LARGEDISH" )    !== "EQUIPMENT_UNAVAILABLE" ) return false;
            if( ps.equipmentStatus( "EQ_SMALLDISH" )    !== "EQUIPMENT_UNAVAILABLE" ) return false;
            if( ps.equipmentStatus( "EQ_GRAVSCANNER2" ) !== "EQUIPMENT_UNAVAILABLE" ) return false;
            return ws.$FixedGS !== 1;		// can sell only if fully repaired
        }
        return false;
    }
    
    Scripts/telescope_repaireq.js
    this.name        = "telescope_repaireq";
    this.author      = "Norby";
    this.copyright   = "2013 Norbert Nagy";
    this.licence     = "CC BY-NC-SA 3.0";
    this.description = "Repair an equipment if damaged only, full repairs if cheaply fixed.";
    this.version     = "1.0";
    
    this.allowAwardEquipment = function( eqKey, ship, context )
    {
        "use strict";
        if( context !== "purchase" ) 	return false;
        var ps = player && player.ship;
        if( ship !== ps )            	return false;
        var actualEq = eqKey,
            endsWith = '',
            parsed = eqKey.split( '_' );
        if( parsed.length === 3 ) {
            actualEq = parsed[ 0 ] + '_' + parsed[ 1 ];
            endsWith = parsed[ 2 ];
        }
        if( !endsWith ) 				return false;					// will be '' if there's only 1 '_'
        let status = ps.equipmentStatus( actualEq );
        let isDamaged = status === "EQUIPMENT_DAMAGED";
        let isWorking = status === "EQUIPMENT_OK";
        if( !isWorking && !isDamaged )  return false;					// EQUIPMENT_UNAVAILABLE or EQUIPMENT_UNKNOWN
        var ws = worldScripts.telescope;
        if(      eqKey === "EQ_TELESCOPE_REPAIR" )        return isDamaged && ws.$FixedTel === 0;
        else if( eqKey === "EQ_GRAVSCANNER_REPAIR" )      return isDamaged && ws.$FixedGS === 0;
        else if( eqKey === "EQ_GRAVSCANNER2_REPAIR" )     return isDamaged && ws.$FixedGS === 0;
        else if( eqKey === "EQ_SMALLDISH_REPAIR" )        return isDamaged && ws.$FixedSD === 0;
        else if( eqKey === "EQ_LARGEDISH_REPAIR" )        return isDamaged && ws.$FixedLD === 0;
        
        else if( eqKey === "EQ_TELESCOPE_FULLREPAIR" )    return isWorking && ws.$FixedTel === 1;
        else if( eqKey === "EQ_TELESCOPEEXT_FULLREPAIR" ) return isDamaged; // has no cheap repair option
        else if( eqKey === "EQ_GRAVSCANNER_FULLREPAIR" )  return isWorking && ws.$FixedGS === 1;
        else if( eqKey === "EQ_GRAVSCANNER2_FULLREPAIR" ) return isWorking && ws.$FixedGS === 1;                       
        else if( eqKey === "EQ_SMALLDISH_FULLREPAIR" )    return isWorking && ws.$FixedSD === 1;
        else if( eqKey === "EQ_LARGEDISH_FULLREPAIR" )    return isWorking && ws.$FixedLD === 1;
        
        return false;
    }
    
    Scripts/telescopeeq.js
    this.name        = "telescopeeq";
    this.author      = "Norby, cag";
    this.copyright   = "2013 Norbert Nagy";
    this.licence     = "CC BY-NC-SA 4.0";
    this.description = "Commands and settings, press mode to cycle, activate to accept or change.";
    this.version     = "2.0.1";
    
    (function(){
    "use strict";
    
    this.$eq_Menu = [ 
    	[ "Nearest target" ],
    	[ "Rescan" ],
    	[ "Step forward in the target list" ],
    	[ "Step back in the target list" ],
    	[ "Steering:", "off", "nearest target only", "both nearest and step in the list"], 
        // cutoff for RemoveInFlight; string 'Steering:' is hard coded in mode()
    	// [ "Lightballs:", "off", "navigation only", "ships", "masslock rings", "bright masslock rings", "large" ],
    	[ "Lightballs:", "off", "navigation only", "include ships", "large" ],
    	[ "Masslock rings:", "current alert/weapons state: off", "current alert/weapons state: on", "brighter" ],
    	[ "Sniper ring km:", "off", "5-25.6", "10-25.6", "15-25.6", "5-30", "10-30", "15-30"], //off=10-10
    	[ "Targets:", "20 and limitation in red alert", 50, 100, 200 ],
    	[ "Visual target:", "off", "weapons off", "no ring", "no station", "no question mark", "all" ],
    	[ "Visual target size:", 1, 2, 3, 4, 5, 6, 7, 8 ] 
    ];
    this.$eq_MenuItem = 0; //current item in the commands or settings menu
    
    //equipment events
    this.activated = function activated() {
    	var that = activated;
    	var ws = (that.ws = that.ws || worldScripts.telescope);
    	var menu = (that.menu = that.menu || this.$eq_Menu);
    	var eq_GetSubItem = (that.eq_GetSubItem = that.eq_GetSubItem || this.$eq_GetSubItem);
    	var fmtMasslockMsg = (that.fmtMasslockMsg = that.fmtMasslockMsg || this.$fmtMasslockMsg);
    
    	var item = this.$eq_MenuItem;
    	if( item < 4 ) { 												//commands
    		switch( item ) {
    			case 0: 												//Nearest target
    				ws._nearest_Sighting(); 							//lock the nearest target
    				break;
    			case 1: 												//Rescan
                    ws._auto_updates( true );
    				break;
    			case 2: 												//Step forward in the target list
                    ws._chg_curr_Sighting( 1 );
    				break;
    			case 3: 												//Step back in the target list
                    ws._chg_curr_Sighting( -1 );
    				break;
    		}
    	} else { 														//settings
    		var submenu = menu[ item ];
    		var subitem = eq_GetSubItem( item ) + 1; 				    //step to the next subitem in the current settings
    		if( subitem >= submenu.length ) subitem = 1; 				//back to the first subitem (0. is the menuitem)
    		switch( item ) {
    			case 4: 												//Steering
    				ws.$TelescopeMenuSteering = subitem;
    				ws._SetSteering( subitem );
    				break;
    			case 5: 												//Lightballs
    				ws.$TelescopeMenuLightballs = subitem;
    				ws._SetLightballs( subitem );
    				break;
    			case 6: 												//MasslockRings
    				ws.$TelescopeMenuMasslockRings = subitem;
    				ws._SetMasslockRings( subitem );
    				break;
    			case 7: 												//Sniper ring km
    				ws.$TelescopeMenuSniper = subitem;
    				ws._SetSniper( subitem );
    				break;
    			case 8: 												//Targets
    				ws.$TelescopeMenuTargets = subitem;
    				ws._SetTargets( subitem );
    				break;
    			case 9: 												//Visual target
    				ws.$TelescopeMenuVisual = subitem;
    				ws._SetVisual( subitem );
    				ws._clear_HUD_Effects();
    				if( ws.$FixedTel === 0 )
    					ws._showVShip(); 							    //repaint visual target and ring
    				else 
                        player.consoleMessage("Your cheaply fixed Telescope can not show visual target, buy full repair.", ws.$ConsoleMsgDurn);
    				break;
    			case 10: 												//Visual target size
    				ws.$TelescopeMenuVisualSize = subitem;
    				if( ws.$TelescopeMenuVisual < 2 ) {
    					ws.$TelescopeMenuVisual = 2; 					//must at least wp off
    					ws._SetVisual( 2 );
    				}
    				ws._SetVisualSize( subitem );
    				ws._clear_HUD_Effects();
    				if( ws.$FixedTel === 0 )
    					ws._showVShip(); 							    //repaint visual target and ring
    				else 
                        player.consoleMessage("Your cheaply fixed Telescope can not show visual target, buy full repair.", ws.$ConsoleMsgDurn);
    				break;
    		}
    		//show subitem in settings
    		// player.consoleMessage( menu[ item ][ 0 ] + " " + menu[ item ][ subitem ], ws.$ConsoleMsgDurn );
    		let menuStr = menu[ item ][ 0 ],
    			subitemStr = menu[ item ][ subitem ];
    		if( item === 6 ) {
    			subitemStr = fmtMasslockMsg( subitemStr );
    		}
    		player.consoleMessage( menuStr + " " + subitemStr, ws.$ConsoleMsgDurn );
    	}
    }
    
    this.$fmtMasslockMsg = function fmtMasslockMsg( msg ) {
    	var that = fmtMasslockMsg;
    	var ws = (that.ws = that.ws || worldScripts.telescope);
    
    	var [ alert, weapons ]  = ws._getShowStateText();
    	return msg.replace( 'current', alert ).replace( 'state', weapons );
    }
    
    this.mode = function mode() {
    	var that = mode;
    	var ws = (that.ws = that.ws || worldScripts.telescope);
    	var orig = (that.orig = that.orig || worldScripts.telescopeeq);
    	var menu = (that.menu = that.menu || this.$eq_Menu);
    	var cutoff = (that.cutoff = that.cutoff || null);
    	var eq_GetSubItem = (that.eq_GetSubItem = that.eq_GetSubItem || this.$eq_GetSubItem);
    	var fmtMasslockMsg = (that.fmtMasslockMsg = that.fmtMasslockMsg || this.$fmtMasslockMsg);
    
        if( cutoff === null ) {                                         // 1st invocation
            for( let i = 0, len = menu.length; i < len; i++ ) {
                if( menu[ i ][ 0 ] === 'Steering:' ) {
                    cutoff = that.cutoff = i;
                    break;
                }
            }
        }
        var item = this.$eq_MenuItem + 1;							    //step forward in the current menu
        if( item >= menu.length || (item > cutoff && ws.$RemoveInFlight) ) 
            item = 0;                      	                            //back to the first menu item
        this.$eq_MenuItem = item;
    	orig.$eq_MenuItem = item; // report mode outside this scope to allow interrogation by 3rd parties.
    
    	if( item < 4 ) {												//no subitem with commands
    		player.consoleMessage( menu[ item ][ 0 ], ws.$ConsoleMsgDurn );
    		if( item < 2 || item > 3 ) {								// not stepping through sightings
    			ws._resetIdentDelay();
    		}
    	} else {
    		ws._resetIdentDelay();
    		let menuStr = menu[ item ][ 0 ],
    			subitemStr = menu[ item ][ eq_GetSubItem( item ) ];
    		if( item === 6 ) {
    			subitemStr = fmtMasslockMsg( subitemStr );
    		}
    		player.consoleMessage( menuStr + ' ' + subitemStr, ws.$ConsoleMsgDurn );		
    	}
    }
    
    //Telescope primable equipment method
    this.$eq_GetSubItem = function eq_GetSubItem( item ) {
    	var that = eq_GetSubItem;
    	var ws = (that.ws = that.ws || worldScripts.telescope);
    
    	var m = null;
    	switch( item ) {
    		case 4:  													//Steering
    			m = ws.$TelescopeMenuSteering;
    			if( m > 0 ) return( m );
    			return( ws.$Steering + 1 );
    		case 5: 													//Lightballs
    			m = ws.$TelescopeMenuLightballs;
    			if( m > 0 ) return( m );
    			if( !ws.$LightBalls ) 			return( 1 ); 			//off ('off')
    			if( !ws.$ShipLightBalls ) 		return( 2 ); 			//ship off ('navigation only')
    			if( !ws.$LargeLightBalls ) 		return( 3 ); 			//large off ('include ships')
    			return( 4 );											//large on ('large')
    		case 6: 													//MasslockRings
    			let state = ws._getShowState(),							// on/off for current alert/weaps state
    				currFlags = ws._currMLFlags();
    			let menu = 1;
    			if( currFlags & state ) {
    				menu = 2;
    				if( ws.$BrightMassLockRings ) 
    					menu = 3;
    			}
    			return menu;
    		case 7:  													//Sniper ring km
    			m = ws.$TelescopeMenuSniper;
    			if( m > 0 ) return( m );
    			var max = ws.$SniperRange;
    			var min = ws.$SniperMinRange;
    			if( max === min ) 	return( 1 ); 						//off
    			var p = 0; 												//max = 25.6km
    			if( max > 25600 ) p = 3; 								//add this to reach subitems with max = 30km
    			if( min <= 5000 ) 	return( 2 + p ); 					// "5-25.6" or "5-30"
    			if( min >= 15000 )	return( 4 + p ); 					// "15-25.6" or "15-30"
    			return( 3 + p ); 										// "10-25.6" or "10-30"
    		case 8:  													//Targets
    			m = ws.$TelescopeMenuTargets;
    			if( m > 0 ) return( m );
    			if( ws.$RedAlertLimiter )	return( 1 ); 				//20 and limitation in red alert
    			if( ws.$MaxTargets <= 50 )	return( 2 ); 				//50
    			if( ws.$MaxTargets >= 200 ) return( 4 ); 				//200
    			return( 3 ); 											//100
    		case 9:  													//Visual target
    			m = ws.$TelescopeMenuVisual;
    			if( m > 0 ) return( m );
    			if( ws.$VisualTargetNormalSize === 0 
                    || ws.$ShowVisualTarget === 0 ) 	return( 1 ); 	//off
    			if( ws.$ShowVisualTarget === 1 )	    return( 2 ); 	//weapons off
    			if( !ws.$Ring ) 						return( 3 ); 	//no ring
    			if( !ws.$ShowVisualStation ) 		 	return( 4 );	//no station
    			if( !ws.$ShowVisualQuestionMark )		return( 5 ); 	//no "?"
    			return( 6 ); 											//all
    		case 10:  													//Visual target size (VZoomSize=0 if full off)
    			m = ws.$TelescopeMenuVisualSize;
    			if( m > 0 ) return( m );
    			return( ws.$VisualTargetNormalSize );
    	}
    	return( -2 ); 													//no subitem with commands
    }
    
    }).call(this);
    
    Scripts/xlonly.js
    "use strict";
    this.name        = "xlonly";
    this.author      = "Norby";
    this.copyright   = "2013 Norbert Nagy";
    this.licence     = "CC BY-NC-SA 3.0";
    this.description = "This equipment is usable only for ships from 400t like Anaconda or Hard Python.";
    this.version     = "1.0";
    
    this.allowAwardEquipment = function(eqKey, ship, context)
    {
    //	player.consoleMessage( eqKey+" "+ship+" "+context );//debug
    	if( ship.mass >= 400000 ) return true;
    	else return false;
    }