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

Expansion Black Market

Content

Manifest

from Expansion Manager's OXP list from Expansion Manifest
Description Adds black markets to most Rock Hermits and some other stations. Adds black markets to most Rock Hermits and some other stations.
Identifier oolite.oxp.phkb.BlackMarket oolite.oxp.phkb.BlackMarket
Title Black Market Black Market
Category Mechanics Mechanics
Author phkb phkb
Version 1.2 1.2
Tags
Required Oolite Version
Maximum Oolite Version
Required Expansions
Optional Expansions
Conflict Expansions
Information URL https://wiki.alioth.net/index.php/Black_Market n/a
Download URL https://wiki.alioth.net/img_auth.php/a/a9/BlackMarket_1.2.oxz n/a
License CC-BY-NC-SA 3.0 CC-BY-NC-SA 3.0
File Size n/a
Upload date 1708758536

Documentation

Also read http://wiki.alioth.net/index.php/Black%20Market

readme.txt

Black Market
by phkb

Overview
========
When docked at most non-GalCop stations, and even GalCop stations in some systems, you will find a new interface screen called "Black Market". This interface provides access to a number of options:
a. Noticeboard: View some of the messages from the black market news boards. Some are good, some aren't. You might even learn about some tasty abandoned cargo!
b. Smuggling contracts: These contracts will be smaller cargo quantities (less than 20t), and they will all be illegal at the destination system, so there will be a high degree of risk with each one. However, the rewards will be substantial, more than double what the cargo would be worth at the destination. (Smugglers OXP required)
c. Purchase fake import permits: You can purchase fake import permits that may or may not work at the destination system. The chance is only a slightly better than 50%. (Smugglers OXP required)
d. Purchase Rock Hermit waypoints: Finding Rock Hermits is important, as they are the primary location to access the Black Market. Waypoints can help you find a Rock Hermit in a given system.
e. Purchase phase scan settings: Not always available but occasionally a trader will offer their settings for a price. (Smugglers OXP required)
f. Sell phase scan settings: If you have discovered a phase scan setting, you can sell it for some real coin! This will only be available once you have purchased a phase scanner. (Smugglers OXP required)
g. Sell restricted equipment: If you've come into possession of some military grade equipment, you can potentially find a buyer on the black market.

Please be aware that GalCop have been known to run sting operations at some Black Markets. If you are caught buying illegal information or goods through a Black Market while a sting operation is in place, you will be penalised.

3rd Party Access To Black Market
================================
It's possible for other OXP's to add items to be sold on the Black Market. You can add your item using the following method:

    var sbm = worldScripts.Smugglers_BlackMarket;
    if (sbm) {
        sbm.$addSaleItem({
            key:"my_key_value",		            // reference to be passed back to your worldscript when the item is sold, so you can know which item it was
            text:"A very exclusive item",       // text to display on the sale screen
            cost:1000,				            // the base cost of the item. a system factor will be applied to this value, either increasing or decreasing it
            worldScript:"my_worldscript_name",  // the name of your worldscript
            sellCallback:"$my_functionname",    // the name of the function to be called in the item is sold
        });
    }
	
    this.$my_functionname = function(key, price) {
        // this is the function that will be called. the key name and the price are two parameters of the function
        // you can now do any additional functions that need to be done (eg removing special equipment or cargo)
    }

Items added to the Black Market will stay there until the next witchspace jump. If you want to remove the item manually, you can use this function:

    var sbm = worldScripts.Smugglers_BlackMarket;
    if (sbm) {
        sbm.$removeSaleItem(key);
    }

You can also remove all sale items for your worldScript with this function:

    var sbm = worldScripts.Smugglers_BlackMarket;
    if (sbm) {
        sbm.$removeWorldScriptItems(ws);
    }

Developer Note
==============
This OXP used to reside inside the Smugglers OXP, and so a lot of the code and descriptions are related to that OXP. However, I had the need of a Black Market for another project, and rather than force players to install Smugglers (which has a big impact on gameplay), I split out the functionality into a separate pack.

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

Skull and crossbones image from http://simpleicon.com/dangerous_sign.html.

Version History
===============
1.2
- Added mission noticeboard items to descriptions.plist.
- Small code improvements.

1.1
- Fixed issue that was displaying "You have a waypoint to a Rock Hermit in this system" on th F7 page for every system.

1.0
- Initial release

Equipment

This expansion declares no equipment.

Ships

Name
Mine

Models

This expansion declares no models.

Scripts

Path
Config/script.js
"use strict";
this.name = "Smugglers_BlackMarket";
this.author = "phkb";
this.description = "Looks after the Black Market interface screen.";
this.licence = "CC BY-NC-SA 3.0";

/*
Ideas: 
- Add an equipment selling facility, so players can try to sell equipment for a profit
	- add interface into SDC to "board ship" then "steal equipment" from NPC ships
	- need ships created by SDC to include default equipment from SC
- Add notes to bulletin board about where you might be able to find the phase scanner, then increase chance at that destination
*/

this._disabled = false;
this._debug = false;

this._smugglingAllegiance = ["pirate", "chaotic"];
this._includeRoles = ["rockhermit", "rockhermit-chaotic", "rockhermit-pirate", "random_hits_any_spacebar"]; // make sure these stations have a Black Market
this._excludeRoles = ["slaver_base", "rrs_slaverbase"]; // stations with these roles won't have a Black Market

this._fakePermits = []; // list of systems where fake permits can be bought, plus the rating of each
// value of 0 means no fake permits can be bought in that system
// value of 0.5 to 0.99 reliability of permits bought (0.99 means almost perfect)
this._display = 0;
this._noticeboard = [];
this._selectedIndex = 0;
this._waypoints = [];
this._fakePermitCost = 120;
this._waypointCost = 20;
this._phaseScanCost = 450;
this._blackMarketSalesPerson = "";
this._sellCommodity = "";
this._sellQuantity = 0;
this._sellType = "";
this._offerQuantity = 0;
this._offerPrice = 0.0;
this._menuColor = "orangeColor";
this._itemColor = "yellowColor";
this._disabledColor = "darkGrayColor";
this._blackMarketSales = [];
this._blackMarketContacts = [];
this._blackMarketShutdown = [];
this._blackMarketWelcome = "";
this._sting = false;
this._stingShip = null;
this._stingStation = null;
this._stingShipChance = 0.3;
this._noBribe = false;
this._lastChoice = ["", "", "", "", "", "", "", "", "", "", ""];
this._additionalSaleItems = [];
this._systemFactor = -1;
this._stationIncludeScriptKey = [];
this._stationExcludeScriptKey = [];
this._bribeChance = 0;
this._stash = [];
this._stashLocation = null;
this._pirates = [];
this._curpage = 0;

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

	if (this._disabled) {
		delete this.playerWillSaveGame;
		delete this.playerEnteredNewGalaxy;
		delete this.shipExitedWitchspace;
		delete this.shipDockedWithStation;
		delete this.shipLaunchedFromStation;
		delete this.startUpComplete;
		return;
	}

	if (worldScripts.Smugglers_Illegal) this._smugglers = true;

	if (missionVariables.Smuggling_Noticeboard) {
		this._noticeboard = JSON.parse(missionVariables.Smuggling_Noticeboard);
	} else {
		this._rebuildTimer = new Timer(this, this.$buildNoticeboard, 2, 0);
	}
	if (this._smugglers) {
		if (missionVariables.Smuggling_FakePermits) {
			this._fakePermits = JSON.parse(missionVariables.Smuggling_FakePermits);
		} else {
			this.$buildFakePermitArray();
		}
	}
	if (missionVariables.Smuggling_Waypoints) this._waypoints = JSON.parse(missionVariables.Smuggling_Waypoints);
	if (missionVariables.Smuggling_BlackMarketSales) {
		this._blackMarketSales = JSON.parse(missionVariables.Smuggling_BlackMarketSales);
		delete missionVariables.Smuggling_BlackMarketSales;
	}
	if (missionVariables.Smuggling_BlackMarketContacts) this._blackMarketContacts = JSON.parse(missionVariables.Smuggling_BlackMarketContacts);
	if (missionVariables.Smuggling_BlackMarketShutdown) this._blackMarketShutdown = JSON.parse(missionVariables.Smuggling_BlackMarketShutdown);
	if (missionVariables.Smuggling_BlackMarketSting) this._sting = missionVariables.Smuggling_BlackMarketSting;
	if (missionVariables.Smuggling_BlackMarketStingShipChance) this._stingShipChance = missionVariables.Smuggling_BlackMarketStingShipChance;
	if (missionVariables.Smuggling_BlackMarketAdditional) this._additionalSaleItems = JSON.parse(missionVariables.Smuggling_BlackMarketAdditional);
	if (missionVariables.Smuggling_BlackMarketSystemFactor) this._systemFactor = missionVariables.Smuggling_BlackMarketSystemFactor;
	if (this._systemFactor === -1) this.$setupSystemFactor();

	if (missionVariables.Smuggling_BlackMarketStashes) this._stash = JSON.parse(missionVariables.Smuggling_BlackMarketStashes);
	for (var i = this._stash.length - 1; i >= 0; i--) {
		if (this._stash[i].systemID == system.ID && (clock.adjustedSeconds - this._stash[i].created) / 86400 < 5) {
			this.$setupCargoStash();
		}
		if ((clock.adjustedSeconds - this._stash[i].created) / 86400 >= 5) {
			this._stash.splice(i, 1);
		}
	}

	// because stations display name may not be populated at this point, create a timer to set up the sting
	if (this._sting) this._delayStingSetup = new Timer(this, this.$setupStingAfterLoad, 2, 0);
	// set up the interface screen, if required
	this.$initMainInterface(player.ship.dockedStation);
	this.$addCoreEquipmentToBlackMarket();
}

//-------------------------------------------------------------------------------------------------------------
this.playerWillSaveGame = function () {
	missionVariables.Smuggling_Noticeboard = JSON.stringify(this._noticeboard);
	if (this._smugglers) missionVariables.Smuggling_FakePermits = JSON.stringify(this._fakePermits);
	missionVariables.Smuggling_Waypoints = JSON.stringify(this._waypoints);
	missionVariables.Smuggling_BlackMarketStingShipChance = this._stingShipChance;
	if (this._blackMarketSales.length > 0) missionVariables.Smuggling_BlackMarketSales = JSON.stringify(this._blackMarketSales);
	if (this._blackMarketContacts.length > 0) missionVariables.Smuggling_BlackMarketContacts = JSON.stringify(this._blackMarketContacts);
	if (this._blackMarketShutdown.length > 0) missionVariables.Smuggling_BlackMarketShutdown = JSON.stringify(this._blackMarketShutdown);
	if (this._additionalSaleItems.length > 0) missionVariables.Smuggling_BlackMarketAdditional = JSON.stringify(this._additionalSaleItems);
	missionVariables.Smuggling_BlackMarketSystemFactor = this._systemFactor;

	if (this._sting) {
		missionVariables.Smuggling_BlackMarketSting = this._sting;
		missionVariables.Smuggling_BlackMarketStingStation = this._stingStation.displayName;
		if (this._stingShip) missionVariables.Smuggling_BlackMarketStingShip = true;
	}
	if (this._stash.length > 0) {
		missionVariables.Smuggling_BlackMarketStashes = JSON.stringify(this._stash);
	} else {
		if (missionVariables.Smuggling_BlackMarketStashes) {
			delete missionVariables.Smuggling_BlackMarketStashes;
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.playerEnteredNewGalaxy = function (galaxyNumber) {
	if (this._smugglers) this.$buildFakePermitArray();
	this._waypoints = [];
	this._blackMarketShutdown = [];
	this._stash.length = 0;
}

//-------------------------------------------------------------------------------------------------------------
this.shipExitedWitchspace = function () {
	this._bribeChance = Math.random();
	this._additionalSaleItems = [];
	this.$setupSystemFactor();

	// rebuild the notice board a few seconds after entering a new system
	// that should give the other systems a chance to finish
	this._rebuildTimer = new Timer(this, this.$buildNoticeboard, 5, 0);
	this.$addRHWaypoint();
	this._blackMarketSales = [];
	// reset any sting info
	this._sting = false;
	this._stingStation = null;
	this._stingShip = null;
	// find a station with a black market
	var stns = system.stations;
	for (var i = 0; i < stns.length; i++) {
		if (this.$doesStationHaveBlackMarket(stns[i]) === true) {
			var hasVisited = this.$playerHasBeenToBlackMarket(stns[i]);
			// there's a small chance of a black market sting if the player has visited the station, and an even smaller chance if they haven't
			if ((hasVisited === true && Math.random() > 0.9) || (hasVisited === false && Math.random() > 0.98)) {
				// set up the sting
				this.$setupSting(stns[i]);
				// don't set up more than one sting operation, so break here when we're done.
				break;
			}
		}
	}
	// clean up any shutdowns that have expired
	if (this._blackMarketShutdown.length > 0) {
		for (var i = this._blackMarketShutdown.length - 1; i >= 0; i--) {
			if (clock.adjustedSeconds - this._blackMarketShutdown[i].date > 2592000) this._blackMarketShutdown.splice(i, 1);
		}
	}
	// cargo stash
	for (var i = this._stash.length - 1; i >= 0; i--) {
		if (this._stash[i].systemID == system.ID && (clock.adjustedSeconds - this._stash[i].created) / 86400 < 5) {
			this.$setupCargoStash();
		}
		if ((clock.adjustedSeconds - this._stash[i].created) / 86400 >= 5) {
			this._stash.splice(i, 1);
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.shipDockedWithStation = function (station) {
	this.$addCoreEquipmentToBlackMarket();
	this.$initMainInterface(station);
	this._blackMarketSalesPerson = "";
	this._blackMarketWelcome = "";
	// remove the sting ship after we dock, if it's still there
	if (this._sting === true && this._stingStation === station && this._stingShip && this._stingShip.isValid) {
		this._stingShip.remove(true);
		this._stingShip = null;
	}
}

//-------------------------------------------------------------------------------------------------------------
this.shipLaunchedFromStation = function (station) {
	this.$addRHWaypoint();
	// remove the sale items when we launch
	this.$removeSaleItem("EQ_CLOAKING_DEVICE");
	var eq = player.ship.equipment;
	for (var i = 0; i < eq.length; i++) {
		if (eq[i].equipmentKey.indexOf("NAVAL") >= 0 || eq[i].equipmentKey.indexOf("MILITARY") >= 0) {
			var item = EquipmentInfo.infoForKey(eq[i].equipmentKey);
			this.$removeSaleItem(eq[i].equipmentKey);
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.guiScreenChanged = function (to, from) {
	if (guiScreen === "GUI_SCREEN_SYSTEM_DATA") {
		this.$checkForWaypoints();
	}
}

//-------------------------------------------------------------------------------------------------------------
this.infoSystemChanged = function (to, from) {
	if (guiScreen === "GUI_SCREEN_SYSTEM_DATA") {
		this.$checkForWaypoints();
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$checkForWaypoints = function() {
	if (this._waypoints.length > 0) {
		var sysID = player.ship.targetSystem;
		if (player.ship.hasOwnProperty("infoSystem")) sysID = player.ship.infoSystem;
		if (this._waypoints.indexOf(sysID) >= 0) {
			mission.addMessageText(expandDescription("[blackmarket-waypoint-here]"));
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
/* adds an item to the Black Market additional sale items
 passed object must have:
	key:				(required) text to be sent back to the calling OXP to identify the sale item
	text:				(required) text to be displayed on the sale screen
	cost:				(required) cost of the item to be sold. must be greater than 0
	worldScript:		(required) name of the calling worldScript
	sellCallback:		(required) name of the function to call if the item is sold
*/
this.$addSaleItem = function $addSaleItem(obj) {
	if (obj.hasOwnProperty("key") === false || obj.key == "") {
		throw "Invalid sale item properties: 'key' must be supplied";
	}
	if (obj.hasOwnProperty("text") === false || obj.text == "") {
		throw "Invalid sale item properties: 'text' must be supplied";
	}
	if (obj.hasOwnProperty("cost") === false) {
		throw "Invalid sale item properties: 'cost' must be supplied";
	}
	if (parseInt(obj.cost) === 0) {
		throw "Invalid sale item properties: 'cost' must be greater than 0";
	}
	if (obj.hasOwnProperty("worldScript") === false || obj.worldScript == "") {
		throw "Invalid sale item properties: 'worldScript' must be supplied";
	}
	if (obj.hasOwnProperty("sellCallback") === false || obj.sellCallback == "") {
		throw "Invalid sale item properties: 'sellCallback' must be supplied";
	}
	for (var i = 0; i < this._additionalSaleItems.length; i++) {
		if (this._additionalSaleItems[i].key === obj.key) {
			throw "Invalid sale item properties: 'key' of " + obj.key + " is already in use.";
		}
	}
	this._additionalSaleItems.push(obj);
}

//-------------------------------------------------------------------------------------------------------------
// removes all items from the sale list linked to a particular worldScript
this.$removeWorldScriptItems = function $removeWorldScriptItems(ws) {
	for (var i = this._additionalSaleItems.length - 1; i >= 0; i--) {
		if (this._additionalSaleItems[i].worldScript === ws) {
			this._additionalSaleItems.splice(i, 1);
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
// removes an item from the sale list with the specific key value
this.$removeSaleItem = function $removeSaleItem(key) {
	for (var i = 0; i < this._additionalSaleItems.length; i++) {
		if (this._additionalSaleItems[i].key === key) {
			this._additionalSaleItems.splice(i, 1);
			break;
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
// add a noticeboard item, with a callback for when it is viewed
this.$addNoticeboardItem = function(msg, ws, fn) {
	function compare(a, b) {
		if (a.idx < b.idx) return -1;
		if (a.idx > b.idx) return 1;
		return 0;
	}

	var item = {
		idx: this.$rand(100),
		message: msg,
	};
	if (ws && ws != "" && fn && fn != "") {
		item["worldscript"] = ws;
		item["function"] = fn;
	}

	this._noticeboard.push(item);

	// sort the list, based on the random index, so the items are mixed up.
	this._noticeboard.sort(compare);
}

//-------------------------------------------------------------------------------------------------------------
// initialise the illegal goods F4 screen entries
this.$initMainInterface = function $initMainInterface(station) {
	// only at pirate or chaotic stations, all rock hermits, and selected main stations
	if (this._debug === true || (this.$doesStationHaveBlackMarket(station) === true)) {
		station.setInterface(this.name, {
			title: "Black market",
			category: "Station Interfaces",
			summary: "Access to the Black Market of the Galactic Underworld.",
			callback: this.$initialBlackMarket.bind(this)
		});
	} else {
		station.setInterface(this.name, null);
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$isStationIncluded = function $isStationIncluded(station) {
	for (var i = 0; i < this._includeRoles.length; i++) {
		if (station.hasRole(this._includeRoles[i]) === true) return true;
	}
	if (this._stationIncludeScriptKey.length > 0) {
		for (var i = 0; i < this._stationIncludeScriptKey.length; i++) {
			var item = this._stationIncludeScriptKey[i].key;
			var value = this._stationIncludeScriptKey[i].value;
			if (station.script.hasOwnProperty(item) === true && station.script[item] == value) return true;
		}
	}
	return false;
}

//-------------------------------------------------------------------------------------------------------------
this.$isStationExcluded = function $isStationExcluded(station) {
	for (var i = 0; i < this._excludeRoles.length; i++) {
		if (station.hasRole(this._excludeRoles[i]) === true) return true;
	}
	if (this._stationExcludeScriptKey.length > 0) {
		for (var i = 0; i < this._stationExcludeScriptKey.length; i++) {
			var item = this._stationExcludeScriptKey[i].key;
			var value = this._stationExcludeScriptKey[i].value;
			if (station.script.hasOwnProperty(item) === true && station.script[item] == value) return true;
		}
	}
	return false;
}

//-------------------------------------------------------------------------------------------------------------
this.$initialBlackMarket = function $initialBlackMarket() {
	this._display = 0;
	this._selectedIndex = 0;
	this._curpage = 0;
	this.$blackMarketOptions();
}

//-------------------------------------------------------------------------------------------------------------
this.$blackMarketOptions = function $blackMarketOptions() {

	var curChoices = {};
	var text = "";
	var p = player.ship;
	var stn = p.dockedStation;
	var def = "";
	var si = worldScripts.Smugglers_Illegal;
	var se = worldScripts.Smugglers_Equipment;
	var sc = worldScripts.Smugglers_Contracts;
	var pagesize = 20;
	var image = "blackmarket-skull.png";

	if (this.$isBigGuiActive() === true) {
		pagesize = 26;
	}

	if (this._smugglers) {
		sc._smugglingPageOverlay = image;
		sc._smugglingPageOverlayHeight = 546;
	}

	var govs = new Array();
	for (var i = 0; i < 8; i++)
		govs.push(String.fromCharCode(i));
	var spc = String.fromCharCode(31);

	// main menu
	if (this._display === 0) {
		// if we haven't shown the initial welcome, get one now
		if (this._blackMarketWelcome === "") {
			// use one of the standard welcomes, but if it's a sting use the alternate ones. Also, there's a chance of the alternate ones anyway, so it's not a complete give-away
			this._blackMarketWelcome = expandDescription("[blackmarket-welcome" + (this._sting === true || Math.random() > 0.8 ? "-sting" : "") + "]", {
				contactname: this.$getBlackMarketContact(p.dockedStation)
			});
		}
		// add this to the text
		text = this._blackMarketWelcome

		// reset the welcome text for the next time we show the main menu, so that the welcome text is only displayed once.
		this._blackMarketWelcome = "Black Market services available:";

		var itemcount = 2;
		curChoices["01_NOTICEBOARD"] = {
			text: "[blackmarket-noticeboard]",
			color: this._menuColor
		};
		if (this._waypoints.length > 0) {
			curChoices["01A_CURRENT_WAYPOINTS"] = {
				text: "[blackmarket-list-waypoints]",
				color: this._menuColor
			};
			itemcount += 1;
		}

		if (!worldScripts.ContractsOnBB && this._smugglers == true) {
			// contracts only available at rock hermits
			if ((this._debug === true ||
					(Ship.roleIsInCategory(p.dockedStation.primaryRole, "oolite-rockhermits") && p.dockedStation.hasRole("slaver_base") === false && p.dockedStation.hasRole("rrs_slaverbase") === false)) &&
				sc._contracts.length > 0) {
				//if ((this._debug === true || this._smugglingAllegiance.indexOf(p.dockedStation.allegiance) >= 0) && sc._contracts.length > 0) { // alternate logic for non-galcop stations
				curChoices["02_CONTRACTS"] = {
					text: "View any smuggling contracts on offer (" + sc._contracts.length + " available)",
					color: this._menuColor
				};
				itemcount += 1;
			}
		}
		curChoices["03_PURCHASE"] = {
			text: "[blackmarket-purchasing]",
			color: this._menuColor
		};

		if (this._smuggling) {
			if (se.$playerHasIllegalCargo(system.mainStation) === true || se.$playerHasHiddenIllegalCargo(system.mainStation) === true) {
				var saleitems = 0;
				var viscargo = p.manifest.list;
				for (var i = 0; i < viscargo.length; i++) {
					if (system.mainStation.market[viscargo[i].commodity].legality_import > 0 && this._blackMarketSales.indexOf(viscargo[i].commodity) === -1) saleitems += 1;
				}
				var smuggle = se.$getSmugglingCargo();
				for (var i = 0; i < smuggle.length; i++) {
					if (system.mainStation.market[smuggle[i].commodity].legality_import > 0 && this._blackMarketSales.indexOf(smuggle[i].commodity) === -1) saleitems += 1;
				}
				if (saleitems > 0) {
					curChoices["04_SELL_ILLEGAL"] = {
						text: "[blackmarket-sellcargo-menu]",
						color: this._menuColor
					};
					itemcount += 1;
				}
			}
			if (se.$sellablePhaseScans() > 0) {
				curChoices["07_SELLSCAN"] = {
					text: "[blackmarket-sellphasescan-menu]",
					color: this._menuColor
				};
				itemcount += 1;
			}
		}
		if (this._additionalSaleItems.length > 0) {
			curChoices["08_SELLADDITIONAL"] = {
				text: "Sell additional items",
				color: this._menuColor
			}
			itemcount += 1;
		}

		curChoices["99_EXIT"] = {
			text: "[blackmarket-return]",
			color: this._itemColor
		};

		for (var i = 0; i < ((pagesize - 4) - itemcount); i++) {
			curChoices["99_SPACER_" + i] = "";
		}

		def = "99_EXIT";
		if (this._lastChoice[this._display] != "") def = this._lastChoice[this._display];

		var opts = {
			screenID: "oolite-smuggling-blackmarket-map",
			title: "Black Market Main Menu",
			overlay: {
				name: image,
				height: 546
			},
			allowInterrupt: true,
			exitScreen: "GUI_SCREEN_INTERFACES",
			choices: curChoices,
			initialChoicesKey: def,
			message: text
		};
	}

	// display noticeboard item
	if (this._display === 2) {

		if (this._selectedIndex < this._noticeboard.length - 1) {
			curChoices["20_NEXTITEM"] = {
				text: "[blackmarket-nextitem]",
				color: this._menuColor
			};
			curChoices["30_LASTITEM"] = {
				text: "[blackmarket-lastitem]",
				color: this._menuColor
			};
		} else {
			curChoices["20_NEXTITEM"] = {
				text: "[blackmarket-nextitem]",
				unselectable: true,
				color: this._disabledColor
			};
			curChoices["30_LASTITEM"] = {
				text: "[blackmarket-lastitem]",
				unselectable: true,
				color: this._disabledColor
			};
		}
		if (this._selectedIndex > 0) {
			curChoices["21_PREVITEM"] = {
				text: "[blackmarket-previtem]",
				color: this._menuColor
			};
			curChoices["31_FIRSTITEM"] = {
				text: "[blackmarket-firstitem]",
				color: this._menuColor
			};
		} else {
			curChoices["21_PREVITEM"] = {
				text: "[blackmarket-previtem]",
				unselectable: true,
				color: this._disabledColor
			};
			curChoices["31_FIRSTITEM"] = {
				text: "[blackmarket-firstitem]",
				unselectable: true,
				color: this._disabledColor
			};
		}

		curChoices["98_EXIT"] = {
			text: "[blackmarket-return]",
			color: this._itemColor
		};

		for (var i = 0; i < (pagesize - 10); i++) {
			curChoices["99_SPACER_" + i] = "";
		}

		if (this._lastChoice[this._display] != "") def = this._lastChoice[this._display];

		var opts = {
			screenID: "oolite-smuggling-blackmarket-map",
			title: "Noticeboard Item (" + (this._selectedIndex + 1) + " of " + (this._noticeboard.length) + ")",
			overlay: {
				name: image,
				height: 546
			},
			allowInterrupt: true,
			choices: curChoices,
			initialChoicesKey: def,
			exitScreen: "GUI_SCREEN_INTERFACES",
			message: this._noticeboard[this._selectedIndex].message
		};
		// call the callback, if present
		if (this._noticeboard[this._selectedIndex].hasOwnProperty("worldscript") && this._noticeboard[this._selectedIndex].hasOwnProperty("function")) {
			var ws = this._noticeboard[this._selectedIndex].hasOwnProperty("worldscript");
			var fn = this._noticeboard[this._selectedIndex].hasOwnProperty("function");
			if (worldScripts[ws][fn]) {
				worldScripts[ws][fn]();
			}
		}
	}

	// purchase items
	if (this._display === 3) {
		text = expandDescription("[blackmarket-purchaseheader]");
		var itemcount = 0;

		if (this._smuggling && this._fakePermits[system.ID] > 0) {
			curChoices["04_FAKEPERMITS"] = {
				text: "[blackmarket-buypermits-menu]",
				color: this._menuColor
			};
			itemcount += 1;
		}
		curChoices["05_WAYPOINTS"] = {
			text: "[blackmarket-buywaypoints-menu]",
			color: this._menuColor
		};
		itemcount += 1;
		if (this._smuggling) {
			curChoices["06_PHASESCAN"] = {
				text: "[blackmarket-buyphasescan-menu]",
				color: this._menuColor
			};
			itemcount += 1;
		}
		curChoices["98_EXIT"] = {
			text: "[blackmarket-return]",
			color: this._itemColor
		};

		for (var i = 0; i < ((pagesize - 1) - itemcount); i++) {
			curChoices["99_SPACER_" + i] = "";
		}

		def = "98_EXIT";
		if (this._lastChoice[this._display] != "") def = this._lastChoice[this._display];

		var opts = {
			screenID: "oolite-smuggling-blackmarket-map",
			title: "Black Market Shop",
			allowInterrupt: true,
			overlay: {
				name: image,
				height: 546
			},
			exitScreen: "GUI_SCREEN_INTERFACES",
			choices: curChoices,
			initialChoicesKey: def,
			message: text
		};

	}

	// purchase fake permits
	if (this._display === 4) {
		text = expandDescription("[blackmarket-buypermits]", {
			cash: formatCredits(player.credits, true, true)
		});
		text += this.$padTextRight("Item", 20) + this.$padTextLeft("Cost", 10);

		// get list of local planets
		var sys = system.info.systemsInRange(15);
		var itemcount = 0;
		// loop through list
		for (var i = 0; i < sys.length; i++) {
			// for any that have illegal goods that can have permits, add the planet name + permit type to the list
			var goods = si.$illegalGoodsList(sys[i].systemID);
			if (goods.length > 0) {
				for (var j = 0; j < goods.length; j++) {
					if (goods[j].permit === 1 && system.scrambledPseudoRandomNumber(j) > 0.6 && si.$playerHasPermit(sys[i].systemID, goods[j].commodity, false) === false) {
						curChoices["60_PERMIT~" + sys[i].systemID + "|" + goods[j].commodity] = {
							text: this.$padTextRight(sys[i].name + " (" + govs[sys[i].government] + spc + "TL" + (sys[i].techlevel + 1) + ") -- " + si.$translateCommodityList(goods[j].commodity), 20) +
								this.$padTextLeft(formatCredits(this._fakePermitCost * stn.equipmentPriceFactor, true, true), 10),
							alignment: "LEFT",
							color: this._menuColor
						};
						itemcount += 1;
					}
				}
			}
		}

		if (itemcount === 0) text += expandDescription("[blackmarket-none-available]");

		curChoices["96_SPACER"] = "";
		itemcount += 1;

		// display the list of options
		curChoices["97_EXIT"] = {
			text: "[blackmarket-return]",
			color: this._itemColor
		};

		for (var i = 0; i < ((pagesize - 4) - itemcount); i++) {
			curChoices["99_SPACER_" + i] = "";
		}

		def = "97_EXIT";
		if (this._lastChoice[this._display] != "") def = this._lastChoice[this._display];

		var opts = {
			screenID: "oolite-smuggling-blackmarket-map",
			title: "Black Market Shop",
			overlay: {
				name: image,
				height: 546
			},
			allowInterrupt: true,
			exitScreen: "GUI_SCREEN_INTERFACES",
			choices: curChoices,
			initialChoicesKey: def,
			message: text
		};

	}

	// purchase rock hermit waypoints
	if (this._display === 5) {
		text = expandDescription("[blackmarket-buywaypoints]", {
			cash: formatCredits(player.credits, true, true)
		});
		text += this.$padTextRight("Item", 20) + this.$padTextLeft("Cost", 10);

		var sys = system.info.systemsInRange(7);
		var itemcount = 0;
		for (var i = 0; i < sys.length; i++) {
			if (this._waypoints.indexOf(sys[i].systemID) === -1 && system.scrambledPseudoRandomNumber(sys[i].systemID) > 0.7) {
				curChoices["61_WAYPOINT_" + (i < 10 ? "0" : "") + i + "~" + sys[i].systemID] = {
					text: this.$padTextRight(sys[i].name + " (" + govs[sys[i].government] + spc + "TL" + (sys[i].techlevel + 1) + ")", 20) +
						this.$padTextLeft(formatCredits(this._waypointCost * stn.equipmentPriceFactor, true, true), 10),
					alignment: "LEFT",
					color: this._menuColor
				};
				itemcount += 1;
			}
		}
		if (itemcount === 0) text += expandDescription("[blackmarket-none-available]");

		curChoices["96_SPACER"] = "";
		itemcount += 1;

		// display the list of options
		curChoices["97_EXIT"] = {
			text: "[blackmarket-return]",
			color: this._itemColor
		};

		for (var i = 0; i < ((pagesize - 4) - itemcount); i++) {
			curChoices["99_SPACER_" + i] = "";
		}

		def = "97_EXIT";
		if (this._lastChoice[this._display] != "") def = this._lastChoice[this._display];

		var opts = {
			screenID: "oolite-smuggling-blackmarket-map",
			title: "Black Market Shop",
			allowInterrupt: true,
			overlay: {
				name: image,
				height: 546
			},
			exitScreen: "GUI_SCREEN_INTERFACES",
			choices: curChoices,
			initialChoicesKey: def,
			message: text
		};

	}

	// purchase phase scan settings
	if (this._display === 6) {
		text = expandDescription("[blackmarket-buyphasescan]", {
			cash: formatCredits(player.credits, true, true)
		});
		text += this.$padTextRight("Item", 20) + this.$padTextLeft("Cost", 10);

		// there won't be many of these
		// get a list of all planets with illegal goods
		var sys = system.info.systemsInRange(7);
		var itemcount = 0;

		for (var i = 0; i < sys.length; i++) {
			var goods = si.$illegalGoodsList(sys[i].systemID);
			if (se.$playerHasPhaseScan(sys[i].systemID) === false && system.scrambledPseudoRandomNumber(sys[i].systemID) > 0.5) {
				curChoices["62_PHASESCAN~" + sys[i].systemID] = {
					text: this.$padTextRight(sys[i].name + " (" + govs[sys[i].government] + spc + "TL" + (sys[i].techlevel + 1) + ")", 20) +
						this.$padTextLeft(formatCredits(this._phaseScanCost * stn.equipmentPriceFactor, true, true), 10),
					alignment: "LEFT",
					color: this._menuColor
				};
				itemcount += 1;
			}
		}

		if (itemcount === 0) text += expandDescription("[blackmarket-none-available]");

		curChoices["96_SPACER"] = "";
		itemcount += 1;

		// display the list of options
		curChoices["97_EXIT"] = {
			text: "[blackmarket-return]",
			color: this._itemColor
		};

		for (var i = 0; i < ((pagesize - 4) - itemcount); i++) {
			curChoices["99_SPACER_" + i] = "";
		}

		def = "97_EXIT";
		if (this._lastChoice[this._display] != "") def = this._lastChoice[this._display];

		var opts = {
			screenID: "oolite-smuggling-blackmarket-map",
			title: "Black Market Shop",
			allowInterrupt: true,
			overlay: {
				name: image,
				height: 546
			},
			exitScreen: "GUI_SCREEN_INTERFACES",
			choices: curChoices,
			initialChoicesKey: def,
			message: text
		};

	}

	// sell phase scan
	if (this._display === 7) {
		text = expandDescription("[blackmarket-sellphasescan]", {
			cash: formatCredits(player.credits, true, true)
		});
		text += this.$padTextRight("Item", 20) + this.$padTextLeft("Cost", 10);

		var list = se.$getSellablePhaseScans();

		for (var i = 0; i < list.length; i++) {
			curChoices["63_PHASESCAN~" + (i < 10 ? "0" : "") + i] = {
				text: this.$padTextRight(list[i].text, 20) +
					this.$padTextLeft(formatCredits(((this._phaseScanCost * stn.equipmentPriceFactor) * 0.8), true, true), 10),
				alignment: "LEFT",
				color: this._menuColor
			};
		}

		curChoices["96_SPACER"] = "";

		// display the list of options
		curChoices["97_EXIT"] = {
			text: "[blackmarket-return]",
			color: this._itemColor
		};

		for (var i = 0; i < ((pagesize - 4) - list.length); i++) {
			curChoices["99_SPACER_" + i] = "";
		}

		def = "97_EXIT";
		if (this._lastChoice[this._display] != "") def = this._lastChoice[this._display];

		var opts = {
			screenID: "oolite-smuggling-blackmarket-map",
			title: "Sell Discovered Phase Scan",
			allowInterrupt: true,
			overlay: {
				name: image,
				height: 546
			},
			exitScreen: "GUI_SCREEN_INTERFACES",
			choices: curChoices,
			initialChoicesKey: def,
			message: text
		};

	}

	// sell illegal cargo on black market
	if (this._display === 8) {
		text = expandDescription("[blackmarket-sellcargo]", {
			cash: formatCredits(player.credits, true, true)
		});
		var sdm = worldScripts.Smugglers_DockMaster;
		var heading = false;
		var itemcount = 0;

		var viscargo = p.manifest.list;
		for (var i = 0; i < viscargo.length; i++) {
			if (system.mainStation.market[viscargo[i].commodity].legality_import > 0 && this._blackMarketSales.indexOf(viscargo[i].commodity) === -1) {
				if (heading === false) {
					curChoices["73_HEADING"] = {
						text: "[blackmarket-standard-hold]",
						alignment: "LEFT",
						color: this._itemColor,
						unselectable: true
					};
					heading = true;
					itemcount += 1;
				}
				var q = viscargo[i].quantity;
				// remove any relabelled cargo from sale
				if (sdm && sdm.$getTotalRelabelled(viscargo[i].commodity) > 0) q = q - sdm.$getTotalRelabelled(viscargo[i].commodity);
				if (q > 0) {
					curChoices["73_SELL~" + viscargo[i].commodity] = {
						text: viscargo[i].displayName + " (" + q + viscargo[i].unit + ")",
						alignment: "LEFT",
						color: this._menuColor
					};
					itemcount += 1;
				}
			}
		}

		heading = false;
		if (this._smuggling) {
			var smuggle = se.$getSmugglingCargo();
			for (var i = 0; i < smuggle.length; i++) {
				if (system.mainStation.market[smuggle[i].commodity].legality_import > 0 && this._blackMarketSales.indexOf(smuggle[i].commodity) === -1) {
					if (heading === false) {
						curChoices["74_HEADING"] = {
							text: "[blackmarket-smuggling-compartment]",
							alignment: "LEFT",
							color: this._itemColor,
							unselectable: true
						};
						heading = true;
						itemcount += 1;
					}
					curChoices["74_SELL~" + smuggle[i].commodity] = {
						text: smuggle[i].quantity + " " + smuggle[i].unit + " × " + smuggle[i].displayName,
						alignment: "LEFT",
						color: this._menuColor
					};
					itemcount += 1;
				}
			}
		}
		if (itemcount === 0) {
			text += expandDescription("[blackmarket-sellcargo-none]");
			itemcount += 1;
		}

		curChoices["96_SPACER"] = "";

		// display the list of options
		curChoices["98_EXIT"] = {
			text: "[blackmarket-return]",
			color: this._itemColor
		};

		for (var i = 0; i < ((pagesize - 4) - itemcount); i++) {
			curChoices["99_SPACER_" + i] = "";
		}

		def = "98_EXIT";
		if (this._lastChoice[this._display] != "") def = this._lastChoice[this._display];

		var opts = {
			screenID: "oolite-smuggling-blackmarket-map",
			title: "Sell Illegal Cargo",
			allowInterrupt: true,
			overlay: {
				name: image,
				height: 546
			},
			exitScreen: "GUI_SCREEN_INTERFACES",
			choices: curChoices,
			initialChoicesKey: def,
			message: text
		};
	}

	// accept/reject black market sell offer
	if (this._display === 9) {
		// work out if there is a buyer for this commodity
		var itemcount = 1;
		var ci = si._commodityInfo[this._sellCommodity];
		var main_markup = ci[1];
		if (main_markup === 0) main_markup = 1; // default to 1 for commodities we have no data for
		var bm_markup = ci[2];
		if (bm_markup === 0) bm_markup = 1.1; // default to 75% of main for commodities we have no data for.

		if (system.scrambledPseudoRandomNumber(bm_markup) < ((28 - system.info.economy) / 7)) {
			// work out how much of this cargo the market is willing to buy
			this._offerQuantity = parseInt(this._sellQuantity * ((7 - system.info.economy) + 1) / 8);
			// there is still a chance there will be a buyer for all the amount
			if (this._offerQuantity < this._sellQuantity && system.scrambledPseudoRandomNumber(this._sellQuantity) > 0.8) {
				this._offerQuantity = this._sellQuantity;
			}
			// better prices if player's smuggling reputation is higher
			this._offerPrice = (system.info.samplePrice(this._sellCommodity) * (bm_markup +
				((system.scrambledPseudoRandomNumber(main_markup) * (sc.$getSmugglingReputationPrecise() / 7))) -
				(0.5 * (1 - sc.$getSmugglingReputationPrecise() / 7))) / 10);

			text = expandDescription("[blackmarket-buyer]", {
				quantity: this._offerQuantity + " " + se.$getCommodityType(this._sellCommodity),
				commodity: displayNameForCommodity(this._sellCommodity),
				price: formatCredits(this._offerPrice, false, true),
				total: formatCredits(this._offerQuantity * this._offerPrice, false, true)
			});

			curChoices["80_ACCEPTOFFER"] = {
				text: "[blackmarket-accept]",
				color: this._menuColor
			};
			curChoices["81_REJECTOFFER"] = {
				text: "[blackmarket-reject]",
				color: this._menuColor
			};
			def = "80_ACCEPTOFFER";
			itemcount += 2;
		} else {
			// no buyer found
			text = expandDescription("[blackmarket-nobuyer]", {
				commodity: this._sellCommodity
			});
			def = "98_EXIT";
			curChoices["98_EXIT"] = {
				text: "[blackmarket-return]",
				color: this._itemColor
			};
		}

		if (this._lastChoice[this._display] != "") def = this._lastChoice[this._display];

		for (var i = 0; i < ((pagesize - 6) - itemcount); i++) {
			curChoices["99_SPACER_" + i] = "";
		}

		var opts = {
			screenID: "oolite-smuggling-blackmarket-map",
			title: "Sell Illegal Cargo",
			allowInterrupt: true,
			overlay: {
				name: image,
				height: 546
			},
			exitScreen: "GUI_SCREEN_INTERFACES",
			choices: curChoices,
			initialChoicesKey: def,
			message: text
		};
	}

	if (this._display === 10) {
		text = expandDescription("[blackmarket-sellotheritems]", {
			cash: formatCredits(player.credits, true, true)
		});
		text += this.$padTextRight("Item", 20) + this.$padTextLeft("Cost", 10);

		var list = this._additionalSaleItems;

		for (var i = 0; i < list.length; i++) {
			curChoices["50_OTHERITEM~" + (i < 10 ? "0" : "") + i] = {
				text: this.$padTextRight(list[i].text, 20) +
					this.$padTextLeft(formatCredits(list[i].cost * this._systemFactor, true, true), 10),
				alignment: "LEFT",
				color: this._menuColor
			};
		}

		curChoices["96_SPACER"] = "";
		// display the list of options
		curChoices["98_EXIT"] = {
			text: "[blackmarket-return]",
			color: this._itemColor
		};

		for (var i = 0; i < ((pagesize - 4) - list.length); i++) {
			curChoices["99_SPACER_" + i] = "";
		}

		def = "98_EXIT";
		if (this._lastChoice[this._display] != "") def = this._lastChoice[this._display];

		var opts = {
			screenID: "oolite-smuggling-blackmarket-map",
			title: "Sell Other Items",
			allowInterrupt: true,
			overlay: {
				name: image,
				height: 546
			},
			exitScreen: "GUI_SCREEN_INTERFACES",
			choices: curChoices,
			initialChoicesKey: def,
			message: text
		};
	}

	// rock hermit waypoints purchased
	if (this._display === 11) {
		var redux = 6;
		var maxpages = Math.ceil(this._waypoints.length / (pagesize - redux));
		if (maxpages === 0) maxpages = 1;

		text = "Rock Hermit waypoints acquired" + (this._waypoints.length > 0 ? " - Page " + (this._curpage + 1) + " of " + maxpages : "") + "\n\n";

		if (this._waypoints.length > 0) {
			var pagestart = this._curpage * (pagesize - redux);
			var pageend = this._curpage * (pagesize - redux) + (pagesize - redux);
			if (pageend > this._waypoints.length) pageend = this._waypoints.length;

			for (var i = pagestart; i < pageend; i++) {
				var info = System.infoForSystem(galaxyNumber, this._waypoints[i]);
				var rt = system.info.routeToSystem(info);
				if (rt) {
					var dist = rt.distance;
				} else {
					var dist = system.info.distanceToSystem(info);
				}
				text += info.name + " (" + govs[info.government] + spc + "TL" + (info.techlevel + 1) + ", dist: " + dist.toFixed(1) + "ly)\n";
			}
		} else {
			text += "  None";
		}

		if (this._curpage === maxpages - 1 || this._waypoints.length === 0) {
			curChoices["10_NEXTPAGE"] = {
				text: "[blackmarket-nextpage]",
				color: this._disabledColor,
				unselectable: true
			};
		} else {
			curChoices["10_NEXTPAGE"] = {
				text: "[blackmarket-nextpage]",
				color: this._menuColor
			};
		}
		if (this._curpage === 0 || this._waypoints.length === 0) {
			curChoices["11_PREVPAGE"] = {
				text: "[blackmarket-prevpage]",
				color: this._disabledColor,
				unselectable: true
			};
		} else {
			curChoices["11_PREVPAGE"] = {
				text: "[blackmarket-prevpage]",
				color: this._menuColor
			};
		}
		curChoices["98_EXIT"] = {
			text: "[blackmarket-return]",
			color: this._itemColor
		};

		def = "98_EXIT";

		var opts = {
			screenID: "oolite-smuggling-waypoints-map",
			title: "Rock Hermit Waypoints",
			allowInterrupt: true,
			overlay: {
				name: "blackmarket-map_marker.png",
				height: 546
			},
			exitScreen: "GUI_SCREEN_INTERFACES",
			choices: curChoices,
			initialChoicesKey: this._lastChoice ? this._lastChoice : def,
			message: text
		};

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

//-------------------------------------------------------------------------------------------------------------
this.$screenHandler = function $screenHandler(choice) {

	if (choice == null) {
		this.$blackMarketOptions();
		return;
	}

	this._lastChoice[this._display] = choice;

	var p = player.ship;
	var stn = p.dockedStation;
	var se = worldScripts.Smugglers_Equipment;

	switch (choice) {
		case "01_NOTICEBOARD":
			this._display = 2;
			this._selectedIndex = 0;
			break;
		case "02_CONTRACTS":
			var sc = worldScripts.Smugglers_Contracts;
			sc.$smugglingContractsScreens();
			break;
		case "03_PURCHASE":
			this._display = 3;
			break;
		case "04_SELL_ILLEGAL":
			this._display = 8;
			break;
		case "04_FAKEPERMITS":
			this._display = 4;
			break;
		case "05_WAYPOINTS":
			this._display = 5;
			break;
		case "06_PHASESCAN":
			this._display = 6;
			break;
		case "07_SELLSCAN":
			this._display = 7;
			break;
		case "97_EXIT":
			this._display = 3;
			break;
		case "08_SELLADDITIONAL":
			this._display = 10;
			break;
		case "01A_CURRENT_WAYPOINTS":
			this._display = 11;
			break;
		case "10_NEXTPAGE":
			this._curpage += 1;
			break;
		case "11_PREVPAGE":
			this._curpage -= 1;
			break;
		case "20_NEXTITEM":
			this._selectedIndex += 1;
			break;
		case "21_PREVITEM":
			this._selectedIndex -= 1;
			break;
		case "30_LASTITEM":
			this._selectedIndex = this._noticeboard.length - 1;
			break;
		case "31_FIRSTITEM":
			this._selectedIndex = 0;
			break;
		case "80_ACCEPTOFFER":
			if (this._sting && p.dockedStation === this._stingStation) {
				this.$stingOperation();
				return;
			}
			// pay the player
			player.credits += this._offerQuantity * this._offerPrice;
			// remove the cargo, but because this is a black market sale, it doesn't appear on the stations market. just remove it.
			switch (this._sellType) {
				case 1: // standard hold
					p.manifest[this._sellCommodity] -= this._offerQuantity;
					break;
				case 2: // smuggling compartment
					if (se) se.$removeCargo(this._offerCommodity, this._offerQuantity);
					break;
			}
			this._blackMarketSales.push(this._sellCommodity);
			se.$playSound("sell");
			player.consoleMessage("You have been credited " + formatCredits(this._offerQuantity * this._offerPrice, true, true) + ".");
			this._sellCommodity = "";
			this._sellQuantity = 0;
			this._sellType = 0;
			this._offerQuantity = 0;
			this._offerPrice = 0;
			this._display = 8;
			break;
		case "81_REJECTOFFER":
			this._sellCommodity = "";
			this._sellQuantity = 0;
			this._sellType = 0;
			this._offerQuantity = 0;
			this._offerPrice = 0;
			this._display = 8;
			break;
		case "98_EXIT":
			this._display = 0;
			break;
	}

	if (choice.indexOf("60_") >= 0) {
		if (this._sting && p.dockedStation === this._stingStation) {
			this.$stingOperation();
			return;
		}
		if (player.credits >= (this._fakePermitCost * stn.equipmentPriceFactor)) {
			var data = choice.substring(choice.indexOf("~") + 1);
			var subdata = data.split("|");
			var sysID = parseInt(subdata[0]);
			var si = worldScripts.Smugglers_Illegal;
			si._permits.push({
				systemID: sysID,
				commodity: subdata[1],
				fake: this._fakePermits[system.ID]
			});
			player.credits -= this._fakePermitCost * stn.equipmentPriceFactor;
			this.$sendPurchasingEmail("Fake import permit for " + si.$translateCommodityList(subdata[1]) + " to " + System.infoForSystem(galaxyNumber, sysID).name,
				this._fakePermitCost * stn.equipmentPriceFactor);
		} else {
			player.consoleMessage("Insufficient funds to purchase item.");
		}
	}

	if (choice.indexOf("61_") >= 0) {
		if (player.credits >= (this._waypointCost * stn.equipmentPriceFactor)) {
			var sysID = parseInt(choice.substring(choice.indexOf("~") + 1));
			this._waypoints.push(sysID);
			player.credits -= this._waypointCost * stn.equipmentPriceFactor;
			this.$sendPurchasingEmail("Rock Hermit waypoint for " + System.infoForSystem(galaxyNumber, sysID).name,
				this._waypointCost * stn.equipmentPriceFactor);
		} else {
			player.consoleMessage("Insufficient funds to purchase item.");
		}
	}

	if (choice.indexOf("62_") >= 0) {
		if (this._sting && p.dockedStation === this._stingStation) {
			this.$stingOperation();
			return;
		}
		if (player.credits >= this._phaseScanCost * stn.equipmentPriceFactor) {
			var sysID = parseInt(choice.substring(choice.indexOf("~") + 1));
			var phase = se.$getSystemPhase(sysID);
			se.$addPhaseScan(phase, sysID, 2);
			player.credits -= this._phaseScanCost * stn.equipmentPriceFactor;
			this.$sendPurchasingEmail("Phase scan setting for " + System.infoForSystem(galaxyNumber, sysID).name,
				this._phaseScanCost * stn.equipmentPriceFactor);
		} else {
			player.consoleMessage("Insufficient funds to purchase item.");
		}
	}

	if (choice.indexOf("63_") >= 0) {
		if (this._sting && p.dockedStation === this._stingStation) {
			this.$stingOperation();
			return;
		}
		var idx = parseInt(choice.substring(choice.indexOf("~") + 1));
		var list = se.$getSellablePhaseScans();
		se.$sellPhaseScan(list[idx].gov, list[idx].tl);
		player.credits += (this._phaseScanCost * stn.equipmentPriceFactor) * 0.8;
		this.$sendSaleEmail("Phase scan setting for " + this.$governmentDescription(list[idx].gov) + " systems of tech level " + (list[idx].tl + 1), (this._phaseScanCost * stn.equipmentPriceFactor) * 0.8);
	}

	if (choice.indexOf("74_SELL") >= 0) { // sell cargo from smuggling compartment
		this._sellCommodity = choice.substring(choice.indexOf("~") + 1);
		this._sellQuantity = se.$getCargoQuantity(this._sellCommodity);
		this._sellType = 2; // smuggling compartment
		this._display = 9;
	}

	if (choice.indexOf("73_SELL") >= 0) { // sell cargo from standard hold
		var sdm = worldScripts.Smugglers_DockMaster;
		this._sellCommodity = choice.substring(choice.indexOf("~") + 1);
		this._sellQuantity = p.manifest[this._sellCommodity];
		// remove any relabelled cargo
		if (sdm && sdm.$getTotalRelabelled(this._sellCommodity) > 0) this._sellQuantity = this._sellQuantity - sdm.$getTotalRelabelled(viscargo[i].commodity);
		this._sellType = 1; // standard hold
		this._display = 9;
	}

	if (choice.indexOf("50_OTHERITEM") >= 0) {
		// todo: make it possible for the player to not find a buyer
		var idx = parseInt(choice.substring(choice.indexOf("~") + 1));
		// tell the calling script the item was sold, passing back the item key and the sell price
		worldScripts[this._additionalSaleItems[idx].worldScript][this._additionalSaleItems[idx].sellCallback](this._additionalSaleItems[idx].key, parseInt((this._additionalSaleItems[idx].cost * this._systemFactor) * 10) / 10);
		// remove the item from the list
		player.credits += parseInt((this._additionalSaleItems[idx].cost * this._systemFactor) * 10) / 10;
		this._additionalSaleItems.splice(idx, 1);
	}

	if (choice != "99_EXIT" && choice != "02_CONTRACTS") {
		this.$blackMarketOptions();
	}
}

//-------------------------------------------------------------------------------------------------------------
// sends an email confirming black market purchase
this.$sendPurchasingEmail = function $sendPurchasingEmail(itemText, cost) {
	var email = worldScripts.EmailSystem;
	if (email) {
		var ga = worldScripts.GalCopAdminServices;
		if (this._blackMarketSalesPerson === "") {
			this._blackMarketSalesPerson = expandDescription("%N [nom]");
		}
		email.$createEmail({
			sender: "Black Market",
			subject: "Purchasing",
			date: clock.seconds,
			message: expandDescription("[blackmarket-purchasing]", {
				item: itemText,
				itemcost: cost.toFixed(2),
				sender: this._blackMarketSalesPerson
			}),
			expiryDays: ga._defaultExpiryDays
		});
	}
}

//-------------------------------------------------------------------------------------------------------------
// sends an email confirming black market sale
this.$sendSaleEmail = function $sendSaleEmail(itemText, cost) {
	var email = worldScripts.EmailSystem;
	if (email) {
		var ga = worldScripts.GalCopAdminServices;
		if (this._blackMarketSalesPerson === "") {
			this._blackMarketSalesPerson = expandDescription("%N [nom]");
		}
		email.$createEmail({
			sender: "Black Market",
			subject: "Sale",
			date: clock.seconds,
			message: expandDescription("[blackmarket-sale]", {
				item: itemText,
				itemcost: cost.toFixed(2),
				sender: this._blackMarketSalesPerson
			}),
			expiryDays: ga._defaultExpiryDays
		});
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$buildNoticeboard = function $buildNoticeboard() {
	function compare(a, b) {
		if (a.idx < b.idx) return -1;
		if (a.idx > b.idx) return 1;
		return 0;
	}

	this._noticeboard = [];

	// type 1: dock master bribe mesages
	// type 2: suggested phase settings
	// type 3: where to get good fake permits
	// type 4: random chatter
	// type 5: cargo stash (or not, maybe)

	var sdm = worldScripts.Smugglers_DockMaster;
	var si = worldScripts.Smugglers_Illegal;
	var se = worldScripts.Smugglers_Equipment;
	var finalcount = 0;

	// type 1: bribe messages
	// get all systems within 15ly
	if (this._smugglers) {
		var sys = system.info.systemsInRange(15);
		var list = [];
		// look for any with a low bribe chance (< 500cr)
		for (var i = 0; i < sys.length; i++) {
			if (sys[i].hasOwnProperty("sun_gone_nova") === false || sys[i].sun_gone_nova === false) {
				var chance = sdm._bribeChance[sys[i].systemID];
				if (chance <= 0.22) {
					// decide whether or not to show it (max 2)
					if (list.length < 2 && Math.random() > 0.6) {
						var amount = parseInt(parseInt(((chance + 0.1) * 32) ^ 2) * 100) / 100;
						list.push({
							system: sys[i].name,
							amount: amount
						});
					}
				}
			}
		}
		for (var i = 0; i < list.length; i++) {
			var msg = expandDescription("[noticeboard-dmbribe]", {
				planet: list[i].system,
				amount: list[i].amount
			});
			this._noticeboard.push({
				idx: this.$rand(100),
				message: msg
			});
		}
		// type 3: fake permits
		finalcount = 0 + (Math.random() > 0.5 ? 1 : 0);
		for (var i = 0; i < sys.length; i++) {
			if (this._fakePermits[sys[i].systemID] >= 0.8) {
				if (Math.random() > 0.7) {
					var msg = expandDescription("[noticeboard-fakepermit]", {
						planet: sys[i].name
					});
					this._noticeboard.push({
						idx: this.$rand(100),
						message: msg
					});
					finalcount += 1;
				}
				// break out if we hit the limit
				if (finalcount >= 2) break;
			}
		}


		// type 2: phase settings
		// will never be super accurate, but will be better than nothing.
		// get all systems with 25ly with illegal goods
		sys = system.info.systemsInRange(25);
		list = [];
		var count = 0;
		for (var i = sys.length - 1; i >= 0; i--) {
			var goods = si.$illegalGoodsList(sys[i].systemID);
			var suggest_phase = se.$getSystemPhase(sys[i].systemID) + (this.$rand(50) * (Math.random() > 0.5 ? 1 : -1));
			// make sure we have a positive number
			if (suggest_phase < 0) suggest_phase = se.$getSystemPhase(sys[i].systemID) + (this.$rand(20) + 20);
			// make sure we don't go over the limit
			if (suggest_phase >= 1000) suggest_phase = se.$getSystemPhase(sys[i].systemID) + ((this.$rand(20) + 20) * -1);

			if (goods.length >= 0) {
				list.push({
					system: sys[i].name,
					phase: suggest_phase,
					hasIllegal: true
				});
				count += 1;
			} else {
				list.push({
					system: sys[i].name,
					phase: suggest_phase,
					hasIllegal: false
				});
			}
		}

		// pick 1 or (at most) 2
		finalcount = 0 + (Math.random() > 0.5 ? 1 : 0);
		if (count > 0) {
			// pick the ones with illegal goods
			for (var i = 0; i < list.length; i++) {
				if (list[i].hasIllegal === true && Math.random() > 0.7) {
					var msg = expandDescription("[noticeboard-phasescan]", {
						planet: list[i].system,
						phase: list[i].phase
					});
					this._noticeboard.push({
						idx: this.$rand(100),
						message: msg
					});
					finalcount += 1;
				}
				// break out if we hit the limit
				if (finalcount >= 2) break;
			}
		} else {
			// pick the ones with illegal goods
			for (var i = 0; i < list.length; i++) {
				if (Math.random() > 0.7) {
					var msg = expandDescription("[noticeboard-phasescan]", {
						planet: list[i].system,
						phase: list[i].phase
					});
					this._noticeboard.push({
						idx: this.$rand(100),
						message: msg
					});
					finalcount += 1;
				}
				// break out if we hit the limit
				if (finalcount >= 2) break;
			}
		}

		// type 5: phase updates
		finalcount = 0;
		if (se._phaseUpdates.length > 0) {
			// get a list of planets in 25 LY that have had a setting change.
			for (var i = 0; i < sys.length; i++) {
				for (var j = 0; j < se._phaseUpdates.length; j++) {
					if (sys[i].government === se._phaseUpdates[j].gov && sys[i].techlevel === se._phaseUpdates[j].tl) {
						finalcount += 1;
						var msg = expandDescription("[noticeboard-phaseupdate]", {
							planetname: sys[i].name
						});
						this._noticeboard.push({
							idx: this.$rand(100),
							message: msg
						});
					}
				}
			}
		}
	}
	// type 4: random chatter
	finalcount = 0;
	var randomchatter = this.$rand(3);
	var checking = [];
	missionVariables.randomphone = (this.$rand(899999) + 100000) + "-" + (this.$rand(899) + 100) + "-" + (this.$rand(89999) + 10000);
	for (var i = 1; i <= randomchatter; i++) {
		var msg = expandDescription("[noticeboard-randomchatter]");
		if (checking.indexOf(msg) === -1) {
			this._noticeboard.push({
				idx: this.$rand(100),
				message: msg
			});
			checking.push(msg);
		}
	}
	delete missionVariables.randomphone;
	// type 5: cargo stash
	var stash = this.$rand(3) - 1; // between 0 and 2
	var sys = system.info.systemsInRange(10);
	for (var i = 0; i < stash; i++) {
		var s = sys[Math.floor(Math.random() * sys.length)];
		// make sure we don't add two stashes in the same system
		var found = false;
		for (var i = 0; i < this._stash.length; i++) {
			if (this._stash[i].systemID == s.systemID) found = true;
		}
		if (found == true) continue;
		this._stash.push({systemID:s.systemID, created:clock.adjustedSeconds});
		missionVariables.stashdist = parseInt(system.info.distanceToSystem(s) * 10) / 10;
		var msg = expandDescription("[noticeboard-cargo-stash]");
		this._noticeboard.push({
			idx: this.$rand(100),
			message: msg
		});
		delete missionVariables.stashdist;
	}

	// sort the list, based on the random index, so the items are mixed up.
	this._noticeboard.sort(compare);
}

//-------------------------------------------------------------------------------------------------------------
// builds the fake permit array
this.$buildFakePermitArray = function $buildFakePermitArray() {
	this._fakePermits = [];

	for (var i = 0; i <= 255; i++) {
		// fake permits can have, at best, a 40% chance of success
		var chance = (Math.random() * 40) / 100;
		if (chance > 0.5) {
			this._fakePermits.push(chance);
		} else {
			this._fakePermits.push(0);
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
// adds a waypoint to the first rock hermit if the player has purchased a waypoint for this system
this.$addRHWaypoint = function $addRHWaypoint() {
	// add a waypoint to the rock hermit, if the player has one
	if (this._waypoints.indexOf(system.ID) >= 0) {
		if (this._debug) log(this.name, "A Rock Hermit waypoint has been found for the current system");
		var stns = system.stations;
		for (var i = 0; i < stns.length; i++) {
			// is this a rockhermit?
			//log(this.name, "station " + stns[i].name);
			if (Ship.roleIsInCategory(stns[i].primaryRole, "oolite-rockhermits") && stns[i].hasRole("slaver_base") === false && stns[i].hasRole("rrs_slaverbase") === false) {
				if (this._debug) log(this.name, "A Rock Hermit has been found in the current system -- adding waypoint");
				// is the waypoint already set?
				if (!system.waypoints[this.name]) {
					system.setWaypoint(
						this.name, stns[i].position, stns[i].orientation, {
							size: 100,
							beaconCode: "H",
							beaconLabel: "Rock Hermit"
						}
					);
					break;
				}
			}
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$doesStationHaveBlackMarket = function $doesStationHaveBlackMarket(station) {
	if (this.$isBlackMarketShutdown(station) === true) return false;
	var alleg = station.allegiance;
	if (alleg == null) alleg = "neutral";
	if (((this._smugglingAllegiance.indexOf(alleg) >= 0 || this.$isStationIncluded(station) === true) && this.$isStationExcluded(station) === false) ||
		(station.isMainStation && this.$mainStationHasBlackMarket() === true)) return true;
	return false;
}

//-------------------------------------------------------------------------------------------------------------
// determines whether the black market will be visible in certain systems at the main station
this.$mainStationHasBlackMarket = function $mainStationHasBlackMarket() {
	// for tl 4 and below, at Anarchies, Feudals, Multi-Gov, Communists, Dictatorships, there's a 70% chance
	if (system.info.techlevel <= 3 && system.government <= 4 && system.pseudoRandomNumber > 0.3) return true;
	// for tl 5/6, at Anarchies, Feudals, Multi-Gov, there's a 50% chance
	if (system.info.techlevel >= 4 && system.info.techlevel <= 6 && system.government <= 2 && system.pseudoRandomNumber > 0.5) return true;
	// for tl 7+, at Anarchies only, have a reducing chance
	if (system.info.techlevel > 6 && system.government === 0 && system.pseudoRandomNumber > (system.info.techlevel / 10)) return true
	// otherwise false
	return false;
}

//-------------------------------------------------------------------------------------------------------------
this.$governmentDescription = function $governmentDescription(gov) {
	switch (gov) {
		case 0: return "Anarchy";
		case 1: return "Feudal";
		case 2: return "Multi-Government";
		case 3: return "Dictatorship";
		case 4: return "Communist";
		case 5: return "Confederacy";
		case 6: return "Democracy";
		case 7: return "Corporate State";
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$setupSting = function $setupSting(station) {

	this._stingStation = station;
	this._sting = true;

	// there's a chance that there'll be a police ship at the edge of scanner range at station where sting operation is in progress
	// if the player has never been to the black market, though, we need a clue other than the name of the agent. So we must include the ship in that case
	if (Math.random() > this._stingShipChance || this.$playerHasBeenToBlackMarket(station) === false) {
		this.$addStingShip(station);
	}

	if (this._debug) {
		//log(this.name, "Sting Operation In Progress at " + this._stingStation.displayName);
		//if (this._stingShip) log(this.name, "Added police ship clue");
		// flag the station so we can easily find it for testing
		if (!this._stingStation.beaconCode || this._stingStation.beaconCode === "") {
			this._stingStation.beaconCode = "S";
			this._stingStation.beaconLabel = "Sting operation in progress";
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
// adds the sting ship to the station
this.$addStingShip = function $addStingShip(station) {
	if (!station || station.isValid === false) return;

	// reduce the chance we'll get another sting ship each time we get one.
	if (this._stingShipChance < 0.9) this._stingShipChance += 0.1;

	// work out the position of the rh dock
	var dock = null;
	var dockpos = null;
	// find the dock object of the station so we can position launched ships
	for (var i = 0; i < station.subEntities.length; i++) {
		if (station.subEntities[i].isDock) {
			dock = station.subEntities[i];
			dockpos = station.position.add( //better formula for non-centered docks
				Vector3D(
					station.heading.direction().multiply(dock.position.z),
					station.vectorUp.direction().multiply(dock.position.y),
					station.vectorRight.direction().multiply(dock.position.x)
				)
			);
			break;
		}
	}
	// move away from dock in straight line, but backwards, towards the edge of scanner range...
	var pos = dockpos.add(station.vectorForward.multiply(-(player.ship.scannerRange * 0.92)));

	// add a police ship at that position, but switch it to null AI so it will just sit there
	this._stingShip = system.addShips("police", 1, pos, 1000)[0];
	if (this._stingShip) {
		this._stingShip.switchAI("nullAI.plist");
		// set up the ship so if it's attacked it doesn't just sit there.
		// monkeypatch if necessary
		if (this._stingShip.script.shipBeingAttacked) this._stingShip.script.$sbm_ovr_shipBeingAttacked = this._stingShip.script.shipBeingAttacked;
		this._stingShip.script.shipBeingAttacked = this.$policeSting_shipBeingAttacked;
		if (this._stingShip.script.shipBeingAttackedByCloaked) this._stingShip.script.$sbm_ovr_shipBeingAttackedByCloaked = this._stingShip.script.shipBeingAttackedByCloaked;
		this._stingShip.script.shipBeingAttackedByCloaked = this.$policeSting_shipBeingAttackedByCloaked;

		this._stingShipTimer = new Timer(this, this.$checkForPlayerProximity, 5, 5);
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$policeSting_shipBeingAttacked = function $policeSting_shipBeingAttacked(whom) {
	// run monkey patch, if required
	if (this.ship.script.$sbm_ovr_shipBeingAttacked) this.ship.script.$sbm_ovr_shipBeingAttacked(whom);
	// under attack, so revert to police AI
	var sbm = worldScripts.Smugglers_BlackMarket;
	sbm._stingShipTimer.stop();
	this.ship.switchAI("oolite-policeAI.js");
	this.ship.target = whom;
	// if the attacker is anyone but the player, then the sting is a bust
	if (whom.isPlayer === false) {
		sbm._stingStation = null;
		sbm._sting = false;
	}
	sbm._stingShip = null;
	sbm.$policeSting_removeMonkeyPatches(this.ship);
}

//-------------------------------------------------------------------------------------------------------------
this.$policeSting_shipBeingAttackedByCloaked = function $policeSting_shipBeingAttackedByCloaked() {
	// run monkey patch, if required
	if (this.ship.script.$sbm_ovr_shipBeingAttackedByCloaked) this.ship.script.$sbm_ovr_shipBeingAttackedByCloaked();
	// under attack, so revert to police AI
	var sbm = worldScripts.Smugglers_BlackMarket;
	sbm._stingShipTimer.stop();
	this.ship.switchAI("oolite-policeAI.js");
	sbm._stingStation = null;
	sbm._sting = false;
	sbm._stingShip = null;
	sbm.$policeSting_removeMonkeyPatches(this.ship);
}

//-------------------------------------------------------------------------------------------------------------
this.$policeSting_removeMonkeyPatches = function $policeSting_removeMonkeyPatches(ship) {
	// remove monkey patches
	if (!ship) return;
	delete ship.script.shipBeingAttacked;
	if (ship.script.$sbm_ovr_shipBeingAttacked) {
		ship.script.shipBeingAttacked = ship.script.$sbm_ovr_shipBeingAttacked;
		delete ship.script.$sbm_ovr_shipBeingAttacked;
	}
	delete ship.script.shipBeingAttackedByCloaked;
	if (ship.script.$sbm_ovr_shipBeingAttackedByCloaked) {
		ship.script.shipBeingAttackedByCloaked = ship.script.$sbm_ovr_shipBeingAttackedByCloaked;
		delete ship.script.$sbm_ovr_shipBeingAttackedByCloaked;
	}
}

//-------------------------------------------------------------------------------------------------------------
// timer deatination to check if the player has moved within 10km of sting ship
this.$checkForPlayerProximity = function $checkForPlayerProximity() {
	if (this._stingShip == null || this._stingShip.isValid === false || this._stingShip.isInSpace === false) {
		this._stingShipTimer.stop();
		return;
	}
	// if the player comes in range, switch back to police mode
	if (player.ship && player.ship.isValid && player.ship.isInSpace && this._stingShip.position.distanceTo(player.ship) < 10000) {
		this._stingShipTimer.stop();
		delete this._stingShip.script.shipBeingAttacked;
		this._stingShip.switchAI("oolite-policeAI.js");
		this._stingShip = null;
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$playerHasBeenToBlackMarket = function $playerHasBeenToBlackMarket(station) {
	for (var i = 0; i < this._blackMarketContacts.length; i++) {
		var contact = this._blackMarketContacts[i];
		if (contact.galaxy === galaxyNumber && contact.systemID === system.ID && contact.stationName === station.displayName) return true;
	}
	return false;
}

//-------------------------------------------------------------------------------------------------------------
this.$getBlackMarketContact = function $getBlackMarketContact(station) {
	// check if a sting is in progress at this station
	if (this._sting && station === this._stingStation) return expandDescription("%N [nom]");
	// see if we already have a contact for this station
	var contactName = "";
	for (var i = 0; i < this._blackMarketContacts.length; i++) {
		var contact = this._blackMarketContacts[i];
		if (contact.galaxy === galaxyNumber && contact.systemID === system.ID && contact.stationName === station.displayName) {
			contactName = contact.name;
			break;
		}
	}
	if (contactName === "") {
		contactName = expandDescription("%N [nom]");
		this._blackMarketContacts.push({
			galaxy: galaxyNumber,
			systemID: system.ID,
			stationName: station.displayName,
			name: contactName
		});
	}
	return contactName;
}

//-------------------------------------------------------------------------------------------------------------
this.$stingOperation = function $stingOperation() {
	var p = player.ship;
	this._newBounty = player.bounty + (parseInt(Math.random() * 10) + 20);
	this._blackMarketShutdown.push({
		galaxy: galaxyNumber,
		system: system.ID,
		stationName: p.dockedStation.displayName,
		date: clock.seconds
	});
	p.dockedStation.setInterface(this.name, null);
	this._noBribe = false;
	this.$runStingScreen();
	this._caughtDate = clock.seconds;
}

//-------------------------------------------------------------------------------------------------------------
this.$runStingScreen = function $runStingScreen() {
	if (player.credits > 2000) {
		var pct = 0.1;
		for (var i = 0; i < 4; i++) {
			if (player.credits * pct < 100000) pct += 0.1;
		}
		this._newPenalty = Math.floor(player.credits * pct);
		if (this._newPenalty > 100000) this._newPenalty = 100000;

		var curChoices = {};
		if (player.credits > 0) {
			if (this._noBribe === false) {
				curChoices["03_BRIBE"] = {
					text: "[blackmarket-attempt-bribe]",
					color: this._menuColor
				};
			}
			curChoices["01_CREDIT_ACCEPT"] = {
				text: "[blackmarket-credits-accept-penalty]",
				color: this._menuColor
			};
		}
		curChoices["02_LEGAL_ACCEPT"] = {
			text: "[blackmarket-legal-accept-penalty]",
			color: this._menuColor
		};

		mission.runScreen({
			screenID: "blackmarket-sting",
			title: "GalCop Security",
			model: "[viper]",
			message: expandDescription("[blackmarket-sting]", {
				penalty: formatCredits(this._newPenalty, false, true)
			}),
			exitScreen: "GUI_SCREEN_STATUS",
			allowInterrupt: false,
			choices: curChoices,
			initialChoicesKey: "01_CREDIT_ACCEPT"
		}, this.$stingScreenHandler, this);

	} else {
		mission.runScreen({
			screenID: "blackmarket-sting",
			title: "GalCop Security",
			model: "[viper]",
			message: expandDescription("[blackmarket-sting-no-credits]"),
			exitScreen: "GUI_SCREEN_INTERFACES",
		}, this.$stingScreenHandler, this);

		player.ship.setBounty(this._newBounty, "underworld activity");
		this.$sendStingNotificationEmail("legal");
		this._newBounty = 0;
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$stingScreenHandler = function $stingScreenHandler(choice) {

	if (choice === "03_BRIBE") {
		this.$getBribeAmount();
		return;
	}
	if (choice === "01_CREDIT_ACCEPT") {
		player.credits -= this._newPenalty;
		this.$sendStingNotificationEmail("credits");
		this._newPenalty = 0;
		return;
	}
	if (choice === "02_LEGAL_ACCEPT") {
		player.ship.setBounty(this._newBounty, "underworld activity");
		this.$sendStingNotificationEmail("legal");
		this._newBounty = 0;
		return;
	}
	if (choice === "01_RETURN") {
		this.$runStingScreen();
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$isBlackMarketShutdown = function $isBlackMarketShutdown(station) {
	for (var i = 0; i < this._blackMarketShutdown.length; i++) {
		var item = this._blackMarketShutdown[i];
		// 2592000 = 30 days in seconds
		if (item.galaxy === galaxyNumber && item.systemID === system.ID && item.stationName === station.displayName && (clock.seconds - item.date) < 2592000) return true;
	}
	return false;
}

//-------------------------------------------------------------------------------------------------------------
this.$setupStingAfterLoad = function $setupStingAfterLoad() {
	// configure the sting op from saved data
	var stnName = missionVariables.Smuggling_BlackMarketStingStation;
	var stns = system.stations;
	var found = false;
	for (var i = 0; i < stns.length; i++) {
		if (stns[i].displayName === stnName) {
			this._stingStation = stns[i];
			found = true;
			break;
		}
	}
	if (found) {
		if (missionVariables.BlackMarketStingShip) this.$addStingShip(this._stingStation);
	} else {
		// couldn't find the same station, so turn off the sting
		this._sting = false;
	}

	delete missionVariables.Smuggling_BlackMarketSting;
	delete missionVariables.Smuggling_BlackMarketStingStation;
	delete missionVariables.Smuggling_BlackMarketStingShip;
}

//-------------------------------------------------------------------------------------------------------------
// calculates a number between 0.2 and 1.7 (approx) 
this.$setupSystemFactor = function $setupSystemFactor() {
	// low tech, anarchic, low economy worlds will pay a premium, hitech worlds not so much
	var eco = system.economy; // 0 to 7 (7 = poor ag)
	var tl = 15 - system.techLevel; // 0 to 14; we're inverting this so that low tech worlds are rated higher
	var gov = (8 - system.government) * 2; // 0 - 7 (0 = anarchic) we're inverting this so that anarchic worlds are rated higher. we're also giving more weight to this number

	// convert this numbers into a factor with a random element as well
	var factor = ((eco + tl + gov) / 21) * (system.scrambledPseudoRandomNumber(clock.days) * 0.4) + 0.5;;
	if (factor < 0.2) factor = 0.2

	this._systemFactor = factor;
}

//-------------------------------------------------------------------------------------------------------------
this.$sellItemCallback = function $sellItemCallback(key) {
	if (player.ship.equipmentStatus(key) === "EQUIPMENT_OK") player.ship.removeEquipment(key);
}

//-------------------------------------------------------------------------------------------------------------
this.$addCoreEquipmentToBlackMarket = function $addCoreEquipmentToBlackMarket() {
	// make sure the list is clear for this worldscript
	this.$removeWorldScriptItems("Smugglers_BlackMarket");
	// add some rare equipment to the sale item list, if they're still functioning anyway - can't sell damaged equipment!
	if (player.ship.equipmentStatus("EQ_CLOAKING_DEVICE") === "EQUIPMENT_OK") {
		var item = EquipmentInfo.infoForKey("EQ_CLOAKING_DEVICE");
		this.$addSaleItem({
			key: "EQ_CLOAKING_DEVICE",
			text: item.name,
			cost: parseInt(item.price / 10),
			worldScript: "Smugglers_BlackMarket",
			sellCallback: "$sellItemCallback"
		});
	}
	var eq = player.ship.equipment;
	for (var i = 0; i < eq.length; i++) {
		if (eq[i].equipmentKey.indexOf("NAVAL") >= 0 || eq[i].equipmentKey.indexOf("MILITARY") >= 0) {
			var item = EquipmentInfo.infoForKey(eq[i].equipmentKey);
			this.$addSaleItem({
				key: eq[i].equipmentKey,
				text: item.name,
				cost: parseInt(item.price / 10),
				worldScript: "Smugglers_BlackMarket",
				sellCallback: "$sellItemCallback"
			});
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$getBribeAmount = function $getBribeAmount() {
	var text = "";
	var inttype = Math.floor((this._bribeChance * 4) + 1);
	var inttypedesc = expandDescription("[bribe-interest-type" + inttype + "]");
	text = expandDescription("[bribe-question]", {
			interesttype: inttypedesc
		}) + "\n\n" +
		"(" + expandDescription("[bribe-cost]", {
			credits: formatCredits(player.credits, false, true)
		}) + ")";

	var opts = {
		screenID: "smuggling_sting_bribe",
		title: "Bribing Official",
		allowInterrupt: false,
		model: "[viper]",
		exitScreen: "GUI_SCREEN_STATUS",
		message: text,
		textEntry: true
	};
	mission.runScreen(opts, this.$getBribeAmountInput, this);
}

//-------------------------------------------------------------------------------------------------------------
// parses the input from the getBribeAmount screen
this.$getBribeAmountInput = function $getBribeAmountInput(param) {
	var p = player.ship;

	if (parseInt(param) >= 1 && parseInt(param) <= player.credits) {
		var amount = parseInt(param);
		// will this work
		var chance = this._bribeChance;
		// higher amounts are more likely to be accepted
		if (this._debug) log(this.name, "min bribe amount = " + (Math.pow(parseInt(chance * 50), 2.5) * (1 + system.productivity / 56300)));
		if ((Math.pow(parseInt(chance * 50), 2.5) * (1 + system.productivity / 56300)) <= amount) {
			player.credits -= amount;
			mission.runScreen({
				screenID: "sting_bribe",
				title: "Bribing Official",
				model: "[viper]",
				message: expandDescription("[bribe-complete]"),
				exitScreen: "GUI_SCREEN_STATUS"
			});
		} else {
			// a failed bribe will result in the legal penalty being applied
			p.setBounty(this._newBounty, "underworld activity");
			this.$sendStingNotificationEmail("legal");
			this._newBounty = 0;

			if (Math.random() > chance) {
				mission.runScreen({
					screenID: "sting_bribe",
					title: "Bribing Official",
					model: "[viper]",
					message: expandDescription("[bribe-angry-nopenalty]"),
					exitScreen: "GUI_SCREEN_STATUS"
				});
			} else {
				var penalty = (worldScripts.Smugglers_CoreFunctions.$rand(10) + 3);
				p.setBounty(player.bounty + penalty, "attempted bribe");
				mission.runScreen({
					screenID: "sting_bribe",
					title: "Bribing Official",
					model: "[viper]",
					message: expandDescription("[bribe-angry-penalty]"),
					exitScreen: "GUI_SCREEN_STATUS"
				});

				// send email (if installed)
				var email = worldScripts.EmailSystem;
				if (email) {
					var ga = worldScripts.GalCopAdminServices;
					email.$createEmail({
						sender: "GalCop Customs",
						subject: "Attempt to bribe official",
						date: clock.seconds,
						message: expandDescription("[failed-bribe-email]", {
							legal_penalty: penalty,
							stationname: player.ship.dockedStation.displayName,
							systemname: System.systemNameForID(system.ID)
						}),
						expiryDays: ga._defaultExpiryDays
					});
				}
			}
		}
	} else {
		this._noBribe = true;
		mission.runScreen({
			screenID: "smuggling_bribe",
			title: "Bribing Official",
			model: "[viper]",
			message: expandDescription("[blackmarket-sting-skipbribe]"),
			exitScreen: "GUI_SCREEN_STATUS",
			choices: {
				"01_RETURN": {
					text: "Press Enter to Continue"
				}
			},
		}, this.$stingScreenHandler, this);
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$sendStingNotificationEmail = function $sendStingNotificationEmail(type) {
	// send email (if installed)
	var email = worldScripts.EmailSystem;
	if (email) {
		var ga = worldScripts.GalCopAdminServices;
		email.$createEmail({
			sender: "GalCop Customs",
			subject: "Illegal Activities",
			date: clock.seconds,
			message: expandDescription("[blackmarket-sting-email-" + type + "]", {
				legal_penalty: this.newBounty,
				credit_penalty: formatCredits(this._newPenalty, false, true),
				date: clock.clockStringForTime(this._caughtDate),
				stationname: player.ship.dockedStation.displayName,
				systemname: System.systemNameForID(system.ID)
			}),
			expiryDays: ga._defaultExpiryDays
		});
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$setupCargoStash = function() {
	var z = -0.4;
	if (system.shipsWithRole("constore").length > 0) z = -0.5;
	var pos = Vector3D(0, 0, z).fromCoordinateSystem("wpu");
	// add the cargo
	var count = this.$rand(5) + 3;
	var cargo = system.addShips("[barrel]", count, pos, 500);
	this._cargoStashCount = cargo.length;
	var cmdties = ["gold", "platinum", "gem_stones"];
	for (var i = 0; i < cargo.length; i++) {
		cargo[i].setCargo(cmdties[this.$rand(3) - 1], this.$rand(10) + 5);
		cargo[i].script.shipDied = this.$cargoDied;
	}
	var type = this.$rand(3);
	if (type == 1 && this.$rand(2) == 1) type = 2; // slightly less of a chance of a pure cargo drop
	switch (type) {
		case 1: // just cargo
			break;
		case 2: // proximity mines p.position.add(p.vectorForward.multiply(10000))
			system.addShips("bm_proximity_mine", 15, pos, 3000);
			break;
		case 3: // proximity qbomb
			var qbomb = system.addShips("bm_proximity_mine", 1, pos, 1);
			qbomb[0]["is_cascade_weapon"] = true;
			break;
		case 4: // pirate trap
			this.$createPirateTrap(this.$rand(3) + 2, pos, 10000);
			break;
	}

	// things to possibly add
	// asteroids
	if (Math.random() > 0.3) {
		system.addShips("asteroid", this.$rand(30) + 20, pos, 30600);
	}
	// derelict ship with some alloys (but not if there's proximity mines)
	if (Math.random() > 0.8 && type != 2 && type != 3) {
		this.$addDerelictShip(pos);
	}
	this._stashLocation = pos;
	this._stashTimer = new Timer(this, this.$checkPlayerLocationToStash, 3, 3);
}

//-------------------------------------------------------------------------------------------------------------
this.$cargoDied = function(whom, why) {
	var sbm = worldScripts.Smugglers_BlackMarket;
	sbm._cargoStashCount - 1;
	if (sbm._cargoStashCount <= 0) {
		sbm.$removeStashSystem();
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$checkPlayerLocationToStash = function $checkPlayerLocationToStash() {
	var p = player.ship;
	if (!p || !p.isValid) {
		this._stashTimer.stop();
		return;
	}
	if (p.position.distanceTo(this._stashLocation) < 26500) {
		this._stashTimer.stop();
		this.$removeStashSystem();
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$removeStashSystem = function() {
	for (var i = this._stash.length - 1; i >= 0; i--) {
		if (this._stash[i].systemID == system.ID) {
			this._stash.splice(i, 1);
			break;
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$addDerelictShip = function(pos) {
	// add derelict ship
	// derelict ship creation code from Eric Walsh's DeepSpaceDredger OXP
	var derelict = null;
	var checkShips = system.addShips("trader", 1, pos, 5000);
	if (checkShips) derelict = checkShips[0];
	if (derelict) {
		derelict.bounty = 0;

		// set script to default, to avoid a special script for the trader doing stuff. (like setting a new AI)
		derelict.setScript("oolite-default-ship-script.js");
		derelict.switchAI("oolite-nullAI.js");

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

		derelict.script.shipLaunchedEscapePod = this.$bm_derelict_shipLaunchedEscapePod; // function to remove the escapepod after launch.
		if (derelict.equipmentStatus("EQ_ESCAPE_POD") === "EQUIPMENT_UNAVAILABLE") derelict.awardEquipment("EQ_ESCAPE_POD");
		derelict.abandonShip(); // make sure no pilot is left behind and this command turns the ship into cargo.
		derelict.primaryRole = "bm_derelict"; // to avoid pirate attacks
		derelict.displayName = derelict.displayName + " (derelict)";
		derelict.lightsActive = false;
		// add some alloys as well
		system.addShips("alloys", 5, derelict.position, 1000);
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$bm_derelict_shipLaunchedEscapePod = function $bm_derelict_shipLaunchedEscapePod(pod, passengers) {
	pod.remove(true); // we don't want floating escapepods around but need them initially to create the derelict.
}

//-------------------------------------------------------------------------------------------------------------
this.$createPirateTrap = function $createPirateTrap(count, pos, spread) {
	var pop = worldScripts["oolite-populator"];
	var grp = system.addGroup("pirate", count, pos, spread);
	var gn = grp.ships;
	if (gn && gn.length > 0) {
		for (var j = 0; j < gn.length; j++) {
			// configure our pirates
			gn[j].setBounty(20 + system.info.government + count + 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("bm_pirateAI.js");
		}
		this._pirates.push({
			group: grp,
			position: pos
		});
	} else {
		log(this.name, "!!ERROR: Pirate trap group not spawned!");
	}
}

//-------------------------------------------------------------------------------------------------------------
// makes the pirates lurk in a particular position
this.$givePiratesLurkPosition = function $givePiratesLurkPosition() {
	var retry = false;
	for (var i = 0; i < this._pirates.length; i++) {
		var grp = this._pirates[i].group;
		var pos = this._pirates[i].position;
		if (grp.leader) {
			if (grp.leader.AIScript.oolite_priorityai) {
				grp.leader.AIScript.oolite_priorityai.setParameter("oolite_pirateLurk", pos);
				grp.leader.AIScript.oolite_priorityai.reconsiderNow();
				for (var j = 0; j < grp.ships.length; j++) {
					if (grp.leader !== grp.ships[j]) {
						var shp = grp.ships[j];
						if (shp.AIScript.oolite_priorityai) {
							shp.AIScript.oolite_priorityai.setParameter("oolite_pirateLurk", pos);
							shp.AIScript.oolite_priorityai.reconsiderNow();
						} else {
							retry = true;
							break;
						}
					}
				}
			} else {
				retry = true;
				break;
			}
		}
		if (retry === true) break;
	}
	if (retry === true) {
		this._pirateSetup = new Timer(this, this.$givePiratesLurkPosition, 1, 0);
	} else {
		this._pirates.length = 0;
	}
}

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

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

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

//-------------------------------------------------------------------------------------------------------------
// return a random number between 1 and max
this.$rand = function $rand(max) {
	return Math.floor((Math.random() * max) + 1)
}
Scripts/bm_proximity_mine.js
"use strict";
this.name = "BlackMarket_ProximityMine";
this.author = "phkb";
this.copyright = "2023 phkb";
this.description = "Ship script for proximity mine";
this.licence = "CC BY-NC-SA 4.0";

this._counter = 0;

//-------------------------------------------------------------------------------------------------------------
this.shipSpawned = function () {
	this._timer = new Timer(this, this.$proximityMine, 1, 1);
	this.ship.switchAI("oolite-nullAI.js");
}

//-------------------------------------------------------------------------------------------------------------
this.shipDied = function () {
	if (this._timer.isRunning) this._timer.stop();
}

//-------------------------------------------------------------------------------------------------------------
this.shipTakingDamage = function (amount, whom, type) {
	if (amount > this.ship.energy) {
		this._timer.stop();
		if (this.ship.is_cascade_weapon) {
			this.ship.becomeCascadeExplosion();
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$proximityMine = function $proximityMine() {
	if (!this.ship || this.ship.isValid === false) {
		this._timer.stop();
		return;
	}
	if (this._counter <= 5) {
		var s = this.ship.checkScanner(true);
		if (s.length > 0) {
			this.ship.commsMessage((5 - this._counter));
			this._counter += 1;
			if (this._counter === 6) {
				log(this.name, "boom");
				this._timer.stop();
				if (this.ship.hasOwnProperty("is_cascade_weapon") && this.ship.is_cascade_weapon) {
					this.ship.becomeCascadeExplosion();
				} else {
					this.$detonation();
				}
			}
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$detonation = function () {
	delete this.shipTakingDamage; // because otherwise, the game will crash!
	this.ship.explode();
	this.ship.dealEnergyDamage(300, 1000, 0.5);
	if (this.ship.position.distanceTo(player.ship) < 2000) {
		player.ship.fuelLeakRate = 0.5;
	}
}