"use strict";
this.name = "GalCopBB_Delivery";
this.author = "phkb";
this.copyright = "2017 phkb";
this.description = "Controls all delivery missions (40-46)";
this.license = "CC BY-NC-SA 4.0";

/*
	Variation for 40 and 41 (part2): after two thirds or three quarters of the cargo has been delivered, 
	spawn a pirate group at 27000 from player (above or below), and give them a lurk position of the player (or the delivery target)
	Then, when they get in range, have some specific comms from the delivery target to indicate these ships are after them
	like "Oh no! They've found me!"
	Also have some specific comms from the pirates, like "There he is, get him!" (like an assassin)
	Pirate group size will be related to Elite ranking, from 2 to a max of 8 ships

	Process
		check if ship is under attack
			switch to trader AI for fight or flee
		if fleeing, dump a part payment before jumping
		if fighting, monitor when it's safe again, and switch back to scavenger AI to collect the rest of the drop

	TODO: add occasional pirate incursions at waypoints for type 46
*/

this._debug = false;
this._maxLocations = 0;
this._strickenFixTimer = null; // timer to control when stricken ships are fixed
this._delayToReceiveFixTimer = null; // timer to control the delay between receiving the fix, and actually being fixed.
this._strickenShip = null; // holding object, containing the stricken ship the player is trying to rescue
this._fixCargo = null; // holding object, containing the cargo canister ejected by the player
this._strickenScoopTimer = null;
this._deliverShip = null; // reference to the ship we are in the process of delivering cargo to
this._deliverCargo = []; // array of cargo items for the target ship to scoop
this._deliverShipPointCounter = 0;
this._deliverShipLastSpeed = 0;
this._deliverShipLastDirection = "";
this._deliverShipDataKey = "";
this._deliverShipName = "";
this._deliverShipPersonality = -1;
this._checkPlayerNearbyTimer = null; // timer to check if the player is nearby
this._waypointMissionID = 0;
this._badCargoTimer = null;
this._setData = []; // hold value for populator routine
this._requestSpecialCargo = ""; // text of commodity, indicating that the player is due to receive 1t of commodity upon docking at GalCop station
this._simulator = false;
this._monitorCargoDump = null;
this._monitorCargo = [];
this._attackers = [];
this._setupAttackerTarget = null;
this._ambushTimer = null;

//-------------------------------------------------------------------------------------------------------------
this.startUp = function () {
	var gcm = worldScripts.GalCopBB_Missions;
	// add these mission types into the main control
	var list = [40, 41, 42, 43, 44, 46];
	gcm._availableMissionTypes = gcm._availableMissionTypes.concat(list);
	gcm._multiStageMissionTypes.push(41);
	gcm._multiStageMissionTypes.push(46);
	gcm._interstellarMissionTypes.push(44);
	// position 7 is not used in any of these mission types, so limit the possibilities 
	this._maxLocations = gcm._positions.length - 1;
	this._debug = gcm._debug;
	// load any stored values
	if (missionVariables.GalCopBBMissions_DeliverShipKey) this._deliverShipDataKey = missionVariables.GalCopBBMissions_DeliverShipKey;
	if (missionVariables.GalCopBBMissions_DeliverShipName) this._deliverShipName = missionVariables.GalCopBBMissions_DeliverShipName;
	if (missionVariables.GalCopBBMissions_DeliverShipPersonality) this._deliverShipPersonality = missionVariables.GalCopBBMissions_DeliverShipPersonality;
}

//-------------------------------------------------------------------------------------------------------------
this.playerWillSaveGame = function () {
	// save any data we currently have
	if (this._deliverShip != null) {
		missionVariables.GalCopBBMissions_DeliverShipKey = this._deliverShip.dataKey;
		missionVariables.GalCopBBMissions_DeliverShipName = this._deliverShip.name;
		missionVariables.GalCopBBMissions_DeliverShipPersonality = this._deliverShip.entityPersonality;
	}
}

//-------------------------------------------------------------------------------------------------------------
this.systemWillPopulate = function () {
	var gcm = worldScripts.GalCopBB_Missions;
	var list = gcm.$getListOfMissions(true, [40, 41, 42, 43, 46]);

	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++) {
			var item = list[i];

			// *** type 40/41 - special delivery - cargo to ship at waypoint
			if ((item.data.missionType === 40 || (item.data.missionType === 41 && item.data.stage === 1)) &&
				item.destination === system.ID &&
				item.data.quantity === 0 &&
				item.data.destroyedQuantity === 0 &&
				item.expiry > clock.adjustedSeconds) {

				this._setData.push({
					missionType: 4041,
					missionID: item.ID,
					source: item.source,
					goons: 0,
					target: item.data.targetQuantity,
					destroyed: 0,
					quantity: 0
				});
				//var mypos = Vector3D(0, 0, -0.1).fromCoordinateSystem("wpu"); // on the other side of the planet from the witchpoint

				system.setPopulator("gcm-delivery-" + item.ID, {
					callback: function (pos) {
						var g = worldScripts.GalCopBB_Missions;
						var gcd = worldScripts.GalCopBB_Delivery;
						var gns = worldScripts.GalCopBB_GoonSquads;
						var missData = gcd.$getMissionData(4041);

						// 3 possibilities:
						// 1 - ship is there, ready to receive (types, 1,2,3,4,5,7,9,10,11,12,13,14,15)
						// 2 - it's a trap! player is attacked by pirates (type 6)
						// 3 - no ship at all - mission is a bust (type 8)
						var type = parseInt(Math.random() * 15) + 1;
						//var type = 1;
						switch (type) {
							case 1:
							case 2:
							case 3:
							case 4:
							case 5:
							case 7:
							case 9:
							case 10:
							case 11:
							case 12:
							case 13:
							case 14:
							case 15:
								// create the ship, using saved specs if present
								var key = (gcd._deliverShipDataKey != "" ? gcd._deliverShipDataKey : g.$getRandomShipKey(parseInt(missData.missionID), missData.target));
								var nam = (gcd._deliverShipName != "" ? gcd._deliverShipName : g.$getRandomShipName(tgt, "trader"));
								var tgt = null;
								var checkShips = null;
								for (var j = 1; j <= 5; j++) {
									checkShips = system.addShips("[" + key + "]", 1, pos, player.ship.scannerRange * 0.75);
									if (checkShips) tgt = checkShips[0];
									if (tgt) break;
								}
								if (tgt) {
									if (g._rsnInstalled) tgt.shipUniqueName = nam;
									if (gcd._deliverShipPersonality != -1) tgt.entityPersonality = gcd._deliverShipPersonality;

									tgt.bounty = 0;
									// set script to default, to avoid a special script for the trader doing stuff. (like setting a new AI)
									tgt.setScript("oolite-default-ship-script.js");
									// now switch off the AI
									tgt.switchAI("oolite-nullAI.js");
									tgt.script._missionID = missData.missionID;

									tgt.fuel = 7;
									tgt.setCrew({
										name: randomName() + " " + randomName(),
										origin: missData.source,
										seed: "0 0 0 0 " + missData.source + " 2"
									});
									// give the ship a unique role so we can look for it later
									tgt.script._savedPrimaryRole = tgt.primaryRole;
									tgt.primaryRole = "gcm_delivery_ship";
									// make sure they have fuel scoops, otherwise they can't pick up the delivery
									tgt.awardEquipment("EQ_FUEL_SCOOPS");
									// make sure they have a destination system set that isn't our originating system
									var sysSet = false;
									var minGov = 4;
									var pop = worldScripts["oolite-populator"];
									do {
										var sys = pop._nearbySafeSystem(minGov);
										if (sys !== system.ID) {
											tgt.destinationSystem = sys;
											sysSet = true;
										}
										minGov -= 1;
									} while (sysSet === false && minGov >= 0);

									// remove any escorts that came with the ship
									if (tgt.escorts && tgt.escorts.length > 0) {
										for (var j = tgt.escorts.length - 1; j >= 0; j--) {
											tgt.escorts[j].remove(true);
										}
									}
									// monkey patch if necessary
									// add our shipDied event to the ship
									if (tgt.script.shipDied) tgt.script.$gcm_hold_shipDied = tgt.script.shipDied;
									tgt.script.shipDied = gcd.$gcd_entity_shipDied;
									// add our shipBeingAttacked event to the ship
									if (tgt.script.shipBeingAttacked) tgt.script.$gcm_hold_shipBeingAttacked = tgt.script.shipBeingAttacked;
									tgt.script.shipBeingAttacked = gcd.$gcd_contact_shipBeingAttacked;

									if (tgt.script.shipScoopedOther) tgt.script.$gcm_hold_shipScoopedOther;
									tgt.script.shipScoopedOther = gcd.$gcd_delivery_shipScoopedOther;
									tgt.script._missionID = missData.missionID;

									if (gcd._checkPlayerNearbyTimer == null || gcd._checkPlayerNearbyTimer.isRunning === false) {
										gcd._checkPlayerNearbyTimer = new Timer(gcd, gcd.$isPlayerNearby, 5, 5);
									}
								} else {
									log("galcobBB_delivery", "!!ERROR: Target ship not spawned!");
								}
								break;

							case 6:
								gns.$createGoonSquad(parseInt((Math.random() * 4) + 3), pos, player.ship.scannerRange * 0.5);
								/*var grp = system.addGroup("pirate", parseInt((Math.random() * 4) + 3), pos, player.ship.scannerRange * 0.5);
								var gn = grp.ships;
								if (gn && gn.length > 0) {
									//var grp = new ShipGroup;
									for (var j = 0; j < gn.length; j++) {
										// configure our pirates
										var pop = worldScripts["oolite-populator"];
										gn[j].setBounty(20 + system.info.government + missData.goons + Math.floor(Math.random() * 8), "setup actions");
										// make sure the pilot has a bounty
										gn[j].setCrew({name:randomName() + " " + randomName(), bounty:gn[j].bounty, insurance:0});
										if (gn[j].hasHyperspaceMotor) {
											pop._setWeapons(gn[j], 1.75); // bigger ones sometimes well-armed
										} else {
											pop._setWeapons(gn[j], 1.3); // rarely well-armed
										}
										// in the safer systems, rarely highly skilled (the skilled ones go elsewhere)
										pop._setSkill(gn[j], 4 - system.info.government);
										if (Math.random() * 16 < system.info.government) {
											pop._setMissiles(gn[j], -1);
										}
										// make sure the AI is switched
										gn[j].switchAI("gcm-pirateAI.js"); 
									}
									gns._goonSquads.push({group:grp, position:pos});
								}*/
								// monitor player's position - when in range of waypoint, set any penalty to 0 
								if (gcd._checkPlayerNearbyTimer == null || gcd._checkPlayerNearbyTimer.isRunning === false) {
									gcd._checkPlayerNearbyTimer = new Timer(gcd, gcd.$isPlayerNearWaypoint, 5, 5);
								}
								break;
							case 8:
								// monitor player's position - when in range of waypoint, set any penalty to 0 
								if (gcd._checkPlayerNearbyTimer == null || gcd._checkPlayerNearbyTimer.isRunning === false) {
									gcd._checkPlayerNearbyTimer = new Timer(gcd, gcd.$isPlayerNearWaypoint, 5, 5);
								}
								break;
						}

						// we'll set the waypoint up regardless of the random roll above, so the player won't know what the result is
						g.$setWaypoint(pos, [0, 0, 0, 0], "M", "Meeting point", "meeting_" + missData.missionID);
						gcd._waypointMissionID = missData.missionID;
					}.bind(this),
					location: "INNER_SYSTEM",
					locationSeed: 0
				});
			}

			// *** type 42 - special delivery - special computers to ship at random location
			if (item.data.missionType === 42 &&
				item.destination === system.ID &&
				item.data.quantity === 0 &&
				item.data.destroyedQuantity === 0 &&
				item.expiry > clock.adjustedSeconds) {

				// add the ship
				var position = gcm.$getRandomPosition(item.data.locationType, 0, item.ID).position;

				this._setData.push({
					missionType: 42,
					missionID: item.ID,
					source: item.source,
					goons: 0,
					target: item.data.targetQuantity,
					destroyed: 0,
					quantity: 0
				});

				system.setPopulator("gcm-delivery-" + item.ID, {
					callback: function (pos) {
						// derelict ship creation code from Eric Walsh's DeepSpaceDredger OXP
						var g = worldScripts.GalCopBB_Missions;
						var gcd = worldScripts.GalCopBB_Delivery;
						var missData = gcd.$getMissionData(42);
						var tgt = null;
						var checkShips = null;
						for (var j = 1; j <= 5; j++) {
							checkShips = system.addShips("[" + g.$getRandomShipKey(parseInt(missData.missionID), 1) + "]", 1, pos, player.ship.scannerRange * 0.75);
							if (checkShips) tgt = checkShips[0];
							if (tgt) break;
						}
						if (tgt) {
							if (g._rsnInstalled) tgt.shipUniqueName = g.$getRandomShipName(tgt, "trader");

							tgt.bounty = 0;
							// make sure they have fuel scoops, otherwise they can't pick up the delivery
							tgt.awardEquipment("EQ_FUEL_SCOOPS");
							// set script to default, to avoid a special script for the trader doing stuff. (like setting a new AI)
							tgt.setScript("oolite-default-ship-script.js");
							// now switch off the AI
							tgt.switchAI("oolite-nullAI.js");
							tgt.script._missionID = missData.missionID;

							tgt.fuel = 7;
							tgt.setCrew({
								name: randomName() + " " + randomName(),
								origin: missData.source,
								seed: "0 0 0 0 " + missData.source + " 2"
							});
							// give the ship a unique role so we can look for it later
							tgt.script._savedPrimaryRole = tgt.primaryRole;
							tgt.primaryRole = "gcm_delivery_ship";
							tgt.awardEquipment("EQ_FUEL_SCOOPS");
							// make sure they have a destination system set that isn't our originating system
							var sysSet = false;
							var minGov = 4;
							var pop = worldScripts["oolite-populator"];
							do {
								var sys = pop._nearbySafeSystem(minGov);
								if (sys !== system.ID) {
									tgt.destinationSystem = sys;
									sysSet = true;
								}
								minGov -= 1;
							} while (sysSet === false && minGov >= 0);

							// remove any escorts that came with the ship
							if (tgt.escorts && tgt.escorts.length > 0) {
								for (var j = tgt.escorts.length - 1; j >= 0; j--) {
									tgt.escorts[j].remove(true);
								}
							}
							// monkey patch if necessary
							// add our shipDied event to the ship
							if (tgt.script.shipDied) tgt.script.$gcm_hold_shipDied = tgt.script.shipDied;
							tgt.script.shipDied = gcd.$gcd_entity_shipDied;
							// add our shipBeingAttacked event to the ship
							if (tgt.script.shipBeingAttacked) tgt.script.$gcm_hold_shipBeingAttacked = tgt.script.shipBeingAttacked;
							tgt.script.shipBeingAttacked = gcd.$gcd_contact_shipBeingAttacked;

							if (tgt.script.shipScoopedOther) tgt.script.$gcm_hold_shipScoopedOther;
							tgt.script.shipScoopedOther = gcd.$gcd_delivery_shipScoopedOther;
						} else {
							log(this.name, "!!ERROR: Target ship not spawned!");
						}

					}.bind(this),
					location: "COORDINATES",
					coordinates: position
				});
				if (this._checkPlayerNearbyTimer == null || this._checkPlayerNearbyTimer.isRunning === false) {
					this._checkPlayerNearbyTimer = new Timer(this, this.$isPlayerNearby, 5, 5);
				}

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

			// *** type 43 - equipment for stricken ship
			if (item.data.missionType === 43 &&
				item.destination === system.ID &&
				item.data.quantity === 0 &&
				item.data.targetQuantity > 0 &&
				item.data.destroyedQuantity === 0 &&
				item.expiry > clock.adjustedSeconds) {

				// add the ship
				var position = gcm.$getRandomPosition(item.data.locationType, 0, item.ID).position;

				this._setData.push({
					missionType: 43,
					missionID: item.ID,
					source: item.source,
					goons: 0,
					target: 1,
					destroyed: 0,
					quantity: 0
				});

				system.setPopulator("gcm-stricken-" + item.ID, {
					callback: function (pos) {
						var g = worldScripts.GalCopBB_Missions;
						var gcd = worldScripts.GalCopBB_Delivery;
						var missData = gcd.$getMissionData(43);
						var stricken = null;
						var checkShips = null;
						for (var j = 1; j <= 5; j++) {
							checkShips = system.addShips("[" + g.$getRandomShipKey(parseInt(missData.missionID), 1) + "]", 1, pos, player.ship.scannerRange * 0.75);
							if (checkShips) stricken = checkShips[0];
							if (stricken) break;
						}
						if (stricken) {
							if (g._rsnInstalled) stricken.shipUniqueName = g.$getRandomShipName(stricken, "trader");

							stricken.bounty = 0;
							// make sure they have fuel scoops, otherwise they can't get help from the player
							stricken.awardEquipment("EQ_FUEL_SCOOPS");
							// set script to default, to avoid a special script for the trader doing stuff. (like setting a new AI)
							stricken.setScript("oolite-default-ship-script.js");
							// now switch off the AI
							stricken.switchAI("oolite-nullAI.js");
							stricken.script._missionID = missData.missionID;
							stricken.fuel = 7;
							// make sure this ship is not going anywhere.
							stricken.script._maxSpeed = stricken.maxSpeed;
							stricken.maxSpeed = 0;
							stricken.desiredSpeed = 0;

							if (missData.target === 1) {
								stricken.setCrew({
									name: randomName() + " " + randomName(),
									origin: missData.source,
									seed: "0 0 0 0 " + missData.source + " 2"
								});
								// give the stricken ship a unique role so we can look for it later
								stricken.script._savedPrimaryRole = stricken.primaryRole;
								stricken.primaryRole = "gcm_stricken_ship";
								stricken.script._sentDistress = false;
								// add our shipBeingAttacked event to the stricken ship
								if (stricken.script.shipBeingAttacked) stricken.script.$gcm_hold_shipBeingAttacked = stricken.script.shipBeingAttacked;
								stricken.script.shipBeingAttacked = gcd.$gcd_stricken_shipBeingAttacked;
							} else {
								// create as a derelict
								stricken.script.shipLaunchedEscapePod = worldScripts.GalCopBB_Derelict.$gcm_derelict_shipLaunchedEscapePod; // function to remove the escapepod after launch.
								if (stricken.equipmentStatus("EQ_ESCAPE_POD") === "EQUIPMENT_UNAVAILABLE") stricken.awardEquipment("EQ_ESCAPE_POD");
								stricken.abandonShip(); // make sure no pilot is left behind and this command turns the ship into cargo.
								stricken.primaryRole = "gcm_derelict"; // to avoid pirate attacks
								stricken.displayName = stricken.displayName + expandDescription("[gcm_no_life_signs]");
							}
							// remove any escorts that came with the ship
							if (stricken.escorts && stricken.escorts.length > 0) {
								for (var j = stricken.escorts.length - 1; j >= 0; j--) {
									stricken.escorts[j].remove(true);
								}
							}
							// monkey patch if necessary
							// add our shipDied event to the stricken ship
							if (stricken.script.shipDied) stricken.script.$gcm_hold_shipDied = stricken.script.shipDied;
							stricken.script.shipDied = gcd.$gcd_entity_shipDied;

							if (missData.target === 1) {
								worldScripts.GalCopBB_LifeSupport.$addShip({
									ent: stricken,
									type: expandDescription("[gcm_stranded_ship]"),
									remaining: Math.floor(Math.random() * 120 + worldScripts.GalCopBB_LifeSupport._lifeSupportDefaultTime),
									last: 0,
									comms: false
								});
							}
						} else {
							log(this.name, "!!ERROR: Stricken ship not spawned!");
						}
					}.bind(this),
					location: "COORDINATES",
					coordinates: position
				});

				if (gcm._debug) gcm.$setWaypoint(position, [0, 0, 0, 0], "D", "Debug position (43)", "43");

			}

			// *** type 46 - special delivery - cargo from/to waypoint stage 0 (collect)
			if ((item.data.missionType === 46) &&
				item.destination === system.ID &&
				item.data.quantity < (item.data.targetQuantity - item.data.destroyedQuantity) &&
				item.data.stage === 0 &&
				item.expiry > clock.adjustedSeconds) {

				this._setData.push({
					missionType: 46,
					missionID: item.ID,
					source: item.source,
					goons: 0,
					quantity: (item.data.targetQuantity - item.data.destroyedQuantity) - item.data.quantity,
					target: item.data.targetQuantity
				});
				/*gcm._cargoMonitor.push({
					quantity: (item.data.targetQuantity - item.data.destroyedQuantity) - item.data.quantity,
					missionID: item.ID
				});*/

				// add the cargo canisters
				system.setPopulator("gcm-delivery-" + item.ID, {
					callback: function (pos) {
						var missData = worldScripts.GalCopBB_Delivery.$getMissionData(46);
						var g = worldScripts.GalCopBB_Missions;

						// only create the ones that haven't been collected
						var pd = g._preferredCargoPods[Math.floor(Math.random() * g._preferredCargoPods.length)];
						var cg = system.addShips("[" + pd + "]", missData.quantity, pos, 5000);
						// if we couldn't create them with our preferred pod type, use the default
						if (!cg) {
							cg = system.addShips("[barrel]", missData.quantity, pos, 5000);
						}
						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");

								item.data.expected += 1;
								cg[j].setCargo(item.data.commodity, 1);
								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 = g.$gcm_cargo_shipDied;

								cg[j].primaryRole = "special_cargo";
								cg[j].name = expandDescription("[gcm_cargo_container]");
							}
							g.$setWaypoint(pos, [0, 0, 0, 0], "C", "Cargo dump location", "46");
							// start a timer to watch what happens to our special cargo
							/*if (g._cargoMonitorTimer == null || g._cargoMonitorTimer.isRunning === false) {
								g._cargoMonitorTimer = new Timer(g, g.$checkSpecialCargo, 3, 3);
							}*/
							worldScripts.GalCopBB_CargoMonitor.$addMonitor(missData.missionID, item.data.commodity, system.ID, false, cg);
						} else {
							log(this.name, "!!ERROR: Cargo not spawned!");
						}
					}.bind(this),
					location: "INNER_SYSTEM",
					locationSeed: 0
				});
				if (Math.random() < parseFloat(expandDescription("[missionType46_assassinChance]"))) {
					//log(this.name, "setting up ambush");
					this._ambushTimer = new Timer(this, this.$setupAmbush, 10, 10);
				}
			}
			// *** type 46 - special delivery - cargo from/to waypoint stage 1 (dump)
			if ((item.data.missionType === 46) &&
				item.destination === system.ID &&
				item.data.quantity < (item.data.targetQuantity - item.data.destroyedQuantity) &&
				item.data.stage === 1 &&
				item.expiry > clock.adjustedSeconds) {

				// add the waypoint
				system.setPopulator("gcm-delivery-" + item.ID, {
					callback: function (pos) {
						var g = worldScripts.GalCopBB_Missions;
						g.$setWaypoint(pos, [0, 0, 0, 0], "C", expandDescription("[gcm_cargo_dump_location]"), "46");
					}.bind(this),
					location: "INNER_SYSTEM",
					locationSeed: 0
				});
				this._monitorCargoMission = item.ID;
				this._monitorCargoDump = new Timer(this, this.$monitorCargoDump, 5, 5);
				if (Math.random() < parseFloat(expandDescription("[missionType46_assassinChance]"))) {
					//log(this.name, "setting up ambush");
					this._ambushTimer = new Timer(this, this.$setupAmbush, 10, 10);
				}
			}
		}
	}
}

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

	var gcm = worldScripts.GalCopBB_Missions;
	var list = gcm.$getListOfMissions(true, 43);

	// if expiry time for type 43 mission expires, convert the stranded ship into a derelict
	for (var i = 0; i < list.length; i++) {
		var item = list[i];
		if (item.data.quantity !== 1 && item.expiry < clock.adjustedSeconds) {
			// turn stranded ship into derelict
			var ss = gcm_findstricken();
			for (var j = 0; j < ss.length; j++) {
				if (ss[j].script._missionID === item.ID) {
					// set script to default, to avoid a special script for the trader doing stuff. (like setting a new AI)
					ss[j].setScript("oolite-default-ship-script.js");
					ss[j].switchAI("oolite-nullAI.js");

					ss[j].script.shipLaunchedEscapePod = worldScripts.GalCopBB_Derelict.$gcm_derelict_shipLaunchedEscapePod; // function to remove the escapepod after launch.
					if (ss[j].equipmentStatus("EQ_ESCAPE_POD") === "EQUIPMENT_UNAVAILABLE") ss[j].awardEquipment("EQ_ESCAPE_POD");
					ss[j].abandonShip(); // make sure no pilot is left behind and this command turns the ship into cargo.
					ss[j].primaryRole = "gcm_derelict";
					ss[j].displayName = ss[j].displayName + expandDescription("[gcm_ship_derelict]");
					item.data.targetQuantity = 0;
					break;
				}
			}
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
// set up interstellar space mission entities here
this.interstellarSpaceWillPopulate = function () {
	var gcm = worldScripts.GalCopBB_Missions;
	var list = gcm.$getListOfMissions(true, 44);
	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++) {
			var item = list[i];
			// *** type 44 - equipment for stricken ship in interstellar space
			if (((gcm._fromSystem === item.destination || gcm._fromSystem === item.source) &&
				(gcm._toSystem === item.destination || gcm._toSystem === item.source)) &&
				item.data.quantity === 0 &&
				item.data.destroyedQuantity === 0 &&
				item.expiry > clock.adjustedSeconds) {

				// add the ship somewhere just inside scanner range
				var dist = (Math.random() * (player.ship.scannerRange * 0.2)) + (player.ship.scannerRange * 0.75);
				var dir = Vector3D.randomDirection(dist);
				var position = Vector3D(0, 0, 0).add(dir);

				this._setData.push({
					missionType: 44,
					missionID: item.ID,
					source: item.source,
					target: item.data.targetQuantity,
					goons: 0,
					quantity: 0
				});

				// add pod with populator
				system.setPopulator("gcm-stricken-" + item.ID, {
					callback: function (pos) {
						var missData = worldScripts.GalCopBB_Delivery.$getMissionData(44);
						var g = worldScripts.GalCopBB_Missions;

						// derelict ship creation code from Eric Walsh's DeepSpaceDredger OXP
						var stricken = null;
						var checkShips = null;
						for (var j = 1; j <= 5; j++) {
							checkShips = system.addShips("[" + g.$getRandomShipKey(missData.missionID, 10) + "]", 1, pos, 1);
							if (checkShips) stricken = checkShips[0];
							if (stricken) break;
						}
						if (stricken) {
							if (g._rsnInstalled) stricken.shipUniqueName = g.$getRandomShipName(stricken, "trader");

							stricken.setCrew({
								name: randomName() + " " + randomName(),
								origin: missData.source,
								seed: "0 0 0 0 " + missData.source + " 2"
							});

							// make sure they have fuel scoops, otherwise they can't get help from the player
							stricken.awardEquipment("EQ_FUEL_SCOOPS");
							// set script to default, to avoid a special script for the trader doing stuff. (like setting a new AI)
							stricken.setScript("oolite-default-ship-script.js");
							// now switch off the AI
							stricken.switchAI("oolite-nullAI.js");
							stricken.script._missionID = missData.missionID;
							stricken.script._maxSpeed = stricken.maxSpeed;
							stricken.maxSpeed = 0;
							stricken.desiredSpeed = 0;

							// remove any escorts that came with the ship
							if (stricken.escorts && stricken.escorts.length > 0) {
								for (var j = stricken.escorts.length - 1; j >= 0; j--) {
									stricken.escorts[j].remove(true);
								}
							}

							if (missData.target === 1) {
								stricken.setCrew({
									name: randomName() + " " + randomName(),
									origin: missData.source,
									seed: "0 0 0 0 " + missData.source + " 2"
								});
								// give the stricken ship a unique role so we can look for it later
								stricken.script._savedPrimaryRole = stricken.primaryRole;
								stricken.primaryRole = "gcm_stricken_ship";
								// "disable" their engines
								stricken.script._sentDistress = false;
								// configure the life support countdown for this ship
								worldScripts.GalCopBB_LifeSupport.$addShip({
									ent: stricken,
									type: expandDescription("[gcm_stranded_ship]"),
									remaining: Math.floor(Math.random() * 120 + worldScripts.GalCopBB_LifeSupport._lifeSupportDefaultTime / 2),
									last: 0,
									comms: false
								});
							} else {
								// create as a derelict
								stricken.script.shipLaunchedEscapePod = worldScripts.GalCopBB_Derelict.$gcm_derelict_shipLaunchedEscapePod; // function to remove the escapepod after launch.
								if (stricken.equipmentStatus("EQ_ESCAPE_POD") === "EQUIPMENT_UNAVAILABLE") stricken.awardEquipment("EQ_ESCAPE_POD");
								stricken.abandonShip(); // make sure no pilot is left behind and this command turns the ship into cargo.
								stricken.primaryRole = "gcm_derelict"; // to avoid pirate attacks
								stricken.displayName = stricken.displayName + expandDescription("[gcm_no_life_signs]");
							}

							// monkey patch if necessary
							// add our shipDied event to the stricken ship
							if (stricken.script.shipDied) stricken.script.$gcm_hold_shipDied = stricken.script.shipDied;
							stricken.script.shipDied = worldScripts.GalCopBB_Delivery.$gcd_entity_shipDied;

						} else {
							log(this.name, "!!ERROR: Stricken ship not spawned!");
						}
					}.bind(this),
					location: "COORDINATES",
					coordinates: position
				});
			}
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.shipWillEnterWitchspace = function () {
	this._attackers.length = 0;
	this._monitorCargo.length = 0;
	this.$stopTimers();
}

//-------------------------------------------------------------------------------------------------------------
this.shipDockedWithStation = function (station) {
	if (this._simulator === true) {
		this._simulator = false;
		return;
	}
	// check for other mission types being completed
	var gcm = worldScripts.GalCopBB_Missions;
	var list = gcm.$getListOfMissions(true, 42);
	for (var i = 0; i < list.length; i++) {
		var item = list[i];
		if (station.allegiance === "galcop" && item.data.quantity === 0 && item.data.destroyedQuantity === 0) {
			// fail - we docked before delivery
			item.data.destroyedQuantity = 1;
			// remove the special computer payload
			player.ship.manifest["computers"] -= 1;
			player.addMessageToArrivalReport(expandDescription("[missionType42_arrivalReport_fail]"));
			// remove the delivery ship so we can't just turn up with any old computers
			// another option would be to leave the ship there and have them attack the player or possibly use strong language
			// if the player tried to give them any old computers.
			if (system.countShipsWithPrimaryRole("gcm_delivery_ship") !== 0) {
				var dvy = system.shipsWithPrimaryRole("gcm_delivery_ship");
				for (var j = 0; j < dvy.length; j++) {
					if (dvy[j].script._missionID === item.ID) {
						dvy[j].remove(true);
						break;
					}
				}
			}
		}
	}
}

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

//-------------------------------------------------------------------------------------------------------------
/*this.shipSpawned = function (ship) {
	var p = player.ship;
	// did the player just dump cargo?
	if (p && ship && ship.isCargo && ship.hasRole("EQ_CARGO_SHEPHERD_MINE") === false && ship.position && p.position && p.position.distanceTo(ship) < 300) {
		this.$checkForStrickenShip(ship)
		this.$checkForSpecialDeliveryShip(ship);
		this.$checkForCargoDump(ship);
	}
}*/

//-------------------------------------------------------------------------------------------------------------
this.shipDumpedCargo = function (cargo) {
	this.$checkForStrickenShip(cargo)
	this.$checkForSpecialDeliveryShip(cargo);
	this.$checkForCargoDump(cargo);
}

//-------------------------------------------------------------------------------------------------------------
this.shipScoopedOther = function (cargo) {
	/*log(this.name, "got here - shipScoopedOther - cargo = " + cargo);
	if (cargo.script) {
		log(this.name, "_delivery = " + cargo.script._delivery);
		log(this.name, "_missionID = " + cargo.script._missionID);
		log(this.name, "_checkMissionID = " + cargo.script._checkMissionID);
		log(this.name, "has shipWasDumped = " + cargo.script.shipWasDumped);
	}*/
	// clean up any properties we might have added
	if (cargo.script && cargo.script._delivery) {
		if (cargo.script.$gcm_hold_shipDied) {
			cargo.script.shipDied = cargo.script.$gcm_hold_shipDied;
			delete cargo.script.$gcm_hold_shipDied;
		}
		if (cargo.script._missionID) delete cargo.script._missionID;
	}
	if (cargo.script && cargo.script._checkMissionID) {
		// we just re-scooped a mission-related cargo item
		// undo the mission status
		var gcm = worldScripts.GalCopBB_Missions;
		var bb = worldScripts.BulletinBoardSystem;
		var item = bb.$getItem(cargo.script._checkMissionID);
		// don't reduce the quantity to less than zero
		if (item.data.quantity > 0) {
			item.data.quantity -= 1;
			bb.$updateBBMissionPercentage(item.ID, (item.data.quantity / (item.data.targetQuantity - item.data.destroyedQuantity)));
			gcm.$updateManifestEntry(cargo.script._checkMissionID);
			gcm.$logMissionData(item.ID);
			player.consoleMessage(expandDescription("[goal_updated]"));
		}
	}
}

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

//-------------------------------------------------------------------------------------------------------------
this.$stopTimers = function $stopTimers() {
	if (this._checkPlayerNearbyTimer && this._checkPlayerNearbyTimer.isRunning) this._checkPlayerNearbyTimer.stop();
	delete this._checkPlayerNearbyTimer;
	if (this._delayToReceiveFixTimer && this._delayToReceiveFixTimer.isRunning) this._delayToReceiveFixTimer.stop();
	delete this._delayToReceiveFixTimer;
	if (this._strickenFixTimer && this._strickenFixTimer.isRunning) this._strickenFixTimer.stop();
	delete this._strickenFixTimer;
	if (this._deliverScoopTimer && this._deliverScoopTimer.isRunning) this._deliverScoopTimer.stop();
	delete this._deliverScoopTimer;
	if (this._strickenScoopTimer && this._strickenScoopTimer.isRunning) this._strickenScoopTimer.stop();
	delete this._strickenScoopTimer
	if (this._badCargoTimer && this._badCargoTimer.isRunning) this._badCargoTimer.stop();
	delete this._badCargoTimer;
}

//-------------------------------------------------------------------------------------------------------------
this.$acceptPendingMission = function $acceptPendingMission(item) {
	// eject some computers from the contact
	var target = worldScripts.GalCopBB_Missions_MFD._target;
	var gcm = worldScripts.GalCopBB_Missions;
	var pd = gcm._preferredCargoPods[Math.floor(Math.random() * gcm._preferredCargoPods.length)];
	var barrel = target.ejectItem("[" + pd + "]");
	barrel.setCargo("computers", 1);

	// attach the default ship script to the barrel
	barrel.setScript("oolite-default-ship-script.js");
	// attach our shipDied script, just in case the player messes up the scoop
	if (barrel.script.shipDied && barrel.script.$gcm_hold_shipDied == null) {
		barrel.script.$gcm_hold_shipDied = barrel.script.shipDied;
		barrel.script.shipDied = this.$gcd_cargo_shipDied;
	}
	barrel.script._specialisedComputers = 1;
	barrel.script._missionID = item.ID;

	// notify the player
	target.commsMessage(expandDescription("[gcm_comms_computers_info]"), player.ship);
}

//-------------------------------------------------------------------------------------------------------------
this.$checkForSpecialDeliveryShip = function $checkForSpecialDeliveryShip(cargo) {
	// get all the delivery ships in range - there should only ever be one
	if (this._deliverShip == null) {
		var targets = system.shipsWithPrimaryRole("gcm_delivery_ship", cargo, player.ship.scannerRange);
		if (targets && targets.length > 0) {
			this._deliverShip = targets[0];
			var bcc = worldScripts.BroadcastCommsMFD;
			if (bcc.$checkMessageExists("gcm_player_arrived_cargo") === true) bcc.$removeMessage("gcm_player_arrived_cargo");
		}
	}

	var gcm = worldScripts.GalCopBB_Missions;
	var bb = worldScripts.BulletinBoardSystem;
	if (this._deliverShip) {
		var item = bb.$getItem(this._deliverShip.script._missionID);

		// add the cargo to the list even if it's the wrong type
		// the target won't know what's inside until they scoop
		if ((item.data.missionType === 40 || item.data.missionType === 41 && item.data.targetQuantity > 1) &&
			//cargo.commodity === item.data.commodity && 
			item.destination === system.ID &&
			item.data.targetQuantity > 0 &&
			item.data.quantity < item.data.targetQuantity) {

			if (cargo.script.shipDied && cargo.script.$gcm_hold_shipDied == null) {
				cargo.script.$gcm_hold_shipDied = cargo.script.shipDied;
				cargo.script.shipDied = this.$gcd_cargo_shipDied;
			}
			cargo.script._missionID = item.ID;
			cargo.script._delivery = true;

			this._deliverCargo.push(cargo);

			// check to see if the ship has switched back to dumb mode because it's waiting for more cargo.
			if (!this._deliverShip.AIScript || this._deliverShip.AIScript.name != "GCM Scavenger AI") {
				// target the new cargo barrel
				this._deliverShip.target = cargo;
				// switch to the collect loot AI so the ship will scoop the barrel
				this._deliverShip.switchAI("gcm-scavengerAI.js");
			}
		}

		if (item.data.missionType === 42 &&
			cargo.commodity === item.data.commodity &&
			item.destination === system.ID &&
			item.data.targetQuantity === 1) {

			if (cargo.script.shipDied && cargo.script.$gcm_hold_shipDied == null) {
				cargo.script.$gcm_hold_shipDied = cargo.script.shipDied;
				cargo.script.shipDied = this.$gcd_cargo_shipDied;
			}
			cargo.script._missionID = item.ID;
			// reset the primary role and ship AI
			this._deliverShip.primaryRole = "trader";
			// now target the cargo barrel
			this._deliverShip.target = cargo;
			// switch to the collect loot AI so the ship will scoop the barrel
			this._deliverShip.switchAI("gcm-scavengerAI.js");
		}
	} else {
		// no one there -- check if there is a mission type 42 active
		if (cargo.commodity === "computers") {
			var missID = gcm.$getActiveMissionIDByType(42);
			if (missID !== -1) {
				if (cargo.script.shipDied && cargo.script.$gcm_hold_shipDied == null) {
					cargo.script.$gcm_hold_shipDied = cargo.script.shipDied;
					cargo.script.shipDied = this.$gcd_cargo_shipDied;
				}
				if (cargo.script.shipWasDumped && cargo.script.$gcm_hold_shipWasDumped == null) {
					cargo.script.$gcm_hold_shipWasDumped = cargo.script.shipWasDumped;
					cargo.script.shipWasDumped = this.$gcd_cargo_shipWasDumped;
				}
				cargo.script._missionID = missID;
				cargo.script._delivery = true;
				cargo.script._specialisedComputers = 1;
			}
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
// looks for any stricken ships within 5km of cargo that was just ejected
this.$checkForStrickenShip = function $checkForStrickenShip(cargo) {
	// if we didn't dump machinery, there's no point in checking further
	// but tell the player about it
	if (cargo.commodity !== "machinery") {
		if (system.shipsWithPrimaryRole("gcm_stricken_ship", player.ship, player.ship.scannerRange).length > 0) {
			this._badCargoTimer = new Timer(this, this.$tellPlayerAboutCargo, 5, 0);
		}
		return;
	}
	if (this._badCargoTimer && this._badCargoTimer.isRunning) this._badCargoTimer.stop();

	var gcm = worldScripts.GalCopBB_Missions;
	var targets = system.shipsWithPrimaryRole("gcm_stricken_ship", cargo, 10000);
	if (targets && targets.length > 0) {
		var list = gcm.$getListOfMissions(true, [43, 44]);
		for (var i = 0; i < list.length; i++) {
			var item = list[i];
			var check = false;
			if (item.data.missionType === 43 && item.destination === system.ID && item.data.targetQuantity === 1) check = true;
			if (item.data.missionType === 44 &&
				((gcm._fromSystem === item.destination || gcm._fromSystem === item.source) &&
					(gcm._toSystem === item.destination || gcm._toSystem === item.source))) check = true;

			if (check) {
				cargo.script.shipDied = this.$gcd_cargo_shipDied;
				this._strickenShip = targets[0];
				this._fixCargo = cargo;
				if (this._delayToReceiveFixTimer && this._delayToReceiveFixTimer.isRunning) this._delayToReceiveFixTimer.stop();
				this._delayToReceiveFixTimer = new Timer(this, this.$fixStrickenShipsInitiate, 5, 2);
				break;
			}
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
// timer destination after player dumps machinery within range of stricken ship
this.$fixStrickenShipsInitiate = function $fixStrickenShipsInitiate() {
	if (this._strickenShip.position.distanceTo(this._fixCargo) < 1000) {
		this._delayToReceiveFixTimer.stop();
		// scoop cargo, delay, then switch AI
		if (this._fixCargo && this._fixCargo.isInSpace) {
			if (this._strickenScoopTimer && this._strickenScoopTimer.isRunning) this._strickenScoopTimer.stop();
			this._strickenScoopTimer = new Timer(this, this.$scoopCargo, 0.5, 0);
		}
	} else {
		if (this._strickenShip.position.distanceTo(this._fixCargo) > 20000) {
			this._delayToReceiveFixTimer.stop();
			player.consoleMessage(expandDescription("[gcm_repair_out_of_range]"));
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$scoopCargo = function $scoopCargo() {
	if (this._fixCargo && this._fixCargo.isInSpace) {
		// check to make sure the cargo didn't get away from the stricken ship
		if (this._strickenShip.position.distanceTo(this._fixCargo) > 1000) {
			// tell the player, and revert back to waiting for the cargo to get close
			this._strickenShip.commsMessage(expandDescription("[gcm_stricken_cantscoop]"), player.ship);
			if (this._delayToReceiveFixTimer && this._delayToReceiveFixTimer.isRunning) this._delayToReceiveFixTimer.stop();
			this._delayToReceiveFixTimer = new Timer(this, this.$fixStrickenShipsInitiate, 5, 2);
		} else {
			// simulate a tractor beam by moving cargo towards stricken ship
			this._fixCargo.velocity = this._strickenShip.position.subtract(this._fixCargo.position).direction().multiply(20).add(this._fixCargo.velocity);
			if (this._strickenShip.position.distanceTo(this._fixCargo) < 100) {
				this._fixCargo.remove(true);
				//this._strickenShip.target = this._fixCargo;
				//this._strickenShip.performCollect();
				this._strickenShip.script._fixed = true;
				if (this._strickenFixTimer == null || this._strickenFixTimer.isRunning === false) {
					this._strickenFixTimer = new Timer(this, this.$fixStrickenShips, 10, 0);
				}
			} else {
				if (this._strickenScoopTimer && this._strickenScoopTimer.isRunning) this._strickenScoopTimer.stop();
				this._strickenScoopTimer = new Timer(this, this.$scoopCargo, 0.5, 0);
			}
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
// timer destination for "repairing" stricken ships
this.$fixStrickenShips = function $fixStrickenShips() {
	var targets = system.shipsWithPrimaryRole("gcm_stricken_ship");
	var bb = worldScripts.BulletinBoardSystem;
	var gcm = worldScripts.GalCopBB_Missions;
	if (targets && targets.length > 0) {
		for (var i = 0; i < targets.length; i++) {
			if (targets[i].script._fixed === true) {

				targets[i].primaryRole = targets[i].script._savedPrimaryRole;
				targets[i].maxSpeed = targets[i].script._maxSpeed;
				targets[i].switchAI("oolite-traderAI.js");
				targets[i].commsMessage(expandDescription("[thanks_for_help]"), player.ship);

				var item = bb.$getItem(targets[i].script._missionID);

				item.data.quantity = 1;
				bb.$updateBBMissionPercentage(item.ID, 1);

				gcm.$logMissionData(item.ID);
				player.consoleMessage(expandDescription("[goal_updated]"));
			}
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
// timer destination for stricken ship detecting the wrong dumped cargo
this.$tellPlayerAboutCargo = function $tellPlayerAboutCargo() {
	var ships = system.shipsWithPrimaryRole("gcm_stricken_ship", player.ship, player.ship.scannerRange);
	if (ships.length > 0) {
		ships[0].commsMessage(expandDescription("[gcm_stricken_bad_cargo]"), player.ship);
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$gcd_delivery_shipScoopedOther = function $gcd_delivery_shipScoopedOther(whom) {
	if (this.ship.script.$gcm_hold_shipScoopedOther) this.ship.script.$gcm_hold_shipScoopedOther(whom);

	if (whom.isCargo === false) return;

	// update the mission
	var gcm = worldScripts.GalCopBB_Missions;
	var gcd = worldScripts.GalCopBB_Delivery;
	var bb = worldScripts.BulletinBoardSystem;
	var item = bb.$getItem(this.ship.script._missionID);

	if (whom.commodity === item.data.commodity) {
		if (item.data.quantity < item.data.targetQuantity) {
			item.data.quantity += 1;
			bb.$updateBBMissionPercentage(item.ID, item.data.quantity / item.data.targetQuantity);

			if (item.data.quantity === item.data.targetQuantity) { //  + item.data.destroyedQuantity
				// switch the AI to an outbound trader so they will hyperspace out
				this.ship.switchAI("exitingTraderAI.plist");
				this.ship.commsMessage(expandDescription("[gcm_scoop_success]"), player.ship);
				// clean up any broadcast comms
				var bcc = worldScripts.BroadcastCommsMFD;
				if (bcc.$checkMessageExists("gcm_player_response_1") === true) bcc.$removeMessage("gcm_player_response_1");
				if (bcc.$checkMessageExists("gcm_player_response_2") === true) bcc.$removeMessage("gcm_player_response_2");
				// delay spawning loot for a couple of seconds to prevent it being rescooped by the originator
				if (item.data.missionType === 40 || (item.data.missionType === 41 && item.data.stage === 1)) {
					if (gcd._deliverScoopTimer && gcd._deliverScoopTimer.isRunning) {
						gcd._deliverScoopTimer.stop();
						delete gcd._deliverScoopTimer;
					}
					// clean up the array
					for (var i = gcd._deliverCargo.length - 1; i >= 0; i--) {
						if (gcd._deliverCargo[i].isValid === false || gcd._deliverCargo[i].status === "STATUS_IN_HOLD") {
							gcd._deliverCargo.splice(i, 1);
						}
					}
					// give the player some loot
					this.ship.commsMessage(expandDescription("[gcm_scoop_reward]"), player.ship);
					var opts = ["gold", "platinum", "gem_stones"];
					var cmdty = opts[Math.floor(Math.random() * opts.length)];
					var qty = parseInt(Math.random() * 30 + 10);
					if (cmdty === "gem_stones") qty *= 2;

					var pd = gcm._preferredCargoPods[Math.floor(Math.random() * gcm._preferredCargoPods.length)];
					var loot = this.ship.ejectItem("[" + pd + "]");
					loot.setCargo(cmdty, qty);
					gcd._deliverShip = null;
					// give the player a trader role
					gcd.$addTraderRoleToPlayer(whom.commodity);
					// update the stage for a type 41 mission
					if (item.data.missionType === 41) item.data.stage += 1;
				}
			} else {
				// is there any more of the cargo scoop?
				var found = false;
				for (var i = 0; i < gcd._deliverCargo.length; i++) {
					if (gcd._deliverCargo[i].isValid && gcd._deliverCargo[i].status === "STATUS_BEING_SCOOPED") {
						// we're already scooping this one!
						found = true;
						break;
					}
					if (gcd._deliverCargo[i].isValid && gcd._deliverCargo[i].status === "STATUS_IN_FLIGHT") {
						found = true;
						break;
					}
				}
				if (found === false) {
					// player didn't provide enough cargo... respond
					// send player message
					this.ship.commsMessage(expandDescription("[gcm_delivery_short]"), player.ship);
					// turn off the loot ai
					this.ship.switchAI("oolite-nullAI.js");
					this.ship.desiredSpeed = 0;
					// give player some response options.
					var bcc = worldScripts.BroadcastCommsMFD;
					if (bcc.$checkMessageExists("gcm_player_response_1") === false) {
						bcc.$createMessage({
							messageName: "gcm_player_response_1",
							callbackFunction: gcd.$transmitResponseNoMore.bind(this),
							displayText: expandDescription("[gcm_player_say_no_more]"),
							messageText: expandDescription("[gcm_player_response_all_out]"),
							ship: this.ship,
							transmissionType: "target",
							delayCallback: 5,
							deleteOnTransmit: true,
							hideOnConditionRed: true
						});
					}
					if (bcc.$checkMessageExists("gcm_player_response_2") === false) {
						bcc.$createMessage({
							messageName: "gcm_player_response_2",
							callbackFunction: gcd.$transmitResponseOops.bind(this),
							displayText: expandDescription("[gcm_player_say_sorry]"),
							messageText: expandDescription("[gcm_player_response_sorry]"),
							ship: this.ship,
							transmissionType: "target",
							delayCallback: 5,
							deleteOnTransmit: true,
							hideOnConditionRed: true
						});
					}
				}
			}
		}
	} else {
		// did we just scoop the cargo we gave the player?
		// eject it again
		if (item.data.quantity + item.data.destroyedQuantity === item.data.targetQuantity) {
			var pd = gcm._preferredCargoPods[Math.floor(Math.random() * gcm._preferredCargoPods.length)];
			var loot = this.ship.ejectItem("[" + pd + "]");
			loot.setCargo(whom.commodity, whom.commodityAmount);
			return;
		}
		// not the right cargo... respond
		// send player message
		// check if there are other containers around and scoop them
		var found = false;
		if (gcd._deliverCargo.length > 0) {
			for (var i = 0; i < gcd._deliverCargo.length; i++) {
				if (gcd._deliverCargo[i].isValid && gcd._deliverCargo[i].status === "STATUS_BEING_SCOOPED") {
					// we're already scooping this one!
					this.ship.target = gcd._deliverCargo[i];
					found = true;
					break;
				}
				if (gcd._deliverCargo[i].isValid && gcd._deliverCargo[i].status === "STATUS_IN_FLIGHT") {
					this.ship.switchAI("oolite-nullAI.js");
					this.ship.switchAI("gcm-scavengerAI.js");
					this.ship.target = gcd._deliverCargo[i];
					found = true;
					break;
				}
			}
		}
		var bcc = worldScripts.BroadcastCommsMFD;
		// stop the ship from moving if there's no cargo to scoop atm
		if (found === false) {
			this.ship.switchAI("oolite-nullAI.js");
			this.ship.desiredSpeed = 0;
			if (bcc.$checkMessageExists("gcm_player_response_1") === false) {
				bcc.$createMessage({
					messageName: "gcm_player_response_1",
					callbackFunction: gcd.$transmitResponseNoMore.bind(gcd),
					displayText: expandDescription("[gcm_player_say_no_more]"),
					messageText: expandDescription("[gcm_player_response_all_out]"),
					ship: this.ship,
					transmissionType: "target",
					delayCallback: 5,
					deleteOnTransmit: true,
					hideOnConditionRed: true
				});
			}
		} else {
			if (bcc.$checkMessageExists("gcm_player_response_1") === true) bcc.$removeMessage("gcm_player_response_1");
		}
		this.ship.commsMessage(expandDescription("[gcm_delivery_wrong_cargo]", {
			commodity: displayNameForCommodity(whom.commodity).toLowerCase()
		}), player.ship);
		// give player some response options.
		if (bcc.$checkMessageExists("gcm_player_response_2") === false) {
			bcc.$createMessage({
				messageName: "gcm_player_response_2",
				callbackFunction: gcd.$transmitResponseOops.bind(gcd),
				displayText: expandDescription("[gcm_player_say_sorry]"),
				messageText: expandDescription("[gcm_player_response_sorry]"),
				ship: this.ship,
				transmissionType: "target",
				delayCallback: 5,
				deleteOnTransmit: true,
				hideOnConditionRed: true
			});
		}
		return;
	}
	gcm.$logMissionData(item.ID);
	player.consoleMessage(expandDescription("[goal_updated]"));
}

//-------------------------------------------------------------------------------------------------------------
// attached to meeting ships, designed to handle what happens if the ship is attacked
this.$gcd_contact_shipBeingAttacked = function $gcd_contact_shipBeingAttacked(whom) {
	if (this.ship.script.$gcm_hold_shipBeingAttacked) this.ship.script.$gcm_hold_shipBeingAttacked(whom);

	if (whom.isPlayer) {
		this.ship.commsMessage(expandDescription("[gcm_contact_attacked]"), whom);
	} else {
		this.ship.commsMessage(expandDescription("[distress-call]"), player.ship);
	}

	var bb = worldScripts.BulletinBoardSystem;
	var missID = this.ship.script._missionID;
	if (missID > 0) {
		var item = bb.$getItem(missID);
		item.data.targetQuantity = 0;

		if (this.ship.AIScript.name === "Null AI") this.ship.switchAI("oolite-traderAI.js");
		this.ship.target = whom;

		// turn off any pending responses
		var g = worldScripts.GalCopBB_Missions_MFD;
		if (g._subMessageTimer && g._subMessageTimer.isRunning) g._subMessageTimer.stop();
		if (g._offerTimer && g._offerTimer.isRunning) g.$declineNewMission();
	}
	// get the ships response
	this.ship.target = whom;
	this.ship.performAttack();
	this.ship.AIScript.oolite_priorityai.reconsiderNow();
	// restore the original script (if there was one)
	if (this.ship.script.$gcm_hold_shipBeingAttacked) {
		this.ship.script.shipBeingAttacked = this.ship.script.$gcm_hold_shipBeingAttacked;
		delete this.ship.script.$gcm_hold_shipBeingAttacked;
	}
	// remove this script so it doesn't keep running
	delete this.ship.script.shipBeingAttacked;
}

//-------------------------------------------------------------------------------------------------------------
this.$gcd_stricken_shipBeingAttacked = function $gcd_stricken_shipBeingAttacked(whom) {
	if (this.ship.script.$gcm_hold_shipBeingAttacked) this.ship.script.$gcm_hold_shipBeingAttacked(whom);

	if (worldScripts.GalCopBB_Missions._debug) log(this.name, "stricken ship being attacked by " + whom);

	// send a distress call 
	if (this.ship.script._sentUnderAttackMessage == null || this.ship.script._sentUnderAttackMessage === false) {
		this.ship.script._sentUnderAttackMessage = true;
		this.ship.broadcastDistressMessage();
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$gcd_cargo_shipWasDumped = function $gcd_cargo_shipWasDumped(dumper) {
	var gcd = worldScripts.GalCopBB_Delivery;
	gcd.$checkForSpecialDeliveryShip(this.ship);
}

//-------------------------------------------------------------------------------------------------------------
this.$gcd_cargo_shipWasDumped2 = function $gcd_cargo_shipWasDumped2(dumper) {
	if (this.ship.script.$gcm_hold_shipWasDumped) this.ship.script.$gcm_hold_shipWasDumped(dumper);
	worldScripts.GalCopBB_Delivery.$checkForCargoDump(this.ship);
}

//-------------------------------------------------------------------------------------------------------------
this.$gcd_cargo_shipDied = function $gcd_cargo_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;

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

	var item = bb.$getItem(this.ship.script._missionID);
	if (item) {
		item.data.destroyedQuantity += 1;
		bb.$updateBBMissionPercentage(item.ID, (item.data.quantity / (item.data.targetQuantity - item.data.destroyedQuantity)));
		player.consoleMessage(expandDescription("[goal_updated]"));
		// reduce the payment amount by half
		if (item.data.missionType === 43 || item.data.missionType == 44) {
			item.payment *= 0.5;
			// switch to the alt manifest entry
			item.data.altManifest = true;
			gcm.$updateManifestEntry(this.ship.script._missionID);
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$gcd_cargo_shipDied2 = function $gcd_cargo_shipDied2(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;

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

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

//-------------------------------------------------------------------------------------------------------------
this.$gcd_entity_shipDied = function $gcd_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;
		// reduce the payment amount by half
		if (item.data.missionType === 43 || item.data.missionType == 44) {
			item.payment *= 0.5;
			// switch to the alt manifest entry
			item.data.altManifest = true;
			gcm.$updateManifestEntry(this.ship.script._missionID);
		}
		if (item.data.missionType === 42) {
			// mission failed - target ship destroyed
			var rep = worldScripts.GalCopBB_Reputation;
			bb.$updateBBManifestText(
				item.ID,
				rep.$transformText(expandDescription("[missionType" + item.data.missionType + "_failedManifest]", {
					system: System.systemNameForID(item.source),
					expiry: ""
				}), item.source, item.destination)
			);
			bb.$updateBBStatusText(
				item.ID,
				rep.$transformText(expandDescription("[missionType" + item.data.missionType + "_failedStatus]", {
					system: System.systemNameForID(item.source)
				}), item.source, item.destination)
			);
		}
		bb.$updateBBMissionPercentage(item.ID, (item.data.quantity / (item.data.targetQuantity - item.data.destroyedQuantity)));
		gcm.$logMissionData(item.ID);
		player.consoleMessage(expandDescription("[goal_updated]"));
	}

	if (gcm._distressMessageTimer && gcm._distressMessageTimer.isRunning) gcm._distressMessageTimer.stop();
}

//-------------------------------------------------------------------------------------------------------------
this.$transmitResponseNoMore = function $transmitResponseNoMore() {
	// player said there is no more
	// terminate the mission, but give the player a token for how much they did provide.
	var gcd = worldScripts.GalCopBB_Delivery;
	if (gcd._deliverShip) {
		var bb = worldScripts.BulletinBoardSystem
		var item = bb.$getItem(gcd._deliverShip.script._missionID);
		// switch the AI to an outbound trader so they will hyperspace out
		gcd._deliverShip.switchAI("exitingTraderAI.plist");
		gcd._deliverShip.commsMessage(expandDescription("[gcm_scoop_part_success]"), player.ship);
		// clean up any broadcast comms
		var bcc = worldScripts.BroadcastCommsMFD;
		if (bcc.$checkMessageExists("gcm_player_response_1") === true) bcc.$removeMessage("gcm_player_response_1");
		if (bcc.$checkMessageExists("gcm_player_response_2") === true) bcc.$removeMessage("gcm_player_response_2");
		if (item.data.missionType === 40 || (item.data.missionType === 41 && item.data.targetQuantity > 1)) {
			if (gcd._deliverScoopTimer && gcd._deliverScoopTimer.isRunning) {
				gcd._deliverScoopTimer.stop();
				delete gcd._deliverScoopTimer;
			}
			// clean up the array
			for (var i = gcd._deliverCargo.length - 1; i >= 0; i--) {
				if (gcd._deliverCargo[i].isValid === false || gcd._deliverCargo[i].status === "STATUS_IN_HOLD") {
					gcd._deliverCargo.splice(i, 1);
				}
			}
			// give the player some loot
			var opts = ["gold", "platinum", "gem_stones"];
			var cmdty = opts[Math.floor(Math.random() * opts.length)];
			var pct = (item.data.quantity + item.data.destroyedQuantity) / item.data.targetQuantity;
			var qty = parseInt((Math.random() * 30 + 10) * pct);
			if (cmdty === "gem_stones") qty *= 2;
			var gcm = worldScripts.GalCopBB_Missions;
			var pd = gcm._preferredCargoPods[Math.floor(Math.random() * gcm._preferredCargoPods.length)];
			var loot = gcd._deliverShip.ejectItem("[" + pd + "]");
			loot.setCargo(cmdty, qty);
		}
		gcd._deliverShip = null;
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$transmitResponseOops = function $transmitResponseOops() {
	// player said 'oops'
	// do nothing  - wait for player to eject more
	// clean up any broadcast comms
	var bcc = worldScripts.BroadcastCommsMFD;
	if (bcc.$checkMessageExists("gcm_player_response_1") === true) bcc.$removeMessage("gcm_player_response_1");
	if (bcc.$checkMessageExists("gcm_player_response_2") === true) bcc.$removeMessage("gcm_player_response_2");
}

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

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

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

	// 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 === 40) {
		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) gcm.$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);
				}
			}
		}
	}

	if (item.data.missionType === 41) {
		player.ship.awardEquipment("EQ_GCM_COMMS_RELAY_SWITCH");
		worldScripts.GalCopBB_DataCache._usageCount = 0;
	}

	// give player special computers for mission type 42
	if (item.data.missionType === 42) {
		if (player.ship.isInSpace === false) {
			if (player.ship.cargoSpaceAvailable === 0 && player.ship.cargoSpaceCapacity >= 1) gcm.$freeCargoSpace(1, "computers");
			player.ship.manifest["computers"] += 1;
		}
	}
	// give player special machinery for mission type 43/44
	if (item.data.missionType === 43 || item.data.missionType === 44) {
		if (player.ship.isInSpace === false) {
			// if the player is docked they can get the cargo straight away
			if (player.ship.cargoSpaceAvailable === 0 && player.ship.cargoSpaceCapacity >= 1) gcm.$freeCargoSpace(1, "machinery");
			player.ship.manifest["machinery"] += 1;
		} else {
			// otherwise flag it so they player will get it when they next dock
			gcm._requestSpecialCargo = "machinery";
			gcm._specialCargoTask[gcm._requestSpecialCargo] = expandDescription("[gcm_repair_message]");
		}
	}

}

//-------------------------------------------------------------------------------------------------------------
this.$completedMission = function $completedMission(missID) {
	var p = player.ship;
	var bb = worldScripts.BulletinBoardSystem;
	var item = bb.$getItem(missID);
	var gcm = worldScripts.GalCopBB_Missions;

	worldScripts.GalCopBB_CargoMonitor.$removeMonitor(missID);

	// update mission history
	gcm.$updateSuccessHistoryReputation(item);
}

//-------------------------------------------------------------------------------------------------------------
this.$confirmCompleted = function $confirmCompleted(missID) {
	return "";
}

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

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

	worldScripts.GalCopBB_CargoMonitor.$removeMonitor(missID);

	// *** type 43/44 - machinery for stranded ship
	if (item.data.missionType === 43 || item.data.missionType === 44) {
		if (player.ship.manifest["machinery"] > 0 && gcm._requestSpecialCargo === "") player.ship.manifest["machinery"] -= 1;
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$failedMission = function $failedMission(missID) {
	var bb = worldScripts.BulletinBoardSystem;
	var sbm = worldScripts.Smugglers_BlackMarket;
	var gcm = worldScripts.GalCopBB_Missions;
	var eq = "";

	if (!sbm) {
		// if there's no blackmarket option, just remove the equipment
		this.$terminateMission(missID);
		return;
	}

	var item = bb.$getItem(missID);
	// update mission history
	gcm.$updateFailedHistoryReputation(item);
	worldScripts.GalCopBB_CargoMonitor.$removeMonitor(missID);

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

//-------------------------------------------------------------------------------------------------------------
this.$updateManifestEntry = function $updateManifestEntry(missID) {
	var gcm = worldScripts.GalCopBB_Missions;
	gcm.$updateManifestEntry(missID);
}

//-------------------------------------------------------------------------------------------------------------
this.$missionAvailability = function $missionAvailability(missID, missType, origSysID) {
	return worldScripts.GalCopBB_Missions.$missionAvailability(missID, missType, origSysID);
}

//-------------------------------------------------------------------------------------------------------------
// 40 - special cargo delivery to waypoint
this.$missionType40_Values = function $missionType40_Values(workTime, routeTime, routeDistance, destSysInfo) {
	if (system.ID === -1) return null;
	// we're leaving out food, textiles and the precious metals and gems
	var c_types = ["luxuries", "computers", "furs", "liquor_wines", "alloys", "radioactives", "alien_items", "machinery", "narcotics", "firearms", "slaves"];
	var result = {};
	var tries = 0;
	result["quantity"] = 0;
	// make sure we don't create missions where the quantity of cargo is more than the player ship
	var max = player.ship.cargoSpaceCapacity;
	// also make sure that we don't ever allocate more than any of our target ships can handle
	if (max > worldScripts.GalCopBB_Missions._maxCargoOfShips) max = worldScripts.GalCopBB_Missions._maxCargoOfShips;
	// make sure we don't end up with something ridiculous, like 100. make the max possible 25
	if (max > 25) max = 25;
	// work out mission details - commodity and quantity
	do {
		result["commodity"] = c_types[Math.floor(Math.random() * c_types.length)];;
		result["quantity"] = parseInt((Math.random() * (max - 3)) + 3);
		// make sure we only set a quantity the local system can provide
		if (system.mainStation.market[result.commodity].quantity < result.quantity) result.quantity = system.mainStation.market[result.commodity].quantity;
		tries += 1;
	} while ((result.quantity > max || result.quantity <= 0) && tries < 5);
	// if we aborted the loop, just return null
	if (tries >= 5 && (result.quantity > max || result.quantity === 0)) return null;
	// otherwise, just set a token reward - player will also receive bonus from waiting ship
	// make sure the deposit amount is included in the price so the BB knows how to display it
	result["price"] = parseInt(((Math.random() * 100) + 50) * 10) / 10 + (result.quantity * (system.mainStation.market[result.commodity].price / 10)) +
		worldScripts.GalCopBB_CoreMissionValues.$calcPlayerBonus(200) + // plus a possible bonus price, based on player score 
		worldScripts.GalCopBB_CoreMissionValues.$calcDistanceBonus(routeDistance, 20); // plus a distance bonus
	result["expiry"] = clock.adjustedSeconds + routeTime + 21600; // transit time + 6 hours to complete 
	result["penalty"] = 0;
	result["deposit"] = result.quantity * (system.mainStation.market[result.commodity].price / 10);
	return result;
}

//-------------------------------------------------------------------------------------------------------------
// 41 - data cache to new waypoint for delivery of cargo
this.$missionType41_Values = function $missionType41_Values(workTime, routeTime, routeDistance, destSysInfo) {
	var result = {};
	// make sure we don't create missions where the quantity of cargo is more than the player ship
	var max = player.ship.cargoSpaceCapacity;
	// also make sure that we don't ever allocate more than any of our target ships can handle
	if (max > worldScripts.GalCopBB_Missions._maxCargoOfShips) max = worldScripts.GalCopBB_Missions._maxCargoOfShips;
	// make sure we don't end up with something ridiculous, like 100. make the max possible 25
	if (max > 25) max = 25;
	// we must have at least 2t otherwise sequencing won't work
	if (max < 2) return null;
	// pick a location
	result["locationType"] = Math.floor(Math.random() * this._maxLocations);
	result["quantity"] = 1; // initial quantity set to 1
	result["price"] = parseInt((parseInt(Math.random() * 40) + 20) / 10) * 10 + (7 - destSysInfo.government) * 20 +
		worldScripts.GalCopBB_CoreMissionValues.$calcPlayerBonus(200) // plus a possible bonus price, based on player score 
		+
		worldScripts.GalCopBB_CoreMissionValues.$calcDistanceBonus(routeDistance, 20); // plus a distance bonus
	result["expiry"] = clock.adjustedSeconds + routeTime + workTime; // transit time + 1 hour to complete
	result["penalty"] = parseInt(result.price / 2);
	return result;
}

//-------------------------------------------------------------------------------------------------------------
// 41 (part 2) - data cache to new waypoint for delivery of cargo
this.$missionType41Updated_Values = function $missionType41Updated_Values(c_relay, missID, secondary) {
	if (system.ID === -1) return false;
	// we'll be doing all the calcs here, as this mission is split into two

	var completed = false;
	var bb = worldScripts.BulletinBoardSystem;
	var gcm = worldScripts.GalCopBB_Missions;
	var item = bb.$getItem(missID);

	// get interstellar range for this mission
	var rng = parseInt(expandDescription("[missionType41_destRange]"));
	var sys = System.infoForSystem(galaxyNumber, system.ID).systemsInRange(rng);
	for (var i = sys.length - 1; i >= 0; i--) {
		// remove any systems that don't meet conditions
		if (sys[i].systemID === item.source) {
			// don't include the original system
			sys.splice(i, 1);
			continue;
		} else {
			if (gcm.$testMissionConditions(expandDescription("[missionType41_destConditions]"), sys[i].systemID, 41, system.ID, secondary) === false) {
				sys.splice(i, 1);
				continue;
			} else if (sys[i].systemID === system.ID) {
				// remove the current system if it's there
				sys.splice(i, 1);
				continue;
			}
		}
		// make sure the trip distance is still inside our desired range
		var rt = system.info.routeToSystem(sys[i]);
		if (rt.distance > rng) {
			sys.splice(i, 1);
			continue;
		}
		// remove systems for any mission types of: 31
		var list = gcm.$getListOfMissions(true, 31);
		for (var j = 0; j < list.length; j++) {
			if (list[j].destination == sys[i].systemID) {
				sys.splice(i, 1);
				continue;
			}
		}
	}

	// 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);
		return false;
	}

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

	var sysSelectType = expandDescription("[missionType41_systemSelectProcess]");
	var l_start = 0;
	var l_end = sys.length;
	var choice = 0; // we'll pick it up in the loop

	for (var i = l_start; i < l_end; i++) {
		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
		var route = system.info.routeToSystem(dest);
		if (route == null) continue; // if there's no route to the system

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

		// time is one way only
		rtime = route.time * 3600;
		// plus 30 minutes transit time in each system
		rtime += route.route.length * gcm._transitTime;
		// if we're adding missions after a witchspace jump, add the extra time now
		rtime += gcm._initialTime;

		// mission specific config
		// we're leaving out food, textiles and the precious metals and gems
		var c_types = ["luxuries", "computers", "furs", "liquor_wines", "alloys", "radioactives", "alien_items", "machinery", "narcotics", "firearms", "slaves"];
		var result = {};
		var tries = 0;
		result["quantity"] = 0;
		// make sure we don't create missions where the quantity of cargo is more than the player ship
		var max = player.ship.cargoSpaceCapacity;
		// also make sure that we don't ever allocate more than any of our target ships can handle
		if (max > worldScripts.GalCopBB_Missions._maxCargoOfShips) max = worldScripts.GalCopBB_Missions._maxCargoOfShips;
		// make sure we don't end up with something ridiculous, like 100. make the max possible 25
		if (max > 25) max = 25;
		// work out mission details - commodity and quantity
		do {
			result["commodity"] = c_types[Math.floor(Math.random() * c_types.length)];;
			result["quantity"] = parseInt((Math.random() * (max - 3)) + 3);
			// make sure we only set a quantity the local system can provide
			if (system.mainStation.market[result.commodity].quantity < result.quantity) result.quantity = system.mainStation.market[result.commodity].quantity;
			tries += 1;
		} while ((result.quantity > max || result.quantity < 2) && tries < 10);
		// if we aborted the loop, just return null
		if (tries >= 10 && (result.quantity > max || result.quantity < 2)) continue;
		result["expiry"] = clock.adjustedSeconds + rtime + (result.quantity * 1800) + 21600; // transit time + 30 mins per t + 6 hours to complete (cargo is sometimes hard to find...)
		result["refund"] = result.quantity * (system.mainStation.market[result.commodity].price / 10);

		// need to update:
		bb.$removeChartMarker(missID);
		//	destination
		item.destination = dest.systemID;
		//	expiry time
		item.expiry = result.expiry;
		//	targetQuantity
		item.data.targetQuantity = result.quantity;
		//	quantity
		item.data.quantity = 0;
		//	commodity
		item.data.commodity = result.commodity;
		//	percentage complete
		bb.$updateBBMissionPercentage(missID, 0);
		//	refund
		//	this enables the player to be paid back for the cost of the merchandise
		item.customDisplayItems.push({
			heading: expandDescription("[gcm_refund_amount]"),
			value: formatCredits(result.refund, false, true)
		});
		item.data.refund = result.refund;
		item.data.stage += 1;

		bb.$addManifestEntry(missID);
		gcm.$logMissionData(item.ID);
		player.consoleMessage(expandDescription("[goal_updated]"));

		var msg = expandDescription("[gcm_updated_destination]", { dest: System.systemNameForID(dest.systemID), quantity: result.quantity, commodity: displayNameForCommodity(result.commodity).toLowerCase() });

		c_relay.commsMessage(msg, player.ship, 10);
		completed = true;
		break;
	}

	return completed;
}

//-------------------------------------------------------------------------------------------------------------
// 42 - special delivery
this.$missionType42_Values = function $missionType42_Values(workTime, routeTime, routeDistance, destSysInfo) {
	var result = {};
	result["quantity"] = 1;
	result["locationType"] = Math.floor(Math.random() * this._maxLocations);
	result["commodity"] = "computers";
	result["price"] = parseInt((parseInt(Math.random() * 50) + 100) / 10) * 10 + (7 - destSysInfo.government) * 50 +
		worldScripts.GalCopBB_CoreMissionValues.$calcDistanceBonus(routeDistance, 10) + // plus a distance bonus
		worldScripts.GalCopBB_CoreMissionValues.$calcPlayerBonus(500); // plus a possible bonus price, based on player score 
	result["expiry"] = clock.adjustedSeconds + routeTime + workTime; // transit time + 1 hour to complete
	result["penalty"] = parseInt(result.price / 2);
	return result;
}

//-------------------------------------------------------------------------------------------------------------
// 43 - rescue stranded ship
this.$missionType43_Values = function $missionType43_Values(workTime, routeTime, routeDistance, destSysInfo) {
	var result = {};
	// pick a location
	result["locationType"] = Math.floor(Math.random() * this._maxLocations);
	result["quantity"] = 1;
	result["price"] = parseInt((parseInt(Math.random() * 50) + 100) / 10) * 10 + (7 - destSysInfo.government) * 50 +
		worldScripts.GalCopBB_CoreMissionValues.$calcDistanceBonus(routeDistance, 10) + // plus a distance bonus
		worldScripts.GalCopBB_CoreMissionValues.$calcPlayerBonus(500); // plus a possible bonus price, based on player score 
	result["expiry"] = clock.adjustedSeconds + routeTime + workTime; // transit time + 1 hour to complete
	result["penalty"] = parseInt(result.price / 2);
	return result;
}

//-------------------------------------------------------------------------------------------------------------
// 44 - rescue stranded ship interstellar
this.$missionType44_Values = function $missionType44_Values(workTime, routeTime, routeDistance, destSysInfo) {
	var result = {};
	result["quantity"] = 1;
	result["price"] = parseInt((parseInt(Math.random() * 500) + 500) / 10) * 10 +
		worldScripts.GalCopBB_CoreMissionValues.$calcDistanceBonus(routeDistance, 10) + // plus a distance bonus
		worldScripts.GalCopBB_CoreMissionValues.$calcPlayerBonus(1000); // plus a possible bonus price, based on player score 
	result["expiry"] = clock.adjustedSeconds + routeTime + workTime; // transit time + 1 hour to complete
	result["penalty"] = parseInt(result.price / 2);
	return result;
}

//-------------------------------------------------------------------------------------------------------------
// 46 - cargo pickup from waypoint/delivery to waypoint
this.$missionType46_Values = function $missionType46_Values(workTime, routeTime, routeDistance, destSysInfo) {
	if (system.ID === -1) return null;
	// we're leaving out food, textiles and the precious metals and gems
	var c_types = ["luxuries", "computers", "furs", "liquor_wines", "alloys", "radioactives", "alien_items", "machinery", "narcotics", "firearms", "slaves"];
	var result = {};
	result["quantity"] = 0;
	// make sure we don't create missions where the quantity of cargo is more than the player ship
	var max = player.ship.cargoSpaceCapacity;
	// make sure we don't end up with something ridiculous, like 100. make the max possible 25
	if (max > 25) max = 25;
	// work out mission details - commodity and quantity
	result["commodity"] = c_types[Math.floor(Math.random() * c_types.length)];;
	result["quantity"] = parseInt((Math.random() * (max - 3)) + 3);

	var rng = parseInt(expandDescription("[missionType46_destRange]"))
	var sys = destSysInfo.systemsInRange(rng);
	var rt = null;
	for (var i = sys.length - 1; i >= 0; i--) {
		// remove any systems that don't meet conditions
		if (sys[i].systemID === system.ID) {
			// don't include the original system
			sys.splice(i, 1);
			continue;
		}
		// make sure the trip is possible
		rt = system.info.routeToSystem(sys[i]);
		if (!rt) {
			sys.splice(i, 1);
			continue;
		}
	}
	if (sys.length == 0) return null;
	// pick one of the remainder
	var tgtSys = sys[Math.floor(Math.random() * sys.length)];
	result["destinationA"] = tgtSys.systemID;
	rt = destSysInfo.routeToSystem(tgtSys, "OPTIMIZED_BY_JUMPS");

	// make sure the deposit amount is included in the price so the BB knows how to display it
	result["price"] = parseInt(((Math.random() * 250) + 250) / 10) * 10 + (7 - destSysInfo.government) * 50 + (7 - tgtSys.government) * 50 +
		worldScripts.GalCopBB_CoreMissionValues.$calcPlayerBonus(200) + // plus a possible bonus price, based on player score 
		worldScripts.GalCopBB_CoreMissionValues.$calcDistanceBonus(routeDistance + rt.distance, 20); // plus a distance bonus
	result["expiry"] = clock.adjustedSeconds + routeTime + (rt.time * 3600) + 7200; // transit time + 2hrs to complete - would need more if mission required return to original station
	result["penalty"] = 0;

	return result;
}

//-------------------------------------------------------------------------------------------------------------
this.$addTraderRoleToPlayer = function $addTraderRoleToPlayer(cmdty) {
	if (system.ID != -1) {
		var typ = "trader";
		if (system.mainStation.market[cmdty].legality_import > 0 || system.mainStation.market[cmdty].legality_export > 0) typ = "trader-smuggler";
		player.setPlayerRole(typ);
	}
}

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

//-------------------------------------------------------------------------------------------------------------
this.$countDeliveryCargo = function $countDeliveryCargo() {
	var result = 0;
	for (var i = 0; i < this._deliverCargo.length; i++) {
		if (this._deliverCargo[i] && this._deliverCargo[i].isValid && this._deliverCargo[i].status === "STATUS_IN_FLIGHT") result += 1;
	}
	return result;
}

//-------------------------------------------------------------------------------------------------------------
this.$isPlayerNearby = function $isPlayerNearby() {
	// don't run this check if the player is busy
	if (player.alertCondition === 3 || player.alertHostiles === true) return;
	// get all powered ships in range
	var ships = player.ship.checkScanner(true);
	var missTypes = [40, 41, 42];
	for (var i = 0; i < ships.length; i++) {
		if (ships[i].script.hasOwnProperty("_missionID") === true && ships[i].hasRole("gcm_delivery_ship") === true) {
			var item = worldScripts.BulletinBoardSystem.$getItem(ships[i].script._missionID);
			if (item && missTypes.indexOf(item.data.missionType) >= 0) {
				if (ships[i].script.hasOwnProperty("_greeted") === false) {
					this._checkPlayerNearbyTimer.stop();
					ships[i].script._greeted = true;
					var bcc = worldScripts.BroadcastCommsMFD;
					// transmit message
					ships[i].commsMessage(expandDescription("[gcm_delivery_question]", {
						commodity: displayNameForCommodity(item.data.commodity).toLowerCase()
					}), player.ship);
					if (bcc.$checkMessageExists("gcm_player_arrived_cargo") === false) {
						bcc.$createMessage({
							messageName: "gcm_player_arrived_cargo",
							callbackFunction: this.$transmitNoResponse.bind(this),
							displayText: expandDescription("[gcm_greet_ship]"),
							messageText: expandDescription("[gcm_player_arrived_cargo]"),
							ship: ships[i],
							transmissionType: "target",
							deleteOnTransmit: true,
							hideOnConditionRed: true
						});
					}
					bcc.$addShipToArray(ships[i], bcc._greeted);
					bcc.$buildMessageList();
				}
			}
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
// clears out the penalty amount from the mission if the mission has to be failed but it's not the player's fault
this.$isPlayerNearWaypoint = function $isPlayerNearWaypoint() {
	if (this._waypointMissionID === 0) {
		this._checkPlayerNearbyTimer.stop();
		return;
	}
	var wp = system.waypoints["meeting_" + this._waypointMissionID];
	if (!wp) {
		this._checkPlayerNearbyTimer.stop();
		return;
	}
	if (player.ship.position.distanceTo(wp.position) < player.ship.scannerRange) {
		var bb = worldScripts.BulletinBoardSystem;
		var item = bb.$getItem(this._waypointMissionID);
		// set the penalty to 0
		item.penalty = 0;
		// add some text so it doesn't look like a bug
		bb.$updateBBStatusText(item.ID, expandDescription("[gcm_ship_did_not_arrive]"));
		bb.$updateBBManifestText(item.ID, expandDescription("[gcm_ship_did_not_arrive]"));
		item.manifestCallback = "";

		// prevent this mission from having an impact on player reputation
		item.data.terminatePenalty = false;
		this._checkPlayerNearbyTimer.stop();
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$transmitNoResponse = function $transmitNoResponse() {
	// this function intentionally left blank
}

//-------------------------------------------------------------------------------------------------------------
this.$monitorCargoDump = function $monitorCargoDump() {
	var mc = this._monitorCargo;
	if (mc && mc.length > 0) {
		var pos = this.$findWaypointPosition();
		var bb = worldScripts.BulletinBoardSystem;
		var gcm = worldScripts.GalCopBB_Missions;
		var item = bb.$getItem(this._monitorCargoMission);
		for (var i = 0; i < mc.length; i++) {
			var cargo = mc[i];
			if (cargo && cargo.isValid && cargo.isInSpace) {
				if (cargo.position.distanceTo(pos) < 21000 && cargo.speed == 0) {
					if (item.data.quantity < (item.data.targetQuantity - item.data.destroyedQuantity)) {
						item.data.quantity += 1;
						bb.$updateBBMissionPercentage(item.ID, (item.data.quantity / (item.data.targetQuantity - item.data.destroyedQuantity)));
						gcm.$logMissionData(item.ID);
						player.consoleMessage(expandDescription("[goal_updated]"));
					}
					if (item.data.quantity >= (item.data.targetQuantity - item.data.destroyedQuantity)) {
						this._monitorCargoDump.stop();
					}
					// add some scripts to help monitor the status of these cargo pods
					// we'll be checking if the pod is destroyed, or if it's scooped by the player
					//log(this.name, "adding script events " + cargo);
					cargo.script._checkMissionID = this._monitorCargoMission;
					if (cargo.script.$gcm_hold_shipDied) {
						cargo.script.shipDied = cargo.script.$gcm_hold_shipDied;
						delete cargo.script.$gcm_hold_shipDied;
					}
					if (cargo.script.shipDied) cargo.script.$gcm_hold_shipDied = cargo.script.shipDied;
					cargo.script.shipDied = this.$gcd_cargo_shipDied2;

					if (cargo.script.$gcm_hold_shipWasDumped) {
						cargo.script.shipWasDumped = cargo.script.$gcm_hold_shipWasDumped;
						delete cargo.script.$gcm_hold_shipWasDumped;
					}
					if (cargo.script.shipWasDumped) cargo.script.$gcm_hold_shipWasDumped = cargo.script.shipWasDumped;
					cargo.script.shipWasDumped = this.$gcd_cargo_shipWasDumped2;
					// take it out of the monitoring array
					mc[i] = null;
				}
			}
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$checkForCargoDump = function $checkForCargoDump(cargoPod) {
	if (this._monitorCargoMission > 0) {
		var bb = worldScripts.BulletinBoardSystem;
		var item = bb.$getItem(this._monitorCargoMission);
		// only add it to the monitor list if it's the right commodity type
		if (item.data.commodity === cargoPod.commodity) {
			var pos = this.$findWaypointPosition();
			if (cargoPod.position.distanceTo(pos) < 21000) this._monitorCargo.push(cargoPod);
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$findWaypointPosition = function $findWaypointPosition() {
	var wps = system.waypoints;
	var keys = Object.keys(wps);
	if (wps && keys.length > 0) {
		for (var i = 0; i < keys.length; i++) {
			if (wps[keys[i]].beaconLabel == expandDescription("[gcm_cargo_dump_location]")) return wps[keys[i]].position;
		}
	}
	return null;
}

//-------------------------------------------------------------------------------------------------------------
// makes the attackers attack a particular target
this.$giveAttackersTarget = function $giveAttackersTarget() {
	var retry = false;
	var pos = this.$findWaypointPosition();
	if (!pos) return;
	for (var i = 0; i < this._attackers.length; i++) {
		var shp = this._attackers[i];
		if (shp && shp.isValid && shp.isInSpace) {
			if (shp.AIScript && shp.AIScript.oolite_priorityai) {
				if (shp.script._configDone === false) {
					shp.AIScript.oolite_priorityai.setParameter("oolite_pirateLurk", pos);
					shp.AIScript.oolite_priorityai.setParameter("oolite_attackTarget", player.ship);
					shp.AIScript.oolite_priorityai.configurationResetWaypoint();
					shp.AIScript.oolite_priorityai.reconsiderNow();
					shp.script._configDone = true;
				}
			} else {
				retry = true;
				break;
			}
		}
		if (retry === true) break;
	}
	if (this._setupAttackerTarget.isRunning === true) this._setupAttackerTarget.stop();
	if (retry === true) {
		this._setupAttackerTarget = new Timer(this, this.$giveAttackersTarget, 1, 0);
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$setupAmbush = function $setupAmbush() {
	var pos = this.$findWaypointPosition();
	if (!pos) {
		this._ambushTimer.stop();
		return
	}
	if (player.ship.position.distanceTo(pos) < player.ship.scannerRange) {
		this._ambushTimer.stop();
		this.$createAmbush();
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$createAmbush = function $createAmbush() {
	var gcm = worldScripts.GalCopBB_Missions;
	// get the initial number of ships to spawn from the config
	var pop = worldScripts["oolite-populator"];
	var pos = gcm.$findPosition(player.ship.position);
	var types = ["pirate-medium-fighter", "pirate-medium-fighter", "pirate-heavy-fighter"];
	var pirates = system.addShips(types[Math.floor(Math.random() * types.length)], Math.floor(Math.random() * 4) + 3, pos, 800);
	// make sure all the attackers are equipped, have names, and bounties
	for (var i = 0; i < pirates.length; i++) {
		var pr = pirates[i];
		if (gcm._rsnInstalled) pr.shipUniqueName = gcm.$getRandomShipName(pr, "pirate");
		pr.setScript("oolite-default-ship-script.js");
		pr.script._configDone = false;
		// configure our attackers
		pr.setBounty(60 + system.government + Math.floor(Math.random() * 8), "setup actions");
		pr.setCrew({
			name: randomName() + " " + randomName(),
			bounty: pr.bounty,
			insurance: 0
		});
		if (pr.hasHyperspaceMotor) {
			pop._setWeapons(pr, 1.75); // bigger ones sometimes well-armed
		} else {
			pop._setWeapons(pr, 1.3); // rarely well-armed
		}
		pop._setSkill(pr, 4 - system.info.government);
		if (Math.random() * 16 < system.info.government) pop._setMissiles(pr, -1);

		// make sure the AI is switched
		pr.switchAI("gcm-attackerAI.js");
		this._attackers.push(pr);
	}
	// get ready to set up the target
	this._setupAttackerTarget = new Timer(this, this.$giveAttackersTarget, 1, 0);
}