"use strict";
this.name = "GalCopBB_Missions";
this.author = "phkb";
this.copyright = "2017 phkb";
this.description = "Adds some local missions to the GalCop Bulletin Board";
this.license = "CC BY-NC-SA 4.0";

/*
	This OXP adds a number of player-selectable missions to most stations in the Ooniverse. 
	The missions are generally local (<= 15 LY travel to destination) and for the most part are
	using whatever ships are installed, rather than needing any customised ships.

	The missions are designed to be achievable by any pilot, although common-sense dictates 
	you won't accept a seek and destroy mission to a dangerous system if 
	your ship isn't combat ready. The missions are all optional - the player has complete control 
	over which missions to accept, and when they will accept them.

	Also included in this mission pack is the concept of mission chains. So, you take one mission, 
	and when you complete it another mission is launched which relates to it.

	It is planned to increase the number of different mission types over time.

	Thanks to Eric Walsh for his derelict ship creation code
	And thanks to cim for allowing his "Black Box" ship model to be included in this OXP.

	To force a particular mission type onto the current mission board (for testing), use the following code:
		worldScripts.GalCopBB_Missions.$testMissionType(136); // change the number to the mission type you want to test
	
	REQUIRED BEFORE BETA:
	- complete missions 36, 37, 45, 47
	- Go over all mission text and improve randomisation
	- Work out the different reputation series, how long each should take to earn highest rank, and what the ranks are.
	- Test all missions, particularly chaining
	- Test disease outbreak event creation and resolution

	TODO
		analyse cargo missions in relation to smugglers relabelled cargo
		overarching mission:
			there are 8 pieces of an ancient artefact, one in each sector, that the player can collect in a number of ways:
				- added as a scoopable item with a type 6/7 mission
				- given to the player from a type 30 mission
				- pulled from the hulk of a ship in a type 22/23/24 mission
				- given to player from a type 40/41/42 mission
				- triggering the entry of an artefact will be based on the number of missions performed in each sector. 
					- 1st artefact will trigger after 10 missions in the sector
					- 2nd artefact will trigger after 20 missions in the sector
					- 3rd artefact will trigger after 30 missions in the sector
					- etc
				- news items to appear after every 10 missions, therefore 55 news items req
			if smugglers is installed
				artefacts can be sold on the black market for big dollars, the more pieces you have the more $
				if player sells items, they can be possibly bought back through the black market for high dollars
					- player will be lead to black market location via items left on the Black market noticeboard
			if smugglers is not installed
				artefacts can be sold through ??
				artefacts can be re-bought through ??
				player lead to location via emails ?? message when docking
			there will be several news items that talk about the artefact in some way to give the player some idea the mission exists in the first place

			once all pieces are found, will lead player to a planet not on the charts. orbiting the planet will be a dockable entity
				there will be thargoids in this part of space, but they will all spontaneously blow up when within 15km of player
				dockable entity will be devoid of F3 and F8 screens. 
				f4 screen will have an "explore station" item
					- turn off hud, show creepy space station backgrounds
					- have small number of locations (5-6 at most), with easy directions accessed via menu
					- goal is a room where the artefact can be installed after completing a small minigame
						- player has to arrange the pieces of the artefact in the correct order by swapping pieces
							- menu will be "Swap piece X...", followed by "...with piece Y"
							- will need an image that can be broken up into 8 pieces
							- will need to limit the number of combinations
						- once solved a short message will be displayed saying target locked
						- when player launches, will receive console messages saying unusual reading coming from planet
						- after a few of these, planet will explode
						- player can then choose to destroy the station
						- station will have defensive turrets (a few of them) that will inhibit the player from doing this
						- possibly there could be drones that launch as well
						- if player doesn't destroy the station... ?

		with goon squads, arrange it so there are some on a direct path between the wp and the destination
		
		Fix issue where ejected policeman shouldn't give player a mission - should be fixed, but test
		Fix issue where ejected police is scooped as a slave - should be fixed, but test
		Have self-destruction police give an exclamation before it explodes.

		Make sure escape pod occupant's name is applied to the client name of any missions they ask you to perform.

		With hit team assassins, they need to declare when they attack why it is they're attacking ("You shouldn't have sold that security device on the BM Commander")
		type 40/41 Check what happens if a cargo container is destroyed after being ejected and before being scooped

	ISSUE: Allowing a mission to fail without having gone to the black market to sell an item, and then afterwards going there to 
		sell the item, would currently mean that no hit teams are sent after the player.
				Q: What to do with items left with the player when a mission expires. 
					Remove them? << currently doing this.
					Allow the player to sell them? 
					Ask the player if they want to dispose of the item?
					>> theoretically this scenario should never happen if the mission is flagged to stop countdown at the point of 
					getting the item.
				* Need interface for disposal of equip items from failed missions so they don't fill up space.
				
	Other mission ideas:
		collection missions
			based on the OXP stations in the game, arrange a multi-point station trip to pickup "macguffins" (or perhaps stranded passengers)
			Possible stations:
				Tionisla Chronicle Array
				Tionisla Orbital Graveyard
				ErehwonStation (from Tionisla Reporter, after mission is complete)
				Taxi station
				Hunting Lodge (Feudal states)
				Astrofactory (Dictators)
				FTZ
				Astromine
				Collective ZGF
			Will need a calculation which will predict which systems will have some of these stations, as they aren't 
			spawned in all systems

		defence missions: defend a target (station, relay, witchpoint beacon, satellite) from waves of attackers
		Multi-part missions (ie, not linked, but actually having multiple parts, involving multiple jumps and different tasks at each jump)
		Opposing missions (for version 2)
			- if there's a black box mission at the GalCop station, there is an equivalent Pirate mission to grab the black box, but only if the initial mission is accepted
			- need to work out what missions could have an opposite position.
				- black box missions (same, but deliver to pirates)
				- data cache missions (same, but deliver to pirates)
				- special cargo recovery (same, but deliver to pirates)
				- collect cargo for sys auth (same, but deliver to pirates)
				- collect cargo for pirate auth (same, but deliver to galcop)
				- meet ship to deliver data (contract kill)
				- recover stolen item (same, but deliver article to pirates)
				- meeting request (opposing mission would be a contract kill)
					? could opposing mission be offered to the player at some intermediate point (email? comms relay near witchpoint?)

		Civil War missions
			Civil war has escalated into space - check out factions and how it sets up fight
		report on pirate strength
			GalCop wants to monitor traffic going in and out of a pirate Rock Hermit in system X. You must fly there, find the hermit, approach and dock while under cloak. Once docked, install special tracking software into station systems from F4 interface. You must not be seen by any pirate ships while you are within 10km of the station. 

		hide and seek
			a ship is hiding in an asteroid field, using a cloaking device. You have to have find them and destroy them

			

	mission types:
	*1 - "Cleanup the spaceways": destroy x number of asteroids in the current system
			any system, max of 1 mission of this type
	*2 - "Making the system safer": destroy x number of pirates in system y.
			any safe system within 7 LY of a feudal or anarchy
			record when mission is accepted and only offer another one to the same system after 10 days
	*3 - "Disrupting trade (System authority)": destroy x number of traders in system y.
			any multi-gov or worse, near a safe system
			record when mission is accepted and only offer another one to the same system after 10 days
	*4 - "Disrupting trade (Pirate authority)": destroy x number of traders in system y.
			any multi-gov or worse, near a safe system
			record when mission is accepted and only offer another one to the same system after 10 days
	*5 - "Thargoid hunt" - intentionally misjump between system X and Y and destroy N number of thargoid motherships (not tharglets)

	*6 - "Collect the deposit" (System authority): find dumped cargo in system X, deliver to system Y.
		- cargo will be guarded by x number of high quality ships
	*7 - "collect the deposit" (Pirate authority)

	*8 - "Pirates for hire (System authority)": attack trader ships in system X to get their cargo (n tons required), 
		return to system Y
	*9 - "Pirates for hire (Pirate authority)": attack trader ships in system X to get their cargo (n tons required), return to system Y
	*10 - "Pirate specific commodity (System authority)": find an amount of a particular commodity but only when in a 
		particular system and only from piracy (not purchased from stations)
	*11 - "Pirate specific commodity (Pirate authority)": find an amount of a particular commodity but only when in a particular system 
		and only from piracy (not purchased from stations)
		
	*12 - "Free slaves": (if IGT is installed) target all ships entering Communist or higher system, and if carrying slaves, 
		demand cargo.
			(Recommend the "Manifest Scanner" be installed)
			Use BroadcastCommsMFD to "Demand slaves"
			Ship will then have the ability to respond: either "OK" (and slaves are dumped), or "I'm not carrying slaves", or "Get lost"
	*13 - "Collect Thargoid wreckage" destroy thargoids and collect wreckage in interstellar space
			add scenario where alloys are destroyed during flight
			>> prevent thargoid alloys from being relabelled by Smugglers
			>> prevent thargoid alloys from being put into storage by SC
	*14 - Gun runner: attack ships in target looking for firearms
	*15 - Drug dealer: attack ships in target looking for narcotics
	*16 - Civil unrest (pirate authority): destroy as many police ships in current system as you can
	*17 - Garbage scow: dump radioactive waste containers near sun so they are destroyed by heat
	*18 - Rival gang war (ie pirate hunt) (pirate authority)

	*20 - Find escape pod in particular system and rescue occupant
			- when in range, send out a distress signal to the player

	*21 - interstellar escape pod rescue
	*22 - Find black box in particular system and collect it 
			- make this different from Rescue Stations by making the black box be inside the hulk of a destroyed ship
			- need some way of locating ship hulk - maybe an intermittent comms signal?
				- have console message appear saying "Weak navigation beacon signal ("U") being picked up."
				- player switches ASC to find "U", gets a heading, then signal disappears. The further away the player is, the more inaccurate the ASC heading is.
				- repeat until the player finds the hulk (ie is comes within scanner range)
			- give player the option of selling black box on the black market
		
	*23 - interstellar black box recovery
	*24 - Destroy derelict ship, scoop data cache (special cargo pod) and return it to station
	*25 - catch runaway escape pod - escape pod ended up with a massive ejection speed and is on it's way to interstellar space. 
		a regular waypoint will be set showing the pods last position
		
	*30 - Find and meet a particular ship in a particular system, will lead to secondary mission
		- via BB as a mission
		- via email when created as a primary mission
		- via a random trader along space lane or at witchpoint

	*31 - go to waypoint in system y, receive message from comms relay
	*32 - Meet ship in system X, give them info package
	*33 - intercept ship carrying stolen data cache,  (use Broadcast comms to demand data pack), which must then be scooped
	34 - track ship from witchpoint to station, then return
			- if player gets with 25 km of target and appears to be following them (eg heading is roughly the same and the player is behind them) they will detect player and flee (mission failed)
			- player can get closer if they are cloaked
			this needs to be more interesting that just following them to the station. 
	36 - Find ship in system X by sending a pass phrase to any/all ships - one of which will reply with a message with details of 
		where to find special cargo to be returned to system Y
	37 - Find ship in system X by sending a pass phrase to any/all ships - one of which will reply with a message with details of 
		where to find data point to be returned to system Y

	delivery-type missions
	*40 - Special delivery - player given unique cargo (poss using Smugglers illegal goods) or equipment, 
			must deliver to remote point in destination, paid in precious metals/gems given to the player at the drop point
	*41 - Special delivery - player given cargo or equipment, must go to system X to find a data point, 
			to get location of drop point in system Y, paid in previous metals/gems given to the player at the drop point
	*42 - special delivery: transport special computer equipment to ship, without docking at a station (docking will render the 
		mission void)
	*43	- transport vital equipment to stricken ship to enable it to continue flying
			- have option of giving player secondary mission here
	*44	- transport vital equipment to stricken ship in interstellar space to enable it to continue flying and escape
	45 - Special delivery (only used for a secondary mission. similar to 40 but without giving cargo) - 
			player told to get Xt of commodity, then deliver to a remote point in destination, paid is precious metals/gems
	46 - pickup cargo from waypoint, delivery to another waypoint
		will need a cargo shepherd, cargo stopper for ejection damper
		arrival at one or other of waypoints could result in an ambush
	47 - (only used for a secondary mission. similar to 41 but without giving cargo) - player told to get Xt of commodity, 
		go to X to find data point to get location of drop point in Y, paid in precious metals/gems
	
	medical missions
	*50 - deliver critical medical supplies to systems with "disease" as part of their description
			- each day do a check for
				(a) is a disease outbreak starting on a system?
					only have, at most 2-3 outbreaks at any one time
					when starting the outbreak, send a news item to snoopers, and define the end point for the outbreak (2-3 months ahead), define propagation chance
						if a player transports medical supplies, take a day off the end point, reduce the propagation chance
				(b) is a disease outbreak stopping on a system?
					send an item to snoopers
				(c) is disease spreading to a neighbouring system
					there is a chance disease will spread to a neighbouring system - add it to the outbreak list, with a news item and end point
			- medical supply missions offered only from hi-tech worlds (TL 12 and above)
				expiry time is based on the volatility of the antibiotics being carried.

	*51 - Transport critically ill patients from disease system to high tech level destination
	*52 - Transport virus specimens
	*53 - collect 3 components for an experimental antidote from different parts of the galaxy
	
	*60 - Hacking witchpoints
		System authorities in system X are concerned about the flow of pirate traffic through their system. They want to hack into the witchpoint beacons of all systems within a 7LY range, that will enable them to monitor traffic flows into their system, outside of GalCop's purview.
			- will require getting within 200m of witchpoint marker, targeting the marker and initiating the special equipment (prime, activate)

	*61 - Restoring witchpoints
		GalCop has become aware of an illegal data stream emanating from witchpoint markers around system X. You need to go to each of the systems listed, and clean the illegal code from the units. 
			- will require getting within 200m of witchpoint marker, targeting the marker and initiating the special equipment (prime, activate)
			- will only be available once mission 60 has been performed at a system

	*62 - deliver security software payload to pirate rock hermit that track all ships coming and going from the station
		- need to use cloaking device to dock with hermit. going red alert within 50k of station will result in mission failure.
		>> TEST
			
	*63 - (from non galcop stations) deliver software payload to main station that will disrupt the trade computers (allowing all 
		commodities to be purchased for 1 cr). (possible) for a period of 2 weeks
			- when installing the software you will be asked what amount to set commodity prices to. You must enter the number 
				specified in the mission description (either 1 or 99).
			- you have to launch within 2 minutes of installing the software (and not redock again for 24 hours) otherwise GalCop 
				will be able to trace the upload and you'll get hit with a massive fine (10000cr, 200 bounty)
		
	*64 - (from non galcop stations) deliver software payload to main station that will disrupt the equipment page, preventing 
		any access for a period of 2 weeks.
			- you have to launch within 2 minutes of installing the software (and not redock again for 24 hours) otherwise GalCop 
				will be able to trace the upload and you'll get hit with a massive fine (10000cr, 200 bounty)

	*65 - get galcop system software 
		Player needs to do a multi-step process in order to get the software:
		1. go to destination (a multi-gov of lower system with TL > 7) and pick up customised escape pod from any non-GalCop station (mission screen will inform player of installation)
		2. return to system and use customised escape pod at more than 100000k from main station
		3. after redocking, launch and wait for the next police ship to launch
		4. follow police ship - it will self-destruct after 5-10 minutes, spewing an escape pod and a cargo item. 
		5. Scoop the cargo item and return it to the source station.

	66 - Perform scan of rock hermits, possibly while cloaked, return collected data
		- need to create scanning equipment item
		- harvest phase scanner code from Smugglers for general operating params

	67 - Perform scan of main station, possibly while cloaked, return collected data
			
	70-79 Solar activity system missions
	*70 - Use scan equipment to take close up measurements of sun
			get to fuel scoop dist = ((system.sun.collisionRadius * system.sun.collisionRadius) / system.sun.position.squaredDistanceTo(player.ship.position)) > 0.75;
			prime scanning device, activate
			await completion
			If player retreats before scan is complete, scan will stop, but will restart automatically when in range again (ie it won't start from the beginning)
	*71 - Recover data cache in low orbit over sun
			find data cache
			transmit security code
			await data package
	72 - Launch monitoring devices (special cargo or missiles)
			must deploy 2 special cargo items from within fuel scoop range, one on the planet side, the other on the far side of the sun.

	*73 - Variation on 70: Go to another system to pick up new equipment, then bring it back and scan the sun
	*74 - Take solar data to hi-tech system for analysis
	75 - return analysed solar data to original system?
	76 - Recover derelict ship near the sun (derelict will be given a high heat shield rating to prevent destruction)
			- Use DeepSpaceDredger for equipment/process of ship recovery?

	Charity missions
	*80/81/82/83 - Give credits to entity
	*84/85/86/87 - purchase and give commodity to entity (slaves for AI, any for the others)
	88 - purchase any commodity (not slaves, firearms or narcs) and give to AI
	89 - donation to nationalist entity of system
		- reward of better commodity prices?
			maybe just better prices on illegals?
		- reward of better equipment prices?
		- reward of better ship prices?
	? - donation to religious entity

	90-99 - war zone missions
	*90 - evacuate refugees
	*91 - transfer injured
	92 - import weapons

	100-109 Earthquake system missions
	*100 - Use scan equipment (seismic resonance scanner) to scan planet in 3-4 locations
		player must get in low enough orbit to perform scan successfully
		have some way of informing the player where the scan points are - waypoints, must be within 2km of waypoints
	*101 - transport injured people to hi-techlevel system for treatment 
	*102 - destroy a number of asteroids around the planet. The gravitational effect of the asteroids means instability on the surface
	103 - collect and deliver special equipment
	*104 - take seismic data to hi-techlevel system for data analysis 

	110-129 Satellite missions (when Satellites OXP is installed)
	110 - Extract data
	111 - Install new firmware
	112 - Destroy satellite
	113 - Hack satellite
	114 - insert data

	130-139 - Investigate unknown signal missions

	150-159 - defend missions
		*150 - defend anaconda from pirates
		151 - defend liner from pirates (need liners OXP installed)
		*152 - defend main station from pirates
		153 - defend other galcop station from pirates (OXP required)
		*154 - defend main station from thargoids
		155 - defend other station from thargoids (OXP's required)
		*156 - defend witchpoint beacon from thargoids

	160 Scan planet from low orbit
		requires to maintain a low orbit for a certain amount of time, with some possibility of attack
*/

this._debug = false; // flag to control whether debugging log messages will appear
this._rsnInstalled = false; // indicates whether randomshipnames OXP is installed
this._igtInstalled = false; // indicates whether Illegal Goods Tweak is installed
this._emailInstalled = false; // indicates whether the email system is installed
this._equipmentFromFailedMissions = []; // array of equipment/data items from failed missions. 
this._lastMission = []; // records last time a mission was accepted in a particular system
this._setData = []; // hold value for populator routine
this._missionHistory = []; // history of what missions have been completed at which systems
this._fromSystem = -1; // system player is leaving
this._toSystem = -1; // system player is heading to
this._initialTime = 0; // initial time when adding new missions after a witchspace jump (to factor in the player's transit time to the station);
this._transitTime = 1900; // transit time in seconds used to calculated expiry times
this._workTime = 3900; // amount of time in seconds to allocate to doing the job in a system
this._storedClientName = ""; // stored name, used to override client name when creating secondary missions
this._waypointList = []; // list of waypoints added to the system
this._positions = ["[gcm_position_0]", "[gcm_position_1]", "[gcm_position_2]", "[gcm_position_3]", "[gcm_position_4]", "[gcm_position_5]", "[gcm_position_6]", "[gcm_position_7]"];
this._positionReferences = ["[gcm_position_0_reference]", "", "", "[gcm_position_3_reference]", "[gcm_position_4_reference]", "[gcm_position_5_reference]", "[gcm_position_6_reference]", "[gcm_position_7_reference]"];
this._newMissionDelayTimer = null; // timer to give the new mission announcement a realistic time delay
this._requestSpecialCargo = ""; // text of commodity, indicating that the player is due to receive 1t of commodity upon docking at GalCop station
this._pendingMissionID = -1; // id of the current pending mission
this._pendingMissionCallback = null; // callback routine for the pending mission
this._pendingMissionOrigID = -1; // id of the original mission that generated the pending one
this._postScoopMissionID = -1; // holds mission ID of the original mission of an escape pod/blackbox
this._forceCargo = [];
this._slaveRescue = false; // flag indicating that a slave rescue mission is in play in the current system
this._slaveDemandHistory = []; // list of ships to whom the player has transmitted a demand to drop all slaves
this._availableMissionTypes = []; // array of all mission type ID's that are currently available for use
this._interstellarMissionTypes = []; // array of mission type ID's that will send a player into interstellar space
this._cargoMissionTypes = []; // array of mission type ID's that relate to cargo transfer
this._multiStageMissionTypes = []; // array of mission type ID's that have a multi-stage process
this._preferredTargetShips = []; // list of trader ships with max speed > 150 to be used when creating target ships
this._maxCargoOfShips = 0; // highest possible amount of cargo in list of preferred ship list
this._initTimer = null; // timer used to create missions after startup
this._thargoidAlloys = false; // flag indicating that a thargoid alloy collection mission is in play
this._thargoidPosition = []; // array of positions where thargoid warships died
this._generateMissionTimer = null; // timer to run when docked to add/remove missions
this._singleMissionOnly = false; // flag to indicate just one mission should be generated
this._generateMissionFrequency = 900; // number of seconds between cycles of adding new and removing old missions
this._preferredCargoPods = []; // array of cargo containers that don't have scripted cargo (ie that can be used safely)
this._emailMissionAdded = false; // flag to indicate that an email-type mission has already been created
this._commodityUnit = {};
this._trueValues = ["yes", "1", 1, "true", true];
this._simulator = false;
this._loopPoint1 = 0;
this._createTimer = null;
this._createDelay = 0.5;
this._forceCreate = [];
this._removeTimers = [];

// there is only one equipment item translation here, but in case we need to add some extras later
this._equipmentProvidingLookup = {
	"EQ_CARGO_SCOOPS": "EQ_FUEL_SCOOPS",
};

// if the player gets special cargo, the related task can be set up here
this._specialCargoTask = {};

// configuration settings for use in Lib_Config
this._gcmConfig = {
	Name: this.name,
	Alias: expandDescription("[gcm_config_alias]"),
	Display: expandDescription("[gcm_config_display]"),
	Alive: "_gcmConfig",
	Bool: {
		B0: {
			Name: "_debug",
			Def: false,
			Desc: expandDescription("[gcm_config_debug]")
		},
		Info: expandDescription("[gcm_config_bool_info]")
	},
};

//=============================================================================================================
// event handlers
//-------------------------------------------------------------------------------------------------------------
this.startUp = function () {
	// load any stored data
	if (missionVariables.GalCopBBMissions_EquipmentFailedMissions) {
		this._equipmentFromFailedMissions = JSON.parse(missionVariables.GalCopBBMissions_EquipmentFailedMissions);
		delete missionVariables.GalCopBBMissions_EquipmentFailedMissions;
	}
	if (missionVariables.GalCopBBMissions_Last) {
		this._lastMission = JSON.parse(missionVariables.GalCopBBMissions_Last);
		delete missionVariables.GalCopBBMissions_Last;
	}
	if (missionVariables.GalCopBBMissions_History) {
		this._missionHistory = JSON.parse(missionVariables.GalCopBBMissions_History);
		delete missionVariables.GalCopBBMissions_History;
	}

	worldScripts["oolite-libPriorityAI"].PriorityAIController.prototype.pirateHasLurkPosition = function () {
		if (this.getParameter("oolite_pirateLurk")) {
			return true;
		} else {
			return false;
		}
	}

	// make sure the BB data is loaded.
	if (missionVariables.BBData) {
		worldScripts.BulletinBoardSystem.startUp();
	}
	if (worldScripts.BulletinBoardSystem._data && worldScripts.BulletinBoardSystem._data.length > 0) {
		this.$dataFix();
	}
}

//-------------------------------------------------------------------------------------------------------------
this.startUpComplete = function () {
	// set a flag if random ship names is installed
	if (worldScripts["randomshipnames"]) this._rsnInstalled = true;
	// set a flag if the illegal goods tweak is installed
	if (worldScripts["illegal_goods_tweak"]) this._igtInstalled = true;
	// set a flag if the email system is installed
	if (worldScripts["EmailSystem"]) this._emailInstalled = true;

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

	if (worldScripts.AutoPrimeEquipment) {
		// sync up autoprime equip so selecting our MFD automatically primes broadcast comms
		worldScripts.AutoPrimeEquipment.$addConfig("GalCopBB_Missions_MFD", "EQ_BROADCASTCOMMSMFD");
	}

	this._lastSource = system.ID;

	// grab a copy of the unit (t, kg, g) for each commodity, for easy retrieval later
	var unittypes = ["t", "kg", "g"];
	for (var c in system.mainStation.market) {
		this._commodityUnit[c] = unittypes[parseInt(system.mainStation.market[c].quantity_unit)];
	}
	//this.$removeAllMissions(); // so we can regenerate everything (testing only)

	// set up a timer to create new missions after startup
	// this is because ShipConfiguration could adjust the cargo space available after doing the setup, which would create some very strange results
	// when a mission will allocate more cargo than the player actually has
	this._initTimer = new Timer(this, this.$initMissions, 1, 0);

	this.$checkForSlaveMission(system.ID);
	if (this._slaveRescue === true) this.$addSlaveCommsMessage();

	// add out custom AI scripts into SDC's exceptions list
	var sdc = worldScripts.StationDockControl;
	if (sdc && sdc._AIScriptExceptions) {
		sdc._AIScriptExceptions["GCM Assassins AI"] = "gcm-assassinAI.js";
		sdc._AIScriptExceptions["GCM Pirate AI"] = "gcm-pirateAI.js";
		sdc._AIScriptExceptions["GCM Scavenger AI"] = "gcm-scavengerAI.js";
		sdc._AIScriptExceptions["GCM Guard AI"] = "gcm-guardAI.js";
	}

}

//-------------------------------------------------------------------------------------------------------------
this.$removeAllMissions = function () {
	var bb = worldScripts.BulletinBoardSystem;
	var list = this.$getListOfMissions(false);
	for (var i = 0; i < list.length; i++) {
		bb.$removeBBMission(list[i].ID);
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$dataFix = function () {
	var bb = worldScripts.BulletinBoardSystem;
	for (var i = 0; i < bb._data.length; i++) {
		var item = bb._data[i];
		if (item.data && item.data.hasOwnProperty("position")) {
			item.data["locationType"] = item.data.position;
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.playerWillSaveGame = function () {
	// clean up any dregs that might have been left from secondary mission creation
	if (missionVariables.stolenItemType) delete missionVariables.stolenItemType;
	if (missionVariables.targetShipKey) delete missionVariables.targetShipKey;
	if (missionVariables.targetShipName) delete missionVariables.targetShipName;

	// save any data we currently have
	if (this._equipmentFromFailedMissions.length > 0) missionVariables.GalCopBBMissions_EquipmentFailedMissions = JSON.stringify(this._equipmentFromFailedMissions);
	if (this._lastMission.length > 0) missionVariables.GalCopBBMissions_Last = JSON.stringify(this._lastMission);
	if (this._missionHistory.length > 0) missionVariables.GalCopBBMissions_History = JSON.stringify(this._missionHistory);
}

//-------------------------------------------------------------------------------------------------------------
this.shipWillEnterWitchspace = function (cause, destination) {
	this._thargoidPosition.length = 0;
	if (cause !== "galactic jump") {
		this._fromSystem = system.ID;
		if (destination === -1) {
			this._toSystem = this.$playerTargetSystem();
		} else {
			this._toSystem = destination;
		}
	}
	// terminate all active missions
	if (cause === "galactic jump") {
		var list = this.$getListOfMissions(true);
		for (var i = list.length - 1; i >= 0; i--) {
			this.$terminateMission(list[i].ID);
		}
	}
	this.$stopTimers();
	this._waypointList.length = 0;
	this._slaveDemandHistory.length = 0;

	// check for slave rescue mission
	if (cause !== "galactic jump") {
		this.$checkForSlaveMission(destination);
		if (this._slaveRescue === true) {
			this.$addSlaveCommsMessage();
		} else {
			this.$removeSlaveCommsMessage();
		}
	} else {
		this._slaveRescue = false;
		this.$removeSlaveCommsMessage();
	}
}

//-------------------------------------------------------------------------------------------------------------
this.shipWillExitWitchspace = function () {
	if (system.ID !== -1) this._lastSource = system.ID;
	this._initialTime = 1800;
	this._loopPoint1 = -1;
	// shuffle order so it isn't always the same variant being checked first
	this._availableMissionTypes.sort(function (a, b) {
		return Math.random() - 0.5;
	});
	this._createTimer = new Timer(this, this.$addLocalMissions, this._createDelay, 0);
	this._initialTime = 0;
}

//-------------------------------------------------------------------------------------------------------------
this.playerEnteredNewGalaxy = function (galaxyNumber) {
	this._lastMission.length = 0;
	// reset the source ID for any equipment hanging around from failed missionScreenEnded
	// this will mean the player can sell it with impunity!
	for (var i = 0; i < this._equipmentFromFailedMissions.length; i++) {
		this._equipmentFromFailedMissions[i].source = -1;
	}
}

//-------------------------------------------------------------------------------------------------------------
this.systemWillPopulate = function () {
	/*
		Programming note: Some data values need to be passed into the populator routines. If those values are 
		static for this system, we can just use "this._my_variable". 
		However, if some values are different for each mission that will be generated in this system, we need 
		a way of passing data into the populator callback that will be called at some point in the future 
		(close in the future ie < 1 sec, but not *immediately*).
		So, we are are using the "this._setData" array, coupled with the "this.$getMissionData" function.
		The way it works is, as mission objects are set up with their own populator callbacks, data is pushed 
		onto the array.
		Then, when the populator actually runs, it calls the function to get the top-most entry from the array 
		for that mission type and removes it.
		That data should be the correct data for the current mission type populator. Because the data is also 
		removed, it means the next mission, if it happens to be of the same type, will again get the topmost 
		entry which should be its data.
	*/
	// force the asteroids population script to run before this one, if it hasn't already
	var ast = worldScripts.GalCopBB_AsteroidFields;
	if (ast._populateComplete === false) {
		ast.systemWillPopulate();
	}

	// make sure we haven't orphaned any active mission records
	this.$validateActiveData();

	// reset the mission populator data array
	this._setData.length = 0;
	this._forceCargo.length = 0;
	this.$getPreferredShipList();
	this.$getPreferredCargoPodList();
	this._thargoidAlloys = false;
	this._thargoidAlloysMissionID = 0;
	var ignoreCargo = [6, 7, 13, 17]; // mission types we don't want to force cargo for

	var list = this.$getListOfMissions(true);

	if (list.length > 0) {
		// loop through all active missions and see if any need to be set up for this system
		for (var i = 0; i < list.length; i++) {
			// if we're looking for any specific commodity in this system, then make sure some ships will have some
			if (list[i].data.commodity !== "" &&
				ignoreCargo.indexOf(list[i].data.missionType) === -1 && // not applicable to Thargoid alloy collections
				list[i].destination === system.ID &&
				list[i].expiry > clock.adjustedSeconds) {
				this._forceCargo.push(list[i].data.commodity);
			}

			// check if any assassins need to be created
			if (Number(list[i].data.assassinChance) > 0 &&
				Math.random() < Number(list[i].data.assassinChance) &&
				list[i].expiry > clock.adjustedSeconds &&
				worldScripts.GalCopBB_DiseaseOutbreak.$systemHasDiseaseOutbreak(system.ID) === false) {
				// create a groups of assassins at the witchpoint
				// how many assassin groups?
				var agc = 1;
				if (Number(list[i].data.assassinChance) >= 0.4 && Number(list[i].data.assassinChance) <= 0.7) {
					if (Math.random() < list[i].data.assassinChance) agc += 1;
				}
				if (Number(list[i].data.assassinChance) > 0.7) {
					if (Math.random() < Number(list[i].data.assassinChance)) agc += 1;
					if (Math.random() < Number(list[i].data.assassinChance)) agc += 1;
				}

				if (this._debug) log(this.name, "adding " + agc + " gcm assassin group(s)");

				system.setPopulator("gcm-assassins-" + list[i].ID, {
					priority: 90,
					location: "WITCHPOINT",
					groupCount: agc,
					callback: this.$addGCMAssassin.bind(this)
				});
			}

			// *** type 6/7 - recover cargo cache
			if ((list[i].data.missionType === 6 || list[i].data.missionType === 7) &&
				list[i].destination === system.ID &&
				list[i].data.quantity < (list[i].data.targetQuantity - list[i].data.destroyedQuantity) &&
				list[i].expiry > clock.adjustedSeconds) {

				//var asterField = ast.$getSystemAsteroidField();
				var position = ast.$getSystemAsteroidField(list[i].data.locationType);
				//var position = this.$getRandomPosition(list[i].data.position, 0.1, list[i].ID).position;

				this._setData.push({
					missionType: 607,
					missionID: list[i].ID,
					source: list[i].source,
					goons: 0,
					quantity: (list[i].data.targetQuantity - list[i].data.destroyedQuantity) - list[i].data.quantity,
					target: list[i].data.targetQuantity
				});

				// add the cargo canisters
				system.setPopulator("gcm-cargo-" + list[i].ID, {
					callback: function (pos) {
						var missData = worldScripts.GalCopBB_Missions.$getMissionData(607);

						// add an asteroid field
						//var as = system.addShips("asteroid", 60, pos, 40E3);

						// only create the ones that haven't been collected
						var pd = this._preferredCargoPods[Math.floor(Math.random() * this._preferredCargoPods.length)];
						var cg = system.addShips("[" + pd + "]", missData.quantity, pos, 10000);
						// if we couldn't create them with our preferred pod type, use the default
						if (!cg) {
							cg = system.addShips("[barrel]", missData.quantity, pos, 10000);
						}
						if (cg && cg.length === missData.quantity) {
							var bb = worldScripts.BulletinBoardSystem;
							var item = bb.$getItem(missData.missionID);
							for (var j = 0; j < cg.length; j++) {
								cg[j].switchAI("oolite-nullAI.js"); // dumbAI.plist
								cg[j].setScript("oolite-default-ship-script.js");

								var cmdty = item.data.commodity;
								// make a note of how much is put into each cargo pod, so we can validate this at completion
								if (system.mainStation.market[item.data.commodity].quantity_unit == "0") {
									var qty = 1;
								} else {
									var qty = Math.floor(system.scrambledPseudoRandomNumber(missData.missionID * j) * 10) + 1;
								}
								item.data.expected += qty;

								cg[j].setCargo(cmdty, qty);
								cg[j].script._missionID = missData.missionID;
								cg[j].script._gcmSpecial = true;

								// monkey patch if necessary
								// add our shipDied event to the cargo
								if (cg[j].script.shipDied) cg[j].script.$gcm_hold_shipDied = cg[j].script.shipDied;
								cg[j].script.shipDied = this.$gcm_cargo_shipDied;

								cg[j].primaryRole = "special_cargo";
								cg[j].name = expandDescription("[gcm_cargo_container]");
							}
							// add our cargo pods to the cargo monitor
							worldScripts.GalCopBB_CargoMonitor.$addMonitor(missData.missionID, item.data.commodity, system.ID, false, cg);
						} else {
							log(this.name, "!!ERROR: Cargo not spawned!");
						}

						// spawn some alloy wreckage as well
						system.addShips("scarred-alloy", (Math.floor(Math.random() * 5) + 2), pos, 5000);

					}.bind(this),
					location: "COORDINATES",
					coordinates: position
				});

				if (this._debug) this.$setWaypoint(position, [0, 0, 0, 0], "D", "Debug position (6)", "6");
			}

			// *** type 13 - alloys/wreckage from thargoid ships
			if (list[i].data.missionType === 13 &&
				list[i].data.quantity < list[i].data.targetQuantity) {
				//log(this.name, "turning on thargoid alloy monitoring flag");
				// tell the shipSpawned event to start monitoring for thargoids
				this._thargoidAlloys = true;
				this._thargoidAlloysMissionID = list[i].ID;
			}
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.interstellarSpaceWillPopulate = function () {
	this._thargoidAlloys = false;
	this._thargoidAlloysMissionID = 0;
	var list = this.$getListOfMissions(true, 13);
	if (list.length > 0) {
		// loop through all active missions and see if any need to be set up for this system
		for (var i = 0; i < list.length; i++) {
			// *** type 13 - alloys/wreckage from thargoid ships
			if (list[i].data.missionType === 13 &&
				list[i].data.quantity < list[i].data.targetQuantity) {
				//log(this.name, "turning on thargoid alloy monitoring flag");
				// tell the shipSpawned event to start monitoring for thargoids
				this._thargoidAlloys = true;
				this._thargoidAlloysMissionID = list[i].ID;
			}
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
/*this.systemWillRepopulate = function() {
	function gcm_findstricken(entity) {
		return (entity.primaryRole === "gcm_stricken_ship");
	}
}*/

//-------------------------------------------------------------------------------------------------------------
this.shipKilledOther = function (whom, damageType) {

	var bb = worldScripts.BulletinBoardSystem;
	// mission type 1 - asteroid hunt
	if (whom.hasRole("asteroid")) {
		var list = this.$getListOfMissions(true, 1);
		for (var i = 0; i < list.length; i++) {
			if (system.ID === list[i].destination &&
				list[i].expiry > clock.adjustedSeconds &&
				list[i].data.quantity < list[i].data.targetQuantity) {

				list[i].data.quantity += 1;
				bb.$updateBBMissionPercentage(list[i].ID, (list[i].data.quantity / list[i].data.targetQuantity));

				this.$logMissionData(list[i].ID);
				player.consoleMessage(expandDescription("[goal_updated]"));
				//break;
			}
		}
	}

	// mission type 2/18 - pirate hunt
	if (whom.bounty >= 0 && (Ship.roleIsInCategory(whom.primaryRole, "oolite-pirate") || (whom.AI && whom.AI.toLowerCase().indexOf("pirate") >= 0) || (whom.AIScript && whom.AIScript.name.toLowerCase().indexOf("pirate") >= 0))) {
		var list = this.$getListOfMissions(true, [2, 18]);
		for (var i = 0; i < list.length; i++) {
			if (system.ID === list[i].destination &&
				list[i].expiry > clock.adjustedSeconds &&
				list[i].data.quantity < list[i].data.targetQuantity) {

				list[i].data.quantity += 1;
				bb.$updateBBMissionPercentage(list[i].ID, (list[i].data.quantity / list[i].data.targetQuantity));

				this.$logMissionData(list[i].ID);
				player.consoleMessage(expandDescription("[goal_updated]"));
				//break;
			}
		}
	}

	// mission type 3/4 - trader hunt
	if (whom.primaryRole && whom.primaryRole.indexOf("trader") >= 0) {
		var list = this.$getListOfMissions(true, [3, 4]);
		for (var i = 0; i < list.length; i++) {
			if (system.ID === list[i].destination &&
				list[i].expiry > clock.adjustedSeconds &&
				list[i].data.quantity < list[i].data.targetQuantity) {

				list[i].data.quantity += 1;
				bb.$updateBBMissionPercentage(list[i].ID, (list[i].data.quantity / list[i].data.targetQuantity));

				this.$logMissionData(list[i].ID);
				player.consoleMessage(expandDescription("[goal_updated]"));
				//break; removed to allow player to have a type 3 and a type 4 active and updated at the same time.
			}
		}
	}

	// mission type 5 - thargoid hunt
	if (whom.isThargoid === true && whom.hasRole("tharglet") === false && whom.hasRole("thargon") === false && whom.hasRole("EQ_THARGON") === false) {
		var list = this.$getListOfMissions(true, 5);
		for (var i = 0; i < list.length; i++) {
			if (list[i].expiry > clock.adjustedSeconds &&
				system.ID === -1 &&
				((this._fromSystem === list[i].destination || this._fromSystem === list[i].source) &&
					(this._toSystem === list[i].destination || this._toSystem === list[i].source)) &&
				list[i].data.quantity < list[i].data.targetQuantity) {

				list[i].data.quantity += 1;
				bb.$updateBBMissionPercentage(list[i].ID, (list[i].data.quantity / list[i].data.targetQuantity));

				this.$logMissionData(list[i].ID);
				player.consoleMessage(expandDescription("[goal_updated]"));
				//break;
			}
		}
	}

	// mission type 16 - police hunt/civil unrest
	if (whom.hasRole("police")) {
		var list = this.$getListOfMissions(true, 16);
		for (var i = 0; i < list.length; i++) {
			if (list[i].expiry > clock.adjustedSeconds &&
				list[i].destination === system.ID &&
				list[i].data.quantity < list[i].data.targetQuantity) {

				list[i].data.quantity += 1;
				bb.$updateBBMissionPercentage(list[i].ID, (list[i].data.quantity / list[i].data.targetQuantity));

				this.$logMissionData(list[i].ID);
				player.consoleMessage(expandDescription("[goal_updated]"));
			}
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.shipScoopedOther = function (whom) {
	if (whom && whom.isCargo && whom.primaryRole !== "gcm_blackbox" && whom.primaryRole !== "escape-capsule" && whom.hasRole("gcm_special_pod") === false) {
		if (whom.script) {
			if (whom.script._specialisedComputers && whom.script._specialisedComputers === 1) {
				delete whom.script._specialisedComputers;
				delete whom.script._missionID;
				return;
			}
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.shipDumpedCargo = function (cargo) {
	if (!cargo.script) return;
	cargo.script._fromPlayer = true;

	if (cargo.commodity === "radioactives") {
		// check to see if a garbage scow mission is active
		var list = this.$getListOfMissions(true, 17);
		for (var i = 0; i < list.length; i++) {
			if (list[i].data.quantity < (list[i].data.targetQuantity - list[i].data.destroyedQuantity)) {
				// attach our custom scripts
				if (cargo.script.shipDied && !cargo.script.$gcm_hold_shipDied) cargo.script.$gcm_hold_shipDied = cargo.script.shipDied;
				cargo.script.shipDied = this.$gcm_garbage_shipDied;
				cargo.script._missionID = list[i].ID;
				break;
			}
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.shipSpawned = function (ship) {
	/*var p = player.ship;
	// did the player just dump cargo?
	if (p && ship && ship.isCargo && ship.position && p.position && p.position.distanceTo(ship) < 300) {
		//gotcha - flag this canister so we can check it later if the player scoops it
		ship.script._fromPlayer = true;
	}*/

	// check for a thargoid alloy mission
	if (this._thargoidAlloys === true) {
		/*if (ship.hasRole("alloy") && ship.name === "Metal fragment") {
			log(this.name, "alloy spawned");
			log(this.name, "missionID = " + ship.script._missionID);
			log(this.name, "nearDeadThargoid = " + this.$shipNearDeadThargoid(ship));
		}*/
		if (ship.hasRole("alloy") && ship.name.toLowerCase() === expandDescription("[gcm_metal_fragment]") && !ship.script._missionID && this._thargoidPosition.length > 0 && this.$shipNearDeadThargoid(ship) === true) {
			//var list = this.$getListOfMissions(true, 13);
			//for (var i = 0; i < list.length; i++) {
			//	if (list[i].data.quantity < list[i].data.targetQuantity) {
			ship.displayName = expandDescription("[gcm_thargoid]") + " " + ship.displayName;
			ship.script._fromThargoid = true;
			ship.script._missionID = this._thargoidAlloysMissionID;
			//	}
			//}
		}
		if (ship.isThargoid === true && ship.hasRole("tharglet") === false && ship.hasRole("thargon") === false && ship.hasRole("EQ_THARGON") === false) {
			//var list = this.$getListOfMissions(true, 13);
			//for (var i = 0; i < list.length; i++) {
			if (ship.script.shipDied) ship.script.$gcm_hold_shipDied = ship.script.shipDied;
			ship.script.shipDied = this.$gcm_monitorThargoid_shipDied;
			ship.script._missionID = this._thargoidAlloysMissionID;
			//}
		}
	}

	// check for a slave rescue mission, and make sure there are some ships with slaves to rescue
	// check for any cargo that needs to be present for the player to pirate/liberate
	if (!system.isInterstellarSpace && this._forceCargo.length > 0) {
		var fc = this._forceCargo;
		for (var i = 0; i < fc.length; i++) {
			var checkShip = false;
			if (system.mainStation.market[fc[i]].legality_import > 0 || system.mainStation.market[fc[i]].legality_export > 0) {
				// for illegal goods we'll add cargo to pirate group leaders
				// possibly smugglers or even normal traders
				if (Ship.roleIsInCategory(ship.primaryRole, "oolite-pirate-leader") || (Math.random() > 0.4 && ship.primaryRole === "trader-smuggler") || (Math.random() > 0.8 && Ship.roleIsInCategory(ship.primaryRole, "oolite-trader")))
					checkShip = true;
			} else {
				// for other cargo types, we'll just add it to traders
				if (Ship.roleIsInCategory(ship.primaryRole, "oolite-trader") && Math.random() > 0.5) checkShip = true;
			}
			if (checkShip === true) {
				// does this ship have any cargo space left
				if (ship.cargoSpaceAvailable > 0) {
					// does this ship have any of this cargo on board already?
					var haveCargo = false;
					for (var j = 0; j < ship.cargoList.length; j++) {
						var itm = ship.cargoList[j];
						if (itm.commodity === fc[i] && itm.quantity > 0) {
							haveCargo = true;
							break;
						}
					}
					// add some cargo
					if (haveCargo === false && Math.random() > 0.7) {
						var max = 3;
						if (ship.cargoSpaceAvailable < 3) max = ship.cargoSpaceAvailable;
						var adjamt = parseInt(Math.floor(Math.random() * max) + 1);
						if (this._debug) log(this.name, "adding " + adjamt + "t " + fc[i] + " to " + ship);
						ship.adjustCargo(fc[i], adjamt);
					}
				}
			}
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.shipDied = function (whom, why) {
	this.$stopTimers();
}

//-------------------------------------------------------------------------------------------------------------
this.shipWillDockWithStation = function (station) {
	if (this._simulator === true) return;
	// if we dock at a station with any pods or stricken ships waiting for us, the quickest time to undock again is 10 minutes, so they will all die
	if (this._lifeSupportTimer && this._lifeSupportTimer.isRunning) {
		// end life support on any entities in-system at the moment
		for (var i = 0; i < this._lifeSupportRemaining.length; i++) {
			this._lifeSupportRemaining[i].remaining = 1;
		}
	}
	// give the player the special machinery if they've docked
	if (this._requestSpecialCargo !== "" && station.allegiance === "galcop") {
		if (player.ship.cargoSpaceAvailable === 0 && player.ship.cargoSpaceCapacity >= 1) this.$freeCargoSpace(1, this._requestSpecialCargo);
		player.ship.manifest[this._requestSpecialCargo] += 1;
		var text = expandDescription("[gcm_give_player_special_cargo]", {
			commodity: displayNameForCommodity(this._requestSpecialCargo).toLowerCase(),
			task: this._specialCargoTask[this._requestSpecialCargo]
		});
		player.addMessageToArrivalReport(text);
		this._requestSpecialCargo = "";
	}
	this._generateMissionTimer = new Timer(this, this.$generateMissions, this._generateMissionFrequency, this._generateMissionFrequency);
}

//-------------------------------------------------------------------------------------------------------------
this.shipDockedWithStation = function (station) {
	if (this._simulator === true) this._simulator = false;
}

//-------------------------------------------------------------------------------------------------------------
this.shipWillLaunchFromStation = function (station) {
	this._emailMissionAdded = false;
	if (system.ID != -1) this._lastSource = system.ID;
	if (this._generateMissionTimer && this._generateMissionTimer.isRunning) this._generateMissionTimer.stop();
}

//-------------------------------------------------------------------------------------------------------------
this.shipLaunchedFromStation = function (station) {
	if (this.$simulatorRunning() === true) this._simulator = true;
}

//-------------------------------------------------------------------------------------------------------------
this.playerSoldCargo = function (commodity, units, price) {
	// check if the player just sold the special computers
	if (commodity === "computers") this.$checkSpecialDeliveryRemoved();
}

//-------------------------------------------------------------------------------------------------------------
this.shipTakingDamage = function (dmg_amount, whom, type) {
	// check if the player just lost special computers through damage
	if (dmg_amount > 0) {
		this.$checkSpecialDeliveryRemoved();
	}
}

//-------------------------------------------------------------------------------------------------------------
this.playerBoughtNewShip = function (ship, price) {
	// check if the player just lost the special computers
	this.$checkSpecialDeliveryRemoved();
}

//-------------------------------------------------------------------------------------------------------------
this.$initMissions = function $initMissions() {
	// if we don't have any available missions, try to add some now.
	var list = this.$getListOfMissions();
	if (list.length === 0) {
		this._loopPoint1 = -1;
		this._availableMissionTypes.sort(function (a, b) {
			return Math.random() - 0.5;
		}); // shuffle order so it isn't always the same variant being checked first
		this._createTimer = new Timer(this, this.$addLocalMissions, this._createDelay, 0);
	}
	delete this._initTimer;
}

//-------------------------------------------------------------------------------------------------------------
// gets mission specific data for the populator routines
// this works on a first in/first out basis - if there are multiple missions of the same type being populated, the mission specific data would
// get pushed in to the setData array in order, and then this routine pulls that data out in the same order
// that's the theory, anyway!
this.$getMissionData = function $getMissionData(missionType) {
	for (var i = 0; i < this._setData.length; i++) {
		if (this._setData[i].missionType === missionType) {
			var result = {
				missionID: this._setData[i].missionID,
				trueMissionType: (this._setData[i].trueMissionType ? this._setData[i].trueMissionType : missionType),
				source: this._setData[i].source,
				goons: this._setData[i].goons,
				quantity: this._setData[i].quantity,
				target: this._setData[i].target
			};
			this._setData.splice(i, 1);
			return result;
		}
	}
	return null;
}

//-------------------------------------------------------------------------------------------------------------
// stop all timers
this.$stopTimers = function $stopTimers() {
	if (this._newMissionDelayTimer && this._newMissionDelayTimer.isRunning) this._newMissionDelayTimer.stop();
	delete this._newMissionDelayTimer;
	if (this._generateMissionTimer && this._generateMissionTimer.isRunning) this._generateMissionTimer.stop();
	delete this._generateMissionTimer;
	if (this._createTimer && this._createTimer.isRunning) this._createTimer.stop();
	delete this._createTimer;
}

//-------------------------------------------------------------------------------------------------------------
this.$setWaypoint = function $setWaypoint(position, orientation, code, lbl, debugCode) {
	system.setWaypoint(
		this.name + (debugCode !== "" ? "_" + debugCode : ""), position, orientation, {
		size: 50,
		beaconCode: code,
		beaconLabel: lbl
	}
	);
	this._waypointList.push(this.name + (debugCode !== "" ? "_" + debugCode : ""));
}

//-------------------------------------------------------------------------------------------------------------
this.$unsetWaypoint = function $unsetWaypoint(debugCode) {
	system.setWaypoint(
		this.name + (debugCode !== "" ? "_" + debugCode : "")
	);
	var idx = this._waypointList.indexOf(this.name + (debugCode !== "" ? "_" + debugCode : ""));
	this._waypointList.splice(idx, 1);
}

//=============================================================================================================
// BB interface handlers
//-------------------------------------------------------------------------------------------------------------

//-------------------------------------------------------------------------------------------------------------
// moves the mission onto the active list, performs any mission specific controls
this.$acceptedMission = function $acceptedMission(missID) {
	function compare(a, b) {
		return ((a.price > b.price) ? 1 : -1);
	}

	if (this._debug) log(this.name, "accepted mission id = " + missID);

	var bb = worldScripts.BulletinBoardSystem;
	var item = bb.$getItem(missID);
	if (!item) {
		log(this.name, "!!ERROR: BB returned null value from $getItem on mission ID " + missID);
		return;
	}
	this.$updateLastMissionDate(item.source, item.data.missionType);
	this.$updateGeneralSettings(item);

	if (item.data.missionType === 6 | item.data.missionType === 7) {
		var ast = worldScripts.GalCopBB_AsteroidFields;
		// check to see if we've previously added an asteroid field in the destination
		if (!ast.$systemHasAsteroidField(item.destination, item.data.locationType)) {
			// if not, tell the asteroid field generator to create one
			ast.$addAsteroidField(item.destination, 0.1, item.ID, item.data.locationType);
		}
	}

	if (this._interstellarMissionTypes.indexOf(item.data.missionType) >= 0 && item.destination >= 0) {
		if (!missionVariables.GalCopBB_Interstellar) {
			missionVariables.GalCopBB_Interstellar = "yes"
			var email = worldScripts.EmailSystem;
			email.$createEmail({
				sender: expandDescription("[gcm_galcop_flight_school]"),
				subject: expandDescription("[gcm_interstellar_tips]"),
				date: global.clock.adjustedSeconds + 10,
				sentFrom: system.ID,
				message: expandDescription("[interstellar_tips]"),
			});
		}
	}

	// turn on flag to monitor thargoid ship destruction
	if (item.data.missionType === 13) {
		this._thargoidAlloys = true;
		this._thargoidAlloysMissionID = item.ID;
	}

	if ([8, 9, 10, 11, 12, 13, 14, 15].indexOf(item.data.missionType) >= 0) {
		var cm = worldScripts.GalCopBB_CargoMonitor;
		// for types 8/9, because they can be any cargo, we'll add monitoring to all commodity types
		if (item.data.missionType === 8 || item.data.missionType === 9) {
			for (var i = 0; i < cm._commodityIDList.length; i++) {
				cm.$addMonitor(item.ID, cm._commodityIDList[i], item.destination, false)
			}
		} else {
			// even though the mission details for type 13's is for interstellar space, we actually don't
			// care where the wreckage is found - system space or interstellar. so long as it comes from a thargoid
			cm.$addMonitor(item.ID, item.data.commodity, (item.data.missionType === 13 ? 256 : item.destination), (item.data.missionType === 13 ? true : false));
		}
	}

	// give player cargo for special delivery mission, but only if they're not in space
	// if they get here from a secondary mission given in space, they'll need to find their own cargo
	if (item.data.missionType === 17) {
		if (player.ship.isInSpace === false) {
			for (var j = 0; j < item.data.targetQuantity; j++) {
				// free up space if required
				if (player.ship.cargoSpaceAvailable === 0 && player.ship.cargoSpaceCapacity >= item.data.targetQuantity) this.$freeCargoSpace(1, item.data.commodity);
				// add the commodity to the player's ship
				player.ship.manifest[item.data.commodity] += 1;
				// reduce the quantity in the station market, if possible
				if (player.ship.dockedStation.market[item.data.commodity].quantity > 0) {
					player.ship.dockedStation.setMarketQuantity(item.data.commodity, player.ship.dockedStation.market[item.data.commodity].quantity - 1);
				}
			}
		}
	}

}

//-------------------------------------------------------------------------------------------------------------
// processes the acceptance of a mission via an email
this.$acceptMissionFromOther = function $acceptMissionFromOther(details) {
	var cust = details.custom;
	var extra = "";
	switch (details.originator) {
		case "email":
			extra = expandDescription("[received_via_email]");
			break;
		case "ship":
			extra = expandDescription("[received_via_ship]");
			break;
	}

	// process any post status messages and prep the object 
	var postMsg = [];
	var rep = worldScripts.GalCopBB_Reputation;
	for (var m = 1; m <= 3; m++) {
		var extract = ""
		switch (m) {
			case 1:
				extract = rep.$transformText(expandDescription("[missionType" + details.missionType + "_initiatedMessage]"), details.sourceID, details.destinationID);
				break;
			case 2:
				extract = rep.$transformText(expandDescription("[missionType" + details.missionType + "_completedMessage]"), details.sourceID, details.destinationID);
				break;
			case 3:
				extract = rep.$transformText(expandDescription("[missionType" + details.missionType + "_terminatedMessage]"), details.sourceID, details.destinationID);
				break;
		}
		if (extract !== "") {
			var brk = extract.split("|");
			var txt = brk[0];
			var rt = "item";
			if (m > 1) rt = "list";
			var bk = "";
			var ov = "";
			var md = "";
			var mdp = 0;
			var mds = true;
			for (var k = 0; k < brk.length; k++) {
				if (brk[k].indexOf("return:") >= 0) rt = brk[k].split(":")[1];
				if (brk[k].indexOf("background:") >= 0) bk = brk[k].split(":")[1];
				if (brk[k].indexOf("overlay:") >= 0) ov = brk[k].split(":")[1];
				if (brk[k].indexOf("model:") >= 0) md = brk[k].split(":")[1];
				if (brk[k].indexOf("modelPersonality:") >= 0) mdp = parseInt(brk[k].split(":")[1]);
				if (brk[k].indexOf("spinModel:") >= 0) mds = brk[k].split(":")[1];
			}
			var pmItem = {};
			pmItem["status"] = (m === 1 ? "initiated" : (m === 2 ? "completed" : "terminated"));
			pmItem["return"] = rt;
			pmItem["text"] = txt;
			if (bk != null && bk !== "") pmItem["background"] = this.$getTexture(bk);
			if (ov != null && ov !== "") pmItem["overlay"] = this.$getTexture(ov);
			if (md !== "") pmItem["model"] = md;
			if (mdp !== 0) pmItem["modelPersonality"] = mdp;
			if (mds === false) pmItem["spinModel"] = mds;
			postMsg.push(pmItem);
		}
	}

	// ok, time to add the mission to the BB with the accepted flag set to true straight away
	var dtls = "";
	var cmdty_unit = this.$getCommodityUnit(details.commodity);
	if (expandDescription("[missionType" + details.missionType + "_phraseGen]") === "1") {
		var fn = expandDescription("[missionType" + details.missionType + "_detailFn]");
		dtls = rep.$transformText(expandDescription(worldScripts.GalCopBB_MissionDetails[fn](), {
			amount: details.amount,
			commodity: displayNameForCommodity(details.commodity).toLowerCase(),
			unit: cmdty_unit,
			target: details.target,
			system: details.system,
			destination: details.destination,
			expiry: details.expiry,
			position: details.location,
			name: details.name
		}), details.sourceID, details.destinationID);
	} else {
		dtls = rep.$transformText(expandDescription("[missionType" + details.missionType + "_details]", {
			amount: details.amount,
			commodity: displayNameForCommodity(details.commodity).toLowerCase(),
			unit: cmdty_unit,
			target: details.target,
			system: details.system,
			destination: details.destination,
			expiry: details.expiry,
			position: details.location,
			name: details.name
		}), details.sourceID, details.destinationID);
	}

	var bb = worldScripts.BulletinBoardSystem;
	var id = bb.$addBBMission({
		source: details.sourceID,
		destination: details.destinationID,
		stationKey: expandDescription("[missionType" + details.missionType + "_stationKeys]"),
		description: rep.$transformText(details.subject, details.sourceID, details.destinationID),
		details: dtls + extra,
		overlay: this.$getTexture(expandDescription("[missionType" + details.missionType + "_bbOverlay]")),
		payment: details.payment,
		penalty: details.penalty,
		deposit: details.deposit,
		allowPartialComplete: expandDescription("[missionType" + details.missionType + "_partialComplete]"),
		completionType: details.completionType,
		stopTimeAtComplete: details.stopTimeAtComplete,
		accepted: true,
		noEmails: true,
		expiry: details.expire,
		disablePercentDisplay: expandDescription("[missionType" + details.missionType + "_disablePercent]"),
		customDisplayItems: (cust.length !== 0 ? cust : ""),
		initiateCallback: "$acceptedMission",
		completedCallback: "$completedMission",
		confirmCompleteCallback: "$confirmCompleted",
		terminateCallback: "$terminateMission",
		failedCallback: "$failedMission",
		manifestCallback: "$updateManifestEntry",
		worldScript: expandDescription("[missionType" + details.missionType + "_mainWorldScript]"),
		postStatusMessages: postMsg,
		data: {
			source: this.name,
			missionType: details.missionType,
			locationType: details.locationType,
			targetQuantity: details.target,
			quantity: 0,
			destroyedQuantity: 0,
			commodity: details.commodityKey,
			missionChain: details.missionChain,
			chained: (details.missionChain === "" ? false : true),
			altManifest: false,
			name: details.name,
			assassinChance: details.assassinChance,
			stolenItemType: details.stolenItemType,
			targetShipKey: details.targetShipKey,
			targetShipName: (details.targetShipName ? details.targetShipName : ""),
			delivered: 0,
			expected: 0,
			origSystemID: details.sourceID,
			satelliteTypes: [],
			destinationA: details.destinationA
		}
	});

	// add this to the active missions list
	this.$acceptedMission(id);
}

//-------------------------------------------------------------------------------------------------------------
this.$terminateMission = function $terminateMission(missID) {
	var bb = worldScripts.BulletinBoardSystem;
	var item = bb.$getItem(missID);

	worldScripts.GalCopBB_CargoMonitor.$removeMonitor(missID);

	// adjust reputation only when the terminatePenalty flag is set to true 
	if (item.data.terminatePenalty === true) {
		// update mission history
		this.$updateFailedHistoryReputation(item);
	}

	//var sbm = worldScripts.Smugglers_BlackMarket;

	// remove any special equipment provided by the mission
}

//-------------------------------------------------------------------------------------------------------------
this.$failedMission = function $failedMission(missID) {
	if (!worldScripts.Smugglers_BlackMarket) {
		// if there's no black market option, just remove the equipment
		this.$terminateMission(missID);
		return;
	}

	var bb = worldScripts.BulletinBoardSystem;
	var item = bb.$getItem(missID);
	// update mission history
	this.$updateFailedHistoryReputation(item);
	var eq = "";

	worldScripts.GalCopBB_CargoMonitor.$removeMonitor(missID);

	if (eq != "") this._equipmentFromFailedMissions.push({
		missionType: item.data.missionType,
		source: item.source,
		equip: eq,
		date: clock.adjustedSeconds
	});
}

//-------------------------------------------------------------------------------------------------------------
this.$checkForFollowupMission = function $checkForFollowupMission(item) {
	if (item.percentComplete === 1 && this._emailInstalled === true && this._emailMissionAdded === false) {
		// work out if there might be a followup mission
		var check = expandDescription("[missionType" + item.data.missionType + "_postMissionChance]");
		if (check !== "") {
			var chance = parseFloat(check);
			check = expandDescription("[missionType" + item.data.missionType + "_postMissionTypes]");
			if (Math.random() <= chance && check !== "") {
				var chain = item.data.missionChain;
				var chainData = chain.split("|");
				var missionList = [];
				// OK, time to work out the next mission for the player
				var getList = check.split(",");
				for (var i = 0; i < getList.length; i++) {
					// don't add any mission types already in the chain
					if (chainData.indexOf(getList[i]) === -1) missionList.push(parseInt(getList[i]));
				}
				if (this._debug) log(this.name, "post mission list = " + missionList);
				if (missionList.length > 0) {
					this._singleMissionOnly = true;
					this._storedClientName = item.data.name;
					var id = this.$addLocalMissions(missionList, item.data.missionType, item.source, chain, true);
					this._storedClientName = "";
					this._singleMissionOnly = false;
					if (id === -2) this._emailMissionAdded = true;
				}
			}
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$completedMission = function $completedMission(missID) {
	var p = player.ship;
	var bb = worldScripts.BulletinBoardSystem;
	var cm = worldScripts.GalCopBB_CargoMonitor;
	var item = bb.$getItem(missID);
	//var payPlayerForCargo = false;

	this.$updateSuccessHistoryReputation(item);

	//var sbm = worldScripts.Smugglers_BlackMarket;

	// if the player is due a refund, pay it now.
	if (item.data.hasOwnProperty("refund") && item.data.refund > 0) {
		player.credits += item.data.refund;
		player.consoleMessage(expandDescription("[gcm_refund]", { amount: formatCredits(item.data.refund, false, true) }));
	}

	if ([6, 7, 8, 9, 10, 11, 12, 13, 14, 15].indexOf(item.data.missionType) >= 0) {
		var amt = cm.$countCargoForMission(missID);
		for (var i = 1; i <= amt; i++) {
			var cargo = cm.$removeCargoForMission(missID, 1);
			if (cargo.commodity != "") {
				//log(this.name, "removing from player ship");
				p.manifest[cargo.commodity] -= cargo.quantity;
			}
		}
	}

	// *** type 6/7 - recover cargo - remove cargo from hold and reward player 
	/*if (item.data.missionType === 6 || item.data.missionType === 7) {
		p.manifest[item.data.commodity] -= item.data.expected;
		//if (payPlayerForCargo === true) player.credits += (p.dockedStation.market[item.data.commodity].price / 10) * item.data.expected;
	}*/
	// *** type 8/9 - steal any cargo - remove cargo from hold and reward player
	// possible issue: if player scooped one commodity, but then sold it/lost it
	// should still be ok, as routine will check if player has enough of that cargo to remove
	/*if (item.data.missionType === 8 || item.data.missionType === 9) {
		if (item.data.cargoRecord && item.data.cargoRecord.length > 0) {
			for (var i = 0; i < item.data.cargoRecord.length; i++) {
				var c = item.data.cargoRecord[i];
				if (p.manifest[c.commodity] >= c.amount) {
					p.manifest[c.commodity] -= c.amount;
					if (payPlayerForCargo === true) player.credits += (p.dockedStation.market[c.commodity].price / 10) * c.amount;
				}
			}
		}
	}*/
	// *** type 10 - steal specific cargo - remove cargo from hold and reward player
	// *** type 11 - steal specific cargo - remove cargo from hold and reward player
	// *** type 13 - collect thargoid alloys - remove cargo from hold and reward player
	// *** type 14 - gun runner - remove cargo from hold and reward player
	// *** type 15 - drug dealer - remove cargo from hold and reward player
	/*if (item.data.missionType === 10 || item.data.missionType === 11 || item.data.missionType === 13 || item.data.missionType === 14 || item.data.missionType === 15) {
		p.manifest[item.data.commodity] -= item.data.quantity;
		//if (payPlayerForCargo === true) player.credits += (p.dockedStation.market[item.data.commodity].price / 10) * item.data.quantity;
	}*/
	// remove any special equipment provided by the mission

	cm.$removeMonitor(missID);
}

//-------------------------------------------------------------------------------------------------------------
// checks that a mission has been completed
this.$confirmCompleted = function $confirmCompleted(missID) {
	var p = player.ship;
	var m = p.manifest;
	var result = "";
	var bb = worldScripts.BulletinBoardSystem;
	var item = bb.$getItem(missID);
	if (item) {
		// *** type 6/7 - recover cargo
		if (item.data.missionType === 6 || item.data.missionType === 7) {
			// check the player has the correct amount of the commodity
			if (m[item.data.commodity] < item.data.expected) {
				var unit = this.$getCommodityUnit(item.data.commodity);
				result += (result === "" ? "" : "\n") + expandDescription("[gcm_insufficient_cargo]", { commodity: displayNameForCommodity(item.data.commodity).toLowerCase(), target: item.data.expected, unit: unit });
			}
		}
		// *** type 12 - free the slaves
		if (item.data.missionType === 12) {
			if (item.data.quantity < item.data.targetQuantity) {
				result += (result === "" ? "" : "\n") + expandDescription("[gcm_insufficient_slaves]", { target: (item.data.targetQuantity - item.data.quantity) });
			}
		}
	}
	return result;
}

//--------------------------------------------------------------------------------------------------------------
// endpoint for custom menu items
// handles case when player wants to hand in a partial amount of mission (cargo) but keep mission open so they can keep going
this.$partialComplete = function $partialComplete(missID) {
	var bb = worldScripts.BulletinBoardSystem;
	var item = bb.$getItem(missID);
	var cm = worldScripts.GalCopBB_CargoMonitor;
	var amt = cm.$countCargoForMission(missID);
	//log(this.name, "amt = " + amt);

	if (amt > 0 && (item.data.targetQuantity - item.data.quantity) >= 0) {
		if (amt > (item.data.targetQuantity - item.data.quantity)) amt = (item.data.targetQuantity - item.data.quantity);
		item.data.quantity += amt;
		for (var i = 1; i <= amt; i++) {
			var cargo = cm.$removeCargoForMission(missID, 1);
			if (cargo.commodity != "") {
				//log(this.name, "removing from player ship");
				player.ship.manifest[cargo.commodity] -= cargo.quantity;
			}
		}
		bb.$updateBBMissionPercentage(item.ID, ((item.data.quantity < item.data.targetQuantity ? item.data.quantity : item.data.targetQuantity) / item.data.targetQuantity));
	}
	// add a new custom menu (same as the old one which will be autoremoved)
	// but only if there is a possibility the player could do another partial complete
	if ((item.data.targetQuantity - item.data.quantity) >= 1) {
		var mnu = expandDescription("[missionType" + item.data.missionType + "_customMenu]");
		var itms = mnu.split("|");
		item.customMenuItems.push({
			text: itms[0],
			worldScript: itms[1],
			callback: itms[2],
			condition: itms[3],
			autoRemove: (this._trueValues.indexOf(itms[4]) >= 0 ? true : false)
		});
	}
}

//-------------------------------------------------------------------------------------------------------------
// checks if the partial load and continue menu can be selected by the player
this.$partialCondition = function $partialCondition(missID) {
	var bb = worldScripts.BulletinBoardSystem;
	var item = bb.$getItem(missID);
	var cm = worldScripts.GalCopBB_CargoMonitor;
	var amt = cm.$countCargoForMission(missID);
	var result = "";
	if (amt === 0) result = (item.data.missionType === 13 ? expandDescription("[gcm_no_cargo_type_13]") : (item.data.missionType === 8 || item.data.missionType === 9 ? expandDescription("[gcm_no_cargo_type_8_9]") : expandDescription("[gcm_no_cargo_generic]", { commodity: displayNameForCommodity(item.data.commodity).toLowerCase() })));
	if (item.completionType == "AT_SOURCE" && item.source != system.ID) result = expandDescription("[gcm_cargo_return]");
	return result;
}

//-------------------------------------------------------------------------------------------------------------
this.$updateManifestEntry = function $updateManifestEntry(missID) {
	var bb = worldScripts.BulletinBoardSystem;
	var item = bb.$getItem(missID);
	if (item) {
		var exp_text = bb.$getTimeRemaining(item.expiry);
		if (exp_text.toLowerCase().indexOf("expired") >= 0) exp_text = expandDescription("[gcm_manifest_no_time]");
		var rep = worldScripts.GalCopBB_Reputation;
		var cm = worldScripts.GalCopBB_CargoMonitor;

		var unit = this.$getCommodityUnit(item.data.commodity);
		// is this mission finished?
		if (item.data.quantity === (item.data.targetQuantity - item.data.destroyedQuantity) && item.data.targetQuantity > 0) {
			var altMan = "";
			// are use using an alternate manifest text entry
			if (item.data.altManifest === true) altMan = "_alt";

			// update the manifest
			bb.$updateBBManifestText(
				missID,
				rep.$transformText(expandDescription("[missionType" + item.data.missionType + altMan + "_finishedManifest]", {
					system: System.systemNameForID(item.source),
					expiry: exp_text
				}), item.source, item.destination)
			);
			// update the status text as well
			// if we're using the alternate manifest, check to see if there is alternate status
			// if not, just switch back to the normal manifest
			if (altMan === "_alt" && expandDescription("[missionType" + item.data.missionType + "_alt_finishedStatus]", {
				system: ""
			}) === "") altMan = "";
			bb.$updateBBStatusText(
				missID,
				rep.$transformText(expandDescription("[missionType" + item.data.missionType + altMan + "_finishedStatus]", {
					system: System.systemNameForID(item.source)
				}), item.source, item.destination)
			);
		} else {
			// so this is an incomplete mission
			// update the manifest text
			var text = rep.$transformText(expandDescription("[missionType" + item.data.missionType + "_manifest]", {
				quantity: item.data.quantity,
				target: item.data.targetQuantity - item.data.destroyedQuantity,
				remaining: (item.data.targetQuantity - item.data.destroyedQuantity) - item.data.quantity,
				hold: cm.$countCargoForMission(item.ID),
				commodity: displayNameForCommodity(item.data.commodity).toLowerCase(),
				unit: unit,
				destination: bb.$systemNameForID(item.destination),
				destinationA: (item.data.hasOwnProperty("destinationA") ? bb.$systemNameForID(item.data.destinationA) : ""),
				position: (item.data.hasOwnProperty("locationType") ? expandDescription("[gcm_position_" + item.data.locationType + "_short]") : ""),
				system: System.systemNameForID(item.source),
				expiry: exp_text,
				stage: (item.data.hasOwnProperty("stage") ? item.data.stage : "0")
			}), item.source, item.destination);
			// check to see if there's a multi-stage mission text to pick up
			if (text.indexOf("_stage_") >= 0) {
				text = rep.$transformText(expandDescription("[" + text + "]", {
					quantity: item.data.quantity,
					target: item.data.targetQuantity - item.data.destroyedQuantity,
					remaining: (item.data.targetQuantity - item.data.destroyedQuantity) - item.data.quantity,
					hold: cm.$countCargoForMission(item.ID),
					commodity: displayNameForCommodity(item.data.commodity).toLowerCase(),
					unit: unit,
					destination: bb.$systemNameForID(item.destination),
					destinationA: (item.data.hasOwnProperty("destinationA") ? bb.$systemNameForID(item.data.destinationA) : ""),
					position: (item.data.hasOwnProperty("locationType") ? expandDescription("[gcm_position_" + item.data.locationType + "_short]") : ""),
					system: System.systemNameForID(item.source),
					expiry: exp_text,
					stage: item.data.stage
				}), item.source, item.destination);
			}
			bb.$updateBBManifestText(item.ID, (missID === this._pendingMissionID ? expandDescription("[gcm_pending]") : "") + text);

			// update the status text as well
			text = rep.$transformText(expandDescription("[missionType" + item.data.missionType + "_status]", {
				quantity: item.data.quantity,
				target: item.data.targetQuantity - item.data.destroyedQuantity,
				remaining: (item.data.targetQuantity - item.data.destroyedQuantity) - item.data.quantity,
				hold: cm.$countCargoForMission(item.ID),
				commodity: displayNameForCommodity(item.data.commodity).toLowerCase(),
				unit: unit,
				destination: bb.$systemNameForID(item.destination),
				destinationA: (item.data.hasOwnProperty("destinationA") ? bb.$systemNameForID(item.data.destinationA) : ""),
				system: System.systemNameForID(item.source),
				stage: (item.data.hasOwnProperty("stage") ? item.data.stage : "0")
			}), item.source, item.destination);
			// check to see if there's a multi-stage mission text to pick up
			if (text.indexOf("_stage_") >= 0) {
				text = rep.$transformText(expandDescription("[" + text + "]", {
					quantity: item.data.quantity,
					target: item.data.targetQuantity - item.data.destroyedQuantity,
					remaining: (item.data.targetQuantity - item.data.destroyedQuantity) - item.data.quantity,
					hold: cm.$countCargoForMission(item.ID),
					commodity: displayNameForCommodity(item.data.commodity).toLowerCase(),
					destination: bb.$systemNameForID(item.destination),
					destinationA: (item.data.destinationA ? bb.$systemNameForID(item.data.destinationA) : ""),
					system: System.systemNameForID(item.source),
					stage: item.data.stage
				}), item.source, item.destination);
			}
			bb.$updateBBStatusText(item.ID, (missID === this._pendingMissionID ? expandDescription("[gcm_pending]") : "") + text);
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
// accept the currently pending mission
this.$acceptPendingMission = function $acceptPendingMission() {
	// convert the "pending" to "active"
	var missID = this._pendingMissionID;
	var bb = worldScripts.BulletinBoardSystem;
	var item = bb.$getItem(missID);
	if (item.data.missionType === 42) {
		worldScripts.GalCopBB_Delivery.$acceptPendingMission(item);
	}

	// flag the old mission as having been chained (if there is an old mission)
	var oldItem = bb.$getItem(this._pendingMissionOrigID);
	if (oldItem != null) oldItem.data.chained = true;

	// reset the pending mission variables
	this._pendingMissionID = -1;
	this.$updateManifestEntry(missID);
	// run the callback function, if it's been set
	if (this._pendingMissionCallback != null) this._pendingMissionCallback();
}

//-------------------------------------------------------------------------------------------------------------
// declines the currently pending mission
this.$declinePendingMission = function $declinePendingMission() {
	var bb = worldScripts.BulletinBoardSystem;
	var item = bb.$getItem(this._pendingMissionOrigID);

	if (item) {
		// if we didn't get a secondary mission, update the original mission to be "stopTimeAtComplete" = false
		// but only if the original mission didn't have a "stopTimeAtComplete" flag set already
		var missType = item.data.missionType;
		var checkType = expandDescription("[missionType" + missType + "_completionType]");
		if (item.stopTimeAtComplete === true && checkType.indexOf("|1") === -1) {
			item.stopTimeAtComplete = false;

			// also, set a new expiry time to be the distance to the source system + 10 minutes
			var info = System.infoForSystem(galaxyNumber, item.source);
			var route = system.info.routeToSystem(info);
			item.expiry = clock.adjustedSeconds + ((route.time * 3600) + 600);
			item.expiry = clock.adjustedSeconds + ((route.time * 3600) + 600);

			// switch to the alternate manifest entry
			item.data.altManifest = true;
		}
	}

	var pendItem = bb.$getItem(this._pendingMissionID);
	// remove email evidence of the pended acceptance
	if (pendItem && pendItem.lastEmailID) {
		var msgID = pendItem.lastEmailID;
		worldScripts.EmailSystem.$deleteSelectedItem(msgID);
	}

	// delete the mission from the BB and from local data
	bb.$removeBBMission(this._pendingMissionID);
	bb.$refreshManifest();
	this._requestSpecialCargo = "";
	this._pendingMissionID = -1;
	// run the callback function, if it's been set
	if (this._pendingMissionCallback != null) this._pendingMissionCallback();
}

//-------------------------------------------------------------------------------------------------------------
// adds local missions to be bulletin board
// this is the main function for generating missions
// also controls adding secondary missions if the overrideMissionType, originalMissionType, original source are passed
this.$addLocalMissions = function $addLocalMissions(overrideMissionTypes, originalMissionType, originalSource, missChain, postMission) {
	function compareDist(a, b) {
		var rt1 = system.info.routeToSystem(a);
		var rt2 = system.info.routeToSystem(b);
		if (rt1 && rt2) {
			if (rt1.distance < rt2.distance) {
				return -1;
			} else {
				return 1;
			}
		} else {
			if (rt1) return 1;
			if (rt2) return -1;
		}
	}

	var sliceAmt = 15;
	// don't add missions in interstellar space
	if (system.ID === -1 && overrideMissionTypes == null) return -1;
	// don't add missions in nova systems
	if (system.sun.hasGoneNova === true) return -1;

	var src = system.ID;
	if (originalSource && originalSource >= 0 && originalSource <= 255) src = originalSource;
	var srcSystem = System.infoForSystem(galaxyNumber, src);

	// make sure the player hasn't done something to warrant suppressing missions in this system
	if (originalMissionType === "" && worldScripts.GalCopBB_HitTeams.$areMissionsSuppressed(src) === true) return -1;

	var missTypes = this._availableMissionTypes;
	if (overrideMissionTypes && overrideMissionTypes.length > 0) {
		missTypes = overrideMissionTypes;
		// shuffle order so it isn't always the same variant being checked first
		missTypes.sort(function (a, b) {
			return Math.random() - 0.5;
		});
	}

	if (originalMissionType == null) originalMissionType = "";

	if (missChain == null || missChain === "") missChain = "";

	var endPoint = missTypes.length;
	if (!overrideMissionTypes) {
		if (this._loopPoint1 === -1) {
			this._loopPoint1 = 0;
		} else {
			this._loopPoint1 += sliceAmt;
		}
		endPoint = this._loopPoint1 + sliceAmt;
		if (endPoint > missTypes.length) endPoint = missTypes.length;
	} else {
		this._loopPoint1 = 0;
	}
	var bb = worldScripts.BulletinBoardSystem;

	// add missions to list
	for (var mt = this._loopPoint1; mt < endPoint; mt++) {
		var selectedMissionType = missTypes[mt];
		// if we hit a "0" mission type, just abort straight away - no mission to create
		if (selectedMissionType === 0) return -1;

		var mainWS = expandDescription("[missionType" + selectedMissionType + "_mainWorldScript]");
		var conditions = expandDescription("[missionType" + selectedMissionType + "_conditions]");
		// check the conditions for this mission
		var testResult = this.$testMissionConditions(conditions, src, selectedMissionType, -1, (originalMissionType === "" ? false : true));
		if (testResult === true && originalMissionType !== "") {
			// force a check of mission availability items (which are normally just checked by the BB itself)
			// however, we don't want to offer the player a secondary mission which they can't actually complete
			var checkAvailability = worldScripts[mainWS].$missionAvailability(-1, selectedMissionType, src);
			if (checkAvailability !== "") testResult = false;
		}
		if (testResult === true) {
			// get interstellar range for this mission
			var rng = parseInt(expandDescription("[missionType" + selectedMissionType + "_destRange]"));
			//log(this.name, "check destination range: " + rng);
			if (rng === 0) {
				// a range value of 0 means it's just the local system
				var sys = [];
				sys.push(srcSystem);
			} else if (rng > 0 && rng < 256) {
				var sys = system.info.systemsInRange(rng);
				for (var i = sys.length - 1; i >= 0; i--) {
					//log(this.name, "checking against " + sys[i].systemID + " - " + sys[i].name + ": " + system.info.distanceToSystem(sys[i]));
					var chkRt = system.info.routeToSystem(sys[i]);
					// remove any unreachable systems or systems whose actual route distance is greater than the requirement
					if (chkRt == null || chkRt.distance > rng)
						sys.splice(i, 1);
					// remove any systems that don't meet conditions
					else if (this.$testMissionConditions(expandDescription("[missionType" + selectedMissionType + "_destConditions]"), sys[i].systemID, selectedMissionType, src, (originalMissionType === "" ? false : true)) === false) {
						sys.splice(i, 1);
					} else if (sys[i].systemID === src) {
						// remove the current system if it's there
						sys.splice(i, 1);
					}
				}

				// if we didn't find any possible destinations, continue
				if (sys == null || sys.length === 0) {
					if (this._debug) log(this.name, "No systems found to generate mission type " + selectedMissionType);
					continue;
				}

				// get a random sequence of index values, so we don't end up getting the same planets coming up first
				sys.sort(function (a, b) {
					return Math.random() - 0.5;
				});
			} else if (rng === -1) {
				var sys = [];
				sys.push({
					systemID: -1,
					name: expandDescription("[gcm_interstellar_space]")
				});
			} else {
				var sys = [];
				sys.push({
					systemID: 256,
					name: expandDescription("[gcm_no_destination]")
				});
			}

			var sysSelectType = expandDescription("[missionType" + selectedMissionType + "_systemSelectProcess]");
			switch (sysSelectType) {
				case "random":
					var l_start = 0;
					var l_end = 1;
					var choice = Math.floor(Math.random() * sys.length);
					break;
				case "multipath": // trip to destination has multiple paths of different lengths
					var l_start = 0;
					var l_end = -1;
					var choice = -1;
					for (var i = 1; i < sys.length; i++) {
						var route1 = system.info.routeToSystem(sys[i], "OPTIMIZED_BY_JUMPS");
						var route2 = system.info.routeToSystem(sys[i], "OPTIMIZED_BY_TIME");
						if ((route1.time - route2.time) > 2) {
							choice = i;
							l_start = 0;
							l_end = 1;
							break;
						}
					}
					break;
				case "sequential":
					var l_start = 0;
					var l_end = sys.length;
					var choice = 0; // we'll pick it up in the loop
					break;
				case "source":
					var l_start = 0;
					var l_end = 1;
					var choice = 0; // only one to select from
					break;
				case "closest":
					sys.sort(compareDist);
					var l_start = 0;
					var l_end = sys.length;
					var choice = 0;
					break;
			}

			for (var i = l_start; i < l_end; i++) {
				if (sysSelectType === "sequential") choice = i;

				// get a systemInfo object of the destination system
				var dest = sys[choice];
				if (dest == null) continue; // in case this ever happens...

				// get the route information from the current system to the destination system
				if (dest.systemID >= 0 && dest.systemID <= 255) {
					var route = system.info.routeToSystem(dest, "OPTIMIZED_BY_JUMPS");
					if (route == null) continue; // if there's no route to the system
				}

				// ship, email or bb?
				// ship means mission was initiated from a NPC ship, but not as a secondary mission
				// email means mission was initiated from an email sent to the player
				// bb means mission was initiated from the BB
				var method = expandDescription("[missionType" + selectedMissionType + "_primaryMethod]");
				if (postMission && postMission === true) {
					method = "email";
				}
				if (this._debug) log(this.name, "missType selected = " + selectedMissionType + ", originalMissionType = " + originalMissionType + ", method = " + method);

				// any secondary mission can only be created as if it was from the BB
				if ((originalMissionType != "" && (!postMission || postMission === false)) && method != "bb") method = "bb";

				// are multiple methods defined?
				if (method.indexOf(",") >= 0) {
					// pick a random one from the list
					var methodItems = method.split(",");
					method = methodItems[Math.floor(Math.random() * methodItems.length)];
					//} else {
					//	var methodItems = [];
					//	methodItems.push(method);
				}

				var cust = [];
				var rtime = 0;
				var rdist = 0;
				var s_type = "0";
				var completeType = expandDescription("[missionType" + selectedMissionType + "_completionType]");
				if (this._debug) log(this.name, "missType selected = " + selectedMissionType + ", completeType = " + completeType);
				// there should be the same number of items in completeType as for method - select the matching item
				if (completeType.indexOf(",") >= 0) {
					var compTypeItems = completeType.split(",");
					completeType = compTypeItems[methodItems.indexOf(method)];
				}

				if (completeType.indexOf("|") >= 0) {
					// check the completion type for a change to the stopTime flag
					s_type = completeType.split("|")[1];
					completeType = completeType.split("|")[0];
				}

				if (completeType === "AT_SOURCE" || completeType === "WHEN_DOCKED_SOURCE") {
					cust.push({
						heading: expandDescription("[gcm_heading_to_complete]"),
						value: expandDescription("[gcm_return_originating]")
					});
				}
				if (completeType === "AT_STATIONKEY" || completeType === "WHEN_DOCKED_STATIONKEY") {
					cust.push({
						heading: expandDescription("[gcm_heading_to_complete]"),
						value: expandDescription("[gcm_return_station]")
					});
				}

				// calculate our time and distance to destination and back to source (if required)
				if (route) {
					if (s_type === "0" && (completeType === "AT_SOURCE" || completeType === "WHEN_DOCKED_SOURCE")) {
						// time includes round trip
						rtime = route.time * 3600 * 2;
						// plus 30 minutes transit time in each system
						rtime += (route.route.length * this._transitTime) * 2;
						rdist = route.distance * 2;
						if (src !== system.ID) {
							// trip out and back might not be the same
							// get system info object of original source system
							var sys2 = srcSystem;
							// get the route from the original source system to the new destination system
							var route2 = sys2.routeToSystem(dest, "OPTIMIZED_BY_JUMPS");
							rtime = (route.time + route2.time) * 3600;
							rtime += (route.route.length * this._transitTime) + (route2.route.length * this._transitTime);
							rdist = route.distance + route2.distance;
						}
					} else {
						// time is one way only
						rtime = route.time * 3600;
						// plus 30 minutes transit time in each system
						rtime += route.route.length * this._transitTime;
						rdist = route.distance;
					}
				} else {
					rtime = 24 * 3600 + this._transitTime;
					rdist = -1;
				}
				// if we're adding missions after a witchspace jump, add the extra time now
				rtime += this._initialTime;

				var stnKey = expandDescription("[missionType" + selectedMissionType + "_stationKeys]");
				// use first reputation entity for controlling client names
				var repEntity = expandDescription("[missionType" + selectedMissionType + "_reputationEntities]");
				var repSel = repEntity;
				if (repEntity.indexOf(",") !== -1) repSel = repEntity.split(",")[0];

				var locType = 0;
				var selectedCmdty = "";
				var pen = 0;
				var qty = 0;
				var amt = 0;
				var expire = 0;
				var dpst = 0;
				var destA = -1;
				var satType = 0;
				var values = {};
				var clientName = worldScripts.GalCopBB_Reputation.$getClientName(src, repSel);
				var ingr = [];

				// check for an interstellar type of mission
				var interstellarWarning = "";
				if (this._interstellarMissionTypes.indexOf(selectedMissionType) >= 0 && dest.systemID >= 0) {
					cust.push({
						heading: expandDescription("[gcm_heading_interstellar]"),
						value: expandDescription("[gcm_yes]")
					});
					interstellarWarning = expandDescription("[interstellar_warning]", {
						system: srcSystem.name,
						destination: dest.name
					});
				}

				// mission specific config
				var ws = worldScripts[expandDescription("[missionType" + selectedMissionType + "_valuesCallbackWS]")];
				if (ws) {
					values = ws[expandDescription("[missionType" + selectedMissionType + "_valuesCallbackFN]")](this._workTime, rtime, rdist, dest);
					// if we didn't get a result back, there must be an error, so just continue with the next mission type
					if (values == null) continue;
					qty = values.quantity;
					if (values.hasOwnProperty("price") === true) amt = values.price;
					expire = values.expiry;
					if (values.hasOwnProperty("penalty") === true) pen = values.penalty;
					if (values.hasOwnProperty("locationType") === true) locType = values.locationType;
					if (values.hasOwnProperty("commodity") === true) selectedCmdty = values.commodity;
					if (values.hasOwnProperty("deposit") === true) dpst = values.deposit;
					if (values.hasOwnProperty("clientName") === true) clientName = values.clientName;
					if (values.hasOwnProperty("donation") === true) cust.push({
						heading: expandDescription("[gcm_heading_donation]"),
						value: formatCredits(values.donation, false, true)
					});
					if (values.hasOwnProperty("ingredients") === true) ingr = values.ingredients;
					if (values.hasOwnProperty("satelliteTypes") === true) satType = values.satelliteTypes;
					if (values.hasOwnProperty("destinationA") === true) destA = values.destinationA;
				} else {
					log(this.name, "!!ERROR: No value callback found for mission type " + selectedMissionType);
					continue;
				}

				// pull up our stored client name if set
				if (this._storedClientName !== "") clientName = this._storedClientName;

				if (this._debug) log(this.name, "stationkeys = " + stnKey + ", locType = " + locType);

				// get the players reputation for this mission type in this system and chart-wide
				var rep_factor = (this.$playerMissionReputation(src, selectedMissionType) + (this.$playerMissionReputation(-1, selectedMissionType) * (this.$playerRank() / 8)));
				// turn off rep factor if the result would be less than any deposit amount
				if (dpst != 0 && amt * rep_factor < dpst) rep_factor = 0;
				if (rep_factor != 0) amt = Math.floor(amt * rep_factor);

				// when in override mode, add new missions as accepted automatically
				var autoAccept = false;
				if (originalMissionType !== "" && (!postMission || postMission === false)) autoAccept = true;

				// set the the possibility of assassins with this mission
				var assassins = 0.0;
				assassins = parseFloat(expandDescription("[missionType" + selectedMissionType + "_assassinChance]"));

				var risk = "";
				if (assassins > 0 && assassins <= 0.3) risk = expandDescription("[gcm_risk_low]");
				if (assassins > 0.3 && assassins <= 0.6) risk = expandDescription("[gcm_risk_medium]");
				if (assassins > 0.6) risk = expandDescription("[gcm_risk_high]");

				cust.push({
					heading: expandDescription("[gcm_heading_client]"),
					value: clientName
				});
				if (risk !== "") cust.push({
					heading: expandDescription("[gcm_heading_danger]"),
					value: risk
				});

				// add equipment requirements
				if (conditions.indexOf("include_equipment") >= 0) {
					var condList = conditions.split(",");
					var eqList = condList[this.$getConditionIndex(condList, "include_equipment")].split(":")[1].split("|");
					if (eqList && eqList.length > 0) {
						var eqText = "";
						for (var j = 0; j < eqList.length; j++) {
							var ref = eqList[j];
							if (this._equipmentProvidingLookup[ref]) ref = this._equipmentProvidingLookup[ref];
							var eqItem = EquipmentInfo.infoForKey(ref);
							if (!eqItem) {
								log(this.name, "!!ERROR: Eq item not found = " + ref);
							} else {
								if (eqText != "") eqText += "\n";
								if (ref === "EQ_PASSENGER_BERTH") {
									eqText += eqItem.name.substring(0, eqItem.name.indexOf(" - ")).trim();
								} else {
									eqText += eqItem.name;
								}
							}
						}
						cust.push({
							heading: expandDescription("[gcm_heading_equipment]"),
							value: eqText
						});
					}
				}

				// special cases
				var type53Menu = null;
				switch (selectedMissionType) {
					case 33:
						// we're going to put some values into mission variables here, so the expandDesc function will include the data
						missionVariables.stolenItemType = expandDescription("[gcm_stolen_item_types]");
						missionVariables.targetShipKey = this.$pickGetawayShip();
						var shipspec = Ship.shipDataForKey(missionVariables.targetShipKey);
						missionVariables.targetShipType = shipspec.name;
						break;
					case 53:
						type53Menu = [];
						for (var i = 0; i < ingr.length; i++) {
							missionVariables["ingredient" + (i + 1)] = expandDescription("[gcm_ingredient_" + ingr[i] + "]");
							type53Menu.push({
								text: expandDescription("[gcm_ingredient_" + ingr[i] + "_menu]"),
								worldScript: mainWS,
								callback: "$type53_collect_" + ingr[i],
								condition: "$type53_confirm_" + ingr[i],
								autoRemove: true
							});
						}
						break;
					case 150:
						missionVariables.targetShipName = worldScripts.GalCopBB_MissionDetails.$shipName();
						break;
				}

				var rep = worldScripts.GalCopBB_Reputation;
				switch (method) {
					case "ship":
						// because some mission conditions don't get checked on BB missions, we need to redo the checks to include them
						var secondCheck = this.$testMissionConditions(conditions, src, selectedMissionType, dest.systemID, true);
						if (secondCheck === false) continue;

						var id = -1;
						var ws = expandDescription("[missionType" + selectedMissionType + "_shipCallbackWS]");
						var fn = expandDescription("[missionType" + selectedMissionType + "_shipCallbackFN]");

						var w = worldScripts[ws];
						if (w) {
							// we're putting a lot of info in the payload, so the mission can be fully configured when it's transferred from another ship to the player
							var payload = {
								missionType: selectedMissionType,
								//originalType:selectedMissionType,
								originator: "ship",
								originalMissionType: originalMissionType,
								amount: formatCredits(amt, false, true),
								payment: amt,
								penalty: 0,
								deposit: 0,
								commodity: displayNameForCommodity(selectedCmdty).toLowerCase(),
								commodityKey: selectedCmdty,
								target: qty,
								system: srcSystem.name,
								sourceID: src,
								destination: dest.name,
								destinationID: dest.systemID,
								destinationA: destA,
								destinationAName: System.systemNameForID(destA),
								expire: expire,
								expiry: bb.$getTimeRemaining(expire),
								completionType: completeType,
								stopTimeAtComplete: (s_type === "1" ? true : false),
								terminatePenalty: false, // missions added via ship-to-ship are auto-accepted, so no penalty on terminate
								risk: risk.toLowerCase(),
								assassinChance: assassins,
								location: expandDescription(this._positions[locType]),
								locationType: locType,
								stolenItemType: missionVariables.stolenItemType,
								targetShipKey: missionVariables.targetShipKey,
								targetShipName: missionVariables.targetShipName,
								custom: cust,
								missionChain: missChain + (missChain === "" ? "" : "|") + originalMissionType,
								subject: expandDescription("[missionType" + selectedMissionType + "_description]"),
								name: clientName
							};

							// execute the function with the mission data payload
							w[fn](payload);

						}
						break;
					case "email":
						// if the primary delivery mode is set to email, make sure it's installed, otherwise just skip this mission
						if (this._emailInstalled === false) continue;
						var email = worldScripts.EmailSystem;

						// because some mission conditions don't get checked on BB missions, we need to redo the checks to include them
						var secondCheck = this.$testMissionConditions(conditions, src, selectedMissionType, dest.systemID, true);
						if (this._debug) log(this.name, "second check result = " + secondCheck);
						if (secondCheck === false) continue;

						var id = -2;

						var accept = expandDescription("[missionType" + selectedMissionType + "_emailAccept]", {
							name: clientName
						});
						if (accept === "") accept = expandDescription("[gcm_email_accept]", {
							name: clientName
						});
						var reject = expandDescription("[missionType" + selectedMissionType + "_emailReject]", {
							name: clientName
						});
						if (reject === "") reject = expandDescription("[gcm_email_reject]", {
							name: clientName
						});
						var expiry = expandDescription("[missionType" + selectedMissionType + "_emailExpiry]");
						if (expiry === "") expandDescription("[gcm_email_expiry]");
						var emailSubject = expandDescription("[missionType" + selectedMissionType + "_emailSubject]");

						var lastMissionTxt = "";
						if (postMission && postMission === true) {
							lastMissionTxt = expandDescription("[missionType" + originalMissionType + "_emailLastMissionText]");
						}

						// we're putting a lot of info in the payload, so the mission can be fully configured when it's accepted through the email system
						var payload = {
							missionType: selectedMissionType,
							originalMissionType: originalMissionType,
							originator: "email",
							amount: formatCredits(amt, false, true),
							payment: amt,
							penalty: 0,
							deposit: 0,
							commodity: displayNameForCommodity(selectedCmdty).toLowerCase(),
							commodityKey: selectedCmdty,
							target: qty,
							system: srcSystem.name,
							sourceID: src,
							destination: dest.name,
							destinationID: dest.systemID,
							destinationA: destA,
							destinationAName: System.systemNameForID(destA),
							expire: expire,
							expiry: bb.$getTimeRemaining(expire),
							completionType: completeType,
							stopTimeAtComplete: (s_type === "1" ? true : false),
							terminatePenalty: true, // emails have an accept/reject method, so its the same as for BB items
							risk: risk.toLowerCase(),
							assassinChance: assassins,
							location: expandDescription(this._positions[locType]),
							locationType: locType,
							position: expandDescription(this._positions[locType]),
							stolenItemType: missionVariables.stolenItemType,
							targetShipKey: missionVariables.targetShipKey,
							targetShipName: missionVariables.targetShipName,
							custom: cust,
							subject: emailSubject,
							name: clientName,
							missionChain: missChain + (missChain === "" ? "" : "|") + originalMissionType,
							lastMissionText: lastMissionTxt,
						};
						// we aren't defining a rejection callback - the mission isn't registered at this point so there is nothing to reject.
						email.$createEmail({
							sender: clientName,
							subject: emailSubject,
							date: global.clock.adjustedSeconds + (postMission && postMission === true ? 10 : 0), //(3600 * parseInt(Math.Random() * 40) + 8) : 0), // put post mission emails in the future
							sentFrom: (dest.systemID >= 0 && dest.systemID <= 255 ? dest.systemID : src),
							message: expandDescription("[missionType" + selectedMissionType + "_email]", payload),
							expiryDate: expire,
							allowExpiryCancel: false,
							expiryText: expiry,
							option1: {
								display: accept.split("|")[0],
								reply: accept.split("|")[1],
								script: "GalCopBB_Missions",
								callback: "$acceptMissionFromOther",
								parameter: payload
							},
							option2: {
								display: reject.split("|")[0],
								reply: reject.split("|")[1]
							}
						});

						break;
					case "bb":
					default:
						// process any post status messages and prep the object 
						var postMsg = [];
						for (var m = 1; m <= 3; m++) {
							var extract = ""
							switch (m) {
								case 1:
									extract = rep.$transformText(expandDescription("[missionType" + selectedMissionType + "_initiatedMessage]"), src, dest.systemID);
									break;
								case 2:
									extract = rep.$transformText(expandDescription("[missionType" + selectedMissionType + "_completedMessage]"), src, dest.systemID);
									break;
								case 3:
									extract = rep.$transformText(expandDescription("[missionType" + selectedMissionType + "_terminatedMessage]"), src, dest.systemID);
									break;
							}
							if (extract !== "") {
								var brk = extract.split("|");
								var txt = brk[0];
								var rt = "item";
								if (m > 1) rt = "list";
								var bk = "";
								var ov = "";
								var md = "";
								var mdp = 0;
								var mds = true;
								for (var k = 0; k < brk.length; k++) {
									if (brk[k].indexOf("return:") >= 0) rt = brk[k].split(":")[1];
									if (brk[k].indexOf("background:") >= 0) bk = brk[k].split(":")[1];
									if (brk[k].indexOf("overlay:") >= 0) ov = brk[k].split(":")[1];
									if (brk[k].indexOf("model:") >= 0) md = brk[k].split(":")[1];
									if (brk[k].indexOf("modelPersonality:") >= 0) mdp = parseInt(brk[k].split(":")[1]);
									if (brk[k].indexOf("spinModel:") >= 0) mds = brk[k].split(":")[1];
								}
								var pmItem = {};
								pmItem["status"] = (m === 1 ? "initiated" : (m === 2 ? "completed" : "terminated"));
								pmItem["return"] = rt;
								pmItem["text"] = txt;
								if (bk !== "") pmItem["background"] = this.$getTexture(bk);
								if (ov !== "") pmItem["overlay"] = this.$getTexture(ov);
								if (md !== "") pmItem["model"] = md;
								if (mdp !== 0) pmItem["modelPersonality"] = mdp;
								if (mds === false) pmItem["spinModel"] = mds;
								postMsg.push(pmItem);
							}
						}
						var linkedtype = "";
						if (originalMissionType !== "" && (!postMission || postMission === false)) {
							linkedtype = "_" + expandDescription("[missionType" + originalMissionType + "_linkedMissionSource]");
						}
						var missText = "";
						var cmdty_unit = this.$getCommodityUnit(selectedCmdty);
						if (linkedtype === "" && expandDescription("[missionType" + selectedMissionType + "_phraseGen]") === "1") {
							var fn = expandDescription("[missionType" + selectedMissionType + "_detailFn]");
							missText = rep.$transformText(expandDescription(worldScripts.GalCopBB_MissionDetails[fn](), {
								amount: formatCredits(amt, false, true),
								commodity: displayNameForCommodity(selectedCmdty).toLowerCase(),
								unit: cmdty_unit,
								target: qty,
								system: srcSystem.name,
								destination: dest.name,
								destinationA: System.systemNameForID(destA),
								expiry: bb.$getTimeRemaining(expire),
								position: expandDescription(this._positions[locType]),
								position_reference: expandDescription(this._positionReferences[locType]),
								name: clientName
							}), src, dest.systemID);
						} else {
							missText = rep.$transformText(expandDescription("[missionType" + selectedMissionType + "_details" + linkedtype + "]", {
								amount: formatCredits(amt, false, true),
								commodity: displayNameForCommodity(selectedCmdty).toLowerCase(),
								unit: cmdty_unit,
								target: qty,
								system: srcSystem.name,
								destination: dest.name,
								destinationA: System.systemNameForID(destA),
								expiry: bb.$getTimeRemaining(expire),
								position: expandDescription(this._positions[locType]),
								position_reference: expandDescription(this._positionReferences[locType]),
								name: clientName
							}), src, dest.systemID);
						}
						var custMenu = null;
						var mnu = expandDescription("[missionType" + selectedMissionType + "_customMenu]");
						if (mnu !== "") {
							var itms = mnu.split("|");
							custMenu = [];
							custMenu.push({
								text: itms[0],
								worldScript: itms[1],
								callback: itms[2],
								condition: itms[3],
								autoRemove: (this._trueValues.indexOf(itms[4]) >= 0 ? true : false)
							});
						} else {
							custMenu = "";
						}
						if (type53Menu != null) custMenu = type53Menu;

						// items added as a secondary item won't get emails.
						var noEmail = false;
						if (postMission && postMission === false) noEmail = true;

						var addMarkers = [];
						if (destA != -1) {
							addMarkers.push({ system: destA, markerShape: "MARKER_SQUARE", markerColor: "orangeColor" });
						}

						// ok, time to add the mission to the BB
						var id = bb.$addBBMission({
							source: src,
							destination: dest.systemID,
							stationKey: stnKey,
							description: rep.$transformText(expandDescription("[missionType" + selectedMissionType + "_description]"), src, dest.systemID),
							details: missText + interstellarWarning,
							overlay: this.$getTexture(expandDescription("[missionType" + selectedMissionType + "_bbOverlay]")),
							payment: amt,
							penalty: pen,
							deposit: dpst,
							allowPartialComplete: expandDescription("[missionType" + selectedMissionType + "_partialComplete]"),
							completionType: completeType,
							stopTimeAtComplete: (s_type === "1" ? true : false),
							accepted: autoAccept,
							expiry: expire,
							disablePercentDisplay: expandDescription("[missionType" + selectedMissionType + "_disablePercent]"),
							customDisplayItems: (cust.length !== 0 ? cust : ""),
							noEmails: noEmail,
							initiateCallback: "$acceptedMission",
							completedCallback: "$completedMission",
							confirmCompleteCallback: "$confirmCompleted",
							terminateCallback: "$terminateMission",
							failedCallback: "$failedMission",
							manifestCallback: "$updateManifestEntry",
							availableCallback: "$missionAvailability",
							worldScript: mainWS,
							customMenuItems: custMenu,
							postStatusMessages: postMsg,
							additionalMarkers: addMarkers,
							data: {
								source: this.name,
								missionType: selectedMissionType,
								locationType: locType,
								quantity: 0,
								targetQuantity: qty,
								destroyedQuantity: 0,
								commodity: selectedCmdty,
								missionChain: missChain + (missChain === "" ? "" : "|") + originalMissionType,
								chained: (originalMissionType === "" ? false : true),
								terminatePenalty: true, // all missions accepted via the bulletin board will have reputation penalties when terminated/failed
								altManifest: false,
								name: clientName,
								assassinChance: assassins,
								stolenItemType: missionVariables.stolenItemType,
								targetShipKey: missionVariables.targetShipKey,
								targetShipName: missionVariables.targetShipName,
								ingredients: ingr,
								delivered: 0,
								expected: 0,
								origSystemID: src,
								satelliteTypes: satType,
								destinationA: destA
							}
						});
						if (this._debug) {
							log(this.name, "BB ID added " + id);
							log(this.name, "Original " + selectedMissionType + ", new mission chain " + missChain + (missChain === "" ? "" : "|") + originalMissionType);
						}
						break;
				}

				// special cases
				switch (selectedMissionType) {
					case 33:
						// clean up
						delete missionVariables.stolenItemType;
						delete missionVariables.targetShipKey;
						delete missionVariables.targetShipType;
						break;
					case 53:
						for (var i = 0; i < ingr.length; i++) {
							delete missionVariables["ingredient" + (i + 1)];
						}
						break;
					case 150:
						delete missionVariables.targetShipName;
						break;
				}

				// if we got here through the secondary mission process, we want to stop after adding one mission, and force it to accepted
				if (originalMissionType !== "" && (!postMission || postMission === false)) {
					// put this mission in as pending - the player will need to accept or decline
					this._pendingMissionID = id;
					this.$acceptedMission(id);
					// make sure the manifest screen is updated as well
					bb.$addManifestEntry(id);
					bb.$initInterface(player.ship.dockedStation);
					return id;
				}
				// if this is a single mission only, return now.
				if (this._singleMissionOnly === true) {
					bb.$initInterface(player.ship.dockedStation);
					return id;
				}
				// retest the conditions to see if we can continue or not
				if (this.$testMissionConditions(expandDescription("[missionType" + selectedMissionType + "_conditions]"), src, selectedMissionType, -1, (originalMissionType === "" ? false : true)) === false) break;
			}
		} else {
			if (this._debug) log(this.name, "Conditions not met for " + selectedMissionType);
		}
	}

	// are we going around again?
	if (!overrideMissionTypes) {
		bb.$initInterface(player.ship.dockedStation);
		if (endPoint < missTypes.length) {
			if (this._createTimer && this._createTimer.isRunning) this._createTimer.stop();
			this._createTimer = new Timer(this, this.$addLocalMissions, this._createDelay, 0);
		}
	}
	// mix up the list a bit, if we aren't in override mode
	//if (originalMissionType === "") bb.$shuffleBBList();
	return -1;
}

//-------------------------------------------------------------------------------------------------------------
// timer target for creating a new mission for the player
// passing a negative for the origMissID will force the routine to act as if a mission of type ABS(origMissID) has just been completed
this.$createSecondaryMission = function $createSecondaryMission(origMissID, liveSource) {

	var id = -1;
	// don't create a secondary in interstellar space
	if (system.ID === -1) return id;

	var bb = worldScripts.BulletinBoardSystem;
	if (origMissID > 0) {
		var item = bb.$getItem(origMissID);
		if (!item) log(this.name, "**ERROR: did not get mission details back for mission ID " + origMissID);
	} else {
		var item = null;
	}
	// don't give a player a new mission they need to make decisions about while they're under hostile red alert
	if (player.ship.alertCondition !== 3 || player.alertHostiles === false) {
		if (this._debug) {
			log(this.name, "Creating second mission from original mission " + origMissID);
			if (item) log(this.name, "Orig misstype " + item.data.missionType);
		}
		if (item) {
			var chain = item.data.missionChain;
			var origType = item.data.missionType;
			var origSource = item.source;

			if (chain == null) chain = "";
		} else {
			var chain = "";
			var origType = Math.abs(origMissID);
			var origSource = system.ID;
		}
		if (this._debug) log(this.name, "new miss type " + origType);
		var linked = expandDescription("[missionType" + origType + "_linkedMissionTypes]");

		var missionList = [];
		var chainData = chain.split("|");
		// OK, time to work out the next mission for the player
		if (linked && linked !== "") {
			var getList = linked.split(",");
			for (var i = 0; i < getList.length; i++) {
				// don't add any mission types already in the chain
				if (chainData.indexOf(getList[i]) === -1) missionList.push(parseInt(getList[i]));
			}
		}

		if (this._debug) log(this.name, "MissionList = " + missionList);
		if (missionList.length > 0) {
			missionList.sort(function (a, b) {
				return Math.random() - 0.5;
			});
			this._singleMissionOnly = true;
			if (liveSource && liveSource === true) {
				id = this.$addLocalMissions(missionList, origType, origSource, chain, false);
			} else {
				id = this.$addLocalMissions(missionList, origType, origSource, chain);
			}
			this._singleMissionOnly = false;
		}
		if (this._debug) log(this.name, "New id " + id);
	}

	if (id < 0 && item) {
		var missType = item.data.missionType;
		var checkType = expandDescription("[missionType" + missType + "_completionType]");
		// if we didn't get a secondary mission, update the original mission to be "stopTimeAtComplete" = false
		if (item.stopTimeAtComplete === true && checkType.indexOf("|1") === -1) {
			item.stopTimeAtComplete = false;

			// also, set a new expiry time to be the distance to the source system + 10 minutes
			var dest = System.infoForSystem(galaxyNumber, item.source);
			var route = system.info.routeToSystem(dest);
			item.expiry = clock.adjustedSeconds + ((route.time * 3600) + 600);

			// switch to the alternate manifest entry
			item.data.altManifest = true;
		}
	} else if (id > 0) {
		// turn the current mission's process to stop time now (it would be a bit harsh to give the player a new mission and expecting the ETA not to change)
		if (item) {
			if (item.stopTimeAtComplete === false) {
				item.stopTimeAtComplete = true;
				// do we need to turn on the alt manifest now?
				if (expandDescription("[missionType" + item.data.missionType + "_completionType]").indexOf("|1") === -1) {
					item.data.altManifest = true;
				}
			}
		}

		// ok, we have a new pending mission - better tell the player about it I suppose...
		var nItem = bb.$getItem(id);
		if (origMissID < 0) {
			// no termination penalty for secondary missions created spontaneously (ie by scooping a black box/escape pod)
			nItem.penalty = 0;
			nItem.data.terminatePenalty = false;
		}

		// special cases
		switch (nItem.data.missionType) {
			case 33:
				// we're going to put some values into mission variables here, so the expandDesc function will include the data
				missionVariables.stolenItemType = expandDescription("[gcm_stolen_item_types]");
				missionVariables.targetShipKey = this.$pickGetawayShip();
				var shipspec = Ship.shipDataForKey(missionVariables.targetShipKey);
				missionVariables.targetShipType = shipspec.name;
				break;
		}

		var linkedtype = expandDescription("[missionType" + origType + "_linkedMissionSource]");
		var txt = expandDescription("[missionType" + nItem.data.missionType + "_miniBriefing_" + linkedtype + "]", {
			target: nItem.data.targetQuantity,
			destination: bb.$systemNameForID(nItem.destination),
			system: System.systemNameForID(nItem.source),
			position: expandDescription(this._positions[nItem.data.locationType]),
			position_reference: expandDescription(this._positionReferences[nItem.data.locationType]),
			expiry: bb.$getTimeRemaining(nItem.expiry),
			commodity: displayNameForCommodity(nItem.data.commodity).toLowerCase(),
			amount: formatCredits(nItem.payment, false, true)
		});

		this._pendingMissionOrigID = origMissID;

		var typ = expandDescription("[missionType" + origType + "_linkedMissionCommsMethod]");
		if (this._debug) log(this.name, "checking " + typ);
		// communicate with player about the new mission
		worldScripts.GalCopBB_Missions_MFD.$updateMFD(
			txt,
			(typ.indexOf("comms") >= 0 ? true : false),
			(typ.indexOf("comms") >= 0 ? worldScripts.GalCopBB_MeetShip._commsNPC : null),
			(typ.indexOf("|1") >= 0 ? true : false)
		);
	}
	return id;
}

//-------------------------------------------------------------------------------------------------------------
// removes an existing mission and adds a new mission
this.$generateMissions = function $generateMissions() {
	// remove one existing mission
	var bb = worldScripts.BulletinBoardSystem;
	for (var i = 0; i < bb._data.length; i++) {
		if (bb._data[i].accepted === false && bb._data[i].data && bb._data[i].source == this.name && Math.random() > 0.6) {
			bb._data.splice(i, 1);
			break;
		}
	}
	this._singleMissionOnly = true;
	// shuffle order so it isn't always the same variant being checked first
	this._availableMissionTypes.sort(function (a, b) {
		return Math.random() - 0.5;
	});
	this.$addLocalMissions();
	this._singleMissionOnly = false;
}

//=============================================================================================================
// general functions
//-------------------------------------------------------------------------------------------------------------
// check the condition string and return either true (conditions passed) or false
// note: conditions checked in this function will prevent a mission from being generated
// the "$missionAvailability" function handles conditions that could be changed by the player just limit the ability of the player to accept the mission.
this.$testMissionConditions = function $testMissionConditions(condString, systemID, missionType, origSystemID, isSecondary) {
	var result = true;
	// if there is no condition, it's true in all situations, so just return the result now.
	if (condString === "") return result;
	// if we're debugging one of the missions, always add it in.
	if (condString.indexOf("debug") >= 0) return result;
	// are we attempting to test a specific mission?
	if (this._forceCreate.indexOf(missionType) >= 0) {
		var fc = this._forceCreate.indexOf(missionType);
		this._forceCreate.splice(fc, 1);
		return result;
	}

	/*
		Note about "stationKeys":
		Normally the stationKeys element would restrict where a mission would be offered from. ie galcop stationKeys would mean galcop only stations.
		However, for secondary missions the stationKey doesn't come into play as they aren't accepted from the BB itself but through some other means
		ie email, comms, ship.
		So that means that a player can start a mission from a pirate station, and still be given a galcop mission (eg get the cargo)
		However, because the description of a pirate-type 6 and a galcop-type 6 should be quite different, this should not really be allowed to happen.
		Note to self: take care when setting up secondary email-type missions.
	*/

	var dest = System.infoForSystem(galaxyNumber, systemID);
	var condList = condString.split(",");
	for (var i = 0; i < condList.length; i++) {
		var condItem = condList[i];
		if (condItem.indexOf(":") >= 0) {
			var items = condItem.split(":");
			var checkVal = this.$getConditionParameter(condItem, origSystemID);
			switch (items[0]) {
				case "function": 
					var ws = expandDescription("[missionType" + missionType + "_mainWorldScript]");
					var fn = items[1];
					var invert = false;
					if (fn.indexOf("!") === 0) {
						fn = fn.substring(fn.indexOf("!") + 1);
						invert = true;
					}
					if (worldScripts[ws] && worldScripts[ws][fn]) {
						var test = worldScripts[ws][fn](systemID);
						if (!invert && !test) result = false;
						if (invert && test) result = false;
					}
					break;
				case "chance": // random chance of offering this mission
					if (Math.random() > (checkVal / 100)) result = false;
					break;
				case "mission_gap_days": // how long until another mission will be generated in this system
					if (this.$lastMissionGapDays(systemID, missionType) < checkVal) result = false;
					break;
				case "max_mission_count": // how many of these missions can be available in order to include this one?
					if (this.$countMissions(missionType) > checkVal) result = false;
					break;
				case "incompatible_with": // if any of these missions already exist in this system, don't include this type
					// trying to avoid things like having trade negotiations breaking down and the result being (a) to destroy traders and (b) steal cargo
					var incompatible = items[1].split("|");
					for (var j = 0; j < incompatible.length; j++) {
						if (this.$countMissions(parseInt(incompatible[j])) > 0) result = false;
					}
					break;
				case "has_player_role":
					if (this.$checkPlayerRoles(1, items[1]) === false) result = false;
					break;
				case "no_player_role":
					if (this.$checkPlayerRoles(1, items[1]) === true) result = false;
					break;
				case "success_missions": // example data: 46|3  means: must have successfully completed at least 3 type 46 missions
					var extra = items[1].split("|");
					var missType = parseInt(extra[0]);
					var num = parseInt(extra[1]);
					if (this.$getSuccessMissions(systemID, missType) < num) result = false;
					break;
				case "fail_missions": // example data: 46|3  means: must have failed at least 3 type 46 missions
					var extra = items[1].split("|");
					var missType = parseInt(extra[0]);
					var num = parseInt(extra[1]);
					if (this.$getFailedMissions(systemID, missType) < num) result = false;
					break;
				case "is_hacked":
					if (checkVal === 0 && worldScripts.GalCopBB_WBSA._wpHacks.indexOf(systemID) >= 0) result = false;
					if (checkVal === 1 && worldScripts.GalCopBB_WBSA._wpHacks.indexOf(systemID) === -1) result = false;
					break;
				case "is_secondary":
					if (checkVal === 0 && isSecondary === true) result = false;
					if (checkVal === 1 && isSecondary === false) result = false;
					break;
				case "min_government":
					if (items[1] === "source_system" && origSystemID >= 0) checkVal = System.infoForSystem(galaxyNumber, origSystemID).government;
					if (dest.government < checkVal) result = false;
					break;
				case "max_government":
					if (items[1] === "source_system" && origSystemID >= 0) checkVal = System.infoForSystem(galaxyNumber, origSystemID).government;
					if (dest.government > checkVal) result = false;
					break;
				case "government":
					if (items[1] === "source_system" && origSystemID >= 0) checkVal = System.infoForSystem(galaxyNumber, origSystemID).government;
					if (items[1].indexOf("|") >= 0) {
						var govList = items[1].split("|");
						var tempresult = false;
						for (var j = 0; j < govList.length; j++) {
							if (dest.government === parseInt(govList[j])) tempresult = true;
						}
						if (tempresult === false) result = false;
					} else {
						if (dest.government !== checkVal) result = false;
					}
					break;
				case "description_properties":
					var props = items[1].split("|");
					for (var j = 0; j < props.length; j++) {
						// look for a "not" clause (indicated by a "!" symbol)
						if (props[j].indexOf("!") === 0) {
							if (dest.description.indexOf(props[j].substring(props[j].indexOf("!") + 1)) >= 0) result = false;
						} else {
							if (dest.description.indexOf(props[j]) === -1) result = false;
						}
					}
					break;
				case "min_economy":
					if (items[1] === "source_system" && origSystemID >= 0) checkVal = System.infoForSystem(galaxyNumber, origSystemID).economy;
					if (dest.economy < checkVal) result = false;
					break;
				case "max_economy":
					if (items[1] === "source_system" && origSystemID >= 0) checkVal = System.infoForSystem(galaxyNumber, origSystemID).economy;
					if (dest.economy > checkVal) result = false;
					break;
				case "economy":
					if (items[1] === "source_system" && origSystemID >= 0) checkVal = System.infoForSystem(galaxyNumber, origSystemID).economy;
					if (items[1].indexOf("|") >= 0) {
						var ecoList = items[1].split("|");
						var tempresult = false;
						for (var j = 0; j < ecoList.length; j++) {
							if (dest.economy === parseInt(ecoList[j])) tempresult = true;
						}
						if (tempresult === false) result = false;
					} else {
						if (dest.economy !== checkVal) result = false;
					}
					break;
				case "min_techlevel":
					if (items[1] === "source_system" && origSystemID >= 0) checkVal = System.infoForSystem(galaxyNumber, origSystemID).techlevel;
					if (dest.techlevel < checkVal) result = false;
					break;
				case "max_techlevel":
					if (items[1] === "source_system" && origSystemID >= 0) checkVal = System.infoForSystem(galaxyNumber, origSystemID).techlevel;
					if (dest.techlevel > checkVal) result = false;
					break;
				case "techlevel":
					if (items[1] === "source_system" && origSystemID >= 0) checkVal = System.infoForSystem(galaxyNumber, origSystemID).techlevel;
					if (items[1].indexOf("|") >= 0) {
						var tlList = items[1].split("|");
						var tempresult = false;
						for (var j = 0; j < tlList.length; j++) {
							if (dest.government === parseInt(tlList[j])) tempresult = true;
						}
						if (tempresult === false) result = false;
					} else {
						if (dest.techlevel !== checkVal) result = false;
					}
					break;
				case "min_dangerous_neighbours":
					var dang = dest.systemsInRange(7);
					var dangCount = 0;
					for (var j = 0; j < dang.length; j++) {
						if (dang[j].government < 3) dangCount += 1;
					}
					if (dangCount < checkVal) result = false;
					break;
				case "max_score":
					if (player.score > checkVal) result = false;
					break;
				case "exclude_equipment": // player cannot have any of these equipment items installed
					var eqList = items[1].split("|");
					for (var j = 0; j < eqList.length; j++) {
						if (eqList[j] !== "" && player.ship.hasEquipmentProviding(eqList[j]) === true) result = false;
					}
					break;
				case "max_bounty":
					if (player.bounty > checkVal) result = false;
					break;
				case "min_bounty":
					if (player.bounty < checkVal) result = false;
					break;
				case "bounty":
					if (player.bounty !== checkVal) result = false;
					break;
				case "max_parcel_reputation":
					if (player.parcelReputation > checkVal) result = false;
					break;
				case "max_cargo_reputation":
					if (player.contractReputation > checkVal) result = false;
					break;
				case "max_passenger_reputation":
					if (player.passengerReputation > checkVal) result = false;
					break;
				case "max_mission_reputation":
					if (this.$playerMissionReputation(systemID, missionType) > checkVal) result = false;
					break;
				case "has_outbreak":
					if (checkVal === 0 && worldScripts.GalCopBB_DiseaseOutbreak.$systemHasDiseaseOutbreak(systemID) === true) result = false;
					if (checkVal === 1 && worldScripts.GalCopBB_DiseaseOutbreak.$systemHasDiseaseOutbreak(systemID) === false) result = false;
					break;
				case "has_solar_activity":
					if (checkVal === 0 && worldScripts.GalCopBB_SolarActivity._solarActivitySystems.indexOf(systemID) >= 0) result = false;
					if (checkVal === 1 && worldScripts.GalCopBB_SolarActivity._solarActivitySystems.indexOf(systemID) === -1) result = false;
					break;
				case "has_earthquake":
					if (checkVal === 0 && worldScripts.GalCopBB_Earthquake._earthquakeSystems.indexOf(systemID) >= 0) result = false;
					if (checkVal === 1 && worldScripts.GalCopBB_Earthquake._earthquakeSystems.indexOf(systemID) === -1) result = false;
					break;
				case "has_pirate_hermit":
					if (checkVal === 0 && worldScripts.GalCopBB_SoftwareInstall._pirateHermitInfo[systemID] >= 0) result = false;
					if (checkVal === 1 && (!worldScripts.GalCopBB_SoftwareInstall._pirateHermitInfo[systemID] || worldScripts.GalCopBB_SoftwareInstall._pirateHermitInfo[systemID] == undefined)) result = false;
					break;
				case "market_disrupted":
					if (checkVal === 0 && worldScripts.GalCopBB_StationDisrupt.$disruptCommodities(systemID) === true) result = false;
					if (checkVal === 1 && worldScripts.GalCopBB_StationDisrupt.$disruptCommodities(systemID) === false) result = false;
					break;
				case "equipment_disrupted":
					if (checkVal === 0 && worldScripts.GalCopBB_StationDisrupt.$disruptEquipment(systemID) === true) result = false;
					if (checkVal === 1 && worldScripts.GalCopBB_StationDisrupt.$disruptEquipment(systemID) === false) result = false;
					break;
				case "security_software":
					if (checkVal === 0 && worldScripts.GalCopBB_SoftwareInstall._pirateHermitInfo[systemID] && worldScripts.GalCopBB_SoftwareInstall._pirateHermitInfo[systemID] > 0) result = false;
					if (checkVal === 1 && (!worldScripts.GalCopBB_SoftwareInstall._pirateHermitInfo[systemID] || worldScripts.GalCopBB_SoftwareInstall._pirateHermitInfo[systemID] == undefined || worldScripts.GalCopBB_SoftwareInstall._pirateHermitInfo[systemID] === 0)) result = false;
					break;
				case "igt_installed":
					if (checkVal === 0 && this._igtInstalled === true) result = false;
					if (checkVal === 1 && this._igtInstalled === false) result = false;
					break;
				case "ups_med_mission":
					// checks to see if a UPS parcel medical mission is underway or not.
					if (worldScripts.ups_parcel) {
						var ups = ["MEDICIN_DELIVERY", "MEDICIN_DELIVERY_2"];
						if (checkVal === 0 && ups.indexOf(worldScripts.ups_parcel.ups_parcel) >= 0) result = false;
						if (checkVal === 1 && ups.indexOf(worldScripts.ups_parcel.ups_parcel) === -1) result = false;
					} else {
						if (checkVal === 1) result = false;
					}
					break;
				case "no_satellite_type":
					var sat = worldScripts.GalCopBB_Satellites;
					var satsys = sat._satArray[systemID];
					if (!satsys || Array.isArray(satsys) === false) {
						result = false;
					} else {
						if (items[1].indexOf("|") >= 0) {
							// can be | separated for an "or" list of types (ie. 2|3 means type 2 or type 3)
							var stypes = items[1].split("|");
						} else {
							// or a single sat type only
							var stypes = [items[1]];
						}
						var found = false;
						for (var j = 0; j < stypes.length; j++) {
							var stype = parseInt(stypes[j]);
							if (satsys.length >= stype && satsys[stype - 1] === 0) found = true;
						}
						if (found === false) result = false;
					}
					break;
				case "has_satellite_type":
					var sat = worldScripts.GalCopBB_Satellites;
					var satsys = sat._satArray[systemID];
					if (!satsys || Array.isArray(satsys) === false) {
						result = false;
					} else {
						if (items[1].indexOf("|") >= 0) {
							// can be | separated for an "or" list of types (ie. 2|3 means type 2 or type 3)
							var stypes = items[1].split("|");
						} else {
							// or a single sat type only
							var stypes = [items[1]];
						}
						var found = false;
						for (var j = 0; j < stypes.length; j++) {
							var stype = parseInt(stypes[j]);
							if (satsys.length >= stype && satsys[stype - 1] > 0) found = true;
						}
						if (found === false) result = false;
					}
					break;
				case "has_pirate_base":
					var b = worldScripts.GalCopBB_PirateBases._bases;
					var found = false;
					for (var j = 0; j < b.length; j++) {
						if (b[j].systemID === systemID) {
							found = true;
							break;
						}
					}
					if (found === false && checkVal === 1) result = false;
					if (found === true && checkVal === 0) result = false;
					break;
				case "check_equipment": // required equipment, but not shown on mission detail page
					// this is to provide options to limit mission availability to higher-speced ships, without making it obvious
					var eqList = items[1].split("|");
					for (var j = 0; j < eqList.length; j++) {
						if (eqList[j] !== "") {
							var check = true;
							if (this._equipmentProvidingLookup[eqList[j]]) {
								check = player.ship.hasEquipmentProviding(eqList[j]);
							} else {
								if (player.ship.equipmentStatus(eqList[j]) != "EQUIPMENT_OK") check = false;
							}
							if (check === false) result = check;
						}
					}
					break;
				case "min_cargo_space":
					if (player.ship.cargoSpaceCapacity < checkVal) result = false;
					break;
				case "min_score":
					if (player.score < checkVal) result = false;
					break;
				case "min_missile_slots":
					if (player.ship.missileCapacity < checkVal) result = false;
					break;
				case "min_cargo_reputation":
				case "min_passenger_reputation":
				case "min_parcel_reputation":
				case "include_equipment":
					// only perform these condition tests when in secondary mission mode
					if (isSecondary === true) {
						var test = this.$missionAvailability(-1, missionType, origSystemID);
						if (test !== "") result = false;
					}
					break;
				case "commodity_legal":
					if (this.$isCommodityIllegal(origSystemID, null, checkVal) === true) result = false;
					break;
				case "commodity_illegal":
					if (this.$isCommodityIllegal(origSystemID, null, checkVal) === false) result = false;
					break;
				case "market_has":
					if (system.mainStation && system.mainStation.market[items[1]].quantity === 0) result = false;
					break;
				case "min_oolite_ver":
					if (oolite.compareVersion(items[1]) > 0) result = false;
					break;
				case "system_position":
					// or combination list: x<4|x>200|y<10|y>200
					var sub = items[1].split("|");
					var check = false;
					for (var j = 0; j < sub.length; j++) {
						var checkCoord = 0;
						var comp = "";
						if (sub[j].indexOf("x") >= 0) checkCoord = dest.internalCoordinates.x;
						if (sub[j].indexOf("y") >= 0) checkCoord = dest.internalCoordinates.y;
						if (sub[j].indexOf("<") >= 0) {
							checkVal = parseInt(sub[j].split("<")[1]);
							comp = "<";
						}
						if (sub[j].indexOf(">") >= 0) {
							checkVal = parseInt(sub[j].split(">")[1]);
							comp = ">";
						}
						switch (comp) {
							case "<":
								if (checkCoord < checkVal) check = true;
								break;
							case ">":
								if (checkCoord > checkVal) check = true;
								break;
						}
					}
					if (check === false) result = false;
					break;
				case "max_system_x_position":
					if (dest.internalCoordinates.x > checkVal) result = false;
					break;
				case "min_system_x_position":
					if (dest.internalCoordinates.x < checkVal) result = false;
					break;
				case "max_system_y_position":
					if (dest.internalCoordinates.y > checkVal) result = false;
					break;
				case "min_system_y_position":
					if (dest.internalCoordinates.y < checkVal) result = false;
					break;
			}
		}
	}
	return result;
}

//-------------------------------------------------------------------------------------------------------------
// returns text to the BB when asked to confirm if a mission is currently available
// blank means available, otherwise the text of why the mission is unavailable
// will be called during secondary mission setup with missID = -1
this.$missionAvailability = function $missionAvailability(missID, missType, origSysID) {
	var result = "";
	// get the BB item this missID (if != -1)
	var bb = worldScripts.BulletinBoardSystem;
	var item = bb.$getItem(missID);
	// if we have an index, get the conditions from the mission itself
	// otherwise we are working in pre-creation mode, so get the conditions from the missionText
	var conditions = "";
	var sysID = -1;
	if (item) {
		conditions = expandDescription("[missionType" + item.data.missionType + "_conditions]");
		sysID = item.source;
	} else if (missType && missType != "") {
		conditions = expandDescription("[missionType" + missType + "_conditions]");
		sysID = system.ID;
	}
	var oSysID = -1;
	if (missID === -1 && origSysID && origSysID >= 0) {
		oSysID = origSysID;
	}
	if (item) oSysID = item.data.origSystemID;

	if (conditions != "") {
		var condList = conditions.split(",");

		// check for cargo space requirements
		if (this.$getConditionIndex(condList, "min_cargo_space") >= 0) {
			var idx = this.$getConditionIndex(condList, "min_cargo_space");
			var data = condList[idx].split(":");
			if (isNaN(data[1]) === false) {
				if (player.ship.cargoSpaceAvailable < parseInt(data[1])) {
					result = expandDescription("[gcm_insufficient_space]");
				}
			} else {
				if (item && data[1] === "quantity") {
					if (player.ship.cargoSpaceAvailable < item.data.targetQuantity) {
						result = expandDescription("[gcm_insufficient_space]");
					}
				}
			}
		}
		// check for min score
		if (this.$getConditionIndex(condList, "min_score") >= 0) {
			var idx = this.$getConditionIndex(condList, "min_score");
			var checkVal = this.$getConditionParameter(condList[idx], oSysID);
			if (player.score < checkVal) {
				result = expandDescription("[gcm_insufficient_experience]");
			}
		}
		// check for missile slots
		if (this.$getConditionIndex(condList, "min_missile_slots") >= 0) {
			var idx = this.$getConditionIndex(condList, "min_missile_slots");
			var checkVal = this.$getConditionParameter(condList[idx], oSysID);
			var flag = false;
			var miss = [];
			try {
				miss = player.ship.missiles;
				flag = true;
			} catch (err) {
				if (this._debug) log(this.name, "!!ERROR: " + err);
			}
			var avail = 0;
			if (player.ship.missileCapacity > 0) {
				avail = player.ship.missileCapacity - miss.length;
			}
			if (avail < checkVal) result = expandDescription("[gcm_insufficient_pylons]");
		}
		// check for equipment requirements
		if (this.$getConditionIndex(condList, "include_equipment") >= 0) {
			var idx = this.$getConditionIndex(condList, "include_equipment");
			var eqList = condList[idx].split(":")[1].split("|");
			for (var j = 0; j < eqList.length; j++) {
				if (eqList[j] !== "") {
					var check = true;
					if (this._equipmentProvidingLookup[eqList[j]]) {
						check = player.ship.hasEquipmentProviding(eqList[j]);
					} else {
						if (player.ship.equipmentStatus(eqList[j]) != "EQUIPMENT_OK") check = false;
					}
					if (check === false) result = expandDescription("[gcm_missing_equipment]");
				}
			}
		}
		// check reputations
		if (this.$getConditionIndex(condList, "min_parcel_reputation") >= 0) {
			var idx = this.$getConditionIndex(condList, "min_parcel_reputation");
			var checkVal = this.$getConditionParameter(condList[idx], oSysID);
			if (player.parcelReputation < checkVal) result = expandDescription("[gcm_insufficient_courier_rep]");
		}
		if (this.$getConditionIndex(condList, "min_cargo_reputation") >= 0) {
			var idx = this.$getConditionIndex(condList, "min_cargo_reputation");
			var checkVal = this.$getConditionParameter(condList[idx], oSysID);
			if (player.parcelReputation < checkVal) result = expandDescription("[gcm_insufficient_cargo_rep]");
		}
		if (this.$getConditionIndex(condList, "min_passenger_reputation") >= 0) {
			var idx = this.$getConditionIndex(condList, "min_passenger_reputation");
			var checkVal = this.$getConditionParameter(condList[idx], oSysID);
			if (player.parcelReputation < checkVal) result = expandDescription("[gcm_insufficient_passenger_rep]");
		}
		if (this.$getConditionIndex(condList, "min_mission_reputation") >= 0) {
			var idx = this.$getConditionIndex(condList, "min_mission_reputation");
			var checkVal = this.$getConditionParameter(condList[idx], oSysID);
			if (this.$playerMissionReputation(sysID, missType) < checkVal) result = expandDescription("[gcm_insufficient_mission_rep]");
		}
	}
	return result;
}

//-------------------------------------------------------------------------------------------------------------
// processes the condition parameter (the bit after the ":") and returns the result
this.$getConditionParameter = function $getConditionParameter(condition, origSystemID) {
	var items = condition.split(":");
	var checkVal = -1;
	if (items[1].indexOf("rand") >= 0) {
		var max = parseInt(items[1].split("_")[1]);
		checkVal = parseInt(Math.random() * (max + 1));
	}
	if (this.$isNumeric(items[1]) === true) {
		checkVal = parseInt(items[1]);
	}
	if (items[1] === "source_system" && origSystemID >= 0) checkVal = origSystemID;
	return checkVal;
}

//-------------------------------------------------------------------------------------------------------------
// returns the index of a particular condition in an array
this.$getConditionIndex = function $getConditionIndex(condList, condition) {
	for (var i = 0; i < condList.length; i++) {
		if (condList[i].indexOf(condition) >= 0) return i;
	}
	return -1;
}

//-------------------------------------------------------------------------------------------------------------
// returns true if player has a role similar to roletype, otherwise false
// cycles controls the number of times the routine will search for the given role (higher number menas more chances to find the role)
this.$checkPlayerRoles = function $checkPlayerRoles(cycles, roleType) {
	var result = false;
	var pws = player.roleWeights;

	for (var i = 1; i <= cycles; i++) {
		var checkrole = pws[parseInt(Math.random() * pws.length)];
		if (checkrole.indexOf(roleType) >= 0) result = true;
	}
	return result;
}

//-------------------------------------------------------------------------------------------------------------
// free up cargo space by selling cargo
// this routine shouldn't be called anymore, as we are controlling access to missions based on cargo space from the BB itself, using $missionAvailability
this.$freeCargoSpace = function $freeCargoSpace(qtyRequired, noSell) {
	var removed = 0;
	// sell something to fit this in
	var p = player.ship;
	var cargo = p.manifest.list;
	var mkt = player.ship.dockedStation.market;
	if (mkt == null) mkt = system.mainStation.market;
	// ignore slaves on the first pass
	for (var i = 0; i < cargo.length; i++) {
		if (cargo[i].quantity > 0 && cargo[i].unit === "t" && cargo[i].commodity !== "slaves" && (noSell === "" || cargo[i].commodity != noSell)) {
			do {
				p.manifest[cargo[i].commodity] -= 1;
				removed += 1;
				player.credits += (mkt[cargo[i].commodity].price / 10);
			} while (removed < qtyRequired && p.manifest[cargo[i].commodity] > 0);
			if (removed === qtyRequired) break;
		}
	}
	if (removed !== qtyRequired) {
		// try again, this time with slaves in the list
		if (p.manifest["slaves"] >= (qtyRequired - removed)) {
			p.manifest["slaves"] -= qtyRequired - removed;
			player.credits += (mkt["slaves"].price / 10);
		} else {
			// argh! no room for this one
			throw "!ERROR: Unable to free up cargo space in player ship!";
		}

	}
}

//-------------------------------------------------------------------------------------------------------------
// returns true if the selected commodity is illegal in the selected system, otherwise false
this.$isCommodityIllegal = function $isCommodityIllegal(sysID, station, cmdty) {
	if (station == null && player.ship.dockedStation) station = player.ship.dockedStation;
	if (station == null) station = system.mainStation;
	if (sysID === system.ID) {
		if (station.market[cmdty].legality_import > 0) return true;
	}
	if (cmdty === "firearms" || cmdty === "narcotics" || cmdty === "slaves") {
		if (this._igtInstalled === true) return true;
	}
	return false;
}
//-------------------------------------------------------------------------------------------------------------
// keeps track of last time a given mission type in a given system was accepted
this.$updateLastMissionDate = function $updateLastMissionDate(sysID, missType) {
	var done = false;
	if (this._lastMission.length > 0) {
		for (var i = 0; i < this._lastMission.length; i++) {
			if (this._lastMission[i].missionType === missType && this._lastMission[i].system === sysID) {
				this._lastMission[i].date === clock.adjustedSeconds;
				done = true;
				break;
			}
		}
	}
	if (done === false) {
		this._lastMission.push({
			missionType: missType,
			system: sysID,
			date: clock.adjustedSeconds
		});
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$updateGeneralSettings = function $updateGeneralSettings(item) {
	if (item.data.assassinChance > 0) {
		var chance = item.data.assassinChance;
		var name = item.data.name;
		// low <= 0.3
		// add possibly once
		if (chance <= 0.3) {
			if ((chance / 0.3) > Math.random()) {
				worldScripts["oolite-contracts-helpers"]._setClientName(name);
			}
		}
		// medium > 0.3, <= 0.6
		// add possibly twice
		if (chance > 0.3 && chance <= 0.6) {
			worldScripts["oolite-contracts-helpers"]._setClientName(name);
			if ((chance - 0.3) / 0.3 > Math.random()) {
				worldScripts["oolite-contracts-helpers"]._setClientName(name);
			}
		}
		// high > 0.6
		// add possibly three times
		if (chance > 0.6) {
			worldScripts["oolite-contracts-helpers"]._setClientName(name);
			worldScripts["oolite-contracts-helpers"]._setClientName(name);
			if ((chance - 0.6) / 0.4 > Math.random()) {
				worldScripts["oolite-contracts-helpers"]._setClientName(name);
			}
		}
	}

	// setup stage 0 for multi staged missions
	if (this._multiStageMissionTypes.indexOf(item.data.missionType) >= 0) {
		item.data.stage = 0;
	}
}

//-------------------------------------------------------------------------------------------------------------
// gets a random position based on a locationType value. locationType can be 0, 1, 2, 3, 4, 5, 6 or 7.
this.$getRandomPosition = function $getRandomPosition(locationType, factor, missID, zrangeMin, zrangeMax) {
	var result;
	var coord = "";
	var dist = 0;
	var z;
	var x = (factor !== 0 ? Math.floor(system.scrambledPseudoRandomNumber(missID) * factor) * (system.scrambledPseudoRandomNumber(missID + 1) > 0.5 ? 1 : -1) : 0);
	var y = (factor !== 0 ? Math.floor(system.scrambledPseudoRandomNumber(missID + 2) * factor) * (system.scrambledPseudoRandomNumber(missID + 3) > 0.5 ? 1 : -1) : 0);

	switch (locationType) {
		case 0: // on the other side of the planet from the witchpoint
			var zmin = (zrangeMin && !isNaN(zrangeMin) ? zrangeMin : 1.3);
			var zmax = (zrangeMax && !isNaN(zrangeMax) ? zrangeMax : 1.6);
			z = (system.scrambledPseudoRandomNumber(missID + 4) * (zmax - zmin)) + zmin;
			coord = "wpu";
			dist = system.mainPlanet.position.distanceTo([0, 0, 0]);
			break;
		case 1: // towards the sun from the witchpoint
			var zmin = (zrangeMin && !isNaN(zrangeMin) ? zrangeMin : 0.3);
			var zmax = (zrangeMax && !isNaN(zrangeMax) ? zrangeMax : 0.6);
			z = (system.scrambledPseudoRandomNumber(missID + 4) * (zmax - zmin)) + zmin;
			coord = "wsu";
		case 2: // towards the sun from the planet
			var zmin = (zrangeMin && !isNaN(zrangeMin) ? zrangeMin : 0.4);
			var zmax = (zrangeMax && !isNaN(zrangeMax) ? zrangeMax : 0.7);
			z = (system.scrambledPseudoRandomNumber(missID + 4) * (zmax - zmin)) + zmin;
			coord = "psu";
			break;
		case 3: // behind the witchpoint from the planet
			var zmin = (zrangeMin && !isNaN(zrangeMin) ? zrangeMin : -0.3);
			var zmax = (zrangeMax && !isNaN(zrangeMax) ? zrangeMax : -0.7);
			z = (system.scrambledPseudoRandomNumber(missID + 4) * (zmax - zmin)) + zmin;
			coord = "wpu";
			dist = system.mainPlanet.position.distanceTo([0, 0, 0]);
			break;
		case 4: // behind the witchpoint from the sun
			var zmin = (zrangeMin && !isNaN(zrangeMin) ? zrangeMin : -0.3);
			var zmax = (zrangeMax && !isNaN(zrangeMax) ? zrangeMax : -0.7);
			z = (system.scrambledPseudoRandomNumber(missID + 4) * (zmax - zmin)) + zmin;
			coord = "wsu";
			dist = system.sun.position.distanceTo([0, 0, 0]);
			break;
		case 5: // on the other side of the sun from the planet
			var zmin = (zrangeMin && !isNaN(zrangeMin) ? zrangeMin : 5);
			var zmax = (zrangeMax && !isNaN(zrangeMax) ? zrangeMax : 20);
			if (zrangeMin) {
				z = (system.scrambledPseudoRandomNumber(missID + 4) * (zmax - zmin)) + zmin;
			} else {
				var ps_dist = system.sun.position.distanceTo(system.mainPlanet);
				var far_dist = system.sun.radius * (system.scrambledPseudoRandomNumber(missID + 4) * (zmax - zmin) + zmin); // between 5 and 20 sun radii beyond
				z = (1 + far_dist / ps_dist);
			}
			coord = "psu";
			dist = system.mainPlanet.position.distanceTo(system.sun);
			break;
		case 6: // on the other side of the sun from the witchpoint
			var zmin = (zrangeMin && !isNaN(zrangeMin) ? zrangeMin : 5);
			var zmax = (zrangeMax && !isNaN(zrangeMax) ? zrangeMax : 20);
			if (zrangeMin) {
				z = (system.scrambledPseudoRandomNumber(missID + 4) * (zmax - zmin)) + zmin;
			} else {
				var ws_dist = system.sun.position.distanceTo([0, 0, 0]);
				var far_dist = system.sun.radius * (system.scrambledPseudoRandomNumber(missID + 4) * (zmax - zmin) + zmin); // between 5 and 20 sun radii beyond
				z = (1 + far_dist / ws_dist);
			}
			coord = "wsu";
			dist = system.sun.position.distanceTo([0, 0, 0]);
			break;
		case 7: // on the other side of the planet from the sun
			var zmin = (zrangeMin && !isNaN(zrangeMin) ? zrangeMin : 2);
			var zmax = (zrangeMax && !isNaN(zrangeMax) ? zrangeMax : 3);
			z = (system.scrambledPseudoRandomNumber(missID + 4) * (zmax - zmin)) + zmin;
			coord = "spu";
			dist = system.mainPlanet.position.distanceTo(system.sun);
			break;
	}

	result = Vector3D(x, y, z).fromCoordinateSystem(coord);
	return {
		position: result,
		x: x,
		y: y,
		z: z,
		coordSystem: coord,
		locType: locationType,
		checkDist: dist
	};
}

//-------------------------------------------------------------------------------------------------------------
// counts the number of instances (active or available in current system) of this particular mission type
this.$countMissions = function $countMission(missionType) {
	var count = 0;
	var list = this.$getListOfMissions(false, missionType);
	if (list.length > 0) {
		for (var i = 0; i < list.length; i++) {
			if (list[i].source === system.ID) count += 1;
		}
	}
	// check for any active missions of this type
	list = this.$getListOfMissions(true, missionType);
	count += list.length;
	return count;
}

//-------------------------------------------------------------------------------------------------------------
// are all the available missions of this type close to expiry? returns true if so, otherwise false
// note: not currently in use
this.$closeExpiry = function $closeExpiry(missionType) {
	var count = 0;
	var expiry_count = 0;
	var list = this.$getListOfMissions(false, missionType);
	for (var i = 0; i < list.length; i++) {
		if (list[i].source === system.ID) {
			count += 1;
			if ((list[i].expiry - global.clock.adjustedSeconds) < 7200) expiry_count += 1;
		}
	}
	if (count === expiry_count) return true;
	return false;
}

//-------------------------------------------------------------------------------------------------------------
// returns the number of days since the last time one of these missions was accepted in the given system
this.$lastMissionGapDays = function $lastMissionGapDays(sysID, missType) {
	if (this._lastMission.length > 0) {
		for (var i = 0; i < this._lastMission.length; i++) {
			if (this._lastMission[i].missionType === missType && this._lastMission[i].system === sysID) {
				return parseInt((clock.adjustedSeconds - this._lastMission[i].date) / 86400);
			}
		}
	}
	return 999;
}

//-------------------------------------------------------------------------------------------------------------
// adds a record of successful missions
this.$addMissionHistory = function $addMissionHistory(sysID, missType, completed, failed) {
	var found = false;
	for (var i = 0; i < this._missionHistory.length; i++) {
		if (this._missionHistory[i].galaxy == galaxyNumber &&
			this._missionHistory[i].system === sysID &&
			this._missionHistory[i].missionType === missType) {

			this._missionHistory[i].completedCount += completed;
			this._missionHistory[i].failedCount += failed;
			found = true;
		}
	}
	if (found === false) {
		this._missionHistory.push({
			galaxy: galaxyNumber,
			system: sysID,
			missionType: missType,
			completedCount: completed,
			failedCount: failed
		});
	}
}

//-------------------------------------------------------------------------------------------------------------
// returns a value between 0.05 and 2 indicating the players reputation for a given mission type in a given system
// used when calculating mission payments
this.$playerMissionReputation = function $playerMissionReputation(sysID, missType) {
	// if the reputation system is online, use it for rep calculations
	if (worldScripts.GalCopBB_Reputation._disabled === false) {
		return worldScripts.GalCopBB_Reputation.$playerMissionReputation(sysID, missType);
	}
	// otherwise use the basic calculation here.
	var result = 1;
	var recCount = 0;
	var subFactor = 0;
	var complete = 0;
	for (var i = 0; i < this._missionHistory.length; i++) {
		if ((sysID < 0 || (this._missionHistory[i].galaxy === galaxyNumber && this._missionHistory[i].system === sysID)) && this._missionHistory[i].missionType === missType) {
			var complete = this._missionHistory[i].completed;
			subFactor = this._missionHistory[i].completed - (this._missionHistory[i].failed * 1.5)
			recCount = this._missionHistory[i].completed + this._missionHistory[i].failed;
		}
	}
	if (recCount > 0) result = (subFactor / recCount) + (1 - Math.sqrt(Math.pow((complete / recCount), 2)));
	if (result < 0) result = Math.abs(result) / 10;
	return result;
}

//-------------------------------------------------------------------------------------------------------------
// returns the number of successfully completed missions of a particular type in a particular system
this.$getSuccessMissions = function $getSuccessMissions(sysID, missType) {
	var result = 0;
	for (var i = 0; i < this._missionHistory.length; i++) {
		if (this._missionHistory[i].galaxy === galaxyNumber && this._missionHistory[i].system === sysID && this._missionHistory[i].missionType === missType) {
			result = this._missionHistory[i].completed;
			break;
		}
	}
	return result;
}

//-------------------------------------------------------------------------------------------------------------
// returns the number of failed missions of a particular type in a particular system
this.$getFailedMissions = function $getFailedMissions(sysID, missType) {
	var result = 0;
	for (var i = 0; i < this._missionHistory.length; i++) {
		if (this._missionHistory[i].galaxy === galaxyNumber && this._missionHistory[i].system === sysID && this._missionHistory[i].missionType === missType) {
			result = this._missionHistory[i].failed;
			break;
		}
	}
	return result;
}

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

//-------------------------------------------------------------------------------------------------------------
// returns the player's target system (1.80) or the next jump to their target system (1.82)
this.$playerTargetSystem = function $playerTargetSystem() {
	if (player.ship.hasOwnProperty("nextSystem")) return player.ship.nextSystem;

	var p = player.ship;
	var target = p.targetSystem;
	if (oolite.compareVersion("1.81") < 0 && p.hasEquipmentProviding("EQ_ADVANCED_NAVIGATIONAL_ARRAY") === true) {
		// in 1.81 or greater, the target system could be more than 7 ly away. It becomes, essentially, the final destination.
		// there could be multiple interim stop points between the current system and the target system.
		// the only way to get this info is to recreate a route using the same logic as entered on the ANA, and pick item 1
		// from the list. That should be the next destination in the list.
		var myRoute = System.infoForSystem(galaxyNumber, this._lastSource).routeToSystem(System.infoForSystem(galaxyNumber, target), p.routeMode);
		if (myRoute) {
			target = myRoute.route[1];
		}
	}
	return target;
}

//-------------------------------------------------------------------------------------------------------------
// returns the index of the first active, non-expired mission with a particular type
this.$getActiveMissionIDByType = function $getActiveMissionIDByType(missType) {
	var list = this.$getListOfMissions(true, missType);
	if (list.length > 0) return list[0].ID;
	return -1;
}

//-------------------------------------------------------------------------------------------------------------
// writes some mission details to the log
this.$logMissionData = function $logMissionData(missID) {
	if (this._debug) {
		var bb = worldScripts.BulletinBoardSystem;
		log(this.name, "missionID " + missID);
		var item = bb.$getItem(missID);
		log(this.name, "target " + item.data.targetQuantity);
		log(this.name, "quantity " + item.data.quantity);
		log(this.name, "expiry " + clock.clockStringForTime(item.expiry));
	}
}

//=============================================================================================================
// spawned ship event handlers
//-------------------------------------------------------------------------------------------------------------
this.$gcm_cargo_shipDied = function $gcm_cargo_shipDied(whom, why) {
	if (this.ship.script.$gcm_hold_shipDied) this.ship.script.$gcm_hold_shipDied(whom, why);

	if (this._debug) log(this.name, "!!OUCH! Special cargo destroyed " + why + ", " + whom);

	var bb = worldScripts.BulletinBoardSystem;
	var item = bb.$getItem(this.ship.script._missionID);
	if (item) {
		item.data.destroyedQuantity += 1;
		if (whom.isPlayer) item.payment = parseInt((item.payment * 0.9) * 10) / 10; // 10% penalty for loss cause by the player
		bb.$updateBBMissionPercentage(item.ID, (item.data.quantity / (item.data.targetQuantity - item.data.destroyedQuantity)));
		player.consoleMessage(expandDescription("[goal_updated]"));
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$gcm_garbage_shipDied = function $gcm_garbage_shipDied(whom, why) {
	if (this.ship.script.$gcm_hold_shipDied) this.ship.script.$gcm_hold_shipDied(whom, why);
	var gcm = worldScripts.GalCopBB_Missions;
	var bb = worldScripts.BulletinBoardSystem;
	var item = bb.$getItem(this.ship.script._missionID);
	if (why === "heat damage") {
		// only increase the mission quantity if we're below the target (minus any that have been destroyed in other ways)
		if (item && item.data.quantity < (item.data.targetQuantity - item.data.destroyedQuantity)) {
			item.data.quantity += 1;
			bb.$updateBBMissionPercentage(item.ID, item.data.quantity / item.data.targetQuantity);
			gcm.$logMissionData(item.ID);
			player.consoleMessage(expandDescription("[goal_updated]"));
		}
	} else {
		// a little surprise...
		this.ship.becomeCascadeExplosion();
		if (item) {
			item.data.destroyedQuantity += 1;
			bb.$updateBBMissionPercentage(item.ID, item.data.quantity / item.data.targetQuantity);
			player.consoleMessage(expandDescription("[goal_updated]"));
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$gcm_entity_shipDied = function $gcm_entity_shipDied(whom, why) {
	var gcm = worldScripts.GalCopBB_Missions;
	var bb = worldScripts.BulletinBoardSystem;

	if (gcm._debug) log(this.name, "running shipDied for " + this.ship + ": reason " + why + ", " + whom);
	if (this.ship.script.$gcm_hold_shipDied) this.ship.script.$gcm_hold_shipDied(whom, why);

	var item = bb.$getItem(this.ship.script._missionID);
	if (item) {
		item.data.destroyedQuantity = 1;
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$gcm_monitorThargoid_shipDied = function $gcm_monitorThargoid_shipDied(whom, why) {
	if (this.ship.script.$gcm_hold_shipDied) this.ship.script.$gcm_hold_shipDied(whom, why);
	//log(this.name, "adding dead thargoid position to array - " + this.ship.position);
	//log(this.name, "Adding to dead thargoids " + this.ship.dataKey);
	var gm = worldScripts.GalCopBB_Missions;
	gm._thargoidPosition.push(this.ship.position);

	var t = new Timer(gm, gm.$removePositionFromArray.bind(gm, this.ship.position), 5, 0);
	gm._removeTimers.push(t);
}

//-------------------------------------------------------------------------------------------------------------
this.$removePositionFromArray = function $removePositionFromArray(pos) {
	var i = this._thargoidPosition.length;
	while (i--) {
		if (this._thargoidPosition[i].distanceTo(pos) == 0) {
			//log(this.name, "removing position " + pos);
			this._thargoidPosition.splice(i, 1);
			break;
		}
	}
}

//=============================================================================================================
// mission-specific functions
//-------------------------------------------------------------------------------------------------------------
this.$postScoopMission = function $postScoopMission() {
	var id = this.$createSecondaryMission(this._postScoopMissionID, true);
}

//-------------------------------------------------------------------------------------------------------------
// checks to see if the special computers have been removed from the ship
this.$checkSpecialDeliveryRemoved = function $checkSpecialDeliveryRemoved() {
	var itemCount = 0;
	var list = this.$getListOfMissions(true, 42);
	for (var i = 0; i < list.length; i++) {
		if (list[i].data.quantity === 0 && list[i].data.destroyedQuantity === 0) itemCount += 1;
	}
	if (itemCount > 0 && itemCount > player.ship.manifest["computers"]) {
		var id = this.$getActiveMissionIDByType(42);
		if (id >= 0) {
			var bb = worldScripts.BulletinBoardSystem;
			var item = bb.$getItem(id);
			item.data.destroyedQuantity = 1;
			item.data.quantity = 0;
			bb.$updateBBMissionPercentage(item.ID, 0);
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
// returns the number of thargoid alloys that have been collected
this.$countThargoidAlloys = function $countThargoidAlloys() {
	var qty = 0;
	var list = this.$getListOfMissions(true, 13);
	for (var i = 0; i < list.length; i++) {
		if (list[i].data.quantity > 0) qty += list[i].data.quantity;
	}
	return qty;
}

//-------------------------------------------------------------------------------------------------------------
// remove waypoints of a particular type that have been added to the system
this.$removeSpecialWaypoint = function $removeSpecialWaypoint(wp_type) {
	var wp = this._waypointList;
	var found = false;

	if (wp.length > 0) {
		for (var i = wp.length - 1; i >= 0; i--) {
			if (wp[i].indexOf(this.name + wp_type) >= 0 && wp[i].indexOf(this.name + "_cache") === -1) {
				this.$unsetWaypoint(wp[i].replace(this.name + "_", ""));
				found = true;
			}
			if (wp[i] && wp[i].indexOf(this.name + "_cache") >= 0) {
				this.$unsetWaypoint(wp[i].replace(this.name + "_", ""));
			}
		}
	}

	return found;
}

//-------------------------------------------------------------------------------------------------------------
this.$transmitFreeSlaves = function $transmitFreeSlaves() {
	if (player.ship.target && player.ship.target.isValid && player.ship.position.distanceTo(player.ship.target) < player.ship.scannerRange) {
		this._slaveDemandHistory.push(player.ship.target);
		// work out the response
		var resptype = 0;
		var qty = 0;
		// option 2: they don't have any slaves (true)
		// option 3: they don't have any slaves (false ie a lie)
		if (resptype === 0) {
			var haveslaves = false;
			for (var i = 0; i < player.ship.target.cargoList.length; i++) {
				var itm = player.ship.target.cargoList[i];
				if (itm.commodity === "slaves") {
					haveslaves = true;
					qty = itm.quantity;
				}
			}
			if (haveslaves === true) {
				if (Ship.roleIsInCategory(player.ship.target.primaryRole, "oolite-pirate")) {
					// they're going to lie
					if (Math.random() > 0.7) resptype = 3;
				}
			} else {
				// they don't have any
				resptype = 2;
			}
		}
		// option 1: they free the slaves (if the player is a bigger threat or they're fleeing)
		if (resptype === 0 && (player.ship.target.threatAssessment() < player.ship.threatAssessment() || player.ship.target.isFleeing === true)) resptype = 1;
		// option 4: they ignore the player or tell them to get lost
		if (resptype === 0) resptype = 4 + (Math.random() > 0.6 ? 1 : 0);

		// send the message
		if (resptype > 0 && resptype < 5) {
			var msg = expandDescription("[gcm_freeslaves_response_type_" + resptype + "]");
			player.ship.target.commsMessage(msg, player.ship);
		}

		if (resptype === 1) {
			// eject all slaves
			player.ship.target.dumpCargo(qty, "slaves");
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$checkForSlaveMission = function $checkForSlaveMission(sysID) {
	this._slaveRescue = false;
	// check for a type 12 mission here - saves having to do these checks every time we target a ship
	var list = this.$getListOfMissions(true, 12);
	for (var i = 0; i < list.length; i++) {
		if (list[i].destination === sysID &&
			list[i].data.quantity < list[i].data.targetQuantity &&
			list[i].expiry > clock.adjustedSeconds) {
			this._slaveRescue = true;
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$addSlaveCommsMessage = function $addSlaveCommsMessage() {
	if (this._slaveRescue === true) {
		// add special message to broadcast comms
		var bcc = worldScripts.BroadcastCommsMFD;
		if (bcc.$checkMessageExists("gcm_demand_slaves") === false) {
			bcc.$createMessage({
				messageName: "gcm_demand_slaves",
				callbackFunction: this.$transmitFreeSlaves.bind(this),
				displayText: expandDescription("[gcm_demand_slaves]"),
				messageText: expandDescription("[gcm_demand_slaves_message]"),
				transmissionType: "target",
				deleteOnTransmit: false,
				delayCallback: 5,
				hideOnConditionRed: false
			});
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$removeSlaveCommsMessage = function $removeSlaveCommsMessage() {
	if (this._slaveRescue === true) {
		// remove special message to broadcast comms
		var bcc = worldScripts.BroadcastCommsMFD;
		if (bcc.$checkMessageExists("gcm_demand_slaves") === true) bcc.$removeMessage("gcm_demand_slaves");
	}
	this._slaveRescue = false;
}

//-------------------------------------------------------------------------------------------------------------
// checks if a ship is within spawning range of a dead thargoid
this.$shipNearDeadThargoid = function $shipNearDeadThargoid(ship) {
	if (this._thargoidPosition.length === 0) return false;
	for (var i = 0; i < this._thargoidPosition.length; i++) {
		//log(this.name, "distance to deadThargoid = " + ship.position.distanceTo(this._thargoidPosition[i]));
		if (ship.position.distanceTo(this._thargoidPosition[i]) < 2000) return true;
	}
	return false;
}

//-------------------------------------------------------------------------------------------------------------
// populates an array with ship data keys for use by the populator routines
this.$getPreferredShipList = function $getPreferredShipList() {
	// we've had instances where occassionally a ship won't be spawned in the destination system
	// I suspect a ship script has a condition that I'm not catering for.
	// to work around the issue, the following list of ship types are known to work OK, so in future
	// only these types will be selected
	var safe = expandDescription("[gcm_safe_ships]").split("|");

	this._preferredTargetShips.length = 0;
	var shipkeys = Ship.keysForRole("trader");
	for (var i = 0; i < shipkeys.length; i++) {
		var shipspec = Ship.shipDataForKey(shipkeys[i]);
		if (safe.indexOf(shipspec.name) >= 0 && shipspec.max_flight_speed > 150 && shipspec.max_cargo > 15 && (typeof shipspec["hyperspace_motor"] == "undefined" || this._trueValues.indexOf(shipspec.hyperspace_motor) >= 0)) {
			this._preferredTargetShips.push({
				key: shipkeys[i],
				cargoSpace: shipspec.max_cargo
			});
			if (shipspec.max_cargo > this._maxCargoOfShips) this._maxCargoOfShips = shipspec.max_cargo;
		}
	}
}

//--------------------------------------------------------------------------------------------------------------
// populates the list of possible cargopods that can be spawned for missions
this.$getPreferredCargoPodList = function $getPreferredCargoPodList() {
	this._preferredCargoPods.length = 0;
	var shipkeys = Ship.keysForRole("cargopod");
	for (var i = 0; i < shipkeys.length; i++) {
		var shipspec = Ship.shipDataForKey(shipkeys[i]);
		if (shipspec.cargo_type != "CARGO_SCRIPTED_ITEM") this._preferredCargoPods.push(shipkeys[i]);
	}
}

//-------------------------------------------------------------------------------------------------------------
// returns a random ship key from the list
this.$getRandomShipKey = function $getRandomShipKey(missID, minCargo) {
	var result = "";
	var tries = 0;
	do {
		var dta = this._preferredTargetShips[parseInt(system.scrambledPseudoRandomNumber(missID) * this._preferredTargetShips.length)];
		if (dta.cargoSpace > minCargo) result = dta.key;
		tries += 1;
	} while (result === "" && tries < 100);
	// if we tried 100 times and still didn't get a result, use the player's ship dataKey 
	if (result === "") result = player.ship.dataKey;
	return result;
}

//--------------------------------------------------------------------------------------------------------------
// converts player's elite ranking into a number
this.$playerRank = function $playerRank() {
	var p = player;
	if (p.score < 8) return 0; // harmless
	if (p.score >= 8 && p.score < 16) return 1; // mostly harmless
	if (p.score >= 16 && p.score < 32) return 2; //poor
	if (p.score >= 32 && p.score < 64) return 3; //average
	if (p.score >= 64 && p.score < 128) return 4; // above average
	if (p.score >= 128 && p.score < 512) return 5; // competant
	if (p.score >= 512 && p.score < 2560) return 6; // dangerous
	if (p.score >= 2560 && p.score < 6400) return 7; // deadly
	if (p.score >= 6400) return 8; // elite
}

//--------------------------------------------------------------------------------------------------------------
// returns true if the passed value is numeric, otherwise false
this.$isNumeric = function $isNumeric(n) {
	return !isNaN(parseFloat(n)) && isFinite(n);
}

//--------------------------------------------------------------------------------------------------------------
// returns the data key of a reasonably high powered ship
this.$pickGetawayShip = function $pickGetawayShip() {
	var shipkeys = Ship.keysForRole("assassin-medium");

	if (shipkeys.length === 0) return "cobra3-pirate";
	return shipkeys[Math.floor(Math.random() * shipkeys.length)];
}

//--------------------------------------------------------------------------------------------------------------
// function used by Ship Configuration to determine if any cargo should not be allowed to be transferred to storage
this.$cargoRestricted = function $cargoRestricted(commodity) {
	var qty = 0;
	var list = this.$getListOfMissions(true, 13);
	for (var i = 0; i < list.length; i++) {
		// look for any thargoid alloys with a type 13 mission
		if (commodity === "alloys" && list[i].data.quantity > 0) qty += list[i].data.quantity;
	}
	return qty;
}

//-------------------------------------------------------------------------------------------------------------
// play sound effects
this.$playSound = function $playSound(soundtype) {
	var mySound = new SoundSource;

	switch (soundtype) {
		case "mode":
			mySound.sound = "[@click]";
			break;
		case "activate":
			mySound.sound = "[@beep]";
			break;
		case "stop":
			mySound.sound = "[@boop]";
			break;
		case "leak":
			mySound.sound = "[fuel-leak]";
			break;
	}
	mySound.loop = false;
	mySound.play();
}

//-------------------------------------------------------------------------------------------------------------
// returns a list of missions
// accepted (boolean) indicating whether only accepted (true) or available (false) missions should be returned
// missType can be an integer, or an array of integers, indicating what types of missions should be returned
this.$getListOfMissions = function $getListOfMissions(accepted, missType) {
	var list = [];
	var bb = worldScripts.BulletinBoardSystem;
	for (var i = 0; i < bb._data.length; i++) {
		var item = bb._data[i];
		if (item.data && item.data.hasOwnProperty("source") && item.data.source === this.name) {
			if (!accepted || item.accepted === accepted) {
				if (!missType) {
					list.push(item);
				} else {
					if (Array.isArray(missType) === false) {
						if (item.data.missionType === missType) list.push(item);
					} else {
						if (missType.indexOf(item.data.missionType) >= 0) list.push(item);
					}
				}
			}
		}
	}
	return list;
}

//-------------------------------------------------------------------------------------------------------------
// cleans up any old records that were created before the "data" element was added to the BB. 
this.$validateActiveData = function $validateActiveData() {
	var bb = worldScripts.BulletinBoardSystem;
	for (var i = bb._data.length - 1; i >= 0; i--) {
		if (bb._data[i].worldScript === this.name &&
			(bb._data[i].hasOwnProperty("data") === false || bb._data[i].data === "")) {
			bb._data.splice(i, 1);
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
// function called by system populator to add special assassins to a system.
// these assassins are only in search of the player and will only appear at the witchpoint.
// although if they can't find the player they will revert to doing what assassins do.
// this is largely a copy of the _addAssassin routine in the oolite populator.
// the only real change is that we are forcing our new AI on these ships
this.$addGCMAssassin = function $addGCMAssassin(pos) {
	var op = worldScripts["oolite-populator"];

	var role = "assassin-light";
	var extra = 0;
	var ws = 2;
	var g = system.info.government + 2;
	if (Math.random() > g / 10) {
		role = "assassin-medium";
		extra = 1;
		ws = 2.5;
		if (Math.random() > g / 5) {
			role = "assassin-heavy";
			ws = 2.8;
		}
	}
	if (!op._roleExists(role)) {
		log(this.name, "No ships with role " + role + " defined - skipping addition");
		return;
	}
	var main = op._addShips(role, 1, pos, 0)[0];
	if (main.autoWeapons) {
		main.awardEquipment("EQ_FUEL_INJECTION");
		main.awardEquipment("EQ_ECM");
		if (2 + Math.random() < ws) {
			main.awardEquipment("EQ_SHIELD_BOOSTER");
		}
		// assassins don't respect escape pods and won't expect anyone else to either.
		main.removeEquipment("EQ_ESCAPE_POD");
		main.fuel = 7;
		op._setWeapons(main, ws);
		op._setSkill(main, extra);
	}
	// make sure the AI is switched
	main.switchAI("gcm-assassinAI.js");

	if (extra > 0) {
		var g = new ShipGroup("assassin group", main);
		main.group = g;
		var numext = Math.floor(Math.random() * 3) + 1;
		if (role === "assassin-heavy") {
			var extras = op._addShips("assassin-medium", numext, pos, 3E3);
		} else {
			var extras = op._addShips("assassin-light", numext, pos, 3E3);
		}
		for (var i = 0; i < numext; i++) {
			extras[i].group = g;
			g.addShip(extras[i]);
			if (extras[i].autoWeapons) {
				extras[i].awardEquipment("EQ_FUEL_INJECTION");
				extras[i].removeEquipment("EQ_ESCAPE_POD");
				extras[i].fuel = 7;
				op._setWeapons(extras[i], 1.8);
			}
			// make sure the AI is switched
			extras[i].switchAI("gcm-assassinAI.js");
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$postScoopMissionCreation = function $postScoopMissionCreation(missID, delay) {
	this._postScoopMissionID = missID;
	this._newMissionDelayTimer = new Timer(this, this.$postScoopMission, delay, 0);
}

//-------------------------------------------------------------------------------------------------------------
this.$getCommodityUnit = function $getCommodityUnit(commodity) {
	if (commodity == "") return "";
	return this._commodityUnit[commodity];
}

//-------------------------------------------------------------------------------------------------------------
// translates a text-based guiTextureIdentifier from descriptions.plist into a proper guiTextureIdentifier
// format is:  filename.png~height=n~width=n
this.$getTexture = function $getTexture(data) {
	if (data === "") return "";
	var items = data.split("~");
	var height = "";
	var width = "";
	if (items.length > 1) {
		if (items[1].indexOf("height") >= 0) height = items[1].split("=")[1];
		if (items[1].indexOf("width") >= 0) width = items[1].split("=")[1];
	}
	if (items.length > 2) {
		if (items[2].indexOf("height") >= 0) height = items[2].split("=")[1];
		if (items[2].indexOf("width") >= 0) width = items[2].split("=")[1];
	}
	if (height === "" && width === "") {
		return items[0];
	}
	if (height === "" && width !== "") {
		return {
			name: items[0],
			width: width
		};
	}
	if (height !== "" && width === "") {
		return {
			name: items[0],
			height: height
		};
	}
	if (height !== "" && width !== "") {
		return {
			name: items[0],
			height: height,
			width: width
		};
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$modelIsAllowed = 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, 6) === "mission") {
				if (missionVariables[cond[offset + 2].replace("mission_", "")]) {
					checking = missionVariables[cond[offset + 2].replace("mission_", "")];
					log(this.name, "field = " + cond[offset + 2] + ", value = " + checking);
				} else {
					log(this.name, "!!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;
					default:
						log(this.name, "!!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(this.name, "!!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(this.name, "!!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);
		if (tempalloy) {
			tempalloy[0].setScript(shipdata.condition_script);
			include = tempalloy[0].script.allowSpawnShip(key);
			tempalloy[0].remove(true);
		}
	} else {
		// otherwise we're free to play
		include = true;
	}
	return include;
}

//-------------------------------------------------------------------------------------------------------------
// routine to check the combat simulator worldscript, to see if it's running or not
this.$simulatorRunning = function $simulatorRunning() {
	var w = worldScripts["Combat Simulator"];
	if (w && w.$checkFight && w.$checkFight.isRunning) return true;
	return false;
}

//-------------------------------------------------------------------------------------------------------------
this.$updateSuccessHistoryReputation = function $updateSuccessHistoryReputation(item) {
	// update mission history
	this.$addMissionHistory(item.source, item.data.missionType, 1, 0);

	this.$checkForFollowupMission(item);

	// adjust reputation with entities
	var rep = worldScripts.GalCopBB_Reputation;
	if (rep._disabled === false) {
		rep.$adjustReputationSuccess(item.data.missionType, item.source, item.destination, item.percentComplete);
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$updateFailedHistoryReputation = function $updateFailedHistoryReputation(item) {
	// update mission history
	this.$addMissionHistory(item.source, item.data.missionType, 0, 1);
	// adjust reputation with entities
	// reputation only goes in one way (up or down)
	// so even if the player completes 90% of a mission and then terminates it, their reputation will go down even if they received most of the payment credits
	var rep = worldScripts.GalCopBB_Reputation;
	if (rep._disabled === false) {
		rep.$adjustReputationFailure(item.data.missionType, item.source, item.destination, item.percentComplete);
	}

}

//-------------------------------------------------------------------------------------------------------------
// finds a spawn position just outside scanner range in a view other than the player's current view
this.$findPosition = function $findPosition(altPos) {
	var p = player.ship;
	// limit is just outside scanner range
	var limit = p.scannerRange + 1000;
	var pos = p.position;
	if (altPos) {
		var pl = altPos;
	} else {
		var pl = system.mainPlanet.position;
	}
	if (p.viewDirection === "VIEW_CUSTOM" || p.viewDirection === "VIEW_GUI_DISPLAY") return null;
	var done = false;
	var redo = true;
	do {
		do {
			var dir = Vector3D.randomDirection(limit);
			var spwn = pos.add(dir);
			var dev1 = 0;
			var dev2 = 0;
			switch (p.viewDirection) {
				case "VIEW_FORWARD":
					dev1 = p.vectorForward.angleTo(spwn.subtract(pos));
					dev2 = p.vectorUp.angleTo(spwn.subtract(pos));
					break;
				case "VIEW_GUI_DISPLAY":
				case "VIEW_AFT":
					dev1 = p.vectorForward.angleTo(pos.subtract(spwn));
					dev2 = p.vectorUp.angleTo(pos.subtract(spwn));
					break;
				case "VIEW_PORT":
					dev1 = p.vectorRight.angleTo(pos.subtract(spwn));
					dev2 = p.vectorUp.angleTo(pos.subtract(spwn));
					break;
				case "VIEW_STARBOARD":
					dev1 = p.vectorRight.angleTo(spwn.subtract(pos));
					dev2 = p.vectorUp.angleTo(pos.subtract(spwn));
					break;
			}
			if (dev1 > 0.8 && dev2 > 1.0) done = true;
		} while (done === false);
		if (pl.distanceTo(spwn) > pl.distanceTo(pos)) redo = false;
	} while (redo === true);
	return spwn;
}

//-------------------------------------------------------------------------------------------------------------
// creates a mission of a specific type, ignoring any mission conditions
this.$testMissionType = function $testMissionType(missionType) {
	this._forceCreate.push(missionType);
	this.$addLocalMissions([missionType], "");
}

//-------------------------------------------------------------------------------------------------------------
// outputs all data for a particular mission
this.$logMissionDataEnhanced = function $logMissionDataEnhanced(missID, header) {
	var bb = worldScripts.BulletinBoardSystem;
	log(this.name, "==========================");
	if (header && header != "") log(this.name, "=== " + header + "===");
	var item = bb.$getItem(missID);
	this.$startLoggingProcess(item, 0);
	log(this.name, "==========================");
}

//-------------------------------------------------------------------------------------------------------------
this.$startLoggingProcess = function $startLoggingProcess(item, indent) {
	var keys = Object.keys(item);
	for (var i = 0; i < keys.length; i++) {
		var vl = item[keys[i]];
		this.$logDetail(keys[i], vl, indent);
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$logDetail = function $logDetail(key, vl, indent) {
	var typ = typeof vl;
	var pads = "......";
	switch (typ) {
		case "object":
			if (Array.isArray(vl)) {
				log(this.name, pads.substring(0, indent) + key + " (array): " + JSON.stringify(vl));
			} else if (vl && vl.constructor == Object) {
				log(this.name, pads.substring(0, indent) + key + " (dictionary):");
				this.$startLoggingProcess(vl, indent + 1);
			}
			break;
		case "string":
			log(this.name, pads.substring(0, indent) + key + ": " + "\"" + vl + "\"");
			break;
		default:
			log(this.name, pads.substring(0, indent) + key + ": " + vl);
	}
}

//-------------------------------------------------------------------------------------------------------------
// appends space to currentText to the specified length in 'em'
this.$padTextRight = function $padTextRight(currentText, desiredLength, leftSwitch) {
    if (currentText == null) currentText = "";
    var space = " ";
    var hairSpace = String.fromCharCode(31);
    var ellip = "…";
    var currentLength = defaultFont.measureString(currentText.replace(/%%/g, "%"));
    var hairSpaceLength = defaultFont.measureString(hairSpace);
    var spaceLength = defaultFont.measureString(space);
    // calculate number needed to fill remaining length
    var spacesNeeded = parseInt(Math.floor((desiredLength - currentLength) / spaceLength));
    if (spacesNeeded < 0) {
        spacesNeeded = 0;
    } else {
        if (!leftSwitch || leftSwitch === false) {
            currentText += new Array(spacesNeeded).join(space);
        } else {
            currentText = new Array(spacesNeeded).join(space) + currentText;
        }
        currentLength = defaultFont.measureString(currentText.replace(/%%/g, "%"));
    }
    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.replace(/%%/g, "%")) > desiredLength);
        currentLength = defaultFont.measureString(tmp.replace(/%%/g, "%"));
        padsNeeded = Math.floor((desiredLength - currentLength) / hairSpaceLength);
        currentText = tmp;
    }
    // quick way of generating a repeated string of that number
    if (!leftSwitch || leftSwitch === false) {
        return currentText + new Array(padsNeeded).join(hairSpace);
    } else {
        return new Array(padsNeeded).join(hairSpace) + currentText;
    }
}

//-------------------------------------------------------------------------------------------------------------
// appends space to currentText to the specified length in 'em'
this.$padTextLeft = function $padTextLeft(currentText, desiredLength) {
	return this.$padTextRight(currentText, desiredLength, true);
}

//-------------------------------------------------------------------------------------------------------------
// 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;
}
