Back to Index Page generated: Nov 12, 2024, 11:02:04 PM

Expansion Station Dock Control

Content

Manifest

from Expansion Manager's OXP list from Expansion Manifest
Description Adds some realism and depth to the game by giving each station with NPC traffic its own list of docked ships that launch when their scheduled departure time lapses. Adds some realism and depth to the game by giving each station with NPC traffic its own list of docked ships that launch when their scheduled departure time lapses.
Identifier oolite.oxp.phkb.StationDockControl oolite.oxp.phkb.StationDockControl
Title Station Dock Control Station Dock Control
Category Mechanics Mechanics
Author phkb phkb
Version 1.1.28 1.1.28
Tags
Required Oolite Version
Maximum Oolite Version
Required Expansions
Optional Expansions
Conflict Expansions
Information URL http://wiki.alioth.net/index.php/Station_Dock_Control n/a
Download URL https://wiki.alioth.net/img_auth.php/1/10/StationDockControl_1.1.28.oxz n/a
License CC-BY-NC-SA 3.0 CC-BY-NC-SA 3.0
File Size n/a
Upload date 1709981651

Documentation

Also read http://wiki.alioth.net/index.php/Station%20Dock%20Control

readme.txt

Station Dock Control
By Nick Rogers

Special thanks go to:
cim, for all his coding help and patience with my incessant list of questions. I couldn't have done this without your help, cim!
Anonymissimus, Norby, Disembodied, another_commander, Wildeblood, Day and all other forum members who have sent me their suggestions and feedback, without whom this OXP would not have been anywhere near as good as it could be.
To all the devs of Oolite, who are committed to making Oolite such a great base from which experiments like this can be launched.

About this OXP
==============
This OXP attempts to add some realism and depth to the game by giving each station with NPC traffic its own list of docked ships that launch when their scheduled departure time lapses. (Note: this is different to, but not incompatible with, Thargoids "Traffic Control OXP", which adds a "Traffic Control" comms voice to stations to aid with docking). I've categorised this OXP as "Mechanics" as it does change the way Oolite works at a fundamental level. Instead of ships being launched from a station on a part-random/part-balancing logic, now ships are defined ahead of time, and will launch into the system regardless of the potential for "unbalancing" the system. 

What does this OXP do:
1. Maintains a detailed list of ships docked at stations that have NPC traffic. This includes ship types and equipment, pilot information, escort and group information, docking bays, destination systems, and arrival and departure times.
2. Launch ships that reach their departure time, and send them on their way to other systems. Perform regular assessments of each station dock, checking its congestion level, and potentially bumping departure times for all vessels to ease launch queue pressure.
3. Automatically dock ships at stations. Most ships that enter a station will end up on the dock list.
4. Move ships through a full range of docking statuses: Inbound, Docking, Unloading, Docked, Loading, awaiting departure, and Launching (see descriptions of each of these below).
5. Simulate real-world pilot activities: Occasionally adjust ships destination, or their departure time, to simulate real-world changes in plan. Some pilots will hide their destination from GalCop authorities, so it doesn't show up on the list.

What this OXP does not do:
This OXP does not fully implement a persistent universe, where things are just where you left them in galaxies far far away. There will undoubtedly be areas where there is inconsistency. Implementing a full, static universe where every ship is accounted for and persists between saves, and where the whole universe is impacted by a time jump, is beyond the scope of this OXP. 

While this OXP is trying to add some realism, it is also attempting to do so with the smallest gameplay impact as possible. If everything works correctly, it should be possible for a player to install this OXP and not notice anything significantly different.

The OXP works on both 1.80 and 1.82, but it works better on 1.82 and later. In 1.80, ships can look slightly different on launch to how they look in the dock, but new features in 1.82 fix this. Plus, there are some bugs in 1.80 relating to ship AI and hyperspace destination systems that are fixed in 1.82. 

This OXP will work with or without RandomShipNames. If you don't use RandomShipNames, the docked list will only show ship types.

Hopefully, the things this OXP does do contribute to a much richer experience for Oolite players. 

What's happening under the surface
----------------------------------
The main change this OXP makes to the core game is to adjust the core populator function. For ships jumping into the system from somewhere else, there is no change. That should still happen using the existing logic. What has changed is the outbound functions, and it's worth taking a moment to explain the logic that this OXP uses. Previously, the standard populator would randomly launch ships of various types, but some of that randomness was mitigated by some balancing functions. That is, if there was more than a certain number of ships with a particular role, no more of that role would be created. What that did was prevent a system from having more than 3 heavy hunter groups, or 4 medium pirate freighter groups. If there are 3 or more heavy hunter groups, the populator wouldn't create anything.

In this OXP, some of that balancing is gone, because all of the ships in the docking queues are defined in advance. For ships coming into the system the balancing factor is still present, but for ships launching at a station there could very well be 5 heavy hunter groups. 

Note: **Any OXP that changes the core Oolite populator function may potentially conflict with this OXP.** If you find an OXP that does conflict please contact the me via the Oolite bulletin board so I can work out the best approach for addressing the conflict.

Any OXP that forces a station to launch ships, particularly trading vessels, shuttles, pirates, hunters or assassins, will work with this OXP, however it should be noted that those launches will happen outside of the view of this OXP and therefore they won't appear on the launching list. 

Dockside operations
===================
When the player is docked at a station that has NPC traffic and allows the list to be shown, an item called "Station traffic control" will be available on the Interfaces (F4) page. The title of this will vary, based on the name of the station. For instance, at a main Coriolis station, the title will be "Coriolis Station traffic control".

On opening the traffic control screen, a list of ships will be displayed. Beside each ship will be a scheduled departure time or current dock status (see below), and, if the ship has lodged a flight plan, what their destination system is. This list will refresh itself periodically, even while you're viewing it.

If you select a ship from the list a "Ship Information" page will be displayed. This page will outline some of the information the station has about the vessel, including the name, legal status, and whether the ship is part of a group or escort. Also, pilot information will be available, including their home system and species.

Please note that data on the list is subject to change. Ship captains can change their minds about when they want to leave, or what their destination will be.

Ship captains tend to keep destination information to themselves, unless there is some benefit to sharing the information, and they will only share the information with pilots who have a reputation they can trust. If a captain has determined to trust the player their destination will be visible in the dock list and they will permit the player to launch before or after their ships, in essence offering a token group membership to the player. When a pilot has permitted this, the options "Launch before this ship" and "Launch after this ship" will be visible in the menu. The availability of these options will be based on the reputation of the player, and the relative danger of the destination system. If the player has a reputation as a pirate hunter, then traders and couriers might value the player's presence. But if the player has the reputation of a pirate or assassin, traders are likely to be wary of the player.

You can't launch before or after any ship that is due to launch in less than 10 minutes. Station launch protocols require a minimum of 10 minutes lead time for a launch.

Occasionally, when you open the Traffic Control display, you will see this: "Establishing communications link with station traffic control. Please stand by." This message indicates that the station's data network is experiencing bandwidth issues. The list will appear after a short delay.

Exemptions: 
1. Police vessels are not shown in the list of docked ships. All police vessels are docked separately to civilian vessels.
2. Miners, Scavengers and other Station-specific vessels: Some stations will have a miner, scavenger, or some other craft that launch and dock periodically. These are not listed in the station traffic control list, as they fall outside of the traffic controller's work policy guidelines.
Note: While the above vessels are not included on the dock list, they will still show up as "Inbound" when they are docking with the station. This is purely for information purposes. Once they enter the station they are controlled by other dockyard services and will not be displayed on the list.

Status descriptions explained
-----------------------------
Ships that appear in the traffic control list can have one of five different statuses. They are:
"Inbound"	This indicates the ship is just outside the station and has requested permission to dock.
"Docking"	This indicates the ship has entered the station and is being manoeuvred into a docking bay, umbilicals are being attached, ship manifests interfaced with station systems.
"Unloading"	This indicates the ship has docked in station and cargo, passengers or parcels, are being unloaded/delivered.
"Docked"	This indicates the ship is waiting for cargo and/or departure instructions.
"Loading"	This indicates the ship is currently loading cargo.
Time (hh:mm)	When a time is displayed, this is the time remaining until they will launch from the station.
"Launching"	This indicates the ship has requested a launch slot from Traffic Control. Once they are cleared (and it may take some time if there is heavy traffic), they will be manoeuvred into the launch tunnel.

Flight operations
=================
GalCop stations also provide a data stream that can be accessed through a multi-function display (MFD). To access this data stream, first target the station, then select a MFD slot using the Shift ":" keyboard sequence, and then cycle through the available MFD's using the ";" key until the Traffic Control list appears:

/-----------------------------------------\
|Ships docking: 2   In launch corridor: 2 |
|Cobra Mark III                   Inbound |
|Python                           Inbound |
|Cobra Mark I                   Launching |
|Adder                               0:05 |
|Boa Cruiser                         0:11 |
|Gecko                               0:11 |
|Krait                               0:11 |
|Anaconda                            0:15 |
|Cobra Mark I                        0:15 |
\-----------------------------------------/

The MFD is a "lite" version of the main Traffic Control screen. It shows, at the top, the number of ships attempting to dock at the station, and the number of ships that are currently in the launch queue. Then the first nine entries from the traffic control list will be displayed. 

Note: There is a difference between "Launching" and being in the launch corridor. A ship that is launching is still inside the station in their allocated docking bay. Ships in the launch queue are being launched by the station traffic controller and should be exiting the docking port shortly.

SDC Hack Chip
=============
In some Anarchy, Feudal and Multi-Government systems where the tech level is greater than or equal to 9, if you can find a Rock Hermit (Salvage Gangs and Hacker Outposts are also possibilities if those OXPs are installed) you might be able to purchase an "SDC Hack Chip". The hack chip bypasses the security protocols of the main station's dock list, giving you complete visibility of all ships destinations. When you view the details for a ship with the hack chip installed, you will have an additional option: "Override launch authority". Selecting this option will give you the ability to launch before or after that ship.

Using the hack chip is risky, though. GalCop takes a dim view of security breaches, and if they catch you using it, be prepared for serious repercussions. GalCop monitor their networks carefully, and using a chip like this cannot go hidden for long. It may take only a few seconds for GalCop to trace the security breach to your ship.

Third Party OXP's
=================
Several methods are available to help third party OXP's add ships to a dock, and then be informed of events that take place to those ships. Please see the separate document "developer notes.txt" for all information about adding ships and monitoring status.

License
=======
This OXP is released under the Creative Commons Attribution - Non-Commercial - Share Alike 3.0 license. To view a copy of this license, visit http://creativecommons.org/licenses/by-nc-sa/3.0/

Station image from http://www.iconshut.com
Lock image from http://simpleicon.com/lock-2.html
Unlock image from http://simpleicon.com/lock-3.html

Discussion
==========
This OXP is discussed at this forum link: http://aegidian.org/bb/viewtopic.php?f=4&t=17468

Version History
===============
1.1.28
- Option to allow all stations to display a launch queue in the MFD is now saved between games.
- Fixed bug when docking at stations in interstellar space that don't have NPC traffic.

1.1.27
- Added an option to the config that allows all stations to have their launch queue visible in the MFD, rather than just GalCop stations.
- Updated background overlays to be more visible in Oolite 1.91ff.
- Fixed issue where NaN could be returned when determining heat insulation for a ship.
- Fixed issue where some ships would turn around and redock just after launching, instead of heading to a new system.
- Fixed possible conflict with Skilled NPC's, where launching ships could override accuracy settings.
- Added extra launch queue logging option - level 3 to turn on AI messages of launched ships.
- Bug fixes.

1.1.26
- Added missing pirate ships from phkb's Factory paint jobs packs.
- Added In System Trader AI to exceptions.

1.1.25
- Added ships from phkb's Factory Paint Jobs packs (for Boa, Boa Mk2 and Anaconda).

1.1.24
- Fix for the thargoidStrikes populator setting.
- Correctly set initialisation for main dataset.
- Added extra dock versions of ships.

1.1.23
- Code refactoring for potential speed and memory usage improvements.

1.1.22
- Added spaces to elements in the MFD.
- When viewing station model on dock list, size of station will now not change after viewing individual ships.

1.1.21
- Added flag (accessible via Library Config) to turn of RandomShipName's ship names in the Docking list MFD.
- Ensured stations in interstellar space do not get dock lists.
- Turned off some debug messages.
- Bug fixes.

1.1.20
- Turned off debug messages.
- Added dock version of ships from Sungs Shady Ships.

1.1.19
- Further tweaks to the populator routine to prevent gridlock and avoid stations with empty docks.
- Bug fixes.

1.1.18
- Additional checking types added to ship condition checking routine.
- Tweaks to timeslot calculation code to try and prevent gridlock conditions.
- Added dock version of ships from Equipment By Ship Class OXP.

1.1.17
- Prevented player ship from appearing twice on the MFD list.
- Removed extraneous ":" character from ship names on MFD when RandomShipNames OXP is not installed.

1.1.16
- Bug fixes.

1.1.15
- Fixed bug that was preventing late station analysis from populating docking lists.
- Added dock version of ships from Laser Cannons OXP.
- Added dock version of ships from Military Shields Ships OXP.
- Excluded carriers (from the Carriers OXP) from being added to a station during the population process.
- Incorrect default zoom factor was being applied to ship models in the dock view.
- Adjusted the model size for some specific ships.

1.1.14
- Improved handling of systems with no data.
- Moved Taxi Stations to low traffic list.

1.1.13
- Excluded Behemoth's from having any traffic.
- Fixed method of determining whether a ship can fit in a station's dock. Function was returning true when actual result should have been false.

1.1.12
- Added Taxi Galactica stations to list of controlled stations.
- Added Tionisla Orbital Graveyard to list of controlled stations.
- Bug fixes.

1.1.11
- Turned off repopulation routines when player starts a hyperspace countdown.
- Turned off some more debug messages.
- Code refactoring.

1.1.10
- Bug fixes.

1.1.9
- Removed debug message.
- Added additional station-specific ship exclusions.
- Added protection against an "undefined" randomshipname being returned.
- Fixed methodology for processing escorts defined in the new format and with "[]" indiciating a specific ship key.
- Excluded ships from Convoys and Freighter Convoys OXP's from being added to a station during the population process.

1.1.8
- Added additional option to external menus to limit their display by station.
- Fixed issue where assassin escorts were displaying the incorrect ship type.
- Fixed issue where displaying the station model on the dock list would create a bogus station entry and empty the dock list for the current station.

1.1.7
- Added code samples contributed by forum member "Day".

1.1.6
- Further attempts to increase performance.

1.1.5
- Added more dock versions of ships from Hard Ships OXP.

1.1.4
- Added dock versions of ships from Hard Ships OXP.
- Added dock versions of ships from Sniper Gun OXP.
- Added dock versions of ships from Ionics OXP.
- Added dock versions of ships from Ferdelance 3G OXP.
- Corrected dock version entries of Classic Superpythons OXP.
- Some minor tweaks for increased performance.

1.1.3
- Added some additional dock versions of a variety of ships.
- Further attempts to increase performance.

1.1.2
- Further attempts to increase performance.

1.1.1
- Attempts to control the amount of pre-work required to check that ships can fit in docks before adding them.

1.1.0
- Major code refactoring for improved performance.
- Limited maximum number of ships in any dock to be 300 to improve performance.

1.0.6
- Fixed up the default zoom setting for viewing ships in dock. Somehow I'd made it super small.
- Added Pitviper Mark I and Pitviper Mark II custom display sizes.
- Cleanup of some empty data elements.
- Added missing "parameter" element to the external interface function.
- Fixed bug in the $removeExternalInterface function.

1.0.5
- Turned off debug mode on interface screen.

1.0.4
- Removed hard-coded scanner range value.
- Added dock versions of ships from PAG Old Ships OXP.
- Bug fixes.

1.0.3
- Fixed compatibility issue with Lib_GUI and Xenon UI, where backgrounds were being reset when F4 interface page refreshed.
- Fixed issue with incorrect personality being used when displaying ship models in dock.

1.0.2
- Restored ship detail text when viewing ships in the dock.

1.0.1
- Fixed Javascript error when docked at a station in interstellar space.
- Added some AI script exceptions.
- Added extra condition properties when checking if a ship is available or not.
- Added dock versions of ships from "Vector" OXP.
- Added code to check whether a ship can physically dock at a station before putting one inside (v1.85ff required).

1.0.0
- Increased failsafe range for selecting departure timeslots from one to two hours to better cope with docking bottlenecks, and added an abort procedure for when it just can't find any slots.
- Changed display settings in Lib_Config are now restored when game is reloaded.
- Bug fixes.

0.13.8
- Added dock versions of some of Griff's ships.
- Added dock versions of some Random Hits ships.
- Added dock versions of some Rescue Stations ships.
- Improvements in the way escorts are attached to motherships, to cater for more situations.
- Reduced the number of debug messages that are generated if stations aren't present on reload.
- Added some configuration settings to Lib_Config (when present).
- Fixed issue with HUD not becoming visible again when launching while viewing the dock list.
- Fixed issue with the hack chip staying installed if you launch before removing it.
- Bug fixes.

0.13.7
- Turned off a debug tool I accidentally left on.

0.13.6
- Fixed bug that was causing docking ships to be docked in non-existent stations.

0.13.5
- Added routines to let third party OXP's provide details of script properties they want SDC to make a note of so they can be relaunched with the same settings.
- Added routines to let third party OXP's provide details of their custom Javascript AI files so ships can be relaunched with the same AI.
- Fixed issue with recognising and storing the correct AI type for bounty hunter leaders.
- Added dock version of hornet_alt_npc.
- Updated "developer notes.txt" to include information about docking exceptions (introduced in version 0.13.4) and the two new abilities noted above.

0.13.4
- Fixed issue where docking shuttles were not being given a valid intra-system destination for when they next launch.
- Fixed faulty looping logic where a failsafe was included. Hitting the failsafe number of interations of the loop was not causing the loop to end.
- Fixed issue where launching ships were not being bumped in high-congestion states.
- Added way of allow some non-core roles to dock and be shown in the dock list.
- Function optimisation for (hopefully) better performance.
- Changed "==" comparisons to "===" for performance improvements.
- Better handling of null conditions in some routines.
- Bug fixes and code cleanup.

0.13.3
- Added some additional zoom factors for various ship types.
- Fixed some issues with the MFD screen when the player ship is destroyed.

0.13.2
- Removed the sort function at the end of the populator routine to improve performance.
- Added some checks for null values.

0.13.1
- Fixed issue with population routine that was encountering a Javascript error in certain circumstances.

0.13.0
- Restructured the main dataset to improve performance.

0.12.5
- Better handling of when ship group name is null.
- Added dock versions of more ships from WildShips.
- Added dock versions of ships from Deepspace ships.
- Added dock versions of ships from Armoured Type Transport.
- Added flag to easily prevent the population of all station dock queues.
- Added a function, $emptyAllQueues, to remove all ships from all docks in the current system.

0.12.4
- Fixed issue with undeclared local variable error introduced in 0.12.3.
- Added some missing ";" characters.
- Fixed "docks is undefined" Javascript error.

0.12.3
- Moved "Pirate Coves" to the "No Traffic" list.
- Further attempts to fix issue with slow performance when new stations are added outside of the systemWillPopulate routine.
- Turned on traffic at FTZ's. Hopefully above fix will correct issue.
- Fixed edge case where some OXP stations can occasionally get a "null" value for allegiance.

0.12.2
- Turned off traffic at Free Trade Zones - due to the way FTZ's are created, there is too much processing required to populate and control their docks.
- Moved a slow performing function call out of the systemWillRepopulate routine.
- Added dock versions of ships from Smivs Classic Ships.
- Added dock versions of ships from Smivs Classic Variety Pack.
- Added dock versions of ships from Smivs SuperPythons.
- Added dock versions of ships from Smivs Clippers.
- Added dock versions of ships from Smivs Classic Variety Pack.
- Added dock versions of ships from Aegidian's X Ships.
- Added dock versions of ships from Neolite's Shipset.

0.12.1
- Added more third party interfaces for monitoring docking ships, and validated other interfaces. 
- Added "developer notes.txt" file that describes all the third party interfaces.
- Move beta notes to a separate text file to clean up main readme.txt file.
- Updated methods for attaching scripts to ships to (hopefully) make it completely compatible with all OXP's that might also set ship scripts.
- Fixed small bug when attempting to update the MFD after the player ship is destroyed.
- Added some overrides for the zoom factor when viewing docked ships, so that Anacondas, Boas, Pythons (plus a few others) look larger than an Adder.
- Report screens or mission screens that add extra time to the clock will now cause the dock list to be refreshed.
- Updated screenID's to enable BGS background sounds.
- Renamed background overlay images to prevent possibility of future duplication.
- Toned down overlay images.
- Added extra flag to allow the ship model to spin, rather than remain static. You'll need to edit "stationdockcontrol_interface.js" and change line 31 from "this._spinModel = false;" to "this._spinModel = true;"
- Code refactoring.

0.12.0
- Fixed issue where the pilot insurance was not being read correctly from docking ships.
- Fixed "ship is null" bug.
- Updated bounty changes to use "setBounty" rather than updating the bounty directly.
- Updated RandomShipNames code to use roleIsInCategory function.
- Fixed issue with Free Trade Zone, where no ships were showing in the dock list.
- Prevented station monkey patch from patching itself.
- Fixed issue where Java script error could occur when checking shuttle destinations.
- Moved more static text into descriptions.plist.
- Added callback routine to ship data array, so OXP's can add a ship and then be told when the ship was launched. 
- Added more dock versions of ships from Mimoriaty ships.
- Improved the process of adding escorts, so that escort definitions that specify a particular ship (eg "[sidewinder-escort]") will now attach to the mothership correctly.
- Removed some duplicates in the controlled roles array.
- Code refactoring and cleanup.

0.11.2
- Tweaked what happens when ships dock with illegal cargo - bribe attempt will be for all goods, not each one.
- Removed redundant call to "checkForLaunchedShips" during an escape pod activation.
- Fixed issue where some escorts where not attaching to their mothership correctly.
- Fixed shipdata entry for one of the Mimoriaty ships.
- Added more dock versions of ships from Random Hits OXP.

0.11.1
- Fixed Java script bug in debug comments during departure time change routine.
- Added more dock versions of ships from Random Hits OXP.
- Added dock versions of ships from Mimoriaty's Radical Logistics ships.

0.11.0
- Added hack chip.
- Added possibility for lowly ranked players to get launch before/after opportunities.

0.10.0
- Added more dock versions of ships from Random Hits OXP.
- Ships that dock with goods illegal to import will sometimes get an increase to their bounty.
- Added extra routine to check for orphaned group members in the rescheduler.
- Divided up WildShip stations into different station lists to better match their individual roles.
- If a station doesn't exist anymore while data resides in the array, ship data will now be slowly removed.
- Added astrofactory from Dictators.oxp to station list.
- Dock list destinations only viewable by clean pilots.
- Dock list launch before/after request options only available to clean pilots.
- Dock list launch before/after request will be hidden if the ship destination is also hidden.
- The decision to hide ship destinations is now based on the player's role as perceived by NPC's and the danger level at their destinations.
- Adjusted destination viewing period to 12 hours.
- Changed the color of menu items so it's less yellow.
- Adjusted the skip factor for adding hunters, making them slightly more regular.
- Code refactoring.
- Bug fixes.

0.9.10
- Restricted MFD display to GalCop aligned stations only.
- Changed license.

0.9.9
- Added Monkey Patch when required to make this OXP compatible with other OXP's that use "otherShipDocked" on stations.
- Added dock versions of ships from the Wildships OXP.
- Added dock versions of ships from the Random Hits OXP.
- Added routines to check for an escort launching before the mothership.
- Added pirate coves to station list.
- Added all rock hermits and pirate coves to list of destinations excluded for shuttles.
- If a ship using a plist AI docks, they should now get that AI back on launch.
- Added an overlay background to the Station interface screen.
- Fixed bug in rescheduler that was preventing rescheduled ships from ever launching.

0.9.8
- Fixed issue where, very rarely, the docking process tries to remove the player ship.
- Fixed bug that was causing the dock list to be recreated after loading a saved game.

0.9.7
- Tweaks to the repopulation function.
- Added the Launch Queue MFD to the HUD Selector.
- Added routine to use 1.83/4 code to check for big GUI HUD's.
- Added rockhermit-chaotic and rockhermit-pirate to station list.
- Fixed issue where requesting a launch after a ship that isn't due to launch for a considerable time may cause the dock list to be inadequately populated.

0.9.6
- Removed some test debug comments.
- Hopefully fixed scenario where purchasing equipment results in an empty dock (although the jury is out on this one!).

0.9.5
- Added check for player ship when docking ships. In a fast-dock scenario the player ship will otherwise try to be docked like other ships.
- Removed the ability to view other station docks (accidentally left on during debugging).
- Reduced logging level to minimal.
- Added functionality to add custom menu items to a ship display.
- Added Generation ships to all exclusions.
- MFD will now auto-hide itself when the target station goes out of range, and reopen when another station is targeted, if the same MFD slot is still available.

0.9.0
- Further tweaks to what stations shuttles can be sent to.
- When a wide font is used, the number of ships docking displayed in the MFD is no longer hidden.
- Tweaked the title of the dock list so it stays within the normal window bounds with wide fonts.
- Fixed issue where cancelling a docking request was not removing the player ship from the MFD list.
- Fixed issue where an expired docking request was not removing the player ship from the MFD list.
- Put "bigTrader" ships into the "no traffic" list, because these ships are likely to disappear between saves, meaning all the dock data will error when attempting to launch.
- Put interface option text into description.plist file.
- Checks for the "allow_big_gui" option on HUD's.
- Added station roles from the Bank of the Black Monks OXP.
- Added station roles from the Jaguar Company OXP.
- Added station roles from the Collector OXP.
- Added station roles from the Aquatics OXP.
- Added station roles from the Resistance Commander OXP.
- Added station roles from the Lave Academy OXP.
- Added station roles from the Free Trade Zone OXP.
- Added station roles from the Planetfall OXP.
- Added dock versions of ships from the Aquatics OXP.
- Ships destinations will now only become visible when the ship's departure time is under 2 hours.

0.8.0
- Prevented shuttles from heading to otherwise hidden in-system destinations (Slaver bases, hacker outposts), plus some other locations.
- Shuttles will now occasionally hide their in-system destination.
- Balancing of shuttle destinations. Closer stations will have higher probability of being selected over distant ones.
- Added dock versions of ships from the UPS Courier OXP.
- Added station roles from UPS Courier OXP.
- Split out the debug mode on the interface screen, so "this._debug" controls output of comments to the log, while "this._mode" controls what functions are available to the player.
- Fixed bug with storing additional ship roles for escorts, where roles were being stored repeatedly, instead of just once.
- Converted some strings into arrays.
- Fixed issue with MFD occasionally showing "NaN" errors after jumping into a new system and before the populator function has run.

0.7.0
- Improvements to the way ships are added to the system when they are added directly, moving them further away from the stations' launch corridor.
- Fixed potential problem with the MFD in the (very rare) situation of having two stations in range of the player at the one time. Last station targeted will now be displayed.
- Improved the chance of making adjustments during a time jump to again better smooth out launches.
- Auto-refreshing the station dock list now doesn't lose the Xenon UI background.
- Code refactoring.

0.6.1
- Turned off station image from main view (left on by mistake!)
- Put ellipsis on text contractions

0.6.0
- Fixed issue where occasionally all system docking data is dumped just after entering a system.
- Inbound items should not appear twice on the MFD any more.
- Better handling in the MFD of the scenario where data is being populated (eg immediately after arrival in a new system).
- Hide shuttle destinations when status is Docking, Unloading, Docked or Loading (ie. same as for other ships).
- Added some adjustment runs (where ships are moved from various statuses) during a time jump process to better smooth out launches.
- Added a check for new stations being added to a system after loading a saved game (ie. via a new OXP being installed). Docked entries will now be added immediately.
- Fixed incompatibility with Gallery OXP, which was causing ship images to be displayed incorrectly.
- Expanded some of the internal documentation.
- Code cleanup and refactoring.

0.5.0
- Removed possibility that a ship will change their destination on launch.
- Included the additional roles variable in the save game file.
- Added a launch script for escort motherships, so that, if there's a delay in an escort launching, they will still get attached when the mothership launches.
- Added check for the scenario where items in the launching array are for another system.
- Removed version numbers from all JS files so there is only one version number in the manifest to update.
- Attempt to get MFD "Inbound" list to match dock.dockingQueueLength.

0.4.0
- Better protection against null station error when checking for ships docking.
- Fixed bug with adding escorts directly to the system.
- Added shuttle destinations to view.
- Added the possibility that a ship will change their destination on launch.
- Added a method to track primary roles I haven't seen before (in escort ships at present) so that those ships can be docked.
- Stability improvements.

0.3.0
- Rebuilding ship data list in each system to allow for ship data conditions. This should fix the issue where some ship images weren't visible on the ship details view.
- Better processing of station index values. Previously, in some conditions, the indexes were being calculated too early, meaning that some stations didn't get an index.
- Now handles an escape pod sequence.
- Force an update to dock list after most time jumps now (ie when purchasing equipment or insta-docking), rather than waiting for a repopulate.
- Motherships and escorts added directly to the system should now pair up correctly using "offerToEscort".
- Updates to code based on suggestions by Wildeblood.
- Added stations from Anarchies to various lists.
- Code cleanup.

0.2.0
- Better handling of when no ship keys exist for one of the controlled roles.
- Added "Next ship" and "Previous ship" menu options when viewing ship details, to make it easier to move through the dock list.
- Code cleanup.

0.1.0
- Initial release

Equipment

Name Visible Cost [deci-credits] Tech-Level
SDC Hack Chip yes 8250 1+

Ships

Name
dock_Falcon-E
dock_PAG_monitor
dock_PAG_monitor2
dock_PAG_monitor2-player
dock_adck_anaconda
dock_adck_boa
dock_adck_boa-mk2
dock_adck_python-blackdog
dock_adck_python-trader
dock_anaconda
dock_anaconda-civilian
dock_anaconda-lasercannon
dock_anaconda-lasercannon2
dock_anaconda-military
dock_anaconda-mms
dock_anaconda-performance
dock_anaconda-pirate-lasercannon
dock_anaconda-pirate-lasercannon2
dock_anaconda-service
dock_anaconda_ptt
dock_aquatics_conger
dock_aquatics_fugu
dock_aquatics_manOWar
dock_arafura
dock_arafura_ind
dock_arafura_s1
dock_arafura_s1b
dock_arafura_s5a
dock_att1
dock_b_neoship_python-cruiser
dock_boa
dock_boa-civilian
dock_boa-clipper
dock_boa-cruiser-mms
dock_boa-dualcannon-mms
dock_boa-lasercannon
dock_boa-military
dock_boa-militcannon-mms
dock_boa-mk2
dock_boa-mk2-lasercannon
dock_boa-mk2-pirate-lasercannon
dock_boa-mk2-pirate-lasercannon2
dock_boa-mk2_ptt
dock_boa-mms
dock_boa-performance
dock_boa-pirate
dock_boa-pirate-lasercannon
dock_boa-pirate-lasercannon2
dock_boa-service
dock_boa-sg
dock_boa3_ptt
dock_boa4_ptt
dock_boa_ptt
dock_boamk2-civilian
dock_boamk2-military
dock_boamk2-service
dock_boamk2-transit
dock_classic_anaconda
dock_classic_anaconda-C
dock_classic_anaconda-pirate
dock_classic_boa
dock_classic_boa-C
dock_classic_boa-D
dock_classic_boa-mk2
dock_classic_boa-mk2-C
dock_classic_boa-mk2-pirate
dock_classic_boa-pirate
dock_classic_python
dock_classic_python-E
dock_classic_python-blackdog
dock_classic_python-cc
dock_classic_python-cc_pirate
dock_classic_python-et
dock_classic_python-et_pirate
dock_classic_python-trader
dock_classic_python_D
dock_deepspace_anaconda
dock_deepspace_anaconda-pirate
dock_deepspace_boa
dock_deepspace_boa-mk2
dock_deepspace_boa-mk2-pirate
dock_deepspace_boa-pirate
dock_deepspace_python
dock_deepspace_python-blackdog
dock_deepspace_python-trader
dock_dtt_atlas_cargo
dock_ferdelance3G_hardTrader3G+Escorted
dock_ferdelance3G_hardTrader3G+EscortedVariant
dock_ferdelance3G_hardTraderEscorted
dock_ferdelance3G_hardTraderEscortedVariant
dock_ferdelance3G_hardTraderEscortedVariant2
dock_ferdelance3G_hardestHunterEscorted
dock_ferdelance3G_pirateKing
dock_griff_anaconda-NPC
dock_griff_anaconda_alt-NPC
dock_griff_anaconda_alt2-NPC
dock_griff_anaconda_pirate-NPC
dock_griff_anaconda_pirate_alt-NPC
dock_griff_anaconda_pirate_alt2-NPC
dock_griff_boa-NPC
dock_griff_boa_MK2-NPC
dock_griff_boa_MK2_alt-NPC
dock_griff_boa_MK2_alt2-NPC
dock_griff_boa_MK2_alt3-NPC
dock_griff_boa_MK2_pirate-NPC
dock_griff_boa_MK2_pirate_alt-NPC
dock_griff_boa_MK2_pirate_alt2-NPC
dock_griff_boa_alt-NPC
dock_griff_boa_alt2-NPC
dock_griff_boa_alt3-NPC
dock_griff_boa_pirate-NPC
dock_griff_boa_pirate_alt-NPC
dock_griff_boa_pirate_alt2-NPC
dock_griff_cobra_Mk1_alt-NPC
dock_griff_moray-NPC
dock_griff_prototype_boa-NPC
dock_griff_prototype_boa-NPC_Pirate
dock_griff_python_alt-NPC
dock_griff_python_blackdog-NPC
dock_griff_python_blackdog_alt-NPC
dock_griff_python_blackdog_alt2-NPC
dock_griff_python_blackdog_alt3-NPC
dock_griff_python_trader-NPC
dock_griff_python_trader_alt-NPC
dock_griff_python_trader_alt2-NPC
dock_griff_python_trader_alt3-NPC
dock_griff_python_trader_alt4-NPC
dock_hard-anaconda
dock_hard-boa
dock_hard-boa2
dock_hardanacondax
dock_hardanacondax-pirate
dock_hardboa-pirate
dock_hardboa2-pirate
dock_hardpython-pirate
dock_himsn_boa_mk2
dock_hornet_alt_npc
dock_ionics_redback-pirate
dock_ionics_redback-trader
dock_ionics_redback_mission
dock_ionics_rlf_leader
dock_kirin-cv
dock_kirin-hunter
dock_kirin-m
dock_kirin-xm
dock_medical_anaconda
dock_neo_anaconda
dock_neo_boa
dock_neo_boa-mk2
dock_neo_python-blackdog
dock_neo_python-trader
dock_noshaders_lira-trader_normal_clean
dock_noshaders_lira-trader_normal_colonial
dock_noshaders_lira-trader_normal_green
dock_noshaders_lira-trader_normal_grey
dock_oolite_template_anaconda
dock_oolite_template_anaconda-pirate
dock_oolite_template_asp-cloaked
dock_oolite_template_boa
dock_oolite_template_boa-mk2
dock_oolite_template_boa-mk2-pirate
dock_oolite_template_boa-pirate
dock_oolite_template_python
dock_oolite_template_python-blackdog
dock_oolite_template_python-trader
dock_phkb_anaconda_1
dock_phkb_anaconda_10
dock_phkb_anaconda_10_pirate
dock_phkb_anaconda_11
dock_phkb_anaconda_11_pirate
dock_phkb_anaconda_12
dock_phkb_anaconda_12_pirate
dock_phkb_anaconda_13
dock_phkb_anaconda_13_pirate
dock_phkb_anaconda_14
dock_phkb_anaconda_14_pirate
dock_phkb_anaconda_15
dock_phkb_anaconda_15_pirate
dock_phkb_anaconda_16
dock_phkb_anaconda_16_pirate
dock_phkb_anaconda_17
dock_phkb_anaconda_17_pirate
dock_phkb_anaconda_18
dock_phkb_anaconda_18_pirate
dock_phkb_anaconda_19
dock_phkb_anaconda_19_pirate
dock_phkb_anaconda_1_pirate
dock_phkb_anaconda_2
dock_phkb_anaconda_20
dock_phkb_anaconda_20_pirate
dock_phkb_anaconda_21
dock_phkb_anaconda_21_pirate
dock_phkb_anaconda_22
dock_phkb_anaconda_22_pirate
dock_phkb_anaconda_23
dock_phkb_anaconda_23_pirate
dock_phkb_anaconda_24
dock_phkb_anaconda_24_pirate
dock_phkb_anaconda_25
dock_phkb_anaconda_25_pirate
dock_phkb_anaconda_26
dock_phkb_anaconda_26_pirate
dock_phkb_anaconda_27
dock_phkb_anaconda_27_pirate
dock_phkb_anaconda_28
dock_phkb_anaconda_28_pirate
dock_phkb_anaconda_29
dock_phkb_anaconda_29_pirate
dock_phkb_anaconda_2_pirate
dock_phkb_anaconda_3
dock_phkb_anaconda_30
dock_phkb_anaconda_30_pirate
dock_phkb_anaconda_31
dock_phkb_anaconda_31_pirate
dock_phkb_anaconda_32
dock_phkb_anaconda_32_pirate
dock_phkb_anaconda_33
dock_phkb_anaconda_33_pirate
dock_phkb_anaconda_34
dock_phkb_anaconda_34_pirate
dock_phkb_anaconda_35
dock_phkb_anaconda_35_pirate
dock_phkb_anaconda_36
dock_phkb_anaconda_36_pirate
dock_phkb_anaconda_3_pirate
dock_phkb_anaconda_4
dock_phkb_anaconda_4_pirate
dock_phkb_anaconda_5
dock_phkb_anaconda_5_pirate
dock_phkb_anaconda_6
dock_phkb_anaconda_6_pirate
dock_phkb_anaconda_7
dock_phkb_anaconda_7_pirate
dock_phkb_anaconda_8
dock_phkb_anaconda_8_pirate
dock_phkb_anaconda_9
dock_phkb_anaconda_9_pirate
dock_phkb_boa-mk2_1
dock_phkb_boa-mk2_10
dock_phkb_boa-mk2_10_pirate
dock_phkb_boa-mk2_11
dock_phkb_boa-mk2_11_pirate
dock_phkb_boa-mk2_12
dock_phkb_boa-mk2_12_pirate
dock_phkb_boa-mk2_13
dock_phkb_boa-mk2_13_pirate
dock_phkb_boa-mk2_14
dock_phkb_boa-mk2_14_pirate
dock_phkb_boa-mk2_15
dock_phkb_boa-mk2_15_pirate
dock_phkb_boa-mk2_16
dock_phkb_boa-mk2_16_pirate
dock_phkb_boa-mk2_17
dock_phkb_boa-mk2_17_pirate
dock_phkb_boa-mk2_18
dock_phkb_boa-mk2_18_pirate
dock_phkb_boa-mk2_19
dock_phkb_boa-mk2_19_pirate
dock_phkb_boa-mk2_1_pirate
dock_phkb_boa-mk2_2
dock_phkb_boa-mk2_20
dock_phkb_boa-mk2_20_pirate
dock_phkb_boa-mk2_21
dock_phkb_boa-mk2_21_pirate
dock_phkb_boa-mk2_22
dock_phkb_boa-mk2_22_pirate
dock_phkb_boa-mk2_23
dock_phkb_boa-mk2_23_pirate
dock_phkb_boa-mk2_24
dock_phkb_boa-mk2_24_pirate
dock_phkb_boa-mk2_25
dock_phkb_boa-mk2_25_pirate
dock_phkb_boa-mk2_26
dock_phkb_boa-mk2_26_pirate
dock_phkb_boa-mk2_27
dock_phkb_boa-mk2_27_pirate
dock_phkb_boa-mk2_28
dock_phkb_boa-mk2_28_pirate
dock_phkb_boa-mk2_29
dock_phkb_boa-mk2_29_pirate
dock_phkb_boa-mk2_2_pirate
dock_phkb_boa-mk2_3
dock_phkb_boa-mk2_30
dock_phkb_boa-mk2_30_pirate
dock_phkb_boa-mk2_31
dock_phkb_boa-mk2_31_pirate
dock_phkb_boa-mk2_32
dock_phkb_boa-mk2_32_pirate
dock_phkb_boa-mk2_33
dock_phkb_boa-mk2_33_pirate
dock_phkb_boa-mk2_34
dock_phkb_boa-mk2_34_pirate
dock_phkb_boa-mk2_35
dock_phkb_boa-mk2_35_pirate
dock_phkb_boa-mk2_36
dock_phkb_boa-mk2_36_pirate
dock_phkb_boa-mk2_3_pirate
dock_phkb_boa-mk2_4
dock_phkb_boa-mk2_4_pirate
dock_phkb_boa-mk2_5
dock_phkb_boa-mk2_5_pirate
dock_phkb_boa-mk2_6
dock_phkb_boa-mk2_6_pirate
dock_phkb_boa-mk2_7
dock_phkb_boa-mk2_7_pirate
dock_phkb_boa-mk2_8
dock_phkb_boa-mk2_8_pirate
dock_phkb_boa-mk2_9
dock_phkb_boa-mk2_9_pirate
dock_phkb_boa_1
dock_phkb_boa_10
dock_phkb_boa_10_pirate
dock_phkb_boa_11
dock_phkb_boa_11_pirate
dock_phkb_boa_12
dock_phkb_boa_12_pirate
dock_phkb_boa_13
dock_phkb_boa_13_pirate
dock_phkb_boa_14
dock_phkb_boa_14_pirate
dock_phkb_boa_15
dock_phkb_boa_15_pirate
dock_phkb_boa_16
dock_phkb_boa_16_pirate
dock_phkb_boa_17
dock_phkb_boa_17_pirate
dock_phkb_boa_18
dock_phkb_boa_18_pirate
dock_phkb_boa_19
dock_phkb_boa_19_pirate
dock_phkb_boa_1_pirate
dock_phkb_boa_2
dock_phkb_boa_20
dock_phkb_boa_20_pirate
dock_phkb_boa_21
dock_phkb_boa_21_pirate
dock_phkb_boa_22
dock_phkb_boa_22_pirate
dock_phkb_boa_23
dock_phkb_boa_23_pirate
dock_phkb_boa_24
dock_phkb_boa_24_pirate
dock_phkb_boa_25
dock_phkb_boa_25_pirate
dock_phkb_boa_26
dock_phkb_boa_26_pirate
dock_phkb_boa_27
dock_phkb_boa_27_pirate
dock_phkb_boa_28
dock_phkb_boa_28_pirate
dock_phkb_boa_29
dock_phkb_boa_29_pirate
dock_phkb_boa_2_pirate
dock_phkb_boa_3
dock_phkb_boa_30
dock_phkb_boa_30_pirate
dock_phkb_boa_31
dock_phkb_boa_31_pirate
dock_phkb_boa_32
dock_phkb_boa_32_pirate
dock_phkb_boa_33
dock_phkb_boa_33_pirate
dock_phkb_boa_34
dock_phkb_boa_34_pirate
dock_phkb_boa_35
dock_phkb_boa_35_pirate
dock_phkb_boa_36
dock_phkb_boa_36_pirate
dock_phkb_boa_3_pirate
dock_phkb_boa_4
dock_phkb_boa_4_pirate
dock_phkb_boa_5
dock_phkb_boa_5_pirate
dock_phkb_boa_6
dock_phkb_boa_6_pirate
dock_phkb_boa_7
dock_phkb_boa_7_pirate
dock_phkb_boa_8
dock_phkb_boa_8_pirate
dock_phkb_boa_9
dock_phkb_boa_9_pirate
dock_python
dock_python-battlecruiser-mms
dock_python-blackdog
dock_python-blackdog-lasercannon
dock_python-blackdog-lasercannon2
dock_python-civilian
dock_python-clipper
dock_python-clipper-alternative
dock_python-lasercannon
dock_python-military
dock_python-mms
dock_python-service
dock_python-sg
dock_python-sg-mms
dock_python-staer9-mms
dock_python-trader
dock_python-trader-lasercannon
dock_python-trader-lasercannon2
dock_python-transit
dock_python2_ptt
dock_python3_ptt
dock_python_ptt
dock_python_red-mms
dock_python_x
dock_random_hits_big_boss_cobra4_spacelane
dock_random_hits_big_boss_iguana_spacelane
dock_random_hits_big_boss_impcourier_spacelane
dock_random_hits_big_boss_imptrader_spacelane
dock_random_hits_big_boss_pitviper_spacelane
dock_random_hits_big_boss_supercobra_spacelane
dock_random_hits_big_boss_vamppurg_spacelane
dock_random_hits_big_boss_wolfmk2SE_spacelane
dock_random_hits_mark_anaconda
dock_random_hits_mark_arachnid
dock_random_hits_mark_asp
dock_random_hits_mark_aspmk1
dock_random_hits_mark_aspspec
dock_random_hits_mark_boa
dock_random_hits_mark_boa2
dock_random_hits_mark_bushmaster
dock_random_hits_mark_cat
dock_random_hits_mark_chameleon
dock_random_hits_mark_cobra3
dock_random_hits_mark_cobra3courier
dock_random_hits_mark_cobra3rapier
dock_random_hits_mark_cobra4
dock_random_hits_mark_cobras9
dock_random_hits_mark_cutlass
dock_random_hits_mark_dttmk1
dock_random_hits_mark_ferdelance
dock_random_hits_mark_galtech_escort_fighter
dock_random_hits_mark_ghavial
dock_random_hits_mark_gnat
dock_random_hits_mark_igu
dock_random_hits_mark_imp
dock_random_hits_mark_imptrader
dock_random_hits_mark_monitor
dock_random_hits_mark_monitor2
dock_random_hits_mark_mussurana
dock_random_hits_mark_pcc
dock_random_hits_mark_phaze
dock_random_hits_mark_pitviper
dock_random_hits_mark_python
dock_random_hits_mark_pythonet
dock_random_hits_mark_revenge1
dock_random_hits_mark_revenge10
dock_random_hits_mark_revenge11
dock_random_hits_mark_revenge13
dock_random_hits_mark_revenge14
dock_random_hits_mark_revenge15
dock_random_hits_mark_revenge2
dock_random_hits_mark_revenge3
dock_random_hits_mark_revenge4
dock_random_hits_mark_revenge5
dock_random_hits_mark_revenge6
dock_random_hits_mark_revenge7
dock_random_hits_mark_revenge8
dock_random_hits_mark_revenge9
dock_random_hits_mark_salamander
dock_random_hits_mark_supercobra
dock_random_hits_mark_vamppurg
dock_random_hits_mark_wolf
dock_rhs_big_boss_cobra4_spacelane_shipset
dock_rhs_big_boss_iguana_spacelane_shipset
dock_rhs_big_boss_impcourier_spacelane_shipset
dock_rhs_big_boss_imptrader_spacelane_shipset
dock_rhs_big_boss_pitviper_spacelane_shipset
dock_rhs_big_boss_supercobra_spacelane_shipset
dock_rhs_big_boss_vamppurg_spacelane_shipset
dock_rhs_big_boss_wolfmk2SE_spacelane_shipset
dock_rhs_mark_arachnid_shipset
dock_rhs_mark_aspmk1_shipset
dock_rhs_mark_cobra3courier_shipset
dock_rhs_mark_cobra3rapier_shipset
dock_rhs_mark_cobra4_shipset
dock_rhs_mark_cobras9_shipset
dock_rhs_mark_drake2_shipset
dock_rhs_mark_dttmk1_shipset
dock_rhs_mark_galtech_escort_fighter_shipset
dock_rhs_mark_ghavial_shipset
dock_rhs_mark_gnat_shipset
dock_rhs_mark_imp_shipset
dock_rhs_mark_imptrader_shipset
dock_rhs_mark_phaze_shipset
dock_rhs_mark_pitviper_shipset
dock_rhs_mark_supercobra_shipset
dock_rhs_mark_vamppurg_shipset
dock_rhs_mark_wolf_shipset
dock_sb_neoship_monitor
dock_sb_neoship_python-cruiser
dock_staer9_monitor
dock_staer9_monitor-pirate
dock_staer9_monitor2
dock_staer9_monitor2-pirate
dock_staer9_python-cruiser
dock_staer9_python-cruiser-pirate
dock_staer9_python-x
dock_staer9_python-x-pirate
dock_taurockhopper
dock_ups-sun-anaconda
dock_ups-sun-boa-mk2
dock_ups-sun2-anaconda
dock_vector
dock_vector_benus
dock_vector_frood
dock_vector_geek
dock_vector_millionaire
dock_vortex-maelstrom-NPC
dock_wildShips_chatu
dock_wildShips_chatu_trader
dock_wildShips_tembo
dock_wildShips_tribalChatu

Models

This expansion declares no models.

Scripts

Path
Scripts/stationdockcontrol.js
(function () {
	"use strict";
	this.name = "StationDockControl";
	this.author = "phkb";
	this.copyright = "2015 phkb";
	this.description = "Displays list of ships docked at the current station, as well as controlling when ships launch.";
	this.license = "CC BY-NC-SA 3.0";

	/*
		This OXP attempts to provide a list of docked ships at most stations. Ships in this list will launch when their allocated 
		launch slot becomes current, and ships that dock at the station are added to the list. See the readme.txt file for 
		more information.

		Future ideas:
		- Having some traders head to other trade stations in system, rather than just jumping out
		- Change $clearLaunchedShips to move ships to their destination system instead of just removing them, possibly dropping a couple of 
			escorts or group members based on system danger level
		- if their destination system is the player's system, then create them as ships in system after the player arrives.
		- Look into ships entering wormholes, and decide how to process:
			- need to wait until ship becomes invalid (which isn't as soon as they enter the wormhole)
			- if the wormhole collapses, make a note of all the ships that entered and dock them at the destination station 
				(maybe drop a couple of escorts or group members based on system danger level)
			- if the wormhole destination is the same as the players, possibly add the ships to the system just before the player arrives, 
				but some distance along the wp-planet lane.
				- again need to check for invalid, as the core will do this itself if they're still valid when the player arrives
		- Make player ship move from Docking to Unloading to Docked
		- Announce arrivals via console messages (ie. "Cobra Mark III: The Iconic has just docked in docking bay CX-123", 
			"Anaconda (plus escorts) departing in 10 minutes")
		- Give player ability to log an alert when a ship changes their destination or departure time
		- Give player ability to make a note of a ship, and when it reaches 10 minutes to departure, alert the player
		- Change commodity levels based on docking/departing ships. Take cargo from docking ships and add to manifest screen, take cargo 
			from manifest screen when ships are in "loading" status. Adjust prices as well.
			- will need some sort of AI to decide how this works on non-main stations.

		Limitations:
		1. The method chosen to force Oolite to create a single ship (without any escorts) involves creating a shipdata.plist 
			entry for each ship that might have escorts.
			This means, if a ship that is unknown to this OXP is added to Oolite, there many be cases where ships are added to the 
			system that don't appear on the traffic control list.

		ISSUES:
		- Occasionally seeing launches that didn't come from the list (eg Hognose evangelists). 
			Where are they coming from, and can/should this be addressed?
		- Some shipdata.plist entries don't have a "auto_ai" value set at all - it's "undefined". In these instances, I'm making 
			the assumption that this means the ship can have an auto ai assigned to it.
		- Because we are creating ships based on data keys, not roles, we need to be a bit more explicit when it comes to AI settings. 
			So we are generally setting the AI for every ship we create, even standard traders, to ensure everything has an AI.
		- Lone escorts that dock with a station will be switched to "trader" if they have cargo space and a hyperdrive. If they 
			have no cargo but only a hyperdrive, they'll choose to become a "trader-courier". Otherwise they'll dry-dock their ship 
			and get some R&R (ship entry will be removed)

		TESTS:
		1. Station traffic at main stations in different systems is equivalent to original
		2. Traffic at non-main stations is equivalent
		3. Traffic at hasNPCTraffic = no is equivalent
		4. Balance of ships is not affected too much
		5. Test what happens to groups/escorts when they dock - does the system pick up all the group info correctly (leader, members 
			appear after leader, difference between escort and group)
		6. Memory footprint - how much memory is being used by the data requirements, how do you test memory usage in Oolite. Task Manager?

	*/
	this._disable = false; // quick way to disable the OXP without removing it.
	this._disableQueues = false; // flag to stop the population of all station dock queues
	this._systemDockingData = []; // main dataset
	this._stationAdjust = false; // flag to determine whether all system stations have had the new "otherShipDocked" function added to them
	this._rsnInstalled = false; // indicates whether randomshipnames OXP is installed
	this._launching = []; // subset of main array used for launching ships
	this._docking = []; // list of ships that are currently attempting to dock with the players docked station
	this._autoGenerationInProgress = false; // indicates that the process of auto-generating docked ships is in progress
	this._stationBayCodes = []; // holding array of station docking bay codes, used during an auto-generation process
	this._quickUpdateTimer = null;
	this._launchTimer = null; // timer to move ships to the launching array
	this._launchListUpdating = false; // flag to indicate the launching list is updating
	this._jumpStarted = false;
	this._randomShips = {}; // array of ship roles, types and data keys
	this._shipData = {};
	this._launchingGroups = []; // used to hold groups being launched at stations
	this._launchingEscorts = []; // used to hold escort leaders being launched at stations
	this._stationList = []; // list of stations and indexes for the current system
	this._shuttleDest = null; // list of in-system destinations (stations) shuttles can be sent to
	this._stationIndexesLoaded = false; // flag to indicate when the station indexes have been loaded
	this._stationIndexCount = 0;
	this._neighbours = null; // array of neighbouring systems
	this._populatorVars = null;
	this._repopulate = false; // flag to determine whether a repopulate process will take place (usually after a large time jump)
	this._running = false;
	this._repopulateFactor = 0; // determines how deep into the dock list ships will be added on repopulate
	this._repopulateStation = null; // object to hold new station which didn't exist before but now needs to be populated
	this._populateStation = [];
	this._populateTimer = null;
	this._adjustDepartureBands = 0; // counter to determine when to run the adjust departure routine
	this._escortCounter = 1; // internal reference counter for escorts
	this._groupCounter = 1; // internal reference counter for groups
	this._shipMonitor = 0; // variable to keep track of when to do a population count to the log file, and also assess station docks
	this._smuggling = false; // flag to indicate the smuggling OXP is in use
	this._lateStations = []; // array of stations created after systemWillPopulate to be processed by the dock populator
	this._limit = 0;
	this._skilledNPCs = false;
	//this._doShipDockDataGrab = {};
	//=============================================================================================================
	// user settings
	this._debug = false; // set to true to output various messages to the log, false will disable all logging messages
	this._logLaunchType = 0; // how to log launch events: 0 = not logged, 1 = minimal, 2 = full, 3 = turn on AI messages for launched ships
	this._logDockingType = 0; // how to log docking events: 0 = not logged, 1 = minimal, 2 = full
	this._logAdjustmentType = 0; // how to log adjustments: 0 = not logged, 1 = type only, 2 = full
	this._logPopulatorType = 0; // how to log populator events: 0 = not logged, 1 = minimal, 2 = full, 3 = full + escort definition data
	// The following settings can be adjusted to effect the number of ships added to the docking list.
	// See the this.$populateStationList function for more information on what is happening with these numbers
	this._traderSkipVesselFactor = 0; // increase to reduce the number of times a potential new trader vessel will be skipped.
	//      0 = no chance of skipping vessels (disabled)
	//		1 = high chance of skipping vessels
	//		2
	//		3
	//		4
	//		... lower and lower chance of skipping vessels
	this._pirateSkipVesselFactor = 5; // increase to reduce the number of times a potential new pirate group will be skipped.
	this._hunterSkipVesselFactor = 8; // increase to reduce the number of times a potential new hunter group will be skipped.
	// these next 5 variables control the frequency and size of adjustments to the docking lists
	this._adjustDepartureChance = 0.3; // decimal between 0 and 1, higher number means more ships will move from "docked" to "loading" or from "loading" to departing
	this._adjustRuns = 2; // how many adjustment runs to do on each cycle (will reduce to 1 when in flight)
	this._adjustDepartureFreq = 0; // number greater than or equal to 0. determines the frequency of the adjust Departure routine.
	// 0 means every 20 seconds (after 1 systemWillRepopulate cycles, ie every time)
	// 1 means every 40 seconds (after 2 systemWillRepopulate cycles)
	// 2 means every 60 seconds (after 3 systemWillRepopulate cycles)
	this._maxAdjustments = 3; // maximum possible number of adjustments to do on any adjustment cycle
	this._noAdjustPeriod = 600; // the number of seconds that must elapse before a ship can be adjusted again (default is 600 = 10 minutes)
	// types of adjustment functions to select from:
	// 0 = do nothing,
	// 1 = docking to unloading, unloading to docked, docked to loading, loading to departing,
	// 2 = adjust departure time,
	// 3 = adjust destination
	this._adjustFunctions = [1, 0, 1, 1, 1, 2, 0, 2, 2, 0, 2, 2, 0, 2, 2, 3, 3];
	// these next items control the amount of traffic that will be generated at the different stations.
	// the default is "High", and any station that doesn't fit any of the roles below will default to this.
	// note: if a station has the "has_npc_traffic" flag set to false, it will override whatever is in this list
	this._trafficMedium = ["constore", "random_hits_any_spacebar", "sfep_station", "wildShips_kiota2Disc", "wildShips_kiota4Disc",
		"wildShips_kiota4Sphere", "wildShips_kiota2Sphere"
	];
	this._trafficLow = ["rrs_group_spacestation", "rrs_astromine", "rrs-mining-outpost", "casinoship", "comczgf", "bigTrader",
		"ups_dependance", "ups_dependance2", "blackmonk_monastery", "aquatics_HQ", "wildShips_kiota2Comms", "wildShips_kiota4Comms",
		"wildShips_kiota2Spur", "wildShips_kiota4Solar", "wildShips_kiota2Solar", "astrofactory", "free_trade_zone", "togy_station", "tgy_station",
		"taxi_station"
	];
	this._trafficVeryLow = ["rockhermit", "rockhermit-chaotic", "rockhermit-pirate", "repaired-buoy-station", "anarchies_renegade_station",
		"anarchies_salvage_gang", "GW-rock-hermit"
	];
	this._trafficNone = ["slaver_base", "rrs_slaverbase", "anarchies_hacker_outpost", "anarchies_sentinel_station", "bigTrader",
		"thecollector_adck_coriolis-station", "jaguar_company_base", "GW-fuel-processor", "GW-comms-station", "laveAcademy_academy",
		"planetFall_surface", "generationship", "IST_genship carrier", "behemoth"
	];
	this._shuttleTrafficNone = ["pirate-cove", "slaver_base", "rrs_slaverbase", "anarchies_hacker_outpost", "anarchies_sentinel_station",
		"bigTrader", "thecollector_adck_coriolis-station", "jaguar_company_base", "GW-fuel-processor", "GW-comms-station", "generationship",
		"IST_genship carrier", "rockhermit", "rockhermit-chaotic", "rockhermit-pirate"
	];
	// this is the list of primary roles we will be looking after as part of this OXP
	// that is, we will create ships with these primary roles. Docking ships must have one of these primary roles for them to appear in the dock list after docking
	this._controlledRoles = ["trader", "trader-courier", "trader-smuggler", "shuttle",
		"pirate", "pirate-light-fighter", "pirate-medium-fighter", "pirate-heavy-fighter", "pirate-interceptor",
		"pirate-light-freighter", "pirate-medium-freighter", "pirate-heavy-freighter",
		"hunter", "hunter-medium", "hunter-heavy", "assassin-light", "assassin-medium", "assassin-heavy",
		"escort", "escort-medium", "escort-heavy" // removed escort-light
	];
	// ships with these roles will be included in all docking processes
	this._dockingRoleExceptions = ["strelka", "trident"];
	// ships with these keys will not be created directly (although they may still dock)
	this._dockingKeyExclusions = ["convoys-adder", "convoys-anaconda", "convoys-anaconda2", "convoys-anaconda3", "convoys-anaconda-admiral",
		"convoys-anaconda-pirate", "convoys-asp", "convoys-asp-pirate", "convoys-boa", "convoys-boa-pirate", "convoys-boa-mk2", "convoys-boa-mk2-pirate",
		"convoys-cobra3", "convoys-cobra3-pirate", "convoys-ferdelance", "convoys-ferdelance-pirate", "convoys-gecko-pirate", "convoys-krait",
		"convoys-krait-pirate", "convoys-mamba", "convoys-moray", "convoys-moray-pirate", "convoys-moray-morayMED", "convoys-python", "convoys-python-team",
		"convoys-python-pirate", "convoys-python-blackdog", "convoys-shuttle", "convoys-transporter", "convoys-transporter-miner", "convoys-worm",
		"convoys-worm-miner", "freighterconvoys-anaconda", "freighterconvoys-anaconda2", "freighterconvoys-anaconda3", "freighterconvoys-anaconda4",
		"freighterconvoys-boa", "freighterconvoys-boa-mk2", "freighterconvoys-cobra3", "freighterconvoys-moray", "freighterconvoys-morayMED",
		"freighterconvoys-python", "freighterconvoys-python-team", "freighterconvoys-python-trader", "freighterconvoys-shuttle", "freighterconvoys-shuttle2",
		"freighterconvoys-transporter", "freighterconvoys-transporter-miner", "freighterconvoys-worm", "freighterconvoys-worm-miner", "freighterconvoys-miner",
		"anaconda-carrier-pirate", "anaconda-carrier", "empty-carrier-pirate", "cobra3-carrier-pirate", "cobra3-carrier",
		"cobra1-carrier-pirate", "cobra1-carrier", "ferdelance-carrier", "ferdelance-carrier-pirate"
	];
	// these roles will switch any attached escorts into a ship group instead
	this._switchEscortsToGroup = ["rhs_big_boss_cobra4_spacelane_shipset", "rhs_big_boss_iguana_spacelane_shipset", "rhs_big_boss_impcourier_spacelane_shipset",
		"rhs_big_boss_imptrader_spacelane_shipset", "rhs_big_boss_pitviper_spacelane_shipset", "rhs_big_boss_supercobra_spacelane_shipset", "rhs_big_boss_vamppurg_spacelane_shipset",
		"rhs_big_boss_wolfmk2SE_spacelane_shipset", "rhs_mark_arachnid_shipset", "rhs_mark_aspmk1_shipset", "rhs_mark_cobra3courier_shipset", "rhs_mark_cobra3rapier_shipset",
		"rhs_mark_cobra4_shipset", "rhs_mark_cobras9_shipset", "rhs_mark_drake2_shipset", "rhs_mark_dttmk1_shipset", "rhs_mark_galtech_escort_fighter_shipset",
		"rhs_mark_ghavial_shipset", "rhs_mark_gnat_shipset", "rhs_mark_imp_shipset", "rhs_mark_imptrader_shipset", "rhs_mark_phaze_shipset", "rhs_mark_pitviper_shipset",
		"rhs_mark_supercobra_shipset", "rhs_mark_vamppurg_shipset", "rhs_mark_wolf_shipset"
	];
	// these script properties will be maintained from dock to relaunch
	this._propertiesToKeep = [];
	this._AIScriptExceptions = {};

	// these are roles we don't control directly, but we might need to dock (eg special escort roles)
	// they will be added during play
	this._additionalRoles = [];
	this._ignoreEquip = ["EQ_ILS"];
	this._pendingDock = []; // array of data objects to search for when ships dock, so a third party OXP can be informed that a specific ship is now docked
	this._trueValues = ["yes", "1", 1, "true", true];
	this._stationNullError = [];
	this._cleanUp = 0;
	this._cleanUpComplete = false;

	// mix of numbers to balance how many in each section
	// band 1 < 3 minutes >> no longer used (creates too many launches)
	// band 2 3-15 minutes
	// band 3 15-30 mins
	// band 4 30-60 mins
	// band 5 1 - 2 hours
	// band 6 2 - 12 hours
	// band 7 12 - 16 hours (loading status)
	// band 8 16 - 20 hours (docked status)
	// band 9 20 - 23 hours (unloading status)
	// band 10 23 - 24 hours (docking status)
	this._bandSrc = {
		2: [2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
			3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
			4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
			5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
			6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
			7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
			8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
			9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
			10, 10, 10
		],
		3: [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
			4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
			5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
			6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
			7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
			8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
			9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
			10, 10, 10
		],
		4: [4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
			5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
			6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
			7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
			8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
			9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
			10, 10, 10
		],
		5: [5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
			6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
			7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
			8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
			9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
			10, 10, 10
		],
		6: [6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
			7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
			8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
			9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
			10, 10, 10
		],
		7: [7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
			8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
			9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
			10, 10, 10
		],
		8: [8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
			9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
			10, 10, 10
		],
		9: [9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
			10, 10, 10
		],
		10: [10, 10, 10],
	};
	this._bandRng = {
		1: {
			low: 1,
			high: 3
		},
		2: {
			low: 3,
			high: 15
		},
		3: {
			low: 15,
			high: 30
		},
		4: {
			low: 30,
			high: 60
		},
		5: {
			low: 60,
			high: 120
		},
		6: {
			low: 120,
			high: 720
		},
		7: {
			low: 720,
			high: 960
		},
		8: {
			low: 960,
			high: 1200
		},
		9: {
			low: 1200,
			high: 1380
		},
		10: {
			low: 1380,
			high: 1440
		},
		11: {
			low: 1440,
			high: 1680
		}
	};
	this._departureBands = [];

	// configuration settings for use in Lib_Config
	this._sdcConfig = {
		Name: this.name,
		Display: "Debug Options",
		Alias: "Station Dock Control",
		Alive: "_sdcConfig",
		Bool: {
			B0: {
				Name: "_debug",
				Def: false,
				Desc: "Enable debug messages"
			},
			Info: "Turns on debug messages."
		},
		SInt: {
			S0: {
				Name: "_logLaunchType",
				Def: 0,
				Desc: "Debug: Launch info",
				Min: 0,
				Max: 3
			},
			S1: {
				Name: "_logDockingType",
				Def: 0,
				Desc: "Debug: Docking info",
				Min: 0,
				Max: 2
			},
			S2: {
				Name: "_logAdjustmentType",
				Def: 0,
				Desc: "Debug: Adjustments info",
				Min: 0,
				Max: 2
			},
			S3: {
				Name: "_logPopulatorType",
				Def: 0,
				Desc: "Debug: Populator info",
				Min: 0,
				Max: 3
			},
			Info: "0 - Debug launch messages. 0 = none, 1 = min, 2 = full, 3 - inc AI msgs\n1 - Debug docking messages. 0 = none, 1 = min, 2 = full\n2 - Debug adjustment messages. 0 = none, 1 = min, 2 = full\n3 - Debug populator messages. 0 = none, 1 = min, 2 = full, 3 = full + escort info"
		}
	};

	// dictionary of stationName/shipTypes with a boolean value indicating if the ship cannot dock at that station.
	// no value means ship can dock
	// this dictionary is being manually populated to increase performance
	this._shipFits = {
		"A Seedy Space Bar|Andromeda": 0,
		"Astromine Penal Colony|Andromeda": 0,
		"Axtech Dodecahedron Station|Andromeda": 0,
		"Capt Kev Dodecahedron Station|Andromeda": 0,
		"Collective SLAPU|Andromeda": 0,
		"Collective ZGF|Andromeda": 0,
		"Constellation Icosahedron Station|Andromeda": 0,
		"Coriolis Station|Andromeda": 0,
		"Dodecahedron Station|Andromeda": 0,
		"DS-A1 Coriolis Station|Andromeda": 0,
		"DS-A1 Dodecahedron Station|Andromeda": 0,
		"DS-A1 Icosahedron Station|Andromeda": 0,
		"Free Trade Zone|Andromeda": 0,
		"FrontierTech Worldranger Icosahedron Station|Andromeda": 0,
		"GalaxyMart Con Store|Andromeda": 0,
		"GASECBlue Dodecahedron Station|Andromeda": 0,
		"Globe Station II|Andromeda": 0,
		"Globe Station III|Andromeda": 0,
		"Greenline Coriolis Station|Andromeda": 0,
		"GRS Buoy Factory|Andromeda": 0,
		"G-X1 Coriolis Station|Andromeda": 0,
		"G-X1 Icosahedron Station|Andromeda": 0,
		"G-Z1 Dodecahedron Station|Andromeda": 0,
		"G-Z1 Icosahedron Station|Andromeda": 0,
		"G-Z1 Octahedron Outpost|Andromeda": 0,
		"G-Z1 Tetrahedron Depot|Andromeda": 0,
		"G-Z2 Octahedron Outpost|Andromeda": 0,
		"G-Z2 Tetrahedron Depot|Andromeda": 0,
		"Habitat Mark II|Andromeda": 0,
		"Icosahedron Station|Andromeda": 0,
		"Imperial AstroFactory|Andromeda": 0,
		"Imperial Dodecahedron Station|Andromeda": 0,
		"Kiota Biosphere Station|Andromeda": 0,
		"Kiota Habitat Station|Andromeda": 0,
		"Kiota Manufacturing Station|Andromeda": 0,
		"Kiota Mega Habitat Station|Andromeda": 0,
		"Kiota Relay Station|Andromeda": 0,
		"Kiota Research Station|Andromeda": 0,
		"Kiota Solar Station|Andromeda": 0,
		"Leesti High|Andromeda": 0,
		"LX5 Dodecahedron Station|Andromeda": 0,
		"Mall-Wart Con Store|Andromeda": 0,
		"Mayan Dodecahedron Station|Andromeda": 0,
		"Metaforce Coriolis Station|Andromeda": 0,
		"M-G2 Globe Station|Andromeda": 0,
		"M-G3 Globe Station|Andromeda": 0,
		"Mining Outpost|Andromeda": 0,
		"M-T1 Torus Station|Andromeda": 0,
		"M-T2 Torus Station|Andromeda": 0,
		"N-A1 Coriolis Station|Andromeda": 0,
		"N-X1 Dodecahedron Station|Andromeda": 0,
		"Octahedron Outpost|Andromeda": 0,
		"Oodles Con Store|Andromeda": 0,
		"Pi-42 Con Store|Andromeda": 0,
		"Pirate Rock|Andromeda": 0,
		"RaTech Gold Coriolis Station|Andromeda": 0,
		"RedTec Coriolis Station|Andromeda": 0,
		"Renegade Station|Andromeda": 0,
		"Rock Hermit|Andromeda": 0,
		"Sainsboory's Con Store|Andromeda": 0,
		"Salvage Gang|Andromeda": 0,
		"Sodalite Station|Andromeda": 0,
		"SolarTec Coriolis Station|Andromeda": 0,
		"Star Con Store|Andromeda": 0,
		"Tescoo Con Store|Andromeda": 0,
		"Tetrahedron Depot|Andromeda": 0,
		"Torus Station|Andromeda": 0,
		"Trade Outpost|Andromeda": 0,
		"Trontech Vulcan Dodecahedron Station|Andromeda": 0,
		"Waspline Coriolis Station|Andromeda": 0,
		"Worldbuilders Icosahedron Station|Andromeda": 0,
		"Imperial AstroFactory|Serpent Class Cruiser": 0,
		"Imperial AstroFactory|Hornet": 0,
		"Imperial AstroFactory|Anaconda": 0,
		"Imperial AstroFactory|Boa Class Cruiser": 0,
		"Imperial AstroFactory|Boa": 0,
		"Imperial AstroFactory|Cobra Mark IV": 0,
	};

	//-------------------------------------------------------------------------------------------------------------
	// obj has following params:
	// 	shipName		name of ship to add menu item for (required if pilotname not specified)
	// 	shipType		ship type (required if pilotname not specified)
	// 	pilotName		name of pilot (optional, but required if shipname and shiptype are blank)
	// 	worldScript		worldScript name for the callback (required)
	// 	callback		worldScript function to call in the callback (required)
	// 	parameter		parameter to add to the function when called (optional)
	//  launchCallback	worldScript function to call when the ship launches after docking (optional)
	this.$awaitShipDocking = function $awaitShipDocking(obj) {
		if ((!obj.shipName || obj.shipName === "") && (!obj.pilotName || obj.pilotName === "")) {
			throw "Invalid settings: shipName or pilotName must be specified";
		}
		if ((!obj.shipType || obj.shipType === "") && (!obj.pilotName || obj.pilotName === "")) {
			throw "Invalid settings: shipType or pilotName must be specified";
		}
		if (!obj.worldScript || obj.worldScript === "") {
			throw "Invalid settings: worldScript (worldScript name) must be specified, and cannot be blank.";
		}
		if (!obj.callback || obj.callback === "") {
			throw "Invalid settings: callback (function name) must be specified, and cannot be blank.";
		}
		this._pendingDock.push({
			shipName: (obj.shipName ? obj.shipName : ""),
			shipType: (obj.shipType ? obj.shipType : ""),
			pilotName: (obj.pilotName ? obj.pilotName : ""),
			callback: obj.callback,
			worldScript: obj.worldScript,
			launchCallback: (obj.launchCallback ? obj.launchCallback : "")
		});
	}

	//-------------------------------------------------------------------------------------------------------------
	// removes all ships from all docks in the current system
	this.$emptyAllQueues = function $emptyAllQueues() {
		this._systemDockingData[system.ID] = {};
		for (var i = this._launching.length - 1; i >= 0; i--) {
			if (this._launching[i].system === system.ID) this._launching.splice(i, 1);
		}
	}

	//=============================================================================================================
	// system functions
	//-------------------------------------------------------------------------------------------------------------
	this.startUp = function () {
		if (this._disable === true) {
			// delete all the routines that would initiate/update data
			// that will effectively disable this OXP while leaving it in place.
			delete this.startUpComplete;
			delete this.shipDied;
			delete this.playerWillSaveGame;
			delete this.shipWillEnterWitchspace;
			delete this.shipExitedWitchspace;
			delete this.shipWillLaunchFromStation;
			delete this.shipWillDockWithStation;
			delete this.playerBoughtEquipment;
			delete this.playerBoughtNewShip;
			delete this.systemWillPopulate;
			delete this.systemWillRepopulate;
			delete this.startUp;
			return;
		}
		if (missionVariables.StationDockControl_ShipFits) {
			delete missionVariables.StationDockControl_ShipFits;
		}
		if (worldScripts["Skilled NPCs"]) {
			this._skilledNPCs = true;
		}
	}

	//-------------------------------------------------------------------------------------------------------------
	this.startUpComplete = function () {
		if (this._debug) {
			log(this.name, "Logging levels:");
			log(this.name, "Populator:   " + this._logPopulatorType);
			log(this.name, "Docking:     " + this._logDockingType);
			log(this.name, "Launching:   " + this._logLaunchType);
			log(this.name, "Adjustments: " + this._logAdjustmentType);
			log(this.name, "===================================================");
		}

		// register our settings, if Lib_Config is present
		if (worldScripts.Lib_Config) worldScripts.Lib_Config._registerSet(this._sdcConfig);

		// set a flag if smugglers is installed
		if (worldScripts.Smugglers_Illegal) this._smuggling = true;
		// set a flag if random ship names is installed
		if (worldScripts["randomshipnames"]) this._rsnInstalled = true;

		// add some AI script exceptions
		if (worldScripts.spicy_hermits_relocator) {
			this._AIScriptExceptions["Spicy Grunt AI"] = "spicy_hermits-gruntAI.js";
			this._AIScriptExceptions["Spicy Pirate AI"] = "spicy_hermits-pirateAI.js";
			this._AIScriptExceptions["In-System Trader AI"] = "in-system-traderAI.js";
		}

		// output the populator outgoing factors
		if (this._debug && this._logPopulatorType >= 2) this.$logOolitePopulatorOutgoingFactors();

		this._repopulate = false;

		// restore saved data if any exists
		if (missionVariables.StationDockControl_Data) delete missionVariables.StationDockControl_Data;
		if (missionVariables.StationDockControl_Main) delete missionVariables.StationDockControl_Main;
		if (missionVariables.StationDockControl_Info) {
			if (this._debug) log(this.name, "Loading stored data...");
			this._systemDockingData = JSON.parse(missionVariables.StationDockControl_Info);
			if (this._debug) log(this.name, "Stored data loaded successfully.");
			delete missionVariables.StationDockControl_Info;

			this._escortCounter = missionVariables.StationDockControl_EscortCounter;
			this._groupCounter = missionVariables.StationDockControl_GroupCounter;
			// reset our escort/group counters if they reach a certain limit
			if (this._escortCounter > 30000) this._escortCounter = 1;
			if (this._groupCounter > 30000) this._groupCounter = 1;

			this._quickUpdateTimer = new Timer(this, this.$basicSetup, 2, 0);
		} else {
			// otherwise create it from scratch
			// create a timer to run the jumpstart routine
			// we do this from a timer to try and make sure all OXP stations are in place - some OXP's might not have finished setting up yet
			this._quickUpdateTimer = new Timer(this, this.$initialSetup, 2, 0);
		}

		// load the launching list, if there is one
		if (missionVariables.StationDockControl_Launching) {
			this._launching = JSON.parse(missionVariables.StationDockControl_Launching);
			delete missionVariables.StationDockControl_Launching;
		}

		// load up any additional roles we found last time
		if (missionVariables.StationDockControl_AdditionalRoles) {
			this._additionalRoles = JSON.parse(missionVariables.StationDockControl_AdditionalRoles);
			delete missionVariables.StationDockControl_AdditionalRoles;
			// fixes bug with storing items repeatedly
			this._additionalRoles = this.$compactArray(this._additionalRoles);
		}

		//this.$grabAllShipDockValues();

		// start a timer to move ships to the launching queue every 60 seconds
		this._launchTimer = new Timer(this, this.$updateLaunchingList, 60, 60);
		this._populateTimer = new Timer(this, this.$runPopulation, 1, 3);
	}

	//-------------------------------------------------------------------------------------------------------------
	// whenever we come into a new system, get a list of neighbouring systems
	this.systemWillPopulate = function () {
		if (system.ID === -1) return;

		if (this._disable === true) {
			delete this.systemWillPopulate;
			return;
		}

		this._launchingGroups.length = 0;
		this._launchingEscorts.length = 0;
		this._neighbours = System.infoForSystem(galaxyNumber, system.ID).systemsInRange();
		// remove ships that will have launched from array
		//this.$clearLaunchedShips();
		this._launching.length = 0;

		// build our list of random ship names/roles/data keys
		this.$loadShipNames();

		this._stationAdjust = true; // tell the repopulator to adjust the station.otherShipDocked events
		this._stationIndexesLoaded = false; // clear the setting that will load station indexes
		this._repopulate = true; // tell the system to repopulate on the next repopulate run
		this._repopulateFactor = 1440; // make the next repopulate run a complete one
		this._shuttleDest = null; // clear out the array so it can be rebuilt later
	}

	//-------------------------------------------------------------------------------------------------------------
	// add the docking script to all stations in system
	this.systemWillRepopulate = function () {
		if (system.ID === -1) return;

		if (this._disable === true) {
			delete this.systemWillRepopulate;
			return;
		}

		if (this._cleanUpComplete === false) this.$dataCleanup();

		this._shipMonitor = (this._shipMonitor + 1) % 6;
		// check our docking queue situation
		if (this._shipMonitor === 1 || this._shipMonitor === 4) this.$assessStationDocks();

		// monitor the total ship count in a system to see how it fluctuates.
		if (this._debug && this._logPopulatorType > 0) {
			if (this._shipMonitor === 1) {
				// only do this full count every 6 times
				log(this.name, "Current ship count: " + system.allShips.length + ", system docked count: " + this.$countSystemItems() + ", main station count: " + this.$countStationItems(system.mainStation));
			} else {
				log(this.name, "Current ship count: " + system.allShips.length);
			}
		}

		// some stations get created during launch or exit from witchspace (rather than through systemWillPopulate), so check here for a change to the number of stations
		if (this._stationAdjust === false && system.stations.length > this._stationIndexCount) {
			if (this.debug && this._logPopulatorType > 0) log(this.name, "New stations found in repopulate routine!");
			this._stationAdjust = true;
			this._stationIndexesLoaded = false;
			this.$missingStationIndexes();
		}

		// adjust the station.otherShipDocked routines to use new version
		if (this._stationAdjust === true) {
			this._stationAdjust = false;

			// update list of station indexes for this system
			this.$stationIndexes();
			var stns = system.stations;
			stns.forEach(function (station) {
				if (station.hasNPCTraffic && this.$checkStationRoleForNoTraffic(station) === false) {
					// check for and save any previous script in otherShipDocked
					if (station.script.otherShipDocked && station.script.otherShipDocked !== this.$sdc_otherShipDocked) {
						if (this._debug) log(this.name, "Monkey patch being applied to " + station.name);
						station.script.$sdc_hold_otherShipDocked = station.script.otherShipDocked;
					}
					// attach our script to the otherShipDocked event
					station.script.otherShipDocked = this.$sdc_otherShipDocked;
				}
			}, this);
		}

		// check if the autoGeneration process failed (Javascript timed out before it completed)
		//if (this._autoGenerationInProgress === true) {
		//	this._autoCount += 1;
		//	if (this._autoCount === 3) {
		//		this._autoCount = 0;
		//		this.$populateStationList(this._repopulateFactor);
		//		this._repopulateFactor = 0;
		//	}
		//}

		// if we've been asked to repopulate the station list, do it here.
		if (this._repopulate === true && this._autoGenerationInProgress === false) {
			this._repopulate = false;
			this.$populateStationList(this._repopulateFactor);
			this._repopulateFactor = 0;
		} else {
			// have we got a late station to process?
			// if so, just do the repopulation process for one station at a time.
			if (this._lateStations.length > 0) {
				//log(this.name, "performing late station repopulate for " + this._lateStations[0]);
				// get station at the top of the list and assign it to the repoulate station variable
				// this will force the repopulate function to just run for a single station
				this._repopulateStation = this._lateStations[0];
				// remove it from the list so this only happens once
				this._lateStations.splice(0, 1);
				// run the repopulate routine for the full 12 hours
				this.$populateStationList(1440);
				// reset the repopulate station variable
				this._repopulateStation = null;
			}
		}

		// repopulate outgoing traffic
		// look for any ships to launch and launch them
		this.$launchPendingShips();

		var adjruns = 1;
		// only do the adjustment stuff while the player is docked.
		if (player.ship.docked) {
			// keep track of which ships are currently trying to dock, so we can make sure they're added to the list
			this.$updateShipsDocking(player.ship.dockedStation);
			// if we're docked, do more runs per cycle
			adjruns = this._adjustRuns;
		}
		this._adjustDepartureBands = (this._adjustDepartureBands + 1) % (this._adjustDepartureFreq + 1);
		// only do this process when our counter hits the frequency
		if (this._adjustDepartureBands === this._adjustDepartureFreq) {
			if (this._debug && this._logAdjustmentType > 0) log(this.name, "-------------------------------------------------");
			for (var i = 1; i <= adjruns; i++) {
				// pick a random acjustment function from the array
				this.$adjustShipDepartureBands(this._adjustFunctions[this.$rand(this._adjustFunctions.length) - 1], 0);
			}
		}

		// check for any ships that have been moved into a "rescheduled" status
		if (this._populateStation.length === 0) this.$checkForRescheduledShips();

	}

	//-------------------------------------------------------------------------------------------------------------
	this.shipDied = function (whom, why) {
		// clean up any active timers
		if (this._launchTimer && this._launchTimer.isRunning) this._launchTimer.stop();
		delete this._launchTimer;
		if (this._populateTimer && this._populateTimer.isRunning) this._populateTimer.stop();
		delete this._populateTimer;
	}

	//-------------------------------------------------------------------------------------------------------------
	this.playerWillSaveGame = function () {
		// save docking info array
		missionVariables.StationDockControl_Info = JSON.stringify(this._systemDockingData);
		missionVariables.StationDockControl_EscortCounter = this._escortCounter;
		missionVariables.StationDockControl_GroupCounter = this._groupCounter;
		// if there's anything in the launching list, save that as well
		if (this._launching.length > 0) missionVariables.StationDockControl_Launching = JSON.stringify(this._launching);
		// if there's anything in the additional roles list, save that too
		if (this._additionalRoles.length > 0) missionVariables.StationDockControl_AdditionalRoles = JSON.stringify(this._additionalRoles);
	}

	//-------------------------------------------------------------------------------------------------------------
	this.playerStartedJumpCountdown = function (type, seconds) {
		this._jumpStarted = true;
	}

	//-------------------------------------------------------------------------------------------------------------
	this.playerCancelledJumpCountdown = this.playerJumpFailed = function () {
		this._jumpStarted = false;
	}

	//-------------------------------------------------------------------------------------------------------------
	this.shipWillEnterWitchspace = function (cause, destination) {
		this._populatorVars = null;
		this._populateStation.length = 0;
		this._autoGenerationInProgress = false;
		//this._doShipDockDataGrab = {};
		this._jumpStarted = false;
		this._lateStations.length = 0;
		this._launching.length = 0; // experimental - may need to remove
		// remove all but the current and destination datasets
		if (system.isInterstellarSpace === false) {
			if (cause != "galactic jump" && destination) {
				this.$dataPurge(system.ID, destination);
			}
		}
	}

	//-------------------------------------------------------------------------------------------------------------
	this.shipExitedWitchspace = function () {
		// output the populator outgoing factors
		if (this._debug && this._logPopulatorType >= 2) this.$logOolitePopulatorOutgoingFactors();
		this._pendingDock.length = 0;
	}

	//-------------------------------------------------------------------------------------------------------------
	this.shipWillLaunchFromStation = function (station) {
		// cleanup
		if (missionVariables.StationDockControl_Data) delete missionVariables.StationDockControl_Data;
		if (missionVariables.StationDockControl_AdditionalRoles) delete missionVariables.StationDockControl_AdditionalRoles;

		this._docking.length = 0;
		// ** should not need to do this -- oolite should auto-dock these ships
		// dock any ship that is currently trying to dock
		//if (this._docking && this._docking.length > 0) {
		//	for (var i = 0; i < this._docking.length; i++) {
		//		if (this._debug) log(this.name, "forcing dock of ship " + this._docking[i]);
		//		this.$logShipDocking(station, this._docking[i]);
		//	}
		// }

		// launch ships that are due in the next ten minutes
		this.$dockingQueueShipClearance(station, 10);
	}

	//-------------------------------------------------------------------------------------------------------------
	this.shipWillDockWithStation = function (station) {
		// for a normal dock (ie. without a dock comp), there is no time jump when docking, so this call should do nothing
		// for an insta-dock, the time jump is 20 miuntes
		// for an escape pod, the time jump could be up to 8 days
		this.$checkForLaunchedShips();
	}

	//-------------------------------------------------------------------------------------------------------------
	// large time jumps can occur here...
	this.playerBoughtEquipment = function (equipment) {
		this.$checkForLaunchedShips();
	}

	//-------------------------------------------------------------------------------------------------------------
	// ...and here
	this.playerBoughtNewShip = function (ship) {
		this.$checkForLaunchedShips();
	}

	//-------------------------------------------------------------------------------------------------------------
	// ...and here
	this.reportScreenEnded = this.missionScreenEnded = function () {
		this.$checkForLaunchedShips();
	}

	//-------------------------------------------------------------------------------------------------------------
	// launch (or remove) any ships that would have launched during a sudden jump in time
	this.$checkForLaunchedShips = function $checkForLaunchedShips() {
		// don't do the check if we're about to do a repopulate routine, or we're in the middle of one
		if (this._repopulate) return;

		// is a time jump happening (ie. seconds !== adjustedSeconds), and the difference is greater than 60 seconds
		if (clock.seconds !== clock.adjustedSeconds && (clock.adjustedSeconds - clock.seconds) > 60) {
			// work out how much the time jump is, in minutes
			var diff = parseInt((clock.adjustedSeconds - clock.seconds) / 60);
			if (this._debug && this._logLaunchType > 0) log(this.name, "Checking for launched ships within period of " + diff + " minutes...");
			var stns = system.stations;
			// go through each station in the system
			stns.forEach(function (station) {
				if (station.hasNPCTraffic && this.$checkStationRoleForNoTraffic(station) === false) {
					this.$dockingQueueShipClearance(station, diff);
				}
			}, this);

			// force docked certain ships, based on the timelapse
			this.$forceDockController(diff);

			// do some departure band adjustments, based on our time differential
			// for 10 minutes, do 3 adjustments, for 20 minutes do 6 adjustments etc
			if (diff > 10 && diff < 960) {
				var adj = (diff / 10) * 3;
				if (this._debug && this._logAdjustmentType > 0) log(this.name, "Bulk adjustments: " + adj);
				this.$adjustShipDepartureBands(1, adj);
			}

			// set up the repopulation function
			if (diff > 30) {
				// add the diff onto the current repopulate factor, just in case there is already one in play and the repopulate routine hasn't run yet
				// that's to cope with the situation where the player buys multiple things quickly
				this._repopulateFactor += diff;
				if (this._debug && this._logPopulatorType > 0) log(this.name, "Repopulate factor = " + this._repopulateFactor + " (diff = " + diff + ")");
				// force the repopulator routine to run immediately
				this.$populateStationList(this._repopulateFactor);
			}
		}
	}

	//-------------------------------------------------------------------------------------------------------------
	// populates the randomShips array with ship names (eg "Cobra Mark III", "Asp Mark II" etc) and their associated role (eg "trader")
	// because most of the time we will be working with a data array, and not an actual ship object, we need a way to link
	// roles to ship types and ship keys. this routine re-compiles the list of possibilties which we can then use throughout the code.
	// the result will be: each role and ship type will have one entry in the list, and the shipKeys element will have a list of
	// possible versions of that ship.
	this.$loadShipNames = function $loadShipNames() {
		//-------------------------------------------------------------------------------------------------------------
		// returns the role frequency out of a data string
		function getRoleFrequency(roleName, roleData) {
			var point1 = roleData.indexOf(roleName) + roleName.length;
			var freq = "1"; // default of 1 (ie there is no bracket value)
			if (roleData.charAt(point1) === "(") {
				// extract the percentage
				var point2 = roleData.indexOf(")", point1);
				freq = roleData.substring(point1 + 1, point2);
			}
			return freq;
		}
		//-------------------------------------------------------------------------------------------------------------
		// checks to see if shipkey can be spawned in this system (ie are any conditions in place);
		function modelIsAllowed(shipkey) {
			var shipdata = Ship.shipDataForKey(shipkey);
			// are we allowed to include this data key in this system? check the conditions if there are some
			var include = true;
			if (shipdata.conditions) {
				var cond = shipdata.conditions.toString().split(",");
				//1,systemGovernment_number equal 4,systemGovernment_number,0,0,4,
				//1,systemEconomy_number notequal 4,systemEconomy_number,1,0,4
				//1,systemEconomy_number lessthan 4,systemEconomy_number,2,0,4
				//1,systemEconomy_number greaterthan 4,systemEconomy_number,3,0,4
				var offset = 0;
				var finish = false;
				var checking = -1;
				do {
					// get the value we're checking
					checking = -1;
					if (cond[offset + 2].substring(0, 7) === "mission") {
						if (missionVariables[cond[offset + 2].replace("mission_", "")]) {
							checking = missionVariables[cond[offset + 2].replace("mission_", "")];
							log("SDC.$loadShipNames.modelIsAllowed", "field = " + cond[offset + 2] + ", value = " + checking);
						} else {
							log("SDC.$loadShipNames.modelIsAllowed", "!!NOTE: Condition value mission variable not set: " + cond[offset + 2]);
						}
					} else {
						switch (cond[offset + 2]) {
							case "systemGovernment_number":
								checking = system.government;
								break;
							case "systemEconomy_number":
								checking = system.economy;
								break;
							case "systemTechLevel_number":
								checking = system.techLevel;
								break;
							case "score_number":
								checking = player.score;
								break;
							case "galaxy_number":
								checking = galaxyNumber;
								break;
							case "planet_number":
								checking = system.ID;
								break;
							default:
								log("SDC.$loadShipNames.modelIsAllowed", "!!NOTE: Condition value not catered for: " + cond[offset + 2]);
								break;
						}
					}
					// in case a mission variable is a text value of some sort
					if (isNaN(parseInt(checking)) && isNaN(parseFloat(checking))) {
						switch (cond[offset + 3]) {
							case "0": // equals
								if (checking != cond[offset + 5]) include = false;
								break;
							case "1": // not equals
								if (checking == cond[offset + 5]) include = false;
								break;
							default:
								log("SDC.$loadShipNames.modelIsAllowed", "!!NOTE: Condition comparison not catered for: " + cond[offset + 3]);
								break;
						}
					} else {
						if (checking >= 0) {
							// work out the type of check, but in negative (or opposite)
							switch (cond[offset + 3]) {
								case "0": // equals
									if (checking !== parseInt(cond[offset + 5])) include = false;
									break;
								case "1": // not equals
									if (checking === parseInt(cond[offset + 5])) include = false;
									break;
								case "2": // lessthan
									if (checking >= parseInt(cond[offset + 5])) include = false;
									break;
								case "3": // greaterthan
									if (checking <= parseInt(cond[offset + 5])) include = false;
									break;
								default:
									log("SDC.$loadShipNames.modelIsAllowed", "!!NOTE: Condition comparison not catered for: " + cond[offset + 3]);
									break;
									// others?
							}
						}
					}
					offset += 6;
					if (offset >= cond.length - 1) finish = true;
				} while (finish === false);
			} else if (shipdata.condition_script) {
				// or the condition script
				// create a dummy object to attach the script to so it can be executed
				var temppos = system.sun.position.cross(system.mainPlanet.position).direction().multiply(4E9).subtract(system.mainPlanet.position);
				var tempalloy = system.addShips("alloy", 1, temppos, 0);
				tempalloy[0].setScript(shipdata.condition_script);
				include = tempalloy[0].script.allowSpawnShip(shipkey);
				tempalloy[0].remove(true);
			} else {
				// otherwise we're free to play
				include = true;
			}
			return include;
		}

		// compile a list of ships/roles
		this._randomShips = {};
		this._shipData = {};
		var indexInList = this.$indexInList;

		var excl = this._dockingKeyExclusions;
		var cr = this._controlledRoles;
		for (var i = 0; i < cr.length; i++) {
			var ctrlRole = cr[i];
			var shipKeys = Ship.keysForRole(ctrlRole);
			var freq = "";
			if (shipKeys) {
				for (var j = 0; j < shipKeys.length; j++) {
					var key = shipKeys[j];
					// don't include any of these keys
					if (indexInList(key, excl) >= 0) continue;

					var include = modelIsAllowed(key);

					if (include) {
						var shipdata = Ship.shipDataForKey(key);
						// populate our holding dictionary
						if (!this._shipData[key]) {
							this._shipData[key] = {
								"auto_ai": shipdata["auto_ai"],
								"auto_weapons": shipdata["auto_weapons"],
								"name": shipdata.name,
								"max_cargo": shipdata["max_cargo"],
								"hyperspace_motor": shipdata["hyperspace_motor"],
								"escorts": shipdata["escorts"],
								"escort_role": shipdata["escort_data"],
								"escort_ship": shipdata["escort_ship"],
								"escort_roles": shipdata["escort_roles"],
								"accuracy": shipdata["accuracy"],
								"heat_insulation": shipdata["heat_insulation"],
							};
						}

						freq = getRoleFrequency(ctrlRole, shipdata.roles);
						// make sure we don't load our "dock" versions of ships
						if (key.indexOf("dock_") === -1 && key.indexOf("bounty_") === -1) {
							if (!this._randomShips[ctrlRole]) {
								this._randomShips[ctrlRole] = {
									shipKeys: [key],
									frequency: [freq]
								};
							} else {
								this._randomShips[ctrlRole].shipKeys.push(key);
								this._randomShips[ctrlRole].frequency.push(freq);
							}
						}
					} else {
						if (this._debug && this._logPopulatorType >= 2) log(this.name, "!!Note: Conditions not met to include " + ctrlRole + " -- " + key);
					}
				}
			} else {
				log(this.name, "!!NOTE: No ship keys found for role " + ctrlRole);
			}
		}
	}

	//-------------------------------------------------------------------------------------------------------------
	// this is a one-shot routine, called by a timer launched through startUpComplete
	// essentially this jump-starts the populator routine so players don't have to wait 20 seconds to see the list of ships
	this.$initialSetup = function $initialSetup() {
		this._repopulate = false;
		this.$stationIndexes();
		this.$populateStationList(1440);
		delete this._quickUpdateTimer;
	}

	//-------------------------------------------------------------------------------------------------------------
	// this is a one-shot routine, called by a timer launched through startUpComplete when data is restored from a savegame file
	// essentially this gets the station indexes populated early, so the player will be able to see data straight away
	this.$basicSetup = function $basicSetup() {
		this.$stationIndexes();
		// if a new station has been added to the system since the last save (eg via an OXP) and it needs traffic, make sure it gets some
		for (var i = 0; i < this._stationList.length; i++) {
			var item = this._stationList[i];
			if (this.$countStationItems(item.station) === 0) {
				if (this._debug && this._logPopulatorType > 0) log(this.name, "Adding station dock entries for new station: " + item.name + "(" + item.index + ")");
				this._repopulateStation = item.station;
				this.$populateStationList(1440);
				this._repopulateStation = null;
			}
		}
		delete this._quickUpdateTimer;
	}

	//-------------------------------------------------------------------------------------------------------------
	// build an array of stations with an index key, for when there is more than 1 of a particular station in a system
	this.$stationIndexes = function $stationIndexes() {
		if (this._stationIndexesLoaded) return;

		this._stationIndexesLoaded = true;

		var stns = system.stations;
		this._stationList.length = 0;
		this._stationIndexCount = system.stations.length;

		stns.forEach(function (station) {
			// only create an index for stations we have an interest in controlling
			if ((station.status === "STATUS_ACTIVE" || station.status === "STATUS_IN_FLIGHT") && station.hasNPCTraffic && this.$checkStationRoleForNoTraffic(station) === false) {
				var idx = 1;
				for (var i = 0; i < this._stationList.length; i++) {
					if (this._stationList[i].station.name === station.name) idx += 1;
				}
				this._stationList.push({
					station: station,
					name: station.name,
					index: idx
				});
				if (this._debug && this._logPopulatorType >= 2)
					log(this.name, "Creating stationList entry: " + station.name + " index = " + idx);
			}
		}, this);
	}

	//-------------------------------------------------------------------------------------------------------------
	// adds any stations missed during the system population routines to the lateStations array (ie stations that get created via some other event)
	this.$missingStationIndexes = function $missingStationIndexes() {
		var stns = system.stations;
		stns.forEach(function (station) {
			var found = false;
			for (var i = 0; i < this._stationList.length; i++) {
				if (this._stationList[i].station === station) found = true;
			}
			if (found === false && this._lateStations.indexOf(station) === -1) {
				if (this._debug && this._logPopulatorType >= 2) log(this.name, "Adding missing station: " + station.name);
				this._lateStations.push(station);
			}
		}, this);
	}

	//-------------------------------------------------------------------------------------------------------------
	// function to process ship docking (based on station "otherShipDocked" function)
	this.$sdc_otherShipDocked = function $sdc_otherShipDocked(whom) {
		// if there was a previous script in place, run it here.
		if (this.ship.script.$sdc_hold_otherShipDocked) this.ship.script.$sdc_hold_otherShipDocked(whom);
		if (whom.isPlayer === false) {
			var w = worldScripts.StationDockControl;
			w.$logShipDocking(this.ship, whom);
		}
	}

	//-------------------------------------------------------------------------------------------------------------
	// adjusts the departure times or destinations of random ships
	// type 1 = change from docking to unloading, from unloading to docked, or from docked to loading
	// type 2 = change departure time
	// type 3 = change destination
	this.$adjustShipDepartureBands = function $adjustShipDepartureBands(adjustType, override_thistime) {

		if (this._debug && this._logAdjustmentType > 0) log(this.name, "Running adjustment type " + adjustType);

		// type 0 = do nothing
		if (adjustType === 0) return;

		var chance = this._adjustDepartureChance;
		var adjustCount = 0;
		// work out how many adjustments we'll do this time
		var thistime = this.$rand(this._maxAdjustments);
		if (override_thistime > 0) {
			thistime = override_thistime;
			chance *= 2; // double the chance of making a change during a bulk update
		}

		if (this._systemDockingData[system.ID] == null) return;

		// move a ship with unknown departure time to a defined time
		// start from the bottom of the list
		var skeys = Object.keys(this._systemDockingData[system.ID]);
		for (var kc = 0; kc < skeys.length; kc++) {
			var dta = this._systemDockingData[system.ID][skeys[kc]];
			if (dta && dta.length > 0) {
				for (var i = dta.length - 1; i >= 0; i--) {
					var item = dta[i];
					var done = false;
					// if this record is for this system and we haven't changed it for over 10 minutes (this._noAdjustPeriod)
					// and it's either a group leader or not in a group at all, or it's an escort leader or not in an escort at all
					// this is so we only initiate a move for individual ships or leader ships
					if ((clock.adjustedSeconds - item.lastChange) > this._noAdjustPeriod &&
						((item.groupName === "" && item.escortName === "") ||
							item.groupLeader === true || item.escortLeader === true) &&
						Math.random() < chance) {

						// get the current departure time in minutes
						var depart = item.departureTime; //(item.departureTime - clock.adjustedSeconds) / 60;
						var newDepart = 0;
						var free = false;
						var tries = 0;

						// adjust from docking to unloading, unloading to docked, or docked to loading
						if (adjustType === 1) {
							// move from docking to unloading
							if (done === false && depart >= 1380 && depart < 1440) {
								// found one
								tries = 0;
								free = false;
								do {
									newDepart = (this.$rand(180) + 1380); // * 60 + clock.adjustedSeconds; // 20-23 hours
									tries += 1;
									free = this.$checkDepartureSlot(item.station, item.stationIndex, newDepart);
								} while (free === false && tries < 5);

								if (free === true) {
									// do loading function here
									// if (this._NPCTrading === true) {
									//	var td = worldScripts.StationDockInfo_NPCTrading;
									//	td.$processCargo_Unloading(item);
									// }
									done = true;
									if (this._debug && this._logAdjustmentType === 2) log(this.name, "Moving " + item.shipType + (item.shipName !== "" ? ": " + item.shipName : "") + " to unloading status");
									item.departureTime = newDepart;
									item.lastChange = clock.adjustedSeconds;
									this.$updateGroupMemberData(skeys[kc], item.groupName, item.escortName, "departureTime", newDepart);
								}
							}

							// move from unloading (1200-1380 minutes) to docked
							if (done === false && depart >= 1200 && depart < 1380) {
								// found one
								tries = 0;
								free = false;
								do {
									newDepart = (this.$rand(240) + 960); // * 60 + clock.adjustedSeconds; // 16-20 hours
									tries += 1;
									free = this.$checkDepartureSlot(item.station, item.stationIndex, newDepart);
								} while (free === false && tries < 5);

								if (free === true) {
									done = true;
									if (this._debug && this._logAdjustmentType === 2) log(this.name, "Moving " + item.shipType + (item.shipName !== "" ? ": " + item.shipName : "") + " to docked status");
									item.departureTime = newDepart;
									item.lastChange = clock.adjustedSeconds;
									this.$updateGroupMemberData(skeys[kc], item.groupName, item.escortName, "departureTime", newDepart);
								}
							}

							// move from docked to loading status
							if (done === false && depart >= 960 && depart < 1200) {
								// found one
								tries = 0;
								free = false;
								do {
									newDepart = (this.$rand(240) + 720); // * 60 + clock.adjustedSeconds; // 12-16 hours
									tries += 1;
									free = this.$checkDepartureSlot(item.station, item.stationIndex, newDepart);
								} while (free === false && tries < 5);

								if (free === true) {
									// do loading function here
									// if (this._NPCTrading === true) {
									//	var td = worldScripts.StationDockInfo_NPCTrading;
									// 	td.$processCargo_Loading(item);
									// }
									done = true;
									if (this._debug && this._logAdjustmentType === 2) log(this.name, "Moving " + item.shipType + (item.shipName !== "" ? ": " + item.shipName : "") + " to loading status");
									item.departureTime = newDepart;
									item.lastChange = clock.adjustedSeconds;
									this.$updateGroupMemberData(skeys[kc], item.groupName, item.escortName, "departureTime", newDepart);
								}
							} // if depart between 16 and 47

							// move from loading to departing
							if (done === false && depart >= 720 && depart < 960) {
								tries = 0;
								free = false;
								do {
									newDepart = (this.$rand(660) + 60); // * 60 + clock.adjustedSeconds; // 1-12 hours
									tries += 1;
									free = this.$checkDepartureSlot(item.station, item.stationIndex, newDepart);
								} while (free === false && tries < 5);

								if (free === true) {
									done = true;
									if (this._debug && this._logAdjustmentType === 2) log(this.name, "Moving " + item.shipType + (item.shipName !== "" ? ": " + item.shipName : "") + " to departing status");
									item.departureTime = newDepart;
									item.lastChange = clock.adjustedSeconds;
									this.$updateGroupMemberData(skeys[kc], item.groupName, item.escortName, "departureTime", newDepart);
								}
							} // if depart between 720 and 960
						} // if adjustType = 1

						// straight change of departure time
						if (adjustType === 2) {
							if (done === false && depart > 120 && depart < 660) {
								tries = 0;
								free = false;
								do {
									// pick a random time (2 minutes to 11:42 hours)
									newDepart = (this.$rand(700) + 2); // * 60 + clock.adjustedSeconds;
									tries += 1;
									free = this.$checkDepartureSlot(item.station, item.stationIndex, newDepart);
								} while (free === false && tries < 5);

								if (free === true) {
									done = true;
									if (this._debug && this._logAdjustmentType === 2) log(this.name, "Changing departure time for " + item.shipType + (item.shipName !== "" ? ": " + item.shipName : ""));
									item.departureTime = newDepart;
									item.lastChange = clock.adjustedSeconds;
									this.$updateGroupMemberData(skeys[kc], item.groupName, item.escortName, "departureTime", newDepart);
								}
							} // if depart
						} // if adjustType = 2

						// adjust destination
						if (adjustType === 3) {
							if (done === false && depart > 120 && depart < 660 &&
								item.destinationSystem !== system.ID &&
								item.destinationSystem >= 0 &&
								item.destinationHidden === false) {

								var newDest = -1;
								var w = worldScripts["oolite-populator"];
								switch (item.primaryRole) {
									case "trader":
										newDest = w._weightedNearbyTradeSystem();
										break;
									case "trader-courier":
										if (Math.random() < 0.5) {
											newDest = this.$nearbySystem(7);
										} else {
											newDest = this.$nearbySystem(25);
										}
										break;
									case "trader-smuggler":
										newDest = this.$nearbySystem(7);
										break;
									case "pirate-light-freighter":
									case "pirate-medium-freighter":
										newDest = w._nearbySafeSystem(system.info.government + 1);
										break;
									case "hunter-medium":
										newDest = w._nearbyDangerousSystem(4);
										break;
									case "hunter-heavy":
										newDest = w._nearbyDangerousSystem(1);
										break;
								}

								if (item.destinationSystem !== newDest && newDest !== -1) {
									done = true;
									item.destinationSystem = newDest;
									item.lastChange = clock.adjustedSeconds;
									if (this._debug && this._logAdjustmentType === 2) log(this.name, "Updating destinationSystem for " + item.shipType + (item.shipName !== "" ? ": " + item.shipName : "") + " to " + newDest);
									this.$updateGroupMemberData(skeys[kc], item.groupName, item.escortName, "destinationSystem", newDest);
								}
							}
						} // if adjustType === 3
					}

					// count the number of times we're adjusting a ship record
					if (done === true) adjustCount += 1;
					// stop once we hit the target for this round
					if (adjustCount === thistime) break;
				} // for i
			} // if dta
		} // for kc

	}

	//-------------------------------------------------------------------------------------------------------------
	// look for any ships in the launching queue that have been moved to "Rescheduled"
	// when found, give them a new departure slot
	// hopefully, with our station dock assessment routine, no ships will ever need to be rescheduled, but just in case...
	this.$checkForRescheduledShips = function $checkForRescheduledShips() {
		if (this._launching.length > 0) {
			for (var i = 0; i < this._launching.length; i++) {
				var launch = this._launching[i];
				var stationkey = launch.station + "_" + launch.stationIndex;
				// if the departure time is set for "rescheduled" and this is either the group/escort leader or not in a group/escort at all...
				if (Math.round(launch.departureTime) === -5) {
					if (((launch.groupName === "" && launch.escortName === "") || (launch.groupLeader || launch.escortLeader))) {
						// found one!
						var newDepart = 0;
						var tries = 0;
						var free = false;
						do {
							// pick a random time (2 minutes to 11:42 hours)
							newDepart = (this.$rand(700) + 2); // * 60 + clock.adjustedSeconds;
							tries += 1;
							free = this.$checkDepartureSlot(launch.station, launch.stationIndex, newDepart);
						} while (free === false && tries < 5);

						if (free === true) {
							if (this._debug && this._logAdjustmentType >= 1) log(this.name, "Changing departure time for " + launch.shipType + (launch.shipName !== "" ? ": " + launch.shipName : ""));
							launch.departureTime = newDepart;
							launch.lastChange = clock.adjustedSeconds;
							this.$updateGroupMemberLaunchingData(launch.groupName, launch.escortName, "departureTime", newDepart);
						} else {
							if (this._debug && this._logAdjustmentType >= 1) log(this.name, "Failed to find a new departure time for " + launch.shipType + (launch.shipName !== "" ? ": " + launch.shipName : ""));
						}
					} else {
						// has the group leader launched?
						if (this.$isGroupLeaderDocked(stationkey, (launch.groupLeader !== "" ? launch.groupLeader : launch.escortLeader)) === false) {
							// get this ship launched ASAP!
							var newDepart = 0;
							var tries = 0;
							var free = false;
							do {
								// pick a random time (2 minutes to 11:42 hours)
								newDepart = this.$rand(3); // * 60 + clock.adjustedSeconds;
								tries += 1;
								free = this.$checkDepartureSlot(launch.station, launch.stationIndex, newDepart);
							} while (free === false && tries < 5);

							if (free === true) {
								if (this._debug && this._logAdjustmentType >= 1) log(this.name, "Changing departure time for orphaned group/escort member " + launch.shipType + (launch.shipName !== "" ? ": " + launch.shipName : ""));
								launch.departureTime = newDepart;
								launch.lastChange = clock.adjustedSeconds;
							} else {
								if (this._debug && this._logAdjustmentType >= 1) log(this.name, "Failed to find a new departure time for orphaned group/escort member " + launch.shipType + (launch.shipName !== "" ? ": " + launch.shipName : ""));
							}
						}
					}
				}

			}
		}
	}

	//-------------------------------------------------------------------------------------------------------------
	// update group/escort members data
	this.$updateGroupMemberData = function $updateGroupMemberData(stationkey, groupName, escortName, keyfield, newvalue) {
		var dta = this._systemDockingData[system.ID][stationkey];
		if (dta && dta.length > 0) {
			for (var i = dta.length - 1; i >= 0; i--) {
				var item = dta[i];
				if (((escortName !== "" && item.escortName === escortName && item.escortLeader === false) ||
						(groupName !== "" && item.groupName === groupName && item.groupLeader === false))) {

					if (this._debug && this._logAdjustmentType === 2) log(this.name, "Updating " + keyfield + " of group/escort member " + item.shipType + (item.shipName !== "" ? ": " + item.shipName : ""));
					// update the keyfield with the new value
					item[keyfield] = newvalue;
					item.lastChange = clock.adjustedSeconds;
				}
			}
		}
	}

	//-------------------------------------------------------------------------------------------------------------
	// update group/escort members data in the launching list
	this.$updateGroupMemberLaunchingData = function $updateGroupMemberLaunchingData(groupName, escortName, keyfield, newvalue) {
		for (var i = this._launching.length - 1; i >= 0; i--) {
			var launch = this._launching[i];
			if (launch.system === system.ID &&
				((escortName !== "" && launch.escortName === escortName && launch.escortLeader === false) ||
					(groupName !== "" && launch.groupName === groupName && launch.groupLeader === false))) {

				if (this._debug && this._logAdjustmentType === 2) log(this.name, "Updating " + keyfield + " of group/escort member " + launch.shipType + (launch.shipName !== "" ? ": " + launch.shipName : ""));
				// update the keyfield with the new value
				launch[keyfield] = newvalue;
				launch.lastChange = clock.adjustedSeconds;
			}
		}
	}

	//-------------------------------------------------------------------------------------------------------------
	// log a new ship docking
	// grab enough info to recreate a new ship later if needed, but don't keep a hold on the object so system resources are not consumed
	this.$logShipDocking = function $logShipDocking(station, ship) {
		// for some reason, we can occasionally end up with the station trying to dock itself here. So make sure we don't!
		// don't log info about police, thargoids, cargo or unpiloted ships
		// check that a station doesn't try to dock itself - bad things happen then!
		if (ship.isPolice || ship.isStation || ship.isThargoid || ship.isCargo || !ship.isPiloted || ship.isPlayer) return;
		// look for any ships with a primary role outside of the ones we are controlling (eg miner, scavenger)
		if (this.$roleIsAllowed(ship.primaryRole) === false && this.$roleIsAllowedAdditional(ship.primaryRole) === false && this._dockingRoleExceptions.indexOf(ship.primaryRole) === -1) {
			if (this._debug && this._logDockingType > 0) log(this.name, "Ship with primary role '" + ship.primaryRole + "' docked -- no data will be added");
			return;
		}
		// make doubly sure the station isn't trying to dock itself!
		if (station === ship) return;

		// get the index of the station we're docking at
		var stnIndex = this.$getStationIndex(station);
		var stationkey = station.name + "_" + stnIndex;

		if (this._debug && this._logDockingType > 0) log(this.name, "Docking ship at station " + station.name + "(" + stnIndex + "): " + ship.displayName + " -- " + ship.dataKey);

		// set departure time to 23-24 hours by default (docking status) for all docking ships so that they follow through the statuses
		var depart = this.$calculateRandomDepartureTime(station, stnIndex, 10, false);
		var destSystem = -1;
		var destHidden = true;
		var isGroupLeader = false;

		var fuel = 7;
		// by default, it will be assumed that ships will refuel on docking
		// however, as player ships can't refuel at constores anymore, NPC ships will also be unable to as well
		// in that case, just set the fuel back to whatever they have
		if (station.hasRole("constore")) fuel = ship.fuel;

		// check if a ship from this group has already docked, and if so, get its departure time
		var sdc_pop = worldScripts.StationDockControl_Populator;
		var lookupgroup = "";
		var leader;

		// is there a group still attached to the ship?
		if (ship.group && ship.group.count > 1) {
			lookupgroup = ship.group.name;
			leader = ship.group.leader;
			if (this._debug && this._logDockingType === 2) log(this.name, "Found group: " + lookupgroup);
		}

		// if not, try looking up the group from our holding array
		if (lookupgroup === "") {
			lookupgroup = sdc_pop.$getGroupName(ship);
			if (lookupgroup !== "") {
				if (this._debug && this._logDockingType === 2) log(this.name, "Found original group: " + lookupgroup);
				leader = sdc_pop.$getGroupLeader(lookupgroup);
			}
		}

		if (lookupgroup !== "") {
			if (leader && leader.isValid) {
				if (this._debug && this._logDockingType === 2) log(this.name, "Group leader: " + leader.displayName + " -- " + leader);
				if (leader == ship) {
					isGroupLeader = true;
				}
			}
			// can we find the leader anywhere in space or in dock
			if ((!leader || leader.isValid === false) && this.$isGroupLeaderDocked(stationkey, lookupgroup) === false) {
				// no leader
				// todo: work out different scenarios for non-escort group members (hunters in particular)
				if (this._debug && this._logDockingType === 2) log(this.name, "Leader no longer present - group is dissolved");
				lookupgroup = "";
			}
		}

		var departSecs = 0;

		// do we still have a group? (We might have cleared it if it dissolved)
		if (lookupgroup !== "") {
			var groupData = this.$getShipGroupInfo(stationkey, lookupgroup);

			if (groupData) {
				// if the leader didn't dock first, make sure their departure time will be before any group members
				if (groupData.depart !== 0) {
					if (isGroupLeader) {
						depart = groupData.depart;
						departSecs = 0;
						groupData.depart = 0;
					}
				}
				// assuming the leader docked first, all subsequent group member dockings will have a departure time be one second after their leader
				// so the ordering of ships is always leader first
				if (groupData.depart !== 0) {
					depart = groupData.depart;
					departSecs = 5;
				}
				// if groupDepart is 0, that means we haven't docked a member from this group before.
				// check the group leader, and set the flag if true
				//if (groupDepart === 0 && lookupgroupleader) isGroupLeader = true;

				if (groupData.destination >= 0) {
					destSystem = groupData.destination;
					destHidden = groupData.destinationHidden;
				}
			} else {
				// no existing docked records for this group
				// try this as an escort group
				var escortData = this.$getShipEscortInfo(stationkey, lookupgroup);
				if (escortData) {
					if (escortData.depart !== 0) {
						if (isGroupLeader) {
							depart = escortData.depart;
							departSecs = 0;
							escortData.depart = 0;
						}
					}
					if (escortData.depart !== 0) {
						depart = escortData.depart;
						departSecs = 0;
					}
					if (escortData.destination >= 0) {
						destSystem = escortData.destination;
						destHidden = escortData.destinationHidden;
					}
				}
			}
		}

		// work out the current AI script name
		// there's probably an easier way of doing this...
		var aiName = "";
		if (ship.autoAI && ship.AIScript) {
			switch (ship.AIScript.name) {
				case "Oolite Assassin AI":
					aiName = "oolite-assassinAI.js";
					break;
				case "Oolite Bounty Hunter AI":
					aiName = "oolite-bountyHunterAI.js";
					break;
				case "Oolite Bounty Hunter Leader AI":
					aiName = "oolite-bountyHunterLeaderAI.js";
					break;
				case "Oolite Constrictor AI":
					aiName = "oolite-constrictorAI.js";
					break;
				case "Oolite Defense Ship AI":
					aiName = "oolite-defenceShipAI.js";
					break;
				case "Oolite Escort AI":
					aiName = "oolite-escortAI.js";
					break;
				case "Oolite Pirate AI":
					aiName = "oolite-pirateAI.js";
					break;
				case "Oolite Pirate Fighter AI":
					aiName = "oolite-pirateFighterAI.js";
					break;
				case "Oolite Pirate Freighter AI":
					aiName = "oolite-pirateFreighterAI.js";
					break;
				case "Oolite Pirate Interceptor AI":
					aiName = "oolite-pirateInterceptorAI.js";
					break;
				case "Oolite Shuttle AI":
					aiName = "oolite-shuttleAI.js";
					break;
				case "Oolite Trader AI":
					aiName = "oolite-traderAI.js";
					break;
				case "Oolite Trader Opportunist AI":
					aiName = "oolite-traderOpportunistAI.js";
					break;
				case "Null AI":
					// pick up the plist version of the ai
					if (ship.AI !== "nullAI.plist") {
						aiName = ship.AI;
					}
					break;
				default:
					if (this._AIScriptExceptions[ship.AIScript.name] && this._AIScriptExceptions[ship.AIScript.name] !== "") aiName = this._AIScriptExceptions[ship.AIScript.name];
					if (aiName === "") log(this.name, "!!ERROR: Unrecognised Auto AI Script: " + ship.AIScript.name);
					break;
			}
		}

		var prim_role = ship.primaryRole;
		var isEscort = false;
		if (ship.primaryRole === "escort" || ship.primaryRole === "escort-light" || ship.primaryRole === "escort-medium" || ship.primaryRole === "escort-heavy") {

			isEscort = true;

			// will we be turning this escort into something else?
			// if the ship is not attached to a group, it's time to switch
			if (lookupgroup === "") {
				isEscort = false;
				if (ship.cargoSpaceCapacity > 0 && ship.hasHyperspaceMotor === true) {
					// if the ship has cargo space and a hyperdrive, make them a trader
					if (this._debug && this._logDockingType === 2) log(this.name, "Switching lone escort " + ship.displayName + " to trader");
					prim_role = "trader";
					aiName = "oolite-traderAI.js";
				} else {
					if (ship.hasHyperspaceMotor === true) {
						// if they just have a hyperdrive, there's a good chance they'll decide to be a courier
						if (this._debug && this._logDockingType === 2) log(this.name, "Switching lone escort " + ship.displayName + " to courier");
						prim_role = "trader-courier";
						aiName = "oolite-traderAI.js";
					} else {
						// otherwise this escort is going for some R&R
						if (this._debug && this._logDockingType === 2) log(this.name, "Dropping lone escort " + ship.displayName + "...heading for R&R");
						return;
					}
				}
				// reset the destination so a new one can be calculated
				destSystem = -1;
			} else {
				// make sure the leader has the escort group name attached
				// isGroupLeader should always be false for an escort ship, but just to be sure
				if (isGroupLeader === false) {
					this.$setGroupEscortLeader(stationkey, lookupgroup);
				}
			}
		}

		// work out what goods and destination this ship will have when it next launches
		var destLoc = "";
		if (destSystem === -1) {
			var dest = this.$getDestinationByRole(prim_role, station);
			destSystem = dest.destination;
			destHidden = dest.destinationHidden;
			if (dest.location !== "") destLoc = dest.location;
		}

		var goods = "";
		switch (prim_role) {
			case "trader":
				goods = "PLENTIFUL_GOODS";
				//if (ship.shipClassName.toLowerCase().indexOf("medical") >= 0) {
				if (ship.shipClassName.search(new RegExp("medical", "i")) !== -1) {
					goods = "MEDICAL_GOODS";
				} else {
					if (ship.bounty !== 0) {
						goods = "PIRATE_GOODS";
					}
				}
				break;
			case "trader-smuggler":
				goods = "ILLEGAL_GOODS";
				break;
		}

		// make sure we're not putting cargo in a ship with no cargo space
		if (ship.cargoSpaceCapacity === 0) goods = "";

		// process cargo
		var cargo = "";
		// if (this._NPCTrading === true) {
		//	var td = worldScripts.StationDockInfo_NPCTrading;
		//	cargo = td.$processCargo_Docking(ship);
		// }

		// check for illegal imports and increase bounty as appropriate.
		if (ship.cargoSpaceCapacity > 0 && ship.cargoList.length > 0) {
			var bribe_attempt = Math.random();
			for (var i = 0; i < ship.cargoList.length; i++) {
				var itm = ship.cargoList[i];
				if (station.market[itm.commodity].legality_import > 0) {
					var chance = 0.7;
					if (this._smuggling === true) {
						chance = worldScripts.Smugglers_DockMaster._bribeChance[system.ID];
					}
					if (bribe_attempt < chance) {
						ship.bounty += itm.quantity * station.market[itm.commodity].legality_import;
						if (this._debug && this._logDockingType >= 1) {
							log(this.name, "Ship was carrying illegal cargo (" + itm.commodity + ") - bounty increased by " + itm.quantity * station.market[itm.commodity].legality_import);
						}
					} else {
						if (this._debug && this._logDockingType >= 1) {
							log(this.name, "Ship was carrying illegal cargo (" + itm.commodity + ") - ship bribed dockmaster");
						}
					}
				}
			}
		}

		var pilot = {};
		pilot["name"] = ship.crew[0].name;
		pilot["insurance"] = ship.crew[0].insuranceCredits;
		pilot["description"] = ship.crew[0].description;
		pilot["homeSystem"] = ship.crew[0].homeSystem;
		pilot["legalStatus"] = (ship.markedForFines ? 0 : ship.crew[0].legalStatus);
		pilot["species"] = ship.crew[0].species;

		var equip = "";
		// pick up all installed equipment
		var eq = ship.equipment;
		for (var i = 0; i < eq.length; i++) {
			var eqItem = eq[i];
			if (this._ignoreEquip.indexOf(eqItem.equipmentKey) !== -1) equip += eqItem.equipmentKey + ",";
		}
		// pick up the installed weapons;
		if (ship.forwardWeapon) equip += "FORE:" + ship.forwardWeapon.equipmentKey + ",";
		if (ship.aftWeapon) equip += "AFT:" + ship.aftWeapon.equipmentKey + ",";
		if (ship.portWeapon) equip += "PORT:" + ship.portWeapon.equipmentKey + ",";
		if (ship.starboardWeapon) equip += "STARBOARD:" + ship.starboardWeapon.equipmentKey + ",";

		var shipname = "";
		shipname = ship.shipUniqueName;
		// occasionally the shipUniqueName returns "" even though it's really there. In that instance, pull the unique name out of the display name
		if (shipname === "") shipname = this.$getShipUniqueName(ship.displayName, ship.shipClassName);

		// check to see if this ship has a callback for when it docks
		var launchCB = "";
		var launchWS = "";
		if (this._pendingDock.length > 0) {
			for (var i = this._pendingDock.length - 1; i >= 0; i--) {
				var item = this._pendingDock[i];
				if ((item.shipName === "" || item.shipName === shipname) &&
					(item.shipType === "" || item.shipType === ship.shipClassName) &&
					(item.pilotName === "" || item.pilotName === pilot.name)) {
					// we found a match, so execute the callback
					var w = worldScripts[item.worldScript];
					if (w) w[item.callback](station, item.parameter);
					if (item.launchCallback !== "") {
						launchCB = item.launchCallback;
						launchWS = item.worldScript;
					}
					// remove the item from the array
					this._pendingDock.splice(i, 1);
				}
			}
		}

		var props = {};
		for (var i = 0; i < this._propertiesToKeep.length; i++) {
			var prop = this._propertiesToKeep[i];
			if (ship.script.hasOwnProperty(prop) === true) {
				props[prop] = ship.script[prop];
			}
		}

		// store all the data in the array
		this.$addShipToArray({
			station: station,
			stationIndex: stnIndex,
			role: prim_role,
			shipType: ship.shipClassName,
			shipDataKey: ship.dataKey,
			shipName: shipname,
			personality: ship.entityPersonality,
			aiName: aiName,
			accuracy: ship.accuracy,
			equipment: equip,
			heatInsulation: ship.heatInsulation,
			fuel: fuel,
			homeSystem: ship.homeSystem,
			bounty: (ship.markedForFines ? 0 : ship.bounty),
			escortName: (isEscort === true ? lookupgroup : ""),
			escortLeader: isGroupLeader,
			groupName: (isEscort === true ? "" : lookupgroup),
			groupLeader: isGroupLeader,
			destinationSystem: destSystem,
			destinationHidden: destHidden,
			destination: destLoc,
			goods: goods,
			cargo: cargo,
			pilot: pilot,
			dockTime: clock.seconds,
			departureTime: depart,
			departureSeconds: departSecs,
			autoLog: 2,
			lastChange: clock.adjustedSeconds,
			launchCallback: launchCB,
			worldScript: launchWS,
			properties: props
		});

		if (this._debug && this._logDockingType === 2 && ship.markedForFines) log(this.name, "Ship was marked for fines and has been had their bounty cleared");

		// remove the ship from the group data
		sdc_pop.$removeShip(ship);
	}

	//-------------------------------------------------------------------------------------------------------------
	// launch any ships whose departure time has expired
	this.$launchPendingShips = function $launchPendingShips() {
		function compare(a, b) {
			return ((a.departureTime * 10 + a.departureSeconds) - (b.departureTime * 10 + b.departureSeconds));
		}
		//-------------------------------------------------------------------------------------------------------------
		// returns the number of docked ships in the same group as the passed record
		function countShipsInGroup(dockingData, shipdata) {
			var items = 1;
			if (shipdata.escortName === "" || shipdata.groupName === "") return 1;
			var dta = dockingData[shipdata.station + "_" + shipdata.stationIndex];
			if (dta && dta.length > 0) {
				for (var i = 0; i < dta.length; i++) {
					var item = dta[i];
					if (item.groupName === shipdata.groupName || item.escortName === shipdata.escortName) items += 1;
				}
			}
			return items;
		}

		// don't launch ships in the middle of an update to the array
		if (this._launchListUpdating === true) return;

		if (this._launching.length > 0) {
			if (this._debug && this._logLaunchType > 0) log(this.name, "Launching pending ships " + this._launching.length);

			var clearlist = [];

			this._launching.sort(compare);

			for (var i = 0; i < this._launching.length; i++) {
				// launch it
				// add the ship to the launch queue
				// will this ship be part of a group?
				var shp = this._launching[i];

				if (shp.system === system.ID) {
					var station = this.$getStationFromName(shp.station, shp.stationIndex);
					if (station == null) {
						// only output to the log the station is null error once for each station
						if (this.$indexInList(shp.station + "_" + shp.stationIndex, this._stationNullError) === -1) {
							log(this.name, "!!ERROR: Station is null (OXP removed?): Removing " + shp.shipName + " -- " + shp.station + " (" + shp.stationIndex + ")");
							this._stationNullError.push(shp.station + "_" + shp.stationIndex);
						}
						// remove the ship if the station doesn't exist anymore
						clearlist.push(i);
					}

					// make sure we have a valid station before trying to launch
					// to catch the case when a station OXP was removed
					if (station && Math.round(shp.departureTime) !== -5) {
						// work out how many ships are going to be launching at the same time
						var shipcount = countShipsInGroup(shp, this._systemDockingData[system.ID]);
						// is there any space in the launch queue for this group of ships
						// so, if there's only 1 slot, and there's 5 ships in this group, don't launch any of them
						if (this.$launchAvailable(station, shipcount) === true) {

							var shpKey = shp.shipDataKey;
							// make sure a single ship is launched (ie without escorts)
							if (this.$dockVersionExists(shpKey)) shpKey = "dock_" + shpKey;

							if (this._debug && this._logLaunchType > 0) this.$writeDataToLog(shp, "Launching ship through " + shp.station + " (" + shp.stationIndex + ")");
							var ship = station.launchShipWithRole("[" + shpKey + "]");

							if (!ship) {
								// Oops! no ship returned!
								// skip it this time and maybe next time it will launch
								if (this._debug && this._logLaunchType > 0) log(this.name, "!!NOTE: Did not create ship of type '" + shpKey + "' - launchShipWithRole returned null");
								continue;
							}

							// make a note of the item to clear from the array
							clearlist.push(i);

							// update details of launched ship
							this.$updateLaunchedShip(ship, shp);

							// if there is an attached callback, call it now.
							if (shp.launchCallback && shp.worldScript) {
								var w = worldScripts[shp.worldScript];
								if (w) w[shp.launchCallback]("launching", ship);
							}
						} else {
							// when launch is not available
							// options are: (1) try again on the next cycle > do nothing
							// 				(2) schedule ship (group) for a new time
							if (Math.random() < 0.4) {
								if (this._debug && this._logLaunchType >= 2) log(this.name, "Ship " + shp.shipName + " being moved to 'rescheduled' status!");
								shp.departureTime = -5;
								shp.lastChange = clock.adjustedSeconds;
								// move any escort/group members to -5, plus 0.1 so they can be sorted correctly
								this.$updateGroupMemberLaunchingData(shp.groupName, shp.escortName, "departureTime", -5);
							}
						}
					} // if station
				} else {
					// if there are launching items from another system, just purge them
					if (this._debug && this._logLaunchType >= 2) log(this.name, "Ship " + shp.shipName + " was to be launched in system " + shp.system + " - purging");
					clearlist.push(i);
				}
			} // for

			// clear the array of all ships we launched
			var i = clearlist.length;
			while (i--) {
				this._launching.splice(clearlist[i], 1);
			}
			if (this._launching.length > 0) {
				if (this._debug && this._logLaunchType >= 2) {
					log(this.name, "Launching queue length: " + this._launching.length);
					if (this._shipMonitor === 1) {
						log(this.name, "=====Current launching queue data=====");
						for (var i = 0; i < this._launching.length; i++) {
							this.$writeDataToLog(this._launching[i], "Launching queue item " + i);
						}
					}
				}
			}
		} // if
	}

	//-------------------------------------------------------------------------------------------------------------
	// populates the docked vessels in each station in the system
	this.$populateStationList = function $populateStationList(timeRange) {
		// don't create anything if the system is nova or going nova
		if (system.sun && (system.sun.isGoingNova || system.sun.hasGoneNova)) return;

		this._autoGenerationInProgress = true;

		// has the station indexes function been run?
		if (this.$getStationIndex(system.mainStation) === 0) {
			this.$stationIndexes();
		}

		// work out the min value to send to the calculateRandomDepartureTime routine
		var slots = this.$calculateSlots(timeRange);

		if (this._debug && this._logPopulatorType >= 2) log(this.name, "timeRange = " + timeRange + ", slot = " + slots);

		// go through all stations in the system
		var stns = system.stations;
		// make sure the player's station goes in first
		if (player.ship.dockedStation && this._repopulateStation == null && player.ship.dockedStation.hasNPCTraffic && this.$checkStationRoleForNoTraffic(player.ship.dockedStation) === false) this.$applyTimeRange(player.ship.dockedStation, slots, timeRange);
		// loop through the available stations
		stns.forEach(function (station) {
			if (station.hasNPCTraffic && this.$checkStationRoleForNoTraffic(station) === false && (this._repopulateStation == null || station === this._repopulateStation)) {
				this.$applyTimeRange(station, slots, timeRange)
			}
		}, this);
	}

	//-------------------------------------------------------------------------------------------------------------
	this.$getPopulatorVars = function $getPopulatorVars() {
		// What follows is an attempt to recreate the oolite-populator functions for outgoing ships.
		// The idea is to put the ships into the docking queue of any stations in the system.
		// Then, on each call of the systemWillRepopulate function, the new function will look for any ships
		// that are due to launch and launch them, based on the template stored in _systemDockingData array.
		// The goal is to have a similar spread of ships launching from stations as would happen with the
		// original script. The only part of the original script I couldn't do was to check the count of
		// various ship types in system, and then launch extras as required. This would maintain a reasonable
		// balance of different ship types. But because this system assumes the ships have to be in a station
		// somewhere, that balancing aspect is gone. Hopefully the deeper sense of realism counteracts this
		// to some degree.

		// This function will be called each time a player enters a new system to populate the docks of all
		// stations in system.

		// the logic here is to use the default populator chance values in ranges:
		// eg for the traders: from 0 - 0.09 is trader freighters, 0.09 to 0.095 is couriers, 0.095 to 0.10 is smugglers, 0.10 to 0.11 is assassins, 0.11 to 0.12 is shuttles
		// finally, we add on the possibility of skipping a vessel.
		// then, a random number is selected from the total range, between 0 and the max value
		// that will hopefully return a similar spread of vessels as the original code.

		var w = worldScripts["oolite-populator"];
		// set up all the ranges for data selection
		var base = 0;
		this._populatorVars = {};
		this._populatorVars["rangeTradeFreight"] = [0, w.$repopulatorFrequencyOutgoing.traderFreighters];
		base += w.$repopulatorFrequencyOutgoing.traderFreighters;
		this._populatorVars["rangeTradeCourier"] = [base, base + w.$repopulatorFrequencyOutgoing.traderCouriers];
		base += w.$repopulatorFrequencyOutgoing.traderCouriers;
		this._populatorVars["rangeTradeSmuggl"] = [base, base + w.$repopulatorFrequencyOutgoing.traderSmugglers];
		base += w.$repopulatorFrequencyOutgoing.traderSmugglers;
		this._populatorVars["rangeAssassin"] = [base, base + w.$repopulatorFrequencyOutgoing.assassins];
		base += w.$repopulatorFrequencyOutgoing.assassins;
		this._populatorVars["rangeShuttle"] = [base, base + (0.005 * system.info.techlevel)];
		base += (0.005 * system.info.techlevel);
		// set the max for this data set, including a skip vessel factor, if required
		this._populatorVars["traderMax"] = base + (this._traderSkipVesselFactor > 0 ? ((5 - base) / this._traderSkipVesselFactor) : 0);

		base = 0;
		this._populatorVars["rangePirateInd"] = [0, w.$repopulatorFrequencyOutgoing.pirateIndependents];
		base += w.$repopulatorFrequencyOutgoing.pirateIndependents;
		this._populatorVars["rangePirateLight"] = [base, base + w.$repopulatorFrequencyOutgoing.pirateLightPacks];
		base += w.$repopulatorFrequencyOutgoing.pirateLightPacks;
		this._populatorVars["rangePirateMedium"] = [base, base + w.$repopulatorFrequencyOutgoing.pirateMediumPacks];
		base += w.$repopulatorFrequencyOutgoing.pirateMediumPacks;
		this._populatorVars["rangePirateHeavy"] = [base, base + w.$repopulatorFrequencyOutgoing.pirateHeavyPacks];
		base += w.$repopulatorFrequencyOutgoing.pirateHeavyPacks;
		// set the max for this data set, including a skip vessel factor, if required
		this._populatorVars["pirateMax"] = base + (this._pirateSkipVesselFactor > 0 ? ((4 - base) / this._pirateSkipVesselFactor) : 0);

		base = 0;
		this._populatorVars["rangeHunterLight"] = [0, w.$repopulatorFrequencyOutgoing.hunterLightPacks];
		base += w.$repopulatorFrequencyOutgoing.hunterLightPacks;
		this._populatorVars["rangeHunterMedium"] = [base, base + w.$repopulatorFrequencyOutgoing.hunterMediumPacks];
		base += w.$repopulatorFrequencyOutgoing.hunterMediumPacks;
		this._populatorVars["rangeHunterHeavy"] = [base, base + w.$repopulatorFrequencyOutgoing.hunterHeavyPacks];
		base += w.$repopulatorFrequencyOutgoing.hunterHeavyPacks;
		// set the max for this data set, including a skip vessel factor, if required
		this._populatorVars["hunterMax"] = base + (this._hunterSkipVesselFactor > 0 ? ((3 - base) / this._hunterSkipVesselFactor) : 0);
	}

	//-------------------------------------------------------------------------------------------------------------
	this.$stationIsRepopulating = function $stationIsRepopulating(station) {
		if (this._populateStation.length === 0) return false;
		for (var i = 0; i < this._populateStation.length; i++) {
			if (this._populateStation[i].stn.displayName === station.displayName) return true;
		}
		return false;
	}

	//-------------------------------------------------------------------------------------------------------------
	this.$applyTimeRange = function $applyTimeRange(station, slots, timeRange) {
		// go through all stations in the system
		var stns = system.stations;
		if (slots === 0) slots = this.$calculateSlots(timeRange);
		// make sure the player's station goes in first
		if (this.$stationIsRepopulating(station) === false) {
			this._populateStation.push({
				stn: station,
				slots: slots,
				timeRange: timeRange
			});
		} else {
			for (var i = 0; i < this._populateStation.length; i++) {
				var item = this._populateStation[i];
				if (item.stn === station) {
					if (item.timeRange < timeRange) {
						item.slots = slots;
						item.timeRange = timeRange;
					}
					break;
				}
			}
		}
	}

	//-------------------------------------------------------------------------------------------------------------
	this.$calculateSlots = function $calculateSlots(timeRange) {
		var slots = 0;
		if (timeRange < 10) slots = 10;
		//if (10 <= timeRange && timeRange < 60) slots = 10;
		//if (60 <= timeRange && timeRange < 180) slots = 9;
		//if (180 <= timeRange && timeRange < 420) slots = 8;
		//if (420 <= timeRange && timeRange < 660) slots = 7;
		//if (660 <= timeRange && timeRange < 1260) slots = 6;
		//if (1260 <= timeRange && timeRange < 1320) slots = 5;
		//if (1320 <= timeRange && timeRange < 1350) slots = 4;
		//if (1350 <= timeRange && timeRange < 1435) slots = 3;
		//if (1435 <= timeRange) slots = 2; // full refresh from this point
		if (10 <= timeRange && timeRange < 60) slots = 10;
		if (60 <= timeRange && timeRange < 180) slots = 8;
		if (180 <= timeRange && timeRange < 420) slots = 6;
		if (420 <= timeRange && timeRange < 660) slots = 4;
		if (660 <= timeRange) slots = 2; // full refresh from this point
		return slots;
	}

	//-------------------------------------------------------------------------------------------------------------
	this.$runPopulation = function $runPopulation() {
		// is there anything in our processing array?
		if (this._populateStation.length === 0) return;

		// check for being in space and red alert - skip doing anything to prevent any glitches
		if (player.ship.isInSpace && player.alertCondition === 3) return;

		// have we done a pre-population of the ship dock values yet?
		// if not, run the process now
		/*if (!this._doShipDockDataGrab[this._populateStation[0].stn.shipClassName]) {
			this.$grabAllShipDockValues(this._populateStation[0].stn);
			this._doShipDockDataGrab[this._populateStation[0].stn.shipClassName] = true;
		}*/

		if (system.isInterstellarSpace) {
			// remove all interstellar stations from the array
			this._populateStation.splice(0, 1);
			return;
		}

		// make sure we don't trip over ourselves
		if (this._running === true) return;
		if (this._jumpStarted === true) return;
		this._running = true;
		var trueValues = this._trueValues;
		var logging = 0;
		if (this._debug && this._logPopulatorType >= 2) logging = 1;

		if (!this._populatorVars) this.$getPopulatorVars();
		var popVars = this._populatorVars;

		var station = this._populateStation[0].stn;
		var slots = this._populateStation[0].slots;
		var random = Math.random;
		var randomInt = this.$rand;
		var indexInList = this.$indexInList;
		var calcIns = this.$calcInsurance;
		var shipDataRef = this._shipData;

		// && "galcop neutral chaotic pirate hunter".indexOf(station.allegiance) >= 0
		if (this._debug && this._logPopulatorType >= 2) {
			var startDate = new Date();
			log(this.name, "==============================================================================");
			log(this.name, "station === " + station);
		}
		// get the index for this station
		var stnIndex = this.$getStationIndex(station);
		var stationkey = station.name + "_" + stnIndex;

		var countTraderFreighter = 0;
		var countTraderCourier = 0;
		var countTraderSmuggler = 0;
		var countAssassin = 0;
		var countShuttle = 0;
		var countPirateInd = 0;
		var countPirateLight = 0;
		var countPirateMedium = 0;
		var countPirateHeavy = 0;
		var countHunterLight = 0;
		var countHunterMedium = 0;
		var countHunterHeavy = 0;

		// create some random data if our array is empty
		// work out number of ships to add to array
		var calctype = 0; // type 0 = original calculation seeds, type 1 = beo's version

		switch (calctype) {
			case 0:
				var factor = 0;
				// hour many trading partners does this system have
				factor += this._neighbours.length * 5;
				// more stable governments mean more ships in system
				factor += system.government;
				// more advanced systems mean more ships in system
				factor += system.techLevel;
				// how many ships are already in dock
				var curr = this.$countStationItems(station);
				// work out the max and min values
				var max = factor * 6;
				var min = factor * 2;
				break;
			case 1: // variation suggested by forum member beo
				var factor = 1;
				// hour many trading partners does this system have
				factor += this._neighbours.length * 3;
				// more stable governments mean more ships in system
				factor += Math.round(system.government / 4);
				// more advanced systems mean more ships in system
				factor += Math.round(system.techLevel / 7);
				// how many ships are already in dock
				var curr = this.$countStationItems(station);
				// work out the max and min values
				var max = factor * 5;
				var min = factor * 2;
				break;
		}

		// work out high/medium/low traffic variations:
		if (this.$checkStationRoleForMediumTraffic(station)) {
			max = parseInt(max / 1.8);
			min = parseInt(min / 1.8);
			//if (this._debug && this._logPopulatorType >= 2) 
				//log(this.name, station.name + " selected for medium traffic (F:" + factor + ", Max:" + max + ", Min:" + min + ")");
		} else if (this.$checkStationRoleForLowTraffic(station)) {
			max = parseInt(max / 4);
			min = parseInt(min / 4);
			//if (this._debug && this._logPopulatorType >= 2) 
				//log(this.name, station.name + " selected for low traffic (F:" + factor + ", Max:" + max + ", Min:" + min + ")");
		} else if (this.$checkStationRoleForVeryLowTraffic(station)) {
			max = parseInt(max / 10);
			min = parseInt(min / 10);
			//if (this._debug && this._logPopulatorType >= 2) 
				//log(this.name, station.name + " selected for very low traffic (F:" + factor + ", Max:" + max + ", Min:" + min + ")");
		} else {
			//if (this._debug && this._logPopulatorType >= 2) 
				//log(this.name, station.name + " selected for normal traffic (F:" + factor + ", Max:" + max + ", Min:" + min + ")");
		}

		// calculate a random number in our range
		var ships = (randomInt(max - min) + min) - curr;
		if (ships > 300) ships = 300; // upper limit

		//if (this._debug && this._logPopulatorType > 0) log(this.name, "Initial estimate of " + ships + " for " + station.name + "(" + stnIndex + ") (" + station.primaryRole + ")");

		if (ships > 0) {
			// work out which ship roles to add to the station
			if (this._debug) log(this.name, stationkey + ": Estimated ship count = " + ships);
			// create the departure slots array for this station
			this.$createDepartureSlots();
			// if we have existing items in this station, remove their slots from the array
			if (this.$countStationItems(station) !== 0) {
				this.$removeExistingSlots(station);
			}

			var actualCount = 0;

			var i = ships;
			while (i--) {
				var shpType = "";
				var shpDataKey = "";
				var shpName = "";
				var role = "";

				var selection = 0;

				var selPirate = random();
				var selHunter = random();

				var depart = 0;
				var pilot = {};
				var bounty = 0;
				var home = 0;
				var species = "";
				var shipDockTime = 0;
				var insurance = 0;
				var destSystem = -1;
				var destHidden = false;
				var aiName = "";
				var skill = 0;
				var weapons = 0;
				var heat = 0;
				var groupName = "";
				var isLeader = false;
				var equip = "";
				var escortGroupName = "";
				var goods = "";
				var escorts = [];
				var groupData = [];
				var autoAI = "";

				var choose = 0;

				depart = this.$calculateRandomDepartureTime(station, stnIndex, slots, true);
				if (depart === -1) continue;

				shipDockTime = (depart * 60 + clock.adjustedSeconds) - (randomInt(720) * 60); // arrival time was up to 12 hours prior to their scheduled departure time

				// work out what type of ship we are going to add
				// some of this will be based on what station type we are currently looking at
				// note: some OXP stations can end up with "null" for station allegiance - in this instance assume "neutral";
				var alleg = station.allegiance;
				if (alleg == null) alleg = "neutral";

				if (alleg === "neutral" || (alleg === "galcop" && selHunter < 0.5) || (alleg === "chaotic" && selPirate < 0.5)) {
					choose = random() * popVars.traderMax;
					if (choose >= popVars.rangeTradeFreight[0] && choose < popVars.rangeTradeFreight[1]) selection = 1;
					if (choose >= popVars.rangeTradeCourier[0] && choose < popVars.rangeTradeCourier[1]) selection = 2;
					if (choose >= popVars.rangeTradeSmuggl[0] && choose < popVars.rangeTradeSmuggl[1]) selection = 3;
					if (choose >= popVars.rangeAssassin[0] && choose < popVars.rangeAssassin[1]) selection = 4;
					if (choose >= popVars.rangeShuttle[0] && choose < popVars.rangeShuttle[1]) selection = 5;
				}

				if (alleg === "pirate" || (alleg === "chaotic" && selPirate >= 0.5)) {
					choose = random() * popVars.pirateMax;
					if (choose >= popVars.rangePirateInd[0] && choose < popVars.rangePirateInd[1]) selection = 6;
					if (choose >= popVars.rangePirateLight[0] && choose < popVars.rangePirateLight[1]) selection = 7;
					if (choose >= popVars.rangePirateMedium[0] && choose < popVars.rangePirateMedium[1]) selection = 8;
					if (choose >= popVars.rangePirateHeavy[0] && choose < popVars.rangePirateHeavy[1]) selection = 9;
				}

				if (alleg === "hunter" || (alleg === "galcop" && selHunter >= 0.5)) {
					choose = random() * popVars.hunterMax;
					if (choose >= popVars.rangeHunterLight[0] && choose < popVars.rangeHunterLight[1]) selection = 10;
					if (choose >= popVars.rangeHunterMedium[0] && choose < popVars.rangeHunterMedium[1]) selection = 11;
					if (choose >= popVars.rangeHunterHeavy[0] && choose < popVars.rangeHunterHeavy[1]) selection = 12;
				}

				if (selection > 0) actualCount += 1;

				switch (selection) {
					case 1: // trader
						countTraderFreighter += 1;
						role = "trader";
						shpDataKey = this.$getRandomShipKeyWithDockCheck(role, station);
						shpName = this.$getRandomShipName(role);
						autoAI = shipDataRef[shpDataKey]["auto_ai"];
						shpType = shipDataRef[shpDataKey].name;

						home = system.ID;
						var dest = this.$getDestinationByRole(role, station);
						destSystem = dest.destination;
						destHidden = dest.destinationHidden;

						// occasionally make the home system the same as the dest system so not every ship in dock calls this system home
						if (random() < 0.3) home = this.$nearbySystem(7);

						var hasAutoAI = (indexInList(autoAI, trueValues) >= 0);
						// not sure if the destination will stick for ships with a specific AI, so just hide the destination so there's no confusion
						if (destHidden === false && hasAutoAI === false) destHidden = true;

						goods = "PLENTIFUL_GOODS";

						if (typeof autoAI === "undefined" || hasAutoAI === true) aiName = "oolite-traderAI.js";

						if (shpType.search(new RegExp("medical", "i")) !== -1) {
							goods = "MEDICAL_GOODS";
							bounty = 0;
						} else {
							if (random() < 0.05) {
								bounty = Math.ceil(random() * 20);
								if (random() < 0.5 && hasAutoAI === true) {
									aiName = "oolite-traderOpportunistAI.js";
									if (indexInList(shipDataRef[shpDataKey]["auto_weapons"], trueValues) >= 0) {
										skill = 2;
										weapons = 2.5;
									}
									goods = "PIRATE_GOODS";
								}
							} else {
								bounty = 0;
							}
						}

						pilot = this.$createPilot((random() < 0.5 ? home : destSystem), bounty, calcIns(bounty));

						if (indexInList(shpDataKey, this._switchEscortsToGroup) >= 0) {
							var groupInfo = this.$processTraderEscorts(shpDataKey);
							groupData = groupInfo.escortData;
							isLeader = groupInfo.leader;
							if (isLeader === true) {
								groupName = "shipgroup-" + this._groupCounter;
								this._groupCounter += 1;
							}
						} else {
							var escortInfo = this.$processTraderEscorts(shpDataKey);
							escorts = escortInfo.escortData;
							isLeader = escortInfo.leader;
							escortGroupName = escortInfo.name;
						}

						this.$addShipToArray({
							station: station,
							stationIndex: stnIndex,
							role: role,
							shipType: shpType,
							shipDataKey: shpDataKey,
							shipName: shpName,
							aiName: aiName,
							accuracy: this.$setAccuracy(shpDataKey, skill),
							equipment: this.$setWeapons(shpDataKey, weapons),
							heatInsulation: this.$setHeatInsulation(shpDataKey, 0),
							homeSystem: home,
							bounty: bounty,
							escortName: escortGroupName,
							escortLeader: (escortGroupName != "" ? isLeader : false),
							groupName: groupName,
							groupLeader: (groupName != "" ? isLeader : false),
							destinationSystem: destSystem,
							destinationHidden: destHidden,
							goods: goods,
							pilot: pilot,
							dockTime: shipDockTime,
							departureTime: depart,
							departureSeconds: 0,
							autoLog: logging
						});

						break;

					case 2: // courier
						countTraderCourier += 1;
						role = "trader-courier";
						if (this.$roleExists(role) === false) role = "trader";
						shpDataKey = this.$getRandomShipKeyWithDockCheck(role, station);
						shpName = this.$getRandomShipName(role);
						autoAI = shipDataRef[shpDataKey]["auto_ai"];
						shpType = shipDataRef[shpDataKey].name;

						var hasAutoAI = (indexInList(autoAI, trueValues) >= 0);
						if (typeof autoAI === "undefined" || hasAutoAI === true) aiName = "oolite-traderAI.js";

						bounty = 0;
						heat = 6;
						home = system.ID;
						// occasionally make the home system the same as the dest system so not every ship in dock calls this system home
						if (random() < 0.3) home = this.$nearbySystem(7);

						var dest = this.$getDestinationByRole(role, station);
						destSystem = dest.destination;
						destHidden = dest.destinationHidden;

						// not sure if the destination will stick for ships with a specific AI, so just hide the destination so there's no confusion
						if (destHidden === false && hasAutoAI === false) destHidden = true;

						pilot = this.$createPilot((random() < 0.5 ? home : destSystem), bounty, calcIns(bounty));
						goods = "PLENTIFUL_GOODS";

						if (indexInList(shpDataKey, this._switchEscortsToGroup) >= 0) {
							var groupInfo = this.$processTraderEscorts(shpDataKey);
							groupData = groupInfo.escortData;
							isLeader = groupInfo.leader;
							if (isLeader === true) {
								groupName = "shipgroup-" + this._groupCounter;
								this._groupCounter += 1;
							}
						} else {
							var escortInfo = this.$processTraderEscorts(shpDataKey);
							escorts = escortInfo.escortData;
							isLeader = escortInfo.leader;
							escortGroupName = escortInfo.name;
						}

						this.$addShipToArray({
							station: station,
							stationIndex: stnIndex,
							role: role,
							shipType: shpType,
							shipDataKey: shpDataKey,
							shipName: shpName,
							aiName: aiName,
							accuracy: this.$setAccuracy(shpDataKey, skill),
							equipment: this.$setWeapons(shpDataKey, weapons),
							heatInsulation: heat,
							homeSystem: home,
							bounty: bounty,
							escortName: escortGroupName,
							escortLeader: (escortGroupName != "" ? isLeader : false),
							groupName: groupName,
							groupLeader: (groupName != "" ? isLeader : false),
							destinationSystem: destSystem,
							destinationHidden: destHidden,
							goods: goods,
							pilot: pilot,
							dockTime: shipDockTime,
							departureTime: depart,
							departureSeconds: 0,
							autoLog: logging
						});

						break;

					case 3: // smuggler
						countTraderSmuggler += 1;
						role = "trader-smuggler";
						if (this.$roleExists(role) === false) role = "trader";
						shpDataKey = this.$getRandomShipKeyWithDockCheck(role, station);
						shpName = this.$getRandomShipName(role);
						autoAI = shipDataRef[shpDataKey]["auto_ai"];
						shpType = shipDataRef[shpDataKey].name;

						var hasAutoAI = (indexInList(autoAI, trueValues) >= 0);
						if (typeof autoAI === "undefined" || hasAutoAI === true) aiName = "oolite-traderAI.js";

						bounty = Math.ceil(random() * 20);
						weapons = 1.2;
						home = system.ID;
						// occasionally make the home system the same as the dest system so not every ship in dock calls this system home
						if (random() < 0.3) home = this.$nearbySystem(7);
						var dest = this.$getDestinationByRole(role, station);
						destSystem = dest.destination;
						destHidden = dest.destinationHidden;

						// not sure if the destination will stick for ships with a specific AI, so just hide the destination so there's no confusion
						if (destHidden === false && hasAutoAI === false) destHidden = true;

						var maxcargo = parseInt(shipDataRef[shpDataKey]["max_cargo"]);
						if (bounty > maxcargo * 2) bounty = maxcargo * 2;

						goods = "ILLEGAL_GOODS";

						if (indexInList(shpDataKey, this._switchEscortsToGroup) >= 0) {
							var groupInfo = this.$processTraderEscorts(shpDataKey);
							groupData = groupInfo.escortData;
							isLeader = groupInfo.leader;
							if (isLeader === true) {
								groupName = "shipgroup-" + this._groupCounter;
								this._groupCounter += 1;
							}
						} else {
							var escortInfo = this.$processTraderEscorts(shpDataKey);
							escorts = escortInfo.escortData;
							isLeader = escortInfo.leader;
							escortGroupName = escortInfo.name;
						}

						equip = this.$setWeapons(shpDataKey, weapons);

						if (indexInList(shipDataRef[shpDataKey]["auto_weapons"], trueValues) >= 0) equip += "EQ_FUEL_INJECTION,"; // smugglers always have injectors

						pilot = this.$createPilot((random() < 0.5 ? home : destSystem), bounty, calcIns(bounty));

						this.$addShipToArray({
							station: station,
							stationIndex: stnIndex,
							role: role,
							shipType: shpType,
							shipDataKey: shpDataKey,
							shipName: shpName,
							aiName: aiName,
							accuracy: this.$setAccuracy(shpDataKey, skill),
							equipment: equip,
							heatInsulation: this.$setHeatInsulation(shpDataKey, 0),
							homeSystem: home,
							bounty: bounty,
							escortName: escortGroupName,
							escortLeader: (escortGroupName != "" ? isLeader : false),
							groupName: groupName,
							groupLeader: (groupName != "" ? isLeader : false),
							destinationSystem: destSystem,
							destinationHidden: destHidden,
							goods: goods,
							pilot: pilot,
							dockTime: shipDockTime,
							departureTime: depart,
							departureSeconds: 0,
							autoLog: logging
						});

						break;

					case 4: // assassin
						countAssassin += 1;
						role = "assassin-light";
						var g = system.info.government + 2;
						skill = 0;
						weapons = 2;
						if (random() > g / 10) {
							role = "assassin-medium";
							skill = 1;
							weapons = 2.5;
							if (random() > g / 5) {
								role = "assassin-heavy";
								weapons = 2.8;
							}
						}
						if (this.$roleExists(role) === false) break; // if there are no assassin roles defined, don't try to create one.

						shpDataKey = this.$getRandomShipKeyWithDockCheck(role, station);
						shpName = this.$getRandomShipName(role);
						autoAI = shipDataRef[shpDataKey]["auto_ai"];
						shpType = shipDataRef[shpDataKey].name;

						home = system.ID;
						// occasionally make the home system the same as the dest system so not every ship in dock calls this system home
						if (random() < 0.3) home = this.$nearbySystem(7);
						var dest = this.$getDestinationByRole(role, station);
						destSystem = dest.destination;;
						destHidden = dest.destinationHidden;

						equip = this.$setWeapons(shpDataKey, weapons);
						if (indexInList(shipDataRef[shpDataKey]["auto_weapons"], trueValues) >= 0) {
							equip += "EQ_FUEL_INJECTION,";
							equip += "EQ_ECM,";
							if (2 + random() < weapons) {
								equip += "EQ_SHIELD_BOOSTER,";
							}
							// assassins don't respect escape pods and won't expect anyone
							// else to either.
							equip += "X:EQ_ESCAPE_POD,";
						}

						if (skill > 0) {
							groupName = "shipgroup-" + this._groupCounter;
							this._groupCounter += 1;
							isLeader = true;
						}
						var hasAutoAI = (indexInList(autoAI, trueValues) >= 0);
						// this should be set by the role, but just in case
						if (typeof autoAI === "undefined" || hasAutoAI === true) aiName = "oolite-assassinAI.js";
						// not sure if the destination will stick for ships with a specific AI, so just hide the destination so there's no confusion
						if (destHidden === false && hasAutoAI === false) destHidden = true;

						pilot = this.$createPilot(system.ID, 0, calcIns(0));
						this.$addShipToArray({
							station: station,
							stationIndex: stnIndex,
							role: role,
							shipType: shpType,
							shipDataKey: shpDataKey,
							shipName: shpName,
							aiName: aiName,
							accuracy: this.$setAccuracy(shpDataKey, skill),
							equipment: equip,
							heatInsulation: this.$setHeatInsulation(shpDataKey, 1),
							homeSystem: home,
							bounty: 0,
							escortName: "",
							escortLeader: false,
							groupName: groupName,
							groupLeader: isLeader,
							destinationSystem: destSystem,
							destinationHidden: destHidden,
							goods: "",
							pilot: pilot,
							dockTime: shipDockTime,
							departureTime: depart,
							departureSeconds: 0,
							autoLog: logging
						});

						if (skill > 0) {
							var numext = Math.floor(random() * 3) + 1;
							var ext_role = "";
							var ext_shpType = "";
							var ext_shpDataKey = "";
							var ext_shpName = "";
							var ext_pilot = [];
							var ext_equip = "";

							for (var j = 0; j < numext; j++) {
								countAssassin += 1;
								if (role === "assassin-heavy") {
									ext_role = "assassin-medium";
								} else {
									ext_role = "assassin-light";
								}
								ext_shpDataKey = this.$getRandomShipKey(ext_role);
								ext_shpName = this.$getRandomShipName(ext_role);
								ext_shpType = shipDataRef[ext_shpDataKey].name;

								ext_equip = this.$setWeapons(ext_shpDataKey, 1.8);
								if (indexInList(shipDataRef[ext_shpDataKey]["auto_weapons"], trueValues) >= 0) {
									ext_equip += "EQ_FUEL_INJECTION,";
									ext_equip += "X:EQ_ESCAPE_POD,";
								}

								aiName = "";
								// this should be set by the role, but just in case
								if (typeof shipDataRef[ext_shpDataKey]["auto_ai"] === "undefined" || indexInList(shipDataRef[ext_shpDataKey]["auto_ai"], trueValues) >= 0) aiName = "oolite-assassinAI.js";

								ext_pilot = this.$createPilot(this.$nearbySystem(7), 0, calcIns(0));

								this.$addShipToArray({
									station: station,
									stationIndex: stnIndex,
									role: ext_role,
									shipType: ext_shpType,
									shipDataKey: ext_shpDataKey,
									shipName: ext_shpName,
									aiName: aiName,
									accuracy: this.$setAccuracy(ext_shpDataKey, 0),
									equipment: ext_equip,
									heatInsulation: this.$setHeatInsulation(shpDataKey, 1),
									homeSystem: home,
									bounty: 0,
									escortName: "",
									escortLeader: false,
									groupName: groupName,
									groupLeader: false,
									destinationSystem: destSystem,
									destinationHidden: destHidden,
									goods: "",
									pilot: ext_pilot,
									dockTime: shipDockTime,
									departureTime: depart,
									departureSeconds: 5,
									autoLog: logging
								});

							}
						}

						break;

					case 5: // shuttle
						countShuttle += 1;
						role = "shuttle";
						if (this.$roleExists(role) === false) break; // if there are no shuttles defined, don't try to create one.
						shpDataKey = this.$getRandomShipKey(role);
						shpName = this.$getRandomShipName(role);
						autoAI = shipDataRef[shpDataKey]["auto_ai"];
						shpType = shipDataRef[shpDataKey].name;

						// this should be set by the role, but just in case
						aiName = "";
						var hasAutoAI = (indexInList(autoAI, trueValues) >= 0);
						// some of griffs have auto_ai = "undefined" which I'm assuming means "yes"
						if (typeof autoAI === "undefined" || hasAutoAI === true) aiName = "oolite-shuttleAI.js";

						home = system.ID;
						var dest = this.$getDestinationByRole(role, station);
						destSystem = dest.destination;
						destHidden = dest.destinationHidden;
						var destLoc = dest.location;

						// even shuttles will hide their destination
						//if (random() < 0.3) destHidden = true;
						// hide their destination if they have a custom AI - we don't know what they'll do!
						if (aiName === "") destHidden = true;

						pilot = this.$createPilot(home, 0, calcIns(0));

						this.$addShipToArray({
							station: station,
							stationIndex: stnIndex,
							role: role,
							shipType: shpType,
							shipDataKey: shpDataKey,
							shipName: shpName,
							aiName: aiName,
							accuracy: this.$setAccuracy(shpDataKey, 1),
							equipment: this.$setWeapons(shpDataKey, 0),
							heatInsulation: this.$setHeatInsulation(shpDataKey, 1),
							homeSystem: home,
							bounty: 0,
							escortName: "",
							escortLeader: false,
							groupName: "",
							groupLeader: false,
							destinationSystem: destSystem,
							destination: destLoc,
							destinationHidden: destHidden,
							goods: "",
							pilot: pilot,
							dockTime: shipDockTime,
							departureTime: depart,
							departureSeconds: 0,
							autoLog: logging
						});

						break;

					case 6: // pirate independant
						countPirateInd += 1;
						role = "pirate";
						var groupSize = 0;
						groupName = "shipgroup-" + this._groupCounter;
						this._groupCounter += 1;

						goods = ""; // pirates launching from station will have empty holds so they can scoop some
						home = system.ID;
						// occasionally make the home system the same as the dest system so not every ship in dock calls this system home
						if (random() < 0.3) home = this.$nearbySystem(7);
						var dest = this.$getDestinationByRole(role, station);
						destSystem = dest.destination;
						destHidden = dest.destinationHidden;

						// same logic as orginal for calculating group size
						groupSize = Math.floor(random() * 3) + Math.floor(random() * 3) + 2;
						if (groupSize > 8 - system.info.government) {
							// in the safer systems may have lost some ships already, though
							groupSize = 1 + Math.floor(random() * groupSize);
						}

						isLeader = true;

						escorts.length = 0;

						for (var j = 0; j < groupSize; j++) {
							countPirateInd += 1;
							shpDataKey = this.$getRandomShipKeyWithDockCheck(role, station);
							shpName = this.$getRandomShipName(role);
							autoAI = shipDataRef[shpDataKey]["auto_ai"];
							shpType = shipDataRef[shpDataKey].name;

							bounty = 20 + system.info.government + groupSize + Math.floor(random() * 8);
							if (indexInList(shipDataRef[shpDataKey]["hyperspace_motor"], trueValues) >= 0) {
								weapons = 1.75;
							} else {
								weapons = 1.3;
							}

							aiName = "";
							if (typeof autoAI === "undefined" || indexInList(autoAI, trueValues) >= 0) aiName = "oolite-pirateAI.js";

							pilot = this.$createPilot(this.$nearbySystem(7), bounty, calcIns(bounty));

							this.$addShipToArray({
								station: station,
								stationIndex: stnIndex,
								role: role,
								shipType: shpType,
								shipDataKey: shpDataKey,
								shipName: shpName,
								aiName: aiName,
								accuracy: this.$setAccuracy(shpDataKey, 4 - system.info.government),
								equipment: this.$setWeapons(shpDataKey, 1.75),
								heatInsulation: this.$setHeatInsulation(shpDataKey, 0),
								homeSystem: home,
								bounty: bounty,
								escortName: "",
								escortLeader: false,
								groupName: groupName,
								groupLeader: isLeader,
								destinationSystem: destSystem,
								destinationHidden: destHidden,
								goods: goods,
								pilot: pilot,
								dockTime: shipDockTime,
								departureTime: depart,
								departureSeconds: (isLeader ? 0 : 5),
								autoLog: logging
							});

							// only the first ship is the leader
							isLeader = false;
						}

						break;

					case 7: // light pirate group
						countPirateLight += 1;
						role = "pirate-light-freighter";
						if (this.$roleExists(role) === false) role = "pirate";

						groupName = "shipgroup-" + this._groupCounter;
						this._groupCounter += 1;

						var leader = this.$addPirateLeader(station, stnIndex, 0.83, role, groupName, shipDockTime, depart);
						// add the rest of the group
						this.$addPirateGroup(station, stnIndex, groupName, leader.homeSystem, leader.destination, leader.destinationHidden, 2, 1, -1, 0, shipDockTime, depart);

						home = leader.homeSystem;
						destSystem = leader.destination;
						destHidden = leader.destinationHidden;

						if (indexInList(leader.dataKey, this._switchEscortsToGroup) >= 0) {
							groupData = this.$processPirateEscorts(stationkey, leader, groupName);
						} else {
							escorts = this.$processPirateEscorts(stationkey, leader, "");
						}
						break;

					case 8: // medium pirate group
						countPirateMedium += 1;
						role = "pirate-medium-freighter";
						if (this.$roleExists(role) === false) role = "pirate";

						groupName = "shipgroup-" + this._groupCounter;
						this._groupCounter += 1;

						var leader = this.$addPirateLeader(station, stnIndex, 0.5, role, groupName, shipDockTime, depart);
						// add the rest of the group
						this.$addPirateGroup(station, stnIndex, groupName, leader.homeSystem, leader.destination, leader.destinationHidden, 3, 2, 0, 1, shipDockTime, depart);

						home = leader.homeSystem;
						destSystem = leader.destination;
						destHidden = leader.destinationHidden;

						if (indexInList(leader.dataKey, this._switchEscortsToGroup) >= 0) {
							groupData = this.$processPirateEscorts(stationkey, leader, groupName);
						} else {
							escorts = this.$processPirateEscorts(stationkey, leader, "");
						}
						break;

					case 9: // heavy pirate group
						countPirateHeavy += 1;
						role = "pirate-medium-freighter";
						if (this.$roleExists(role) === false) role = "pirate";

						groupName = "shipgroup-" + this._groupCounter;
						this._groupCounter += 1;

						var leader = this.$addPirateLeader(station, stnIndex, 0.5, role, groupName, shipDockTime, depart);
						// add the rest of the group
						this.$addPirateGroup(station, stnIndex, groupName, leader.homeSystem, leader.destination, leader.destinationHidden, 4, 4, 2, 2, shipDockTime, depart);

						home = leader.homeSystem;
						destSystem = leader.destination;
						destHidden = leader.destinationHidden;

						if (indexInList(leader.dataKey, this._switchEscortsToGroup) >= 0) {
							groupData = this.$processPirateEscorts(stationkey, leader, groupName);
						} else {
							escorts = this.$processPirateEscorts(stationkey, leader, "");
						}
						break;

					case 10: // light hunter group
						countHunterLight += 1;
						role = "hunter";

						groupName = "shipgroup-" + this._groupCounter;
						this._groupCounter += 1;
						groupSize = Math.floor(random() * 2) + Math.floor(random() * 2) + 2;

						home = system.ID;
						var dest = this.$getDestinationByRole(role, station);
						destSystem = dest.destination;
						destHidden = dest.destinationHidden;

						weapons = 1.5;
						skill = -1;
						isLeader = true;

						for (var j = 0; j < groupSize; j++) {
							shpDataKey = this.$getRandomShipKeyWithDockCheck(role, station);
							shpName = this.$getRandomShipName(role);
							autoAI = shipDataRef[shpDataKey]["auto_ai"];
							shpType = shipDataRef[shpDataKey].name;

							aiName = "";
							// this should normally be set by the role, but just in case...
							if (typeof autoAI === "undefined" || indexInList(autoAI, trueValues) >= 0) aiName = "oolite-bountyHunterAI.js";

							pilot = this.$createPilot(home, 0, calcIns(0));

							this.$addShipToArray({
								station: station,
								stationIndex: stnIndex,
								role: role,
								shipType: shpType,
								shipDataKey: shpDataKey,
								shipName: shpName,
								aiName: aiName,
								accuracy: this.$setAccuracy(shpDataKey, 0),
								equipment: this.$setWeapons(shpDataKey, weapons),
								heatInsulation: this.$setHeatInsulation(shpDataKey, 0),
								homeSystem: home,
								bounty: 0,
								escortName: "",
								escortLeader: false,
								groupName: groupName,
								groupLeader: isLeader,
								destinationSystem: destSystem,
								destinationHidden: destHidden,
								goods: "",
								pilot: pilot,
								dockTime: shipDockTime,
								departureTime: depart,
								departureSeconds: (isLeader ? 0 : 5),
								autoLog: logging
							});

							// only the first ship is the leader
							isLeader = false;
						}

						break;

					case 11: // medium hunter group
						countHunterMedium += 1;
						role = "hunter-medium";

						if (this.$roleExists(role) === false) role = "hunter";

						home = system.ID;
						var dest = this.$getDestinationByRole(role, station);
						destSystem = dest.destination;
						destHidden = dest.destinationHidden;

						groupName = "shipgroup-" + this._groupCounter;
						this._groupCounter += 1;

						this.$addHunterLeader(station, stnIndex, role, groupName, destSystem, destHidden, shipDockTime, depart);
						this.$addHunterGroup(station, stnIndex, groupName, destSystem, destHidden, shipDockTime, depart);

						break;

					case 12: // heavy hunter group
						countHunterHeavy += 1;
						role = "hunter-heavy";

						if (this.$roleExists(role) === false) role = "hunter";

						home = system.ID;
						var dest = this.$getDestinationByRole(role, station);
						destSystem = dest.destination;
						destHidden = dest.destinationHidden;

						groupName = "shipgroup-" + this._groupCounter;
						this._groupCounter += 1;

						this.$addHunterLeader(station, stnIndex, role, groupName, destSystem, destHidden, shipDockTime, depart);
						this.$addHunterGroup(station, stnIndex, groupName, destSystem, destHidden, shipDockTime, depart);

						break;

				} // switch

				// add any escorts that were defined
				if ((escorts && escorts.length > 0) || (groupData && groupData.length > 0)) {
					var dta = escorts;
					if (groupData) dta = groupData;
					for (var j = 0; j < dta.length; j++) {
						var esc_role = dta[j].role;
						var esc_key = dta[j].shipKey;
						var esc_bounty = 0;
						var esc_name = this.$getRandomShipName(esc_role);
						// make sure the key is in the dictionary
						if (!shipDataRef[esc_key]) {
							log(this.name, "!NOTE: Escort ship key not in core dictionary - " + esc_role + "/" + esc_key);
							shipDataRef[esc_key] = Ship.shipDataForKey(esc_key);
						}
						var esc_autoAI = shipDataRef[esc_key]["auto_ai"];
						if (shipDataRef[esc_key]) {
							var esc_type = shipDataRef[esc_key].name;
							if (bounty !== 0) esc_bounty = 3 + Math.floor(random() * 12);
							var esc_pilot = this.$createPilot(this.$nearbySystem(7), esc_bounty, calcIns(esc_bounty));
							aiName = "";
							if (typeof esc_autoAI === "undefined" || indexInList(esc_autoAI, trueValues) >= 0) aiName = "oolite-escortAI.js";

							if (indexInList(shipDataRef[esc_key]["auto_weapons"], trueValues) >= 0) {
								if (esc_role === "escort" || esc_role === "pirate-light-fighter") {
									weapons = 1.3; // usually lightly armed as escorts
								} else if (esc_role === "escort-medium" || esc_role === "pirate-medium-fighter") {
									weapons = 1.8; // usually heavily armed as escorts
								} else if (esc_role === "escort-heavy" || esc_role === "pirate-heavy-fighter") {
									weapons = 2.05; // rarely have an aft laser
								}
							}
							// note: add 0.5 seconds to the escort members so they will always come after the leader in any sorted list
							this.$addShipToArray({
								station: station,
								stationIndex: stnIndex,
								role: esc_role,
								shipType: esc_type,
								shipDataKey: esc_key,
								shipName: esc_name,
								aiName: aiName,
								accuracy: this.$setAccuracy(esc_key, skill),
								equipment: this.$setWeapons(esc_key, weapons),
								heatInsulation: this.$setHeatInsulation(esc_key, heat),
								homeSystem: home,
								bounty: esc_bounty,
								escortName: (dta === escorts ? escortGroupName : ""),
								escortLeader: false,
								groupName: (dta === groupData ? groupName : ""),
								groupLeader: false,
								destinationSystem: destSystem,
								destinationHidden: destHidden,
								goods: "",
								pilot: esc_pilot,
								dockTime: shipDockTime,
								departureTime: depart,
								departureSeconds: 5,
								autoLog: logging
							});

						} else {
							log(this.name, "!!NOTE: No ship data returned for ship data key " + dta[j].shipKey);
						}
					} // for j
				} // if escorts/groupdata
			} // while i--
			/*if (this._debug && this._logPopulatorType > 0) {
				log(this.name, "Actual ships created: " + actualCount + ", difference of " + (ships - actualCount));
				log(this.name, "Traders added:        " + countTraderFreighter);
				log(this.name, "Couriers added:       " + countTraderCourier);
				log(this.name, "Smugglers added:      " + countTraderSmuggler);
				log(this.name, "Assassins added:      " + countAssassin);
				log(this.name, "Shuttles added:       " + countShuttle);
				log(this.name, "Pirate Ind added:     " + countPirateInd);
				log(this.name, "Pirate Light groups:  " + countPirateLight);
				log(this.name, "Pirate Medium groups: " + countPirateMedium);
				log(this.name, "Pirate Heavy groups:  " + countPirateHeavy);
				log(this.name, "Hunter Light groups:  " + countHunterLight);
				log(this.name, "Hunter Medium groups: " + countHunterMedium);
				log(this.name, "Hunter Heavy groups:  " + countHunterHeavy);
			}*/
			this._departureBands.length = 0;
		} // if ships

		// set up process for adding docking bay codes
		this.$addBayCodes(station);

		// remove this station from the array
		this._populateStation.splice(0, 1);
		// check if the array is empty - if so, turn off the auto generation flag
		if (this._populateStation.length === 0) {
			this._autoGenerationInProgress = false;
		}

		this._running = false;
		if (this._debug && this._logPopulatorType > 0)
			log(this.name, "Populator run complete in ms: " + (new Date().getTime() - startDate.getTime()));
	}

	//-------------------------------------------------------------------------------------------------------------
	// returns a pilot object
	this.$createPilot = function $createPilot(home, legal, insurance) {

		var pilot = {};

		// create the pilot
		pilot["name"] = expandDescription("%N [nom]");
		pilot["description"] = "a " + System.infoForSystem(galaxyNumber, home).inhabitant.toLowerCase() + " from " + System.systemNameForID(home); // check
		pilot["homeSystem"] = home;
		pilot["legalStatus"] = legal;
		pilot["insurance"] = (!insurance == null ? insurance : 0);
		pilot["species"] = System.infoForSystem(galaxyNumber, home).inhabitant.toLowerCase();

		return pilot;
	}

	//-------------------------------------------------------------------------------------------------------------
	// adds a ship to the stations docked list
	// used by the populator function
	this.$addShipToArray = function $addShipToArray(dockObj) {

		// work out any defaults
		var stnIndex = (dockObj.hasOwnProperty("stationIndex") ? dockObj.stationIndex : this.$getStationIndex(dockObj.station));
		var bay = ((this._autoGenerationInProgress === true) ? "" : this.$getDockingBayCode(dockObj.station, stnIndex));
		var hide = (dockObj.hasOwnProperty("destinationHidden") && dockObj.destinationHidden === true ? true : false);
		var pers = (dockObj.hasOwnProperty("personality") ? dockObj.personality : (this.$rand(32768) - 1));
		var fuel = (dockObj.hasOwnProperty("fuel") ? dockObj.fuel : 7);
		var lastChange = (dockObj.hasOwnProperty("lastChange") ? dockObj.lastChange : clock.adjustedSeconds);
		var cargo = (dockObj.hasOwnProperty("cargo") ? dockObj.cargo : "");
		var tradeComplete = (dockObj.hasOwnProperty("tradeComplete") ? dockObj.tradeComplete : 0);
		var dest = (dockObj.hasOwnProperty("destination") && dockObj.destination !== "" ? dockObj.destination : "");

		if (this._systemDockingData[system.ID] == null) {
			this._systemDockingData[system.ID] = {};
		}
		if (this._systemDockingData[system.ID][dockObj.station.name + "_" + stnIndex] == null) {
			this._systemDockingData[system.ID][dockObj.station.name + "_" + stnIndex] = [];
		}
		this._systemDockingData[system.ID][dockObj.station.name + "_" + stnIndex].push({
			system: system.ID,
			station: dockObj.station.name,
			stationIndex: stnIndex,
			shipName: dockObj.shipName,
			shipType: dockObj.shipType,
			shipDataKey: dockObj.shipDataKey,
			personality: pers,
			primaryRole: dockObj.role,
			shipAI: dockObj.aiName,
			accuracy: dockObj.accuracy,
			equipment: dockObj.equipment,
			heatInsulation: dockObj.heatInsulation,
			fuel: fuel,
			homeSystem: dockObj.homeSystem,
			bounty: dockObj.bounty,
			escortName: dockObj.escortName,
			escortLeader: dockObj.escortLeader,
			groupName: dockObj.groupName,
			groupLeader: dockObj.groupLeader,
			destinationSystem: dockObj.destinationSystem,
			destination: dest,
			destinationHidden: hide,
			goods: dockObj.goods,
			cargo: cargo,
			tradeComplete: tradeComplete,
			pilotDescription: dockObj.pilot.description,
			pilotName: dockObj.pilot.name,
			pilotHomeSystem: dockObj.pilot.homeSystem,
			pilotLegalStatus: dockObj.pilot.legalStatus,
			pilotInsurance: dockObj.pilot.insurance,
			pilotSpecies: dockObj.pilot.species,
			isPlayer: false,
			dockTime: dockObj.dockTime,
			dockingBay: bay,
			departureTime: dockObj.departureTime,
			departureSeconds: dockObj.departureSeconds,
			lastChange: lastChange,
			launchCallback: (dockObj.hasOwnProperty("launchCallback") && dockObj.launchCallback !== "" ? dockObj.launchCallback : null),
			worldScript: (dockObj.hasOwnProperty("worldScript") && dockObj.worldScript !== "" ? dockObj.worldScript : null),
			additionalProperties: (dockObj.hasOwnProperty("properties") && dockObj.properties !== "" ? dockObj.properties : "")
		});

		if (dockObj.autoLog === 2) {
			this.$writeDataToLog(this._systemDockingData[system.ID][dockObj.station.name + "_" + stnIndex][this._systemDockingData[system.ID][dockObj.station.name + "_" + stnIndex].length - 1], "Docking ship at station " + dockObj.station.name + "(" + stnIndex + ")");
		} else if (dockObj.autoLog === 1) {
			this.$writeShortDataToLog(this._systemDockingData[system.ID][dockObj.station.name + "_" + stnIndex][this._systemDockingData[system.ID][dockObj.station.name + "_" + stnIndex].length - 1]);
		}

	}

	//-------------------------------------------------------------------------------------------------------------
	// adds a pirate leader and returns the ship data key
	this.$addPirateLeader = function $addPirateLeader(station, stnIndex, localFactor, role, groupName, dockTime, depart) {

		var bounty = 60 + system.government + Math.floor(Math.random() * 8);
		var home = system.ID;
		var destSystem;
		var destHidden = true;
		var aiName = "";
		var trueValues = this._trueValues;

		// occasionally make the home system the same as the dest system so not every ship in dock calls this system home
		if (Math.random() < 0.3) home = this.$nearbySystem(7);
		if (Math.random() < localFactor) {
			destSystem = system.ID;
		} else {
			var dest = this.$getDestinationByRole(role, station);
			destSystem = dest.destination;
			destHidden = dest.destinationHidden;
		}

		var shpDataKey = this.$getRandomShipKeyWithDockCheck(role, station);
		var shpName = this.$getRandomShipName(role);
		var autoAI = this._shipData[shpDataKey]["auto_ai"];
		var shpType = this._shipData[shpDataKey].name;

		if (typeof autoAI === "undefined" || this.$indexInList(autoAI, trueValues) >= 0) aiName = "oolite-pirateFreighterAI.js";

		var pilot = this.$createPilot(this.$nearbySystem(7), bounty, this.$calcInsurance(bounty));

		var equip = this.$setWeapons(shpDataKey, 2.8);
		if (this.$indexInList(this._shipData[shpDataKey]["auto_weapons"], trueValues) >= 0) {
			equip += "EQ_SHIELD_BOOSTER,";
			equip += "EQ_ECM,";
		}

		// add the leader
		this.$addShipToArray({
			station: station,
			stationIndex: stnIndex,
			role: role,
			shipType: shpType,
			shipDataKey: shpDataKey,
			shipName: shpName,
			aiName: aiName,
			accuracy: this.$setAccuracy(shpDataKey, 3),
			equipment: equip,
			heatInsualtion: this.$setHeatInsulation(shpDataKey, 0),
			homeSystem: home,
			bounty: bounty,
			escortName: "",
			escortLeader: false,
			groupName: groupName,
			groupLeader: true,
			destinationSystem: destSystem,
			destinationHidden: destHidden,
			goods: "",
			pilot: pilot,
			dockTime: dockTime,
			dockingBay: "",
			departureTime: depart,
			departureSeconds: 0,
			autoLog: (this._debug && this._logPopulatorType >= 2 ? 1 : 0)
		});

		return {
			dataKey: shpDataKey,
			name: shpName,
			type: shpType,
			homeSystem: home,
			destination: destSystem,
			destinationHidden: destHidden
		};
	}

	//-------------------------------------------------------------------------------------------------------------
	// adds members of a pirate group
	this.$addPirateGroup = function $addPirateGroup(station, stnIndex, groupName, home, dest, destHidden, lf, mf, hf, thug, dockTime, depart) {

		for (var i = Math.floor(lf + (0.5 + Math.random() - Math.random())); i > 0; i--) {
			this.$addPirateAssistant("pirate-light-fighter", station, stnIndex, groupName, home, dest, destHidden, dockTime, depart);
		}
		for (var i = Math.floor(mf + (0.5 + Math.random() - Math.random())); i > 0; i--) {
			this.$addPirateAssistant("pirate-medium-fighter", station, stnIndex, groupName, home, dest, destHidden, dockTime, depart);
		}
		for (var i = Math.floor(hf + (0.5 + Math.random() - Math.random())); i > 0; i--) {
			this.$addPirateAssistant("pirate-heavy-fighter", station, stnIndex, groupName, home, dest, destHidden, dockTime, depart);
		}
		for (var i = Math.floor(thug + (0.5 + Math.random() - Math.random())); i > 0; i--) {
			this.$addPirateAssistant("pirate-interceptor", station, stnIndex, groupName, home, dest, destHidden, dockTime, depart);
		}

	}

	//-------------------------------------------------------------------------------------------------------------
	// adds the individual member of a pirate group
	this.$addPirateAssistant = function $addPirateAssistant(role, station, stnIndex, groupName, home, dest, destHidden, dockTime, depart) {

		if (this.$roleExists(role) === false) role = "pirate";

		var shpDataKey = this.$getRandomShipKey(role);
		var shpName = this.$getRandomShipName(role);
		var shpType = this._shipData[shpDataKey].name;
		var trueValues = this._trueValues;

		var bounty = 0;
		var equip = "";
		var aiName = "";
		var autoAI = this._shipData[shpDataKey]["auto_ai"];
		var autoWeapons = this._shipData[shpDataKey]["auto_weapons"];
		var hasAutoAI = (this.$indexInList(autoAI, trueValues) >= 0);

		if (typeof autoAI === "undefined" || hasAutoAI === true) aiName = "oolite-pirateFighterAI.js";

		if (role === "pirate-interceptor") {
			if (typeof autoAI === "undefined" || hasAutoAI === true) aiName = "oolite-pirateInterceptorAI.js";

			equip += this.$setWeapons(shpDataKey, 2.3);
			if (this.$indexInList(autoWeapons, trueValues) >= 0) equip += "EQ_FUEL_INJECTION,";

			bounty = 50 + system.government + Math.floor(Math.random() * 36);
		} else {
			bounty = 20 + system.government + Math.floor(Math.random() * 12);
			if (role === "pirate-light-fighter") {
				equip += this.$setWeapons(shpDataKey, 1.2); // basic fighters
			} else if (role === "pirate-medium-fighter") {
				equip += this.$setWeapons(shpDataKey, 1.8); // often beam weapons
			} else if (role === "pirate-heavy-fighter") {
				equip += this.$setWeapons(shpDataKey, 2.05); // very rarely aft lasers
			}
		}

		var pilot = this.$createPilot(this.$nearbySystem(7), bounty, this.$calcInsurance(bounty));

		this.$addShipToArray({
			station: station,
			stationIndex: stnIndex,
			role: role,
			shipType: shpType,
			shipDataKey: shpDataKey,
			shipName: shpName,
			aiName: aiName,
			accuracy: this.$setAccuracy(shpDataKey, 0),
			equipment: equip,
			heatInsulation: this.$setHeatInsulation(shpDataKey, 0),
			homeSystem: home,
			bounty: bounty,
			escortName: "",
			escortLeader: false,
			groupName: groupName,
			groupLeader: false,
			destinationSystem: dest,
			destinationHidden: destHidden,
			goods: "",
			pilot: pilot,
			dockTime: dockTime,
			dockingBay: "",
			departureTime: depart,
			departureSeconds: 5,
			autoLog: (this._debug && this._logPopulatorType >= 2 ? 1 : 0)
		});
	}

	//-------------------------------------------------------------------------------------------------------------
	// process pirate escort data for a given leader object
	this.$processPirateEscorts = function $processPirateEscorts(stationkey, leader, groupName) {

		if (typeof leader === "undefined" || typeof groupName === "undefined") return null;

		var escorts = this.$getEscortData(leader.dataKey);
		if (escorts && escorts.length > 0) {
			if (groupName === "") {
				this._escortCounter += 1;
				var escortGroupName = "escortgroup-" + this._escortCounter;
			} else {
				this._groupCounter += 1;
			}
			// find leader in data and update escort info
			// should be last entry, so start from the end
			var dta = this._systemDockingData[system.ID][stationkey];
			if (dta && dta.length > 0) {
				var i = dta.length;
				while (i--) {
					var item = dta[i];
					if (item.shipDataKey === leader.dataKey && item.shipType === leader.type && item.shipName === leader.name) {
						if (groupName === "") {
							item.escortName = escortGroupName;
							item.escortLeader = true;
						} else {
							item.groupName = groupName;
							item.groupLeader = true;
						}
						break;
					}
				}
			}
		}
		return escorts;
	}

	//-------------------------------------------------------------------------------------------------------------
	// process escort info for a given ship data key
	this.$processTraderEscorts = function $processTraderEscorts(shipDataKey) {

		var escorts = this.$getEscortData(shipDataKey);
		var isLeader = false;
		var escortName = "";

		if (escorts && escorts.length > 0) {
			this._escortCounter += 1;
			escortName = "escortgroup-" + this._escortCounter;
			isLeader = true;
		}

		return {
			escortData: escorts,
			leader: isLeader,
			name: escortName
		};
	}

	//-------------------------------------------------------------------------------------------------------------
	// adds a hunter leader
	this.$addHunterLeader = function $addHunterLeader(station, stnIndex, role, groupName, destSystem, destHidden, dockTime, depart) {

		var shpDataKey = this.$getRandomShipKeyWithDockCheck(role, station);
		var shpName = this.$getRandomShipName(role);
		var autoAI = this._shipData[shpDataKey]["auto_ai"];
		var shpType = this._shipData[shpDataKey].name;

		var equip = "";
		if (role === "hunter-heavy") {
			// occasionally give heavy hunters aft lasers
			equip += this.$setWeapons(shpDataKey, 2.2);
		} else {
			// usually ensure medium hunters have beam lasers
			equip += this.$setWeapons(shpDataKey, 1.9);
		}
		var skill = 3; // likely to be good pilot

		var aiName = "";
		// auto AI will normally get this already but not if the hunter
		// addition used fallback roles
		if (typeof autoAI === "undefined" || this.$indexInList(autoAI, this._trueValues) >= 0) aiName = "oolite-bountyHunterLeaderAI.js";

		var pilot = this.$createPilot(system.ID, 0, this.$calcInsurance(0));

		this.$addShipToArray({
			station: station,
			stationIndex: stnIndex,
			role: role,
			shipType: shpType,
			shipDataKey: shpDataKey,
			shipName: shpName,
			aiName: aiName,
			accuracy: this.$setAccuracy(shpDataKey, skill),
			equipment: equip,
			heatInsulation: this.$setHeatInsulation(shpDataKey, 1),
			homeSystem: system.ID,
			bounty: 0,
			escortName: "",
			escortLeader: false,
			groupName: groupName,
			groupLeader: true,
			destinationSystem: destSystem,
			destinationHidden: destHidden,
			goods: "",
			pilot: pilot,
			dockTime: dockTime,
			dockingBay: "",
			departureTime: depart,
			departureSeconds: 0,
			autoLog: (this._debug && this._logPopulatorType >= 2 ? 1 : 0)
		});
	}

	//-------------------------------------------------------------------------------------------------------------
	// adds the members of a hunter group
	this.$addHunterGroup = function $addHunterGroup(station, stnIndex, groupName, destSystem, destHidden, dockTime, depart) {

		var role = "hunter";

		var groupSize = 1 + Math.floor(Math.random() * 4) + Math.floor(Math.random() * 4);

		for (var i = 0; i < groupSize; i++) {
			var shpDataKey = this.$getRandomShipKey(role);
			var shpName = this.$getRandomShipName(role);
			var autoAI = this._shipData[shpDataKey]["auto_ai"];
			var shpType = this._shipData[shpDataKey].name;

			var equip = this.$setWeapons(shpDataKey, 1.5);
			var pilot = this.$createPilot(this.$nearbySystem(7), 0, this.$calcInsurance(0));
			var aiName = "";
			// this should normally be set by the role, but just in case...
			if (typeof autoAI === "undefined" || this.$indexInList(autoAI, this._trueValues) >= 0) aiName = "oolite-bountyHunterAI.js";

			this.$addShipToArray({
				station: station,
				stationIndex: stnIndex,
				role: role,
				shipType: shpType,
				shipDataKey: shpDataKey,
				shipName: shpName,
				aiName: aiName,
				accuracy: this.$setAccuracy(shpDataKey, 0),
				equipment: equip,
				heatInsulation: this.$setHeatInsulation(shpDataKey, 1),
				homeSystem: system.ID,
				bounty: 0,
				escortName: "",
				escortLeader: false,
				groupName: groupName,
				groupLeader: false,
				destinationSystem: destSystem,
				destinationHidden: destHidden,
				goods: "",
				pilot: pilot,
				dockTime: dockTime,
				dockingBay: "",
				departureTime: depart,
				departureSeconds: 5,
				autoLog: (this._debug && this._logPopulatorType >= 2 ? 1 : 0)
			});
		}

	}

	//=============================================================================================================
	// helper functions

	//-------------------------------------------------------------------------------------------------------------
	// return a random number between 1 and max
	this.$rand_old = function $rand_old(max) {
		return Math.floor((Math.random() * max) + 1);
	}
	// contributed by Day
	this.$rand = function $rand(max) {
		var that = $rand;
		var floor = (that.floor = that.floor || Math.floor);
		var random = (that.random = that.random || Math.random);
		return floor((random() * max) + 1);
	}

	//-------------------------------------------------------------------------------------------------------------
	// gets a subset of vessels for viewing
	this.$getViewingList = function $getViewingList(station, mfd) {

		var stationList = [];
		var idx = this.$getStationIndex(station);

		if (station != null && idx > 0) {
			// add entries from the main data repository
			var dta = this._systemDockingData[system.ID][station.name + "_" + idx];
			if (dta && dta.length > 0) {
				for (var i = 0; i < dta.length; i++) {
					var item = dta[i];
					if (item.departureTime > 0) {
						stationList.push(item);
					}
				}
			}
			// add any ships on the launching queue to the list
			if (this._launching.length > 0) {
				for (var i = 0; i < this._launching.length; i++) {
					var item = this._launching[i];
					if (item.system === system.ID && item.station === station.name && item.stationIndex === idx) {
						stationList.push(item);
					}
				}
			}
			// add any docking ships to the list, if this isn't an MFD call
			if (mfd === false) this.$addShipsDockingToList(station, stationList);
		}

		return stationList;
	}

	//-------------------------------------------------------------------------------------------------------------
	this.$addShipsDockingToList = function $addShipsDockingToList(station, viewlist) {
		var shipname = "";

		if (this._docking && this._docking.length > 0) {
			for (var i = 0; i < this._docking.length; i++) {
				var shp = this._docking[i];
				if (shp.isValid) {
					shipname = shp.shipUniqueName;
					// occasionally the shipUniqueName returns "" even though it's really there. In that instance, pull the unique name out of the display name
					if (shipname === "") shipname = this.$getShipUniqueName(shp.displayName, shp.shipClassName);

					var escortlead = (shp.escortGroup && shp.escortGroup.leader === shp ? true : false);
					var grouplead = (shp.group && shp.group.leader === shp ? true : false);
					// add just enough info to the list, rather than all the details
					viewlist.push({
						system: system.ID,
						station: station.name,
						shipName: shipname,
						shipType: shp.shipClassName,
						shipDataKey: shp.dataKey,
						personality: shp.entityPersonality,
						primaryRole: shp.primaryRole,
						bounty: shp.bounty,
						destinationSystem: -1,
						escortName: ((shp.escortGroup) ? shp.escortGroup.name : ""),
						escortLeader: (escortlead ? true : false),
						groupName: ((shp.group) ? shp.group.name : ""),
						groupLeader: (grouplead ? true : false),
						pilotDescription: (shp.crew ? shp.crew[0].description : ""),
						pilotName: (shp.crew ? shp.crew[0].name : ""),
						pilotHomeSystem: (shp.crew ? shp.crew[0].homeSystem : -1),
						pilotLegalStatus: (shp.crew ? shp.crew[0].legalStatus : 0),
						pilotSpecies: (shp.crew ? shp.crew[0].species : ""),
						isPlayer: false,
						dockTime: -1,
						departureTime: -10,
						departureSeconds: (escortlead || grouplead ? 5 : 0)
					});
				}
			}
		}
	}

	//-------------------------------------------------------------------------------------------------------------
	// returns the number of docked ships at a given station
	this.$countStationItems = function $countStationItems(station) {
		var items = 0;
		var idx = this.$getStationIndex(station);
		if (this._systemDockingData[system.ID] != null) {
			var dta = this._systemDockingData[system.ID][station.name + "_" + idx];
			if (dta && dta.length > 0) {
				for (var i = 0; i < dta.length; i++) {
					if (dta[i].departureTime > -5) items += 1;
				}
			}
		}
		return items;
	}

	//-------------------------------------------------------------------------------------------------------------
	// returns the number of docked ships in the system
	this.$countSystemItems = function $countSystemItems() {
		var items = 0;
		if (!this._systemDockingData[system.ID]) return 0;
		var skeys = Object.keys(this._systemDockingData[system.ID]);
		for (var kc = 0; kc < skeys.length; kc++) {
			var dta = this._systemDockingData[system.ID][skeys[kc]];
			if (dta && dta.length > 0) items += dta.length;
		}
		return items;
	}

	//-------------------------------------------------------------------------------------------------------------
	// returns data for a particular group
	this.$getShipGroupInfo = function $getShipGroupInfo(stationkey, groupName) {
		var groupData = null;

		var dta = this._systemDockingData[system.ID][stationkey];
		if (dta && dta.length > 0) {
			for (var i = 0; i < dta.length; i++) {
				var item = dta[i];
				if (item.groupName === groupName && item.groupLeader === true) {
					// if we find the leader of the group, return their departure time immediately
					return {
						depart: item.departureTime,
						departSecs: item.departureSeconds,
						destination: item.destinationSystem,
						destinationHidden: item.destinationHidden
					};
				}
				if (item.groupName === groupName && item.groupLeader === false) {
					// if we just find a group member, hold the departure value, in case the leader is stored later in the array
					groupData = {
						depart: item.departureTime,
						departSecs: item.departureSeconds,
						destination: item.destinationSystem,
						destinationHidden: item.destinationHidden
					};
				}
			}
		}
		return groupData;
	}

	//-------------------------------------------------------------------------------------------------------------
	// returns data for a particular escort group
	this.$getShipEscortInfo = function $getShipEscortInfo(stationkey, escortName) {
		var escortData = null;

		var dta = this._systemDockingData[system.ID][stationkey];
		if (dta && dta.length > 0) {
			for (var i = 0; i < dta.length; i++) {
				var item = dta[i];
				if (item.escortName === escortName && item.escortLeader === true) {
					// if we find the leader of the group, return their departure time immediately
					return {
						depart: item.departureTime,
						departSecs: item.departureSeconds,
						destination: item.destinationSystem,
						destinationHidden: item.destinationHidden
					};
				}
				if (item.system === system.ID && item.escortName === escortName && item.escortLeader === false) {
					// if we just find a group member, hold the departure value, in case the leader is stored later in the array
					escortData = {
						depart: item.departureTime,
						departSecs: item.departureSeconds,
						destination: item.destinationSystem,
						destinationHidden: item.destinationHidden
					};
				}
			}
		}
		return escortData;
	}

	//-------------------------------------------------------------------------------------------------------------
	// returns true if the leader of a particular group is docked, otherwise false
	this.$isGroupLeaderDocked = function $isGroupLeaderDocked(stationkey, groupName) {
		var dta = this._systemDockingData[system.ID][stationkey];
		if (dta && dta.length > 0) {
			for (var i = 0; i < dta.length; i++) {
				var item = dta[i];
				if ((item.groupName === groupName && item.groupLeader === true) ||
					(item.escortName === groupName && item.escortLeader === true)) {
					return true;
				}
			}
		}
		return false;
	}

	//-------------------------------------------------------------------------------------------------------------
	// updates the leader of an escort with an escort group name
	// we need to do this because, when the leader docks, we may not know whether their group is for an escort or a normal group
	// when a ship with an escort role docks and links up to the leader, we need to update the leader record
	this.$setGroupEscortLeader = function $setGroupEscortLeader(stationkey, escortName) {
		var dta = this._systemDockingData[system.ID][stationkey];
		if (dta && dta.length > 0) {
			for (var i = 0; i < dta.length; i++) {
				var item = dta[i];
				if (item.groupName === escortName && item.escortLeader === true) {
					if (this._debug && this._logDockingType > 0) log(this.name, "Updating escort name of group leader " + item.shipType + (item.shipName !== "" ? ": " + item.shipName : ""));
					item.escortName = escortName;
					item.groupName = "";
					item.groupLeader = false;
					break;
				}
			}
		}
	}

	//-------------------------------------------------------------------------------------------------------------
	// clears any ships that would have launched after player exits witchspace, regardless of system
	// note: not called any longer, as a shorter method is in place
	this.$clearLaunchedShips = function $clearLaunchedShips() {

		// clear the launching list
		this._launching.length = 0;

		var remove = false;

		// compile short list of neighbouring systems
		var neighbour_list = [];
		for (var i = 0; i < this._neighbours.length; i++) {
			neighbour_list.push(this._neighbours[i].systemID);
		}

		var syslist = Object.keys(this._systemDockingData);
		for (var sl = 0; sl < syslist.length; sl++) {
			var skeys = Object.keys(this._systemDockingData[syslist[sl]]);
			for (var kc = 0; kc < skeys.length; kc++) {
				var dta = this._systemDockingData[syslist[sl]][skeys[kc]];
				if (dta && dta.length > 0) {
					for (var i = dta.length - 1; i >= 0; i--) {
						var item = dta[i];
						remove = false;
						// remove all ships that are outside the list of neighbours
						if (this.$indexInList(syslist[sl], neighbour_list) === -1) remove = true;
						// remove all ships that would have launched, regardless of system
						if (remove === false && (item.departureTime * 60 + item.lastChange) <= clock.adjustedSeconds) remove = true;

						if (remove) dta.splice(i, 1);
					}
				}
			}
		}
	}

	//-------------------------------------------------------------------------------------------------------------
	// moves items from the main dataset into the launching array so the repopulator doesn't have to work so hard
	this.$updateLaunchingList = function $updateLaunchingList() {

		// don't do the check if we're about to do a repopulate routine, or we're in the middle of one
		if (this._repopulate || this._autoGenerationInProgress) return;

		this._launchListUpdating = true;

		if (this._systemDockingData[system.ID]) {
			var skeys = Object.keys(this._systemDockingData[system.ID]);
			for (var kc = 0; kc < skeys.length; kc++) {
				var dta = this._systemDockingData[system.ID][skeys[kc]];
				if (dta && dta.length > 0) {
					for (var i = dta.length - 1; i >= 0; i--) {
						var item = dta[i];
						var lt = (item.departureTime * 60) + item.lastChange;
						if (lt <= clock.adjustedSeconds) {
							// move it do the launching array (if it's within 4 minutes of the adjusted seconds, regardless of the state of the dock
							if ((clock.adjustedSeconds - lt) / 60 <= 4) {
								if (this._debug && this._logLaunchType >= 2) log(this.name, item.station + " adding launching item " + i + " - " + item.shipType + (item.shipName !== "" ? ": " + item.shipName : ""));
								this._launching.push(item);
							} else {
								if (this._debug && this._logLaunchType >= 2) log(this.name, item.station + " removing expired item " + i + " - " + item.shipType + (item.shipName !== "" ? ": " + item.shipName : ""));
							}
							// remove it from the main array
							dta.splice(i, 1);
						} else {
							item.departureTime -= 1;
							if (item.departureTime < 0) item.departureTime = 0;
							item.lastChange = clock.adjustedSeconds;
						}
					}
				}
			}
		}
		this._launchListUpdating = false;

	}

	//-------------------------------------------------------------------------------------------------------------
	// this is used when the player launches from a station to launch any ships that would have been in the queue ahead of them
	this.$dockingQueueShipClearance = function $dockingQueueShipClearance(station, timeLapse) {
		function compare(a, b) {
			return (a.departureTime * 10 + a.departureSeconds) - (b.departureTime * 10 + b.departureSeconds);
		}
		
		//log(this.name, station.displayName + " - " + timeLapse);
		var forceLaunch = [];
		var idx = this.$getStationIndex(station);
		var stationkey = station.name + "_" + idx;
		if (this._systemDockingData[system.ID] == null) return;
		var dta = this._systemDockingData[system.ID][stationkey];
		if (dta && dta.length > 0) {
			var i = dta.length;
			while (i--) {
				var item = dta[i];
				if (((item.departureTime * 60) + item.lastChange) <= clock.adjustedSeconds) {
					// move it do the launching array
					forceLaunch.push(item);
					// remove it from the main array
					dta.splice(i, 1);
				} else {
					item.departureTime -= timeLapse;
					item.lastChange = clock.adjustedSeconds;
				}
			}
		}
		// do the same with the launching list
		if (this._launching.length > 0) {
			var i = this._launching.length;
			while (i--) {
				var item = this._launching[i];
				// only do it if the departure time is still current (ie it hasn't been moved to "Rescheduled")
				if (item.station === station.name && item.stationIndex === idx && Math.round(item.departureTime) !== -5) {
					if (((item.departureTime * 60) + item.lastChange) <= clock.adjustedSeconds) {
						// move it do the launching array
						forceLaunch.push(item);
						// remove it from the array
						this._launching.splice(i, 1);
					} else {
						if (item.departureTime - timeLapse > 0) {
							item.departureTime -= timeLapse;
							item.lastChange = clock.adjustedSeconds;
						}
					}
				}
			}
		}

		forceLaunch.sort(compare);

		// push the ships out with the addShips function
		if (this._debug && this._logLaunchType > 0) log(this.name, "Clearing pending ships:" + forceLaunch.length + " -- timeLapse:" + timeLapse);

		var dock = null;
		var dockpos = null;
		// find the dock object of the station so we can position launched ships
		for (var i = 0; i < station.subEntities.length; i++) {
			var subent = station.subEntities[i];
			if (subent.isDock) {
				dock = subent;
				dockpos = station.position.add( //better formula for non-centered docks
					Vector3D(
						station.heading.direction().multiply(dock.position.z),
						station.vectorUp.direction().multiply(dock.position.y),
						station.vectorRight.direction().multiply(dock.position.x)
					)
				);
				break;
			}
		}

		var launchperiod = 4; // this defines the time for which ships will be created. If the difference in minutes between the departure time and the adjusted seconds is
		// less than or equal to this value, the ship will be created. Anything more than this and it will be assumed the ship jumped after launch.
		// the larger this number, the more crowded it will be around the station after launch
		var spread = 0;
		var dist = 2000;

		var last_pos = null;
		var last_group = "";
		var last_escort = "";

		if (forceLaunch.length > 0) {
			for (var i = 0; i < forceLaunch.length; i++) {
				var shp = forceLaunch[i];
				var lt = (shp.departureTime * 60) + shp.lastChange;
				// if their departure time is > 4 minutes and their destination is somewhere else, don't create them.
				// group members and escorts should still fall into this logic, as the destination system is added to each group/escort member, regardless of whether
				// they have a hyperdrive motor or not, and their departure time should be (almost) the same as their leader
				if ((clock.adjustedSeconds - lt) / 60 <= launchperiod) {
					// launch it
					var timediff = Math.abs(clock.adjustedSeconds - lt) / 60;
					timediff += 1; // make sure we never have a zero value

					// if it's a group or escort member, use the last position
					if ((shp.escortName !== "" && shp.escortName === last_escort) || (shp.groupName !== "" && shp.groupName === last_group) && last_pos !== null) {
						var pos = last_pos;
						spread = 1000;
					} else {
						// work out the position of this launch
						// the first items in the array will need to be further away, as they would have launched first
						var side = this.$rand(8);
						// move away from dock in straight line first...
						var pos = dockpos.add(station.vectorForward.multiply(timediff * dist));
						//var pos = dockpos.add(station.heading.direction().multiply(dist + timediff * dist));
						// based on which side was picked, move the ship in that direction
						switch (side) {
							case 1: // right
								pos = pos.add(station.vectorRight.multiply(timediff * dist));
								break;
							case 2: // up
								pos = pos.add(station.vectorUp.multiply(timediff * dist));
								break;
							case 3: // left
								pos = pos.add(station.vectorRight.multiply(timediff * dist * -1));
								break;
							case 4: // down
								pos = pos.add(station.vectorUp.multiply(timediff * dist * -1));
								break;
							case 5: // up and right
								pos = pos.add(station.vectorRight.multiply(timediff * dist));
								pos = pos.add(station.vectorUp.multiply(timediff * dist));
								break;
							case 6: // up and left
								pos = pos.add(station.vectorRight.multiply(timediff * dist * -1));
								pos = pos.add(station.vectorUp.multiply(timediff * dist));
								break;
							case 7: // down and left
								pos = pos.add(station.vectorRight.multiply(timediff * dist * -1));
								pos = pos.add(station.vectorUp.multiply(timediff * dist * -1));
								break;
							case 8: // down and right
								pos = pos.add(station.vectorRight.multiply(timediff * dist));
								pos = pos.add(station.vectorUp.multiply(timediff * dist * -1));
								break;
						}
						spread = 50;
					}

					// add ship to system
					var shpKey = shp.shipDataKey;
					// make sure a single version is launched (ie without escorts)
					if (this.$dockVersionExists(shpKey)) shpKey = "dock_" + shpKey;

					if (this._debug && this._logLaunchType >= 2) this.$writeDataToLog(shp, "Adding ship to system from " + shp.station);
					var ship = system.addShips("[" + shpKey + "]", 1, pos, spread);

					if (!ship || !ship[0]) {
						// Oops! no ship returned!
						// skip it this time and maybe next time it will launch
						log(this.name, "!!ERROR: No ship created");
						continue;
					}
					if (this._debug && this._logLaunchType >= 2) log(this.name, "Added ship: " + ship[0]);

					// update ship details
					this.$updateLaunchedShip(ship[0], shp);
					// if there is an attached callback, call it now.
					if (shp.launchCallback && shp.worldScript) {
						log(this.name, "callback " + shp.worldScript + "." + shp.launchCallback);
						var w = worldScripts[shp.worldScript];
						if (w) w[shp.launchCallback]("launched", ship[0]);
					}
					last_pos = pos;
					last_group = shp.groupName;
					last_escort = shp.escortName;
				} else { // if
					// if there is an attached callback, call it now.
					if (shp.launchCallback && shp.worldScript) {
						log(this.name, "callback " + shp.worldScript + "." + shp.launchCallback);
						var w = worldScripts[shp.worldScript];
						if (w) w[shp.launchCallback]("cleared", shp);
					}
					// logic here to add ships to destination system directly (ie. add to _systemDockingData)
					//if (this._debug) this.$writeDataToLog(shp, "Removing ship from data - assumed jump to other system or reached destination");
				}
			} // for
		} // if
	}

	//-------------------------------------------------------------------------------------------------------------
	// pushes details from the data array onto a launched ship
	this.$updateLaunchedShip = function $updateLaunchedShip(ship, shipData) {

		// push all the stored data onto the ship;
		if (oolite.compareVersion("1.80") < 0) {
			ship.entityPersonality = shipData.personality; // can only do this in Oolite 1.81 or greater
		}
		ship.primaryRole = shipData.primaryRole;
		// if skilled NPC's is in play, don't worry about the accuracy - it will be adjusted later
		if (this._skilledNPCs == false) {
			ship.accuracy = shipData.accuracy;
		}
		ship.heatInsulation = shipData.heatInsulation;
		ship.setBounty(shipData.bounty, "setup actions");
		ship.shipUniqueName = shipData.shipName;
		ship.homeSystem = shipData.homeSystem;

		// this is the "lying" option - essentially doing a bait-n-switch on the player.
		// but because there's no visible clue that this is happening it's kind of unfair.
		// one option would be to send some sort of comms message saying "Changing destination now", but that feels out of place to me.
		// anyway, for the moment this code is disabled

		// if this ship hasn't been hiding their destination, and they're not a shuttle, and either an independant ship or a group/escort leader, then....
		//if (shipData.destinationHidden === false && ship.primaryRole !== "shuttle" && ((shipData.groupName === "" && shipData.escortName === "") || (shipData.groupLeader === true || shipData.escortLeader === true))) {
		//	// there's a chance they're going to change their destination
		//	if (Math.random() < 0.2) {
		//		var newDest = this.$getDestinationByRole(shipData.primaryRole);
		//		if (newDest !== shipData.destinationSystem) {
		//			log(this.name, "!!ALERT: Ship " + ship.displayName + " is changing their destination on launch from " + shipData.destinationSystem + " to " + newDest);
		//		}
		//		shipData.destinationSystem = newDest;
		//	}
		// }

		ship.destinationSystem = shipData.destinationSystem;
		ship.fuel = shipData.fuel;

		// set the seed values so that the pilot's species can be defined by their home system
		var seed = "0 0 0 0 " + shipData.pilotHomeSystem.toString() + " 2";

		// todo: check what happens to "species"
		if (shipData.pilotDescription !== "") {
			ship.setCrew({
				name: shipData.pilotName,
				origin: shipData.pilotHomeSystem,
				insurance: shipData.pilotInsurance,
				bounty: shipData.pilotLegalStatus,
				short_description: shipData.pilotDescription,
				random_seed: seed
			});
		} else {
			ship.setCrew({
				name: shipData.pilotName,
				origin: shipData.pilotHomeSystem,
				insurance: shipData.pilotInsurance,
				bounty: shipData.pilotLegalStatus,
				random_seed: seed
			});
		}

		// if we have goods defined (but not 'cargo')
		if (shipData.goods !== "" && (!shipData.cargo || shipData.cargo === "")) {
			// put some cargo in the hold
			ship.setCargoType(shipData.goods);
		}
		// process any cargo data we have stored
		//if (this._NPCTrading === true && shipData.cargo !== "") {
		//	var td = worldScripts.StationDockInfo_NPCTrading;
		//	td.$processCargo_Launching(ship, shipData.cargo);
		// }

		if (shipData.equipment !== "") {
			// process all the equipment additions
			var items = shipData.equipment.split(",");
			for (var j = 0; j < items.length; j++) {
				var eq = items[j];
				if (eq.indexOf(":") >= 0) {
					var subitems = eq.split(":");
					switch (subitems[0]) {
						case "X":
							ship.removeEquipment(subitems[1]);
							break;
						case "FORE":
							ship.forwardWeapon = subitems[1];
							break;
						case "AFT":
							ship.aftWeapon = subitems[1];
							break;
						case "PORT":
							ship.portWeapon = subitems[1];
							break;
						case "STARBOARD":
							ship.starboardWeapon = subitems[1];
							break;
					}
				} else {
					if (eq !== "") {
						ship.awardEquipment(eq);
					}
				}
			}
		}

		// set the ai of the ship (if required)
		if (shipData.shipAI !== "") ship.switchAI(shipData.shipAI);

		// if a intra-system destination is set, set the destination to its position
		if (shipData.destination && shipData.destination !== "") {
			if (this._debug && this._logLaunchType >= 2) log(this.name, "Shuttle " + ship.displayName + " - attaching launch script");
			var w = worldScripts.StationDockControl_ShuttleLaunch;
			ship.script.$initialise = w.$initialise;
			ship.script.$setDestination = w.$setDestination;
			if (!ship.script.$sdc_hold_shipDied && ship.script.shipDied) ship.script.$sdc_hold_shipDied = ship.script.shipDied;
			ship.script.shipDied = w.npc_shipDied;
			if (!ship.script.$sdc_hold_shipWillEnterWormhole && ship.script.shipWillEnterWormhole) ship.script.$sdc_hold_shipWillEnterWormhole = ship.script.shipWillEnterWormhole;
			ship.script.shipWillEnterWormhole = w.npc_shipWillEnterWormhole;
			// set the mothership in that script
			ship.script._dest = shipData.destination;
			ship.script.$initialise();
		}

		// add any additional properties to the launching ship
		if (shipData.hasOwnProperty("additionalProperties") === true && shipData.additionalProperties !== "") {
			var keys = Object.keys(shipData.additionalProperties);
			for (var i = 0; i < keys.length; i++) {
				var key = keys[i];
				ship.script[key] = shipData.additionalProperties[key];
			}
		}

		// is this ship part of a group or escort?
		if (shipData.groupName !== "") {
			var stdGroup = null;
			// look for an existing group
			if (this._launchingGroups.length > 0) {
				for (var j = 0; j < this._launchingGroups.length; j++) {
					if (this._launchingGroups[j].name === shipData.groupName) {
						stdGroup = this._launchingGroups[j];
						break;
					}
				}
			}

			// if we didn't find a group, create a new one
			if (!stdGroup) {
				// is this ship the leader
				if (shipData.groupLeader) {
					// create a group with the ship as the leader
					stdGroup = new ShipGroup(shipData.groupName, ship);
					ship.group = stdGroup;
					if (this._debug && this._logLaunchType >= 2) log(this.name, "Creating new group with leader: " + ship.displayName + ", " + shipData.groupName + ", " + ship.scanClass);
				} else {
					// otherwise just create the group and add the ship individually
					stdGroup = new ShipGroup(shipData.groupName);
					ship.group = stdGroup;
					stdGroup.addShip(ship);
					if (this._debug && this._logLaunchType >= 2) log(this.name, "Creating new group: " + ship.displayName + ", " + shipData.groupName + ", " + ship.scanClass);
				}
				this._launchingGroups.push(stdGroup);
			} else {
				// if we found a group, and this is a standard group, just add the ship
				ship.group = stdGroup;
				stdGroup.addShip(ship);
				if (this._debug && this._logLaunchType >= 2) log(this.name, "Adding to existing group: " + ship.displayName + ", " + shipData.groupName + ", " + ship.scanClass);
				// is the ship the leader
				if (shipData.groupLeader) {
					// add the ship as the leader
					stdGroup.leader = ship;
				} else {
					ship.performEscort();
				}
			}
		}

		if (shipData.escortName !== "") {
			// process this as an escort
			if (shipData.escortLeader) {
				// for the leader, add the ship to the escort list
				if (this.$leaderIsInArray(shipData.escortName, ship) === false) {
					this._launchingEscorts.push({
						escortName: shipData.escortName,
						leader: ship
					});
				}
				if (this._debug && this._logLaunchType >= 2) log(this.name, "Escort leader details: " + ship.displayName + ", " + shipData.escortName + ", " + ship.scanClass);
				// monkey patch the ship, but only if we haven't already done so.
				if (!ship.script.$sdc_hold_shipLaunchedFromStation && ship.script.shipLaunchedFromStation) {
					ship.script.$sdc_hold_shipLaunchedFromStation = ship.script.shipLaunchedFromStation;
				}
				if (!ship.script.$sdc_hold_shipDied && ship.script.shipDied) {
					ship.script.$sdc_hold_shipDied = ship.script.shipDied;
				}
				if (!ship.script.$sdc_hold_shipWillEnterWormhole && ship.script.shipWillEnterWormhole) {
					ship.script.$sdc_hold_shipWillEnterWormhole = ship.script.shipWillEnterWormhole;
				}
				if (!ship.script.$addEscort) {
					var esc = worldScripts.StationDockControl_EscortLaunch;
					ship.script.$addEscort = esc.$addEscort;
					ship.script.$removeEscort = esc.$removeEscort;
					ship.script.$attachEscort = esc.$attachEscort;

					ship.script.shipLaunchedFromStation = esc.npc_shipLaunchedFromStation;
					ship.script.shipDied = esc.npc_shipDied;
					ship.script.shipWillEnterWormhole = esc.npc_shipWillEnterWormhole;
					ship.script._done = false;
				}
			} else {
				// for an escort, find the leader, and offer to escort them
				if (this._debug && this._logLaunchType >= 2) log(this.name, "Escort member details: " + ship.displayName + ", " + shipData.escortName + ", " + ship.scanClass);
				for (var i = 0; i < this._launchingEscorts.length; i++) {
					if (this._launchingEscorts[i].escortName === shipData.escortName) {
						var mShip = this._launchingEscorts[i].leader;
						if (this._debug && this._logLaunchType >= 2) log(this.name, "Found leader (" + mShip.displayName + ") - attaching launch script");
						// if the mothership is already in space, we can do the offer to escort here
						if (mShip.status === "STATUS_IN_FLIGHT" && ship.status === "STATUS_IN_FLIGHT") {
							var ret = ship.offerToEscort(mShip);
							if (ret) {
								if (this._debug && this._logLaunchType >= 2) log(this.name, ship.displayName + ": found leader (" + mShip.displayName + ") - offer to escort: " + ret);
							} else {
								if (!mShip) log(this.name, "!!ERROR: Mothership is null");
								if (mShip && mShip.isValid === false) log(this.name, "!!ERROR: Mothership is invalid");
								log(this.name, "!!ERROR: Ship launch script failed to escort mothership! " + (mShip && mShip.isValid ? "(Mthr: " + mShip.displayName + " (" + mShip.status + "), " : "") + "Ship: " + ship.displayName + " (" + ship.status + ")");
							}
						} else {
							// otherwise, attach the escort launch script
							if (!ship.script.$sdc_hold_shipLaunchedFromStation) {
								if (ship.script.shipLaunchedFromStation) ship.script.$sdc_hold_shipLaunchedFromStation = ship.script.shipLaunchedFromStation;
							}
							if (!ship.script.$sdc_hold_shipDied && ship.script.shipDied) {
								ship.script.$sdc_hold_shipDied = ship.script.shipDied;
							}
							if (!ship.script.$sdc_hold_shipWillEnterWormhole && ship.script.shipWillEnterWormhole) {
								ship.script.$sdc_hold_shipWillEnterWormhole = ship.script.shipWillEnterWormhole;
							}
							var esc = worldScripts.StationDockControl_EscortLaunch;
							ship.script.$addEscort = esc.$addEscort;
							ship.script.$removeEscort = esc.$removeEscort;
							ship.script.$attachEscort = esc.$attachEscort;
							ship.script._done = false;
							ship.script.shipLaunchedFromStation = esc.npc_shipLaunchedFromStation;
							ship.script.shipDied = esc.npc_shipDied;
							ship.script.shipWillEnterWormhole = esc.npc_shipWillEnterWormhole;
							// set the mothership in that script
							ship.script._mothership = mShip;
							// add escort to mothership script
							if (!mShip.script.$addEscort) {
								if (!mShip.script.$sdc_hold_shipLaunchedFromStation && mShip.script.shipLaunchedFromStation) {
									mShip.script.$sdc_hold_shipLaunchedFromStation = mship.script.shipLaunchedFromStation;
								}
								if (!mShip.script.$sdc_hold_shipDied && mShip.script.shipDied) {
									mShip.script.$sdc_hold_shipDied = mShip.script.shipDied;
								}
								if (!mShip.script.$sdc_hold_shipWillEnterWormhole && mShip.script.shipWillEnterWormhole) {
									mShip.script.$sdc_hold_shipWillEnterWormhole = mShip.script.shipWillEnterWormhole;
								}
								mShip.script.$addEscort = esc.$addEscort;
								mShip.script.$removeEscort = esc.$removeEscort;
								mShip.script.$attachEscort = esc.$attachEscort;
								mShip.script.shipLaunchedFromStation = esc.npc_shipLaunchedFromStation;
								mShip.script.shipDied = esc.npc_shipDied;
								mShip.script.shipWillEnterWormhole = esc.npc_shipWillEnterWormhole;
								mShip.script._done = false;
							}
							mShip.script.$addEscort(ship);
						}
					}
				}
			}
		} // if

		if (this._debug && this._logLaunchType >= 2) {
			// lets see what our ships do...
			ship.reportAIMessages = true;
			ship.script._aiTimer = new Timer(this, this.$setAIReporting.bind(this, ship), 1, 0);
		}

		// tell ships with a destination system other than the current system to jump out
		if (ship.destinationSystem != system.ID) {
			ship.script._destTimer = new Timer(this, this.$setEnterWitchspaceBehaviour.bind(this, ship), 1, 0);
		}
	}

	//-------------------------------------------------------------------------------------------------------------
	// set the witchspace destination parameter, and tell ship to reconsider it's AI options
	this.$setEnterWitchspaceBehaviour = function $setEnterWitchspaceBehaviour(ship) {
		if (ship.AIScript.oolite_priorityai) {
			ship.AIScript.oolite_priorityai.setParameter("oolite_witchspaceDestination", ship.destinationSystem);
			ship.AIScript.oolite_priorityai.reconsiderNow();
		}
	}

	//-------------------------------------------------------------------------------------------------------------
	// turn on AI message logging
	this.$setAIReporting = function $setAIReporting(ship) {
		if (ship.AIScript.oolite_priorityai) {
			ship.AIScript.oolite_priorityai.setParameter("oolite_flag_behaviourLogging", true);
		}
	}

	//-------------------------------------------------------------------------------------------------------------
	// checks if the escort leader is already in the launching array in the given escort group
	this.$leaderIsInArray = function $leaderIsInArray(escortName, leader) {
		for (var i = 0; i < this._launchingEscorts.length; i++) {
			var item = this._launchingEscorts[i];
			if (item.escortName === escortName && item.leader === leader) return true;
		}
		return false;
	}

	//-------------------------------------------------------------------------------------------------------------
	// return a random departure time, based on a number of different slots
	// returned time is in seconds, and should refer to a time in the future
	this.$calculateRandomDepartureTime = function $calculateRandomDepartureTime(station, idx, slots, allowAbort) {

		// work out min value, based on timeFrame
		var depart = 0;

		// pick a departure band
		if (slots < 2) slots = 2;
		if (slots > 10) slots = 10;
		var band = this._bandSrc[slots];

		var free = false;
		var band_num = 0;
		var tries = 0;
		var secondAttempt = 0;

		do {
			band_num = band[this.$rand(band.length) - 1];
			switch (band_num) {
				//case 1:
				//	depart = this.$rand(5) + 1;
				//	break;
				case 2: // 3-15 minutes
					depart = this.$rand(12) + 3;
					break;
				case 3: // 15-30 minutes
					depart = this.$rand(15) + 15;
					break;
				case 4: // 30-60 minutes
					depart = this.$rand(30) + 30;
					break;
				case 5: // 60-120 minutes (1-2 hours)
					depart = this.$rand(60) + 60;
					break;
				case 6: // 120-720 minutes (2-12 hours)
					depart = this.$rand(600) + 120;
					break;
				case 7: // 720-960 minutes (12-16 hours) loading
					depart = this.$rand(240) + 720;
					break;
				case 8: // 960-1200 minutes (16-20 hours) docked
					depart = this.$rand(240) + 960;
					break;
				case 9: // 1200-1380 minutes (20-23 hours) unloading
					depart = this.$rand(180) + 1200;
					break;
				case 10: // 1380-1440 minutes (23-24 hours) docking
					depart = this.$rand(60) + 1380;
					break;
				case 11: // 1440-1680 minutes (24-28 hours) docking (failsafe)
					depart = this.$rand(240) + 1440;
					break;
			}
			// check this launch slot for free positions
			free = true;
			if (this._departureBands.length > 0) {
				var check = this._departureBands[depart];
				if (check === 1) {
					this._departureBands[depart] = 0;
				} else {
					free = false;
				}
			} else {
				free = this.$checkDepartureSlot(station.name, idx, depart);
			}
			if (free === false) tries++;
			// if we've tried 7 (formerly 10) times to find a slot, jump into the failsafe zone
			if (tries === 7) {
				if (this.$indexInList(11, band) >= 0 && allowAbort === true) {
					log(this.name, "!!NOTE: Search for free launch time failed - " + station.name + " (" + idx + ") " + slots);
					return -1;
				}
				// make a note of when we hit the retry limit
				log(this.name, "!!NOTE: Search for free launch time hit limit - " + station.name + " (" + idx + ") " + slots);
				this._limit++;
				if (this._limit >= 30) {
					this.$displayDetails(station.name, idx);
					this._limit = 0;
				}
				// have a really good go at the slot period
				//if (secondAttempt === 0) {
				//	band = new Array(30).join(slots);
				//	secondAttempt += 1;
				//} else 
				if (secondAttempt === 0) {
					// do a slot search the hard way
					var slow_val = this.$nextFreeDepartureSlot(station.name, idx, band_num);
					if (slow_val !== -1) {
						free = true;
						depart = slow_val;
					}
					secondAttempt += 1;
				} else {
					// then try anywhere
					band = [3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 9, 9, 9, 9, 10, 10, 10, 10, 11, 11];
				}
				tries = 0;
			}
		} while (free === false);

		return depart;
	}

	//-------------------------------------------------------------------------------------------------------------
	// checks a departure slot to see if it's free at a particular station
	this.$checkDepartureSlot = function $checkDepartureSlot(stationName, stnIndex, departureTime) {
		if (!this._systemDockingData[system.ID]) return false;
		var dta = this._systemDockingData[system.ID][stationName + "_" + stnIndex];
		if (dta && dta.length > 0) {
			var i = dta.length;
			while (i--) {
				// if the time difference is less than 60 seconds...
				if (dta[i].departureTime == departureTime) return false;
			}
		}
		return true;
	}

	//-------------------------------------------------------------------------------------------------------------
	// find a free departure slot in a particular time band the hard way
	this.$nextFreeDepartureSlot = function $nextFreeDepartureSlot(stationName, stnIndex, band) {
		function compareDeparture(a, b) {
			if ((a.departureTime * 10 + a.departureSeconds) < (b.departureTime * 10 + b.departureSeconds)) return (worldScripts.StationDockControl_Interface._sortDir * -1);
			if ((a.departureTime * 10 + a.departureSeconds) > (b.departureTime * 10 + b.departureSeconds)) return (worldScripts.StationDockControl_Interface._sortDir);
			return 0;
		}
		if (this._debug) log(this.name, "Hit slow slot selection process on " + stationName + " (" + stnIndex + ") - band " + band);

		if (!this._systemDockingData[system.ID]) {
			log(this.name, "!ERROR - No docking data found for system " + system.ID);
			return -1;
		}

		var rng = this._bandRng[band];
		if (!rng) {
			log(this.name, "!ERROR - No docking band data found for band " + band);
			return -1
		}

		if (this._departureBands.length === 0) {
			// during a std population (non-full)
			var dta = this._systemDockingData[system.ID][stationName + "_" + stnIndex];
			dta.sort(compareDeparture);

			var point = startPoint;
			if (dta && dta.length > 0) {
				for (var i = 0; i < dta.length; i++) {
					var item = dta[i];
					if (item.departureTime >= rng.low && item.departureTime < rng.high) {
						if (item.departureTime != point) {
							if (this._debug && this._logPopulatorType >= 2) log(this.name, "found slot " + point);
							return point;
						}
						point += 1;
					}
					if (point > item.departureTime) break;
				}
			}
		} else {
			// during a full repop, when the _departureBands array is available
			for (var i = rng.low; i <= rng.high; i++) {
				if (this._departureBands[i] === 1) {
					if (this._debug && this._logPopulatorType >= 2) log(this.name, "found quick slot " + i);
					this._departureBands[i] = 0;
					return i;
				}
			}
		}
		log(this.name, "Failed!")
		return -1;
	}

	//-------------------------------------------------------------------------------------------------------------
	this.$displayDetails = function $displayDetails(stationName, stnIndex) {
		function compareDeparture(a, b) {
			if ((a.departureTime * 10 + a.departureSeconds) < (b.departureTime * 10 + b.departureSeconds)) return (worldScripts.StationDockControl_Interface._sortDir * -1);
			if ((a.departureTime * 10 + a.departureSeconds) > (b.departureTime * 10 + b.departureSeconds)) return (worldScripts.StationDockControl_Interface._sortDir);
			return 0;
		}

		if (!this._systemDockingData[system.ID]) return;
		var dta = this._systemDockingData[system.ID][stationName + "_" + stnIndex];
		dta.sort(compareDeparture);
		for (var i = 0; i < dta.length; i++) {
			log(this.name, dta[i].departureTime + ": " + dta[i].shipName);
		}
	}

	//-------------------------------------------------------------------------------------------------------------
	// put all available slots into an array for easy checking
	this.$createDepartureSlots = function $createDepartureSlots() {
		// create 1 minute departure slots for the current station, up to 25 hours (buffer for potential time slot overruns)
		this._departureBands.length = 0;
		for (var i = 1; i <= 1500; i++) {
			this._departureBands.push(1);
		}
	}

	//-------------------------------------------------------------------------------------------------------------
	// remove any slots already taken by currently docked ships
	this.$removeExistingSlots = function $removeExistingSlots(station) {
		// get list of ships at this station
		var docklist = this.$getViewingList(station, true);
		// for each item, work out the number of minutes till launch
		for (var i = 0; i < docklist.length; i++) {
			//var mins = parseInt(((docklist.departureTime * 60 + docklist.lastChange) - clock.adjustedSeconds) / 60);
			var mins = docklist[i].departureTime;
			this._departureBands[mins] = 0;
		}
	}

	//-------------------------------------------------------------------------------------------------------------
	// checks if the role exists in our array of ships
	this.$roleExists = function $roleExists(role) {
		return (this._randomShips[role] ? true : false);
	}

	//-------------------------------------------------------------------------------------------------------------
	// returns a system ID of a system in a particular range
	// borrowed from oolite-populator.js
	this.$nearbySystem = function $nearbySystem(range) {
		if (range !== 7) {
			var poss = system.info.systemsInRange(range);
		} else {
			var poss = this._neighbours;
		}
		if (poss.length === 0) return system.ID;
		return poss[Math.floor(Math.random() * poss.length)].systemID;
	}

	//-------------------------------------------------------------------------------------------------------------
	// select a random ship type key (eg "noshaders_z_groovy_cobra1_india_npc") for a particular the role (eg "trader")
	this.$getRandomShipKey_old = function $getRandomShipKey_old(role) {
		var items = null;
		var freq = null;
		if (this._randomShips[role]) {
			items = this._randomShips[role].shipKeys;
			freq = this._randomShips[role].frequency;
		}
		if (!items) {
			// should never happen...
			return "";
		}
		var choice = -1;
		do {
			choice = this.$rand(items.length) - 1;
			// will we keep this choice or reset it?
			if (Math.random() > parseFloat(freq[choice])) choice = -1;
		} while (choice === -1);
		var result = items[choice];
		return result;
	}
	// contributed by Day
	this.$getRandomShipKey = function $getRandomShipKey(role) {
		var items = null;
		var freq = null;
		if (this._randomShips[role]) {
			items = this._randomShips[role].shipKeys;
			freq = this._randomShips[role].frequency;
		}
		if (!items) {
			// should never happen...
			return "";
		}
		var choice = -1;
		var that = $getRandomShipKey;
		var random = (that.random = that.random || Math.random);
		do {
			choice = this.$rand(items.length) - 1;
			// will we keep this choice or reset it?
			if (random() > parseFloat(freq[choice])) choice = -1;
		} while (choice === -1);
		var result = items[choice];
		return result;
	}

	//-------------------------------------------------------------------------------------------------------------
	// runs the "getRandomShipKey" function, but also checks whether the ship will actually fit in the stations dock
	this.$getRandomShipKeyWithDockCheck = function $getRandomShipKeyWithDockCheck(role, station) {
		// checks if a ship will fit in the dock of a station, just ensuring we don't put a ship inside a station that could never have fitted
		function checkShipCanDock(shipType, station, shipFits) {
			var lookup = station.shipClassName + "|" + shipType;
			if (shipFits[lookup] != undefined) return (shipFits[lookup] === 1 ? true : false);
			return true;
		}

		var loopCount = 0;
		var result = "";
		var finished = false;
		do {
			result = $getRandomShipKey(role);
			var shiptype = this._shipData[result].name;
			if (checkShipCanDock(shiptype, station, this._shipFits) === true) finished = true;
			loopCount += 1;
		} while (finished === false && loopCount < 5);
		return result;
	}

	//-------------------------------------------------------------------------------------------------------------
	// use randomshipnames OXP to generate ship names (if installed) - otherwise just return a blank string (no name)
	this.$getRandomShipName = function $getRandomShipName(role) {
		var randomShipName = "";
		if (this._rsnInstalled) {
			if (player.ship) {
				try {
					if (Ship.roleIsInCategory(role, "oolite-bounty-hunter") || Ship.roleIsInCategory(role, "oolite-assassin"))
						randomShipName = worldScripts["randomshipnames"].$randomHunterName(player.ship);
					else if (Ship.roleIsInCategory(role, "oolite-pirate"))
						randomShipName = worldScripts["randomshipnames"].$randomPirateName(player.ship);
					// catch everything else as a trader
					if (randomShipName === "")
						randomShipName = worldScripts["randomshipnames"].$randomTraderName(player.ship);
				} catch (err) {
					log(this.name, "!!ERROR: Unable to get ship name from RSN: " + err);
				}
			}
		}
		if (randomShipName === undefined) randomShipName = "";
		return randomShipName;
	}

	//-------------------------------------------------------------------------------------------------------------
	// create list of escort ships to be included with parent ship
	// escort data keys are accessed directly, not from our data array
	this.$getEscortData = function $getEscortData(shipKey) {

		var escorts = this._shipData[shipKey]["escorts"]; // number
		var escort_role = this._shipData[shipKey]["escort_role"]; // eg "asp-pirate"
		var escort_shipKey = this._shipData[shipKey]["escort_ship"]; // eg "cobra3-alternate"
		var escort_roles = this._shipData[shipKey]["escort_roles"]; // dict
		var escort_type = "";

		var ships = [];
		var count = 0;

		if (escort_roles && escort_roles.length > 0 && escorts && parseInt(escorts) > 0) {
			if (this._debug) log(this.name, "!!NOTE: Ship key '" + shipKey + "' has both types of escort definitions - new type will be used");
		}

		if ((!escort_roles || escort_roles.length === 0) && escorts && parseInt(escorts) > 0) {
			if (this.$dockVersionExists(shipKey) === false) {
				log(this.name, "!!NOTE: Escorts required for '" + shipKey + "' (Escorts: " + escorts + ") but no 'dock' version of the shipdata exists");
				// in this case, don't try to create any - it will only conflict with the automatically generated ones. Just return
				return;
			}
			// old style
			// this will need adjusting for better matching of Oolite weighting
			if (!escort_role || escort_role === "") escort_role = "escort";
			var allowed = this.$roleIsAllowed(escort_role);
			var i = this.$rand(parseInt(escorts));
			while (i--) {
				// is this role one of ours? then get a ship key from our array
				if (allowed) {
					escort_shipKey = this.$getRandomShipKey(escort_role);
				} else {
					// otherwise, this mothership has a custom escort role defined, so use that instead
					var keys = Ship.keysForRole(escort_role);
					escort_shipKey = keys[this.$rand(keys.length) - 1];
					if (escort_shipKey === undefined) escort_shipKey = this.$getRandomShipKey(escort_role);
					// check if we have this in the additional roles array
					if (this.$indexInList(escort_role, this._additionalRoles) === -1) this._additionalRoles.push(escort_role);
				}
				ships.push({
					role: (escort_role.indexOf("[") === 0 ? "escort" : escort_role),
					shipKey: escort_shipKey
				});
				count += 1;
				if (count === 16) break;
			}
			if (count > 0) {
				if (this._debug && this._logPopulatorType > 2) log(this.name, "Processing role:" + escort_role + ", max:" + escorts + ", result:" + count);
			}
		} else if (escort_roles && escort_roles.length > 0) {
			if (this.$dockVersionExists(shipKey) === false) {
				log(this.name, "!!NOTE: Escorts required for '" + shipKey + "' (Types: " + escort_roles.length + ", first:" + escort_roles[0].role + ", max:" + escort_roles[0].max + ", min:" + escort_roles[0].min + ") but no 'dock' version of the shipdata exists");
				// in this case, don't try to create any - it will only conflict with the automatically generated ones. Just return
				return;
			}
			// new style
			var i = escort_roles.length;
			while (i--) {
				var item = escort_roles[i];
				var role = item.role;
				var min = parseInt(item.min);
				var max = parseInt(item.max);

				// this will need adjusting for better matching of Oolite weighting
				if (role !== "") {
					var inc = this.$rand(max - min) + min;
					if (this._debug && this._logPopulatorType > 2) log(this.name, "Processing role:" + role + ", min:" + min + ", max:" + max + ", result:" + inc);
					if (inc > 0) {
						var allowed = this.$roleIsAllowed(role);
						var j = inc;
						//for (var j = 0; j < inc; j++) {
						while (j--) {
							if (count < 16) {
								// is this role one of ours? then get a ship key from our array
								if (allowed) {
									escort_shipKey = this.$getRandomShipKey(role);
								} else {
									// otherwise, this mothership has a custom escort role defined, so use that instead
									if (role.indexOf("[") === 0 && role.indexOf("]") > 0) {
										escort_shipKey = role.substring(1, role.length - 1);
									} else {
										var keys = Ship.keysForRole(role);
										if (!keys) {
											log(this.name, "!!ERROR: requested " + role + " for escorts, but no data keys found - no escorts created");
											return;
										}
										escort_shipKey = keys[this.$rand(keys.length) - 1];
										// check if we have this in the additional roles array
										if (this.$indexInList(escort_role, this._additionalRoles) === -1) this._additionalRoles.push(escort_role);
									}
								}
								// in the case where a specific ship has been specified in the role (eg "[sidewinder-escort]"), output "escort" instead of the actual role
								ships.push({
									role: (role.indexOf("[") === 0 ? "escort" : role),
									shipKey: escort_shipKey
								});
								count += 1;
							}
							// max of 16 escorts
							if (count === 16) break;
						}
					}
				}
				// max of 16 escorts
				if (count === 16) break;
			}
		}
		return ships;
	}

	//-------------------------------------------------------------------------------------------------------------
	// returns true if a "dock_shipkey" entry exists in the shipdata.plist file. Otherwise false
	// used when launching/adding ships to the system to ensure that only 1 ship is created, without escorts
	// escorts are created manually
	this.$dockVersionExists = function $dockVersionExists(shipKey) {
		var shipdata = Ship.shipDataForKey("dock_" + shipKey);
		if (shipdata) {
			return true;
		} else {
			return false;
		}
	}

	//-------------------------------------------------------------------------------------------------------------
	// sets up the weapon info for a particular ship key
	this.$setWeapons = function $setWeapons(shpDataKey, level) {
		var autoWeapons = true;
		if (this._shipData[shpDataKey]) {
			var def = this._shipData[shpDataKey]["auto_weapons"];
			if (this.$indexInList(def, this._trueValues) === -1) autoWeapons = false;
		}
		if (autoWeapons === false) {
			// default is not to change anything
			return "";
		}
		var equip = "";
		var choice = Math.floor(level);
		if (level - Math.floor(level) > Math.random()) {
			choice++;
		}
		if (choice <= 1) {
			equip += "FORE:EQ_WEAPON_PULSE_LASER,";
		} else if (choice === 2) {
			equip += "FORE:EQ_WEAPON_BEAM_LASER,";
		} else if (choice === 3) {
			equip += "FORE:EQ_WEAPON_BEAM_LASER,";
			equip += "AFT:EQ_WEAPON_BEAM_LASER,";
		} else if (choice === 4) {
			equip += "FORE:EQ_WEAPON_MILITARY_LASER,";
			equip += "ALT:EQ_WEAPON_BEAM_LASER,";
		} else if (choice >= 5) {
			equip += "FORE:EQ_WEAPON_MILITARY_LASER,";
			equip += "AFT:EQ_WEAPON_MILITARY_LASER,";
		}
		return equip;
	}

	//-------------------------------------------------------------------------------------------------------------
	// sets up the accuracy of a particular ship key
	this.$setAccuracy = function $setAccuracy(shpDataKey, skill) {
		// set an initial accuracy value, based on the shipdata value
		var init = this.$rand(10) - 5;
		var autoWeapons = true;

		if (this._shipData[shpDataKey]) {
			if (this._shipData[shpDataKey]["accuracy"] != undefined) init = this._shipData[shpDataKey]["accuracy"];
			var def = this._shipData[shpDataKey]["auto_weapons"];
			if (this.$indexInList(def, this._trueValues) === -1) autoWeapons = false;
		}
		if (autoWeapons === true && init < 5 && skill !== 0) {
			// shift skill towards end of accuracy range
			var target = 4.99;
			if (skill < 0) target = -5;

			var acc = init;
			for (var i = Math.abs(skill); i > 0; i--) {
				acc += (target - acc) * Math.random();
			}
			return acc;
		} else {
			return init;
		}
	}

	//-------------------------------------------------------------------------------------------------------------
	// returns a destination system based on a primary role
	this.$getDestinationByRole = function $getDestinationByRole(primaryRole, dockedStation) {
		//-------------------------------------------------------------------------------------------------------------
		// returns true if player has a role similar to roletype, otherwise false
		// if abortType1 or abortType2 is not blank and found, will force an automatic false response
		var $checkPlayerRoles = function (cycles, roleType, abortType1, abortType2) {
			var result = false;
			var prw = player.roleWeights;

			for (var i = 1; i <= cycles; i++) {
				var checkrole = prw[parseInt(Math.random() * prw.length)];

				if (checkrole.indexOf(roleType) >= 0) result = true;
				if (abortType1 !== "" && checkrole.indexOf(abortType1) >= 0) return false;
				if (abortType2 !== "" && checkrole.indexOf(abortType2) >= 0) return false;
			}
			return result;
		}

		var wpop = worldScripts["oolite-populator"];
		var destSystem = -1;
		var destHidden = true;
		var destLoc = "";
		// the number in each of these calls basically says how hard do we look for the role. 1 = try once, 4 = try four times
		var playerIsTrader = $checkPlayerRoles(1, "trader", "pirate", "");
		var playerIsSmuggler = $checkPlayerRoles(2, "smuggler", "pirate", "");
		var playerIsCourier = $checkPlayerRoles(1, "courier", "assassin", "pirate");
		var playerIsHunter = $checkPlayerRoles(3, "hunter", "assassin", "pirate");
		var playerIsPirate = $checkPlayerRoles(4, "pirate", "hunter", "");
		var playerIsAssassin = $checkPlayerRoles(4, "assassin", "hunter", "");

		switch (primaryRole) {
			case "trader":
				destSystem = wpop._weightedNearbyTradeSystem();
				// if the player is a trader and not a pirate and the danger level of the destination system is high, they will probably want some company.
				if ((playerIsTrader || (playerIsHunter && player.score >= 256)) && !playerIsPirate && ((4 - System.infoForSystem(galaxyNumber, destSystem).government) / 5) > Math.random())
					destHidden = false;
				// sometimes let an inexperienced player have some launch before/after slot
				if (!playerIsHunter && !playerIsCourier && !playerIsSmuggler && !playerIsPirate && !playerIsAssassin && System.infoForSystem(galaxyNumber, destSystem).government > 5 && Math.random() > (0.8 + (player.score / 64)))
					destHidden = false;
				break;
			case "trader-courier":
				if (Math.random() < 0.5) {
					destSystem = this.$nearbySystem(7);
				} else {
					destSystem = this.$nearbySystem(25);
				}
				// if the player is a courier and not a pirate or assassin and the danger level of the destination system is high, they will probably want some company.
				if (((playerIsCourier && (player.passengerReputation + player.parcelReputation) >= 3) || (playerIsHunter && player.score >= 512)) &&
					!playerIsPirate &&
					!playerIsAssassin &&
					((3 - System.infoForSystem(galaxyNumber, destSystem).government) / 5) > Math.random())
					destHidden = false;
				break;
			case "trader-smuggler":
				destSystem = this.$nearbySystem(7);
				// if the player is a trader and not a pirate and the danger level of the destination system is high, they will probably want some company.
				if ((playerIsTrader || playerIsSmuggler || (playerIsHunter && player.score >= 512)) && !playerIsPirate && ((3 - System.infoForSystem(galaxyNumber, destSystem).government) / 5) > Math.random())
					destHidden = false;
				break;
			case "assassin-light":
			case "assassin-medium":
			case "assassin-heavy":
				destSystem = system.ID;
				/* Note: this routine doesn't work yet. Assassins default AI won't jump to a target system, and there is no guarantee they'll see the player as their target.
				// will this assassin try to lure the player?
				if ((playerIsHunter || playerIsCourier) && !playerIsAssassin && Math.random() > 0.1) {
					destSystem = wpop._nearbySafeSystem(4);
					log(this.name, "got here with " + destSystem + ", curr system " + system.ID);
					if (destSystem !== system.ID) {
						destHidden = false;
						if (this._debug && this._logPopulatorType >= 1) log(this.name, "Assassin trap set - destination " + System.infoForSystem(galaxyNumber, destSystem).name);
					}
				}
				*/
				break;
			case "hunter":
				destSystem = system.ID;
				break;
			case "hunter-medium":
				destSystem = wpop._nearbyDangerousSystem(4);
				// if the player is a pirate and not a hunter and the danger level of the destination system is high, they will probably want some company.
				if (playerIsHunter && !playerIsPirate && !playerIsAssassin && player.score >= 256 && ((4 - System.infoForSystem(galaxyNumber, destSystem).government) / 5) > Math.random())
					destHidden = false;
				break;
			case "hunter-heavy":
				destSystem = wpop._nearbyDangerousSystem(1);
				// if the player is a pirate and not a hunter and the danger level of the destination system is high, they will probably want some company.
				if (playerIsHunter && !playerIsPirate && !playerIsAssassin && player.score >= 512 && ((4 - System.infoForSystem(galaxyNumber, destSystem).government) / 5) > Math.random())
					destHidden = false;
				break;
			case "shuttle":
				destSystem = system.ID;
				destLoc = this.$getRandomSystemLocation(dockedStation);
				// if the player is a trader and not a pirate and the danger level of the destination system is high, they will probably want some company.
				if ((playerIsTrader || playerIsCourier) && !playerIsPirate && ((3 - system.info.government) / 5) > Math.random())
					destHidden = false;
				break;
			case "pirate":
				destSystem = system.ID;
				// if the player is a pirate and not a hunter and the danger level of the destination system is high, they will probably want some company.
				if (playerIsPirate && !playerIsHunter && player.score >= 256 && ((4 - System.infoForSystem(galaxyNumber, destSystem).government) / 5) > Math.random())
					destHidden = false;
				break;
			case "pirate-light-freighter":
			case "pirate-medium-freighter":
			case "pirate-heavy-freighter":
				destSystem = wpop._nearbySafeSystem(system.info.government + 1);
				if (playerIsPirate && !playerIsHunter && player.score >= 512 && ((4 - System.infoForSystem(galaxyNumber, destSystem).government) / 5) > Math.random()) destHidden = false;
				break;
		}
		return {
			destination: destSystem,
			destinationHidden: destHidden,
			location: destLoc
		};
	}

	//-------------------------------------------------------------------------------------------------------------
	// gets the station obbect from a given station name and index
	this.$getStationFromName = function $getStationFromName(stationName, stationIndex) {
		for (var i = 0; i < this._stationList.length; i++) {
			var item = this._stationList[i];
			if (item.name === stationName && item.index === stationIndex) return item.station;
		}
		return null;
	}

	//-------------------------------------------------------------------------------------------------------------
	// returns the index of a particular station
	this.$getStationIndex = function $getStationIndex(station) {
		for (var i = 0; i < this._stationList.length; i++) {
			if (this._stationList[i].station === station) return this._stationList[i].index;
		}
		return 0;
	}

	//-------------------------------------------------------------------------------------------------------------
	// returns true if station has a role in the list of roles for medium traffic stations
	this.$checkStationRoleForMediumTraffic = function $checkStationRoleForMediumTraffic(station) {
		if (station.roles) {
			for (var i = 0; i < station.roles.length; i++) {
				if (this.$indexInList(station.roles[i], this._trafficMedium) >= 0) return true;
			}
		}
		return false;
	}

	//-------------------------------------------------------------------------------------------------------------
	// returns true if station has a role in the list of roles for low traffic stations
	this.$checkStationRoleForLowTraffic = function $checkStationRoleForLowTraffic(station) {
		if (station.roles) {
			for (var i = 0; i < station.roles.length; i++) {
				if (this.$indexInList(station.roles[i], this._trafficLow) >= 0) return true;
			}
		}
		return false;
	}

	//-------------------------------------------------------------------------------------------------------------
	// returns true if station has a role in the list of roles for low traffic stations
	this.$checkStationRoleForVeryLowTraffic = function $checkStationRoleForVeryLowTraffic(station) {
		if (station.roles) {
			for (var i = 0; i < station.roles.length; i++) {
				if (this.$indexInList(station.roles[i], this._trafficVeryLow) >= 0) return true;
			}
		}
		return false;
	}

	//-------------------------------------------------------------------------------------------------------------
	// returns true if station has a role in the list of roles for no traffic stations
	this.$checkStationRoleForNoTraffic = function $checkStationRoleForNoTraffic(station) {
		if (station.roles) {
			for (var i = 0; i < station.roles.length; i++) {
				if (this.$indexInList(station.roles[i], this._trafficNone) >= 0) return true;
			}
		}
		return false;
	}

	//-------------------------------------------------------------------------------------------------------------
	// returns true if the station has a role in the list of roles for no shuttle traffic
	this.$checkShuttleDestForNoTraffic = function $checkShuttleDestForNoTraffic(station) {
		if (station.roles) {
			for (var i = 0; i < station.roles.length; i++) {
				if (this.$indexInList(station.roles[i], this._shuttleTrafficNone) >= 0) return true;
			}
		}
		return false;
	}

	//-------------------------------------------------------------------------------------------------------------
	// sets up the heat insulation for a particular ship key
	this.$setHeatInsulation = function $setHeatInsulation(shpDataKey, initial) {
		var shipHeat = 1;
		if (this._shipData[shpDataKey] && this._shipData[shpDataKey]["heat_insulation"]) shipHeat = parseInt(this._shipData[shpDataKey]["heat_insulation"]);
		if (initial !== 0) shipHeat = initial;
		return shipHeat;
	}

	//-------------------------------------------------------------------------------------------------------------
	// calculates a random insurance amount for a new pilot
	this.$calcInsurance = function $calcInsurance(bounty) {
		var result = 0;
		if (bounty === 0) {
			if (Math.random() > 0.9) {
				result = parseInt(Math.random() * 100) + 100;
			} else if (Math.random() > 0.6) {
				result = parseInt(Math.random() * 50) + 50;
			} else {
				result = parseInt(Math.random() * 20) + 10;
			}
		}
		return result;
	}

	//-------------------------------------------------------------------------------------------------------------
	// determines whether there is room to launch a certain number of ships
	this.$launchAvailable = function $launchAvailable(station, ships) {
		if (station) {
			var subent = station.subEntities;
			var space = 0;
			var queued = 0;
			if (subent) {
				for (var i = 0; i < subent.length; i++) {
					var ent = subent[i];
					if (ent.isDock) {
						space += 16 - ent.launchingQueueLength;
						queued += ent.launchingQueueLength;
					}
				}
			} else {
				if (this._debug) log(this.name, "!NOTE: $launchAvailable -- subent is null (no subentities on station), could not find dock, returning false");
			}
			if (queued === 0 && space === 0 && this._debug) log(this.name, "!NOTE: $launchAvailable -- no queued ships but space is 0 - no docks found?");
			return (space >= ships);
		} else {
			if (this._debug) log(this.name, "!ERROR: $launchAvailable -- station is null, defaulting to false");
			return false;
		}
	}

	//-------------------------------------------------------------------------------------------------------------
	// returns an array of ships currently attempting to dock at the station
	this.$getShipsDocking = function $getShipsDocking(station) {
		function $isDocking(entity) {
			return entity.isShip && entity.isValid && entity.isPiloted && !entity.isStation && entity.dockingInstructions && entity.dockingInstructions.station === station;
		}
		return system.filteredEntities(this, $isDocking, station, station.scannerRange);
	}

	//-------------------------------------------------------------------------------------------------------------
	// forces a specific ship to dock at a specific station
	this.$forceShipToDock = function $forceShipToDock(station, ship) {
		var died = false;
		// if this is an individual ship, or not the leader of a group/escort, then decide if they "make it"
		// note: this code should never get used, as we aren't forcing docked ships that aren't trying to dock anymore
		if (ship.position.distanceTo(station) > (station.scannerRange * 1.4)) {
			var chance = (system.government + 1) / 8;
			// three strike policy
			if (Math.random() > chance && Math.random() > chance && Math.random() > chance) died = true;
			if (this._debug && this._logDockingType === 2 && died) log(this.name, "Ship reckoned as destroyed: " + ship);
		}
		// dock the ship
		if (died === false) {
			if (this._debug && this._logDockingType === 2) log(this.name, "Forcing ship to dock..." + ship);
			this.$logShipDocking(station, ship);
		}
		// remove the ship from the system
		ship.remove(false);
	}

	//-------------------------------------------------------------------------------------------------------------
	// controller to look after the force-dock of ships in a given time lapse
	this.$forceDockController = function $forceDockController(timeLapse) {
		var stns = system.stations;
		// any ships currently trying to dock? if the time lapse is 10 minutes or greater, dock them
		if (timeLapse >= 10) {
			stns.forEach(function (station) {
				var docking = this.$getShipsDocking(station);
				if (docking && docking.length > 0) {
					for (var i = 0; i < docking.length; i++) {
						var dock = docking[i];
						if (dock.isValid && dock.isStation === false && dock.isPlayer === false) {
							var group = [];
							var escort = [];
							if (dock.group) {
								for (var j = 0; j < dock.group.ships.length; j++) {
									group.push(dock.group.ships[j]);
								}
							}
							if (dock.escortGroup) {
								for (var j = 0; j < dock.escortGroup.ships.length; j++) {
									escort.push(dock.escortGroup.ships[j]);
								}
							}
							this.$forceShipToDock(station, dock);
							// dock all his mates as well
							if (group.length > 0) {
								for (var j = 0; j < group.length; j++) {
									var grp = group[j];
									if (grp.isValid && grp.isStation === false) {
										this.$forceShipToDock(station, grp);
									}
								}
							}
							if (escort.length > 0) {
								for (var j = 0; j < escort.length; j++) {
									var esc = escort[j];
									if (esc.isValid && esc.isStation === false) {
										this.$forceShipToDock(station, esc);
									}
								}
							}
						} // if docking is valid
					} // for i (loop on docking ships)
				} // if docking and length > 0
				docking.length = 0;
			}, this); // stns.foreach
		} // if timelapse
	}

	//-------------------------------------------------------------------------------------------------------------
	// these docking bay number functions are only used when docking a single ship
	// the checking function is too slow when running in a full population routine

	//-------------------------------------------------------------------------------------------------------------
	// gets a new docking back code
	this.$getDockingBayCode = function $getDockingBayCode(station, stationIndex) {
		if (!this._systemDockingData[system.ID]) return "";
		var stnmax = parseInt(station.mass / 800000);
		var baynum = "";
		var exclude = 0;

		// if this is the players docked station, pull up the docking bay code allocated to the player
		if (player.ship.docked) {
			if (player.ship.dockedStation === station) exclude = worldScripts.StationDockControl_Interface._playerDockingBay
		}

		do {
			// get a random docking bay code that isn't the players
			do {
				baynum = this.$rand(stnmax);
			} while (exclude > 0 && baynum === exclude);
			// check to make sure this bay is clear of any other ships
		} while (this.$checkForClearBay(baynum, station, stationIndex) === false);

		return baynum;
	}

	//-------------------------------------------------------------------------------------------------------------
	// check if this docking bay number has been used at this station
	this.$checkForClearBay = function $checkForClearBay(baynum, station, stationIndex) {
		var dta = this._systemDockingData[system.ID][station.name + "_" + stationIndex];
		if (dta && dta.length > 0) {
			for (var i = 0; i < dta.length; i++) {
				if (dta[i].dockingBay === baynum) return false;
			}
		}
		return true;
	}

	//-------------------------------------------------------------------------------------------------------------
	// these docking bay code functions are used in isolation, after the main populator has added all the docked ships
	// this is to prevent timeout issues during the main populator routine.

	//-------------------------------------------------------------------------------------------------------------
	// add missing docking bay codes to docked ships
	this.$addBayCodes = function $addBayCodes(station) {
		if (this._debug && this._logPopulatorType > 0) log(this.name, "Adding docking bay codes");
		var idx = this.$getStationIndex(station);
		// check station has traffic and for which we will be possibly showing docked ship info...
		if (station.hasNPCTraffic && worldScripts.StationDockControl_Interface.$checkStationRoleForView(station)) {
			// create the list of available docking bay codes for this station
			if (this._debug && this._logPopulatorType >= 2) log(this.name, "Prepopulating docking bay codes for " + station.name + " (index " + idx + ")");
			this.$populateStationBayCodes(station);

			// add a docking bay code to each vessel
			if (this._debug && this._logPopulatorType >= 2) log(this.name, "Applying docking bay codes to data");
			this.$applyCodesToStation(station, idx);
		}
		if (this._debug && this._logPopulatorType >= 2) log(this.name, "Adding docking bay codes complete");
	}

	//-------------------------------------------------------------------------------------------------------------
	// adds docking bay codes to station data in bulk
	this.$applyCodesToStation = function $applyCodesToStation(station, stnIndex) {
		var bay = 0;
		if (!this._systemDockingData[system.ID]) return;
		var dta = this._systemDockingData[system.ID][station.name + "_" + stnIndex];
		if (dta && dta.length > 0) {
			for (var j = 0; j < dta.length; j++) {
				var item = dta[j];
				if (item.dockingBay === "") {

					var found = false;
					do {
						bay += 1;
						if (this._stationBayCodes[bay] !== "") {
							item.dockingBay = this._stationBayCodes[bay];
							found = true;
						}
					} while (found === false);
				}
			}
		}
	}

	//-------------------------------------------------------------------------------------------------------------
	// we're doing this for performance reasons - it's faster to have all the codes pre-entered, and then cross them off
	// than to check each one every time
	this.$populateStationBayCodes = function $populateStationBayCodes(station) {
		var stnmax = parseInt(station.mass / 800000);
		this._stationBayCodes = new Array(stnmax);
		for (var i = 0; i < stnmax; i++) {
			this._stationBayCodes[i] = i + 1;
		}
	}

	//-------------------------------------------------------------------------------------------------------------
	// updates the _docking array with the list of currently docking ships
	this.$updateShipsDocking = function $updateShipsDocking(station) {
		function compare(a, b) {
			if (a.shipClassName < b.shipClassName) return -1;
			if (a.shipClassName > b.shipClassName) return 1;
			return 0;
		}
		this._docking.length = 0;
		this._docking = this.$getShipsDocking(station);
		// we going to sort the list by classname, as we don't know (and can't know) the order ships will dock,
		// so sorting by class is as good a sort as any
		this._docking.sort(compare);
	}

	//-------------------------------------------------------------------------------------------------------------
	// occasionally the shipUniqueName returns "" even though it's really there. In that instance, pull the unique name out of the display name
	this.$getShipUniqueName = function $getShipUniqueName(displayName, shipClassName) {
		return displayName.replace(shipClassName + ": ", "");
	}

	//-------------------------------------------------------------------------------------------------------------
	// checks that the primary role of a ship is in our list of controlled roles.
	this.$roleIsAllowed = function $roleIsAllowed(primaryRole) {
		if (this._controlledRoles.indexOf(primaryRole) !== -1) return true;
		return false;
	}

	//-------------------------------------------------------------------------------------------------------------
	// checks the additional array for any extra roles we might have come across
	this.$roleIsAllowedAdditional = function $roleIsAllowedAdditional(primaryRole) {
		if (this._additionalRoles.indexOf(primaryRole) !== -1) return true;
		return false;
	}

	//-------------------------------------------------------------------------------------------------------------
	// assesses the current state of the station dock. Logic is:
	// If 1 or more ships are trying to dock, and there's either more than 4 ships in the launch queue already, or theres more than 4 ships trying to dock,
	// and there are ships within 3 minutes of departure,
	// bump all station traffic back 90 seconds.
	this.$assessStationDocks = function $assessStationDocks() {
		if (this._stationIndexesLoaded === false) return; // no point doing the assessment if the indexes aren't loaded
		if (this._debug && this._logLaunchType > 0) log(this.name, "Assessing station docks");
		if (this._systemDockingData[system.ID] == null) {
			log(this.name, "!!NOTE: No data for system found! Unable to assess docks. Resetting data entry for system.");
			this._systemDockingData[system.ID] = {};
			//return;
		}
		var stns = system.stations;
		stns.forEach(function (station) {
			if (station.hasNPCTraffic && this.$checkStationRoleForNoTraffic(station) === false) {
				var docking = 0;
				var queue = 0;
				// how many ships are already in the launch queue?
				for (var i = 0; i < station.subEntities.length; i++) {
					var subent = station.subEntities[i];
					if (subent.isDock) {
						docking += subent.dockingQueueLength;
						queue += subent.launchingQueueLength;
					}
				}
				// if there's more than 4, check the number of ships about to launch
				if (this._debug && this._logDockingType >= 1) log(this.name, station.name + " inbound length: " + docking);
				if (this._debug && this._logLaunchType >= 2) log(this.name, station.name + " launch queue length: " + queue);
				if (docking >= 1 && (queue >= 4 || docking > 4)) {
					var idx = this.$getStationIndex(station);
					var dta = this._systemDockingData[system.ID][station.name + "_" + idx];
					var found = false;
					if (dta && dta.length > 0) {
						// count ships less then 3 minutes from departure
						for (var i = 0; i < dta.length; i++) {
							//if (dta[i].departureTime - clock.adjustedSeconds <= 180) {
							if (dta[i].departureTime <= 3) {
								found = true;
								break;
							}
						}
					}
					if (!dta && this._lateStations.indexOf(station) === -1) {
						// no data? add this station to the late station list so it can be repopulated
						this._lateStations.push(station);
						//log(this.name, "adding last station " + station);
					}
					if (this._debug && this._logLaunchType >= 2) log(this.name, station.name + "(" + idx + ") pending launch found: " + found);
					if (found) this.$bumpStationTraffic(station, idx);
				} // if docking >= 1 && queue >= 4
			} // if station has NPC traffic...
		}, this);
	}

	//-------------------------------------------------------------------------------------------------------------
	// adds 90 seconds to all the ships at a particular station
	this.$bumpStationTraffic = function $bumpStationTraffic(station, stnIndex) {
		if (this._debug && this._logLaunchType > 0) log(this.name, "** Bumping traffic at " + station.name + "(" + stnIndex + ") **");
		var dta = this._systemDockingData[system.ID][station.name + "_" + stnIndex];
		if (dta && dta.length > 0) {
			for (var i = 0; i < dta.length; i++) {
				var item = dta[i];
				if (item.departureTime > 0) {
					item.departureTime += 2;
					item.lastChange = clock.adjustedSeconds;
				}
			}
		}
	}

	//-------------------------------------------------------------------------------------------------------------
	// writes the Oolite populator outgoing factors to the log
	this.$logOolitePopulatorOutgoingFactors = function $logOolitePopulatorOutgoingFactors() {
		var w = worldScripts["oolite-populator"];
		log(this.name, "Populator outgoing frequency values for " + system.name + " (" + system.ID + "):");
		log(this.name, "trader-freighters: " + w.$repopulatorFrequencyOutgoing.traderFreighters);
		log(this.name, "trader-couriers:   " + w.$repopulatorFrequencyOutgoing.traderCouriers);
		log(this.name, "trader-smugglers:  " + w.$repopulatorFrequencyOutgoing.traderSmugglers);
		log(this.name, "assassin:          " + w.$repopulatorFrequencyOutgoing.assassins);
		log(this.name, "shuttle:           " + (0.005 * system.info.techlevel));
		log(this.name, "pirate-ind:        " + w.$repopulatorFrequencyOutgoing.pirateIndependents);
		log(this.name, "pirate-light:      " + w.$repopulatorFrequencyOutgoing.pirateLightPacks);
		log(this.name, "pirate-medium:     " + w.$repopulatorFrequencyOutgoing.pirateMediumPacks);
		log(this.name, "pirate-heavy:      " + w.$repopulatorFrequencyOutgoing.pirateHeavyPacks);
		log(this.name, "hunter-light:      " + w.$repopulatorFrequencyOutgoing.hunterLightPacks);
		log(this.name, "hunter-medium:     " + w.$repopulatorFrequencyOutgoing.hunterMediumPacks);
		log(this.name, "hunter-heavy:      " + w.$repopulatorFrequencyOutgoing.hunterHeavyPacks);
	}

	//-------------------------------------------------------------------------------------------------------------
	// writes the details of the selected record to the log
	this.$writeDataToLog = function $writeDataToLog(record, reason) {
		if (typeof record === "undefined") return;

		if (reason.indexOf("Docking ship") >= 0) {
			if (this._logDockingType === 0) return;
		}
		if (reason.indexOf("Launching ship") >= 0) {
			if (this._logLaunchType === 0) return;
		}

		log(this.name, "=========================================================");
		log(this.name, reason + ":");
		log(this.name, "---------------------------------------------------------");

		log(this.name, "System:             " + record.system);
		log(this.name, "Station:            " + record.station);
		log(this.name, "Station index:      " + record.stationIndex);
		log(this.name, "Ship name:          " + record.shipName);
		log(this.name, "Ship type:          " + record.shipType);
		log(this.name, "Ship DataKey:       " + record.shipDataKey);
		log(this.name, "Ship personality:   " + record.personality);
		log(this.name, "Primary role:       " + record.primaryRole);

		if (reason.indexOf("Docking ship") >= 0) {
			if (this._logDockingType === 1) return;
		}
		if (reason.indexOf("Launching ship") >= 0) {
			if (this._logLaunchType === 1) return;
		}

		log(this.name, "AI Name:            " + record.shipAI);
		log(this.name, "Accuracy:           " + record.accuracy);
		log(this.name, "Equipment:          " + record.equipment);
		log(this.name, "Heat insulation:    " + record.heatInsulation);
		log(this.name, "Home system:        " + record.homeSystem);
		log(this.name, "Bounty:             " + record.bounty);
		log(this.name, "Escort name:        " + record.escortName);
		log(this.name, "Is escort leader:   " + record.escortLeader);
		log(this.name, "Group name:         " + record.groupName);
		log(this.name, "Is group leader:    " + record.groupLeader);
		log(this.name, "Destination:        " + record.destinationSystem);
		log(this.name, "Intra-system dest:  " + (record.destination ? record.destination : ""));
		log(this.name, "Destination hidden: " + record.destinationHidden);
		log(this.name, "Goods:              " + record.goods);
		log(this.name, "Dock time:          " + record.dockTime);
		log(this.name, "Docking bay:        " + record.dockingBay);
		log(this.name, "Departure time:     " + record.departureTime);
		log(this.name, "Departure seconds:  " + record.departureSeconds);
		log(this.name, "Pilot name:         " + record.pilotName);
		log(this.name, "Pilot description:  " + record.pilotDescription);
		log(this.name, "Pilot home system:  " + record.pilotHomeSystem);
		log(this.name, "Pilot species:      " + record.pilotSpecies);
		log(this.name, "Pilot legal status: " + record.pilotLegalStatus);
		log(this.name, "Pilot insurance:    " + record.pilotInsurance);
		log(this.name, "Last change:        " + record.lastChange);
	}

	//-------------------------------------------------------------------------------------------------------------
	// writes a short version of the data record to the log
	this.$writeShortDataToLog = function $writeShortDataToLog(record) {
		log(this.name, "Ship details: " + record.shipType + (record.shipName !== "" ? ": " + record.shipName : "") + " (" + record.shipDataKey + ") -- " + record.primaryRole);
	}

	//-------------------------------------------------------------------------------------------------------------
	// get a random station from one of the ones in the system, excluding the origin station
	this.$getRandomSystemLocation = function $getRandomSystemLocation(originStation) {
		// pick the planet as the destination
		if (Math.random() < 0.2) return "Planet|0";

		if (this._shuttleDest == null) this._shuttleDest = this.$getShuttleDestinations();

		var sel = -1;
		var max_dist = 0;
		var min_dist = -1;
		var max_id = -1;
		var min_id = -1;
		var dist = 0;

		// only do the distance checks if there are more than two stations in system
		if (this._shuttleDest.length > 2) {
			for (var i = 0; i < this._shuttleDest.length; i++) {
				var dest = this._shuttleDest[i];
				if (dest.position) {
					dist = originStation.position.distanceTo(dest);
					if (dist > max_dist) {
						max_dist = dist;
						max_id = i;
					}
					if ((min_dist !== -1 && dist < min_dist) || min_dist === -1) {
						min_dist = dist;
						min_id = i;
					}
				}
			}
		}
		var count = 0;

		do {
			sel = this.$rand(this._shuttleDest.length) - 1;
			if (this._shuttleDest[sel] === originStation) sel = -1;
			// if this is the most distance place in the region, make it less likely as a destination
			if (sel >= 0 && sel === max_id && Math.random() < 0.9) sel = -1;
			// if it's not the closest or the furthest, reduce the chance, to increase the chance that the closest stations get more traffic
			if (sel >= 0 && sel !== min_id && Math.random() < 0.4) sel = -1;

			// keep track over home many times we go through this loop
			count += 1;
			// if we hit the limit, default to the planet
			if (count >= 10) return "Planet|0";
		} while (sel === -1);

		var idx = this.$getStationIndex(this._shuttleDest[sel]);
		return this._shuttleDest[sel].name + "|" + idx;
	}

	//-------------------------------------------------------------------------------------------------------------
	// convert a "station|index" value into the actual station location
	this.$convertSystemLocationToPosition = function $convertSystemLocationToPosition(location) {
		var items = location.split("|");
		var stnName = items(0);
		var idx = parseInt(items(1));

		if (stnName !== "Planet") {
			dest = this.$getStationFromName(shpName, idx);
			if (dest) {
				return dest.position;
			} else {
				// station mustn't exist anymore -- return null
				return null;
			}
		} else {
			// return the planetary coordinates
			return null;
		}
	}

	//-------------------------------------------------------------------------------------------------------------
	// compiles list of stations we can send shuttles to
	this.$getShuttleDestinations = function $getShuttleDestinations() {
		var stns = system.stations;
		var dest = [];
		// only add stations not in the "trafficNone" list and aren't police ships
		for (var i = 0; i < stns.length; i++) {
			var stn = stns[i];
			if (this.$checkShuttleDestForNoTraffic(stn) === false && !stn.isPolice) {
				dest.push(stn);
			}
		}
		return dest;
	}

	//-------------------------------------------------------------------------------------------------------------
	// reduces the size of an array by removing duplicates
	this.$compactArray = function $compactArray(array) {
		// get a distinct list of items
		var items = [];
		for (var i = 0; i < array.length; i++) {
			var item = array[i];
			if (item != null && item !== "" && typeof item != "undefined") {
				if (items.indexOf(item) === -1) items.push(item);
			}
		}
		// return the new array
		return items;
	}

	//-------------------------------------------------------------------------------------------------------------
	// removes empty data elements from the main dataset
	this.$dataCleanup = function $dataCleanup() {
		// look for empty data for a system, but only in small batches so we don't cause any slowdowns
		for (var i = this._cleanUp; i < (this._cleanUp + 5); i++) {
			if (i >= 256) {
				if (this._debug) log(this.name, "cleanup complete!");
				this._cleanUpComplete = true;
				break;
			}
			if (i !== system.ID && this._systemDockingData[i]) {
				var dta = JSON.stringify(this._systemDockingData[i]);
				// if there's no mention of "system", there is no docking data for that system, so delete the entire entry
				if (dta && dta.indexOf("system:") === -1) {
					if (this._debug) log(this.name, "cleaning up system " + i);
					delete this._systemDockingData[i];
				}
			}
		}
		if (this._cleanUpComplete === false) this._cleanUp += 5;
	}

	//-------------------------------------------------------------------------------------------------------------
	// removes all but the data for systems 1 and 2
	this.$dataPurge = function $dataPurge(id1, id2) {
		var keys = Object.keys(this._systemDockingData);
		for (var i = 0; i < keys.length; i++) {
			if (parseInt(keys[i]) != id1 && parseInt(keys[i]) != id2) {
				delete this._systemDockingData[keys[i]];
			}
		}
	}

	//-------------------------------------------------------------------------------------------------------------
	// by cag for speed improvements instead of using "indexOf"
	this.$indexInList = function $indexInList(item, list) { // for arrays only
		var k = list.length;
		while (k--) {
			if (list[k] === item) return k;
		}
		return -1;
	}

}).call(this);
Scripts/stationdockcontrol_conditions.js
"use strict";
this.name = "StationDockControl_Conditions";
this.author = "phkb";
this.copyright = "2015 phkb";
this.description = "Condition script for determining whether the SDFC Hack Chip can be purchased";
this.license = "CC BY-NC-SA 3.0";

this._includeRoles = ["rockhermit", "rockhermit-chaotic", "rockhermit-pirate", "anarchies_salvage_gang", "anarchies_hacker_outpost"];

//-------------------------------------------------------------------------------------------------------------
this.allowAwardEquipment = function (equipment, ship, context) {

	// always return true for scripted install
	if (context === "scripted") return true;

	if (context != "purchase") return false;
	// check the techlevel of the system
	if (system.info.techlevel < 8) return false;
	// check the government type: all anarchies, most feudals, some multi-govs.
	if ((2 - system.info.government) / 2 < system.scrambledPseudoRandomNumber(system.ID)) return false;

	var p = player.ship;
	var stn = p.dockedStation;
	var found = false;

	// check the station type
	for (var i = 0; i < this._includeRoles.length; i++) {
		if (stn.hasRole(this._includeRoles[i])) found = true;
	}
	// if we get here, it's ok to buy
	return found;
}
Scripts/stationdockcontrol_escortlaunch.js
"use strict";
this.name = "StationDockControl_EscortLaunch";
this.author = "phkb";
this.copyright = "2015 phkb";
this.description = "Script to attach to a ship after launch to get it to escort a mothership";
this.license = "CC BY-NC-SA 3.0";

/*
	This script is attached to the mothership and escort ships. occasionally a mothership will launch well ahead of some of its escorts
	(due to docking queue pressure). When that happens, a mothership can enter hyperspace before an escort even launches.
	This script will attach escorts to the mothership in one of two ways:
	1. If the mothership launches ahead of the escort, the mothership will use the "this._escorts" array to attach the escorts
	2. As a backup, the escort itself will attempt to attach itself after launch
	Hopefully this will cover all scenarios for escort launches and ensure no escort is left behind
*/

//-------------------------------------------------------------------------------------------------------------
// add an escort to the list
this.$addEscort = function $addEscort(escortShip) {
	if (this.ship.script._done) return false;
	if (!this.ship.script._escorts) this.ship.script._escorts = [];
	this.ship.script._escorts.push(escortShip);
	return true;
}

//-------------------------------------------------------------------------------------------------------------
this.$removeEscort = function $removeEscort(escortShip) {
	if (!this.ship.script._escorts) this.ship.script._escorts = [];
	for (var i = this.ship.script._escorts.length - 1; i >= 0; i--) {
		if (this.ship.script._escorts[i] === escortShip) {
			this.ship.script._escorts.splice(i, 1);
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.npc_shipDied = function npc_shipDied(whom, why) {
	if (this.ship.script.$sdc_hold_shipDied) this.ship.script.$sdc_hold_shipDied(whom, why);
	if (this.ship.script._timer && this.ship.script._timer.isRunning) this.ship.script._timer.stop();
}

//-------------------------------------------------------------------------------------------------------------
this.npc_shipWillEnterWormhole = function npc_shipWillEnterWormhole() {
	if (this.ship.script.$sdc_hold_shipWillEnterWormhole) this.ship.script.$sdc_hold_shipWillEnterWormhole();
	if (this.ship.script._timer && this.ship.script._timer.isRunning) this.ship.script._timer.stop();
}

//-------------------------------------------------------------------------------------------------------------
// used for mothership and escort ship launches. One of these should fire and cancel the other out.
this.npc_shipLaunchedFromStation = function npc_shipLaunchedFromStation(station) {
	var s = this.ship;
	if (s.script.$sdc_hold_shipLaunchedFromStation) s.script.$sdc_hold_shipLaunchedFromStation(station);

	var ret = false;
	// pull down the debug settings from the master function
	var w = worldScripts.StationDockControl;
	var debug = w._debug;
	var l_debug = w._logLaunchType;

	if (s.script._mothership && s.script._done === false) {
		// type 1 - escort launch
		if (debug && l_debug >= 2) log(this.name, s.displayName + ": escort launch successful");

		// offer to escort the mothership
		if (s.script._mothership && s.script._mothership.isValid && s.script._mothership.status === "STATUS_IN_FLIGHT") {
			ret = s.offerToEscort(s.script._mothership);
			if (debug && l_debug >= 2) log(this.name, s.displayName + ": found leader (" + s.script._mothership.displayName + ") - offer to escort: " + ret);
		}

		// remove this ship from the mothership launch array if it was successfully added
		if (ret === true) {
			if (s.script._mothership.script.$removeEscort) s.script._mothership.script.$removeEscort(s);
		}

		// report on any errors
		if (ret === false) {
			if (!s.script._mothership) log(this.name, "!!ERROR: Mothership is null");
			if (s.script._mothership && s.script._mothership.isValid === false) log(this.name, "!!ERROR: Mothership is invalid");
			if (s.script._mothership && s.script._mothership.isValid && s.script._mothership.status !== "STATUS_IN_FLIGHT") {
				if (s.script._mothership.status !== "STATUS_LAUNCHING") log(this.name, "!!ERROR: Mothership is not in flight: " + s.script._mothership.status);
				if (s.script._mothership.status === "STATUS_LAUNCHING") {
					if (debug && l_debug >= 2) log(this.name, "!!NOTE: Mothership is launching -- setting timer to await launch");
					if (s.script._timer && s.script._timer.isRunning) s.script._timer.stop();
					s.script._timer = new Timer(this, s.script.$attachEscort, 2, 0);
					return;
				}
			}
			if (s.status === "STATUS_IN_FLIGHT" && s.script._mothership && s.script._mothership.isValid && s.script._mothership.status === "STATUS_IN_FLIGHT") {
				if (debug && l_debug >= 2) log(this.name, "!!NOTE: Escort and mothership are in flight -- setting timer to try again");
				if (s.script._timer && s.script._timer.isRunning) s.script._timer.stop();
				s.script._timer = new Timer(this, s.script.$attachEscort, 2, 0);
				return;
			}

			log(this.name, "!!ERROR: Ship launch script failed to escort mothership! " + (s.script._mothership && s.script._mothership.isValid ? "(Mthr: " + s.script._mothership.displayName + " (" + s.script._mothership.status + "), " : "") + "This: " + s.displayName + " (" + s.status + ")");
		}

	} else if (s.script._escorts && s.script._escorts.length > 0 && s.script._done === false) {
		// type 2 - mothership launch
		if (debug && l_debug >= 2) log(this.name, s.displayName + ": mothership launch successful");

		if (s.isValid && s.status === "STATUS_IN_FLIGHT") {
			if (!s.script._escorts) s.script._escorts = [];
			for (var i = 0; i < s.script._escorts.length; i++) {
				ret = s.script._escorts[i].offerToEscort(s);
				if (debug && l_debug >= 2) log(this.name, s.script._escorts[i].displayName + ": found leader (" + s.displayName + ") - offer to escort: " + ret);
				// turn off the escorts's individual launch script
				if (ret === true) {
					s.script._escorts[i].script._done = true;
				}
				// report on any errors
				if (ret === false) {
					if (s && s.status === "STATUS_LAUNCHING") {
						if (debug && l_debug >= 2) log(this.name, "!!NOTE: Mothership is still launching -- setting timer to try again");
						if (s.script._timer && s.script._timer.isRunning) s.script._timer.stop();
						s.script._timer = new Timer(this, s.script.$attachEscort, 2, 0);
						return;
					}
					if (s && s.isValid && s.status !== "STATUS_IN_FLIGHT") log(this.name, "!!ERROR: Mothership is not in flight: " + s.status);
					log(this.name, "!!ERROR: Mothership launch script failed to add escort! " + s.displayName + " (" + s.status + ", " + s.script._escorts.length + "), Escort: " + s.script._escorts[i].displayName + " (" + s.script._escorts[i].status + ")");
				}
			}
		}
	}

	s.script._done = true;

	// cleanup
	s.script._mothership = null;
	if (s.script._escorts) s.script._escorts.length = 0;
	delete s.script._timer;
	delete s.shipLaunchedFromStation;
	if (s.script.$sdc_hold_shipLaunchedFromStation) s.script.shipLaunchedFromStation = $sdc_hold_shipLaunchedFromStation;
}

//-------------------------------------------------------------------------------------------------------------
this.$attachEscort = function $attachEscort() {
	var s = this.ship;
	var ret = false;
	var w = worldScripts.StationDockControl;
	var debug = w._debug;
	var l_debug = w._logLaunchType;

	if (!s || s.isValid === false) return;
	if (s.script._done === true) return;

	if (s.script._mothership && s.script._mothership.isValid && s.script._mothership.status === "STATUS_IN_FLIGHT") {
		ret = s.offerToEscort(s.script._mothership);
		if (debug && l_debug >= 2) log(this.name, s.displayName + ": (timer) found leader (" + s.script._mothership.displayName + ") - offer to escort: " + ret);
	}
	if (s.script._escorts && s.script._escorts.length > 0 && s.status === "STATUS_IN_FLIGHT") {
		for (var i = 0; i < s.script._escorts.length; i++) {
			ret = s.script._escorts[i].offerToEscort(s);
			if (debug && l_debug >= 2) log(this.name, s.script._escorts[i].displayName + ": (timer) found leader (" + s.displayName + ") - offer to escort: " + ret);
			// turn off the escorts's individual launch script
			if (ret === true) {
				s.script._escorts[i].script._done = true;
			}
			// report on any errors
			if (ret === false) {
				if (s && s.status === "STATUS_LAUNCHING") {
					if (debug && l_debug >= 2) log(this.name, "!!NOTE: Escorts still launching -- setting timer to try again");
					if (s.script._timer && s.script._timer.isRunning) s.script._timer.stop();
					s.script._timer = new Timer(this, s.script.$attachEscort, 2, 0);
				}
				if (s && s.isValid && s.status !== "STATUS_IN_FLIGHT") log(this.name, "!!ERROR: Mothership is not in flight: " + s.status);
				//log(this.name, "!!ERROR: Mothership launch script failed to add escort! " + s.displayName + " (" + s.status + ", " + s.script._escorts.length + "), Escort: " + s.script._escorts[i].displayName + " (" + s.script._escorts[i].status + ")");
			}
		}
	}

	if (ret === false) {
		if (!s.script._timerCount) s.script._timerCount = 0;
		s.script._timerCount += 1;
		if (s.script._timerCount < 10) {
			delete s.script._timer;
			s.script._timer = new Timer(this, s.script.$attachEscort, 2, 0);
		}
		if (s.script._mothership) {
			if (debug && l_debug >= 2) log(this.name, s.displayName + ": (timer) awaiting mothership launch - try " + s.script._timerCount);
		} else {
			if (debug && l_debug >= 2) log(this.name, s.displayName + ": (timer) awaiting mothership/escorts launch - try " + s.script._timerCount);
		}
	} else {
		s.script._done = true;
		// cleanup
		s.script._mothership = null;
		if (s.script._escorts) s.script._escorts.length = 0;
	}
}
Scripts/stationdockcontrol_hackchip.js
"use strict";
this.name = "StationDockControl_HackChip";
this.author = "phkb";
this.copyright = "2015 phkb";
this.description = "Script to control the use of the SDC Hack Chip";
this.license = "CC BY-NC-SA 3.0";

this._chipInstalled = false;
this._installTimer = null;
this._installCount = 0;
this._installStation = null;
this._confiscate = false;
this._debug = false;

//-------------------------------------------------------------------------------------------------------------
this.startUpComplete = function () {
	if (missionVariables.StationDockControl_HackChipCount) this._installCount = missionVariables.StationDockControl_HackChipCount;
	this.$initInterface(player.ship.dockedStation);

	// remove the hack chip from any maintenance emails
	var w = worldScripts.GalCopAdminServices;
	if (w) w._maint_ignore_equip.push("EQ_SDC_HACK_CHIP");
}

//-------------------------------------------------------------------------------------------------------------
this.shipDockedWithStation = function (station) {
	if (this._confiscate === true && station.allegiance === "galcop") {
		player.ship.removeEquipment("EQ_SDC_HACK_CHIP");
		// send an email
		var w = worldScripts.EmailSystem;
		if (w) {
			w.$createEmail({
				sender: "GalCop Security",
				subject: "Computer systems security breach",
				date: global.clock.seconds,
				message: "Commander " + player.name + ",\n\nGalCop network specialists had detected the use of an illegal security device while your ship was previously docked in this system. " +
					" This device has now been appropriated." +
					"\n\nGalCop takes the security of it's network seriously and it would be prudent to do the same yourself.\n\nGood day, Commander.\n\n" +
					expandDescription("%N [nom]") + "\nGalCop Security Officer"
			});
		}
	}
	this.$initInterface(station);
}

//-------------------------------------------------------------------------------------------------------------
this.shipExitedWitchspace = function () {
	// reset the install counter
	this._installCount = 0;
	this._confiscate = false;
}

//-------------------------------------------------------------------------------------------------------------
this.playerBoughtEquipment = function (equipmentKey) {
	if (equipmentKey === "EQ_SDC_HACK_CHIP") {
		this.$initInterface(player.ship.dockedStation);
	}
}

//-------------------------------------------------------------------------------------------------------------
this.playerWillSaveGame = function () {
	missionVariables.StationDockControl_HackChipCount = this._installCount;
	/* Note: We're not storing the state of the hack chip. This means, if the player installs the hack chip and then saves the game, when they
	restore the game the chip won't be installed. However, the number of times they have installed the chip is captured.
	This is probably the easier way of limiting any save game abuse, without having to store the state of the timer.
	*/
}

//-------------------------------------------------------------------------------------------------------------
this.shipLaunchedFromStation = function (station) {
	if (this._chipInstalled === true) {
		// there's a couple of options here, based on how the hack chip would actually be installed
		// if the hack ship is installed inside your ship, then launching would simplky uninstalled it
		// if the hack chip is installed at a computer port outside your ship, then launching would leave it behind
		// for the moment i'm going with the second option
		this._chipInstalled = false;
		player.ship.removeEquipment("EQ_SDC_HACK_CHIP");
		this._installTimer.stop();
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$initInterface = function $initInterface(station) {
	if (player.ship.equipmentStatus("EQ_SDC_HACK_CHIP") === "EQUIPMENT_OK" && (station.allegiance === "galcop" || this._debug === true)) {
		station.setInterface(this.name, {
			title: "Install SDC hack chip",
			category: "Ship Systems",
			summary: "Installs the SDC hack chip into the GalCop ship systems interface terminal.",
			callback: this.$installHackChip.bind(this)
		});
	} else {
		this.$removeInterface(station);
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$removeInterface = function $removeInterface(station) {
	station.setInterface(this.name, null);
}

//-------------------------------------------------------------------------------------------------------------
this.$installHackChip = function $installHackChip() {
	this._chipInstalled = true;
	// switch the interface display
	player.ship.dockedStation.setInterface(this.name, {
		title: "Remove SDC hack chip",
		category: "Ship Systems",
		summary: "Removes the SDC hack chip from the GalCop ship systems interface terminal.",
		callback: this.$removeHackChip.bind(this)
	});
	// start a timer
	// calc how long the player has
	var secs = (25 - (this._installCount * 10)) - system.info.techlevel;
	if (secs < 5) secs = 5;

	this._installCount += 1;
	this._installStation = player.ship.dockedStation;

	mission.runScreen({
		screenID: "oolite-sdc-hackchip-summary",
		title: "SDC Hack Chip",
		overlay: {
			name: "sdc-unlock.png",
			height: 546
		},
		message: "The SDC Hack Chip has been installed successfully",
		exitScreen: "GUI_SCREEN_INTERFACES"
	});

	if (this._installTimer && this._installTimer.isRunning) this._installTimer.stop();
	this._installTimer = new Timer(this, this.$installTimer, secs, 0);
}

//-------------------------------------------------------------------------------------------------------------
this.$installTimer = function $installTimer() {
	// caught!!
	this._chipInstalled = false;
	this._installTimer = null;
	// remove the hack chip (if we're still docked)
	if (player.alertCondition === 0) {
		player.ship.removeEquipment("EQ_SDC_HACK_CHIP");
		this.$removeInterface(this._installStation);
	}
	// fine the player
	player.ship.setBounty(player.bounty + 30, "illegal activity");
	var fine_amount = 5000;
	if (player.credits > fine_amount) {
		player.credits -= fine_amount;
	} else {
		fine_amount = player.credits;
		player.credits = 0;
	}
	var w = worldScripts.EmailSystem;
	if (player.alertCondition !== 0) {
		// confiscate the chip if the play docks in the same system
		this._confiscate = true;
		this._installStation.commsMessage("GalCop security breach detected! You have been fined and your bounty increased!", player.ship);
		if (w) {
			w.$createEmail({
				sender: "GalCop Security",
				subject: "Computer systems security breach",
				date: global.clock.seconds,
				message: "Commander " + player.name + ",\n\nGalCop network specialists detected a security breach and traced the packet data to an " +
					"illegal device attached to a computer terminal in your vessel while you docked in " + system.info.name + ". You have been given a bounty and fined " + formatCredits(fine_amount, true, true) +
					".\n\nGalCop takes the security of it's network seriously and it would be prudent to do the same yourself.\n\nGood day, Commander.\n\n" +
					expandDescription("%N [nom]") + "\nGalCop Security Officer"
			});
		}
	} else {
		player.consoleMessage("GalCop security breach detected! You have been fined, your bounty has been increased, and the device confiscated!");
		// send an email, if installed
		if (w) {
			w.$createEmail({
				sender: "GalCop Security",
				subject: "Computer systems security breach",
				date: global.clock.seconds,
				message: "Commander " + player.name + ",\n\nGalCop network specialists detected a security breach and traced the packet data to an " +
					"illegal device attached to a computer terminal in your vessel. This device has been appropriated, and you have been given a bounty and fined " + formatCredits(fine_amount, true, true) +
					".\n\nGalCop takes the security of it's network seriously and it would be prudent to do the same yourself.\n\nGood day, Commander.\n\n" +
					expandDescription("%N [nom]") + "\nGalCop Security Officer"
			});
		}
	}

}

//-------------------------------------------------------------------------------------------------------------
this.$removeHackChip = function $removeHackChip() {
	this._installTimer.stop();
	this._chipInstalled = false;
	this.$initInterface(player.ship.dockedStation);
	mission.runScreen({
		screenID: "oolite-sdc-hackchip-summary",
		title: "SDC Hack Chip",
		overlay: {
			name: "sdc-lock.png",
			height: 546
		},
		message: "The SDC Hack Chip has been removed successfully",
		exitScreen: "GUI_SCREEN_INTERFACES"
	});
}
Scripts/stationdockcontrol_interface.js
"use strict";
this.name = "StationDockControl_Interface";
this.author = "phkb";
this.copyright = "2015 phkb";
this.description = "Controls the Interfaces (F4) traffic control screen.";
this.license = "CC BY-NC-SA 3.0";

this._maxpage = 0; // total number of pages available for display
this._curpage = 1; // the current page being displayed
this._msRows = 16; // rows to display on the mission screen
this._msCols = 32; // columns to display on the mission screen
this._showAll = false; // flag that controls whether the short or long list of ships is shown
this._displayMode = 0; // controls the type of display on the Traffic Control screen (0 = dock list, 1 = individual ship)
this._selectedItem = {}; // item selected on the Traffic Control screen
this._selectedIndex = 0; // the array index of the selected item
this._sortCol = "departureTime"; // default sort column
this._sortDir = 1; // default sort direction
this._rsnInstalled = false; // indicates whether randomshipnames OXP is installed
this._playerDockingBay = ""; // stores the docking bay assigned to the player
this._playerDockTime = 0; // stores the time the player docked at the station
this._initialPageLimit = 3; // sets the initial number of pages that will be shown when the list is first opened
this._refreshPageTime = 20; // number of seconds between page refreshes, when the view is open
this._rsnInMFD = true;
this._mfdAllStations = false;

this._viewing = []; // subset of main array used for viewing docked vessels on the interface screen
this._SDC = null; // link to the main station dock control worldscript
this._debug = false; // controls output of information to the log. Slaved to StationDockControl._debug
this._selectedStation = null; // the station whose dock list is being viewed. When not in debug mode, this will always be the players docked station.
this._refresh = null; // link to a timer that refreshes the dock list
this._stationModel = false; // controls whether a model of the station is included on the station traffic contorl screen
this._shipModel = true; // controls whether a model of the selected ship is included on the ship information screen
this._spinModel = false; // controls whether the ship model will be spinning or not
this._showDataKey = false; // include the ship's datakey and personality info on the ship details page
this._showEquipment = false; // include an option to view ship equipment
this._mode = 0; // display mode: 0 = full debug, 1 = standard operations
this._itemColor = "yellowColor";
this._menuColor = "orangeColor";
this._exitColor = "yellowColor";
this._disabledColor = "darkGrayColor";
this._defaultZoomFactor = 3.6;
this._destViewingRange = 12; // the number of hours until departure time that the destination will be viewable

this._externalMenu = [];

this._sdciConfig = {
	Name: this.name,
	Display: "Interface Options",
	Alias: "Station Dock Control",
	Alive: "_sdciConfig",
	Bool: {
		B0: {
			Name: "_shipModel",
			Def: true,
			Desc: "Show ship model"
		},
		B1: {
			Name: "_spinModel",
			Def: false,
			Desc: "Spin ship model"
		},
		B2: {
			Name: "_showDataKey",
			Def: false,
			Desc: "Show data key"
		},
		B3: {
			Name: "_stationModel",
			Def: false,
			Desc: "Show station model"
		},
		B4: {
			Name: "_rsnInMFD",
			Def: true,
			Desc: "Show RSN name in MFD"
		},
		B5: {
			Name: "_mfdAllStations",
			Def: false,
			Desc: "MFD available on all stations"
		},
		Info: "0 - Shows the ship model when viewing entries.\n1 - Ship models will spin when viewed.\n2 - Displays the shipDataKey when viewing entries.\n3 - Shows the station model when viewing main list.\n4 - Shows RSN ship name in docking MFD\n5 - Launch queue MFD available at all stations"
	},
	SInt: {
		S0: {
			Name: "_mode",
			Def: 1,
			Min: 0,
			Max: 1,
			Desc: "Display mode"
		},
		Info: "SDC interface display mode. 0 = full debug, 1 = standard (default)"
	},
};

// note: if a station has the "has_npc_traffic" flag set to false, it will override whatever is in this list
// this item controls which station roles will *not* have the "Traffic Control" interface
this._noDockView = ["slaver_base", "rrs_slaverbase", "rrs_group_spacestation", "rrs_astromine", "rrs-mining-outpost", "liners_liner", "navystat",
	"casinoship", "astromine", "random_hits_any_spacebar", "comczgf", "thecollector_adck_coriolis-station", "jaguar_company_base", "GW-fuel-processor", "GW-comms-station",
	"laveAcademy_academy", "planetFall_surface", "generationship", "IST_genship carrier", "pirate-cove"
];

// default zoom is 3.6
// these values make ships look bigger or smaller, as appropriate
this._zoomOverride = {
	"Adder": 4.6,
	"Adder (transit)": 4.6,
	"Adder (civilian)": 4.6,
	"Adder (military)": 4.6,
	"Adder (performance)": 4.6,
	"Anaconda": 2.3,
	"Anaconda MMS": 2.3,
	"Anaconda (service)": 2.3,
	"Anaconda (civilian)": 2.3,
	"Anaconda (military)": 2.3,
	"Anaconda (performance)": 2.3,
	"Aphid": 3.2,
	"Arafura": 3,
	"Boa Class Cruiser": 2.5,
	"Boa Class Cruiser MMS": 2.5,
	"Boa MkII (transit)": 2.5,
	"Boa MkII (civilian)": 2.5,
	"Boa MkII (military)": 2.5,
	"Boa MkII (service)": 2.5,
	"Boa": 2.5,
	"Boa MMS": 2.5,
	"Boa MkI (service)": 2.5,
	"Boa MkI (civilian)": 2.5,
	"Boa MkI (military)": 2.5,
	"Boa MkI (performance)": 2.5,
	"Bug": 4.4,
	"Chameleon": 3.3,
	"Chopped Cobra": 3,
	"Cobra Mark I": 4.6,
	"Cobra MkI (transit)": 4.6,
	"Cobra MkI (civilian)": 4.6,
	"Cobra MkI (military)": 4.6,
	"Cobra MkI (performance)": 4.6,
	"Cobra Mark II-X": 4,
	"Cobra Mark III": 3,
	"Cobra Mark III MMS": 3,
	"Cobra MkIII (transit)": 3,
	"Cobra MkIII (civilian)": 3,
	"Cobra MkIII (military)": 3,
	"Cobra MkIII (service)": 3,
	"Cobra Mark III-XT": 3,
	"Cobra Mark IV": 2.9,
	"Cobra Mark IV MMS": 2.9,
	"Cobra Rapier": 3,
	"Copperhead Mk2": 3,
	"Cruzer": 3,
	"D.T.T. Atlas": 2.5,
	"D.T.T. Cyclops": 2.5,
	"D.T.T. Kraken": 2.8,
	"D.T.T. MK-1": 3.4,
	"D.T.T. Snake Charmer": 2.7,
	"D.T.T. War Lance": 3.3,
	"Fer-de-Lance": 3.3,
	"Fer de Lance (transit)": 3.3,
	"Fer de Lance (service)": 3.3,
	"Fer de Lance (military)": 3.3,
	"Fer de Lance (performance)": 3.3,
	"Gecko": 4.2,
	"Gecko (transit)": 4.2,
	"Gecko (service)": 4.2,
	"Gecko (military)": 4.2,
	"Gecko (performance)": 4.2,
	"Griffin Mk I": 3.3,
	"Griff Boa Prototype": 2.5,
	"Hognose": 4.3,
	"Iguana": 3.2,
	"Krait": 4.2,
	"Krait (transit)": 4.2,
	"Krait (service)": 4.2,
	"Krait (military)": 4.2,
	"Krait (performance)": 4.2,
	"Lira": 1.9,
	"Mamba Escort": 4.2,
	"Mamba": 4.2,
	"Mamba (transit)": 4.2,
	"Mamba (service)": 4.2,
	"Mamba (military)": 4.2,
	"Mamba (performance)": 4.2,
	"Miner Cobra Mark III": 3,
	"Mining Transporter": 4.6,
	"Monitor Mark II": 3,
	"Monitor": 3,
	"Moray Medical Boat": 4.2,
	"Moray (service)": 4.2,
	"Moray Star Boat": 4.2,
	"Moray (civilian)": 4.2,
	"Moray (military)": 4.2,
	"Mussurana": 3,
	"Night Adder": 4.2,
	"Ophidian": 3,
	"Orbital Shuttle": 4.6,
	"Pitviper": 3.4,
	"Pitviper (Vindicator)": 3.4,
	"Pitviper (Buccaneer)": 3.4,
	"Pitviper (Bountyhunter)": 3.4,
	"Pitviper S.E. Beast": 3.4,
	"Pitviper Mark II": 3,
	"Python Class Cruiser": 2.5,
	"Python ET Special": 2.6,
	"Python ET Special MMS": 2.6,
	"Python": 2.5,
	"Python MMS": 2.5,
	"Python (transit)": 2.5,
	"Python (civilian)": 2.5,
	"Python (military)": 2.5,
	"Python (performance)": 2.5,
	"Python SG": 2.5,
	"Python SG-MMS": 2.5,
	"Python BattleCruiser MMS": 2.5,
	"Serpent Class Cruiser": 2.5,
	"Sidewinder Scout Ship": 4.5,
	"Sidewinder (transit)": 4.5,
	"Sidewinder (service)": 4.5,
	"Sidewinder (civilian)": 4.5,
	"Sidewinder (performance)": 4.5,
	"Sidewinder Special": 4.3,
	"Skat": 3.4,
	"Taxi Cab": 4.6,
	"Tembo": 2.2,
	"Transporter": 4.6,
	"Transporter (civilian)": 4.6,
	"Transporter (service)": 4.6,
	"Transporter (military)": 4.6,
	"Transporter (performance)": 4.6,
	"Worm": 4.6,
	"Worm (transit)": 4.6,
	"Worm (service)": 4.6,
	"Worm (military)": 4.6,
	"Worm (performance)": 4.6,
	"Yasen-N": 3.2,
	"Yasen-N 'Advanced' variant 1": 3.2,
	"Yasen-N 'Advanced' variant 2": 3.2,
	"Yasen-N 'Advanced' variant 3": 3.2,
	"YellOo Cab": 4.4,
};

this._trueValues = ["yes", "1", 1, "true", true];

//=============================================================================================================
// system functions
//-------------------------------------------------------------------------------------------------------------
this.startUp = function () {
	this._hudHidden = false;
	this._dockListOpen = false;
}

//-------------------------------------------------------------------------------------------------------------
this.startUpComplete = function () {
	// register our settings, if Lib_Config is present
	if (worldScripts.Lib_Config) worldScripts.Lib_Config._registerSet(this._sdciConfig);

	if (missionVariables.StationDockControl_ShipModel) this._shipModel = (this._trueValues.indexOf(missionVariables.StationDockControl_ShipModel) >= 0 ? true : false);
	if (missionVariables.StationDockControl_SpinModel) this._spinModel = (this._trueValues.indexOf(missionVariables.StationDockControl_SpinModel) >= 0 ? true : false);
	if (missionVariables.StationDockControl_DataKey) this._showDataKey = (this._trueValues.indexOf(missionVariables.StationDockControl_ShowDataKey) >= 0 ? true : false);
	if (missionVariables.StationDockControl_StationModel) this._stationModel = (this._trueValues.indexOf(missionVariables.StationDockControl_StationModel) >= 0 ? true : false);
	if (missionVariables.StationDockControl_RSNinMFD) this._rsnInMFD = (this._trueValues.indexOf(missionVariables.StationDockControl_RSNinMFD) >= 0 ? true : false);
	if (missionVariables.StationDockControl_MFDAllStations) this._mfdAllStations = (this._trueValues.indexOf(missionVariables.StationDockControl_MFDAllStations) >= 0 ? true : false);

	this._SDC = worldScripts.StationDockControl;

	if (this._SDC._disable === true) {
		delete this.shipDied;
		delete this.guiScreenChanged;
		delete this.shipDockedWithStation;
		delete this.shipWillLaunchFromStation;
		delete this.startUpComplete;
		return;
	}

	this._debug = this._SDC._debug;

	// is randomship names installed?
	if (worldScripts["randomshipnames"]) this._rsnInstalled = true;
	// if it isn't, we will have one extra display row on the interface screen.
	if (this._rsnInstalled === false) this._msRows += 1;

	if (missionVariables.StationDockControl_PlayerDockingBay) {
		this._playerDockingBay = missionVariables.StationDockControl_PlayerDockingBay;
	}
	if (missionVariables.StationDockControl_PlayerDockTime) {
		this._playerDockTime = missionVariables.StationDockControl_PlayerDockTime;
	} else {
		// if we don't have a saved dock time, default to 5 minutes ago
		this._playerDockTime = global.clock.adjustedSeconds - (5 * 60);
	}

	// set up the interface screen, if required
	var p = player.ship;
	if (p.dockedStation) {
		if (p.dockedStation.hasNPCTraffic && this.$checkStationRoleForView(p.dockedStation)) {
			this._selectedStation = p.dockedStation;
			this.$initInterface(p.dockedStation);
		}
	}

	// if there are external menu items in the save game, restore them here
	if (missionVariables.StationDockControl_ExternalMenu) {
		this._externalMenu = JSON.parse(missionVariables.StationDockControl_ExternalMenu);
		delete missionVariables.StationDockControl_ExternalMenu;
	}
}

//-------------------------------------------------------------------------------------------------------------
this.shipDied = function (whom, why) {
	// clean up any active timers
	if (this._refresh && this._refresh.isRunning) this._refresh.stop();
}

//-------------------------------------------------------------------------------------------------------------
this.guiScreenChanged = function (to, from) {
	if (from === "GUI_SCREEN_MISSION" && this._dockListOpen) {
		this._dockListOpen = false;
		if (this._hudHidden === false && player.ship.hudHidden === true) {
			player.ship.hudHidden = false;
		}
		if (this._refresh && this._refresh.isRunning) this._refresh.stop();
	}
}

//-------------------------------------------------------------------------------------------------------------
this.shipDockedWithStation = function (station) {
	// set up the interface screen, if required
	this._playerDockTime = global.clock.adjustedSeconds;
	if (system.ID != -1 && station.hasNPCTraffic && this.$checkStationRoleForView(station)) {
		this._selectedStation = station;
		this.$initInterface(station);
	}
}

//-------------------------------------------------------------------------------------------------------------
this.shipWillLaunchFromStation = function (station) {
	this._playerDockingBay = "";
}

//-------------------------------------------------------------------------------------------------------------
this.playerWillSaveGame = function () {
	if (this._playerDockingBay !== "") {
		missionVariables.StationDockControl_PlayerDockingBay = this._playerDockingBay;
	} else {
		delete missionVariables.StationDockControl_PlayerDockingBay;
	}
	missionVariables.StationDockControl_PlayerDockTime = this._playerDockTime;

	// save any external menu items
	if (this._externalMenu.length > 0) {
		missionVariables.StationDockControl_ExternalMenu = JSON.stringify(this._externalMenu);
	}
	missionVariables.StationDockControl_ShipModel = this._shipModel;
	missionVariables.StationDockControl_SpinModel = this._spinModel;
	missionVariables.StationDockControl_ShowDataKey = this._showDataKey;
	missionVariables.StationDockControl_StationModel = this._stationModel;
	missionVariables.StationDockControl_RSNinMFD = this._rsnInMFD;
	missionVariables.StationDockControl_MFDAllStations = this._mfdAllStations;
}

//=============================================================================================================
// external interfaces

//-------------------------------------------------------------------------------------------------------------
// add a custom menu to the ship display, with a callback
// obj has following params:
// 	shipName		name of ship to add menu item for (required if pilotname not specified)
// 	shipType		ship type (required if pilotname not specified)
// 	pilotName		name of pilot (optional, but required if shipname and shiptype are blank)
// 	menuText		text to display in the menu (required)
// 	worldScript		worldScript name for the callback (required)
// 	callback		worldScript function to call in the callback (required)
// 	parameter		parameter to add to the function when called (optional)
this.$externalInterface = function $externalInterface(obj) {
	if ((!obj.shipName || obj.shipName === "") && (!obj.pilotName || obj.pilotName === "")) {
		throw "Invalid settings: shipName or pilotName must be specified";
	}
	if ((!obj.shipType || obj.shipType === "") && (!obj.pilotName || obj.pilotName === "")) {
		throw "Invalid settings: shipType or pilotName must be specified";
	}
	if (!obj.menuText || obj.menuText === "") {
		throw "Invalid settings: menuText must be specified, and cannot be blank.";
	}
	if (!obj.worldScript || obj.worldScript === "") {
		throw "Invalid settings: worldScript (worldScript name) must be specified, and cannot be blank.";
	}
	if (!obj.callback || obj.callback === "") {
		throw "Invalid settings: callback (function name) must be specified, and cannot be blank.";
	}
	var stnName = "";
	var stnIndex = 0;
	if (obj.station) {
		stnIndex = this._SDC.$getStationIndex(obj.station);
		stnName = obj.name;
	}

	this._externalMenu.push({
		shipName: (obj.shipName ? obj.shipName : ""),
		shipType: (obj.shipType ? obj.shipType : ""),
		pilotName: (obj.pilotName ? obj.pilotName : ""),
		stationName: stnName,
		stationIndex: stnIndex,
		menuText: obj.menuText,
		callback: obj.callback,
		worldScript: obj.worldScript,
		parameter: (obj.hasOwnProperty("parameter") ? obj.parameter : "")
	});
}

//-------------------------------------------------------------------------------------------------------------
// removes a custom menu from the list
// obj has following params:
// 	shipName		name of ship to remove the menu item for. optional
// 	shipType		ship type. optional
// 	pilotName		name of pilot. optional.
// 	menuText		text displayed in the menu. required
// 	worldScript		worldScript name. required.
this.$removeExternalInterface = function $removeExternalInterface(obj) {
	for (var i = this._externalMenu.length - 1; i >= 0; i--) {
		if ((!obj.shipName || (this._externalMenu[i].shipName === obj.shipName)) &&
			(!obj.shipType || (this._externalMenu[i].shipType === obj.shipType)) &&
			(!obj.pilotName || (this._externalMenu[i].pilotName === obj.pilotName)) &&
			this._externalMenu[i].menuText === obj.menuText &&
			this._externalMenu[i].worldScript === obj.worldScript) {
			this._externalMenu.splice(i, 1);
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
// returns true if an external menu item exists for the ship/pilot & menu/ws combination
// obj has following params:
// 	shipName		name of ship to remove the menu item for. optional
// 	shipType		ship type. optional
// 	pilotName		name of pilot. optional.
// 	menuText		text displayed in the menu. required
// 	worldScript		worldScript name. required.
this.$externalInterfaceExists = function $externalInterfaceExists(obj) {
	for (var i = this._externalMenu.length - 1; i >= 0; i--) {
		if ((!obj.shipName || (this._externalMenu[i].shipName === obj.shipName)) &&
			(!obj.shipType || (this._externalMenu[i].shipType === obj.shipType)) &&
			(!obj.pilotName || (this._externalMenu[i].pilotName === obj.pilotName)) &&
			this._externalMenu[i].menuText === obj.menuText &&
			this._externalMenu[i].worldScript === obj.worldScript) {
			return true;
		}
	}
	return false;
}

//=============================================================================================================
// screen interfaces

//-------------------------------------------------------------------------------------------------------------
// initialise the F4 screen entries
this.$initInterface = function $initInterface(station) {
	station.setInterface(this.name, {
		title: this._selectedStation.name + " " + expandDescription("[sdc_title]"),
		category: expandDescription("[sdc_category]"),
		summary: expandDescription("[sdc_summary]"),
		callback: this.$showDockedList.bind(this)
	});
	// set up interface for switching the dock view to another station in system
	if (this._mode === 0) {
		station.setInterface(this.name + "_dv", {
			title: "Select station dock view",
			category: expandDescription("[sdc_category]"),
			summary: "Switch the dock view to another station in system",
			callback: this.$selectStationList.bind(this)
		});
	}
}

//-------------------------------------------------------------------------------------------------------------
// show the station selection list
this.$selectStationList = function $selectStationList() {
	var curChoices = {};
	var stns = system.stations;
	for (var i = 0; i < stns.length; i++) {
		if (stns[i].hasNPCTraffic) {
			curChoices["01_STN~" + stns[i].displayName] = {
				text: stns[i].displayName,
				color: "yellowColor"
			};
		}
	}

	curChoices["99_EXIT"] = {
		text: "Exit",
		color: "yellowColor"
	};
	var opts = {
		screenID: "oolite-sdc-switch-map",
		title: "Select Station Traffic Control",
		allowInterrupt: true,
		exitScreen: "GUI_SCREEN_INTERFACES",
		choices: curChoices,
		initialChoicesKey: "99_EXIT",
		message: "Select a station whose traffic control you wish to monitor\n\nCurrently viewing: " + this._selectedStation.displayName +
			"\n\nNote: This function is for testing purposes only. It will not be in the official release of this OXP."
	};
	mission.runScreen(opts, this.$selectStationHandler, this);
}

//-------------------------------------------------------------------------------------------------------------
// handles UI responses from the station selection list
this.$selectStationHandler = function $selectStationHandler(choice) {
	if (choice.indexOf("01_STN") >= 0) {
		var nam = choice.substring(7);
		var stns = system.stations;
		for (var i = 0; i < stns.length; i++) {
			if (stns[i].displayName === nam) {
				this._selectedStation = stns[i];
				break;
			}
		}
		this.$initInterface(player.ship.dockedStation);
	}
	if (choice !== "99_EXIT") {
		this.$selectStationList();
	}
}

//-------------------------------------------------------------------------------------------------------------
// initialise and open the dock list
this.$showDockedList = function $showDockedList() {
	if (this._SDC.$stationIsRepopulating(this._selectedStation) === false) {
		this._SDC.$updateShipsDocking(this._selectedStation);
		this._viewing = this._SDC.$getViewingList(this._selectedStation, false);
		if (this._selectedStation === player.ship.dockedStation) {
			this.$addPlayerToArray(this._viewing);
		}
	}
	this._curpage = 0;
	this._displayMode = 0;
	this._showAll = false;
	this.$showPage();
}

//-------------------------------------------------------------------------------------------------------------
// auto-refresh of page
this.$refreshPage = function $refreshPage() {
	// launched ships should be removed from the array elsewhere, so all we need to do is refresh the page
	if (this._dockListOpen === true) {
		if (this._SDC.$stationIsRepopulating(this._selectedStation) === false) {
			this._SDC.$updateShipsDocking(this._selectedStation);
			this._viewing = this._SDC.$getViewingList(this._selectedStation, false);
			// add the player to the dock list if the docked station is the selected station
			if (this._selectedStation === player.ship.dockedStation) this.$addPlayerToArray(this._viewing);
		}
		this.$showPage();
	}
}

//-------------------------------------------------------------------------------------------------------------
// show the dock list
this.$showPage = function $showPage() {
	function compareDeparture(a, b) {
		if ((a.departureTime * 10 + a.departureSeconds) < (b.departureTime * 10 + b.departureSeconds)) return (worldScripts.StationDockControl_Interface._sortDir * -1);
		if ((a.departureTime * 10 + a.departureSeconds) > (b.departureTime * 10 + b.departureSeconds)) return (worldScripts.StationDockControl_Interface._sortDir);
		return 0;
	}

	function compareShipName(a, b) {
		if (a.shipName < b.shipName) return (worldScripts.StationDockControl_Interface._sortDir * -1);
		if (a.shipName > b.shipName) return (worldScripts.StationDockControl_Interface._sortDir);
		return 0;
	}

	function compareShipType(a, b) {
		if (a.shipType < b.shipType) return (worldScripts.StationDockControl_Interface._sortDir * -1);
		if (a.shipType > b.shipType) return (worldScripts.StationDockControl_Interface._sortDir);
		return 0;
	}

	this._hudHidden = player.ship.hudHidden;
	if (this.$isBigGuiActive() === false) {
		player.ship.hudHidden = true;
	}
	this._dockListOpen = true;

	var p = player.ship;
	var curChoices = {};
	var text = "";
	var def = "99_EXIT";

	var hackchip = worldScripts.StationDockControl_HackChip._chipInstalled;

	// stop the timer if it's running
	if (this._refresh && this._refresh.isRunning) this._refresh.stop();

	if (this._displayMode === 0) {
		// start a timer to refresh the screen
		this._refresh = new Timer(this, this.$refreshPage, this._refreshPageTime, 0);

		switch (this._sortCol) {
			case "departureTime":
				this._viewing.sort(compareDeparture);
				break;
			case "shipType":
				this._viewing.sort(compareShipType);
				break;
			case "shipName":
				this._viewing.sort(compareShipName);
				break;
		}

		// calculate the max number of pages
		this._maxpage = Math.ceil(this._viewing.length / this._msRows);
		if (this._showAll === false && this._maxpage > this._initialPageLimit) this._maxpage = this._initialPageLimit;

		// if we only have 1 page, add some extra lines to the page and hide the next/prev page options
		var extra = 0;
		if (this._maxpage <= 1) extra = 2;

		var col1 = 21;
		var col2 = 4.5;
		var col3 = 6.5;

		// build the header
		text += this.$padTextRight(" Ship", col1);
		text += this.$padTextRight(" Status", col2);
		text += this.$padTextRight(" Destination", col3);

		var disable_screen = false;

		if (this._SDC._repopulate === true || this._SDC.$stationIsRepopulating(this._selectedStation) === true) {
			// if the data is about to be changed in the next repopulate loop, don't show anything
			text += "\n" + this.$duplicate("-", 32) + expandDescription("[sdc-data-loading]") + this.$duplicate("-", 32) + "\n";
			this._maxpage = 0;
			this._curpage = 0;
			disable_screen = true;
		} else {

			var items = 0;
			var start = this._curpage * (this._msRows + extra);
			var end = (this._curpage * (this._msRows + extra)) + (this._msRows + extra);
			var shipItem = "";
			var shipRef = 0;
			for (var i = 0; i < this._viewing.length; i++) {
				items += 1;
				if ((items - 1) >= start && (items - 1) < end) {
					shipRef += 1;
					if (shipRef === (this._msRows + extra) && this._curpage === (this._initialPageLimit - 1) && this._showAll === false) {
						curChoices["01_99_EXPAND"] = {
							text: expandDescription("[showall]"),
							color: this._menuColor,
							alignment: "LEFT"
						};
					} else {
						switch (this._sortCol) {
							case "departureTime":
							case "shipType":
								shipItem = this.$padTextRight(this._viewing[i].shipType +
										((this._viewing[i].shipName && this._viewing[i].shipName.length > 1) ? ": " + this._viewing[i].shipName : ""), col1) +
									this.$padTextRight(this.$departureTimeFormat(this._viewing[i].departureTime), col2) +
									this.$padTextRight((hackchip || this.$viewDestination(this._viewing[i]) ? this.$outputDestination(this._viewing[i]) : ""), col3);
								break;
							case "shipName":
								shipItem = this.$padTextRight(((this._viewing[i].shipName && this._viewing[i].shipName.length > 1) ? this._viewing[i].shipName + " - " : "") + this._viewing[i].shipType, col1) +
									this.$padTextRight(this.$departureTimeFormat(this._viewing[i].departureTime), col2) +
									this.$padTextRight((hackchip || this.$viewDestination(this._viewing[i]) ? this.$outputDestination(this._viewing[i]) : ""), col3);
								break;
						}

						curChoices["01_" + (shipRef < 10 ? "0" : "") + shipRef + "_SHIPITEM_" + i] = {
							text: shipItem,
							color: this._itemColor,
							alignment: "LEFT"
						};
					}
				}
				if ((items - 1) >= end) break;
			}

			for (var i = 0; i < ((this._msRows + extra) + 1) - shipRef; i++) {
				curChoices["02_SPACER_" + i.toString()] = "";
			}
		}

		if (this._maxpage > 1) {
			if (this._curpage < this._maxpage - 1) {
				curChoices["10_GOTONEXT"] = {
					text: "[option_nextpage]",
					color: this._menuColor
				};
			} else {
				curChoices["10_GOTONEXT"] = {
					text: "[option_nextpage]",
					color: this._disabledColor,
					unselectable: true
				};
			}
			if (this._curpage > 0) {
				curChoices["11_GOTOPREV"] = {
					text: "[option_prevpage]",
					color: this._menuColor
				};
			} else {
				curChoices["11_GOTOPREV"] = {
					text: "[option_prevpage]",
					color: this._disabledColor,
					unselectable: true
				};
			}
		}
		if (this._sortCol === "shipType" || disable_screen === true) {
			curChoices["30_SORTBY_SHIPTYPE"] = {
				text: "[option_sortshiptype]",
				color: this._disabledColor,
				unselectable: true
			};
		} else {
			curChoices["30_SORTBY_SHIPTYPE"] = {
				text: "[option_sortshiptype]",
				color: this._menuColor
			};
		}
		if (this._rsnInstalled === true) {
			if (this._sortCol === "shipName" || disable_screen === true) {
				curChoices["31_SORTBY_SHIPNAME"] = {
					text: "[option_sortshipname]",
					color: this._disabledColor,
					unselectable: true
				};
			} else {
				curChoices["31_SORTBY_SHIPNAME"] = {
					text: "[option_sortshipname]",
					color: this._menuColor
				};
			}
		}
		if (this._sortCol === "departureTime" || disable_screen === true) {
			curChoices["32_SORTBY_DEPART"] = {
				text: "[option_sortstatus]",
				color: this._disabledColor,
				unselectable: true
			};
		} else {
			curChoices["32_SORTBY_DEPART"] = {
				text: "[option_sortstatus]",
				color: this._menuColor
			};
		}

		if (this._sortDir === 1 || disable_screen === true) {
			curChoices["35_SORT_ASC"] = {
				text: "[option_sortasc]",
				color: this._disabledColor,
				unselectable: true
			};
		} else {
			curChoices["35_SORT_ASC"] = {
				text: "[option_sortasc]",
				color: this._menuColor
			};
		}
		if (this._sortDir === -1 || disable_screen === true) {
			curChoices["36_SORT_DESC"] = {
				text: "[option_sortdesc]",
				color: this._disabledColor,
				unselectable: true
			};
		} else {
			curChoices["36_SORT_DESC"] = {
				text: "[option_sortdesc]",
				color: this._menuColor
			};
		}
		curChoices["99_EXIT"] = {
			text: "[option_exit]",
			color: this._exitColor
		};

		var pgtitle = this._selectedStation.name + " " + expandDescription("[sdc_title_proper]") + (disable_screen === true || this._maxpage <= 1 ? "" : " - Page " + (this._curpage + 1) + " of " + ((this._maxpage > 3 && this._showAll === false) ? this._initialPageLimit : this._maxpage));
		if (defaultFont.measureString(pgtitle) > 17) {
			pgtitle = expandDescription("[sdc_title_proper]") + (disable_screen === true || this._maxpage <= 1 ? "" : " - Page " + (this._curpage + 1) + " of " + ((this._maxpage > 3 && this._showAll === false) ? this._initialPageLimit : this._maxpage));
		}
		var opts = {
			screenID: "oolite-sdc-main-map",
			title: pgtitle,
			allowInterrupt: true,
			exitScreen: "GUI_SCREEN_INTERFACES",
			choices: curChoices,
			initialChoicesKey: this._lastchoice ? this._lastchoice : def,
			message: text
		};
		if (this._stationModel === true) {
			opts["model"] = "[" + this._selectedStation.dataKey + "]";
			opts["modelPersonality"] = this._selectedStation.entityPersonality;
			opts["spinModel"] = this._spinModel;
		} else {
			opts["overlay"] = {
				name: "sdc-station.png",
				height: 546
			};
		}
	}

	// displaying details of an individual ship
	if (this._displayMode === 1) {
		var depart = this.$departureTimeFormat(this._selectedItem.departureTime);
		var column = 11;

		// ship name/type
		text += ((this._selectedItem.shipName && this._selectedItem.shipName.length > 1) ? this.$padTextRight(expandDescription("[shipdetails-shipname]"), column) + this._selectedItem.shipName + "\n" : "") +
			this.$padTextRight(expandDescription("[shipdetails-shiptype]"), column) + this._selectedItem.shipType + ((this._mode === 0) ? " (" + this._selectedItem.primaryRole + ")" : "") + "\n";

		if (this._showDataKey) {
			text += this.$padTextRight(expandDescription("[shipdetails-datakey]"), column) + this._selectedItem.shipDataKey + " (" + this._selectedItem.personality + ")\n";
			//log(this.name, "player ent person = " + player.ship.entityPersonality);
		}

		// legal status
		if (p.equipmentStatus("EQ_SCANNER_SHOW_MISSILE_TARGET") === "EQUIPMENT_OK") {
			text += this.$padTextRight(expandDescription("[shipdetails-legal]"), column) +
				(this._selectedItem.bounty === 0 ? "Clean" : (this._selectedItem.bounty > 0 && this._selectedItem.bounty <= 50 ? "Offender" : "Fugitive")) + "\n";
		}
		// docking bay/dock time
		if (depart !== "Inbound") {
			if (this._selectedItem.dockingBay !== "") {
				var pref = this.$getDockingBayPrefix(p.dockedStation);
				text += this.$padTextRight(expandDescription("[shipdetails-dockingbay]"), column) + pref + this._selectedItem.dockingBay + "\n";
			}
			text += this.$padTextRight(expandDescription("[shipdetails-docktime]"), column) + global.clock.clockStringForTime(this._selectedItem.dockTime) + "\n";
		}

		// departure time/status
		var lt = (this._selectedItem.departureTime * 60 + this._selectedItem.lastChange);
		text += this.$padTextRight(((lt > (global.clock.adjustedSeconds + 60) &&
			lt < (global.clock.adjustedSeconds + (12 * 60 * 60))) ? expandDescription("[shipdetails-departtime]") : expandDescription("[shipdetails-status]")), column) + depart + "\n";

		// destination
		switch (depart) {
			// for these items hide the destination - the ship hasn't loaded yet, so no decision on destination has been made
			case "Inbound":
			case "Docking":
			case "Unloading":
			case "Docked":
				break;
			default:
				if (hackchip || this.$viewDestination(this._selectedItem) === true) {
					text += this.$padTextRight(expandDescription("[shipdetails-destinationsystem]"), column) + System.systemNameForID(this._selectedItem.destinationSystem) + "\n";
				}
				if (this._selectedItem.destination && this._selectedItem.destination !== "") {
					text += this.$padTextRight(expandDescription("[shipdetails-destination]"), column) + this.$viewLocation(this._selectedItem.destination) + "\n";
				}
				break;
		}

		// ship groups
		if (this._selectedItem.groupName !== "" && depart !== "Inbound") {
			text += "\n";
			var group = this.$getGroupMembers(this._selectedItem.groupName, this._selectedItem);
			if (group.length > 0) {
				if (this._selectedItem.groupLeader === true) {
					text += this.$padTextRight(expandDescription("[shipdetails-groupleader]"), column) + "Yes\n";
				} else {
					text += this.$padTextRight(expandDescription("[shipdetails-groupmemberleader]"), column) + this.$padTextRight(this.$getGroupLeader(this._selectedItem.groupName), 32 - column) + "\n";
				}
				for (var i = 0; i < group.length; i++) {
					if (i === 0) {
						text += this.$padTextRight(expandDescription("[shipdetails-groupmembers]"), column);
					} else {
						text += this.$padTextRight("", column);
					}
					text += this.$padTextRight(group[i].shipType + (group[i].shipName.length > 1 ? ": " + group[i].shipName : ""), 32 - column) + "\n";
				}
			}
		}
		// ship escorts
		if (this._selectedItem.escortName !== "" && depart !== "Inbound") {
			if (this._selectedItem.groupName === "") text += "\n";
			var escort = this.$getEscortMembers(this._selectedItem.escortName, this._selectedItem);
			if (escort.length > 0) {
				if (this._selectedItem.escortLeader === true) {
					text += this.$padTextRight(expandDescription("[shipdetails-escortleader]"), column) + "Yes\n";
				} else {
					text += this.$padTextRight(expandDescription("[shipdetails-escortmemberleader]"), column) + this.$padTextRight(this.$getEscortLeader(this._selectedItem.escortName), 32 - column) + "\n";
				}
				for (var i = 0; i < escort.length; i++) {
					if (i === 0) {
						text += this.$padTextRight(expandDescription("[shipdetails-escortmembers]"), column);
					} else {
						text += this.$padTextRight("", column);
					}
					text += this.$padTextRight(escort[i].shipType + (escort[i].shipName.length > 1 ? ": " + escort[i].shipName : ""), 32 - column) + "\n";
				}
			}
		}

		// pilot info (if we have it)
		if (this._selectedItem.pilotName !== "") {
			text += "\n";
			text += this.$padTextRight(expandDescription("[shipdetails-pilotname]"), column);
			var colText = this.$columnText((this._selectedItem.isPlayer ? "Commander " : "") + this._selectedItem.pilotName + (this._selectedItem.pilotDescription !== "" ? ", " + this._selectedItem.pilotDescription : ""), 31 - column);
			for (var i = 0; i < colText.length; i++) {
				if (i !== 0) text += this.$padTextRight("", column);
				text += colText[i] + "\n";
			}
			if (this._selectedItem.pilotDescription === "") {
				if (this._selectedItem.pilotSpecies !== "") text += this.$padTextRight(expandDescription("[shipdetails-pilotspecies]"), column) + this.$toProperCase(this._selectedItem.pilotSpecies) + "\n";
				if (this._selectedItem.pilotHomeSystem !== -1) text += this.$padTextRight(expandDescription("[shipdetails-pilothomesystem]"), column) + System.systemNameForID(this._selectedItem.pilotHomeSystem) + "\n";
			}
		}

		if (this._selectedIndex === this._viewing.length - 1) {
			curChoices["90_NEXTSHIP"] = {
				text: "[option_nextship]",
				color: this._disabledColor,
				unselectable: true
			};
		} else {
			curChoices["90_NEXTSHIP"] = {
				text: "[option_nextship]",
				color: this._menuColor
			};
		}
		if (this._selectedIndex === 0) {
			curChoices["91_PREVSHIP"] = {
				text: "[option_prevship]",
				color: this._disabledColor,
				unselectable: true
			};
		} else {
			curChoices["91_PREVSHIP"] = {
				text: "[option_prevship]",
				color: this._menuColor
			};
		}
		if (this._showEquipment === true) curChoices["93_EQUIPMENT"] = {
			text: "[option_equipment]",
			color: this._menuColor
		};

		// only offer a launch before/after if the ship has a scheduled departure time visible
		if (p.dockedStation === this._selectedStation) {
			if (depart.indexOf(":") >= 0 && ((player.bounty === 0 && this._selectedItem.destinationHidden === false) || this._mode === 0)) {
				// and they're not due in the 10 minutes or less
				if (lt > (global.clock.adjustedSeconds + (10 * 60)) && depart !== "00:10") {
					curChoices["95_LAUNCH_BEFORE"] = {
						text: "[option_requestbefore]",
						color: this._menuColor
					};
				}
				if (lt >= (global.clock.adjustedSeconds + (10 * 60))) {
					curChoices["96_LAUNCH_AFTER"] = {
						text: "[option_requestafter]",
						color: this._menuColor
					};
				}
			}
		}
		if (hackchip && depart.indexOf(":") >= 0 && this._selectedItem.destinationHidden === true && lt >= (global.clock.adjustedSeconds + (10 * 60))) {
			curChoices["97_HACKCHIP"] = {
				text: "[hackchip-override]",
				color: this._menuColor
			};
		}

		// add the custom menu items here, if any
		for (var i = 0; i < this._externalMenu.length; i++) {
			// don't add menus to ships that are inbound
			if (depart !== "Inbound") {
				// is this menu limited to a specific station?
				var stnName = this._externalMenu[i].stationName;
				var stnIndex = parseInt(this._externalMenu[i].stationIndex);
				var stn = null;
				if (stnName !== "") stn = this._SDC.$getStationFromName(stnName, stnIndex);
				// if the specific station is not the current player station, just continue.
				if (stn !== null && stn !== player.ship.dockedStation) continue;
				// match based on shipname/type or pilot name
				if (this._externalMenu[i].shipName === "*" ||
					(this._externalMenu[i].shipName === this._selectedItem.shipName && this._externalMenu[i].shipType === this._selectedItem.shipType) ||
					(this._externalMenu[i].pilotName === this._selectedItem.pilotName)) {
					curChoices["80_EXT:" + (i < 10 ? "0" : "") + i.toString()] = {
						text: this._externalMenu[i].menuText,
						color: this._menuColor
					};
				}
			}
		}

		if (this._mode === 0) curChoices["97_LOG"] = {
			text: "[option_log]",
			color: this._menuColor
		};
		curChoices["98_CLOSE"] = {
			text: "[option_close]",
			color: this._exitColor
		};
		def = "98_CLOSE";
		var opts = {
			screenID: "oolite-sdc-ship-map",
			title: expandDescription("[ship_details_title]"),
			allowInterrupt: true,
			exitScreen: "GUI_SCREEN_INTERFACES",
			choices: curChoices,
			initialChoicesKey: this._shiplastchoice ? this._shiplastchoice : def,
			message: text,
		};
		if (this._shipModel === true) {
			opts["model"] = "[" + this._selectedItem.shipDataKey + "]";
			opts["modelPersonality"] = this._selectedItem.personality;
			opts["spinModel"] = this._spinModel;
		}
	}

	if (this._displayMode === 2) {
		var column = 11;

		// ship name/type
		text += ((this._selectedItem.shipName && this._selectedItem.shipName.length > 1) ? this.$padTextRight(expandDescription("[shipdetails-shipname]"), column) + this._selectedItem.shipName + "\n" : "") +
			this.$padTextRight(expandDescription("[shipdetails-shiptype]"), column) + this._selectedItem.shipType + ((this._mode === 0) ? " (" + this._selectedItem.primaryRole + ")" : "") + "\n\n";

		var eqlist = this.$equipmentListDisplay(this._selectedItem);
		if (eqlist.length > 0) {
			for (var i = 0; i < eqlist.length; i++) {
				text += this.$padTextRight((i === 0 ? "Equipment:" : ""), column) + eqlist[i] + "\n";
			}
		}
		curChoices["97_CLOSE"] = {
			text: "[option_close]",
			color: this._exitColor
		};
		def = "97_CLOSE";

		var opts = {
			screenID: "oolite-sdc-ship-map",
			title: expandDescription("[ship_equipment_title]"),
			allowInterrupt: true,
			exitScreen: "GUI_SCREEN_INTERFACES",
			choices: curChoices,
			initialChoicesKey: def,
			message: text
		};

		if (this._shipModel === true) {
			opts["model"] = "[" + this._selectedItem.shipDataKey + "]";
			opts["modelPersonality"] = this._selectedItem.personality;
			opts["spinModel"] = this._spinModel;
		}
	}

	mission.runScreen(opts, this.$screenHandler, this);

	var m = mission.displayModel;
	// rotate model for a better view
	if (m) {
		// if gallery has been loaded and used, remove the frame callback
		var w = worldScripts.gallery;
		if (w && isValidFrameCallback(w.$GalleryFCB)) removeFrameCallback(w.$GalleryFCB);

		var zoom_factor = this._defaultZoomFactor;
		if (this._displayMode === 1 && this._shipModel && this._spinModel === false) {
			if (this._zoomOverride[this._selectedItem.shipType]) zoom_factor = this._zoomOverride[this._selectedItem.shipType];
		}
		// zoom and rotate the image
		m.position = Vector3D(0, 0, m.collisionRadius * zoom_factor);
		m.orientation = m.orientation.rotate([0, 0, 0]);
		m.orientation = m.orientation.rotateZ(Math.PI * 1.2);
		m.orientation = m.orientation.rotateX(-1.1);
	}

	// refreshing the page would lose the XenonUI background, so force a reload here.
	if (!worldScripts.Lib_GUI) {
		if (worldScripts.XenonUI) worldScripts.XenonUI.guiScreenChanged("", "");
		if (worldScripts.XenonReduxUI) worldScripts.XenonReduxUI.guiScreenChanged("", "");
	} else {
		worldScripts.Lib_GUI.guiScreenChanged();
	}
}

//-------------------------------------------------------------------------------------------------------------
// handles UI responses on the dock list screen
this.$screenHandler = function $screenHandler(choice) {
	delete this._lastchoice;
	delete this._shiplastchoice;
	var newChoice = "";

	if (!choice) {
		return; // launched while reading?
	} else if (choice.indexOf("SHIPITEM") >= 0) {
		// get the selected item
		// use the index attached to the end of the choice
		this._selectedIndex = parseInt(choice.substring(choice.indexOf("SHIPITEM") + 9));
		this._selectedItem = this._viewing[this._selectedIndex];
		this._displayMode = 1;
		this._savedChoice = choice;
	} else if (choice.indexOf("80_EXT:") >= 0) {
		// call the external function
		var idx = parseInt(choice.substring(choice.indexOf(":") + 1));
		var w = worldScripts[this._externalMenu[idx].worldScript];
		var result = false;
		if (w) {
			result = w[this._externalMenu[idx].callback](this._selectedIndex, this._externalMenu[idx].parameter);
		}
		if (result === false) return;

	} else if (choice === "01_99_EXPAND") {
		this._showAll = true;
		newChoice = "10_GOTONEXT";
	} else if (choice === "11_GOTOPREV") {
		this._curpage -= 1;
		if (this._curpage === 0) {
			newChoice = "10_GOTONEXT";
		}
	} else if (choice === "10_GOTONEXT") {
		this._curpage += 1;
		if (this._curpage === this._maxpage - 1) {
			newChoice = "11_GOTOPREV";
		}
	} else if (choice === "31_SORTBY_SHIPNAME") {
		this._sortCol = "shipName";
		this._sortDir = 1;
		newChoice = "32_SORTBY_DEPART";
	} else if (choice === "30_SORTBY_SHIPTYPE") {
		this._sortCol = "shipType";
		this._sortDir = 1;
		newChoice = "31_SORTBY_SHIPNAME";
		if (this._rsnInstalled === false) newChoice = "32_SORTBY_DEPART";
	} else if (choice === "32_SORTBY_DEPART") {
		this._sortCol = "departureTime";
		this._sortDir = 1;
		newChoice = "31_SORTBY_SHIPNAME";
		if (this._rsnInstalled === false) newChoice = "30_SORTBY_SHIPTYPE";
	} else if (choice === "35_SORT_ASC") {
		this._sortDir = 1;
		newChoice = "36_SORT_DESC";
	} else if (choice === "36_SORT_DESC") {
		this._sortDir = -1;
		newChoice = "35_SORT_ASC";
	} else if (choice === "90_NEXTSHIP") {
		this._selectedIndex += 1;
		this._selectedItem = this._viewing[this._selectedIndex];
		this._shiplastchoice = choice;
		if (this._selectedIndex === this._viewing.length - 1) {
			this._shiplastchoice = "91_PREVSHIP";
		}
	} else if (choice === "91_PREVSHIP") {
		this._selectedIndex -= 1;
		this._selectedItem = this._viewing[this._selectedIndex];
		this._shiplastchoice = choice;
		if (this._selectedIndex === 0) {
			this._shiplastchoice = "90_NEXTSHIP";
		}
	} else if (choice === "93_EQUIPMENT") {
		this._displayMode = 2;

	} else if (choice === "95_LAUNCH_BEFORE") {
		// check if the ship will accept the request
		//if (this.$checkPermission(this._selectedItem) === false) {
		//	player.consoleMessage("Your request to launch before this ship was denied by the pilot.");
		// } else {
		// advance time to 11 minutes before selected ship departs
		var lt = (this._selectedItem.departureTime * 60 + this._selectedItem.lastChange);
		this.$advanceAndLaunch((lt - (10.5 * 60 + 30)) - global.clock.adjustedSeconds);
		choice = "99_EXIT";
		// }

	} else if (choice === "96_LAUNCH_AFTER") {
		// check if the ship will accept the request
		//if (this.$checkPermission(this._selectedItem) === false) {
		//	player.consoleMessage("Your request to launch after this ship was denied by the pilot.");
		// } else {
		// advance time to to 9 minutes before selected ship departs
		var lt = (this._selectedItem.departureTime * 60 + this._selectedItem.lastChange);
		this.$advanceAndLaunch((lt - (9 * 60 + 30)) - global.clock.adjustedSeconds);
		choice = "99_EXIT";
		// }
	} else if (choice === "97_HACKCHIP") {
		this._selectedItem.destinationHidden = false;
	} else if (choice === "97_LOG") {
		this._SDC.$writeDataToLog(this._selectedItem, "Logging data for ship record");
	} else if (choice === "97_CLOSE") {
		this._displayMode = 1;
	} else if (choice === "98_CLOSE") {
		this._displayMode = 0;
		this._selectedItem = {};
		newChoice = this._savedChoice;
		delete this._savedChoice;
	}

	this._lastchoice = choice;
	if (newChoice !== "") {
		this._lastchoice = newChoice;
	}

	if (choice !== "99_EXIT") {
		this.$showPage();
	}
}

//=============================================================================================================
// helper functions

//-------------------------------------------------------------------------------------------------------------
// function to return current state of repopulate control
this.$repopulateInProgress = function $repopulateInProgress() {
	return this._SDC._repopulate;
}

//-------------------------------------------------------------------------------------------------------------
// used by MFD to get list of ships in dock
this.$getStationDockList = function $getStationDockList(station) {
	function compare(a, b) {
		if ((a.departureTime * 10 + a.departureSeconds) < (b.departureTime * 10 + b.departureSeconds)) return -1;
		if ((a.departureTime * 10 + a.departureSeconds) > (b.departureTime * 10 + b.departureSeconds)) return 1;
		return 0;
	}

	if (this._SDC._repopulate === true) {
		this._viewing = [];
	} else {
		this._viewing = this._SDC.$getViewingList(station, true);
	}
	this._viewing.sort(compare);
	return this._viewing;
}

//-------------------------------------------------------------------------------------------------------------
// returns the shipname of the leader of a particular group
this.$getGroupLeader = function $getGroupLeader(groupName) {
	var leader = "";
	for (var i = 0; i < this._viewing.length; i++) {
		if (this._viewing[i].groupName === groupName && this._viewing[i].groupLeader === true) {
			leader = this._viewing[i].shipType + (this._viewing[i].shipName.length > 1 ? ": " + this._viewing[i].shipName : "");
			break;
		}
	}
	if (leader === "") leader = "(undocked vessel)";
	return leader;
}

//-------------------------------------------------------------------------------------------------------------
// returns the shipname of the leader of a particular escort
this.$getEscortLeader = function $getEscortLeader(escortName) {
	var leader = "";
	for (var i = 0; i < this._viewing.length; i++) {
		if (this._viewing[i].escortName === escortName && this._viewing[i].escortLeader === true) {
			leader = this._viewing[i].shipType + (this._viewing[i].shipName.length > 1 ? ": " + this._viewing[i].shipName : "");
			break;
		}
	}
	if (leader === "") leader = "(undocked vessel)";
	return leader;
}

//-------------------------------------------------------------------------------------------------------------
// returns the members of a particular group as a list of data records. "Exclude" is a record that will be left off
this.$getGroupMembers = function $getGroupMembers(groupName, exclude) {
	var shiplist = [];
	for (var i = 0; i < this._viewing.length; i++) {
		if (this._viewing[i].groupName === groupName && this._viewing[i].groupLeader === false && this._viewing[i] !== exclude) {
			shiplist.push(this._viewing[i]);
		}
	}
	return shiplist;
}

//-------------------------------------------------------------------------------------------------------------
// returns the members of a particular escort as a list of data records. "Exclude" is a record that will be left off
this.$getEscortMembers = function $getEscortMembers(escortName, exclude) {
	var shiplist = [];
	for (var i = 0; i < this._viewing.length; i++) {
		if (this._viewing[i].escortName === escortName && this._viewing[i].escortLeader === false && this._viewing[i] !== exclude) {
			shiplist.push(this._viewing[i]);
		}
	}
	return shiplist;
}

//-------------------------------------------------------------------------------------------------------------
// occasionally the shipUniqueName returns "" even though it's really there. In that instance, pull the unique name out of the display name
this.$getShipUniqueName = function $getShipUniqueName(displayName, shipClassName) {
	return displayName.replace(shipClassName + ": ", "");
}

//-------------------------------------------------------------------------------------------------------------
// return text as "Proper Case"
this.$toProperCase = function $toProperCase(text) {
	return text.replace(/\w\S*/g, function (txt) {
		return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
	});
}

//-------------------------------------------------------------------------------------------------------------
// returns the leader item of a particular group
this.$getGroupLeaderItem = function $getGroupLeaderItem(groupName) {
	for (var i = 0; i < this._viewing.length; i++) {
		if (this._viewing[i].groupName === groupName && this._viewing[i].groupLeader === true) {
			return this._viewing[i];
		}
	}
	return null;
}

//-------------------------------------------------------------------------------------------------------------
// returns the leader item of a particular escort
this.$getEscortLeaderItem = function $getEscortLeaderItem(escortName) {
	for (var i = 0; i < this._viewing.length; i++) {
		if (this._viewing[i].escortName === escortName && this._viewing[i].escortLeader === true) {
			return this._viewing[i];
		}
	}
	return null;
}

//-------------------------------------------------------------------------------------------------------------
// appends space to currentText to the specified length in 'em'
this.$padTextRight = function $padTextRight(currentText, desiredLength) {
	if (currentText == null) currentText = "";
	var hairSpace = String.fromCharCode(31);
	var ellip = "…";
	var currentLength = defaultFont.measureString(currentText);
	var hairSpaceLength = defaultFont.measureString(hairSpace);
	// calculate number needed to fill remaining length
	var padsNeeded = Math.floor((desiredLength - currentLength) / hairSpaceLength);
	if (padsNeeded < 1) {
		// text is too long for column, so start pulling characters off
		var tmp = currentText;
		do {
			tmp = tmp.substring(0, tmp.length - 2) + ellip;
			if (tmp === ellip) break;
		} while (defaultFont.measureString(tmp) > desiredLength);
		currentLength = defaultFont.measureString(tmp);
		padsNeeded = Math.floor((desiredLength - currentLength) / hairSpaceLength);
		currentText = tmp;
	}
	// quick way of generating a repeated string of that number
	return currentText + new Array(padsNeeded).join(hairSpace);
}

//-------------------------------------------------------------------------------------------------------------
// arranges text into a array of strings with a particular column width
this.$columnText = function $columnText(originalText, columnWidth) {
	var returnText = [];
	if (defaultFont.measureString(originalText) > columnWidth) {
		var hold = originalText;
		do {
			var newline = "";
			var remain = "";
			var point = hold.length;
			do {
				point = hold.lastIndexOf(" ", point - 1);
				newline = hold.substring(0, point).trim();
				remain = hold.substring(point + 1).trim();
			} while (defaultFont.measureString(newline) > columnWidth);
			returnText.push(newline);
			if (remain !== "") {
				if (defaultFont.measureString(remain) <= columnWidth) {
					returnText.push(remain);
					hold = "";
				} else {
					hold = remain;
				}
			} else {
				hold = "";
			}
		} while (hold !== "");
	} else {
		returnText.push(originalText);
	}
	return returnText;
}

//-------------------------------------------------------------------------------------------------------------
// duplicates text until it is just less than the desired length;
this.$duplicate = function $duplicate(text, desiredLength) {
	var res = "";

	do {
		res += text;
	} while (defaultFont.measureString(res) < desiredLength);

	res = res.substring(1, res.length);

	return res;
}

//-------------------------------------------------------------------------------------------------------------
// returns the docking bay prefix for the selected station
this.$getDockingBayPrefix = function $getDockingBayPrefix(station) {
	if (!station) return "";
	var pref = station.name.toString().substring(0, 1) + String.fromCharCode(65 + (system.ID % 26)) + "-";

	if (station.mass < 50000000) {
		// some smaller stations won't have a prefix
		switch (system.ID % 5) {
			case 0:
			case 2:
				pref = "";
				break;
		}
	} else {
		switch (system.ID % 5) {
			case 0:
				pref += "P-";
				break;
			case 1:
				pref += "T-";
				break;
			case 2:
				pref += "S-";
				break;
			case 3:
				pref += "L-";
				break;
			case 4:
				pref += "G-";
				break;
		}
	}

	return pref;
}

//-------------------------------------------------------------------------------------------------------------
// adds the player to the stations docked list
this.$addPlayerToArray = function $addPlayerToArray(stationList) {
	var p = player.ship;
	if (this._playerDockingBay === "") {
		this._playerDockingBay = this._SDC.$getDockingBayCode(p.dockedStation);
	}

	stationList.push({
		system: system.ID,
		station: p.dockedStation.name,
		shipName: p.shipUniqueName,
		shipType: p.shipClassName,
		shipDataKey: p.dataKey,
		personality: p.entityPersonality,
		primaryRole: p.primaryRole,
		shipAI: "",
		accuracy: 0,
		equipment: "",
		heatInsulation: 0,
		homeSystem: -1,
		bounty: p.bounty,
		escortName: "",
		escortLeader: false,
		groupName: "",
		groupLeader: false,
		destinationSystem: p.targetSystem,
		destinationHidden: false,
		goods: "",
		pilotDescription: "",
		pilotName: player.name,
		pilotHomeSystem: -1,
		pilotLegalStatus: p.bounty,
		pilotInsurance: 0,
		pilotSpecies: "",
		isPlayer: true,
		dockTime: this._playerDockTime,
		dockingBay: this._playerDockingBay,
		departureTime: (20 * 60),
		departureSeconds: 0
	});
}

//-------------------------------------------------------------------------------------------------------------
// returns a formatted version of a departure time (hh:mm)
this.$departureTimeFormat = function $departureTimeFormat(depart) {
	// get hours, minutes from depart
	//var sec_num = depart - global.clock.adjustedSeconds;

	if (depart === -10) {
		return "Inbound";
	} else if (Math.round(depart) === -5) {
		return "Rescheduled";
	} else if (depart < 1) {
		return "Launching";
	} else {
		var hours = Math.floor(depart / 60);
		var minutes = Math.floor(depart - (hours * 60));

		// determine status based on time range
		if (hours >= 23) return "Docking";
		if (hours >= 20 && hours < 23) return "Unloading";
		if (hours >= 16 && hours < 20) return "Docked";
		if (hours >= 12 && hours < 16) return "Loading";

		return (hours < 10 ? "0" : "") + hours + ":" + (minutes < 10 ? "0" : "") + minutes;
	}
}

//-------------------------------------------------------------------------------------------------------------
// reurns false if station has a role in the list of roles where no view is allowed. Otherwise true
this.$checkStationRoleForView = function $checkStationRoleForView(station) {
	for (var i = 0; i < station.roles.length; i++) {
		if (this._noDockView.indexOf(station.roles[i]) >= 0) return false;
	}
	return true;
}

//-------------------------------------------------------------------------------------------------------------
// interprets the data of a particular entry and determines whether the destination is viewable or not
this.$viewDestination = function $viewDestination(item) {
	// if the destination is set (not equal to -1)
	//  and player.bounty equals 0
	// 	and not equal to the current system.ID,
	//  and the destinationHidden flag is not set,
	//  and the departure time is within the viewable period
	// then return true (ie the destination is viewable), otherwise false
	var lt = (item.departureTime * 60 + item.lastChange)
	if (item.destinationSystem !== -1 && player.bounty === 0 &&
		(item.destinationSystem !== system.ID || (item.destination && item.destination !== "")) &&
		item.destinationHidden === false &&
		lt < (global.clock.adjustedSeconds + (this._destViewingRange * 60 * 60))) return true;
	return false;
}

//-------------------------------------------------------------------------------------------------------------
this.$viewLocation = function $viewLocation(item) {
	if (item) {
		var items = item.split("|");
		return items[0];
	} else {
		return "";
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$outputDestination = function $outputDestination(item) {
	if (item.destination && item.destination !== "") {
		return this.$viewLocation(item.destination);
	} else {
		return System.systemNameForID(item.destinationSystem);
	}
}

//-------------------------------------------------------------------------------------------------------------
// work out if the ship will allow the player to launch with them
// returns true if allowed, otherwise false
this.$checkPermission = function $checkPermission(item) {
	var retVal = true;
	var checking = item;
	// get the parent item if this is an escort
	if (item.escortName !== "" && item.escortLeader === false) {
		checking = this.$getEscortLeaderItem(item.escortName);
	}
	if (item.groupName !== "" && item.groupLeader === false) {
		checking = this.$getGroupLeaderItem(item.groupName);
	}
	var checkrole = player.roleWeights[parseInt(system.scrambledPseudoRandomNumber(checking.personality) * player.roleWeights.length)];
	switch (checking.primaryRole) {
		case "shuttle":
		case "trader":
		case "trader-courier":
		case "trader-smuggler":
			if (checkrole.indexOf("pirate") >= 0 || checkrole.indexOf("assassin") >= 0) retVal = false;
			break;
		case "pirate":
		case "pirate-light-freighter":
		case "pirate-medium-freighter":
			if (checkrole.indexOf("hunter") >= 0 || checkrole.indexOf("assassin") >= 0) retVal = false;
			break;
		case "assassin-light":
		case "assassin-medium":
		case "assassin-heavy":
			if (checkrole.indexOf("assassin") >= 0) retVal = false;
			break;
		case "hunter":
		case "hunter-medium":
		case "hunter-heavy":
			if (checkrole.indexOf("pirate") >= 0 || checkrole.indexOf("assassin") >= 0) retVal = false;
			break;
	}
	if (this._debug) log(this.name, "Checking permission for " + checking.primaryRole + " against player role " + checkrole + ": result " + retVal);
	return retVal;
}

//-------------------------------------------------------------------------------------------------------------
// advance a given amount of time and launch the player
this.$advanceAndLaunch = function $advanceAndLaunch(advanceTime) {

	var p = player.ship;
	if (this._debug) log(this.name, "Advancing time " + advanceTime + " seconds");
	global.clock.addSeconds(advanceTime);
	var sdc = worldScripts.StationDockControl;
	if (sdc && sdc._disable === false) sdc.$checkForLaunchedShips();

	// launch player
	if (this._debug) log(this.name, "Launching the player");
	if (this.$isBigGuiActive() === false) {
		p.hudHidden = false;
	}
	p.launch();
	this._hudHidden = false;
}

//-------------------------------------------------------------------------------------------------------------
// returns true if a HUD with allowBigGUI is enabled, otherwise false
this.$isBigGuiActive = function $isBigGuiActive() {
	if (oolite.compareVersion("1.83") <= 0) {
		return player.ship.hudAllowsBigGui;
	} else {
		var bigGuiHUD = ["XenonHUD.plist", "coluber_hud_ch01-dock.plist"]; // until there is a property we can check, I'll be listing HUD's that have the allow_big_gui property set here
		if (bigGuiHUD.indexOf(player.ship.hud) >= 0) {
			return true;
		} else {
			return false;
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
// returns array of equipment item descriptions for the specified ship item
this.$equipmentListDisplay = function $equipmentListDisplay(item) {
	var eqList = [];
	if (item.equipment !== "") {
		var list = item.equipment.split(",");
		for (var i = 0; i < list.length; i++) {
			var eq = list[i];
			var def = "";
			var extra = "";
			if (list[i].indexOf(":") >= 0) {
				var subs = list[i].split(":");
				def = subs[0];
				eq = subs[1];
			}
			if (def !== "X") {
				switch (def) {
					case "FORE":
						extra = "Forward laser: ";
						break;
					case "AFT":
						extra = "Aft laser: ";
						break;
					case "PORT":
						extra = "Port laser: ";
						break;
					case "STARBOARD":
						extra = "Starboard laser: ";
						break;
				}
			}
			if (eq !== "") {
				var eqDef = EquipmentInfo.infoForKey(eq);
				if (eqDef) eqList.push(extra + eqDef.name);
			}
		}
	} else {
		var ship = Ship.shipDataForKey(item.shipDataKey);
		var weapon = this.$translateWeaponInfo(ship.forward_weapon_type);
		if (weapon !== "") eqList.push("Forward laser: " + weapon);
		weapon = this.$translateWeaponInfo(ship.aft_weapon_type);
		if (weapon !== "") eqList.push("Aft laser: " + weapon);
		weapon = this.$translateWeaponInfo(ship.port_weapon_type);
		if (weapon !== "") eqList.push("Port laser: " + weapon);
		weapon = this.$translateWeaponInfo(ship.starboard_weapon_type);
		if (weapon !== "") eqList.push("Starboard laser: " + weapon);
		if (this.$checkValue(ship.has_cloaking_device) === true) eqList.push("Cloaking Device");
		if (this.$checkValue(ship.has_ecm) === true) eqList.push("E.C.M. System");
		if (this.$checkValue(ship.has_escape_pod) === true) eqList.push("Escape Pod");
		if (this.$checkValue(ship.has_fuel_injection) === true) eqList.push("Witchdrive Fuel Injectors");
		if (this.$checkValue(ship.has_scoop) === true) eqList.push("Fuel Scoops");
		if (this.$checkValue(ship.has_shield_booster) === true) eqList.push("Shield Boosters");
		if (this.$checkValue(ship.has_shield_enhancer) === true) eqList.push("Military Shield Enhancement");
	}
	return eqList;
}

//-------------------------------------------------------------------------------------------------------------
// translates the weapon info into an actual name
this.$translateWeaponInfo = function $translateWeaponInfo(dta) {
	var result = "";
	if (!dta == null && dta !== "WEAPON_NONE") {
		var wpn = "EQ_" + dta;
		var eq = EquipmentInfo.infoForKey(wpn);
		result = eq.name;
	}
	return result;
}

//-------------------------------------------------------------------------------------------------------------
// checks a value for true or false
this.$checkValue = function $checkValue(dta) {
	if (!dta == null) {
		if (dta === "yes" || dta === "true" || dta === "1" || parseInt(dta) > 1) {
			return true;
		} else {
			return false;
		}
	} else {
		return false;
	}
}
Scripts/stationdockcontrol_mfd.js
"use strict";
this.name = "StationDockControl_MFD";
this.author = "phkb";
this.copyright = "2015 phkb";
this.description = "Displays the list of ships docked at the current station in an MFD.";
this.license = "CC BY-NC-SA 3.0";

this._mfdLineLength = 14.2; // horizontal measure of the max width of a line in the MFD (15 is the width of an MFD, minus some margin)
this._mfdMaxLines = 10; // maximum number of lines visible in the MFD (10 - 1 for the header line)
this._shipSpace = 13; // column width for ship name (remaining space will be departure time)
this._update = null; // object to hold timer that will refresh the MFD
this._refreshMFDTime = 11; // number of seconds between MFD view refreshes
this._stationTarget = null; // the current target for the MFD
this._playerIsDocking = false; // flag to indicate the player has requested docking clearance
this._dockTimer = null;
this._mfdID = -1;
this._debug = false;

//=============================================================================================================
// system functions

//-------------------------------------------------------------------------------------------------------------
this.startUpComplete = function () {
	// if the OXP is disabled, then delete the relevant functions here
	if (worldScripts.StationDockControl._disable === true) {
		delete this.shipTargetAcquired;
		delete this.playerRequestedDockingClearance;
		delete this.shipWillLaunchFromStation;
		delete this.shipExitedWitchspace;
		delete this.startUpComplete;
		delete this.shipDockedWithStation;
		delete this.playerWillEnterWithspace;
		delete this.playerStartedAutoPilot;
		delete this.playerCancelledAutoPilot;
	} else {
		this._debug = worldScripts.StationDockControl._debug;

		var h = worldScripts.hudselector;
		if (h) h.$HUDSelectorAddMFD(this.name, "LaunchQueueMFD");
	}
}

//-------------------------------------------------------------------------------------------------------------
// turn on mfd when a station is targeted
this.shipTargetAcquired = function (target) {
	if (target.isStation) {
		// if the control is enabled and we don't have a station target set yet...
		//if (!this._stationTarget) { // removed this condition, to cater for the (very rare) situation of having two stations in range of the player.
		// if the station has NPC traffic and is publishing a view of docked ships...
		//if (target.hasNPCTraffic && worldScripts.StationDockControl_Interface.$checkStationRoleForView(target)) {
		// only show the MFD for galcop aligned stations
		if (target.hasNPCTraffic && (worldScripts.StationDockControl_Interface._mfdAllStations == true || target.allegiance === "galcop")) {
			// set the target station and start running the update timer
			this._stationTarget = target;
			if (!this._update || this._update.isRunning === false) {
				this._update = new Timer(this, this.$mfd_updateview, 1, this._refreshMFDTime);
			}
		}
		// }
	}
}

//-------------------------------------------------------------------------------------------------------------
this.shipWillLaunchFromStation = function (station) {
	this._playerIsDocking = false;
	this.$mfd_updateview();
}

//-------------------------------------------------------------------------------------------------------------
this.playerRequestedDockingClearance = function (message) {
	if (this._debug) log(this.name, "Player docking request message: " + message);
	if (this._stationTarget) {
		if (message === "DOCKING_CLEARANCE_GRANTED" || message === "DOCKING_CLEARANCE_EXTENDED" || message === "DOCKING_CLEARANCE_DENIED_TRAFFIC_INBOUND" || message === "DOCKING_CLEARANCE_DENIED_TRAFFIC_OUTBOUND") {
			this._playerIsDocking = true;
			if (message === "DOCKING_CLEARANCE_GRANTED" || message === "DOCKING_CLEARANCE_EXTENDED") {
				// start a timer so we can track when the clearance expires
				this.$stopTimers();
				this._dockTimer = new Timer(this, this.$dockingExpired, 121, 0);
			}
		} else {
			this._playerIsDocking = false;
			this.$stopTimers();
		}
		this.$mfd_updateview();
	}
}

//-------------------------------------------------------------------------------------------------------------
this.playerStartedAutoPilot = function () {
	this._playerIsDocking = true;
	this.$mfd_updateview();
}

//-------------------------------------------------------------------------------------------------------------
this.playerCancelledAutoPilot = function () {
	this._playerIsDocking = false;
	this.$mfd_updateview();
}

//-------------------------------------------------------------------------------------------------------------
this.shipDockedWithStation = function (station) {
	this.$stopTimers();
}

//-------------------------------------------------------------------------------------------------------------
this.playerWillEnterWitchspace = function () {
	this.$stopTimers();
}

//-------------------------------------------------------------------------------------------------------------
this.shipExitedWitchspace = function () {
	this._stationTarget = null;
	this.$mfd_updateview();
}

//=============================================================================================================
// mfd functions

//-------------------------------------------------------------------------------------------------------------
// stops the dock timer
this.$stopTimers = function $stopTimers() {
	if (this._dockTimer && this._dockTimer.isRunning) {
		this._dockTimer.stop();
		delete this._dockTimer;
	}
}

//-------------------------------------------------------------------------------------------------------------
// checks for an expired docking request
this.$dockingExpired = function $dockingExpired() {
	if (this._playerIsDocking === true) {
		this._playerIsDocking = false;
		this.$mfd_updateview();
	}
}

//-------------------------------------------------------------------------------------------------------------
// updates the text in the MFD
this.$mfd_updateview = function $mfd_updateview() {
	var p = player.ship;
	if (p.isValid === false) return;

	this.$findMFDID();

	if (this._stationTarget && this._stationTarget.isValid && p.position.distanceTo(this._stationTarget) < p.scannerRange) {
		if (p.isInSpace) {
			var w = worldScripts.StationDockControl_Interface;
			var sdc = worldScripts.StationDockControl;
			var output = "";
			var lineCount = 1; // header line
			// if a repopulate is in progress, don't try and show data
			if (sdc.$stationIsRepopulating(this._stationTarget) === false) {
				var docklist = w.$getStationDockList(this._stationTarget);
				// find dock for station
				if (this._stationTarget.subEntities) {
					for (var i = 0; i < this._stationTarget.subEntities.length; i++) {
						if (this._stationTarget.subEntities[i].isDock) {
							// get count on number of ships docking
							// note: this seems to get out of sync with the count of ships returned by the $getDockingShips function.
							// Need to find out why
							var docking = 0;
							docking = this._stationTarget.subEntities[i].dockingQueueLength;
							// the player is not counted in dockingQueueLength, so add them in here, if they're docking
							if (this._playerIsDocking) docking += 1;
							// start building the output
							var pt1 = "Ships docking: " + docking;
							var pt2 = "In launch corridor: " + this._stationTarget.subEntities[i].launchingQueueLength;
							// build the top line so part 1 is on the left and part 2 is on the right, with all the extra space in between
							if (defaultFont.measureString(pt1 + pt2) < this._mfdLineLength) {
								output = w.$padTextRight(pt2 + pt1, this._mfdLineLength);
								output = output.substring(pt2.length) + pt2 + "\n";
							} else {
								// if it's already too wide width any spacing, just concat the text and let the MFD handle the squashing
								output = pt1 + " " + pt2 + "\n";
							}

							// if there's ships docking, get the details here
							if (docking > 0) {
								var data = this.$getDockingShips(this._stationTarget);
								output += data.text;
								lineCount += data.lines;
							}
							break;
						}
					}
				}
				// if we haven't filled up the MFD with inbound ships, then fill the rest with docked ships
				var rsn = sdc._rsnInstalled;
				if (rsn === true && w._rsnInMFD === false) rsn = false;
				if (lineCount <= this._mfdMaxLines) {
					for (var i = 0; i < docklist.length; i++) {
						var offset = 0;
						if (docklist[i].departureTime < 1) offset = 2;

						output += w.$padTextRight(docklist[i].shipType + (rsn === true ? ": " + docklist[i].shipName : ""), this._shipSpace - offset) + " " + w.$departureTimeFormat(docklist[i].departureTime) + "\n";
						lineCount += 1;
						// break out once we reach the limit
						if (lineCount === this._mfdMaxLines) break;
					}
				}
			} else {
				output += "Establishing communications link\n";
				output += "with station traffic control.\n";
				output += "Please stand by.";
			}
			// set the text in the MFD
			p.setMultiFunctionText("LaunchQueueMFD", output, false);
			// check if the last place to have the launch queue mfd is blank (that is, unset), and if so, set it back to be the launch queue mfd
			if (this._mfdID >= 0 && p.multiFunctionDisplayList[this._mfdID] === "") {
				p.setMultiFunctionDisplay(this._mfdID, "LaunchQueueMFD");
			}
		}
	} else {
		// out of range, so display some appropriate text
		p.setMultiFunctionText("LaunchQueueMFD", "Traffic Control\n\nNo station in range", false);
		if (this._mfdID >= 0) p.setMultiFunctionDisplay(this._mfdID, "");

		if (this._update && this._update.isRunning) this._update.stop();
		delete this._update;
		this._stationTarget = null;
	}
}

//-------------------------------------------------------------------------------------------------------------
// gets list of ships who are currently trying to dock with the station and returns formatted output for mfd
this.$getDockingShips = function $getDockingShips(station) {
	function $isDocking(entity) {
		return entity.isShip && !entity.isPlayer && entity.isValid && entity.isPiloted && !entity.isStation && entity.dockingInstructions && entity.dockingInstructions.station === station;
	}
	function compare(a, b) {
		if (a.shipClassName < b.shipClassName) return -1;
		if (a.shipClassName > b.shipClassName) return 1;
		return 0;
	}

	var ships = system.filteredEntities(this, $isDocking, station, station.scannerRange);
	var output = "";
	var w = worldScripts.StationDockControl_Interface;
	var sdc = worldScripts.StationDockControl;
	var rsn = sdc._rsnInstalled;
	if (!w._rsnInMFD) rsn = false;
	var lineCount = 0;

	// if the player is trying to dock, put them first on the list, so they will always be visible
	if (this._playerIsDocking) {
		ships.push(player.ship);
	}

	// sort the list by ship class name. We don't know (and can't know) the order in which ships dock, so a classname sort order will do
	ships.sort(compare);

	// build the output
	for (var i = 0; i < ships.length; i++) {
		output += w.$padTextRight((rsn == false ? ships[i].shipClassName : ships[i].displayName), this._shipSpace) + " Inbound\n";
		lineCount += 1;
		if (lineCount === this._mfdMaxLines) break;
	}

	return {
		text: output,
		lines: lineCount
	};
}

//-------------------------------------------------------------------------------------------------------------
// records the index of the MFD that currently holds the traffic control mfd
this.$findMFDID = function $findMFDID() {
	var p = player.ship;
	if (p && p.multiFunctionDisplayList) {
		for (var i = 0; i < p.multiFunctionDisplayList.length; i++) {
			if (p.multiFunctionDisplayList[i] === "LaunchQueueMFD") this._mfdID = i;
		}
	}
}
Scripts/stationdockcontrol_populator.js
"use strict";
this.name = "StationDockControl_Populator";
this.author = "phkb";
this.copyright = "2015 phkb";
this.description = "Overrides the systemWillRepopulate with a new one that allows the station dock control to launch ships from stations.";
this.license = "CC BY-NC-SA 3.0";

this._groupContent = []; // array of ships and groups so we can keep track of group info when ships dock
this._groupCount = 1; // a counter to get added to group names for uniqueness
this._SDC = null; // object ref to main SDC system
this._debug = false; // controls output of information to the log. Slaved to StationDockControl._debug
this._logPopulator = 0; // controls level of information output to log. Slaved to StationDockControl._logPopulatorType

/*
	This script file replaces the "systemWillRepopulate" function of the standard built in populator.
	Essentially, all the "outbound" routines are removed for traders, shuttles, pirates, assassins and hunters.
	Police launches and Aegis raiders are unaffected.
	All outbound launches are controlled via the stationdockcontrol worldscript.

	We also make a list of all groups and group members here. The reason for this is that when ships dock with a station, generally all group
	information is dropped or missing from the ships at the point of docking, which means we can't tell what group they're in when they dock.
	So, we need to keep track of groups and group members outside of the ship group mechanism so we can reapply ship groups to the docking array.
*/

//-------------------------------------------------------------------------------------------------------------
this.startUpComplete = function () {

	this._SDC = worldScripts.StationDockControl;
	this._debug = this._SDC._debug;
	this._logPopulator = this._SDC._logPopulatorType;

	if (this._SDC._disable === false) {
		// replace the original repopulating function with our new one.
		worldScripts["oolite-populator"].systemWillRepopulate = this.$stationdockcontrol_systemWillRepopulate;
	} else {
		delete this.shipSpawned;
		delete this.shipDied;
		delete this.shipWillEnterWitchspace;
	}

	delete this.startUpComplete;
}

//-------------------------------------------------------------------------------------------------------------
// this handles the process of updating group names to make them unique
// this enables the dock controller to see the whole group when docked
this.shipSpawned = function (ship) {
	if (ship.group) {
		if (!ship.isPolice && !ship.isStation && !ship.isThargoid && ship.isPiloted) {
			if (ship.group.name && ship.group.name.indexOf("-") === -1) {
				ship.group.name = ship.group.name + "-" + this._groupCount;
				this._groupCount += 1;
			}
			if (!ship.group.name) {
				ship.group.name = "unknown group-" + this._groupCount;
				this._groupCount += 1;
			}
			if (ship.group.count > 1) {
				if (this._debug && this._logPopulator >= 2) log(this.name, "Group '" + ship.group.name + "', ship: " + ship.displayName);
				this.$logShipGroup(ship.group.name, ship, (ship.group.leader === ship ? true : false));
			}
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.shipWillEnterWitchspace = function (cause, destination) {
	// clear out the array
	this._groupContent.length = 0;
}

//-------------------------------------------------------------------------------------------------------------
// adds a ship group and leader combination to the array
this.$logShipGroup = function $logShipGroup(groupName, ship, leader) {
	for (var i = 0; i < this._groupContent.length; i++) {
		if (this._groupContent[i].groupName === groupName && this._groupContent[i].ship === ship) return;
	}
	this._groupContent.push({
		groupName: groupName,
		ship: ship,
		leader: leader
	});
}

//-------------------------------------------------------------------------------------------------------------
// removes a ship from the data
this.$removeShip = function $removeShip(ship) {
	if (this._debug && this._logPopulator >= 2) log(this.name, "Removing ship: " + ship.displayName);
	for (var i = this._groupContent.length - 1; i >= 0; i--) {
		if (this._groupContent[i].ship === ship) {
			// remove the item from the array
			this._groupContent.splice(i, 1);
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
// gets the group leader for a particular group
this.$getGroupLeader = function $getGroupLeader(groupName) {
	for (var i = 0; i < this._groupContent.length; i++) {
		if (this._groupContent[i].groupName === groupName && this._groupContent[i].leader === true) return this._groupContent[i].ship;
	}
	return null;
}

//-------------------------------------------------------------------------------------------------------------
// get the group name attached to a particular ship
this.$getGroupName = function $getGroupName(ship) {
	for (var i = 0; i < this._groupContent.length; i++) {
		if (this._groupContent[i].ship === ship) return this._groupContent[i].groupName;
	}
	return "";
}

//-------------------------------------------------------------------------------------------------------------
// function responsible for replenishing system contents
this.$stationdockcontrol_systemWillRepopulate = function $stationdockcontrol_systemWillRepopulate() {

	// if main station or planet is missing, something odd has
	// happened, so stop repopulation
	if (system.sun.isGoingNova || !system.mainStation || !system.mainPlanet) {
		return;
	}
	/* repopulate incoming traffic */

	// traders
	if (Math.random() < this.$repopulatorFrequencyIncoming.traderFreighters) {
		this._debugR("Incoming freighter");
		this._addFreighter(this._wormholePos());
	}
	if (Math.random() < this.$repopulatorFrequencyIncoming.traderCouriers) {
		this._debugR("Incoming courier");
		if (Math.random() < 0.5) {
			this._addCourierShort(this._wormholePos());
		} else {
			this._addCourierLong(this._wormholePos());
		}
	}
	if (Math.random() < this.$repopulatorFrequencyIncoming.traderSmugglers) {
		this._debugR("Incoming smuggler");
		this._addSmuggler(this._wormholePos());
	}

	// pirates
	if (Math.random() < this.$repopulatorFrequencyIncoming.pirateLightPacks) {
		if (system.countShipsWithPrimaryRole("pirate-light-freighter") < 6) {
			this._debugR("Incoming light pirate");
			this._addLightPirateRemote(this._wormholePos());
		}
	}
	if (Math.random() < this.$repopulatorFrequencyIncoming.pirateLightPacksReturn) {
		if (system.countShipsWithPrimaryRole("pirate-light-freighter") < 6) {
			this._debugR("Returning light pirate");
			this._addLightPirateReturn(this._wormholePos());
		}
	}
	if (Math.random() < this.$repopulatorFrequencyIncoming.pirateMediumPacks) {
		if (system.countShipsWithPrimaryRole("pirate-medium-freighter") < 4) {
			this._debugR("Incoming medium pirate");
			this._addMediumPirateRemote(this._wormholePos());
		}
	}
	if (Math.random() < this.$repopulatorFrequencyIncoming.pirateMediumPacksReturn) {
		if (system.countShipsWithPrimaryRole("pirate-medium-freighter") < 4) {
			this._debugR("Returning medium pirate");
			this._addMediumPirateReturn(this._wormholePos());
		}
	}
	if (Math.random() < this.$repopulatorFrequencyIncoming.pirateHeavyPacks) {
		if (system.countShipsWithPrimaryRole("pirate-heavy-freighter") < 2) {
			this._debugR("Incoming heavy pirate");
			this._addHeavyPirateRemote(this._wormholePos());
		}
	}
	if (Math.random() < this.$repopulatorFrequencyIncoming.pirateHeavyPacksReturn) {
		if (system.countShipsWithPrimaryRole("pirate-heavy-freighter") < 2) {
			this._debugR("Returning heavy pirate");
			this._addHeavyPirateReturn(this._wormholePos());
		}
	}

	// hunters
	if (Math.random() < this.$repopulatorFrequencyIncoming.hunterMediumPacks) {
		if (system.countShipsWithPrimaryRole("hunter-medium") < 5) {
			this._debugR("Incoming medium hunter");
			this._addMediumHunterRemote(this._wormholePos());
		}
	}
	if (Math.random() < this.$repopulatorFrequencyIncoming.hunterMediumPacksReturn) {
		if (system.countShipsWithPrimaryRole("hunter-medium") < 5) {
			this._debugR("Returning medium hunter");
			this._addMediumHunterReturn(this._wormholePos());
		}
	}
	if (Math.random() < this.$repopulatorFrequencyIncoming.hunterHeavyPacks) {
		if (system.countShipsWithPrimaryRole("hunter-heavy") < 3) {
			this._debugR("Incoming heavy hunter");
			this._addHeavyHunterRemote(this._wormholePos());
		}
	}
	if (Math.random() < this.$repopulatorFrequencyIncoming.hunterHeavyPacksReturn) {
		if (system.countShipsWithPrimaryRole("hunter-heavy") < 3) {
			this._debugR("Returning heavy hunter");
			this._addHeavyHunterReturn(this._wormholePos());
		}
	}

	// thargoids (do not appear at normal witchpoint)
	if (Math.random() < this.$repopulatorFrequencyIncoming.thargoidScouts) {
		this._debugR("Incoming thargoid scout");
		this._addThargoidScout(system.locationFromCode("TRIANGLE"));
	}
	if (Math.random() < this.$repopulatorFrequencyIncoming.thargoidStrikes) {
		this._debugR("Incoming thargoid strike force");
		this._addThargoidStrike(system.locationFromCode("TRIANGLE"));
	}

	if (Math.random() < this.$repopulatorFrequencyOutgoing.pirateAegisRaiders) {
		this._addAegisRaiders();
	}

	// police
	if (Math.random() < this.$repopulatorFrequencyOutgoing.policePacks) {
		if (system.countShipsWithPrimaryRole("police") < 30) {
			this._debugR("Launching police patrol");
			this._addPolicePatrol(this._policeLaunch());
		}
	}
	if (Math.random() < this.$repopulatorFrequencyOutgoing.policeInterceptors) {
		if (system.countShipsWithPrimaryRole("police-witchpoint-patrol") < 30) {
			this._debugR("Launching police interception patrol");
			this._addInterceptors(this._policeLaunch());
		}
	}

}
Scripts/stationdockcontrol_shuttlelaunch.js
"use strict";
this.name = "StationDockControl_ShuttleLaunch";
this.author = "phkb";
this.copyright = "2015 phkb";
this.description = "Script to attach to a shuttle after launch to set its target";
this.license = "CC BY-NC-SA 3.0";

/*
	This script is designed to force a particular in-system destination on a shuttle.
	Because the priority AI controller takes a few moments to start up, we need to wait for it to be active before we can redirect the shuttle.
	So a timer is used. The timer is a one-shot, but it will be restarted if the controller isn't active.
*/

//-------------------------------------------------------------------------------------------------------------
this.$initialise = function $initialise() {
	// run a timer so the ai controller has a chance to startup besofe we try and set the destination
	this.ship.script._retryCounter = 0;
	//log(this.name, "setting timer for shuttle destination - " + this.ship.displayName);
	this.ship.script._setDest = new Timer(this, this.ship.script.$setDestination, 2, 0);
}

//-------------------------------------------------------------------------------------------------------------
this.$setDestination = function $setDestination() {
	// do we have a ai controller yet?
	if (!this.ship.AIScript.oolite_priorityai) {
		// start counting after ship is in flight
		if (this.ship.status !== "STATUS_IN_FLIGHT") {
			this.ship.script._retryCounter += 1;
		}
		// give up after ten tries
		if (this.ship.script._retryCounter === 10) {
			//log(this.name, "giving up trying to set shuttle destination for " + this.ship.displayName);
			return;
		}

		// otherwise, reset the timer and try again later
		delete this.ship.script._setDest;

		//log(this.name, "resetting timer for shuttle destination - " + this.ship.displayName);
		this.ship.script._setDest = new Timer(this, this.ship.script.$setDestination, 2, 0);
		return;
	}

	var w = worldScripts.StationDockControl;
	var debug = w._debug;
	var l_debug = w._logLaunchType;

	var items = this._dest.split("|");
	if (items[0] !== "Planet") {
		var stnLoc = w.$getStationFromName(items[0], parseInt(items[1]));
		// check if we got an actual ship location returned
		if (stnLoc) {
			this.ship.AIScript.oolite_priorityai.setParameter("oolite_selectedPlanet", null);
			this.ship.AIScript.oolite_priorityai.setParameter("oolite_selectedStation", stnLoc);
			if (debug && l_debug >= 2) log(this.name, "Setting " + this.ship.displayName + " destination location to " + stnLoc);
		}
	} else {
		this.ship.AIScript.oolite_priorityai.setParameter("oolite_selectedStation", null);
		this.ship.AIScript.oolite_priorityai.setParameter("oolite_selectedPlanet", system.mainPlanet);
		if (debug && l_debug >= 2) log(this.name, "Setting " + this.ship.displayName + " destination location to main planet");
	}
	this.ship.AIScript.oolite_priorityai.reconsiderNow();

	delete this.ship.script._setDest;
}

//-------------------------------------------------------------------------------------------------------------
this.npc_shipDied = function npc_shipDied(whom, why) {
	if (this.ship.script.$sdc_hold_shipDied) this.ship.script.$sdc_hold_shipDied(whom, why);
	if (this.ship.script._setDest && this.ship.script._setDest.isRunning) this.ship.script._setDest.stop();
}

//-------------------------------------------------------------------------------------------------------------
this.npc_shipWillEnterWormhole = function npc_shipWillEnterWormhole() {
	if (this.ship.script.$sdc_hold_shipWillEnterWormhole) this.ship.script.$sdc_hold_shipWillEnterWormhole();
	if (this.ship.script._setDest && this.ship.script._setDest.isRunning) this.ship.script._setDest.stop();
}