Back to Index Page generated: Jun 13, 2026, 7:54:53 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.7 1.7
Tags
Required Oolite Version
Maximum Oolite Version
Required Expansions
Optional Expansions
Conflict Expansions
Dependent Expansions
  • oolite.oxp.phkb.Smugglers_TGU:2.12
  • Information URL https://wiki.alioth.net/index.php/Black_Market n/a
    Download URL https://wiki.alioth.net/img_auth.php/f/f4/BlackMarket_1.7.oxz n/a
    License CC-BY-NC-SA 3.0 CC-BY-NC-SA 3.0
    File Size n/a
    Upload date 1778415420

    Relationships Diagram

    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);
        }
    
    It's possible for other OXP's to add items to be purchased on the Black Market. You can add your item using the following method:
    
        var sbm = worldScripts.Smugglers_BlackMarket;
        if (sbm) {
            sbm.$addPurchaseItem({
                key:"my_key_value",		            // reference to be passed back to your worldscript when the item is bough, 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
                purchaseCallback:"$my_functionname",// the name of the function to be called in the item is bought
            });
        }
    	
        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 adding 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.$removePurchaseItem(key);
        }
    
    You can also remove all purchase items for your worldScript with this function:
    
        var sbm = worldScripts.Smugglers_BlackMarket;
        if (sbm) {
            sbm.$removePurchaseWorldScriptItems(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.7
    - Added Pirate Coves to list of RH's that will have a Black Market.
    - Black Market Info Booklet now only purchasable through the Black Market.
    - Added new section on Black Market: "Buy Other Items", with associated API calls.
    - Added the buy/sell sound effects when buying/selling items.
    - Switched to using logcontrol for debug messages.
    
    1.6
    - Added Black Market Info Booklet to Ship's Library (if installed).
    
    1.5
    - Darkened overlay images for better compatibility with Oolite 1.92.
    
    1.4
    - Moved all text into descriptions.plist for easier localisation.
    
    1.3
    - Added sound when a fuel leak occurs.
    
    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

    Name Visible Cost [deci-credits] Tech-Level
    Black Market Info Booklet no 400 1+

    Ships

    Name
    Mine

    Models

    This expansion declares no models.

    Scripts

    Path
    Scripts/blackmarket_book.js
    "use strict";
    this.name = "Blackmarket_Book";
    this.author = "phkb";
    this.description = "Adds the Blackmarket info booklet to the Ship's Library";
    
    //-------------------------------------------------------------------------------------------------------------
    this.startUpComplete = function () {
        if (worldScripts["Ships Library"]) this._registerBook();
    
        this.$addBlackMarketBook();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipDockedWithStation = function () {
        this.$addBlackMarketBook();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$addBlackMarketBook = function () {
        if (!player.ship.hasEquipmentProviding("EQ_BLACKMARKET_BOOKLET")) {
            var sbm = worldScripts.Smugglers_BlackMarket;
            // make sure it's not there already before we add it back.
            sbm.$removePurchaseItem("EQ_BLACKMARKET_BOOKLET");
            sbm.$addPurchaseItem({
                key: "EQ_BLACKMARKET_BOOKLET",
                text: expandMissionText("blackmarket_book_summary"),
                cost: 40,
                worldScript: "Blackmarket_Book",
                purchaseCallback: "$boughtBook",
            });
        }
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this._registerBook = function () {
    
        if (!player.ship.hasEquipmentProviding("EQ_BLACKMARKET_BOOKLET")) return;
    
        this._extra = "";
        if (worldScripts.Smugglers_Illegal) {
            this._extra = expandMissionText("blackmarket_smugglers");
        }
        var contents = [
            { level: 0, key: "blackmarket_intro", backgrounds: [{ name: "blackmarket-skull.png", height: 512 }, ""] },
            { level: 1, key: "blackmarket_chap1", backgrounds: [{ name: "blackmarket-skull.png", height: 512 }, ""] },
            { level: 1, key: "blackmarket_chap2", params: [function () { return worldScripts.Blackmarket_Book._extra; }], backgrounds: [{ name: "blackmarket-skull.png", height: 512 }, ""] },
            { level: 1, key: "blackmarket_chap3", backgrounds: [{ name: "blackmarket-skull.png", height: 512 }, ""] },
            { level: 1, key: "blackmarket_chap4", backgrounds: [{ name: "blackmarket-skull.png", height: 512 }, ""] },
            { level: 1, key: "blackmarket_chap5", backgrounds: [{ name: "blackmarket-skull.png", height: 512 }, ""] }
        ];
    
        if (worldScripts.Smugglers_Illegal) {
            contents = contents.concat([
                { level: 1, key: "blackmarket_chap6", backgrounds: [{ name: "blackmarket-skull.png", height: 512 }, ""] },
                { level: 1, key: "blackmarket_chap7", backgrounds: [{ name: "blackmarket-skull.png", height: 512 }, ""] },
                { level: 1, key: "blackmarket_chap8", backgrounds: [{ name: "blackmarket-skull.png", height: 512 }, ""] }
            ]);
        }
    
        contents.push({ level: 1, key: "blackmarket_chap9", backgrounds: [{ name: "blackmarket-skull.png", height: 512 }, ""] });
    
        worldScripts["Ships Library"]._registerBook("blackmarket", expandMissionText("blackmarket_title"), contents, 22);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$boughtBook = function (key) {
        if (key == "EQ_BLACKMARKET_BOOKLET") {
            player.ship.awardEquipment("EQ_BLACKMARKET_BOOKLET");
            this._registerBook();
        }
    }
    Scripts/blackmarket_conditions.js
    "use strict";
    this.name = "BlackMarket_Conditions";
    this.author = "phkb";
    this.copyright = "2026 phkb";
    this.description = "Condition script for determining when to include blackmarket equipment";
    this.licence = "CC BY-NC-SA 3.0";
    
    //-------------------------------------------------------------------------------------------------------------
    this.allowAwardEquipment = function (equipment, ship, context) {
        if (context == "scripted") return true;
        return false;
    }
    Scripts/blackmarket_main.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", "pirate-cove"]; // 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._additionalPurchaseItems = [];
    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_BlackMarketAddPurchase) this._additionalPurchaseItems = JSON.parse(missionVariables.Smuggling_BlackMarketAddPurchase);
    	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);
    	} else {
    		delete missionVariables.Smuggling_BlackMarketAdditional;
    	}
    	if (this._additionalPurchaseItems.length > 0) {
    		missionVariables.Smuggling_BlackMarketAddPurchase = JSON.stringify(this._additionalPurchaseItems);
    	} else {
    		delete missionVariables.Smuggling_BlackMarketAddPurchase;
    	}
    	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._additionalPurchaseItems = [];
    	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;
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    /* adds an item to the Black Market additional purchase 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 bought. must be greater than 0
    	worldScript:		(required) name of the calling worldScript
    	purchaseCallback:	(required) name of the function to call if the item is bought
    */
    this.$addPurchaseItem = function $addPurchaseItem(obj) {
    	if (obj.hasOwnProperty("key") === false || obj.key == "") {
    		throw "Invalid item properties: 'key' must be supplied";
    	}
    	if (obj.hasOwnProperty("text") === false || obj.text == "") {
    		throw "Invalid item properties: 'text' must be supplied";
    	}
    	if (obj.hasOwnProperty("cost") === false) {
    		throw "Invalid item properties: 'cost' must be supplied";
    	}
    	if (parseInt(obj.cost) === 0) {
    		throw "Invalid item properties: 'cost' must be greater than 0";
    	}
    	if (obj.hasOwnProperty("worldScript") === false || obj.worldScript == "") {
    		throw "Invalid item properties: 'worldScript' must be supplied";
    	}
    	if (obj.hasOwnProperty("purchaseCallback") === false || obj.purchaseCallback == "") {
    		throw "Invalid item properties: 'purchaseCallback' must be supplied";
    	}
    	for (var i = 0; i < this._additionalPurchaseItems.length; i++) {
    		if (this._additionalPurchaseItems[i].key === obj.key) {
    			throw "Invalid item properties: 'key' of " + obj.key + " is already in use.";
    		}
    	}
    	this._additionalPurchaseItems.push(obj);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // removes all items from the purchase list linked to a particular worldScript
    this.$removePurchaseWorldScriptItems = function $removePurchaseWorldScriptItems(ws) {
    	for (var i = this._additionalPurchaseItems.length - 1; i >= 0; i--) {
    		if (this._additionalPurchaseItems[i].worldScript === ws) {
    			this._additionalPurchaseItems.splice(i, 1);
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // removes an item from the purchase list with the specific key value
    this.$removePurchaseItem = function $removePurchaseItem(key) {
    	for (var i = 0; i < this._additionalPurchaseItems.length; i++) {
    		if (this._additionalPurchaseItems[i].key === key) {
    			this._additionalPurchaseItems.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: expandDescription("[blackmarket_interface_title]"),
    			category: expandDescription("[blackmarket_interface_category]"),
    			summary: expandDescription("[blackmarket_interface_summary]"),
    			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 = expandDescription("[blackmarket_services]");
    
    		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: expandDescription("[blackmarket_smuggling]", { count: sc._contracts.length }),
    					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: "[blackmarket-sell-additional]",
    				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: expandDescription("[blackmarket_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: expandDescription("[blackmarket_noticeboard]", { num: (this._selectedIndex + 1), max: (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;
    		}
    		if (this._additionalPurchaseItems.length > 0) {
    			curChoices["07_BUYADDITIONAL"] = {
    				text: "[blackmarket-buy-additional]",
    				color: this._menuColor
    			}
    			itemcount += 1;
    		}
    		curChoices["98_EXIT"] = {
    			text: "[blackmarket-return]",
    			color: this._itemColor
    		};
    
    		for (var i = 0; i < ((pagesize - 2) - 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: expandDescription("[blackmarket_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(expandDescription("[blackmarket_item_header]"), 20) + this.$padTextLeft(expandDescription("[blackmarket_cost_header]"), 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: expandDescription("[blackmarket_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(expandDescription("[blackmarket_item_header]"), 20) + this.$padTextLeft(expandDescription("[blackmarket_cost_header]"), 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: expandDescription("[blackmarket_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(expandDescription("[blackmarket_item_header]"), 20) + this.$padTextLeft(expandDescription("[blackmarket_cost_header]"), 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: expandDescription("[blackmarket_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(expandDescription("[blackmarket_item_header]"), 20) + this.$padTextLeft(expandDescription("[blackmarket_cost_header]"), 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: expandDescription("[blackmarket_sell_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: expandDescription("[blackmarket_sell_illegals]"),
    			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: expandDescription("[blackmarket_sell_illegals]"),
    			allowInterrupt: true,
    			overlay: {
    				name: image,
    				height: 546
    			},
    			exitScreen: "GUI_SCREEN_INTERFACES",
    			choices: curChoices,
    			initialChoicesKey: def,
    			message: text
    		};
    	}
    
    	// sell other items
    	if (this._display === 10) {
    		text = expandDescription("[blackmarket-sellotheritems]", {
    			cash: formatCredits(player.credits, true, true)
    		});
    		text += this.$padTextRight(expandDescription("[blackmarket_item_header]"), 20) + this.$padTextLeft(expandDescription("[blackmarket_cost_header]"), 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: expandDescription("[blackmarket_sell_other]"),
    			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 = expandDescription("[blackmarket_rh_waypoints]", { page: (this._curpage + 1), max: maxpages });
    
    		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 += expandDescription("[blackmarket_system_info]", { sysname: info.name, gov: govs[info.government], tl: (info.techlevel + 1), dist: dist.toFixed(1) });
    			}
    		} else {
    			text += expandDescription("[blackmarket_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: expandDescription("[blackmarket_rh_waypoints_title]"),
    			allowInterrupt: true,
    			overlay: {
    				name: "blackmarket-map_marker.png",
    				height: 546
    			},
    			exitScreen: "GUI_SCREEN_INTERFACES",
    			choices: curChoices,
    			initialChoicesKey: this._lastChoice ? this._lastChoice : def,
    			message: text
    		};
    
    	}
    
    	// buy additional items
    	if (this._display === 12) {
    		text = expandDescription("[blackmarket-buyotheritems]", {
    			cash: formatCredits(player.credits, true, true)
    		});
    		text += this.$padTextRight(expandDescription("[blackmarket_item_header]"), 26) + this.$padTextLeft(expandDescription("[blackmarket_cost_header]"), 5);
    
    		var list = this._additionalPurchaseItems;
    
    		for (var i = 0; i < list.length; i++) {
    			curChoices["40_OTHERITEM~" + (i < 10 ? "0" : "") + i] = {
    				text: this.$padTextRight(list[i].text, 26) +
    					this.$padTextLeft(formatCredits(parseInt(list[i].cost * this._systemFactor), true, true), 5),
    				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 - 5) - 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: expandDescription("[blackmarket_buy_other]"),
    			allowInterrupt: true,
    			overlay: {
    				name: image,
    				height: 546
    			},
    			exitScreen: "GUI_SCREEN_INTERFACES",
    			choices: curChoices,
    			initialChoicesKey: 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 "07_BUYADDITIONAL":
    			this._display = 12;
    			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);
    			this.$playSound("sell");
    			player.consoleMessage(expandDescription("[blackmarket_credit]", { amount: 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)) {
    			this.$playSound("buy");
    			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(expandDescription("[blackmarket_email_fake_permit]", { commodity: si.$translateCommodityList(subdata[1]), dest: System.infoForSystem(galaxyNumber, sysID).name }),
    				this._fakePermitCost * stn.equipmentPriceFactor);
    		} else {
    			player.consoleMessage(expandDescription("[blackmarket_no_cash]"));
    		}
    	}
    
    	if (choice.indexOf("61_") >= 0) {
    		if (player.credits >= (this._waypointCost * stn.equipmentPriceFactor)) {
    			var sysID = parseInt(choice.substring(choice.indexOf("~") + 1));
    			this._waypoints.push(sysID);
    			this.$playSound("buy");
    			player.credits -= this._waypointCost * stn.equipmentPriceFactor;
    			this.$sendPurchasingEmail(expandDescription("[blackmarket_email_rh_waypoint]", { dest: System.infoForSystem(galaxyNumber, sysID).name }),
    				this._waypointCost * stn.equipmentPriceFactor);
    		} else {
    			player.consoleMessage(expandDescription("[blackmarket_no_cash]"));
    		}
    	}
    
    	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);
    			this.$playSound("buy");
    			se.$addPhaseScan(phase, sysID, 2);
    			player.credits -= this._phaseScanCost * stn.equipmentPriceFactor;
    			this.$sendPurchasingEmail(expandDescription("[blackmarket_email_phase_scan]", { dest: System.infoForSystem(galaxyNumber, sysID).name }),
    				this._phaseScanCost * stn.equipmentPriceFactor);
    		} else {
    			player.consoleMessage(expandDescription("[blackmarket_no_cash]"));
    		}
    	}
    
    	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();
    		this.$playSound("sell");
    		se.$sellPhaseScan(list[idx].gov, list[idx].tl);
    		player.credits += (this._phaseScanCost * stn.equipmentPriceFactor) * 0.8;
    		this.$sendSaleEmail(expandDescription("[blackmarket_email_govtype_phase_scan]", { gov: this.$governmentDescription(list[idx].gov), tl: (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("40_OTHERITEM") >= 0) {
    		var idx = parseInt(choice.substring(choice.indexOf("~") + 1));
    		var item = this._additionalPurchaseItems[idx];
    		var cost = parseInt(item.cost * this._systemFactor);
    		if (player.credits >= cost) {
    			this.$playSound("buy");
    			// tell the calling script the item was bought, passing back the item key and the purchase price
    			worldScripts[item.worldScript][item.purchaseCallback](item.key, cost);
    			// remove the item from the list
    			player.credits -= cost;
    			this._additionalPurchaseItems.splice(idx, 1);
    		}
    	}
    
    	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));
    		var item = this._additionalSaleItems[idx];
    		var cost = parseInt((item.cost * this._systemFactor) * 10) / 10;
    		this.$playSound("sell");
    		// tell the calling script the item was sold, passing back the item key and the sell price
    		worldScripts[item.worldScript][item.sellCallback](item.key, cost);
    		// remove the item from the list
    		player.credits += cost;
    		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: expandDescription("[blackmarket_email_sender]"),
    			subject: expandDescription("[blackmarket_email_subject_buy]"),
    			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: expandDescription("[blackmarket_email_sender]"),
    			subject: expandDescription("[blackmarket_email_subject_sell]"),
    			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) {
    		log(this.name + ".debug", "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) {
    				log(this.name + ".debug", "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: expandDescription("[blackmarket_rh_beacon]")
    					}
    					);
    					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.scrambledPseudoRandomNumber(1190) > 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.scrambledPseudoRandomNumber(1190) > 0.5) return true;
    	// for tl 7+, at Anarchies only, have a reducing chance
    	if (system.info.techlevel > 6 && system.government === 0 && system.scrambledPseudoRandomNumber(1190) > (system.info.techlevel / 10)) return true
    	// otherwise false
    	return false;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$governmentDescription = function $governmentDescription(gov) {
    	switch (gov) {
    		case 0: return expandDescription("[blackmarket_gov_anarchy]");
    		case 1: return expandDescription("[blackmarket_gov_feudal]");
    		case 2: return expandDescription("[blackmarket_gov_multigov]");
    		case 3: return expandDescription("[blackmarket_gov_dictatorship]");
    		case 4: return expandDescription("[blackmarket_gov_communist]");
    		case 5: return expandDescription("[blackmarket_gov_confederacy]");
    		case 6: return expandDescription("[blackmarket_gov_democracy]");
    		case 7: return expandDescription("[blackmarket_gov_corpstate]");
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    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 + ".debug", "Sting Operation In Progress at " + this._stingStation.displayName);
    		//if (this._stingShip) log(this.name + ".debug", "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: expandDescription("[blackmarket_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: expandDescription("[blackmarket_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: expandDescription("[blackmarket_bribe_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
    		log(this.name + ".debug", "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: expandDescription("[blackmarket_bribe_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: expandDescription("[blackmarket_bribe_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: expandDescription("[blackmarket_bribe_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: expandDescription("[blackmarket_email_galcop_customs]"),
    						subject: expandDescription("[blackmarket_email_bribe_subject]"),
    						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: expandDescription("[blackmarket_bribe_official]"),
    			model: "[viper]",
    			message: expandDescription("[blackmarket-sting-skipbribe]"),
    			exitScreen: "GUI_SCREEN_STATUS",
    			choices: {
    				"01_RETURN": {
    					text: "[blackmarket_press_enter]"
    				}
    			},
    		}, 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: expandDescription("[blackmarket_email_galcop_customs]"),
    			subject: expandDescription("[blackmarket_email_illegal_subject]"),
    			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;
    	if (this._stashTimer && this._stashTimer.isRunning) this._stashTimer.stop();
    	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) < p.scannerRange) {
    		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 + expandDescription("[blackmarket_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)
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // play the buy/sell sound effects
    this.$playSound = function $playSound(soundtype) {
    	var mySound = new SoundSource;
    
    	switch (soundtype) {
    		case "buy":
    			mySound.sound = "[buy-commodity]";
    			break;
    		case "sell":
    			mySound.sound = "[sell-commodity]";
    			break;
    		case "mode":
    			mySound.sound = "[@click]";
    			break;
    		case "activate":
    			mySound.sound = "[@beep]";
    			break;
    		case "stop":
    			mySound.sound = "[@boop]";
    			break;
    	}
    	mySound.loop = false;
    	mySound.play();
    }
    
    Scripts/blackmarket_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).toString());
    			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;
    		this.$playSound("leak");
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // plays the hydraulic sound of the silicone sealant being applied
    this.$playSound = function $playSound(type) {
    	var mySound = new SoundSource;
        switch (type) {
            case "leak":
                mySound.sound = "[fuel-leak]";
                break;
        }
    	mySound.loop = false;
    	mySound.play();
    }