"use strict";
this.name = "GalCopBB_Satellites";
this.author = "phkb";
this.copyright = "2017 phkb";
this.description = "Uses Satellites as basis for missions (110-129)";
this.license = "CC BY-NC-SA 4.0";

/*
	TODO: 
	check what happens to left over attackers when all sats destroyed
	need to add government bias to mission availability

	Missions involving satellites from Satellites.OXP

	110/111/112/113/114 - Extract data
		*type a: sat 1 - security data (galcop - foreign - risk)
		*type b: sats 2,3 - comm logs (galcop - foreign - risk)
		*type c: sat 4 - scan data from telescope (galcop - foreign - risk)
		*type d: sat 1 - security data (pirate - local - risk)
			where is the risk? no chance for witchpoint assassins if the task is local
			need initial step which is risky - how to get passcode?
			maybe transmit an encrypted packet to a police ship with device while police is occupied (fighting)
			doing it if they aren't fighting will mean a mission fail and legal penalty
		type e: sats 2,3 - comm logs (pirate - local - norisk)
	115/116/117 - Install new firmware
		*type a - local system update (galcop - no risk maintenance)
		type b - local system update (pirate - high risk subvert)
		type c - foreign system update (galcop - high risk subvert)
	118/119/120/121 - insert data
		*type a - sat 1 (galcop - foreign - hi risk) 
		type b - sats 2,3 (galcop - foreign - hi risk)
		type c - sat 1 (pirate - local - hi risk)
		type d - sats 2,3 (pirate - local - hi risk)
	122/123 - Destroy sats
		*type a - destroy all type 1,2,3 (galcop, foreign)
		type b - destroy all type 1,2,3 (pirate, local)
		todo: get manifest entry to have number of satellites left to destroy
	124 - Defend satellites
		*type a - defend a satellite(s) from a number of enemies (galcop, local)
		might need signal/comms message to let player know when to move from one satellite to another if there is more than one.
		mission ends when all attackers have been eliminated
		attacked spawned just our of scanner range, away from the player's current view

		adjust beacon on specific target satellite with "**"
		give player x minutes to reach satellite, then start spawning ships
		need "invader" AI to target the satellite unless the player intervenes

	for data interface types, player will be given a one-time passcode, that can only be used between particular times
	(period will be 15 min)
	using the code outside those times will invalidate the mission and potentially give player offender status
	(if any galcop ships are in range when passcode sent)
	all mission text should say "all surveillance satellites" or "all COM satellites" in system, as there may be more than 1

	TODO:
	need differentiation between missions - different challenges
	firmware will be a longer install period
	destroy mission - make sure there are some police on orbit patrol
	police orbit patrol ai - similar to satellite ai, but with ability to switch to standard police ai when offence committed
	test guard AI
	Idea: mission to get passcode? fly to way point, wait for ship?
*/

this._debug = false;
this._rsnInstalled = false;
this._initial = false;
this._satArray = {};
this._secCodeMissionTypes = [110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121];
this._secCodeIllegalTypes = [110, 111, 112, 113, 114, 116, 117, 118, 119, 120, 121];
this._functionAMissionTypes = [110, 111, 112, 113, 114];
this._functionBMissionTypes = [115, 116, 117];
this._functionCMissionTypes = [118, 119, 120, 121];
this._satTarget = null;
this._satRespondTimer = null;
this._satRange = 2000;
this._codeExpiry = 0;
this._dataReceived = 0;
this._dataTransferTimer = null;
this._dataTransferFreq = 0;
this._dataTransferCounter = 0;
this._updateType = 0;
this._msgCounter = 0;
this._seenByPolice = false;
this._satDefenders = [];
this._satDefenderTimer = null;
this._satToDefend = null;
this._satAttackers = [];
this._attackers = [];
this._satAttackerTimer = null;
this._beginAttackWaves = null;
this._wavesToCreate = 0;
this._preferredAssassinLightShips = [];
this._preferredAssassinMediumShips = [];
this._countDelay = 0;

//-------------------------------------------------------------------------------------------------------------
this.startUp = function () {
	// disable the default scripts for satellites - we'll create them during system population
	if (worldScripts["Satellite"]) {
		if (missionVariables.GalCopBBMissions_Satellites) {
			this._satArray = JSON.parse(missionVariables.GalCopBBMissions_Satellites);
		}

		// prepopulate for testing
		//for (var i = 0; i <= 255; i++) {
		//    this._satArray[i] = [1,1,1,1];
		//}

		var gcm = worldScripts.GalCopBB_Missions;
		// add these mission types into the main control
		var list = [110, 111, 112, 115, 118, 122, 123, 124];
		gcm._availableMissionTypes = gcm._availableMissionTypes.concat(list);
		//gcm._availableMissionTypes.push(113); disabled for the moment
		this._debug = gcm._debug;
		this.$getRandomShipName = gcm.$getRandomShipName;

	} else {
		delete this.systemWillPopulate;
		delete this.systemWillRepopulate;
		delete this.startUpComplete;
		delete this.playerWillSaveGame;
		delete this.playerEnteredNewGalaxy;
	}
	// set a flag if random ship names is installed
	if (worldScripts["randomshipnames"]) this._rsnInstalled = true;
}

//-------------------------------------------------------------------------------------------------------------
this.playerEnteredNewGalaxy = function () {
	this._satArray = {};
}

//-------------------------------------------------------------------------------------------------------------
this.playerWillSaveGame = function () {
	missionVariables.GalCopBBMissions_Satellites = JSON.stringify(this._satArray);
}

//-------------------------------------------------------------------------------------------------------------
this.systemWillPopulate = function () {
	this._initial = true;
	this.$getAssassinShipLists();
}

//-------------------------------------------------------------------------------------------------------------
this.systemWillRepopulate = function () {
	if (this._initial === true) {
		this._initial = false;
		this.$checkForActiveMissions();
		this.$checkSatellitesInSystem();
	}
}

//-------------------------------------------------------------------------------------------------------------
this.shipWillLaunchFromStation = function (station) {
	this.$checkForActiveMissions();
}

//-------------------------------------------------------------------------------------------------------------
this.shipWillEnterWitchspace = function (cause, destination) {
	this.$stopTimers();
	this._codeExpiry = 0;
}

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

//-------------------------------------------------------------------------------------------------------------
this.$stopTimers = function $stopTimers() {
	if (this._satRespondTimer && this._satRespondTimer.isRunning) this._satRespondTimer.stop();
	delete this._satRespondTimer;
	if (this._dataTransferTimer && this._dataTransferTimer.isRunning) this._dataTransferTimer.stop();
	delete this._dataTransferTimer;
	if (this._satDefenderTimer && this._satDefenderTimer.isRunning) this._satDefenderTimer.stop();
	delete this._satDefenderTimer;
	if (this._satAttackerTimer && this._satAttackerTimer.isRunning) this._satAttackerTimer.stop();
	delete this._satAttackerTimer;
	if (this._beginAttackWaves && this._beginAttackWaves.isRunning) this._beginAttackWaves.stop();
	delete this._beginAttackWaves;
}

//-------------------------------------------------------------------------------------------------------------
this.$checkSatellitesInSystem = function $checkSatellitesInSystem() {
	var sats = system.shipsWithRole("RSsatellite");
	if (sats.length > 0) {
		var ary = [0, 0, 0, 0];
		for (var i = 0; i < sats.length; i++) {
			// find all satellites and add a bcc message to them
			this.$resetSatComms(sats[i]);
			// update array values
			if (sats[i].dataKey === "RSSatellite_1") ary[0] += 1;
			if (sats[i].dataKey === "RSSatellite_2") ary[1] += 1;
			if (sats[i].dataKey === "RSSatellite_3") ary[2] += 1;
			if (sats[i].dataKey === "RSSatellite_4") ary[3] += 1;
		}
		this._satArray[system.ID] = ary;
	} else {
		this._satArray[system.ID] = [0, 0, 0, 0];
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$checkForActiveMissions = function $checkForActiveMissions() {
	var gcm = worldScripts.GalCopBB_Missions;
	var list = gcm.$getListOfMissions(true, [110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 122, 123, 124]);
	if (list.length > 0) {
		// loop through all active missions and see if any need to be set up for this system
		for (var i = 0; i < list.length; i++) {
			// *** type 110/111/112/113/114/115/116 - extract data/update firmware/upload data
			if (this._secCodeMissionTypes.indexOf(list[i].data.missionType) >= 0 &&
				list[i].destination === system.ID &&
				list[i].data.quantity === 0 &&
				list[i].data.destroyedQuantity === 0) {

				// find the right satellite
				var sats = system.shipsWithRole("RSsatellite");
				var pcode = expandDescription("[gcm_passcode]");
				if (this._codeExpiry === 0 && list[i].expiry != -1) this._codeExpiry = list[i].expiry; //clock.adjustedSeconds + (16 * 60);
				// for the 115 mission there is no expiry on the passcode
				if (list[i].data.missionType === 115) this._codeExpiry = clock.adjustedSeconds + (86400 * 10);
				for (var j = 0; j < sats.length; j++) {
					// we're going to slow down the max speed of these satellites so the player has some chance of keeping up no matter what they're flying
					var add = false;
					if (sats[j].dataKey === "RSSatellite_1" && list[i].data.satelliteTypes.indexOf("1") >= 0) {
						sats[j].maxSpeed = player.ship.maxSpeed - 30;
						add = true;
					}
					if (sats[j].dataKey === "RSSatellite_2" && list[i].data.satelliteTypes.indexOf("2") >= 0) {
						sats[j].maxSpeed = player.ship.maxSpeed - 50;
						add = true;
					}
					if (sats[j].dataKey === "RSSatellite_3" && list[i].data.satelliteTypes.indexOf("3") >= 0) {
						sats[j].maxSpeed = player.ship.maxSpeed - 70;
						add = true;
					}
					if (sats[j].dataKey === "RSSatellite_4" && list[i].data.satelliteTypes.indexOf("4") >= 0) add = true;

					// set up broadcast comms interface
					if (add === true) {
						// make sure our mission satellites have a beacon, and a meaningful label
						this.$addBeaconToSatellite(sats[j]);
						var scr = sats[j].script;
						// add the mission id and passcode
						scr._missionID = list[i].ID;
						scr._passcode = pcode;
						if (this._secCodeIllegalTypes.indexOf(list[i].data.missionType) >= 0) {
							scr._checkPolice = true;
						}
						// plug in our ship scripts
						if (scr.shipBeingAttacked && !scr.$gcm_hold_shipBeingAttacked) scr.$gcm_hold_shipBeingAttacked = scr.shipBeingAttacked;
						scr.shipBeingAttacked = this.$sat_shipBeingAttacked;
						if (scr.shipAttackedWithMissile && !scr.$gcm_hold_shipAttackedWithMissile) scr.$gcm_hold_shipAttackedWithMissile = scr.shipAttackedWithMissile;
						scr.shipAttackedWithMissile = this.$sat_shipAttackedWithMissile;
					}
				}
			}

			// defend
			if (list[i].data.missionType === 124 &&
				list[i].destination === system.ID &&
				list[i].data.quantity < (list[i].data.targetQuantity - list[i].data.destroyedQuantity) &&
				(list[i].expiry === -1 || list[i].expiry > clock.adjustedSeconds)) {
				// slow down any satellites currently in system so the player and attackers can catch them
				var sats = system.shipsWithRole("RSsatellite");
				for (var j = 0; j < sats.length; j++) {
					var scr = sats[j].script;
					if (scr.shipDied && !scr.$gcm_hold_shipDied) scr.$gcm_hold_shipDied = scr.shipDied;
					scr.shipDied = this.$sat_shipDied;
					this.$addBeaconToSatellite(sats[j]);
					scr._missionID = list[i].ID;
					if (sats[j].dataKey === "RSSatellite_1") sats[j].maxSpeed = 200;
					if (sats[j].dataKey === "RSSatellite_2") sats[j].maxSpeed = 170;
					if (sats[j].dataKey === "RSSatellite_3") sats[j].maxSpeed = 150;
					if (sats[j].dataKey === "RSSatellite_4") sats[j].maxSpeed = 100;
				}
				var satIndex = Math.floor(Math.random() * sats.length);
				sats[satIndex].beaconLabel = "**" + sats[satIndex].beaconLabel;
				this._satToDefend = sats[satIndex];
				this._wavesToCreate = Math.floor(Math.random() * 3) + 1;
				if (!this._beginAttackWaves || this._beginAttackWaves.isRunning === false)
					this._beginAttackWaves = new Timer(this, this.$beginAttacks, 10, 10);
				this._countDelay = 0;
			}

			// destroy satellites
			if ((list[i].data.missionType === 122 || list[i].data.missionType === 123) &&
				list[i].destination === system.ID &&
				list[i].data.quantity < (list[i].data.targetQuantity - list[i].data.destroyedQuantity) &&
				list[i].expiry > clock.adjustedSeconds) {
				// add some lurking assassins around the satellites
				var sats = system.shipsWithRole("RSsatellite");
				for (var j = 0; j < sats.length; j++) {
					// we're going to slow down the max speed of these satellites so the guards have some chance of keeping up
					var add = false;
					if ((sats[j].dataKey === "RSSatellite_1") && list[i].data.satelliteTypes.indexOf("1") >= 0) {
						sats[j].maxSpeed = 200;
						add = true;
					}
					if ((sats[j].dataKey === "RSSatellite_2") && list[i].data.satelliteTypes.indexOf("2") >= 0) {
						sats[j].maxSpeed = 170;
						add = true;
					}
					if ((sats[j].dataKey === "RSSatellite_3") && list[i].data.satelliteTypes.indexOf("3") >= 0) {
						sats[j].maxSpeed = 150;
						add = true;
					}
					if (add === true) {
						var scr = sats[j].script;
						if (scr.shipDied && !scr.$gcm_hold_shipDied) scr.$gcm_hold_shipDied = scr.shipDied;
						scr.shipDied = this.$sat_shipDied;
						if (scr.shipBeingAttacked && !scr.$gcm_hold_shipBeingAttacked) scr.$gcm_hold_shipBeingAttacked = scr.shipBeingAttacked;
						scr.shipBeingAttacked = this.$sat_shipBeingAttacked;
						if (scr.shipBeingAttackedUnsuccessfully && !scr.$gcm_hold_shipBeingAttackedUnsuccessfully) scr.$gcm_hold_shipBeingAttackedUnsuccessfully = scr.shipBeingAttackedUnsuccessfully;
						scr.shipBeingAttackedUnsuccessfully = this.$sat_shipBeingAttackedUnsuccessfully;
						if (scr.shipAttackedWithMissile && !scr.$gcm_hold_shipAttackedWithMissile) scr.$gcm_hold_shipAttackedWithMissile = scr.shipAttackedWithMissile;
						scr.shipAttackedWithMissile = this.$sat_shipAttackedWithMissile;

						// make sure our mission satellites have a beacon, and a meaningful label
						this.$addBeaconToSatellite(sats[j]);
						// add the guards for this satellite
						this.$addGuards(sats[j]);
						// add our mission ID
						scr._missionID = list[i].ID;
					}
				}
			}
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$addBeaconToSatellite = function $addBeaconToSatellite(sat) {
	sat.beaconCode = "RSSatellite_location";
	sat.beaconLabel = sat.name;
}

//-------------------------------------------------------------------------------------------------------------
this.$resetSatComms = function $resetSatComms(sat) {
	var bcc = worldScripts.BroadcastCommsMFD;
	this._msgCounter += 1;
	var msg = "";
	if (sat.script._passcode) {
		msg = expandDescription("[gcm_transmitting_sat_passcode]", { code: sat.script._passcode });
	} else {
		msg = expandDescription("[gcm_transmitting_sat_passcode]", { code: expandDescription("[gcm_default_sat_passcode]") });
	}
	bcc.$createMessage({
		messageName: "gcm_transmit_sat_passcode_" + this._msgCounter,
		callbackFunction: this.$transmitSecurityCode.bind(this),
		displayText: expandDescription("[gcm_transmit_passcode_player]"),
		messageText: msg,
		ship: sat,
		transmissionType: "target",
		deleteOnTransmit: true,
		delayCallback: 2,
		hideOnConditionRed: true
	});
	if (bcc.$checkMessageExists("gcm_transmit_function_A") === true) bcc.$removeMessage("gcm_transmit_function_A");
	if (bcc.$checkMessageExists("gcm_transmit_function_B") === true) bcc.$removeMessage("gcm_transmit_function_B");
	if (bcc.$checkMessageExists("gcm_transmit_function_C") === true) bcc.$removeMessage("gcm_transmit_function_C");
}

//-------------------------------------------------------------------------------------------------------------
this.$clearSatFunctions = function $clearSatFunctions(sat) {
	var bcc = worldScripts.BroadcastCommsMFD;
	if (bcc.$checkMessageExists("gcm_transmit_function_A") === true) bcc.$removeMessage("gcm_transmit_function_A");
	if (bcc.$checkMessageExists("gcm_transmit_function_B") === true) bcc.$removeMessage("gcm_transmit_function_B");
	if (bcc.$checkMessageExists("gcm_transmit_function_C") === true) bcc.$removeMessage("gcm_transmit_function_C");
}

//-------------------------------------------------------------------------------------------------------------
this.$transmitSecurityCode = function $transmitSecurityCode() {
	var target = player.ship.target;
	if (player.ship.position.distanceTo(target) > this._satRange) {
		player.consoleMessage(expandDescription("[gcm_satellite_out_of_range]"));
		this.$resetSatComms(target);
		return;
	}
	target.commsMessage(expandDescription("[gcm_analysing_code_satellite]"), player.ship);
	// start a timer to respond to passcode
	this._satTarget = target;
	this._satRespondTimer = new Timer(this, this.$checkPasscode.bind(this), 10, 0);
}

//-------------------------------------------------------------------------------------------------------------
this.$checkPasscode = function $checkPasscode() {
	if (player.ship.position.distanceTo(this._satTarget) > this._satRange) {
		player.consoleMessage(expandDescription("[gcm_satellite_out_of_range]"));
		this.$resetSatComms(this._satTarget);
		this._satTarget = null;
	}
	if (player.ship.target != this._satTarget) {
		player.consoleMessage(expandDescription("[gcm_comms_link_disconnected]"));
		this.$resetSatComms(this._satTarget);
		this._satTarget = null;
	}
	if (this._codeExpiry != 0 && clock.adjustedSeconds > this._codeExpiry) {
		this._satTarget.commsMessage(expandDescription("[gcm_satellite_expired]"), player.ship);
		this.$resetSatComms(this._satTarget);
		this._satTarget = null;
		return;
	}
	if (this._codeExpiry === 0 || !this._satTarget.script._passcode) {
		this._satTarget.commsMessage(expandDescription("[gcm_satellite_invalid]"), player.ship);
		// result? penalise player if police in range?
		this.$resetSatComms(this._satTarget);
		this._satTarget = null;
		return;
	}
	this._satTarget.commsMessage(expandDescription("[gcm_passcode_success_sat]"), player.ship);
	var bcc = worldScripts.BroadcastCommsMFD;
	bcc.$createMessage({
		messageName: "gcm_transmit_function_A",
		callbackFunction: this.$transmitFunctionA.bind(this),
		displayText: "--" + expandDescription("[gcm_satellite_download]"),
		messageText: expandDescription("[gcm_satellite_download]"),
		ship: this._satTarget,
		transmissionType: "target",
		deleteOnTransmit: false,
		delayCallback: 3,
		hideOnConditionRed: true
	});
	var bcc = worldScripts.BroadcastCommsMFD;
	bcc.$createMessage({
		messageName: "gcm_transmit_function_B",
		callbackFunction: this.$transmitFunctionB.bind(this),
		displayText: "--" + expandDescription("[gcm_satellite_firmware]"),
		messageText: expandDescription("[gcm_satellite_firmware]"),
		ship: this._satTarget,
		transmissionType: "target",
		deleteOnTransmit: false,
		delayCallback: 3,
		hideOnConditionRed: true
	});
	var bcc = worldScripts.BroadcastCommsMFD;
	bcc.$createMessage({
		messageName: "gcm_transmit_function_C",
		callbackFunction: this.$transmitFunctionC.bind(this),
		displayText: "--" + expandDescription("[gcm_satellite_upload]"),
		messageText: expandDescription("[gcm_satellite_upload]"),
		ship: this._satTarget,
		transmissionType: "target",
		deleteOnTransmit: false,
		delayCallback: 3,
		hideOnConditionRed: true
	});
}

//-------------------------------------------------------------------------------------------------------------
this.$transmitFunctionA = function $transmitFunctionA() {
	if (this._dataTransferTimer && this._dataTransferTimer.isRunning) return;
	if (player.ship.position.distanceTo(this._satTarget) > this._satRange) {
		player.consoleMessage(expandDescription("[gcm_satellite_out_of_range]"));
		this.$resetSatComms(this._satTarget);
		this._satTarget = null;
	}
	var item = worldScripts.BulletinBoardSystem.$getItem(this._satTarget.script._missionID);
	if (this._functionAMissionTypes.indexOf(item.data.missionType) >= 0) {
		this._updateType = 1;
		this._satTarget.commsMessage(expandDescription("[gcm_satellite_commencing_" + this._updateType + "]"), player.ship);
		this._dataTransferFreq = 5;
		this._dataTransferCounter = 0;
		this._dataTransferTimer = new Timer(this, this.$checkRangeToSatellite.bind(this), 1, 1);
		this._dataReceived = 0;
		this.$clearSatFunctions(this._satTarget);
	} else {
		this._satTarget.commsMessage(expandDescription("[gcm_invalid_function]"), player.ship);
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$transmitFunctionB = function $transmitFunctionB() {
	if (this._dataTransferTimer && this._dataTransferTimer.isRunning) return;
	if (player.ship.position.distanceTo(this._satTarget) > this._satRange) {
		player.consoleMessage(expandDescription("[gcm_satellite_out_of_range]"));
		this.$resetSatComms(this._satTarget);
		this._satTarget = null;
	}
	var item = worldScripts.BulletinBoardSystem.$getItem(this._satTarget.script._missionID);
	if (this._functionBMissionTypes.indexOf(item.data.missionType) >= 0) {
		this._updateType = 2;
		this._satTarget.commsMessage(expandDescription("[gcm_satellite_commencing_" + this._updateType + "]"), player.ship);
		this._dataTransferFreq = 10;
		this._dataTransferCounter = 0;
		this._dataTransferTimer = new Timer(this, this.$checkRangeToSatellite.bind(this), 1, 1);
		this._dataReceived = 0;
		this.$clearSatFunctions(this._satTarget);
	} else {
		this._satTarget.commsMessage(expandDescription("[gcm_invalid_function]"), player.ship);
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$transmitFunctionC = function $transmitFunctionC() {
	if (this._dataTransferTimer && this._dataTransferTimer.isRunning) return;
	if (player.ship.position.distanceTo(this._satTarget) > this._satRange) {
		player.consoleMessage(expandDescription("[gcm_satellite_out_of_range]"));
		this.$resetSatComms(this._satTarget);
		this._satTarget = null;
	}
	var item = worldScripts.BulletinBoardSystem.$getItem(this._satTarget.script._missionID);
	if (this._functionCMissionTypes.indexOf(item.data.missionType) >= 0) {
		this._updateType = 3;
		this._satTarget.commsMessage(expandDescription("[gcm_satellite_commencing_" + this._updateType + "]"), player.ship);
		this._dataTransferFreq = 10;
		this._dataTransferTimer = new Timer(this, this.$checkRangeToSatellite.bind(this), 1, 1);
		this._dataReceived = 0;
		this._dataTransferCounter = 0;
		this.$clearSatFunctions(this._satTarget);
	} else {
		this._satTarget.commsMessage(expandDescription("[gcm_invalid_function]"), player.ship);
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$checkRangeToSatellite = function $checkRangeToSatellite() {
	if (this._satTarget.isValid === false) {
		this._dataTransferTimer.stop();
		return;
	}
	var p = player.ship;
	var pos = p.position;
	var sat = this._satTarget;
	if (pos.distanceTo(sat) > this._satRange) {
		player.consoleMessage(expandDescription("[gcm_satellite_out_of_range]") + " " + expandDescription("[gcm_satellite_interrupted_" + this._updateType + "]"));
		this._dataTransferTimer.stop();
		this.$resetSatComms(sat);
		return;
	}
	if (p.target != sat) {
		player.consoleMessage(expandDescription("[gcm_comms_link_disconnected]"));
		this._dataTransferTimer.stop();
		this.$resetSatComms(sat);
		return;
	}
	// being spotted by a police ship will gain you a bounty, but won't fail the mission
	if (sat._checkPolice && sat._checkPolice === true && this._seenByPolice === false) {
		var police = this.$findLawVessels(p);
		if (police.length > 0) {
			this._seenByPolice = true;
			msg = expandDescription("[gcm_wbsa_detected_police]");
			penalty = (Math.random() * 20) + 10;
			p.setBounty(player.bounty + penalty, "seen by police");
			police[0].commsMessage(msg, p);
		}
	}
	// the timer runs every second, by the actual data transfer happens at another frequency
	this._dataTransferCounter += 1;
	if (this._dataTransferCounter < this._dataTransferFreq) return;
	this._dataTransferCounter = 0;
	this._dataReceived += 1;
	player.consoleMessage(expandDescription("[gcm_satellite_progress_" + this._updateType + "]", { complete: (this._dataReceived * 10) }));
	if (this._dataReceived >= 10) {
		this._dataReceived = 0;
		this._dataTransferTimer.stop();
		this._dataTransferTimer = null;
		this.$dataTransferComplete();
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$dataTransferComplete = function $dataTransferComplete() {
	this._satTarget.commsMessage(expandDescription("[gcm_satellite_complete_" + this._updateType + "]"), player.ship);
	var missID = this._satTarget.script._missionID;
	if (!missID) {
		this.$resetSatComms(this._satTarget);
		this._satTarget = null;
		return;
	}
	var bb = worldScripts.BulletinBoardSystem;
	var item = bb.$getItem(missID);
	item.data.quantity += 1;
	// the mission can now be updated
	bb.$updateBBMissionPercentage(missID, item.data.quantity / item.data.targetQuantity);

	worldScripts.GalCopBB_Missions.$logMissionData(item.ID);
	player.consoleMessage(expandDescription("[goal_updated]"));

	delete this._satTarget.script._missionID;
	delete this._satTarget.script._passcode;
	this.$resetSatComms(this._satTarget);
	this._satTarget = null;
}

//-------------------------------------------------------------------------------------------------------------
// find all police in range of ship
this.$findLawVessels = function $findLawVessels(npc) {
	var ships = npc.checkScanner(true);
	for (var i = ships.length - 1; i >= 0; i--) {
		if (ships[i].isPolice === false) ships.splice(i, 1);
	}
	return ships;
}

//-------------------------------------------------------------------------------------------------------------
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;
	}
	if (item.data.missionType === 115) {
		// put a serious passcode on all the local satellites
		var sats = system.shipsWithRole("RSsatellite");
		if (sats.length > 0) {
			for (var i = 0; i < sats.length; i++) {
				sats[i].script._passcode = expandDescription("[gcm_passcode]");
			}
		}
	}
	gcm.$updateLastMissionDate(item.source, item.data.missionType);
	gcm.$updateGeneralSettings(item);
}

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

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

//-------------------------------------------------------------------------------------------------------------
this.$confirmCompleted = function $confirmCompleted(missID) {
	var p = player.ship;
	var result = "";
	var bb = worldScripts.BulletinBoardSystem;
	var item = bb.$getItem(missID);
	if (item) {

	}
	return result;
}

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

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

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

//-------------------------------------------------------------------------------------------------------------
// 110 - extract data from surveillance satellites
this.$missionType110_Values = function $missionType110_Values(workTime, routeTime, routeDistance, destSysInfo) {
	var result = {};
	result["quantity"] = this._satArray[destSysInfo.systemID][0];
	result["price"] = parseInt((parseInt(Math.random() * 1000) + 1000) / 10) * 10 +
		worldScripts.GalCopBB_CoreMissionValues.$calcPlayerBonus(500); // plus a possible bonus price, based on player score 
	result["expiry"] = clock.adjustedSeconds + routeTime + (workTime * 2); // transit time + 1 hour to complete
	result["penalty"] = parseInt(result.price * 0.5); // failure is costly in this case
	result["satelliteTypes"] = ["1"];
	return result;
}

//-------------------------------------------------------------------------------------------------------------
// 111 - extract data from comms satellites
this.$missionType111_Values = function $missionType111_Values(workTime, routeTime, routeDistance, destSysInfo) {
	var result = {};
	result["quantity"] = this._satArray[destSysInfo.systemID][1] + this._satArray[destSysInfo.systemID][2];
	result["price"] = parseInt((parseInt(Math.random() * 1000) + 1000) / 10) * 10 +
		worldScripts.GalCopBB_CoreMissionValues.$calcPlayerBonus(500); // plus a possible bonus price, based on player score 
	result["expiry"] = clock.adjustedSeconds + routeTime + (workTime * 2); // transit time + 1 hour to complete
	result["penalty"] = parseInt(result.price * 0.5); // failure is costly in this case
	result["satelliteTypes"] = ["2", "3"];
	return result;
}

//-------------------------------------------------------------------------------------------------------------
// 112 - extract data from telescope satellites
this.$missionType112_Values = function $missionType112_Values(workTime, routeTime, routeDistance, destSysInfo) {
	var result = {};
	result["quantity"] = this._satArray[destSysInfo.systemID][3];
	result["price"] = parseInt((parseInt(Math.random() * 500) + 500) / 10) * 10 +
		worldScripts.GalCopBB_CoreMissionValues.$calcPlayerBonus(500); // plus a possible bonus price, based on player score 
	result["expiry"] = clock.adjustedSeconds + routeTime + (workTime * 2); // transit time + 1 hour to complete
	result["penalty"] = parseInt(result.price * 0.5); // failure is costly in this case
	result["satelliteTypes"] = ["4"];
	return result;
}

//-------------------------------------------------------------------------------------------------------------
// 113 - extract data from sys and comms satellites
this.$missionType113_Values = function $missionType113_Values(workTime, routeTime, routeDistance, destSysInfo) {
	var result = {};
	result["quantity"] = this._satArray[destSysInfo.systemID][0] + this._satArray[destSysInfo.systemID][1] + this._satArray[destSysInfo.systemID][2];
	result["price"] = parseInt((parseInt(Math.random() * 1000) + 1000 + parseInt(Math.random() * 1000) + 1000) / 10) * 10 +
		worldScripts.GalCopBB_CoreMissionValues.$calcPlayerBonus(500); // plus a possible bonus price, based on player score 
	result["expiry"] = clock.adjustedSeconds + routeTime + (workTime * 2); // transit time + 1 hour to complete
	result["penalty"] = parseInt(result.price * 0.5); // failure is costly in this case
	result["satelliteTypes"] = ["1", "2", "3"];
	return result;
}

//-------------------------------------------------------------------------------------------------------------
// 115 - update firmware of satellites
this.$missionType115_Values = function $missionType115_Values(workTime, routeTime, routeDistance, destSysInfo) {
	var result = {};
	result["quantity"] = this._satArray[destSysInfo.systemID][0] + this._satArray[destSysInfo.systemID][1] + this._satArray[destSysInfo.systemID][2] + this._satArray[destSysInfo.systemID][3];
	//log(this.name, "quantity " + result.quantity);
	result["price"] = (parseInt((parseInt(Math.random() * 40) + 40) / 10) * 10) * result.quantity +
		worldScripts.GalCopBB_CoreMissionValues.$calcPlayerBonus(500); // plus a possible bonus price, based on player score 
	result["expiry"] = clock.adjustedSeconds + routeTime + 3600 + (900 * result.quantity); // transit time + 60 mins + extra 15 mins per satellite to complete
	result["penalty"] = 0;
	result["satelliteTypes"] = ["1", "2", "3", "4"];
	return result;
}

//-------------------------------------------------------------------------------------------------------------
// 118 - upload data to surveillance satellite
this.$missionType118_Values = function $missionType118_Values(workTime, routeTime, routeDistance, destSysInfo) {
	var result = {};
	result["quantity"] = 1;
	result["price"] = parseInt((parseInt(Math.random() * 1000) + 1000 + parseInt(Math.random() * 1000) + 1000) / 10) * 10 +
		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 * 0.5); // failure is costly in this case
	result["satelliteTypes"] = ["1"];
	return result;
}

//-------------------------------------------------------------------------------------------------------------
// 122 - destroy sys and comms satellites
this.$missionType122_Values = function $missionType122_Values(workTime, routeTime, routeDistance, destSysInfo) {
	var result = {};
	result["quantity"] = this._satArray[destSysInfo.systemID][0] + this._satArray[destSysInfo.systemID][1] + this._satArray[destSysInfo.systemID][2];
	result["price"] = parseInt(parseInt(Math.random() * 1000 + 500) * result.quantity) + (parseInt(Math.random() * 1000 + 1000) / 10) * 10 +
		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 * 0.5); // failure is costly in this case
	result["satelliteTypes"] = ["1", "2", "3"];
	return result;
}

//-------------------------------------------------------------------------------------------------------------
// 123 - destroy sys and comms satellites (pirate)
this.$missionType123_Values = function $missionType123_Values(workTime, routeTime, routeDistance, destSysInfo) {
	var result = {};
	result["quantity"] = this._satArray[destSysInfo.systemID][0] + this._satArray[destSysInfo.systemID][1] + this._satArray[destSysInfo.systemID][2];
	result["price"] = parseInt(parseInt(Math.random() * 1000 + 1000) * result.quantity) + (parseInt(Math.random() * 1000 + 1000) / 10) * 10 +
		worldScripts.GalCopBB_CoreMissionValues.$calcPlayerBonus(500); // plus a possible bonus price, based on player score 
	result["expiry"] = clock.adjustedSeconds + workTime; // transit time + 1 hour to complete
	result["penalty"] = parseInt(result.price * 0.5); // failure is costly in this case
	result["satelliteTypes"] = ["1", "2", "3"];
	return result;
}

//-------------------------------------------------------------------------------------------------------------
// 124 - defend satellites
this.$missionType124_Values = function $missionType124_Values(workTime, routeTime, routeDistance, destSysInfo) {
	var result = {};
	result["quantity"] = this._satArray[destSysInfo.systemID][0] + this._satArray[destSysInfo.systemID][1] + this._satArray[destSysInfo.systemID][2] + this._satArray[destSysInfo.systemID][3];
	result["price"] = parseInt((parseInt(Math.random() * 1000 + 800) * result.quantity + parseInt(Math.random() * 1000) + 1000) / 10) * 10 +
		worldScripts.GalCopBB_CoreMissionValues.$calcPlayerBonus(500); // plus a possible bonus price, based on player score 
	result["expiry"] = -1;
	result["penalty"] = parseInt(result.price * 0.5); // failure is costly in this case
	result["satelliteTypes"] = ["1", "2", "3", "4"];
	return result;
}

//-------------------------------------------------------------------------------------------------------------
this.$sat_shipBeingAttacked = function $sat_shipBeingAttacked(whom, alt) {
	if (!alt)
		if (this.ship.script.$gcm_hold_shipBeingAttacked) this.ship.script.$gcm_hold_shipBeingAttacked(whom);
	this.ship.target = whom;
	this.ship.script._attacker = whom;
	var p = player.ship;
	var s = worldScripts.GalCopBB_Satellites;
	if (s._satTarget === this.ship) {
		if (s._dataTransferTimer && s._dataTransferTimer.isRunning) {
			s._dataTransferTimer.stop();
			s._satTarget.script._passcode = "";
			if (p.position.distanceTo(s._satTarget) < p.scannerRange) {
				s._satTarget.commsMessage(expandDescription("[gcm_satellite_under_attack]"), p);
			}
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$sat_shipBeingAttackedUnsuccessfully = function $sat_shipBeingAttackedUnsuccessfully(whom) {
	if (this.ship.script.$gcm_hold_shipBeingAttackedUnsuccessfully) this.ship.script.$gcm_hold_shipBeingAttackedUnsuccessfully(whom);
	this.ship.script.shipBeingAttacked(whom, true);
}

//-------------------------------------------------------------------------------------------------------------
this.$sat_shipAttackedWithMissile = function $sat_shipAttackedWithMissile(missile, whom) {
	if (this.ship.script.$gcm_hold_shipAttackedWithMissile) this.ship.script.$gcm_hold_shipAttackedWithMissile(missile, whom);
	this.ship.script.shipBeingAttacked(whom, true);
}

//-------------------------------------------------------------------------------------------------------------
this.$sat_shipDied = function $sat_shipDied(whom, why) {
	if (this.ship.script.$gcm_hold_shipDied) this.ship.script.$gcm_hold_shipDied(whom, why);
	if (this.ship.script._missionID) {
		var bb = worldScripts.BulletinBoardSystem;
		var item = bb.$getItem(this.ship.script._missionID);
		if (item.data.missionType === 122) {
			item.data.quantity += 1;
			// the mission can now be updated
			bb.$updateBBMissionPercentage(item.ID, item.data.quantity / item.data.targetQuantity);

			worldScripts.GalCopBB_Missions.$logMissionData(item.ID);
			player.consoleMessage(expandDescription("[goal_updated]"));
		}
		if (item.data.missionType === 124) {
			item.data.destroyedQuantity += 1;
			var s = worldScripts.GalCopBB_Satellites;
			player.consoleMessage(expandDescription("[gcm_satellite_destroyed]"), 5);
			// the mission can now be updated
			bb.$updateBBMissionPercentage(item.ID, item.data.quantity / (item.data.targetQuantity - item.data.destroyedQuantity));

			worldScripts.GalCopBB_Missions.$logMissionData(item.ID);
			player.consoleMessage(expandDescription("[goal_updated]"));
			s.$setNextSatellite();
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$attacker_shipDied = function $attacker_shipDied(whom, why) {
	if (this.ship.script.$gcm_hold_shipDied) this.ship.script.$gcm_hold_shipDied(whom, why);
	if (this.ship.script._missionID) {
		var bb = worldScripts.BulletinBoardSystem;
		var item = bb.$getItem(this.ship.script._missionID);
		if (item.data.missionType === 124) {
			var s = worldScripts.GalCopBB_Satellites;
			for (var i = 0; i < s._attackers.length; i++) {
				if (s._attackers[i] == this.ship) {
					s._attackers.splice(i, 1);
					break;
				}
			}
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
// makes the guards follow a particular target
this.$giveSatDefenderDefendTarget = function $giveSatDefenderDefendTarget() {
	var retry = false;
	for (var i = 0; i < this._satDefenders.length; i++) {
		var shps = this._satDefenders[i].group.ships;
		var tgt = this._satDefenders[i].target;
		for (var j = 0; j < shps.length; j++) {
			var shp = shps[j];
			if (shp.AIScript.oolite_priorityai) {
				shp.AIScript.oolite_priorityai.setParameter("oolite_defendTarget", tgt);
				shp.AIScript.oolite_priorityai.reconsiderNow();
			} else {
				retry = true;
				break;
			}
		}
		if (retry === true) break;
	}
	if (retry === true) {
		this._satDefenderTimer = new Timer(this, this.$giveSatDefenderDefendTarget, 1, 0);
	} else {
		this._satDefenders.length = 0;
	}
}

//-------------------------------------------------------------------------------------------------------------
// makes the attackers attack a particular target
this.$giveSatAttackersTarget = function $giveSatAttackersTarget() {
	var retry = false;
	for (var i = 0; i < this._satAttackers.length; i++) {
		var shps = this._satAttackers[i].group.ships;
		var tgt = this._satAttackers[i].target;
		for (var j = 0; j < shps.length; j++) {
			var shp = shps[j];
			if (shp.AIScript.oolite_priorityai) {
				shp.AIScript.oolite_priorityai.setParameter("oolite_attackTarget", tgt);
				shp.AIScript.oolite_priorityai.configurationResetWaypoint();
				shp.AIScript.oolite_priorityai.reconsiderNow();
			} else {
				retry = true;
				break;
			}
		}
		if (retry === true) break;
	}
	if (retry === true) {
		this._satAttackerTimer = new Timer(this, this.$giveSatAttackersTarget, 1, 0);
	} else {
		this._satAttackers.length = 0;
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$beginAttacks = function $beginAttacks() {
	var p = player.ship;
	if (!p || !p.position) return;
	var dist = p.position.distanceTo(this._satToDefend);
	// don't add any if we're too far away
	if (dist > (p.scannerRange * 0.75)) {
		this._countDelay += 1;
		// has it been too long? (1.16 minutes for each scanner range distance), minimum of 5 minutes
		var delay = Math.floor((dist / p.scannerRange) * 70);
		if (delay < 300) delay = 300;
		if ((this._countDelay * 6) >= delay) {
			// how far away are we
			if (dist > p.scannerRange * 3) {
				// oh dear ... too long
				this._satToDefend.explode();
				this._countDelay = 0;
			}
		}
		return;
	}
	// reset the countdown so we can keep the satellite alive
	this._countDelay = 0;
	var atk = this._attackers;
	var gcm = worldScripts.GalCopBB_Missions;
	// how many left from last wave?
	// check: what happens if ship flees and therefore this._attackers doesn't get updated
	if (atk.length === 0 && this._wavesToCreate <= 0) {
		if (this._satToDefend.isValid === true) {
			var bb = worldScripts.BulletinBoardSystem;
			var item = bb.$getItem(this._satToDefend.script._missionID);
			// the mission can now be updated
			item.data.quantity += 1;
			bb.$updateBBMissionPercentage(item.ID, item.data.quantity / (item.data.targetQuantity - item.data.destroyedQuantity));

			worldScripts.GalCopBB_Missions.$logMissionData(item.ID);
			player.consoleMessage(expandDescription("[goal_updated]"));
			this._satToDefend.beaconLabel = this._satToDefend.beaconLabel.replace("**", "");
			delete this._satToDefend.script._missionID;
			this._satToDefend = null;
			// do we need to go on?
			if (item.data.quantity < (item.data.targetQuantity - item.data.destroyedQuantity)) this.$setNextSatellite();
		}
		// didn't find a satellite to protect? we're done!
		if (this._satToDefend == null) this._beginAttackWaves.stop();
	} else {
		// look for, and clean up, any non-participants
		for (var i = atk.length - 1; i >= 0; i--) {
			// remove derelict ships
			if (atk[i].isDerelict) {
				atk.splice(i, 1);
			} else {
				// ...and ships more than 60000 distance
				var pos1 = atk[i].position;
				if (pos1.distanceTo(this._satToDefend) > (p.scannerRange * 4) && pos1.distanceTo(p) > (p.scannerRange * 4)) {
					atk[i].remove();
					atk.splice(i, 1);
				}
			}
		}
	}

	if (this._satToDefend && atk.length <= Math.floor(Math.random() * 2)) {
		var spwn = gcm.$findPosition(this._satToDefend.script._orbiting.position);
		if (spwn) this.$addAttackers(spwn);
	}
}

//-------------------------------------------------------------------------------------------------------------
// set up the next satellite to defend
this.$setNextSatellite = function $setNextSatellite() {
	// move on to the next satellite
	var sats = system.shipsWithRole("RSsatellite");
	this._satToDefend = null;
	for (var i = 0; i < sats.length; i++) {
		if (sats[i].isValid && sats[i].script._missionID && sats[i].script._missionID > 0) {
			this._satToDefend = sats[i];
			this._satToDefend.beaconLabel = "**" + this._satToDefend.beaconLabel;
			this._wavesToCreate = Math.floor(Math.random() * 3) + 1;
			break;
		}
	}
	if (this._satToDefend) {
		this._countDelay = 0;
		player.commsMessage(expandDescription("[gcm_satellite_attackers_marked]"), 6);
		for (var i = 0; i < this._attackers.length; i++) {
			this._attackers[i].AIScript.oolite_priorityai.setParameter("oolite_attackTarget", this._satToDefend);
			this._attackers[i].AIScript.oolite_priorityai.configurationResetWaypoint();
			this._attackers[i].AIScript.oolite_priorityai.reconsiderNow();
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
// adds a variable number of guards around a particular satellite
// then sets a timer to trigger ai config
this.$addGuards = function $addGuards(sat) {
	var num = Math.floor(Math.random() * 4) + 1;
	var assassin = this._preferredAssassinLightShips;
	if (system.government >= 5) assassin = this._preferredAssassinMediumShips;
	// we're going to create our group manually, to avoid any really slow escort ships
	var gn = new ShipGroup();
	for (var i = 0; i < num; i++) {
		var checkShips = system.addShips("[" + assassin[Math.floor(Math.random() * assassin.length)] + "]", 1, sat.position, 10000);
		if (checkShips) var a = checkShips[0];
		if (a) {
			if (this._rsnInstalled) a.shipUniqueName = this.$getRandomShipName(a, "assassin-light");
			// remove any escorts that came with the ship
			if (a.escorts) {
				for (var j = a.escorts.length - 1; j >= 0; j--) a.escorts[j].remove(true);
			}
			gn.addShip(a);
		}
	}
	if (gn.ships && gn.ships.length > 0) {
		var pop = worldScripts["oolite-populator"];
		for (var i = 0; i < gn.ships.length; i++) {
			var shp = gn.ships[i];
			// configure our guards
			shp.setCrew({
				name: randomName() + " " + randomName(),
				bounty: 0,
				insurance: 0
			});
			if (shp.hasHyperspaceMotor) {
				pop._setWeapons(shp, 1.75); // bigger ones sometimes well-armed
			} else {
				pop._setWeapons(shp, 1.3); // rarely well-armed
			}
			pop._setSkill(shp, 4 - system.info.government);
			if (Math.random() * 16 < system.info.government) {
				pop._setMissiles(shp, -1);
			}
			// make sure the AI is switched
			shp.switchAI("gcm-guardAI.js");
		}
		// need to get the lurk position to move
		this._satDefenders.push({
			group: gn,
			target: sat
		});
		if (!this._satDefenderTimer || this._satDefenderTimer.isRunning === false) {
			this._satDefenderTimer = new Timer(this, this.$giveSatDefenderDefendTarget, 1, 0);
		}
	} else {
		log(this.name, "!!ERROR: Satellite guards not spawned!");
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$addAttackers = function $addAttackers(pos) {
	var sat = this._satToDefend;
	var num = Math.floor(Math.random() * 3) + 1;
	var assassin = this._preferredAssassinLightShips;
	if (system.government >= 5) assassin = this._preferredAssassinMediumShips;
	// we're going to create our group manually, to avoid any really slow escort ships
	var gn = new ShipGroup();
	for (var i = 0; i < num; i++) {
		var checkShips = system.addShips("[" + assassin[Math.floor(Math.random() * assassin.length)] + "]", 1, pos, 500);
		if (checkShips) var a = checkShips[0];
		if (a) {
			if (this._rsnInstalled) a.shipUniqueName = this.$getRandomShipName(a, "assassin-light");
			// remove any escorts that came with the ship
			if (a.escorts) {
				for (var j = a.escorts.length - 1; j >= 0; j--) a.escorts[j].remove(true);
			}
			gn.addShip(a);
		}
	}
	if (gn.ships && gn.ships.length > 0) {
		this._wavesToCreate -= 1;
		if (this._wavesToCreate <= 0) this._wavesToCreate = 0;
		var pop = worldScripts["oolite-populator"];
		for (var i = 0; i < gn.ships.length; i++) {
			var shp = gn.ships[i];
			var scr = shp.script;
			if (scr.shipDied && !scr.$gcm_hold_shipDied) scr.$gcm_hold_shipDied = scr.shipDied;
			scr.shipDied = this.$attacker_shipDied;
			// configure our attackers
			shp.setCrew({
				name: randomName() + " " + randomName(),
				bounty: Math.floor(Math.random() * 15 + 5),
				insurance: 0
			});
			scr._missionID = sat.script._missionID;
			if (shp.hasHyperspaceMotor) {
				pop._setWeapons(shp, 1.75); // bigger ones sometimes well-armed
			} else {
				pop._setWeapons(shp, 1.3); // rarely well-armed
			}
			pop._setSkill(shp, 4 - system.info.government);
			if (Math.random() * 16 < system.info.government) {
				pop._setMissiles(shp, -1);
			}
			// make sure they have an escape pod 
			shp.awardEquipment("EQ_ESCAPE_POD");
			// make sure the AI is switched
			shp.switchAI("gcm-attackerAI.js");
			this._attackers.push(shp);
		}
		// need to get the satellite position to move
		this._satAttackers.push({
			group: gn,
			target: sat
		});
		if (!this._satAttackerTimer || this._satAttackerTimer.isRunning === false) {
			this._satAttackerTimer = new Timer(this, this.$giveSatAttackersTarget, 1, 0);
		}
	} else {
		log(this.name, "!!ERROR: Satellite attackers not spawned!");
	}
}

//-------------------------------------------------------------------------------------------------------------
// populates an array with ship data keys for use by the populator routines
this.$getAssassinShipLists = function $getAssassinShipLists() {
	this._preferredAssassinLightShips.length = 0;
	var shipkeys = Ship.keysForRole("assassin-light");
	for (var i = 0; i < shipkeys.length; i++) {
		this._preferredAssassinLightShips.push(shipkeys[i]);
	}
	this._preferredAssassinMediumShips.length = 0;
	var shipkeys = Ship.keysForRole("assassin-medium");
	for (var i = 0; i < shipkeys.length; i++) {
		this._preferredAssassinMediumShips.push(shipkeys[i]);
	}
}