Back to Index Page generated: Dec 20, 2024, 7:22:09 AM

Expansion Bulletin Board System

Content

Manifest

from Expansion Manager's OXP list from Expansion Manifest
Description Adds an interface screen for local or event-driven mission opportunities. Adds an interface screen for local or event-driven mission opportunities.
Identifier oolite.oxp.phkb.BulletinBoardSystem oolite.oxp.phkb.BulletinBoardSystem
Title Bulletin Board System Bulletin Board System
Category Miscellaneous Miscellaneous
Author phkb phkb
Version 2.6.3 2.6.3
Tags
Required Oolite Version
Maximum Oolite Version
Required Expansions
Optional Expansions
Conflict Expansions
Information URL https://wiki.alioth.net/index.php/Bulletin_Board_System n/a
Download URL https://wiki.alioth.net/img_auth.php/a/ad/BulletinBoardSystem_2.6.3.oxz n/a
License CC-BY-NC-SA 4.0 CC-BY-NC-SA 4.0
File Size n/a
Upload date 1716769959

Documentation

Also read http://wiki.alioth.net/index.php/Bulletin%20Board%20System

readme.txt

Bulletin Board System
By Nick Rogers

Overview
========
This OXP adds a bulletin board to most stations in Oolite, from which missions (added by other OXP's) can be viewed and accepted. This OXP doesn't include any missions itself - it is a framework that other OXP's can utilise to make the process of adding missions easier.

Player Usage
============
The Bulletin Board is accessed through the Interfaces screen (F4), and it's normally found under the "Contracts" section. If there are no missions available, and you don't have any active missions, the interface won't be listed. When there are missions available, or you have active missions, the number of these will be displayed on the interface entry. eg "GalCop bulletin board (4 available, 2 active)".

When the interface is opened, any active missions will be grouped at the top of the list. Active missions are marked with a "•" and also have a value in the "%" (percentage completed) column. All other entries in the list are available missions.

If the destination system of a mission is directly on your currently plotted course on the F6 system map, the mission will be coloured green. If the destination is within 7ly of any system on your currently plotted course, the mission will be colours dark green. Using the Library Config OXP (see http://wiki.alioth.net/index.php/Library), the use of colour can be changed to instead use markers, where a † beside the destination system name means "on current course", and a ‡ means "near current course". 

When a mission is selected from the list, details of the mission will be displayed. These details include:
    Description              The short description of this mission.
    Details                  Full details of the mission parameters.
    Destination system       Where the mission needs to be completed at.
    Mission expires in       How much time you have to complete the mission.
    Payment on completion    How much you will be paid when the mission is completed.
    Penalty                  How much you will be charged if you fail to complete the mission. This value will be scaled by how much of the mission you completed.

If you have accepted a mission you will also see:    
    Percent complete         How much of the mission has been completed so far.

At the bottom of the screen will be the following options:
    Accept mission           This will signal that you are starting this mission.
    Show destination         This will display a chart (either a short range or long range, depending on the distance to the destination), allowing the player to 
                             see where the destination is.

Once you have accepted a mission, you will see the following options:
    Terminate mission        This will signal that you are giving up on this mission. If the mission has a penalty, it will be applied now. The mission will be removed from the list.
    Complete mission         This will only be available once the percent complete reaches 100%. This option will give you your payment and remove the mission from the list.

Active missions are shown on the F5F5 Manifest screen, under the "Bulletin Board Missions" heading. Depending on the mission, you will likely see how much of the mission you have yet to complete, and how much time is remaining before the mission expires.

Some missions will automatically complete as soon as you reach 100%, some will complete the next time you dock. It's important to check back on the Bulletin Board to see if you need to "Complete" the mission yourself.

Technical Details
=================
The purpose of the Bulletin Board system is to create a simple yet flexible interface which OXP authors can use to offer custom missions to the player. Missions added to the list are intended to be only available in the current system. Active missions are stored by the system and saved in the save game file, but the list of available missions will be cleared out with each hyperspace jump. 

To add a new mission to the Bulletin Board, you need to run the "$addBBMission" function and pass an object with the following parameters:

   ---------------------------------------------------------------------------------------------------------------------------
    var bb = worldScripts.BulletinBoardSystem;
    if (bb) {
        var myID = bb.$addBBMission({
                description:"My first mission",
                source:7,
                destination:129,
                stationKey:"galcop",
                details:"This is my first sample mission available on the Bulletin board.",
                manifestText:"My mission is active!",
                statusText:"A special text description to include on the briefing screen.",
                expiry:clock.seconds + 86400,
                payment:1000,
                penalty:100,
                deposit:0,
                allowPartialComplete:false,
                allowTerminate:true,
                accepted:false,
                percentComplete:0,
                completionType:"AT_SOURCE",
                stopTimeAtComplete:false,
                markerShape:"MARKER_PLUS",
                markerColor:"cyanColor",
                markerScale:1.0,
                model:"police",
                spinModel:true,
                disablePercentDisplay:true,
                playAcceptedSound:true,
                noEmails:true,
                customDisplayItems:"",
                initiateCallback:"$myMissionInitiate",
                completedCallback:"$myMissionComplete",
                confirmCompleteCallback:"$myMissionConfirmComplete",
                terminateCallback:"$myMissionTerminate",
                failedCallback:"$myMissionFailed",
                manifestCallback:"$myMissionUpdateManifest",
                availableCallback:"$myMissionAvailableCheck",
                worldScript:"MyFirstMission",
                keepAvailable:false
        });
    }
   ---------------------------------------------------------------------------------------------------------------------------

In detail these parameters are:
    ID                          (optional) You can specify your own ID number, rather than let the BB system create one. 
                                    If the ID you specify is already in use an error will be generated.
    description                 (required) This is the description of the mission shown in the Bulletin board list
    source                      (optional) The system ID of the source of this mission. If not included it will default to the 
                                    current system's ID.
    destination                 (required) This is the system ID of the destination system where the mission will be carried out. 
                                    This can be the current system, if the mission is local.
                                    Setting destination to -1 equals Interstellar space.
                                    Setting destination to 256 means there is no fixed destination.
    stationKey                  (optional) The stationKey helps to limit which stations the mission will be offered in. 
                                    See the section below title "Station Keys" for more details. 
                                    If omitted this will default to "" (blank), meaning this mission will be available from all 
                                    stations in this system
    details                     This is the full details of the mission parameters.
    manifestText                (optional) This is the text to display on the F5F5 manifest screen when the mission has been 
                                    accepted. See also "manifestCallback" below.
    statusText                  (optional) This is the text to display on the mission briefing screen when the mission is active. 
                                    If not set, the "manifestText" property will be used instead.
    expiry                      (required) The time is clock seconds when the mission must be completed by. If set to -1 this
                                    means the item has no expiry time.
    payment                     (optional) The number of credits the player will be given when they complete the mission.
    penalty                     (optional) The number of credits the player will be charged if they fail to complete the mission, 
                                    scaled by the percentage completed.
    deposit                     (optional) The number of credits the player will need to pay up-front to accept the mission. 
                                    This amount will be refunded when the mission is completed.
    allowPartialComplete        (optional) True/false value that indicates whether the player can complete the mission with less 
                                    than the full percentage. 
                                    Payment and deposit will be scaled by the percentage completed. Penalties will also apply, 
                                    again scaled by the percentage completed.
                                    For example, if the payment is 100 cr and the penalty 10 cr, and the player completes 70% of 
                                    the mission, if they hand it in they would receive 70 cr (70% of 100), and the penalty would 
                                    be 3 cr (30% of 10), meaning their total payment would be 73 cr.
                                    Default is false.
    allowTerminate              (optional) True/false value indicating whether the "Terminate mission" will be available to the 
                                    player after accepting the mission. The default is true.
    accepted                    (optional) True/false value indicating whether this mission will be added to the list as already 
                                    accepted by the player. Under normal circumstances this item can be left out. However, if 
                                    you want to link missions together, so completing one mission automatically starts a second 
                                    mission, you may want to add the mission to the list as "accepted:true" so the player 
                                    doesn't have to go to the Bulletin Board and manually accept the second mission.
                                    The default is false.
    percentComplete             (optional) A decimal value between 0 and 1. Allows you to create missions that already have some 
                                    part completed.
    completionType              (optional) A text value indicating what should happen when the player completes the mission 
                                    (ie the percentComplete value reached 1, or 100%). Can be one of these:
                                    AT_SOURCE                Player must return to the source system/station, open the mission 
                                                                and select "Complete mission".
                                    AT_STATIONKEY            Player can return to any system, dock at any station with the same 
                                                                stationKey, open the mission and select "Complete mission".
                                    ANYWHERE                 Player can return to any system, dock at any station, open the 
                                                                mission and select "Complete mission".
                                    IMMEDIATE                Player is rewarded immediately when the mission is flagged as 100% 
                                                                complete - player won't need to dock anywhere.
                                    WHEN_DOCKED_SOURCE       Player is automatically rewarded as soon as they next dock at the 
                                                                source system/station. Notice will appear in arrival report.
                                    WHEN_DOCKED_STATIONKEY   Player is automatically rewarded as soon as they next dock at any 
                                                                station, any system, but with the same station key. Notice will 
                                                                appear in arrival report.
                                    WHEN_DOCKED_ANYWHERE     Player is automatically rewarded as soon as they next dock at any 
                                                                station, any system. Notice will appear in arrival report.
                                    The default is "AT_SOURCE".
    stopTimeAtComplete          (optional) True/false flag to indicate that the clock will stop when the mission is flagged 
                                    100% complete. 
                                    Default false. This means that, for a completionType of "AT_SOURCE" the player has to 
                                    return to the original station within the allowed time in order to complete the mission. 
                                    If this flag is set to true, once the player completes the mission at the destination, 
                                    they are free to take as much time as they like to return to the original station and 
                                    hand in their mission.
                                    This flag will be ignored if the completionType is set to "IMMEDIATE".
    arrivalReportText           (optional) When the completionType is set to "WHEN_DOCKED_*" this text will be displayed on the 
                                    arrival report when the player completes the mission.
                                    If omitted, will default to "'<description>' mission complete. You have been awarded <payment>."
    model                       (optional) Role of a ship to use as the background on the mission details screen.
    modelPersonality            (optional) The entityPersonality assigned to the ship model.
    spinModel                   (optional) True/false value indicating whether the ship model will rotate or not. 
                                    The default is true.
    background                  (optional) guiTextureSpecifier (name of a picture used as background)
    overlay                     (optional) guiTextureSpecifier (name of a picture used as overlay)
                                    Will default to the bulletin board graphic when not set.
    mapOverlay                  (optional) guiTextureSpecifier (name of a picture used as overlay) for the map screen.
                                    Will default to the "overlay" setting (if set), otherwise will use the bulletin board graphic.
    forceLongRangeChart         (optional) True/false value which, when true, indicates that the map display for this mission 
                                    should always be a long range chart. When false, map zoom will be chosen based on the distance
                                    between the source and destination systems.
                                    The default is false.
    markerShape                 (optional) Allows the shape of the galactic chart marker to be overridden. 
                                    Default is "MARKER_PLUS". Possible values are:
                                    NONE             No galactic chart marker will be added.
                                    MARKER_X         Uses an "X" to mark the system.
                                    MARKER_PLUS      Uses a "+" to mark the system.
                                    MARKER_SQUARE    Uses a square shape to mark the system.
                                    MARKER_DIAMOND   Uses a diamond shape to mark the system.
    markerColor                 (optional) Allows the color of the galactic chart marker to be overridden. 
                                    Default is "redColor".
    markerScale                 (optional) Allows the scale setting of the galactic chart marker to be overridden. 
                                    Value between 0.5 and 2.0. Default is 1.0.
    additionalMarkers           (optional) Array of marker dictionaries definition any extra systems that should be marked on
                                    the system map for this mission.
                                    Each item can have:
                                    system          system ID to mark (required)
                                    markerShape     one of the standard marker shapes (default MARKER_PLUS)
                                    markerColor     color of the marker (default redColor)
                                    markerScale     scale of the marker (default 1.0)
    disablePercentDisplay       (optional) Controls the display of the "Percent complete" value on this mission details page. 
                                    Default is false.
                                    When set to true, only the mission manifest text will be shown on the mission details page.
    playAcceptedSound           (optional) Controls whether the [contract-accepted] sound will be player upon contract acceptance. 
                                    Default is true.
    noEmails                    (optional) If the Email System is installed, but you don't want this item to send confirmation 
                                    emails, set this flag to true.
    customDisplayItems          (optional) Allows a mission to specify additional, non-standard items on the details page.
                                    When used, should be set to an array of dictionary items, having "heading" and "value" 
                                    properties in each dictionary object.
                                    eg [{heading:"Special Instructions:", value:"Be very, very qwiet..."}]
    customMenuItems             (optional) Provides a way for a third party to add custom menu entries to a BB item. 
                                    When used, should be set to an array of dictionary items, having the following elements:
                                    text            (required) Text to display in the menu item
                                    worldScript     (required) Name of the worldScript which contains the callback function
                                    callback        (required) Name of the callback function 
                                                        (the mission ID will be passed as a parameter)
                                    condition       (optional) Name of a callback function that will return either a blank string,
                                                        meaning the menu item is available, or a string containing the reason why
                                                        the menu item is unavailable.
                                                        (the mission ID will be passed as a parameter)
                                    activeOnly      (optional) Boolean value indicating the menu item is only visible when the
                                                        mission is active. Default is true.
                                    autoRemove      (optional) Boolean value that indicates the menu should be removed from 
                                                        the BB item as soon as the player selects it. Default is false.
    initiateCallback            (optional) The function name to call back when the mission is accepted by the player.
    completedCallback           (optional) The function name to call back when the mission is completed by the player, 
                                    based on the completionType setting.
    confirmCompleteCallback     (optional) A function name to call to confirm the mission has been completed. Useful if there 
                                    are secondary steps that must have been completed by the player.
                                    For instance, you might want to check that the player has a particular amount of a certain 
                                    commodity in their hold.
                                    Expects a string return value of blank (meaning mission is completed) or [string explaining 
                                    why mission cannot be completed] (meaning the mission is not complete).
    terminateCallback           (optional) The function name to call back when the mission is terminated/cancelled by the player.
    failedCallback              (optional) The function name to call back when the player fails to complete the mission in the 
                                    alloted time. Called during the "shipWillDockWithStation" event.
    manifestCallback            (optional) The function name to call back when the manifest entry for the mission requires updating.
                                    Note: If both the manifestText and manifestCallback are blank, no information will be
                                    displayed for this mission if it becomes active.
    availableCallback           (optional) The function to call to check if the mission is actually available. The mission ID will 
                                    be passed as a parameter. Function should return a string value of either a blank (""), to 
                                    indicate the mission is available, or a short description of the reason why the mission is
                                    unavailable (eg "Insufficient cargo space").
    bonusCalculationCallback    (optional) The function to call to calculate and return any bonus payment when the player 
                                    successfully completes a mission. Will only be called if the mission is 100% completed 
                                    inside the allotted time. If set, must return a number >= 0. Will be called after the 
                                    completedCallback function, if set.
    worldScript                 (required) The name of the worldScript where these functions reside.
    keepAvailable               (optional) Flag to indicate whether mission should kept when player leaves the system. Default false.
    postStatusMessages          (optional) Array of dictionary objects defining a message to be displayed to the player when 
                                    they accept, complete or terminate a mission.
                                    status             (required) Defines which status the message will follow. Possible values:
                                                           initiated, completed, terminated
                                    text               (required) The text of the message to be displayed
                                    return             (optional) Where to return the player to after the mission page is displayed
                                                           item     Returns player to the mission details page (N/A for completed/terminated)
                                                           list     Returns player to the mission listing
                                                           exit     Returns player to the F4 Interfaces screen
                                    background         (optional) guiTextureSpecifier to use for the background of the message
                                    overlay            (optional) guiTextureSpecifier to use for the overlay of the message
                                    model              (optional) ship model to use on the message.
                                    modelPersonality   (optional) The entityPersonality assigned to the ship model.
                                    spinModel          (optional) True/false value indicating whether the ship model will rotate or not. 
                                                           The default is true.
                                    If any of the optional parameters are left out, the equivalent parameter from the BB item will be used.
    data                        (optional) Any data that might be useful to store with the mission for later access.

This call will return a unique ID (integer) value (either created by the BB system or passed from the caller) that can be stored and used to reference this mission in other bulletin board functions.

Once a mission has been added, it is up to the calling script to work out and track the criteria required to complete a mission. The BB system will handle the display of information, and will pay or charge the player if they complete or fail the mission. But the rules which govern the mission criteria are controlled solely by the calling routine. It is up to the calling routine to track the mission and tell the BB system when updates are required.

With that in mind, the following functions will be of use to the calling party:

   ---------------------------------------------------------------------------------------------------------------------------
   $updateBBMissionPercentage(bbID, percentage);
   ---------------------------------------------------------------------------------------------------------------------------
   This function will update the percentage completed value for a given mission ID (as provided by the $addBBMission function).
   
   When this function is called, it will also initiate the "manifestCallback" routine, giving the OXP an opportunity to update the manifest entry.
   
   If the percentage is 1 (ie. 100%), and the completionType is "IMMEDIATE", the "completedCallback" routine will be called and the mission will be removed from the list. Otherwise, the mission will be completed at another event (eg when docking, or when the player selects "Complete Mission" on the mission details screen.)
   
   ---------------------------------------------------------------------------------------------------------------------------
   $updateBBManifestText(bbID, newtext);
   ---------------------------------------------------------------------------------------------------------------------------
   This function will update the manifest text entry for a given mission ID. This function should generally be called from the "manifestCallback" routine.

   ---------------------------------------------------------------------------------------------------------------------------
   $updateBBStatusText(bbID, newtext);
   ---------------------------------------------------------------------------------------------------------------------------
   This function will update the status text on the mission briefing screen for a given mission ID. This function should generally be called from the "manifestCallback" routine. If no status text is supplied, the "manifestText" property will be used instead.

   ---------------------------------------------------------------------------------------------------------------------------
   $shuffleBBList()
   ---------------------------------------------------------------------------------------------------------------------------
   A function to randomise the order of bulletin board items. This can be handy if you add missions to the list in a sequence, but you don't want that sequence to be visible to the player by having all the same types of missions grouped together.
   
   ---------------------------------------------------------------------------------------------------------------------------
   $percentCompleted(bbID)
   ---------------------------------------------------------------------------------------------------------------------------
   Returns the current percentage complete value for a particular mission ID.

   ---------------------------------------------------------------------------------------------------------------------------
   $removeBBMission(bbID)
   ---------------------------------------------------------------------------------------------------------------------------
   Removes a particular mission ID from the list. This method skips any mission constraints and completed/failed events and just removed the record. This can be useful when chaining missions together, if the previous mission needs to be removed completely, without performing any of the callbacks.

   ---------------------------------------------------------------------------------------------------------------------------
   $getItem(bbID)
   ---------------------------------------------------------------------------------------------------------------------------
   Returns a bulletin board mission object for the given mission ID.

   ---------------------------------------------------------------------------------------------------------------------------
   $getIndex(bbID)
   ---------------------------------------------------------------------------------------------------------------------------
   Returns the index value of the given mission ID in the internal data array. This can be useful if you would like to update mission parameters directly (eg. changing the "stopTimeAtComplete" flag).
   Note: Because index values are subject to change without notices (particularly between saves) the value returned from this function should be used and then discarded. Do not try to save the index value in some method and use it later - you may be updating a completely different mission!

   ---------------------------------------------------------------------------------------------------------------------------
   $removeCustomMenuItem(bbID, index)
   ---------------------------------------------------------------------------------------------------------------------------
   Removes the custom menu item from the passed mission ID at the index point.

   ---------------------------------------------------------------------------------------------------------------------------
   $registerBBEvent(wsName, fnName, eventName)
   ---------------------------------------------------------------------------------------------------------------------------
   Registers a worldScript/function to be called when a particular BB system event takes place. 
       wsName                         Name of worldscript where function can be found.
       fnName                         Name of the function to call.
       eventName                      Name of the event to attach to.
   At the moment, the possible events are:
       missionAdded                   Occurs when a mission is added to the BB.
       open                           Occurs when the BB is opened.
       close                          Occurs when the BB is closed using the "Close" option.
       exit                           Occurs when the BB is closed by the player selecting another Fn command.
       launchExit                     Occurs when the BB is closed by the player launching their ship. The station launched from will be passed as a parameter.
       preListDisplay                 Occurs just before the BB mission list is displayed
       postListDisplay                Occurs just after the BB mission list is displayed
       preItemDisplay                 Occurs just before a mission detail page is displayed. The mission ID will be passed as a parameter.
       postItemDisplay                Occurs just after a mission detail page is displayed. The mission ID will be passed as a parameter.
       preItemChartDisplay            Occurs just before the chart view of a mission is displayed. The mission ID will be passed as a parameter.
       postItemChartDisplay           Occurs just after the chart view of a mission is displayed. The mission ID will be passed as a parameter.
       shipWillDockWithStation_start  Occurs at the beginning of the shipWillDockWithStation world event.
       shipWillDockWithStation_end    Occurs at the end of the shipWillWillDockWithStation world event.

   ---------------------------------------------------------------------------------------------------------------------------
   $unregisterBBEvent(wsName, fnName, eventName)
   ---------------------------------------------------------------------------------------------------------------------------
   Removes a callback from the registration list.
       wsName                     Name of worldscript where function can be found.
       fnName                     Name of the function to call.
       eventName                  Name of the event to detach from.

   ---------------------------------------------------------------------------------------------------------------------------
   $setBackgroundDefault(guiTextureSpecifer)
   ---------------------------------------------------------------------------------------------------------------------------
   Sets the default background to use on the BB.

   ---------------------------------------------------------------------------------------------------------------------------
   $resetBackgroundDefault()
   ---------------------------------------------------------------------------------------------------------------------------
   Resets the default background back to the original default.

   ---------------------------------------------------------------------------------------------------------------------------
   $setOverlayDefault(guiTextureSpecifier)
   ---------------------------------------------------------------------------------------------------------------------------
   Sets the default overlay to use on the BB.

   ---------------------------------------------------------------------------------------------------------------------------
   $resetOverlayDefault()
   ---------------------------------------------------------------------------------------------------------------------------
   Resets the default overlay back to the original default.

Station Keys
------------
To limit the stations at which a mission will be available from, a station key is required. When the "stationKey" is not provided to the "$addBBMission" function, the mission will be available at all stations in the source system.

Each station can have multiple station keys, depending on the worldScript, but if no specific station key is added, the default station key will be the stations "allegiance" property. So, a station with an allegiance of "galcop" will have a default station key of "galcop". Therefore, if you want to limit your mission to only be available at GalCop-aligned stations, you would include "galcop" in the "stationKey" when adding the mission to the bulletin board. If you want to include your mission at both "galcop" and "chaotic" stations, you would use a comma separator and make your mission stationKey "galcop,chaotic".

But what if allegiance is insufficient to identify the specific station you want to add your mission to? In these cases, you need to give your target station a special stationKey of its own. At some point after the system has been populated, (for instance, during the systemWillPopulate or systemWillRepopulate events), find the station you want to give your special key to and perform the following function:

    var bb = worldScripts.BulletinBoardSystem;
    var stns = system.stations;
    
    // loop through all the available stations
    for (var i = 0; i < stns.length; i++) {
        // if this station is the one I'm looking for....you'll need to provide some logic for identifying your station
        if (stns[i].~~~something~~~) {
            // add the station key for this worldScript
            bb.$addStationKey(this.name, stns[i], "myStationKey");

            // first parameter is "worldScriptName". Normally this would be "this.name".
            // second parameter is "station". This is a reference to the station.
            // third parameter is "stationKey". This is the station key that will be added to the list of keys for this worldScript and station combination.
        }
    }

Once the station has been given this custom key for your worldScript, you can use that key when adding missions. The custom key will override whatever default key the station may have had previously. A station can have multiple keys for your worldScript.

For example, let's say I wanted to create a mission that was only available at the main station of the system. However, with OXP's like "Stations For Extra Planets" installed, I can't just rely on the station.allegiance. What I need to do is this:

    this.systemWillPopulate = function() {
        if (system.mainStation) {
            var bb = worldScript.BulletinBoardSystem;
            bb.$addStationKey(this.name, system.mainStation, "mainStation");
        }
    }

Now I can apply the "mainStation" station key to any bulletin board missions I add, and they will now only be visible at the main station in the system.

Removing the Bulletin Board from a Station
------------------------------------------
You may decide that you don't want to have the bulletin board at a particular station. This can be achieved in one of two ways:

1. In the shipdata.plist file for the station, include the following item in the "script_info" section:

    "script_info" = {
        "bb_hide" = 1;
    };

2. At some point after the system has been populated, find the station you want to hide the bulletin board on and add "bb_hide" value to the "script" property by doing the following:

    var stns = system.stations;
    for (var i = 0; i < stns.length; i++) {
        // if this station is the one I'm looking for....you'll need to provide some logic for identifying your station
        if (stns[i].~~~something~~~) {
            stns[i].script.bb_hide = 1;
        }
    }
    
Adding main menu items
======================
There may be times when you want a special menu item to appear on the main control listing page of the BB system, not just on the mission details page itself. To do this you can use the $addMainMenuItem and $removeMainMenuItem functions.

To add a menu item use the $addMainMenuItem function:

   ---------------------------------------------------------------------------------------------------------------------------
    var bb = worldScripts.BulletinBoardSystem;
    if (bb) {
        bb.$addMainMenuItem({
            text: "Special function text",
            color:"greenColor",
            unselectable: false,
            autoRemove: true,
            worldScript:"myWorldScriptName",
            menuCallback:"myFunctionName"
        });
    }
   ---------------------------------------------------------------------------------------------------------------------------

In detail these parameters are:
	text                    Text to display on the menu
	color                   Color of the item. Will default to this._menuColor (orangeColor)
	unselectable            Flag to control whether the item should be unselectable. Defaults to false.
                                If true, color will be set to this._disabledColor (darkGreyColor).
	autoRemove              Flag to indicate the item should be removed from the menu when selected by the player.
                                Defaults to false.
	worldScript             WorldScript name for the callback function.
	menuCallback            Function to call when the user selects the item.

To manually remove a menu item (ie if the "autoRemove" option is set to false), use the $removeMainMenuItem function:

   ---------------------------------------------------------------------------------------------------------------------------
    var bb = worldScripts.BulletinBoardSystem;
    if (bb) {
        bb.$removeMainMenuItem("myWorldScriptName", "myFunctionName");
    }
   ---------------------------------------------------------------------------------------------------------------------------

   This function accepts two parameters:
   worldScriptName          The name of the worldScript registered on the item.
   functionCallbackName     The name of the menu function callback on the item.

Example Mission Script
======================
The following is a very brief example demonstrating the methods of adding a mission to the bulletin board, and setting up the callbacks. This is a subset of a larger project, but should suffice in providing code examples of the techniques outlined above. This has also been included as an OXP in the "Resources" folder of the BB OXP package. Move the "AsteroidHunter.OXP" into your "AddOns" folder to see this OXP in action.

    "use strict";
    this.name        = "AsteroidHunter";
    this.author      = "phkb";
    this.copyright   = "2017 phkb";
    this.description = "Adds a simple asteroid hunt mission to the Bulletin Board";
    this.licence     = "CC BY-NC-SA 4.0";

    this._bb = null;				// link to Bulletin Board worldScript

    //-------------------------------------------------------------------------------------------------------------
    this.startUp = function() {
        // establish a link to the bulletin board worldscript
        this._bb = worldScripts.BulletinBoardSystem;
    }

    //-------------------------------------------------------------------------------------------------------------
    this.startUpComplete = function() {
        this.$addLocalMissions()
    }

    //-------------------------------------------------------------------------------------------------------------
    this.shipWillExitWitchspace = function() {
        // adds new missions to the list
        this.$addLocalMissions();
    }

    //-------------------------------------------------------------------------------------------------------------
    this.shipKilledOther = function(whom, damageType) {
        // mission type 1 - asteroid hunt - monitor when the player shoots an asteroid
        if (whom.hasRole("asteroid")) {
            var item = null;
            // cycle through the bulletin board list
            if (this._bb._data.length > 0) {
                for (var i = 0; i < this._bb._data.length; i++) {
                    item = this._bb._data[i];
                    // is this an accepted mission, and the target destination is the same as the current system...
                    if (item.accepted === true && item.destination === system.ID) {
                        // ..and there is some data on the item, and the data has a property of "type" and the type is 1 (asteroid hunt) 
                        // and the number of destroyed asteroids is less than the target quantity
                        if (item.hasOwnProperty("data") && item.data.hasOwnProperty("type") && item.data["type"] == 1 && item.data.quantity < item.data.targetQuantity) {
                            // ...then update the quantity
                            item.data.quantity += 1;
                            // signal the bulletin board that we've had a change in the mission completion percentage
                            this._bb.$updateBBMissionPercentage(item.missionID, (item.data.quantity / item.data.targetQuantity));

                            // notify the player that we just did something that affected a mission
                            player.consoleMessage("Mission goal updated");
                        }
                    }
                }
            }
        }
    }

    //-------------------------------------------------------------------------------------------------------------
    // handles the manifest entry callback from the bulletin board
    this.$updateManifestEntry = function(missID) {
        var item = this._bb.$getItem(missID);
        if (item.data.quantity == item.data.targetQuantity) {
            this._bb.$updateBBManifestText(missID, 
                expandDescription("Mission to destroy asteroids complete. Return to the main station in [system] to claim payment.", 
                    {system:System.systemNameForID(item.source)})
            );
        } else {
            // otherwise, just update the description with what is currently expected
            this._bb.$updateBBManifestText(
                missID,
                expandDescription("Destroy [quantity] asteroids in [destination] within [expiry].", 
                    {quantity:(item.data.targetQuantity - item.data.quantity), 
                    destination:system.name, 
                    expiry:this._bb.$getTimeRemaining(item.expiry)})
            );
        }
    }

    //-------------------------------------------------------------------------------------------------------------
    // adds the asteroid hunt mission to the bulletin board
    this.$addLocalMissions = function() {
        // if we haven't got any missions in the available list, try to add one now
        // first, count up the number already in player
        var chk = 0;
        if (this._bb._data.length > 0) {
            for (var i = 0; i < this._bb._data.length; i++) {
                if (this._bb._data[i].destination === system.ID &&
                    this._bb._data[i].hasOwnProperty("data") && 
                    this._bb._data[i].data.hasOwnProperty("type") && 
                    this._bb._data[i].data["type"] == 1) 
                    chk += 1;
            }
        }
        // if there's one there already, exit now
        if (chk > 0) return;

        // pick a random number of asteroid that need to be destroyed
        var qty = parseInt((Math.random() * 10) + 10);
        // calculate the payment
        var amt = qty * 10;
        // calculate the expiry time
        var expire = clock.adjustedSeconds + 21600; // 6 hours to complete.
        // add the mission to the bulletin board, and make a note of the id
        var id = this._bb.$addBBMission({
            source:system.ID,
            destination:system.ID,
            stationKey:"galcop",
            description:"Clean up the spaceways",
            details:expandDescription("Commanders are needed to help clean up the spacelanes by removing [quantity] asteroids in this system.", {quantity:qty}),
            payment:amt,
            expiry:expire,
            penalty:0,
            model:"asteroid",
            modelPersonality:parseInt(Math.random() * 32767),
            manifestCallback:"$updateManifestEntry",
            worldScript:this.name,
            data:{type:1, targetQuantity:qty, quantity:0}
        });
    }

Licence
=======
This work is licensed under the Creative Commons Attribution-Noncommercial-Share Alike 4.0 Unported License. To view a copy of this license, visit http://creativecommons.org/licenses/by-nc-sa/4.0/

Bulletin board image by icons8.com from http://www.iconsplace.com/black-icons/copy-icon with licence Creative Commons Attribution-NoDerivs 3.0 Unported

Version History
===============
2.6.3
- Added check for deferred bounty system (Bounty System OXP) when missions with completion type "IMMEDIATE" are completed.
- Improved clarity for when you have completed a mission but are docked at the wrong station type.

2.6.2
- When a contract is complete, the "Set course for" option is now only shown if the completion type is "AT_SOURCE" or "WHEN_DOCKED_SOURCE".

2.6.1
- Corrected name of HUD.

2.6
- Switched method of displaying a full screen so that the message box is visible when messages are sent.

2.5
- Really fixed issue with missions not able to be created in systems with ID 0 or 255.

2.4
- Fixed issue that would prevent missions being created in systems with ID 0 or 255.
- Added a "keepAvailable" flag to prevent a mission from being dropped if it isn't accepted when you leave the system.

2.3
- Better protection against missions in the list whose worldscript is no longer available.
- Better cleanup of system markers where missions are removed from the bulletin board.

2.2
- Fixed issue that could lead to missions having the same ID.

2.1
- The email ID of the last email sent relating to a BB item is now accessible through the "lastEmailID" property. Requires Email System v1.7.8.

2.0 
- Fixed issue were text was covering the map when the Advanced Navigation Array wasn't installed.

1.9
- Tweaks to the calculation for determining if an unaccepted mission is expired.

1.8
- Fixed issue where the calculation to determine whether an unaccepted mission should be considered expired was being a little too broad.
- Bug fixes.
- Code refactoring.

1.7
- Calls to 'confirmCompletedCallback' now performed each time mission screen is opened when a "Complete mission" menu item is displayed.

1.6
- Added "additionalMarkers", an array of marker dictionaries, which enables a single mission to have multiple markers on the system chart.
- Added "forceLongRangeChart", a boolean value (default false), which will force the map screen to always show the long range chart, rather than a short range or custom chart.
- Fixed issue with "playAcceptedSound" not defaulting to true correctly.
- Fixed issue where some available items may not be shown on the main mission list if there are any active missions.

1.5.1
- Fixed issue calculating route time if there is no route to destination.

1.5
- Added option to highlight missions whose target system is on or near to your current course. Highlight can be via color or markers, switchable via Library.
- Code refactoring.
- Added mission description and payment amount to the map screen.
- Added "Next contract" menu item to mission details page.
- Included option (switchable via Library Config) of adding "Next contract" to map screen.

1.4
- Makes use of new short range chart and custom chart options available in Oolite 1.87.
- Better method of determining when missions are expired and then removing them from the BB list.
- Added a flag to Library Config that will allow the internal ID of a mission to be added to the mission details page, which can be useful for debugging.
- Now plays the "[contract-accepted]" sound when any contract is accepted.
- Added new flag to the "$addBBMission" object: "playAcceptedSound", a boolean value allowing the contract to control whether the "[contract-accepted]" sound will be played.

1.3
- When setting a course to a target system, F7 screen will now display the new destination.

1.2
- Added expiry time in hours to the mission details screen.
- When 1 day is remaining, it will now read "1 day" rather than "1 days" (same for hours, minutes and seconds).

1.1
- Better handling of scenario where player doesn't have the Advanced Navigational Array installed.

1.0
- Added option to force the Bulletin Board interface screen to always be shown, even if no contracts are available.
- Fixed bug where missions that do not have a specific system destination (ie 256) were always displaying a "Close to expiry" warning.
- Turned on decimal place display on all credit value output.
- Bug fixes.

0.29
- Added additional text to mission briefings for missions close to expiry, to better explain the fact.
- Added a cleanup function to remove marked systems that have become orphaned from their mission.

0.28
- Sorted available missions by payment amount. Accepted missions will still be sorted by when they were accepted.

0.27
- Added "shipWillDockWithStation_start" and "shipWillDockWithStation_end" events, triggers at the start and then at the end of the shipWillDockWithStation, to allow mission OXP's to know in which order things will be processed, which is particularly important for BB items flagged as "WHEN_DOCKED_SOURCE", "WHEN_DOCKED_STATIONKEY", and "WHEN_DOCKED_ANYWHERE".
- Method of using of "formatCredits" function is now consistent.

0.26
- Added the "condition" property to custom menu items, to allow for the item to be disabled in certain conditions.
- Added the "activeOnly" property to custom menu items, to allow the item to be hidden if the mission isn't active.

0.25
- At some point I'd managed to delete the "confirmCompleteCallback" property when adding a new mission to the board. Fixed.

0.24
- Fixed issue with missions where completion type is AT_SOURCE, where the "Complete mission" menu item was not being displayed.

0.23
- Improved handling of missions with intergalactic source or destination systems.
- Fixed issue with Xenon Redux UI sometimes losing its background images on the BB.

0.22
- Added facility to add menu items to the main BB contract listing page.
- A BB item can now have the destination set to -1 (Interstellar space), or 256 (no fixed destination).
- Setting the payment of a BB item to zero will now hide the "Payment" line on the mission details page.
- "payment" is no longer a required element.
- Fixed more JS errors when terminating a contract.

0.21
- Expanded number of contracts shown on main listing to 21.
- Added "bonusCalculationCallback" to allow bonus payments to be calculated and given to the player when a mission is successfully completed.
- Fixed JS error when terminating a contract.
- Fixed reference error when attempting to retrieve a postStatusMessage for a completed mission.
- Added validations for postStatusMessages when adding a mission to the board.

0.20
- Fixed issue with new sorting methods conflicting with old indexing system for selected item.

0.19
- Added postStatusMessages property for individual missions to allow a mission screen to be shown to the player after status changes.
- When missions are accepted they will appear in the accepted list and manifest screen in the same order as they were accepted in.
- "initiateCallback" and "completedCallback" are no longer required elements.
- Corrections and adjustments to the "AsteroidHunter" sample script.
- Code cleanup and refactoring.

0.18
- Adjusted precedence of colors for items in the mission list. If a mission is both unavailable and close to expiry, unavailability will be selected for the item color.
- Added new events: "open" (fires when BB is opened), "close" (fires when BB is closed from the menu), "exit" (fires when BB is exited without specifically selecting 'Close'), "launchExit" (fires when BB is exited by the player launching their ship).
- Added "data" element to BB item, so that relevant data can be stored with the mission, for later retrieval. This data is not displayed to the player.
- Adding a mission with the expiry parameter set to -1 will result in a mission with an unlimited expiry.
- Fixed issue where explicitly setting "allowTerminate:true" was not being correctly set.
- Page reset when returning from the map display to the briefing screen.
- Added the "mapOverlay" option to allow a specific image to be applied to the map screen for a mission.
- Added "setOverlayDefault" and "resetOverlayDefault" functions to control the default overlay used on the BB.
- Added "setBackgroundDefault" and "resetBackgroundDefault" functions to control the default background used on the BB.
- Overlay setting for item now applies on confirmation screen and mission incomplete screen.
- Adjusted procedure for applying overlays to screen, making better use of defaults.
- "terminateCallback" is no longer a required element.
- Simplified the sample script "AsteroidHunter".

0.17
- Added number of jumps to destination to the various mission screens.
- Origination system and destination system details will now wrap inside the column correctly.
- Cleanup of chart route mode handling.

0.16
- Attempt to fix the event callback function generating error when calling functions.

0.15
- Added "remoteDepositProcess" property, to control whether deduction of the deposit amount should be handled by the BB or by some external process.
- Changed colour of unavailable items in the main list to be a slightly darker shade of gray.
- Added a "Net payment" line to missions that have a deposit.
- Unavailable items will now be put at the bottom of the mission list.
- Removed the backgroundHeight and overlayHeight properties. The background and overlay properties are now defined as guiTextureSpecifier, which can include these other elements as part of a dictionary.

0.14
- The "availableCallback" function was not being called on the short or long range chart screens.
- Added an event registration and execution system, so OXP's can know when various interface events have taken place.
- Days now included in mission time remaining on the mission detail screen.
- Fixed issue where "Set course for" options were not being displayed on map screens.
- Fixed issue where numeric custom display item values were causing an error.
- Fixed issue where an error would occur if a mission does not have a manifestText entry as well as no manifestCallback.
- Fixed issue where adding multiple station keys to the same station were not adding data correctly.
- The stationKeys array was not being reset after a jump.
- Corrected errors in documentation.
- Bug fixes.
- Code refactoring.

0.13
- Added an "availableCallback" function, to allow for cases where a mission can be added to the BB, but conditions prevent the player from being able to accept it (eg insufficient cargo space).
- If a mission has a deposit amount as part of the terms of acceptance, this value will be deducted from the payment amount on the first page of the bulletin board display. Brings it into line with the cargo contracts screen.
- Fixed bug with displaying custom items on a mission detail page.

0.12
- Moved restoring data process to startUp function.

0.11
- Added custom menu items that can be used with callbacks to allow custom functions to be called on each mission.
- Changed methodology for creating mission ID's to eliminate the possibility of ever having two missions with the same ID.
- Changed short range chart selection range to 7.5ly, rather than 15ly.
- Fixed another minor bug with the players destination getting reset unexpectedly when viewing mission destinations.
- Missions added with "autoAccept" turned on will now get their destination marker put on the system map, and (if the Email System is installed) will have confirmation emails sent.
- Fixed Javascript reference error when terminating a mission.
- Better handling of interstellar space conditions.
- Added the "noEmails" option, to stop a mission from sending confirmation emails.
- Added ability for calling routines to specify their own ID for reference, overriding the default.
- Changed the console message that is displayed when a mission is manually terminated by the player.
- Chart markers were not being removed from the chart after missions were completed.
- Fixed issue with HUD not becoming visible again when launching while viewing the Bulletin Board.
- Setting a blank value ("") for a mission station key should have allowed the mission to be found and completed at any station in the source system.
- Swapped "Expiry" and "Payment" columns in the main list to align better with other contract screens (parcels, cargo, passengers).

0.10
- "terminateCallback" was not being called when a mission was terminated by the player. "failedCallback" was being called instead.
- Fixed Javascript bug in "failedCallback" routine.

0.9
- Added optional "deposit" property to mission items, which will be deducted from the player's account when accepting the mission.
- Added travel time (in hours) to the destination system info and source system info.
- "Set course" option now takes player's full course into consideration when working out whether the option should be displayed, rather than just the final destination.
- Fixed issue where player destination was getting reset when a chart screen is exited by pressing a function key, rather than via the "Exit" command.

0.8
- Fixed issue with AT_SOURCE missions that allow for partial completion, where they could be partially completed at non-source stations.
- Active but expired missions that are shown on the main BB list will now just show as "Expired".
- Added "customDisplayItems", so missions can add their own heading/value information items to the mission details page.
- "Set course for" menu items won't be shown if player already has that system set as their destination.
- Changed "==" comparisons to "===" for performance improvements.

0.7
- Accepting a mission now doesn't close the mission briefing screen.
- Added a "stopTimeAtComplete" boolean value to enable missions to be setup so that, as soon as the mission is flagged 100% complete, the expiry time countdown will stop. This means that the player can take as much time as they want to return to the system and hand in their completed mission. The default for this value is false.
- Added a "allowPartialComplete" boolean value so that a mission can be handed in early, with a reduction in the payment based on the percentage completed. If penalties also apply it will reduced by the percentage completed.
- Added a "statusText" text value that will be displayed on the mission briefing screen as the "Current status" instead of the manifestText value.
- Added the "$updateBBStatusText" function to enable the status text to be updated.
- Added the originating system to the mission details page.
- Added the destination system to the main mission list.
- Moved "Percentage Complete" column to be the last column on the main mission list screen.
- Changed the "Expires" column to only show hours (or minutes if the time remaining is less than 100 minutes) on the main mission list screen.
- Fixed issue with "allowTerminate" not defaulting correctly.
- Changed how stationKeys are added, making it possible for a station to have multiple keys, as well as allowing for multiple worldScripts to have different station keys for the same station.
- Fixed issue where exiting a multi-page mission briefing and re-opening it was not resetting the current page back to 1.
- Fixed issue where boolean values of true or false (that is not a text string with "true" or "false" in it) were not being recognised correctly when adding a mission.
- Changed the way the number of active missions is counted. Now any active mission is listed, not just the non-expired missions.
- When viewing a completed Bulletin Board mission and the player is not in the originating system, the option to "Set course for [originating system]" will now be available.
- Corrected logic for determining when a property has been passed in an object when adding new missions.
- Changed completion types "AT_STATION" and "WHEN_DOCKED_STATION" to be "AT_STATIONKEY" and "WHEN_DOCKED_STATIONKEY" to better describe their purpose.
- Added a debug flag to turn on/off log messages.
- If the Email System is installed, accepting, terminating, completing and failing missions will now generate a confirmation email.
- Added a comma between "# available" and "# active" on the interface screen.

0.6
- Added an additional check when creating a new mission. Expiry time must be in the future.
- Fixed bug with "markerShape" setting not being correctly set up.
- Active missions that have expired are now coloured red in the bulletin board list.
- Included manifest text on mission briefing page when mission is completed but not yet handed in. Also included expiry time, which is still relevant for completed-yet-not-handed-in missions.
- Forced manifest entries to be updated each time the manifest screen is viewed in order to keep "Expires in" field up to date.
- Added "%" to percentage completed text on mission screen.
- Added a "disablePercentDisplay" boolean value to enable a mission to turn off the display of the "Percent complete" value. This would leave just the manifest text as the method of showing mission status.
- Completed missions that are about to be handed in will no longer show as being close to expiry.
- Better handling missions complete when docking where the confirmCompleteCallback function returns a value.
- Fixed bug when attempting to show the long range chart.
- Fixed bug with mission variable declaration in $failedMission routine.
- Code refactoring.

0.5
- Fixed remainder of missing "clock" references.

0.4
- Turned off some debug messages.
- Fixed missing "clock" reference when updating mission percentage.

0.3
- Fixed Javascript reference error when displaying mission details.
- Fixed issue where setting course for the destination system wasn't actually setting the course.
- Bulletin Board was not resetting itself correctly between views.
- Fixed issue where the space between accepted and available missions was not appearing if there were only 1 of each type in the list.
- Added a confirmation screen to the terminate mission process.
- Added a "Set course for" option to the mission briefing page.
- Added the "confirmCompleteCallback" callback function, so that secondary steps can be confirmed before a mission can be accepted as complete. This would be useful if there is a possibility that something might have changed between completing the required steps of a mission, and getting to a dock to enter the "Complete Mission" response (eg loosing or selling cargo).

0.2
- Missions whose expiry time has already passed are now excluded from the list of available missions.
- Missions whose expiry time is extremely tight are now highlighted on the list of available missions and in the mission details as well.
- clock.adjustedSeconds now used to calculate remaining time to complete mission.
- Spelling corrections.

0.1
- Initial release

Equipment

This expansion declares no equipment.

Ships

This expansion declares no ships.

Models

This expansion declares no models.

Scripts

Path
Scripts/bb_system.js
"use strict";
this.name = "BulletinBoardSystem";
this.author = "phkb";
this.copyright = "2017 phkb";
this.description = "Interface screen for localised or event-driven mission opportunities.";
this.license = "CC BY-NC-SA 4.0";

// CHECK: add a unaccepted mission for another system (ie not the current) - will it show up on the BB?

this._bbOpen = false; // flag to indicate the bulletin board has been opened
this._bbExiting = 0; // int to track what sort of exit from the board is taking place (1 = direct function key, 2 = from close menu)
this._maxpage = 0; // total number of pages of inbox to display
this._curpage = 0; // the current page of the inbox being displayed
this._msRows = 21; // rows to display on the mission screen
this._msCols = 32; // columns to display on the mission screen
this._displayType = 0; // controls the view.
this._displayPage = 0; // which page of the mission item are we showing
this._itemList = [];
this._routeMode = ""; // current route mode for long range chart (if ANA is installed)
this._itemColor = "yellowColor";
this._menuColor = "orangeColor";
this._exitColor = "yellowColor";
this._disabledColor = "darkGrayColor";
this._unavailableColor = "grayColor";
this._warningColor = "redColor";
this._onPathColor = "greenColor"; // colour for available items that are directly on your current course
this._nearPathColor = "0 0.6 0 1"; // colour for available items that are near to your current course
this._shuffleTries = 1; // how many times to shuffle the BB to make it as unsorted as possible.
this._updateRequired = false; // flag set after a new mission is added to the bb to indicate the interface entry needs to be updated
this._stationKeys = []; // array of worldscripts/stationkeys for the currently docked station
this._markers = ["NONE", "MARKER_X", "MARKER_PLUS", "MARKER_SQUARE", "MARKER_DIAMOND"];
this._completionTypes = ["AT_SOURCE", "AT_STATIONKEY", "ANYWHERE", "IMMEDIATE", "WHEN_DOCKED_SOURCE", "WHEN_DOCKED_STATIONKEY", "WHEN_DOCKED_ANYWHERE"];
this._lastChoice = ["", "", "", ""]; // stores the last choice on each of the mission screens
this._notCompleteText = ""; // text returned from the confirmCompleteCallback function
this._bbAdminName = {}; // names to attach to confirmation emails (when Email System is installed)
this._suspendedDestination = -1;
this._tempMarkers = -1;
this._nextID = 100;
this._eventRegister = {};
this._overlayDefault = {
	name: "bb-overlay.png",
	height: 546
};
this._overlay = this._overlayDefault;
this._backgroundDefault = "";
this._background = this._backgroundDefault;
this._holdItem = {};
this._mainMenuItems = []; // array of menu items to appear of the first page of the BB
this._alwaysShowBB = false;
this._showID = false; // flag which determines whether the ID number is shown on mission details page
this._nextContractOnMap = false;
this._nearPathRange = 7; // range (in LY) to consider a system to be "near"
this._useMarkers = 0; // how to use near system markers on BB list: 
// 0 = no markers, just colours, 1 = markers only, no colours, 2 = markers and colours, 4 = turn off feature
this._oldVersion = 1.4;
this._storeHUD = "";
this._zoomDist = [{
		dist: 45,
		zoom: 3.5
	},
	{
		dist: 40,
		zoom: 3.2
	},
	{
		dist: 35,
		zoom: 2.9
	},
	{
		dist: 30,
		zoom: 2.5
	},
	{
		dist: 25,
		zoom: 2.2
	},
	{
		dist: 20,
		zoom: 1.7
	},
	{
		dist: 15,
		zoom: 1.4
	},
	{
		dist: 7,
		zoom: 1.0
	},
];

// configuration settings for use in Lib_Config
this._bbConfig = {
	Name: this.name,
	Alias: "Bulletin Board System",
	Display: "Display Options",
	Alive: "_bbConfig",
	Bool: {
		B0: {
			Name: "_showID",
			Def: false,
			Desc: "Show mission ID"
		},
		B1: {
			Name: "_nextContractOnMap",
			Def: false,
			Desc: "'Next Contract' on map"
		},
		Info: "0 - Displays the internal mission ID on details page.\n1 - Shows the 'Next contract' option on all map screens."
	},
	SInt: {
		S0: {
			Name: "_nearPathRange",
			Def: 7,
			Min: 1,
			Max: 15,
			Desc: "Range of near path"
		},
		S1: {
			Name: "_useMarkers",
			Def: 0,
			Min: 0,
			Max: 3,
			Desc: "Near system markers"
		},
		Info: "0 - Sets the range (in LY) defining a system as being near current path.\n1 - 0=No markers, 1=Markers Only, 2=Markers & Colors, 3=Turn off feature"
	}
};
this._trueValues = ["yes", "1", 1, "true", true];

/* array Specifications
	text                    Text to display on the menu
	color                   Color of the item. Will default to this._menuColor (orangeColor)
	unselectable            Flag to control whether the item should be unselectable. 
							If true, color will be set to this._disabledColor (darkGreyColor)
	autoRemove              Flag to indicate the item should be removed from the menu when selected by the player.
	worldScript             WorldScript name for the callback function.
	menuCallback            Function to call when the user selects the item.
*/

this._data = []; // array of available and accepted missions
/* Array Specifications
	data array
	ID						Numerical id of the mission
	stationKey				text key used for limiting new mission access to particular stations
							will default to blank (all stations) if not provided. can include multiple items, 
							comma-separated (eg "galcop,chaotic")
	description				one line description of the mission (used on the main BB list)
	source					system where mission is available
	sourceName				name of the source system (auto-generated from the source value)
	sourceGalaxy			galaxy number where source system resides (will default to current galaxy)
	destination				system where mission must be completed: 0-255 for planets, -1 for interstellar, 
							256 for no destination
	destinationName			name of the destination system (auto-generated from the destination value)
	destinationGalaxy		galaxy number where the source system resides (will default to the current galaxy)
	galaxy					galaxy number where mission was created
	details					expanded description of the mission
	manifestText			text to display on the manifest screen
	statusText				text to include on the mission briefing screen when the mission is active.
							Will default to manifestText when not supplied
	expiry					the time the mission must be completed by. -1 means unlimited time.
	accepted				boolean flag indicating the mission has been accepted by the player. default false.
	percentComplete			how much of the mission has been completed by the player
	payment					how much the player will be paid on completion of the mission
	penalty					how much the player will be penalised for not completing the mission
	deposit					(optional) how much the player needs to pay as a deposit for taking the mission.
							This amount will be refunded if the mission is completed successfully.
							Amount will be adjusted based on the percentage completed of the mission.
	allowPartialComplete	boolean flag that indicates whether the player can complete the mission with less than 
							the full percentage. 
							Payment will be scaled by the percentage completed. Penalties will also apply, again scaled 
							by the percentage completed.
							For example, if the payment is 100 cr and the penalty 10 cr, and the player completes 
							70% of the mission, if they hand it in they would receive 70 cr (70% of 100), and the 
							penalty would be 3 cr (30% of 10), meaning their total payment would be 67 cr.
							Default is false.
	model					role of a ship to use as the background on the mission details screen.
	modelPersonality		the entityPersonality assigned to the ship model.
	spinModel				True/false value indicating whether the ship model will rotate or not. The default to true.
	background				guiTextureSpecifier (name of a picture used as background)
	overlay					guiTextureSpecifier (name of a picture used as an overlay). Will default to the bulletin board 
							graphic when not set.
	mapOverlay              guiTextureSpecifier for map screen (name of a picture used as background). 
							Will default to the overlay setting (if provided) when not set, otherwise the bulletin board 
							graphic.
	forceLongRangeChart		boolean flag indicating whether the map screen for this mission will be forced to use 
							the long range chart. Default false, meaning the map will calculate the best zoom level 
							required based on the source and destination systems.
	markerShape				the shape of the destination system marker to use on the galactic chart (default "MARKER_PLUS").
							Use "NONE" to leave off marking the chart.
	markerColor				the color of the marker on the galactic chart (default "redColor")
	markerScale				the scale of the marker on the galactic chart (default 1.0)
	additionalMarkers		array of dictionary items, defining extra markers that will be placed on the system map
							system			system ID where marker will be places
							markerShape		shape of the system marker (default "MARKER_PLUS")
							markerColor		color of the marker (default "redColor")
							markerScale		scale of the marker (default 1.0);
	allowTerminate			boolean flag to indicate whether the "Terminate mission" option will be available after 
							accepting the mission. (default true)
	completionType			what happens when mission is completed: "AT_SOURCE", "AT_STATIONKEY", "ANYWHERE", 
							"IMMEDIATE", "WHEN_DOCKED_SOURCE", "WHEN_DOCKED_STATIONKEY", "WHEN_DOCKED_ANYWHERE"
							AT_SOURCE: player must return to the source system, dock at any station with the same 
								stationKey, open the mission and select "Complete mission"
							AT_STATIONKEY: player can return to any system, dock at any station with the same 
								stationKey, open the mission and select "Complete mission"
							ANYWHERE: player can return to any system, dock at any station, open the mission and 
								select "Complete mission"
							IMMEDIATE: player is rewarded immediately when the mission is flagged as 100% complete - 
								player won't need to dock anywhere
							WHEN_DOCKED_SOURCE: player is automatically rewarded as soon as they next dock at the source 
								station
							WHEN_DOCKED_STATIONKEY: player is automatically rewarded as soon as they next dock at any 
								station with the same station key
							WHEN_DOCKED_ANYWHERE: player is automatically rewarded as soon as they next dock at any station
							(default "AT_SOURCE")
	stopTimeAtComplete		boolean flag to indicate that the clock will stop when the mission is flagged 100% complete. 
							Default false. This means that, for a completionType of "AT_SOURCE" the player has to return to 
							the original station within the allowed time in order to complete the mission. If this flag is 
							set to true, once the player completes the mission at the destination, they are free to take as 
							much time as they like to return to the original station and hand in their mission.
	disablePercentDisplay	boolean flag that allows the "Percent complete" item to be hidden on the mission details page. 
							Default false.
	noEmails				boolean flag that stops the transmission of confirmation emails 
							(if the Email System is installed)
	statusValue				value to display instead of the percentComplete value.
	arrivalReportText		text to display on the arrival report after the player completes mission and completionType 
							set to "WHEN_DOCKED_*"
	customDisplayItems		array of dictionary objects containing header/value key pairs to be displayed on the mission 
							screen as separate items.
	customMenuItems			array of dictionary objects containing additional items to be shown in the menu
								text		text to display on the menu
								worldScript	worldscript of the callback
								callback	function name of the callback object
								condition	function name of a callback object that will return either a blank, 
											meaning menu item is available, or some text, giving the reason why the item 
											is unavailable
								activeOnly	boolean indicating whether the menu item will only be visible when the 
											mission is active. default true.
								autoRemove	boolean indicating whether the menu item will be removed when selected
	remoteDepositProcess	boolean flag to indicate whether deduction of any deposit amount should be processed by the 
							BB or remotely.
							Default is false, meaning the deposit will be deducted by the BB.
	initiateCallback		function name to callback when contract is accepted
	confirmCompleteCallback function name to callback to check if a contract can be completed.
	completedCallback		function name to callback when the player flags the mission as completed
	terminateCallback		function name to callback when the player gives up on the mission
	failedCallback			function name to callback when the player fails a mission (called when the player docks)
	manifestCallback		function name to callback when the text on the manifest screen needs updating
	availableCallback		function name to callback when checking if this contract is available to the player
							function should return either a blank string to indicate contract is available,
							or a string with the reason why the contract is unavailable
							if no callback is set, it is assumed contract is always available
	worldScript				name of worldscript containing the callback functions
	postStatusMessages		array of dictionary objects used to display text to the user after initiated, completed, or 
							terminated.
							Will only be shown for completionTypes AT_SOURCE, AT_STATIONKEY, and ANYWHERE.
							For any other completionType it is assumed the originating script will display additional info 
							the player, or the "arrivalReportText" will be used.
                                status        Can be either initiated, completed, or terminated 
								return        What to display after player pressed enter. 
													"item" to display the mission details, 
													"list" to display the mission list
													"exit" to exit the BB completely
                                text          Text to be displayed
                                background    Background image to be used on the display
                                model         Model to be shown on the display
                                overlay       Overlay to be shown on the display
	data					object containing reference data for calling WS.
*/

//-------------------------------------------------------------------------------------------------------------
this.startUp = function () {
	// load up player data
	if (missionVariables.BBData) {
		this._data = JSON.parse(missionVariables.BBData);
		delete missionVariables.BBData;
		this.$updateData();
	}
}

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

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

	this.$addAcceptedDate();

	if (missionVariables.BBNextID) this._nextID = missionVariables.BBNextID;
	// add a mission screen exception to Xenon UI
	if (worldScripts.XenonUI) {
		var wx = worldScripts.XenonUI;
		wx.$addMissionScreenException("oolite-bbsystem-shortrangechart-map");
		wx.$addMissionScreenException("oolite-bbsystem-longrangechart-map");
	}
	// add a mission screen exception to Xenon Redux UI
	if (worldScripts.XenonReduxUI) {
		var wxr = worldScripts.XenonReduxUI;
		wxr.$addMissionScreenException("oolite-bbsystem-shortrangechart-map");
		wxr.$addMissionScreenException("oolite-bbsystem-longrangechart-map");
	}
	if (worldScripts.DisplayCurrentCourse) {
		var dcc = worldScripts.DisplayCurrentCourse;
		dcc._screenIDList.push("oolite-bbsystem-shortrangechart-map");
		dcc._screenIDList.push("oolite-bbsystem-longrangechart-map");
	}

	this._suspendedDestination = -1;
	this._tempMarkers = -1;
	this._hudHidden = false;

	if (missionVariables.BBUseMarkers) this._useMarkers = parseInt(missionVariables.BBUseMarkers);
	if (missionVariables.BBNearPathRange) this._nearPathRange = parseInt(missionVariables.BBNearPathRange);
	if (missionVariables.BBOldVersion) this._oldVersion = parseFloat(missionVariables.BBOldVersion);
	if (missionVariables.BBNextContractOnMap) this._nextContractOnMap = this._trueValues.indexOf(missionVariables.BBNextContractOnMap) >= 0 ? true : false;

	if (player.ship.docked) this.$initInterface(player.ship.dockedStation);

	// dud data cleanup
	//for (var i = 1; i < 10; i++) {
	//	for (var j = 0; j <= 255; j++) {
	//		mission.unmarkSystem({system:j, name:"GalCopBB_Missions" + "_" + i});		
	//	}
	// }
	this.$refreshManifest();
	this.$dataCleanup();
}

//-------------------------------------------------------------------------------------------------------------
this.shipWillDockWithStation = function (station) {
	this.$triggerBBEvent("shipWillDockWithStation_start", station);
	this.$checkForCompleteOnDock(station);
	this.$initInterface(station);
	this.$triggerBBEvent("shipWillDockWithStation_end", station);
}

//-------------------------------------------------------------------------------------------------------------
this.playerWillSaveGame = function () {
	missionVariables.BBOldVersion = this._oldVersion;
	missionVariables.BBNextID = this._nextID;
	if (this._data.length > 0) {
		missionVariables.BBData = JSON.stringify(this._data);
	} else {
		delete missionVariables.BBData;
	}
	missionVariables.BBNearPathRange = this._nearPathRange;
	missionVariables.BBUseMarkers = this._useMarkers;
	missionVariables.BBNextContractOnMap = this._nextContractOnMap;
}

//-------------------------------------------------------------------------------------------------------------
this.guiScreenWillChange = function (to, from) {
	// force the manifest entries to update
	// this keeps the "number of hours remaining" value up to date.
	if (to === "GUI_SCREEN_MANIFEST") {
		for (var i = 0; i < this._data.length; i++) {
			if (this._data[i].accepted === true) {
				if (this._data[i].manifestCallback != "") {
					if (worldScripts[this._data[i].worldScript] && worldScripts[this._data[i].worldScript][this._data[i].manifestCallback]) {
						worldScripts[this._data[i].worldScript][this._data[i].manifestCallback](this._data[i].ID);
					}
				}
			}
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.guiScreenChanged = function (to, from) {
	var p = player.ship;
	if (from === "GUI_SCREEN_MISSION" && this._bbOpen) {
		this._bbOpen = false;
		//if (this._hudHidden === false && p.hudHidden === true) p.hudHidden = this._hudHidden;
		if (this._suspendedDestination >= 0) p.targetSystem = this._suspendedDestination;
		this._suspendedDestination = -1;
		if (this._tempMarkers >= 0) this.$removeChartMarker(this._tempMarkers);
		this._tempMarkers = -1;
	}
	if (guiScreen === "GUI_SCREEN_INTERFACES" || this._updateRequired === true) {
		// update the interfaces screen
		this._updateRequired = false;
		if (p.dockedStation != null) {
			if (p.docked) this.$initInterface(p.dockedStation);
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.missionScreenOpportunity = function () {
	if (this._bbExiting === 1) {
		this._bbExiting = 0;
		this.$triggerBBEvent("exit");
	}
	this._bbExiting = 0;
}

//-------------------------------------------------------------------------------------------------------------
this.shipLaunchedFromStation = function (station) {
	if (this._bbExiting === 1) {
		this._bbExiting = 0;
		this.$triggerBBEvent("launchExit", station);
	}
	this._bbExiting = 0;
}

//-------------------------------------------------------------------------------------------------------------
this.shipWillEnterWitchspace = function (cause, destination) {
	// clear out any unaccepted missions whenever we do a witchspace jump
	for (var i = this._data.length - 1; i >= 0; i--) {
		if (this._data[i].source === system.ID && this._data[i].accepted === false && 
			(!this._data[i].hasOwnProperty("keepAvailable") || this._data[i].keepAvailable == false)) 
			this.$removeBBMission(this._data[i].ID);
	}
	// reset the BB admin name dictionary so new names will be generated for this system
	this._bbAdminName = {};
	// reset the station keys
	this._stationKeys = [];
}

//-------------------------------------------------------------------------------------------------------------
// adds a mission to the BB.
this.$addBBMission = function $addBBMission(bbObj) {

	var truetypes = ["yes", "1", "true", true, 1, -1];
	var falsetypes = ["no", "0", "false", false, 0];

	var src = system.ID;
	if (bbObj.hasOwnProperty("source")) {
		if (parseInt(bbObj.source) > 255 || parseInt(bbObj.source) < 0) {
			throw "Invalid BB mission settings: 'source' system ID (" + bbObj.source + ") must be between 0 and 255.";
		}
		src = bbObj.source;
	}
	if (bbObj.hasOwnProperty("destination") === false || parseInt(bbObj.destination) > 256 || parseInt(bbObj.destination) < -1) {
		throw "Invalid BB mission settings: 'destination' system ID (" + bbObj.destination + ") must be supplied and between -1 and 256.";
	}
	if (bbObj.hasOwnProperty("description") === false || bbObj.description === "") {
		throw "Invalid BB mission settings: 'description' must be supplied.";
	}
	if (bbObj.hasOwnProperty("details") === false || bbObj.details === "") {
		throw "Invalid BB mission settings: 'details' must be supplied.";
	}
	if (bbObj.hasOwnProperty("payment") === true && bbObj.payment < 0) {
		throw "Invalid BB mission settings: 'payment' must be greater than or equal to 0.";
	}
	if (bbObj.hasOwnProperty("expiry") === false || bbObj.expiry === 0) {
		throw "Invalid BB mission settings: 'expiry' must be supplied.";
	}
	if (bbObj.expiry > 0 && bbObj.expiry < clock.adjustedSeconds) {
		throw "Invalid BB mission settings: 'expiry' must be in the future.";
	}
	if (bbObj.hasOwnProperty("worldScript") === false || bbObj.worldScript === "") {
		throw "Invalid BB mission settings: 'worldScript' must be supplied.";
	}

	// work out some defaults, and if they've been overridden
	// completionType
	var completeType = "AT_SOURCE";
	if (bbObj.hasOwnProperty("completionType") && bbObj.completionType != "") {
		if (this._completionTypes.indexOf(bbObj.completionType) >= 0) {
			completeType = bbObj.completionType;
		} else {
			throw "Invalid BB mission settings: unrecognised 'completionType' setting (" + bbObj.completionType + "). Must be one of " + this._completionTypes;
		}
	}

	var stopTime = false;
	if (bbObj.hasOwnProperty("stopTimeAtComplete") && truetypes.indexOf(bbObj.stopTimeAtComplete) >= 0) {
		stopTime = true;
	}

	// markerShape
	var markShape = "MARKER_PLUS";
	if (bbObj.hasOwnProperty("markerShape") && bbObj.markerShape != "") {
		if (this._markers.indexOf(bbObj.markerShape) >= 0) {
			markShape = bbObj.markerShape;
		} else {
			throw "Invalid BB mission settings: unrecognised 'markerShape' setting (" + bbObj.markerShape + "). Must be one of " + this._markers;
		}
	}

	// validate any postStatusMessages
	if (bbObj.hasOwnProperty("postStatusMessages")) {
		var statusTypes = ["initiated", "completed", "terminated"];
		var list = bbObj.postStatusMessages;
		if (list && list.length > 0) {
			for (var i = 0; i < list.length; i++) {
				if (list[i].hasOwnProperty("status") === false) {
					throw "Invalid BB mission settings: postStatusMessages does not include 'status' property.";
				}
				if (statusTypes.indexOf(list[i].status) === -1) {
					throw "Invalid BB mission settings: postStatusMessages 'status' value (" + list[i].status + ") not recognised. Must be one of " + statusTypes;
				}
				if (list[i].hasOwnProperty("text") === false) {
					throw "Invalid BB mission settings: postStatusMessages does not include 'text' property.";
				}
				if (list[i].text === "") {
					throw "Invalid BB mission settings: postStatusMessages 'text' value is blank";
				}
			}
		}
	}

	var addMarkers = [];
	if (bbObj.hasOwnProperty("additionalMarkers") === true) {
		// make sure each additional marker is valid
		if (Array.isArray(bbObj.additionalMarkers) === true) {
			for (var i = 0; i < bbObj.additionalMarkers.length; i++) {
				var item = bbObj.additionalMarkers[i];
				if (item.hasOwnProperty("system") === true) {
					var def = {};
					def["system"] = item.system;
					if (item.hasOwnProperty("markerShape") === true) {
						if (this._markers.indexOf(item.markerShape) >= 0) {
							def["markerShape"] = item.markerShape;
						} else {
							throw "Invalid BB mission settings: unrecognised 'markerShape' setting (" + item.markerShape + "). Must be one of " + this._markers;
						}
					} else {
						def["markerShape"] = "MARKER_PLUS";
					}
					if (item.hasOwnProperty("markerColor") === true) {
						def["markerColor"] = item.markerColor;
					} else {
						def["markerColor"] = "redColor";
					}
					if (item.hasOwnProperty("MARKER_SCALE") === true) {
						def["markerScale"] = item.markerScale;
					} else {
						def["markerScale"] = 1.0;
					}
					addMarkers.push(def);
				}
			}
		}
	}

	var id = 0;
	if (bbObj.hasOwnProperty("ID") && isNaN(bbObj.ID) === false && parseInt(bbObj.ID) > 0) {
		id = parseInt(bbObj.ID);
		for (var i = 0; i < this._data.length; i++) {
			if (this._data[i].ID === id) {
				throw "Invalid BB mission settings: ID " + id + " is already in use!";
			}
		}
	} else {
		id = this.$nextID();
	}

	this._data.push({
		ID: id,
		stationKey: (bbObj.stationKey && bbObj.stationKey != "" ? bbObj.stationKey : ""),
		source: src,
		sourceName: System.systemNameForID(src),
		sourceGalaxy: galaxyNumber,
		destination: bbObj.destination,
		destinationName: this.$systemNameForID(bbObj.destination),
		destinationGalaxy: galaxyNumber,
		description: bbObj.description,
		details: bbObj.details,
		manifestText: (bbObj.hasOwnProperty("manifestText") ? bbObj.manifestText : ""),
		originalManifestText: (bbObj.hasOwnProperty("manifestText") ? bbObj.manifestText : ""),
		statusText: (bbObj.hasOwnProperty("statusText") ? bbObj.statusText : ""),
		payment: (bbObj.hasOwnProperty("payment") ? bbObj.payment : 0),
		penalty: (bbObj.hasOwnProperty("penalty") && bbObj.penalty > 0 ? bbObj.penalty : 0),
		deposit: (bbObj.hasOwnProperty("deposit") && parseInt(bbObj.deposit) > 0 ? parseInt(bbObj.deposit) : 0),
		allowPartialComplete: (bbObj.hasOwnProperty("allowPartialComplete") && truetypes.indexOf(bbObj.allowPartialComplete) >= 0 ? true : false),
		expiry: bbObj.expiry,
		playAcceptedSound: (!bbObj.hasOwnProperty("playAcceptedSound") || truetypes.indexOf(bbObj.playAcceptedSound) >= 0 ? true : false),
		accepted: (bbObj.hasOwnProperty("accepted") && truetypes.indexOf(bbObj.accepted) >= 0 ? true : false),
		allowTerminate: (bbObj.hasOwnProperty("allowTerminate") && falsetypes.indexOf(bbObj.allowTerminate) >= 0 ? false : true),
		percentComplete: (bbObj.hasOwnProperty("percentComplete") && bbObj.percentComplete > 0 ? bbObj.percentComplete : 0.0),
		completionType: completeType,
		stopTimeAtComplete: stopTime,
		completionTime: (bbObj.hasOwnProperty("completionTime") && bbObj.completionTime > 0 ? bbObj.completionTime : 0),
		arrivalReportText: (bbObj.hasOwnProperty("arrivalReportText") ? bbObj.arrivalReportText : ""),
		model: (bbObj.hasOwnProperty("model") ? bbObj.model : ""),
		modelPersonality: (bbObj.hasOwnProperty("modelPersonality") && parseInt(bbObj.modelPersonality) > 0 ? bbObj.modelPersonality : 0),
		spinModel: (bbObj.hasOwnProperty("spinModel") && falsetypes(bbObj.spinModel) ? false : true),
		background: (bbObj.hasOwnProperty("background") ? bbObj.background : ""),
		overlay: (bbObj.hasOwnProperty("overlay") ? bbObj.overlay : ""),
		mapOverlay: (bbObj.hasOwnProperty("mapOverlay") ? bbObj.mapOverlay : (bbObj.hasOwnProperty("overlay") ? bbObj.overlay : "")),
		forceLongRangeChart: (bbObj.hasOwnProperty("forceLongRangeChart") && truetypes.indexOf(bbObj.forceLongRangeChart) >= 0 ? true : false),
		markerShape: markShape,
		markerColor: (bbObj.hasOwnProperty("markerColor") ? bbObj.markerColor : "redColor"),
		markerScale: (bbObj.hasOwnProperty("markerScale") && bbObj.markerScale >= 0.5 && bbObj.markerScale <= 2.0 ? bbObj.markerScale : 1.0),
		additionalMarkers: addMarkers,
		disablePercentDisplay: (bbObj.hasOwnProperty("disablePercentDisplay") && truetypes.indexOf(bbObj.disablePercentDisplay) >= 0 ? true : false),
		noEmails: (bbObj.hasOwnProperty("noEmails") && bbObj.noEmails == true ? true : false),
		statusValue: (bbObj.hasOwnProperty("statusValue") && bbObj.statusValue != "" ? bbObj.statusValue : ""),
		customDisplayItems: (bbObj.hasOwnProperty("customDisplayItems") ? bbObj.customDisplayItems : ""),
		customMenuItems: (bbObj.hasOwnProperty("customMenuItems") ? bbObj.customMenuItems : ""),
		remoteDepositProcess: (bbObj.hasOwnProperty("remoteDepositProcess") && truetypes.indexOf(bbObj.remoteDepositProcess) >= 0 ? true : false),
		initiateCallback: (bbObj.hasOwnProperty("initiateCallback") ? bbObj.initiateCallback : ""),
		confirmCompleteCallback: (bbObj.hasOwnProperty("confirmCompleteCallback") ? bbObj.confirmCompleteCallback : ""),
		completedCallback: (bbObj.hasOwnProperty("completedCallback") ? bbObj.completedCallback : ""),
		terminateCallback: (bbObj.hasOwnProperty("terminateCallback") ? bbObj.terminateCallback : ""),
		failedCallback: (bbObj.hasOwnProperty("failedCallback") ? bbObj.failedCallback : ""),
		manifestCallback: (bbObj.hasOwnProperty("manifestCallback") ? bbObj.manifestCallback : ""),
		availableCallback: (bbObj.hasOwnProperty("availableCallback") ? bbObj.availableCallback : ""),
		bonusCalculationCallback: (bbObj.hasOwnProperty("bonusCalculationCallback") ? bbObj.bonusCalculationCallback : ""),
		worldScript: bbObj.worldScript,
		postStatusMessages: (bbObj.hasOwnProperty("postStatusMessages") ? bbObj.postStatusMessages : []),
		data: (bbObj.hasOwnProperty("data") ? bbObj.data : null),
		acceptedDate: (bbObj.hasOwnProperty("accepted") && truetypes.indexOf(bbObj.accepted) >= 0 ? clock.adjustedSeconds : 0),
		keepAvailable: (bbObj.hasOwnProperty("keepAvailable") ? bbObj.keep : false)
	});

	this._updateRequired = true;

	// auto-accepted items should get their manifest entry updated immediately
	if (bbObj.hasOwnProperty("accepted") && bbObj.accepted === true) {
		this.$addManifestEntry(id);
		// send an email (if installed)
		this.$sendEmail(player.ship.dockedStation, "accepted", id);
		// update f4 interface entry, if docked
		if (player.ship.docked) this.$initInterface(player.ship.dockedStation);
	}

	this.$triggerBBEvent("missionAdded");
	return id;
}

//-------------------------------------------------------------------------------------------------------------
// adds an item to the BB main menu
this.$addMainMenuItem = function $addMainMenuItem(mnu) {
	if (mnu.hasOwnProperty("text") === false || mnu.text === "") {
		throw "Invalid BB menu setting: 'text' property must be supplied and not blank.";
	}
	if (mnu.hasOwnProperty("worldScript") === false || mnu.worldScript === "") {
		throw "Invalid BB menu setting: 'worldScript' property must be supplied and not blank.";
	}
	if (mnu.hasOwnProperty("menuCallback") === false || mnu.menuCallback === "") {
		throw "Invalid BB menu setting: 'menuCallback' property must be supplied and not blank.";
	}

	this._mainMenuItems.push({
		text: mnu.text,
		color: (mnu.hasOwnProperty("color") ? mnu.color : this._menuColor),
		unselectable: (mnu.hasOwnProperty("unselectable") ? mnu.unselectable : false),
		autoRemove: (mnu.hasOwnProperty("autoRemove") ? mnu.autoRemove : false),
		worldScript: mnu.worldScript,
		menuCallback: mnu.menuCallback
	})
}

//-------------------------------------------------------------------------------------------------------------
// removes an item from the main menu based on worldScript name and function callback name
this.$removeMainMenuItem = function $removeMainMenuItem(wsName, fnName) {
	for (var i = this._mainMenuItems.length - 1; i >= 0; i--) {
		if (this._mainMenuItems[i].worldScript === wsName && this._mainMenuItems[i].menuCallback === fnName) {
			this._mainMenuItems.splice(i, 1);
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
// sets the default BB background to a new guiTextureSpecifier
this.$setBackgroundDefault = function $setBackgroundDefault(gui) {
	this._background = gui;
}

//-------------------------------------------------------------------------------------------------------------
// resets the default BB background back to the default
this.$resetBackgroundDefault = function $resetBackgroundDefault() {
	this._background = this._backgroundDefault;
}

//-------------------------------------------------------------------------------------------------------------
// sets the default BB overlay to a new guiTextureSpecifier
this.$setOverlayDefault = function $setOverlayDefault(gui) {
	this._overlay = gui;
}

//-------------------------------------------------------------------------------------------------------------
// resets the default BB overlay back to the default
this.$resetOverlayDefault = function $resetOverlayDefault() {
	this._overlay = this._overlayDefault;
}

//-------------------------------------------------------------------------------------------------------------
// registers a worldscript function to be called whenever a particular event occurs
this.$registerBBEvent = function $registerBBEvent(wsName, fnName, eventName) {
	var list = this._eventRegister[eventName];
	if (!list) list = [];
	var found = false;
	for (var i = 0; i < list.length; i++) {
		if (list[i].worldScript === wsName && list[i].functionName === fnName) {
			found = true;
			break;
		}
	}
	if (found === false) {
		list.push({
			worldScript: wsName,
			functionName: fnName
		});
		this._eventRegister[eventName] = list;
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$unregisterBBEvent = function $unregisterBBEvent(wsName, fnName, eventName) {
	var list = this._eventRegister[eventName];
	if (!list) return;
	for (var i = 0; i < list.length; i++) {
		if (list[i].worldScript === wsName && list[i].functionName === fnName) {
			list.splice(i, 1);
			break;
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
// performs all callbacks for a given event
this.$triggerBBEvent = function $triggerBBEvent(eventName, param) {
	if (!this._eventRegister) return;
	var list = this._eventRegister[eventName];
	if (!list) return;
	for (var i = 0; i < list.length; i++) {
		try {
			if (param) {
				if (worldScripts[list[i].worldScript] && worldScripts[list[i].worldScript][list[i].functionName]) {
					worldScripts[list[i].worldScript][list[i].functionName](param);
				}
			} else {
				if (worldScripts[list[i].worldScript] && worldScripts[list[i].worldScript][list[i].functionName]) {
					worldScripts[list[i].worldScript][list[i].functionName]();
				}
			}
		} catch (err) {
			log(this.name, "!!ERROR: Unable to call event callback. Event:" + eventName + ", WS:" + list[i].worldScript + " FN:" + list[i].functionName + " Error:" + err);
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
// external function call to reorder the list randomly
this.$shuffleBBList = function $shuffleBBList() {
	for (var i = 0; i < this._shuffleTries; i++)
		this._data.sort(function (a, b) {
			return Math.random() - 0.5;
		}); // shuffle order so it isn't always the same variant being checked first
}

//-------------------------------------------------------------------------------------------------------------
// external function call to remove a particular custom menu item from a BB entry
this.$removeCustomMenuItem = function $removeCustomMenuItem(bbID, index) {
	var itm = this.$getItem(bbID);
	if (itm.customMenuItems) {
		var mnu = item.customMenuItems;
		if (mnu != "" && mnu.length > index) {
			mnu.splice(index, 1);
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
// looks for any completed missions whose completion method is "WHEN_DOCKED_SOURCE", "WHEN_DOCKED_STATIONKEY" or "WHEN_DOCKED_ANYWHERE"
this.$checkForCompleteOnDock = function $checkForCompleteOnDock(station) {
	this._stationKeyDefault = this.$getStationKeyDefault(station);
	for (var i = this._data.length - 1; i >= 0; i--) {
		var item = this._data[i];
		// look for any active missions
		if (item.accepted === true) {
			// check if this mission is complete within the time required, and if the completion type is one of the "DOCKED" types.
			// logic: if the mission is flagged as completed (percentComplete = 1), and we can only set percentComplete to 1 if it's still within the time allowed (see $updateBBMissionPercentage)
			if (item.percentComplete === 1 && this.$isMissionExpired(item) === false) { // clock.adjustedSeconds < item.expiry
				if (((item.completionType === "WHEN_DOCKED_SOURCE" && item.source === system.ID && (item.stationKey === "" || this.$checkMissionStationKey(item.worldScript, station, item.stationKey) === true)) ||
						(item.completionType === "WHEN_DOCKED_STATIONKEY" && (item.stationKey === "" || this.$checkMissionStationKey(item.worldScript, station, item.stationKey) === true)) ||
						item.completionType === "WHEN_DOCKED_ANYWHERE")) {

					var result = "";
					if (item.confirmCompleteCallback) {
						if (worldScripts[item.worldScript] && worldScripts[item.worldScript][item.confirmCompleteCallback]) {
							result = worldScripts[item.worldScript][item.confirmCompleteCallback](item.ID);
						}
					}
					if (result === "") {
						// complete the mission
						this.$completeBBMission(item.ID);
					} else {
						// add the error details to the arrival report.
						player.addMessageToArrivalReport(result);
						// fail the mission
						this.$failedBBMission(item.ID, false);
					}
				}
			} else if (clock.adjustedSeconds >= item.expiry && item.expiry > 0) {
				// too late, so fail the mission
				this.$failedBBMission(item.ID, false);
			}
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
// returns the current completed percentage for a mission
this.$percentCompleted = function $percentCompleted(bbID) {
	var itm = this.$getItem(bbID);
	if (itm) return itm.percentComplete;
	return 0;
}

//-------------------------------------------------------------------------------------------------------------
// updates the completed percentage of a mission
this.$updateBBMissionPercentage = function $updateBBMissionPercentage(bbID, pct) {
	var itm = this.$getItem(bbID);
	// only update missions that are still active (if it's expired, no further updates should happen)
	if (itm && (clock.adjustedSeconds < itm.expiry || itm.expiry === -1)) {
		itm.percentComplete = pct;
		// tell the originator to update their manifest text
		if (itm.manifestCallback != "") {
			if (worldScripts[itm.worldScript] && worldScripts[itm.worldScript][itm.manifestCallback]) {
				worldScripts[itm.worldScript][itm.manifestCallback](itm.ID);
			}
		}
		// check if we've completed the mission and the completion type is set to "IMMEDIATE"
		if (pct === 1) {
			itm.completionTime = clock.adjustedSeconds;
			switch (itm.completionType) {
				case "IMMEDIATE":
					// theoretically there should be no need to call the confirmCompleteCallback here, 
					// as the calling worldScript has just flagged the mission complete.
					this.$completeBBMission(bbID);
					// if the bounty system is installed, rerun the process to take a snapshot of credits/score
					if (worldScripts.BountySystem_Deferred) {
						if (player.ship.isInSpace) worldScripts.BountySystem_Deferred.$setPlayerBaseline();
					}
					break;
				case "AT_SOURCE":
				case "WHEN_DOCKED_SOURCE":
					this.$revertChartMarker(bbID);
					break;
				case "AT_STATIONKEY":
				case "WHEN_DOCKED_STATIONKEY":
				case "WHEN_DOCKED_ANYWHERE":
				case "ANYWHERE":
					this.$removeChartMarker(bbID);
					break;
			}
		}
		return;
	}
}

//-------------------------------------------------------------------------------------------------------------
// updates the manifest screen text for a particular mission
// this should be called by the originating script when the manifestCallback routine is called
this.$updateBBManifestText = function $updateBBManifestText(bbID, newtext) {
	var item = this.$getItem(bbID);
	item.manifestText = newtext;
	// grab a copy of the first version of the manifest so we can use it in emails.
	if (item.originalManifestText === "") item.originalManifestText = newtext;
	this.$refreshManifest();
}

//-------------------------------------------------------------------------------------------------------------
// updates the status text for a particular mission
this.$updateBBStatusText = function $updateBBStatusText(bbID, newtext) {
	var item = this.$getItem(bbID);
	item.statusText = newtext;
}

//-------------------------------------------------------------------------------------------------------------
// executes various functions when a mission is completed
this.$completeBBMission = function $completeBBMission(bbID) {
	var item = this.$getItem(bbID);
	// execute the callback
	if (item.completedCallback != "") {
		if (worldScripts[item.worldScript] && worldScripts[item.worldScript][item.completedCallback]) {
			worldScripts[item.worldScript][item.completedCallback](item.ID);
		}
	}
	// pay the player their payment
	if (item.payment != 0 || item.bonusCalculationCallback != "") {
		// calculate the payment amount. normally percentComplete will be 1.0, but the "allowPartialComplete" flag means we should always scale the figure
		var total = item.payment * item.percentComplete;
		// add the deposit, if present
		//total += (item.deposit && item.deposit > 0 ? item.deposit * item.percentComplete : 0); -- deposit amount should be included in payment amount
		// apply the factored penalty, if present. Completing 30% of a mission means 70% of the penalty will apply.
		if (item.allowPartialComplete && item.penalty > 0) total -= item.penalty * (1 - item.percentComplete);
		if (item.percentComplete === 1 && item.bonusCalculationCallback !== "") {
			if (worldScripts[item.worldScript] && worldScripts[item.worldScript][item.bonusCalculationCallback]) {
				var bonus = worldScripts[item.worldScript][item.bonusCalculationCallback](item.ID);
				total += bonus;
			}
		}
		player.credits += total;

		if (total !== 0) {
			if (player.ship.status === "STATUS_DOCKING") {
				// work out what message to add to the arrival report
				var msg = "";
				if (item.arrivalReportText != "") {
					msg = item.arrivalReportText;
				} else {
					msg = expandDescription("[bb-arrival-completed]", {
						description: item.description,
						payment: formatCredits(total, true, true)
					});
				}
				player.addMessageToArrivalReport(msg);
			} else {
				player.consoleMessage(expandDescription("[bb-console-completed]", {
					description: item.description,
					payment: formatCredits(total, true, true)
				}), 5);
			}
		}
	}
	// send an email (if installed)
	this.$sendEmail(player.ship.dockedStation, "success", item.ID, total);

	// remove item from manifest screen
	this.$removeManifestEntry(item.ID);
	// remove the mission from the list
	this.$removeBBMission(item.ID);
	// update the interface screen entry
	if (player.ship.dockedStation) this.$initInterface(player.ship.dockedStation);
}

//-------------------------------------------------------------------------------------------------------------
// fails the mission (either through manual termination, or by docking after the time expires)
this.$failedBBMission = function $failedBBMission(bbID, manual) {
	var item = this.$getItem(bbID);
	var pen = 0;

	// call the failed function, if supplied
	if (manual === false && item.failedCallback !== "") {
		if (worldScripts[item.worldScript] && worldScripts[item.worldScript][item.failedCallback]) {
			worldScripts[item.worldScript][item.failedCallback](bbID);
		}
	}
	// call the terminate function, if supplied
	if (manual === true && item.terminateCallback !== "") {
		if (worldScripts[item.worldScript] && worldScripts[item.worldScript][item.terminateCallback]) {
			worldScripts[item.worldScript][item.terminateCallback](bbID);
		}
	}

	// if there was a penalty for failing the mission, penalise the player now
	if (item.penalty != 0) {
		pen = Math.round(item.penalty * (1 - item.percentComplete));
		// if they've partially completed the mission, and the specs allow for it, give the player the amount they've completed
		if (item.allowPartialComplete && item.payment > 0 && item.percentComplete > 0) {
			pen -= (item.payment & item.percentComplete);
		}
		player.credits -= pen;
	}
	if (player.ship.status === "STATUS_DOCKING") {
		// work out what message to add to the arrival report
		var msg = "";
		if (pen === 0) {
			msg = expandDescription("[bb-arrival-failed-nopenalty]", {
				description: item.description
			});
		} else if (pen > 0) {
			msg = expandDescription("[bb-arrival-failed-penalty]", {
				description: item.description,
				penalty: formatCredits(pen, true, true)
			});
		} else {
			msg = expandDescription("[bb-arrival-failed-payment]", {
				description: item.description,
				payment: formatCredits(Math.abs(pen), true, true)
			})
		}
		player.addMessageToArrivalReport(msg);
	} else {
		var type = "failed";
		if (manual === true) type = "terminate";

		if (pen === 0) {
			player.consoleMessage(expandDescription("[bb-console-" + type + "-nopenalty]", {
				description: item.description
			}), 5);
		} else if (pen > 0) {
			player.consoleMessage(expandDescription("[bb-console-" + type + "-penalty]", {
				description: item.description,
				penalty: formatCredits(pen, true, true)
			}), 5);
		} else {
			player.consoleMessage(expandDescription("[bb-console-" + type + "-payment]", {
				description: item.description,
				penalty: formatCredits(Math.abs(pen), true, true)
			}), 5);
		}
	}
	// send an email (if installed)
	if (manual === false) {
		// if the manual flag is false (ie not from the player manually terminating the mission)
		this.$sendEmail(player.ship.dockedStation, "fail", item.ID, pen);
	} else {
		// send an email (if installed)
		this.$sendEmail(player.ship.dockedStation, "terminated", item.ID, pen);
	}

	// remove item from manifest screen
	this.$removeManifestEntry(item.ID);
	this.$removeBBMission(item.ID);
	this.$initInterface(player.ship.dockedStation);
}

//-------------------------------------------------------------------------------------------------------------
// removes a mission from the datalist
this.$removeBBMission = function $removeBBMission(bbID) {
	for (var i = this._data.length - 1; i >= 0; i--) {
		if (this._data[i].ID === bbID) {
			this.$removeChartMarker(bbID);
			this._data.splice(i, 1);
			return;
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
// returns the next ID number for new missions
this.$nextID = function $nextID() {
	var ok = false;
	do {
		this._nextID += 1;
		if (this._nextID > 30000) this._nextID = 1;
		ok = true;
		// is this id available?
		for (var i = 0; i < this._data.length; i++) {
			if (this._data[i].ID === this._nextID) {
				// dang it. lets do this loop again
				ok = false;
				break;
			}
		}
	} while (ok === false);
	var result = this._nextID;
	return result;
}

//-------------------------------------------------------------------------------------------------------------
// counts the number of missions available at the current station
this.$countAvailable = function $countAvailable(station) {
	var avail = 0;
	for (var i = 0; i < this._data.length; i++) {
		if (this._data[i].source === system.ID && this._data[i].accepted === false && (this._data[i].expiry === -1 || this.$isMissionExpired(this._data[i]) === false) &&
			(this._data[i].stationKey === "" || this.$checkMissionStationKey(this._data[i].worldScript, station, this._data[i].stationKey) === true))
			avail += 1;
	}
	return avail;
}

//-------------------------------------------------------------------------------------------------------------
// counts the number of active missions (but not expired missions)
this.$countActive = function $countActive() {
	var active = 0;
	for (var i = 0; i < this._data.length; i++) {
		if (this._data[i].accepted === true) active += 1;
	}
	return active;
}

//-------------------------------------------------------------------------------------------------------------
// returns true if a mission has expired, otherwise false
this.$isMissionExpired = function $isMissionExpired(itm) {
	if (itm.expiry === -1) return false;
	var checkTime = 0;
	if (itm.completionTime != 0 && itm.stopTimeAtComplete === true) {
		checkTime = itm.completionTime;
	} else {
		checkTime = clock.adjustedSeconds + (itm.accepted === false ? this.$estimatedMissionTime(itm) : 0);
		// if we're at the destination system, give a little bit of leeway
		if (itm.destination === system.ID) checkTime -= 1800;
	}

	if (checkTime < itm.expiry && itm.expiry > 0) {
		return false;
	} else {
		return true;
	}
}

//-------------------------------------------------------------------------------------------------------------
// returns true if the mission is going to be hard to complete within the time frame, otherwise false
this.$isMissionCloseToExpiry = function $isMissionCloseToExpiry(itm) {
	var result = false;
	// assume anything in another galaxy is close to expiry
	if (itm.destinationGalaxy !== galaxyNumber) return true;

	var time = this.$estimatedMissionTime(itm);
	if (time === -1) return result;

	// check if the destination system is not the current system
	if (itm.destination != system.ID && itm.destination <= 255 && itm.destination >= 0 && itm.percentComplete < 1) {
		if (clock.adjustedSeconds + time > itm.expiry && itm.expiry > 0) result = true;
	} else {
		if (itm.expiry > 0 && itm.expiry - clock.adjustedSeconds < 1800 && itm.percentComplete < 1) result = true;
	}
	return result;
}

//-------------------------------------------------------------------------------------------------------------
// performs callback to determine if mission is actually available to the player
this.$isMissionAvailable = function $isMissionAvailable(itm) {
	if (itm.accepted === true) return true;
	if (itm.hasOwnProperty("availableCallback") && itm.availableCallback != "") {
		if (!worldScripts[itm.worldScript] || !worldScripts[itm.worldScript][itm.availableCallback]) return false;
		var test = worldScripts[itm.worldScript][itm.availableCallback](itm.ID);
		if (test === "") {
			return true;
		} else {
			return false;
		}
	} else {
		return true;
	}
}

//-------------------------------------------------------------------------------------------------------------
// performs the availableCallback and returns the unavailability reason, if any
this.$missionUnavailableReason = function $missionUnavailableReason(bbID) {
	var itm = this.$getItem(bbID);
	if (itm.accepted === true) return "";
	if (itm.hasOwnProperty("availableCallback") === false || itm.availableCallback === "") return "";
	if (!worldScripts[itm.worldScript] || !worldScripts[itm.worldScript][itm.availableCallback]) return "";
	return worldScripts[itm.worldScript][itm.availableCallback](bbID);
}

//-------------------------------------------------------------------------------------------------------------
// estimated amount of time the mission is likely to take
this.$estimatedMissionTime = function $estimatedMissionTime(itm) {
	if (itm.percentComplete === 1 && ((itm.stopTimeAtComplete === true && (itm.completionTime < itm.expiry || itm.expiry === -1)) ||
			(itm.stopTimeAtComplete === false && (clock.adjustedSeconds < itm.expiry || itm.expiry === -1))))
		return -1;
	var time = 0;
	// first, check if the destination system is not the current system
	if (itm.destination != system.ID && itm.destination <= 255 && itm.destination >= 0 && itm.percentComplete < 1) {
		// calculate time for a return journey
		var info = null;
		var route = null;
		// outward journey
		if (itm.destination != system.ID && itm.destination >= 0 && itm.destination <= 255) {
			info = System.infoForSystem(galaxyNumber, itm.destination);
			route = system.info.routeToSystem(info, "OPTIMIZED_BY_TIME");
			if (route) {
				time += route.time * 3600;
				// plus 15 minutes transit time in each system
				time += route.route.length * 900;
			}
		} else {
			time += 24 * 3600;
		}
		// return journey (if stopTimeAtComplete is false)
		if (itm.stopTimeAtComplete === false && route != null) {
			switch (itm.completeType) {
				case "AT_SOURCE":
				case "WHEN_DOCKED_SOURCE":
					if (itm.source === system.ID) {
						time += route.time * 3600;
						// plus 30 minutes transit time in each system
						time += route.route.length * 1800;
					} else {
						var src = System.infoForSystem(galaxyNumber, itm.source);
						route = src.routeToSystem(info, "OPTIMIZED_BY_TIME");
						time += route.time * 3600;
						// plus 15 minutes transit time in each system
						time += route.route.length * 900;
					}
					break;
			}
		}
		// bit of a buffer
		time += 900;
	} else {
		if (itm.expiry > 0 && itm.percentComplete < 1) time = itm.expiry - clock.adjustedSeconds;
	}
	return time;
}

//-------------------------------------------------------------------------------------------------------------
// returns the mission details for a particular mission
this.$getItem = function $getItem(bbID) {
	var checkval = parseInt(bbID);
	for (var i = 0; i < this._data.length; i++) {
		if (this._data[i].ID === checkval) return this._data[i];
	}
	return null;
}

//-------------------------------------------------------------------------------------------------------------
// gets the data index of a particular BB item
// should not be used in most cases, as list can be resorted, making the index stale
this.$getIndex = function $getIndex(bbID) {
	var checkval = parseInt(bbID);
	for (var i = 0; i < this._data.length; i++) {
		if (this._data[i].ID === bbID) return i;
	}
	return -1;
}

//-------------------------------------------------------------------------------------------------------------
this.$addStationKey = function $addStationKey(missionWorldScript, stn, stationKey) {
	var found = false;
	for (var i = 0; i < this._stationKeys.length; i++) {
		if (this._stationKeys[i].station === stn && this._stationKeys[i].worldScript === missionWorldScript && this._stationKeys[i].key === stationKey) {
			found = true;
		}
	}
	if (found === false) {
		this._stationKeys.push({
			station: stn,
			worldScript: missionWorldScript,
			key: stationKey
		});
	}
}

//-------------------------------------------------------------------------------------------------------------
// checks if the station/worldScript combination has any specific station keys added. Return true if found, otherwise false
this.$stationHasKeys = function $stationHasKeys(worldScript, stn) {
	for (var i = 0; i < this._stationKeys.length; i++) {
		if (this._stationKeys[i].station === stn && this._stationKeys[i].worldScript === worldScript) return true;
	}
	return false;
}

//-------------------------------------------------------------------------------------------------------------
// works out the stationKey for the current station
this.$getStationKeyDefault = function $getStationKeyDefault(station) {
	var stnKey = "";
	// does the station have a particular station key set in script info?
	if (station.scriptInfo.bb_station_key) stnKey = station.scriptInfo.bb_station_key;
	// what about in the script object for the station? try there too...
	if (stnKey === "" && station.script.bb_station_key) stnKey = station.script.bb_station_key;
	// if not, does this station have an allegiance value set
	if (stnKey === "" && station.allegiance != null) stnKey = station.allegiance;
	return stnKey;
}

//-------------------------------------------------------------------------------------------------------------
// checks the current stationKey against mission's station keys. returns true if the current stationKey is one of the mission's station keys
// otherwise false
this.$checkMissionStationKey = function $checkMissionStationKey(missionWorldScript, station, missionStnKey) {
	// a blank station key means anywhere
	if (missionStnKey === "") return true;
	var items = missionStnKey.split(",");
	var def = this.$getStationKeyDefault(station);
	var found = false;
	var useDefault = (this.$stationHasKeys(missionWorldScript, station) ? false : true);

	for (var i = 0; i < items.length; i++) {
		if (useDefault === true) {
			if (items[i] === def) found = true;
		} else {
			for (var j = 0; j < this._stationKeys.length; j++) {
				if (this._stationKeys[j].station === station && items[i] === this._stationKeys[j].key && this._stationKeys[j].worldScript === missionWorldScript) found = true;
			}
		}
	}
	return found;
}

//-------------------------------------------------------------------------------------------------------------
// works out whether the bulletin board is hidden on this station
this.$stationIsAllowedBB = function $stationIsAllowedBB(station) {
	var result = true;
	if (station.scriptInfo && station.scriptInfo.bb_hide && station.scriptInfo.bb_hide === 1) result = false;
	if (station.script && station.script.bb_hide && station.script.bb_hide === 1) result = false;
	return result;
}

//-------------------------------------------------------------------------------------------------------------
this.$initInterface = function $initInterface(station) {
	if (!station) return;
	// get the station key for this station
	this._stationKeyDefault = this.$getStationKeyDefault(station);
	// count how many missions are available here
	var avail = this.$countAvailable(station);
	// count how many missions are active
	var active = this.$countActive();
	// create some additional text to add to the interface screen
	var addtext = (avail > 0 ? avail + " available" : "") + (avail > 0 && active > 0 ? ", " : "") + (active > 0 ? active + " active" : "");
	if (addtext != "") addtext = " (" + addtext + ")";
	// work out the prefix for the interface
	if ((addtext != "" || this._alwaysShowBB === true) && this.$stationIsAllowedBB(station) === true) {
		var prefix = "Station";
		if (station.allegiance === "galcop") prefix = "GalCop";

		station.setInterface(this.name, {
			title: prefix + " bulletin board" + addtext,
			category: "Contracts",
			summary: "Lists any local or specialised mission opportunities available at this station.",
			callback: this.$showBB.bind(this)
		});
	} else {
		station.setInterface(this.name, null);
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$showBB = function $showBB() {
	this._lastChoice = ["", "", "", ""];
	this._maxpage = Math.ceil(this.$countAvailable(player.ship.dockedStation) / this._msRows);
	this._curpage = 0;
	this._displayType = 0;
	if (this._oldVersion != parseFloat(this.version)) this._displayType = 99;
	this.$triggerBBEvent("open");
	this._bbExiting = 1;
	this._routeMode = "OPTIMIZED_BY_NONE";
	if (player.ship.hasEquipmentProviding("EQ_ADVANCED_NAVIGATIONAL_ARRAY")) {
		this._routeMode = player.ship.routeMode;
		// if we have the array, by the current setting is "NONE", default to jumps
		if (this._routeMode === "OPTIMIZED_BY_NONE") this._routeMode = "OPTIMIZED_BY_JUMPS";
	}
	this.$showPage();
}

//-------------------------------------------------------------------------------------------------------------
this.$showPage = function $showPage() {
	function compareID(a, b) {
		return ((a.ID > b.ID) ? 1 : -1);
	}

	function compareDate(a, b) {
		return ((a.acceptedDate > b.acceptedDate) ? 1 : -1);
	}

	function comparePayment(a, b) {
		return (((a.payment - a.deposit) < (b.payment - b.deposit)) ? 1 : -1);
	}

	var p = player.ship;
	var stn = p.dockedStation;

	if (this._displayType === -1) {
		this._displayType = 0;
		return;
	}

	//this._hudHidden = p.hudHidden;
	if (this.$isBigGuiActive() === false) {
        if (p.hud != "bbs_biggui_hud.plist") this._storeHUD = p.hud;
        p.hud = "bbs_biggui_hud.plist";
	}

	this._bbOpen = true;

	var text = "";
	var opts;
	var curChoices = {};
	var def = "";
	var iStart = 0;
	var iEnd = 0;
	var items = 0;
	var flagCol = 1.0;
	var jmpIndent = 6.45;

	if (defaultFont.measureString("•") > 1) flagCol = defaultFont.measureString("•") + 0.1;

	// help for new updates
	if (this._displayType === 99) {
		var update = false;
		if (this._oldVersion === 1.4) {
			update = true;
			var ln = 0;
			curChoices["0" + ln.toString() + "_A"] = {
				text: "New feature for version 1.5: On/Near Path Notifications.",
				alignment: "LEFT",
				unselectable: true,
				color: "whiteColor"
			};
			ln += 1;
			curChoices[(ln <= 9 ? "0" : "") + ln.toString() + "_A"] = "";
			ln += 1;
			var lines = this.$columnText("Available or active mission items whose destination systems are directly on your currently plotted course will be coloured green on the mission listing. For example:", 32);
			for (var i = 0; i < lines.length; i++) {
				curChoices[(ln <= 9 ? "0" : "") + ln.toString() + "_A"] = {
					text: lines[i],
					unselectable: true,
					alignment: "LEFT",
					color: this._itemColor
				};
				ln += 1;
			}
			curChoices[(ln <= 9 ? "0" : "") + ln.toString() + "_A"] = {
				text: this.$padTextRight(" ", flagCol) +
					this.$padTextRight("Sample mission 1", 13) +
					this.$padTextRight("Lave", 5) +
					this.$padTextLeft("10 hrs", 5) +
					this.$padTextLeft(formatCredits(100, false, true), 5) +
					this.$padTextLeft("", 3),
				alignment: "LEFT",
				color: this._onPathColor,
				unselectable: true
			};
			ln += 1;
			curChoices[(ln <= 9 ? "0" : "") + ln.toString() + "_A"] = "";
			ln += 1;
			lines = this.$columnText("Available or active mission items whose destination systems are within 7ly of any system on your currently plotted course will be coloured dark green on the mission listing. For example:", 32);
			for (var i = 0; i < lines.length; i++) {
				curChoices[(ln <= 9 ? "0" : "") + ln.toString() + "_A"] = {
					text: lines[i],
					alignment: "LEFT",
					unselectable: true,
					color: this._itemColor
				};
				ln += 1;
			}
			curChoices[(ln <= 9 ? "0" : "") + ln.toString() + "_A"] = {
				text: this.$padTextRight(" ", flagCol) +
					this.$padTextRight("Sample mission 2", 13) +
					this.$padTextRight("Tionisla", 5) +
					this.$padTextLeft("15 hrs", 5) +
					this.$padTextLeft(formatCredits(200, false, true), 5) +
					this.$padTextLeft("", 3),
				alignment: "LEFT",
				color: this._nearPathColor,
				unselectable: true
			};
			ln += 1;
			curChoices[(ln <= 9 ? "0" : "") + ln.toString() + "_A"] = "";
			ln += 1;
			lines = this.$columnText("You can change the range for near systems, and also use markers to highlight these missions. Configuration options can be found in Library Config" + (worldScripts.Lib_Config ? "" : " (if Library.OXP is installed)") + ".", 32);
			for (var i = 0; i < lines.length; i++) {
				curChoices[(ln <= 9 ? "0" : "") + ln.toString() + "_A"] = {
					text: lines[i],
					alignment: "LEFT",
					unselectable: true,
					color: this._itemColor
				};
				ln += 1;
			}
			// spacers
			for (var i = 0; i < (this._msRows + 5) - ln; i++) {
				curChoices[(ln + i <= 9 ? "0" : "") + (ln + i).toString() + "_A"] = "";
			}
			this._oldVersion = 1.5;
		}

		if (update === true) {
			curChoices["98_EXIT"] = {
				text: "Press Enter to continue",
				color: this._menuColor
			};

			var opts = {
				screenID: "oolite-bbsystem-main-map",
				title: "Bulletin Board - Update Info",
				allowInterrupt: true,
				exitScreen: "GUI_SCREEN_INTERFACES",
				choices: curChoices,
				initialChoicesKey: "98_EXIT",
				message: text
			};
			if (this._overlay !== "") opts.overlay = this._overlay;
			if (this._background !== "") opts.background = this._background;
		} else {
			// if there were no updates this time, switch back to the opening list
			this._displayType = 0;
		}
	}

	// main mission list
	if (this._displayType === 0) {
		this.$triggerBBEvent("preListDisplay");
		if (this._data.length > 0) {
			text = $padTextRight(" ", flagCol + 0.3) +
				this.$padTextRight(expandDescription("[bb-header-description]"), 13) +
				this.$padTextRight(expandDescription("[bb-header-destination]"), 5) +
				this.$padTextLeft(expandDescription("[bb-header-expiry]"), 5) +
				this.$padTextLeft(expandDescription("[bb-header-payment]"), 5) +
				this.$padTextLeft("%", 3) + "\n\n";
			var active = this.$countActive();
			this._maxpage = Math.ceil((this.$countAvailable(stn) + (active > 0 ? 1 : 0) + active) / this._msRows);
			if (this._maxpage === 0) this._maxpage = 1;
			if (this._curpage > (this._maxpage - 1)) this._curpage = this._maxpage - 1;

			this._data.sort(comparePayment);
			var subdata = [];
			var top = 0;
			// add any missions available at this station
			for (var i = 0; i < this._data.length; i++) {
				if (this._data[i].source === system.ID && this._data[i].accepted === false && (this._data[i].expiry === -1 || this.$isMissionExpired(this._data[i]) === false) && // this._data[i].expiry > clock.adjustedSeconds
					(!this._data[i].stationKey || this._data[i].stationKey === "" || this.$checkMissionStationKey(this._data[i].worldScript, stn, this._data[i].stationKey) === true)) {
					if (this.$isMissionAvailable(this._data[i]) === true) {
						// available missions should go at the top of the list
						subdata.splice(top, 0, this._data[i]);
						top += 1;
					} else {
						// any missions that are unavailable for whatever reason are put at the bottom
						subdata.push(this._data[i]);
					}
				}
			}
			// then put all the accepted missions at the top of the list
			this._data.sort(compareDate);
			top = 0;
			for (var i = 0; i < this._data.length; i++) {
				if (this._data[i].accepted === true) {
					subdata.splice(top, 0, this._data[i]);
					top += 1;
				}
			}
			if (top !== 0) {
				// insert a dummy record to force a space between accepted and available missions
				subdata.splice(top, 0, {
					ID: -1
				});
			}
			this._itemList.length = 0;
			if (subdata.length > 0) {
				// grab a copy of all the ID's of items in the list
				for (var i = 0; i < subdata.length; i++) {
					if (subdata[i].ID != -1) this._itemList.push(subdata[i].ID);
				}
				// set out initial end point
				iStart = (this._curpage * this._msRows);
				iEnd = iStart + this._msRows;
				if (iEnd > subdata.length) iEnd = subdata.length;
				for (var i = iStart; i < iEnd; i++) {
					items += 1;
					if (subdata[i].ID === -1) {
						// add a spacer
						curChoices["01_ITEM-" + (items < 10 ? "0" : "") + items + "~0"] = {
							text: "",
							alignment: "LEFT",
							unselectable: true
						};
					} else {
						// work out color of item
						var colr = this._itemColor;
						var onPath = this.$systemInCurrentPlot(subdata[i].destination);
						var nearPath = (onPath === false ? this.$systemNearCurrentPlot(subdata[i].destination) : false);
						if (this._useMarkers === 0 || this._useMarkers === 2) {
							if (onPath === true) colr = this._onPathColor;
							if (nearPath === true) colr = this._nearPathColor;
						}

						if (subdata[i].accepted === false && subdata[i].expiry > 0 && subdata[i].expiry < clock.adjustedSeconds) colr = this._warningColor;
						if (this.$isMissionAvailable(subdata[i]) === false) colr = this._unavailableColor;
						if (this.$isMissionCloseToExpiry(subdata[i]) === true) colr = this._menuColor;

						curChoices["01_ITEM-" + (items < 10 ? "0" : "") + items + "~" + subdata[i].ID] = {
							text: this.$padTextRight((subdata[i].accepted === true ? "•" : " "), flagCol) +
								this.$padTextRight(subdata[i].description, 13) +
								this.$padTextRight(subdata[i].destinationName + (this._useMarkers === 1 || this._useMarkers === 2 ? (onPath === true ? " †" : (nearPath === true ? " ‡" : "")) : ""), 5) +
								this.$padTextLeft((subdata[i].percentComplete === 1 && subdata[i].stopTimeAtComplete === true ? " " : (subdata[i].expiry === -1 ? " " : this.$getTimeRemaining(subdata[i].expiry, true))), 5) +
								this.$padTextLeft((subdata[i].payment > 0 ? formatCredits(subdata[i].payment - subdata[i].deposit, true, true) : ""), 5) +
								this.$padTextLeft((subdata[i].accepted === true && (!subdata[i].disablePercentDisplay || subdata[i].disablePercentDisplay === false) ? (subdata[i].percentComplete * 100).toFixed(1) : ""), 3),
							alignment: "LEFT",
							color: colr
						};
					}
				}
				for (var i = 0; i < (((this._msRows + 1) - this._mainMenuItems.length) - items); i++) {
					curChoices["02_SPACER_" + i] = "";
				}
			} else {
				text += "No items to display";
			}
			if (this._mainMenuItems.length > 0) {
				for (var i = 0; i < this._mainMenuItems.length; i++) {
					var col = this._menuColor;
					if (this._mainMenuItems[i].hasOwnProperty("color")) col = this._mainMenuItems[i].color;
					var disabled = false;
					if (this._mainMenuItems[i].hasOwnProperty("unselectable") && this._mainMenuItems[i].unselectable === true) {
						disabled = true;
						col = this._disabledColor;
					}
					curChoices["03_MENU~" + (i < 10 ? "0" : "") + i.toString()] = {
						text: this._mainMenuItems[i].text,
						color: col,
						unselectable: disabled
					};
				}
			}
			if (this._curpage < this._maxpage - 1) {
				curChoices["10_GOTONEXT"] = {
					text: "[bb-nextpage]",
					color: this._menuColor
				};
			} else {
				curChoices["10_GOTONEXT"] = {
					text: "[bb-nextpage]",
					color: this._disabledColor,
					unselectable: true
				};
			}
			if (this._curpage > 0) {
				curChoices["11_GOTOPREV"] = {
					text: "[bb-prevpage]",
					color: this._menuColor
				};
			} else {
				curChoices["11_GOTOPREV"] = {
					text: "[bb-prevpage]",
					color: this._disabledColor,
					unselectable: true
				};
			}
		} else {
			text += "\n" + expandDescription("[bb-no-items]");
			this._maxpage = 1;
		}

		curChoices["99_EXIT"] = {
			text: "Exit",
			color: this._exitColor
		};
		var def = "99_EXIT";
		if (this._lastChoice[this._displayType] != "") def = this._lastChoice[this._displayType];

		var opts = {
			screenID: "oolite-bbsystem-main-map",
			title: "Bulletin Board - Page " + (this._curpage + 1) + " of " + this._maxpage,
			allowInterrupt: true,
			exitScreen: "GUI_SCREEN_INTERFACES",
			choices: curChoices,
			initialChoicesKey: def,
			message: text
		};
		if (this._overlay !== "") opts.overlay = this._overlay;
		if (this._background !== "") opts.background = this._background;

		this.$triggerBBEvent("postListDisplay");
	}

	// mission details
	if (this._displayType === 1) {
		var govs = new Array();
		for (var i = 0; i < 8; i++)
			govs.push(String.fromCharCode(i));
		var spc = String.fromCharCode(31);
		var colWidth = 11;
		var output = [];
		var item = this.$getItem(this._selectedItem);
		this.$triggerBBEvent("preItemDisplay", this._selectedItem);

		// build up the array of output lines
		// mission description
		output.push(this.$padTextRight(expandDescription("[bb-item-description]"), colWidth) + item.description);
		output.push("");
		if (this._showID === true) {
			output.push(this.$padTextRight("Mission ID:", colWidth) + item.ID);
		}
		// mission details
		var coltext = [];
		// allow for newline characters in the details text
		var secthead = false;
		var dtls = item.details.split("\n");
		for (var j = 0; j < dtls.length; j++) {
			coltext = this.$columnText(dtls[j], 32 - colWidth);
			for (var i = 0; i < coltext.length; i++) {
				if (secthead === false) {
					output.push(this.$padTextRight(expandDescription("[bb-item-details]"), colWidth) + coltext[i]);
					secthead = true;
				} else {
					output.push(this.$padTextRight(" ", colWidth) + coltext[i]);
				}
			}
		}
		output.push("");

		var rt = null;
		var dist = 0;
		var time = 0;
		var jumps = 0;
		var expired = this.$isMissionExpired(item);

		var sysID = system.ID;
		if (system.ID === -1) sysID = p.targetSystem;

		// source system info (only shown once a mission is accepted)
		if (item.source != sysID || item.accepted === true) {
			if (item.sourceGalaxy === galaxyNumber) {
				var orig = System.infoForSystem(galaxyNumber, item.source);
				var origtext = "";
				rt = System.infoForSystem(galaxyNumber, sysID).routeToSystem(orig, this._routeMode);
				if (rt) {
					dist = rt.distance;
					time = rt.time;
					jumps = rt.route.length - 1;
					if (this._routeMode === "OPTIMIZED_BY_NONE") {
						origtext = orig.name + " (" + govs[orig.government] + spc + "TL" + (orig.techlevel + 1) +
							(item.source === system.ID ? ", current system)" : ", dist: " + dist.toFixed(1) + "ly, " +
								(jumps > 0 ? jumps + " jump" + (jumps > 1 ? "s" : "") + ", " : "") + time.toFixed(1) + " hrs)");
					} else {
						origtext = orig.name + " (" + govs[orig.government] + spc + "TL" + (orig.techlevel + 1) +
							(item.source === system.ID ? ", current system)" : ", dist: " + dist.toFixed(1) + "ly)");
					}
				} else {
					dist = System.infoForSystem(galaxyNumber, sysID).distanceToSystem(orig);
					origtext = orig.name + " (" + govs[orig.government] + spc + "TL" + (orig.techlevel + 1) + ", dist:" + dist.toFixed(1) + "ly, unreachable)";
				}
			} else {
				origtext = item.sourceName + " (G" + (item.sourceGalaxy + 1) + ", unreachable)";
			}
			secthead = false;
			coltext = this.$columnText(origtext, 32 - colWidth);
			for (var k = 0; k < coltext.length; k++) {
				if (secthead === false) {
					output.push(this.$padTextRight(expandDescription("[bb-item-originating]"), colWidth) + coltext[k]);
					secthead = true;
				} else {
					output.push(this.$padTextRight(" ", colWidth) + coltext[k]);
				}
			}
		}

		// destination system info
		var textitem = "";
		if (item.destination < 256) {
			if (item.destination >= 0 && item.destination <= 255) {
				if (item.destinationGalaxy === galaxyNumber) {
					var sys = System.infoForSystem(galaxyNumber, item.destination);

					rt = System.infoForSystem(galaxyNumber, sysID).routeToSystem(sys, this._routeMode);
					if (rt) {
						dist = rt.distance;
						time = rt.time;
						jumps = rt.route.length - 1;
					} else {
						dist = -1;
						time = -1;
						jumps = -1;
					}

					if (dist >= 0) {
						if (this._routeMode !== "OPTIMIZED_BY_NONE") {
							textitem = sys.name + " (" + govs[sys.government] + spc + "TL" + (sys.techlevel + 1) +
								(item.destination === system.ID ? ", current system)" : ", dist: " + dist.toFixed(1) + "ly, " +
									(jumps > 0 ? jumps + " jump" + (jumps > 1 ? "s" : "") + ", " : "") + time.toFixed(1) + " hrs)");
						} else {
							// if we don't have the array, the distance is point-to-point, not route based.
							if (p.hasEquipmentProviding("EQ_ADVANCED_NAVIGATIONAL_ARRAY") === false) dist = system.info.distanceToSystem(sys);
							textitem = sys.name + " (" + govs[sys.government] + spc + "TL" + (sys.techlevel + 1) +
								(item.destination === system.ID ? ", current system)" : ", dist: " + dist.toFixed(1) + "ly, " + time.toFixed(1) + " hrs)");
						}
					} else {
						dist = System.infoForSystem(galaxyNumber, sysID).distanceToSystem(sys);
						textitem = sys.name + " (" + govs[sys.government] + spc + "TL" + (sys.techlevel + 1) + ", dist:" + dist.toFixed(1) + " ly, unreachable)";
					}
				} else {
					textitem = item.destinationName + " (G" + (item.destinationGalaxy + 1) + ")";
				}
			} else if (item.destination === -1) {
				textitem = "Interstellar space";
			}
			secthead = false;
			coltext = this.$columnText(textitem, 32 - colWidth);
			for (var k = 0; k < coltext.length; k++) {
				if (secthead === false) {
					output.push(this.$padTextRight(expandDescription("[bb-item-destination]"), colWidth) + coltext[k]);
					secthead = true;
				} else {
					output.push(this.$padTextRight(" ", colWidth) + coltext[k]);
				}
			}
		}

		// expiry (if required)
		if (item.expiry > 0 && (item.percentComplete < 1 || item.stopTimeAtComplete === false)) {
			var closeExpiry = this.$isMissionCloseToExpiry(item);
			var exp = this.$getTimeRemaining(item.expiry);
			output.push(
				this.$padTextRight(expandDescription("[bb-item-expiry]"), colWidth) +
				exp +
				(exp.indexOf("day") >= 0 ? " (" + this.$getTimeRemaining(item.expiry, true) + ")" : "") +
				((closeExpiry === true && item.expiry > clock.adjustedSeconds) ? " **" : "")
			);
			if (closeExpiry === true) output.push(this.$padTextRight(" ", colWidth) + "** Close to expiry warning");
		}
		// payment amount
		if (item.payment > 0) {
			output.push(this.$padTextRight(expandDescription("[bb-item-payment]"), colWidth) + formatCredits(item.payment, true, true));
		}
		// bonus amount (if applicable)
		if (expired === false && item.percentComplete === 1 && item.bonusCalculationCallback !== "") {
			if (worldScripts[item.worldScript] && worldScripts[item.worldScript][itm.bonusCalculationCallback]) {
				var bonus = worldScripts[item.worldScript][item.bonusCalculationCallback](item.ID);
				output.push(this.$padTextRight(expandDescription("[bb-item-bonus]"), colWidth) + formatCredits(bonus, true, true));
			}
		}
		// penalty amount (if required)
		if (item.penalty > 0 && ((item.accepted === true && (item.percentComplete < 1 || expired === true)) || item.accepted === false)) {
			output.push(this.$padTextRight(expandDescription("[bb-item-penalty]"), colWidth) + formatCredits(item.penalty * (1 - item.percentComplete), true, true));
		}
		// deposit amount (if supplied)
		if (item.deposit && item.deposit > 0) {
			output.push(this.$padTextRight(expandDescription("[bb-item-deposit]"), colWidth) + formatCredits(item.deposit, true, true));
			output.push(this.$padTextRight(expandDescription("[bb-item-netpayment]"), colWidth) + formatCredits(item.payment - item.deposit, true, true));
		}
		// add any custom items (making sure we allow for long text items)
		if (item.customDisplayItems && item.customDisplayItems != "") {
			for (var i = 0; i < item.customDisplayItems.length; i++) {
				secthead = false;
				dtls = item.customDisplayItems[i].value.toString().split("\n");
				if (dtls && dtls.length > 0) {
					for (var j = 0; j < dtls.length; j++) {
						coltext = this.$columnText(dtls[j], 32 - colWidth);
						for (var k = 0; k < coltext.length; k++) {
							if (secthead === false) {
								output.push(this.$padTextRight(expandDescription(item.customDisplayItems[i].heading), colWidth) + coltext[k]);
								secthead = true;
							} else {
								output.push(this.$padTextRight(" ", colWidth) + coltext[k]);
							}
						}
					}
				}
			}
		}
		if (item.accepted === true) {
			// percentage completed (if required)
			if (!item.disablePercentDisplay || item.disablePercentDisplay === false) {
				output.push(this.$padTextRight(expandDescription("[bb-item-percentcomplete]"), colWidth) + (item.percentComplete * 100).toFixed(1) + "%");
			}
			// any mission status text
			if (item.manifestText != "" || item.statusText != "") { // item.percentComplete < 1 && 
				if (item.statusText != "") {
					coltext = this.$columnText(item.statusText, 32 - colWidth);
				} else {
					coltext = this.$columnText(item.manifestText, 32 - colWidth);
				}
				for (var i = 0; i < coltext.length; i++) {
					if (i === 0) {
						output.push(this.$padTextRight(expandDescription("[bb-item-status]"), colWidth) + coltext[i]);
					} else {
						output.push(this.$padTextRight(" ", colWidth) + coltext[i]);
					}
				}
			}
		}

		var cmdCount = 0;
		// add "Accept Mission"" item
		if (item.accepted === false) {
			var test = this.$missionUnavailableReason(item.ID);
			if (test === "") {
				curChoices["30_ACCEPT"] = {
					text: "[bb-item-accept]",
					color: this._menuColor
				};
			} else {
				curChoices["30_ACCEPT"] = {
					text: "Unavailable (" + test + ")",
					color: this._disabledColor,
					unselectable: true
				};
			}
			cmdCount += 1;
		}
		var completeAvail = false;
		if (item.accepted === true) {
			// add "Terminate mission" item
			if (item.percentComplete < 1 || expired === true) {
				if (item.allowTerminate === true) {
					curChoices["31_TERMINATE"] = {
						text: "[bb-item-cancel]" + (item.penalty > 0 ? expandDescription("[bb-item-cancel-warning]") : ""),
						color: this._menuColor
					};
					cmdCount += 1;
				}
			}
			if (expired === false && (item.percentComplete === 1 || (item.allowPartialComplete && item.percentComplete > 0))) {
				var result = "";
				if (item.confirmCompleteCallback && item.confirmCompleteCallback !== "") {
					if (worldScripts[item.worldScript] && worldScripts[item.worldScript][item.confirmCompleteCallback]) {
						result = worldScripts[item.worldScript][item.confirmCompleteCallback](item.ID)
					}
				}
				switch (item.completionType) {
					case "AT_SOURCE":
						// if the allowPartialComplete is on, the player should get a "Complete Mission" option, but only at the source station
						// for any other station we want to hide the option completely
						if (item.sourceGalaxy === galaxyNumber) {
							if (item.source != system.ID) {
								if (item.percentComplete === 1) {
									curChoices["32_COMPLETE"] = {
										text: expandDescription("[bb-item-complete]") + " (return to " + System.infoForSystem(galaxyNumber, item.source).name + ")",
										color: this._disabledColor,
										unselectable: true
									};
								} else if (this.$checkMissionStationKey(item.worldScript, stn, item.stationKey) === false && item.percentComplete === 1) {
									curChoices["32_COMPLETE"] = {
										text: expandDescription("[bb-item-complete]") + expandDescription("[bb-item-dock-original]"),
										color: this._disabledColor,
										unselectable: true
									};
								} else {
									curChoices["32_COMPLETE"] = {
										text: expandDescription("[bb-item-complete]") + (item.percentComplete < 1 ? expandDescription("[bb-item-partial]") : ""),
										color: this._disabledColor,
										unselectable: true
									};
								}
							} else {
								if (this.$checkMissionStationKey(item.worldScript, stn, item.stationKey) === false && item.percentComplete === 1) {
									curChoices["32_COMPLETE"] = {
										text: expandDescription("[bb-item-complete]") + expandDescription("[bb-item-dock-original]"),
										color: this._disabledColor,
										unselectable: true
									};
								} else {
									curChoices["32_COMPLETE"] = {
										text: expandDescription("[bb-item-complete]") + (result != "" ? " (" + result + ")" : (item.percentComplete < 1 ? expandDescription("[bb-item-partial]") : "")),
										color: (result != "" ? this._disabledColor : this._menuColor),
										unselectable: (result != "" ? true : false)
									};
								}
							}
							if (curChoices["32_COMPLETE"].unselectable === false) completeAvail = true;
							cmdCount += 1;
						}
						break;
					case "AT_STATIONKEY":
						if (this.$checkMissionStationKey(item.worldScript, stn, item.stationKey) === false && item.percentComplete === 1) {
							curChoices["32_COMPLETE"] = {
								text: expandDescription("[bb-item-complete]") + expandDescription("[bb-item-dock-mainstation]"),
								color: this._disabledColor,
								unselectable: true
							};
						} else {
							curChoices["32_COMPLETE"] = {
								text: expandDescription("[bb-item-complete]") + (result != "" ? " (" + result + ")" : (item.percentComplete < 1 ? expandDescription("[bb-item-partial]") : "")),
								color: (result != "" ? this._disabledColor : this._menuColor),
								unselectable: (result != "" ? true : false)
							};
						}
						if (curChoices["32_COMPLETE"].unselectable === false) completeAvail = true;
						cmdCount += 1;
						break;
					case "ANYWHERE":
						curChoices["32_COMPLETE"] = {
							text: expandDescription("[bb-item-complete]") + (result != "" ? " (" + result + ")" : (item.percentComplete < 1 ? expandDescription("[bb-item-partial]") : "")),
							color: (result != "" ? this._disabledColor : this._menuColor),
							unselectable: (result != "" ? true : false)
						};
						if (curChoices["32_COMPLETE"].unselectable === false) completeAvail = true;
						cmdCount += 1;
						break;
				}
			}
		}
		// add "Show Map" item (if applicable)
		if ((item.percentComplete < 1 || completeAvail === false) && item.destinationGalaxy === galaxyNumber && item.destination != system.ID && item.destination >= 0 && item.destination <= 255) {
			if (item.hasOwnProperty("forceLongRangeChart") === true && item.forceLongRangeChart === true) {
				curChoices["25_SHOWMAP_LONG"] = {
					text: "[bb-item-showmap]",
					color: this._menuColor
				};
			} else {
				var dist = Math.round(system.info.distanceToSystem(sys), 2);
				if (dist >= 50 || (dist > 7.2 && oolite.compareVersion("1.87") > 0)) {
					curChoices["25_SHOWMAP_LONG"] = {
						text: "[bb-item-showmap]",
						color: this._menuColor
					};
				} else if (dist > 7.0 && oolite.compareVersion("1.87") <= 0) {
					curChoices["25_SHOWMAP_CUSTOM"] = {
						text: "[bb-item-showmap]",
						color: this._menuColor
					};
				} else {
					curChoices["25_SHOWMAP_SHORT"] = {
						text: "[bb-item-showmap]",
						color: this._menuColor
					};
				}
			}
			cmdCount += 1;
		}
		// add any "Set Course For" items (if applicable)
		if ((item.percentComplete < 1 || completeAvail === false) && item.destination != system.ID && 
			item.destinationGalaxy === galaxyNumber && item.destination >= 0 && item.destination <= 255 && 
			this.$systemInCurrentPlot(item.destination) === false && 
			p.hasEquipmentProviding("EQ_ADVANCED_NAVIGATIONAL_ARRAY")) {
			curChoices["96_SETCOURSE~" + item.destination] = {
				text: "Set course for " + sys.name,
				color: this._menuColor
			};
			cmdCount += 1;
		}
		// when complete, only show the "Set course" if the source isn't the current system
		if (item.percentComplete === 1 && item.source != system.ID && 
			item.sourceGalaxy === galaxyNumber && this.$systemInCurrentPlot(item.source) === false &&
			(item.completionType == "AT_SOURCE" || item.completionType == "WHEN_DOCKED_SOURCE") && 
			p.hasEquipmentProviding("EQ_ADVANCED_NAVIGATIONAL_ARRAY")) {
			curChoices["96_SETCOURSE~" + item.source] = {
				text: "Set course for " + System.systemNameForID(item.source),
				color: this._menuColor
			};
			cmdCount += 1;
		}
		// process any custom menu items
		if (item.customMenuItems && item.customMenuItems != "") {
			for (var i = 0; i < item.customMenuItems.length; i++) {
				var mnu = item.customMenuItems[i];
				var a_only = true;
				var m_avail = "";
				if (mnu.hasOwnProperty("activeOnly") === true && mnu.activeOnly === false) a_only = false;
				if (item.accepted == a_only) {
					if (mnu.hasOwnProperty("condition") === true) {
						if (worldScripts[mnu.worldScript] && worldScripts[mnu.worldScript][mnu.condition]) {
							m_avail = worldScripts[mnu.worldScript][mnu.condition](item.ID);
						}
					}
					if (m_avail === "") {
						curChoices["97_CUSTOM~" + i] = {
							text: mnu.text,
							color: this._menuColor
						};
						cmdCount += 1;
					}
					if (m_avail !== "") {
						curChoices["97_CUSTOM~" + i] = {
							text: mnu.text + " (" + m_avail + ")",
							color: this._disabledColor,
							unselectable: true
						};
						cmdCount += 1;
					}
				}
			}
		}
		if (this._itemList.indexOf(this._selectedItem) === this._itemList.length - 1) {
			curChoices["19_NEXTMISSION"] = {
				text: "[bb-item-nextmission]",
				color: this._disabledColor,
				unselectable: true
			};
		} else {
			curChoices["19_NEXTMISSION"] = {
				text: "[bb-item-nextmission]",
				color: this._menuColor
			};
		}

		curChoices["98_EXIT"] = {
			text: "[bb-item-close]",
			color: this._exitColor
		};
		cmdCount += 1;

		var start = 0;
		var end = output.length - 1;
		var extratext = "";

		// check to see if we need to use paging
		if (output.length > (27 - (cmdCount + 1))) {
			// paging required
			cmdCount += 2;
			var pagelen = (27 - (cmdCount + 1));
			var maxpage = Math.ceil(output.length / pagelen);
			if (this._displayPage === 0 && this._displayPage < maxpage - 1) {
				curChoices["21_NEXTPAGE"] = {
					text: "[bb-nextpage]",
					color: this._menuColor
				};
			} else {
				curChoices["21_NEXTPAGE"] = {
					text: "[bb-nextpage]",
					color: this._disabledColor,
					unselectable: true
				};
			}
			if (this._displayPage > 0) {
				curChoices["22_PREVPAGE"] = {
					text: "[bb-prevpage]",
					color: this._menuColor
				};
			} else {
				curChoices["22_PREVPAGE"] = {
					text: "[bb-prevpage]",
					color: this._disabledColor,
					unselectable: true
				};
			}
			start = this._displayPage * pagelen;
			end = this._displayPage * pagelen + pagelen - 1;
			if (end > output.length - 1) end = output.length - 1;
			extratext = " - Page " + (this._displayPage + 1) + " of " + maxpage;
		}

		// output the text lines
		for (var i = start; i <= end; i++) {
			text += output[i] + "\n";
		}

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

		var opts = {
			screenID: "oolite-bbsystem-item-map",
			title: expandDescription("[bb-item-heading]") + extratext,
			allowInterrupt: true,
			exitScreen: "GUI_SCREEN_INTERFACES",
			choices: curChoices,
			initialChoicesKey: def,
			message: text
		};
		if (this._overlay !== "") opts.overlay = this._overlay;
		if (item.overlay !== "") opts.overlay = item.overlay;

		if (this._background !== "") opts.background = this._background;
		if (item.background !== "") opts.background = item.background;

		if (item.model != "") {
			opts["model"] = item.model;
			if (item.modelPersonality && item.modelPersonality != 0) opts["modelPersonality"] = item.modelPersonality;
			if (item.spinModel && item.spinModel === false) opts["spinModel"] = false;
			// if a model as been set, remove any overlay
			delete opts["overlay"];
		}
		this.$triggerBBEvent("postItemDisplay", this._selectedItem);
	}

	// short range chart/custom chart
	if (this._displayType === 2 || this._displayType === 3 || this._displayType === 7) {
		// force the route mode to be the same as the player's current route mode
		// this is because the short range chart doesn't have the option of switching between modes
		//if (p.hasEquipmentProviding("EQ_ADVANCED_NAVIGATIONAL_ARRAY")) this._routeMode = p.routeMode;
		text = "";
		var item = this.$getItem(this._selectedItem);
		this.$triggerBBEvent("preItemChartDisplay", this._selectedItem);
		var sys = System.infoForSystem(galaxyNumber, item.destination);
		var screenPos = false;
		if (item.destination != system.ID && p.hasEquipmentProviding("EQ_ADVANCED_NAVIGATIONAL_ARRAY") && this._routeMode !== "OPTIMIZED_BY_NONE") {
			var rt = System.infoForSystem(galaxyNumber, system.ID).routeToSystem(sys, this._routeMode);
			if (rt && rt.route.length > 1) {
				screenPos = true;
				if (this._displayType === 3) {
					text = "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" + this.$padTextRight("", jmpIndent) + expandDescription("[bb-item-jumps]") + " " + (rt.route.length - 1);
				} else {
					text = "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" + this.$padTextRight("", jmpIndent) + expandDescription("[bb-item-jumps]") + " " + (rt.route.length - 1);
				}
			}
		}
		var lines = "\n";
		if (screenPos == false) {
			if (this._displayType === 3) {
				lines= "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n";
			} else {
				lines = "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n";
			}
		}
		text += lines + this.$padTextRight(item.description, 20) + this.$padTextLeft((item.payment > 0 ? "Payment: " + formatCredits(item.payment, true, true) : ""), 12);

		// we need to get the current plot info before we switch the players destination, otherwise the check will always be true
		var inCurrPlot_dest = this.$systemInCurrentPlot(item.destination);
		var inCurrPlot_src = this.$systemInCurrentPlot(item.source);

		// hold the player's destination
		this._suspendedDestination = p.targetSystem;
		// override it for the display
		p.targetSystem = item.destination;

		if (item.accepted === false) {
			this.$addAdditionalMarkers(this._selectedItem);
			this._tempMarkers = this._selectedItem;
			var test = this.$missionUnavailableReason(item.ID);
			if (test === "") {
				curChoices["30_ACCEPT"] = {
					text: "[bb-item-accept]",
					color: this._menuColor
				};
			} else {
				curChoices["30_ACCEPT"] = {
					text: expandDescription("[bb-item-unavailable]") + " (" + test + ")",
					color: this._disabledColor,
					unselectable: true
				};
			}
		}

		var bg = "";
		switch (this._displayType) {
			case 2:
				bg = "SHORT_RANGE_CHART";
				break;
			case 3:
				bg = "LONG_RANGE_CHART";
				break;
			case 7:
				bg = "CUSTOM_CHART";
				break;
		}
		if (oolite.compareVersion("1.87") <= 0) {
			if (p.hasEquipmentProviding("EQ_ADVANCED_NAVIGATIONAL_ARRAY")) {
				if (this._routeMode === "OPTIMIZED_BY_JUMPS") {
					bg += "_SHORTEST";
					curChoices["26_SHORTEST"] = {
						text: "[bb-item-shortest]",
						color: this._disabledColor,
						unselectable: true
					};
					curChoices["27_QUICKEST"] = {
						text: "[bb-item-quickest]",
						color: this._menuColor
					};
				} else {
					bg += "_QUICKEST";
					curChoices["26_SHORTEST"] = {
						text: "[bb-item-shortest]",
						color: this._menuColor
					};
					curChoices["27_QUICKEST"] = {
						text: "[bb-item-quickest]",
						color: this._disabledColor,
						unselectable: true
					};
				}
			}
		}
		var result = "";
		if (item.confirmCompleteCallback && item.confirmCompleteCallback !== "") {
			if (worldScripts[item.worldScript] && worldScripts[item.worldScript][item.confirmCompleteCallback]) {
				result = worldScripts[item.worldScript][item.confirmCompleteCallback](item.ID)
			}
		}
		curChoices["20_RETURN"] = {
			text: "[bb-item-return]",
			color: this._menuColor
		};
		if ((item.percentComplete < 1 || result != "") && item.destination != system.ID && inCurrPlot_dest === false && p.hasEquipmentProviding("EQ_ADVANCED_NAVIGATIONAL_ARRAY")) {
			curChoices["96_SETCOURSE~" + item.destination] = {
				text: "Set course for " + sys.name,
				color: this._menuColor
			};
		}
		if (item.percentComplete === 1 && item.source != system.ID && inCurrPlot_src === false && p.hasEquipmentProviding("EQ_ADVANCED_NAVIGATIONAL_ARRAY")) {
			curChoices["96_SETCOURSE~" + item.source] = {
				text: "Set course for " + System.systemNameForID(item.source),
				color: this._menuColor
			};
			cmdCount += 1;
		}
		if (this._nextContractOnMap === false) {
			curChoices["97_CLOSE"] = {
				text: "[bb-item-close]",
				color: this._exitColor
			};
			def = "97_CLOSE";
		} else {
			if (this._itemList.indexOf(this._selectedItem) === this._itemList.length - 1) {
				curChoices["19_NEXTMISSION"] = {
					text: "[bb-item-nextmission]",
					color: this._disabledColor,
					unselectable: true
				};
			} else {
				curChoices["19_NEXTMISSION"] = {
					text: "[bb-item-nextmission]",
					color: this._menuColor
				};
			}
			def = "20_RETURN";
		}
		if (this._lastChoice[this._displayType] != "") def = this._lastChoice[this._displayType];

		var opts = {
			screenID: (this._displayType === 3 ? "oolite-bbsystem-longrangechart-map" : "oolite-bbsystem-shortrangechart-map"),
			title: "Mission Details - Chart",
			backgroundSpecial: bg,
			allowInterrupt: true,
			exitScreen: "GUI_SCREEN_INTERFACES",
			choices: curChoices,
			initialChoicesKey: def,
			message: text
		};
		// a custom chart view, for missions with destinations between 7 and 50 LY away (Oolite 1.87 only)
		if (this._displayType === 7) {
			var dist = Math.round(system.info.distanceToSystem(sys), 2);
			for (var i = 0; i < this._zoomDist.length; i++) {
				if (dist >= this._zoomDist[i].dist) {
					opts["customChartZoom"] = this._zoomDist[i].zoom;
					break;
				}
			}
			// calculate the midpoint between the source and destination, so we get the best view of the route
			var point1 = system.info.coordinates;
			var point2 = sys.coordinates;
			var xdiff = (point1.x - point2.x) / 2;
			var ydiff = (point1.y - point2.y) / 2;
			opts["customChartCentreInLY"] = new Vector3D(point1.x - xdiff, point1.y - ydiff, 0);
		}
		if (this._overlay !== "") opts.overlay = this._overlay;
		if (item.overlay !== "") opts.overlay = item.overlay;
		if (item.mapOverlay !== "") opts.overlay = item.mapOverlay;

		this.$triggerBBEvent("postItemChartDisplay", this._selectedItem);
	}

	// confirm terminate
	if (this._displayType === 4) {
		var item = this.$getItem(this._selectedItem);
		text = expandDescription("[bb-confirm-terminate]");
		curChoices["40_YES"] = {
			text: "[bb-item-confirm-yes]",
			color: this._menuColor
		};
		curChoices["41_NO"] = {
			text: "[bb-item-confirm-no]",
			color: this._menuColor
		};
		def = "41_NO";

		var opts = {
			screenID: "oolite-bbsystem-confirmterminate-map",
			title: "Mission Terminate - Confirmation",
			allowInterrupt: true,
			exitScreen: "GUI_SCREEN_INTERFACES",
			choices: curChoices,
			initialChoicesKey: def,
			message: text
		};

		if (this._overlay !== "") opts.overlay = this._overlay;
		if (item.overlay !== "") opts.overlay = item.overlay;

		if (this._background !== "") opts.background = this._background;
		if (item.background !== "") opts.background = item.background;

		if (item.model != "") {
			opts["model"] = item.model;
			if (item.modelPersonality && item.modelPersonality != 0) opts["modelPersonality"] = item.modelPersonality;
			if (item.spinModel && item.spinModel === false) opts["spinModel"] = false;
			// if a model as been set, remove any overlay
			delete opts["overlay"];
		}
	}

	// unable to complete mission screen result
	if (this._displayType === 5) {
		var item = this.$getItem(this._selectedItem);
		text = expandDescription("[bb-unable-to-complete]") + "\n\n" + this._notCompleteText;
		curChoices["97A_CLOSE"] = {
			text: "[bb-item-close]",
			color: this._menuColor
		};
		def = "97A_CLOSE";

		var opts = {
			screenID: "oolite-bbsystem-incomplete-map",
			title: "Mission Details - Incomplete",
			allowInterrupt: true,
			exitScreen: "GUI_SCREEN_INTERFACES",
			choices: curChoices,
			initialChoicesKey: def,
			message: text
		};

		if (this._overlay !== "") opts.overlay = this._overlay;
		if (item.overlay !== "") opts.overlay = item.overlay;

		if (this._background !== "") opts.background = this._background;
		if (item.background !== "") opts.background = item.background;

		if (item.model != "") {
			opts["model"] = item.model;
			if (item.modelPersonality != 0) opts["modelPersonality"] = item.modelPersonality;
			if (item.spinModel === false) opts["spinModel"] = false;
			// if a model as been set, remove any overlay
			delete opts["overlay"];
		}
	}

	// post messages
	if (this._displayType === 10 || this._displayType === 11 || this._displayType === 12) {
		var type = ["initiated", "completed", "terminated"];
		var post = this._holdItem;
		text = expandDescription(post.text);
		if (!post.return || post.return === "list") {
			curChoices["97_CLOSE"] = {
				text: "[bb-item-close]",
				color: this._menuColor
			};
			def = "97_CLOSE";
		}
		if (post.return && post.return === "item") {
			curChoices["97A_CLOSE"] = {
				text: "[bb-item-close]",
				color: this._menuColor
			};
			def = "97A_CLOSE";
		}
		if (post.return && post.return === "exit") {
			curChoices["99_EXIT"] = {
				text: "[bb-item-close]",
				color: this._menuColor
			};
			def = "99_EXIT";
		}
		var opts = {
			screenID: "oolite-bbsystem-incomplete-map",
			title: expandDescription("[bb-title-" + type[this._displayType - 10] + "]"),
			allowInterrupt: true,
			exitScreen: "GUI_SCREEN_INTERFACES",
			choices: curChoices,
			initialChoicesKey: def,
			message: text
		};

		if (this._overlay !== "") opts.overlay = this._overlay;
		if (post.overlay !== "") opts.overlay = post.overlay;

		if (this._background !== "") opts.background = this._background;
		if (post.background !== "") opts.background = post.background;

		if (post.model && post.model != "") {
			opts["model"] = post.model;
			if (post.modelPersonality && post.modelPersonality != 0) opts["modelPersonality"] = post.modelPersonality;
			if (post.spinModel && post.spinModel === false) opts["spinModel"] = false;
			// if a model as been set, remove any overlay
			delete opts["overlay"];
		}
	}

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

//-------------------------------------------------------------------------------------------------------------
// handles player selections on the BB screen
this.$bbHandler = function $bbHandler(choice) {

	if (this._suspendedDestination >= 0) player.ship.targetSystem = this._suspendedDestination;
	this._suspendedDestination = -1;
	if (this._tempMarkers >= 0) {
		this.$removeChartMarker(this._tempMarkers);
	}

	if (!choice) {
		this.$triggerBBEvent("exit");
		return;
	}

	var newChoice = "";

	this._lastChoice[this._displayType] = choice;

	// selected bb item from main list
	if (choice.indexOf("01_ITEM") >= 0) {
		this._selectedItem = parseInt(choice.substring(choice.indexOf("~") + 1));
		this._displayType = 1;
		this._displayPage = 0;
		// from screen 0 into screen 1, always default to the "Exit" option.
		this._lastChoice[this._displayType] = "98_EXIT";
	}
	// user defined bb main menu item
	if (choice.indexOf("03_MENU") >= 0) {
		var idx = parseInt(choice.substring(choice.indexOf("~") + 1));
		if (worldScripts[this._mainMenuItems[idx].worldScript] && worldScripts[this._mainMenuItems[idx].worldScript][this._mainMenuItems[idx].menuCallback]) {
			worldScripts[this._mainMenuItems[idx].worldScript][this._mainMenuItems[idx].menuCallback]();
		}
		if (this._mainMenuItems[idx] && this._mainMenuItems[idx].hasOwnProperty("autoRemove") && this._mainMenuItems[idx].autoRemove === true) {
			this._mainMenuItems.splice(idx, 1);
		}
		// if the function needs to display a mission page during it's callback, 
		// it should set BulletinBoardSystem._displayPage = -1 before it finishes
		// that will jump the BB out of it's cycle
		// then, to jump back in, set BulletinBoardSystem._displayPage to 0 and call BulletinBoardSystem.$showPage()
		// that will return the BB back to the main contract listing page
	}
	// selected "set course to" from details page
	if (choice.indexOf("96_SETCOURSE") >= 0) {
		var dest = parseInt(choice.substring(choice.indexOf("~") + 1));
		if (dest >= 0 && dest <= 255) {
			player.ship.targetSystem = dest;
			player.ship.infoSystem = player.ship.targetSystem;
			player.consoleMessage("Course set for " + System.systemNameForID(dest));
		}
	}
	if (choice.indexOf("97_CUSTOM") >= 0) {
		var idx = parseInt(choice.substring(choice.indexOf("~") + 1));
		var item = this.$getItem(this._selectedItem);
		var mnu = item.customMenuItems[idx];

		if (mnu.worldScript && mnu.worldScript != "" && mnu.callback && mnu.callback != "") {
			if (worldScripts[mnu.worldScript] && worldScripts[mnu.worldScript][mnu.callback]) {
				worldScripts[mnu.worldScript][mnu.callback](item.ID);
			}
			if (mnu.hasOwnProperty("autoRemove") && mnu.autoRemove === true) item.customMenuItems.splice(idx, 1);
		}
		newChoice = "98_EXIT";
	}
	// other choices
	switch (choice) {
		case "11_GOTOPREV":
			this._curpage -= 1;
			if (this._curpage === 0) newChoice = "10_GOTONEXT";
			break;
		case "10_GOTONEXT":
			this._curpage += 1;
			if (this._curpage === this._maxpage - 1) newChoice = "11_GOTOPREV";
			break;
		case "19_NEXTMISSION":
			for (var i = 0; i < this._itemList.length; i++) {
				if (this._itemList[i] === this._selectedItem) {
					var target = i + 1;
					if (target < this._itemList.length) {
						this._displayType = 1;
						this._displayPage = 0;
						this._selectedItem = this._itemList[target];
						this._lastChoice[0] = "99_EXIT";
					}
					break;
				}
			}
			break;
		case "21_NEXTPAGE":
			this._displayPage += 1;
			break;
		case "22_PREVPAGE":
			this._displayPage -= 1;
			break;
		case "25_SHOWMAP_SHORT":
			this._displayType = 2;
			break;
		case "25_SHOWMAP_CUSTOM":
			this._displayType = 7;
			break;
		case "25_SHOWMAP_LONG":
			this._displayType = 3;
			break;
		case "26_SHORTEST":
			this._routeMode = "OPTIMIZED_BY_JUMPS";
			newChoice = "27_QUICKEST";
			break;
		case "27_QUICKEST":
			this._routeMode = "OPTIMIZED_BY_TIME";
			newChoice = "26_SHORTEST";
			break;
		case "30_ACCEPT":
			var item = this.$getItem(this._selectedItem);
			if (item.deposit && item.deposit > 0) {
				if (player.credits < item.deposit) {
					player.consoleMessage("Unable to accept mission. Insufficient credits to cover required deposit.", 5);
					break;
				} else {
					if (item.hasOwnProperty("remoteDepositProcess") === false || item.remoteDepositProcess === false) {
						player.credits -= item.deposit;
						player.consoleMessage("Deposit amount of " + formatCredits(item.deposit, true, true) + " was deducted from your account.", 4);
					}
				}
			}
			if (!item.hasOwnProperty("playAcceptedSound") || item.playAcceptedSound === true) this.$playAcceptContractSound();
			item.accepted = true;
			item.acceptedDate = clock.adjustedSeconds;
			if (item.initiateCallback !== "") {
				if (worldScripts[item.worldScript] && worldScripts[item.worldScript][item.initiateCallback]) {
					worldScripts[item.worldScript][item.initiateCallback](item.ID);
				}
			}
			// add item to manifest screen
			this.$addManifestEntry(item.ID);
			// send an email (if installed)
			this.$sendEmail(player.ship.dockedStation, "accepted", item.ID);

			this.$initInterface(player.ship.dockedStation);

			if (this.$postDisplayAvailable(item.ID, "initiated") === true) {
				this._displayType = 10;
				this.$storePostMessageDetails(item.ID, "initiated");
			}
			break;
		case "31_TERMINATE":
			this._displayType = 4;
			break;
		case "32_COMPLETE":
			var item = this.$getItem(this._selectedItem);
			var result = "";
			if (item.confirmCompleteCallback && item.confirmCompleteCallback !== "") {
				if (worldScripts[item.worldScript] && worldScripts[item.worldScript][item.confirmCompleteCallback]) {
					result = worldScripts[item.worldScript][item.confirmCompleteCallback](item.ID)
				}
			}
			if (result === "") {
				this._displayType = 0;
				if (this.$postDisplayAvailable(item.ID, "completed") === true) {
					this._displayType = 11;
					this.$storePostMessageDetails(item.ID, "completed");
				}
				this.$completeBBMission(item.ID);
			} else {
				this._notCompleteText = result;
				this._displayType = 5;
			}
			break;
		case "20_RETURN":
			this._displayType = 1;
			break;
		case "40_YES":
			this._displayType = 0;
			if (this.$postDisplayAvailable(this._selectedItem, "terminated") === true) {
				this._displayType = 12;
				this.$storePostMessageDetails(this._selectedItem, "terminated");
			}
			this.$failedBBMission(this._selectedItem, true);
			break;
		case "41_NO":
			this._displayType = 1;
			break;
		case "97_CLOSE":
			this._displayType = 0;
			break;
		case "97A_CLOSE":
			this._displayType = 1;
			this._displayPage = 0;
			break;
		case "98_EXIT":
			this._displayType = 0;
			break;
	}

	if (newChoice != "") this._lastChoice[this._displayType] = newChoice;

	if (choice != "99_EXIT") {
		this.$showPage();
	} else {
		this._bbExiting = 2;
		this.$triggerBBEvent("close");
	}
}

//-------------------------------------------------------------------------------------------------------------
this.missionScreenEnded = function(screenID) {
	if (this._storeHUD != "") player.ship.hud = this._storeHUD;
	this._storeHUD = "";
}

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

//-------------------------------------------------------------------------------------------------------------
// arranges text into a array of strings with a particular column width
this.$columnText = function $columnText(originalText, columnWidth) {
	var returnText = [];
	if (defaultFont.measureString(originalText) > columnWidth) {
		var hold = originalText;
		do {
			var newline = "";
			var remain = "";
			var point = hold.length;
			do {
				point = hold.lastIndexOf(" ", point - 1);
				newline = hold.substring(0, point).trim();
				remain = hold.substring(point + 1).trim();
			} while (defaultFont.measureString(newline) > columnWidth);
			returnText.push(newline);
			if (remain != "") {
				if (defaultFont.measureString(remain) <= columnWidth) {
					returnText.push(remain);
					hold = "";
				} else {
					hold = remain;
				}
			} else {
				hold = "";
			}
		} while (hold != "");
	} else {
		returnText.push(originalText);
	}
	return returnText;
}

//-------------------------------------------------------------------------------------------------------------
// returns a string containing the days, hours, minutes (and possibly seconds) remaining until the expiry time is reached
this.$getTimeRemaining = function $getTimeRemaining(expiry, hoursOnly) {
	var hrsOnly = (hoursOnly && hoursOnly === true ? true : false);
	var diff = expiry - clock.adjustedSeconds;
	var result = "";
	if (diff > 0) {
		var days = (hrsOnly === true ? 0 : Math.floor(diff / 86400));
		var hours = Math.floor((diff - (days * 86400)) / 3600);
		var mins = Math.floor((diff - (days * 86400 + hours * 3600)) / 60);
		var secs = Math.floor(diff - (days * 86400) - (hours * 3600) - (mins * 60));
		// special case - reduce 1 hour down to mins
		if (days === 0 && hours === 1 && mins < 40) {
			hours = 0;
			mins += 60;
		}
		// special case - reduce 1 min down to secs
		if (days === 0 && hours === 0 && mins === 1 && secs < 40) {
			mins = 0;
			secs += 60;
		}
		if (hrsOnly === true && mins > 30 && hours > 1) hours += 1;
		if (days > 0) result += days + (days > 1 ? " days" : " day");
		if (hours > 0) result += (result === "" ? "" : " ") + hours + (hours > 1 ? " hrs" : " hr");
		if (hrsOnly === false || (hours === 0 && mins > 0)) {
			if (mins > 0) result += (result === "" ? "" : " ") + mins + (mins > 1 ? " mins" : " min");
		}
		if (hrsOnly === false || (hours === 0 && mins === 0 && secs > 0)) {
			if (hours === 0 && secs > 0) result += (result === "" ? "" : " ") + secs + (secs > 1 ? " secs" : " sec");
		}
	} else {
		if (hrsOnly === false) {
			result = "No time - mission time expired!";
		} else {
			result = "Expired";
		}
	}
	return result;
}

//-------------------------------------------------------------------------------------------------------------
// adds a mark to the galactic chart when a new mission is accepted
// also forces the manifest details to be updated
this.$addManifestEntry = function $addManifestEntry(bbID) {
	var item = this.$getItem(bbID);
	if (!item) return;
	if (item.destination >= 0 && item.destination <= 255) {
		if (item.markerShape != "NONE")
			mission.markSystem({
				system: item.destination,
				name: item.worldScript + "_" + bbID,
				markerShape: item.markerShape,
				markerColor: item.markerColor,
				markerScale: item.markerScale
			});
	}
	this.$addAdditionalMarkers(bbID);

	// if we don't have any manifest text yet, tell the originator to populate it
	if (item.manifestText === "" && item.manifestCallback) {
		if (worldScripts[item.worldScript] && worldScripts[item.worldScript][item.manifestCallback]) {
			worldScripts[item.worldScript][item.manifestCallback](item.ID);
		}
	}
	this.$refreshManifest();
}

//-------------------------------------------------------------------------------------------------------------
this.$addAdditionalMarkers = function $addAdditionalMarkers(bbID) {
	var item = this.$getItem(bbID);
	if (item.hasOwnProperty("additionalMarkers") === true) {
		for (var i = 0; i < item.additionalMarkers.length; i++) {
			var ai = item.additionalMarkers[i];
			mission.markSystem({
				system: ai.system,
				name: item.worldScript + "_" + bbID,
				markerShape: ai.markerShape,
				markerColor: ai.markerColor,
				markerScale: ai.markerScale
			});
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
// refreshes the mission text on the manifest screen
this.$refreshManifest = function $refreshManifest() {
	function compareDate(a, b) {
		return ((a.acceptedDate > b.acceptedDate) ? 1 : -1);
	}
	this._data.sort(compareDate);
	var textData = [];
	textData.push(expandDescription("[bb-manifest-header]"));
	for (var i = 0; i < this._data.length; i++) {
		if (this._data[i].accepted === true) {
			if (this._data[i].manifestText != "") {
				// make sure the mission text will fit on the display by breaking up the text into screen-width columns
				var coltext = this.$columnText(this._data[i].manifestText, 30);
				for (var j = 0; j < coltext.length; j++) {
					textData.push((j === 0 ? "" : " ") + coltext[j]);
				}
			}
		}
	}
	if (textData.length === 1) {
		mission.setInstructions(null, this.name);
	} else {
		mission.setInstructions(textData, this.name);
	}
}

//-------------------------------------------------------------------------------------------------------------
// reverts the chart marker back to the source location (for when a mission is complete and the player needs to return to the source for payment)
this.$revertChartMarker = function $revertChartMarker(bbID) {
	var item = this.$getItem(bbID);
	if (item.markerShape != "NONE") {
		// remove the existing chart marker
		this.$removeChartMarker(bbID);
		// point it at the source system
		mission.markSystem({
			system: item.source,
			name: item.worldScript + "_" + bbID,
			markerShape: item.markerShape,
			markerColor: item.markerColor,
			markerScale: item.markerScale
		});
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$removeChartMarker = function $removeChartMarker(bbID) {
	var item = this.$getItem(bbID);
	// remove the chart marker
	if (item && (item.markerShape != "NONE" || (item.hasOwnProperty("additionalMarkers") === true && item.additionalMarkers.length > 0))) {
		// we're cycling through every possible system in case the destination was updated mid-mission
		for (var i = 0; i <= 255; i++) {
			mission.unmarkSystem({
				system: i,
				name: item.worldScript + "_" + bbID
			});
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
// removes the manifest screen entry for a particular mission
this.$removeManifestEntry = function $removeManifestEntry(bbID) {
	// remove the chart marker
	this.$removeChartMarker(bbID);

	var item = this.$getItem(bbID);
	// make sure this item gets removed from the manifest display as well
	if (item) item.manifestText = "";

	this.$refreshManifest();
}

//-------------------------------------------------------------------------------------------------------------
// sends confirmation emails (if the email system is installed)
this.$sendEmail = function $sendEmail(station, eventType, missID, trueAmt) {
	var email = worldScripts.EmailSystem;
	// don't bother sending email if the system isn't installed
	if (email == null) return;

	var itm = this.$getItem(missID);
	if (!itm || (itm.hasOwnProperty("noEmails") && itm.noEmails === true)) return;

	var text = "";
	var subj = "Confirmed: '" + itm.description + "'";

	if (trueAmt == null) trueAmt = itm.payment;

	switch (eventType) {
		case "accepted":
			text += "This is to confirm that you have agreed to the following mission: " + itm.manifestText;
			text += "\n\nFull description of the mission:\n" + itm.details;
			text += (itm.payment > 0 ? "\n\nThe agreed payment for successfully completing this mission is " + formatCredits(itm.payment, true, true) + "." : "");
			if (itm.penalty > 0) text += "\n\nIf you are unable to complete the mission within the time period allowed, you will be penalised " + formatCredits(itm.penalty, true, true) + ".";
			subj += " accepted";
			break;
		case "terminated":
			text += "This is to confirm that you have terminated the following mission: " + itm.originalManifestText;
			if (trueAmt > 0) {
				text += "\n\nYou have be penalised " + formatCredits(trueAmt, true, true) + " for terminating the mission before completion.";
			} else if (trueAmt < 0) {
				text += "\n\nBecause you have partially completed the mission, you have been awarded " + formatCredits(trueAmt, true, true) + ", which has been credited to your account.";
			}
			subj += " terminated";
			break;
		case "success":
			text += "This is to confirm that you have successfully completed the following mission: " + itm.originalManifestText;
			if (trueAmt > 0) text += "\n\nThe agreed payment for successfully completing this mission was " + formatCredits(trueAmt, true, true) + ", which has been credited to your account.";
			subj += " completed";
			break;
		case "fail":
			text += "This is to confirm that you have failed to complete the following mission within the specified time period: " + itm.originalManifestText;
			if (trueAmt > 0) {
				text += "\n\nBecause of this, you have been penalised " + formatCredits(trueAmt, true, true) + ".";
			} else if (trueAmt < 0) {
				text += "\n\nHowever, because you have partially completed the mission, you have been awarded " + formatCredits(trueAmt, true, true) + ", which has been credited to your account.";
			}
			subj += " failed";
			break;
	}

	// each station can have it's own BB admin name
	var stnName = "default";
	if (station == null) {
		if (system != -1) stnName = system.mainStation.displayName;
	} else {
		stnName = station.displayName;
	}

	if (this._bbAdminName[stnName] == null || this._bbAdminName[stnName] === "") this.$setupRepName(stnName);

	text += expandDescription("\n\n[name]\nBulletin Board Administration Authority", {
		name: this._bbAdminName[stnName]
	});

	var emailID = email.$createEmail({
		sender: "Bulletin Board Admin",
		subject: subj,
		date: global.clock.seconds,
		message: text
	});

	if (emailID && emailID > 0) {
		itm.lastEmailID = emailID;
	}
}

//-------------------------------------------------------------------------------------------------------------
// sets up the admin authority user name for confirmation emails
this.$setupRepName = function $setupRepName(stationName) {
	this._bbAdminName[stationName] = randomName() + " " + randomName();
}

//-------------------------------------------------------------------------------------------------------------
// returns true if the passes System ID is in the player's currently plotted course, otherwise false
this.$systemInCurrentPlot = function $systemInCurrentPlot(sysID) {

	var result = false;
	var target = player.ship.targetSystem;

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

	return result;
}

//-------------------------------------------------------------------------------------------------------------
// returns true if the passes System ID is in the player's currently plotted course, otherwise false
this.$systemNearCurrentPlot = function $systemNearCurrentPlot(sysID) {
	var result = false;
	var target = player.ship.targetSystem;

	if (oolite.compareVersion("1.81") < 0) {
		// in 1.81 or greater, the target system could be more than 7 ly away. It becomes, essentially, the final destination.
		// there could be multiple interim stop points between the current system and the target system.
		// the only way to get this info is to recreate a route using the same logic as entered on the ANA, and pick item 1
		// from the list. That should be the next destination in the list.
		var myRoute = System.infoForSystem(galaxyNumber, global.system.ID).routeToSystem(System.infoForSystem(galaxyNumber, target), player.ship.routeMode);
		if (myRoute) {
			for (var i = 0; i < myRoute.route.length; i++) {
				var rtSys = myRoute.route[i];
				var sys = System.infoForSystem(galaxyNumber, rtSys).systemsInRange(this._nearPathRange);
				for (var j = 0; j < sys.length; j++) {
					if (sys[j].systemID === sysID) {
						result = true;
						break;
					}
				}
				if (result === true) break;
			}
		}
	} else {
		var sys = System.infoForSystem(galaxyNumber, target).systemsInRange(this._nearPathRange);
		for (var i = 0; i < sys.length; i++) {
			if (sys[i].systemID === sysID) {
				result = true;
				break;
			}
		}
	}

	return result;
}

//-------------------------------------------------------------------------------------------------------------
// returns true if there is a post-display message available for a particular status, otherwise false
this.$postDisplayAvailable = function $postDisplayAvailable(bbID, type) {
	var item = worldScripts.BulletinBoardSystem.$getItem(bbID);
	var list = item.postStatusMessages;
	var result = false;
	if (list && list.length > 0) {
		for (var i = 0; i < list.length; i++) {
			if (list[i].status === type) result = true;
		}
	}
	return result;
}

//-------------------------------------------------------------------------------------------------------------
// returns the post display dictionary item for a particular status
this.$getPostDisplay = function $getPostDisplay(bbItem, type) {
	var list = bbItem.postStatusMessages;
	var result = {};
	if (list && list.length > 0) {
		for (var i = 0; i < list.length; i++) {
			if (list[i].status === type) result = list[i];
		}
	}
	return result;
}

//-------------------------------------------------------------------------------------------------------------
// stores details of a particular post message dictionary
// this is so we can delete the BB item but still show the message to the player
this.$storePostMessageDetails = function $storePostMessageDetails(bbID, type) {
	var item = this.$getItem(bbID);
	var post = this.$getPostDisplay(item, type);
	this._holdItem = {};
	this._holdItem["text"] = post.text;
	if (post.model) this._holdItem["model"] = post.model;
	if (post.modelPersonality) this._holdItem["modelPersonality"] = post.modelPersonality;
	if (post.spinModel) this._holdItem["spinModel"] = post.spinModel;
	if (post.overlay) this._holdItem["overlay"] = post.overlay;
	if (post.background) this._holdItem["background"] = post.background;
	if (post.return) this._holdItem["return"] = post.return;
}

//-------------------------------------------------------------------------------------------------------------
// make sure all records have an accepted date value (used for sorting)
this.$addAcceptedDate = function $addAcceptedDate() {
	for (var i = 0; i < this._data.length; i++) {
		if (this._data[i].hasOwnProperty("acceptedDate") === false) {
			if (this._data[i].accepted === true) {
				this._data[i].acceptedDate = clock.adjustedSeconds;
			} else {
				this._data[i].acceptedDate = 0;
			}
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$systemNameForID = function $systemNameForID(dest) {
	if (dest === 256) return "";
	return System.systemNameForID(dest);
}

//-------------------------------------------------------------------------------------------------------------
// adds additional data elements to records, if not present
this.$updateData = function $updateData() {
	for (var i = 0; i < this._data.length; i++) {
		if (this._data[i].hasOwnProperty("sourceGalaxy") === false) this._data[i].sourceGalaxy = galaxyNumber;
		if (this._data[i].hasOwnProperty("destinationGalaxy") === false) this._data[i].destinationGalaxy = galaxyNumber;
		if (this._data[i].hasOwnProperty("sourceName") === false) this._data[i].sourceName = System.systemNameForID(this._data[i].source);
		if (this._data[i].hasOwnProperty("destinationName") === false) this._data[i].destinationName = this.$systemNameForID(this._data[i].destination);
	}
}

//-------------------------------------------------------------------------------------------------------------
// look for any orphaned system marks and remove them
this.$dataCleanup = function $dataCleanup() {
	var mk = mission.markedSystems;
	for (var i = mk.length - 1; i >= 0; i--) {
		if (mk[i].name.indexOf("GalCopBB_Missions") >= 0) {
			// get the mission ID
			var id = parseInt(mk[i].name.substring(mk[i].name.lastIndexOf("_") + 1));
			if (this.$getItem(id) === null) {
				mission.unmarkSystem({
					system: mk[i].system,
					name: mk[i].name
				});
			}
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$playAcceptContractSound = function $playAcceptContractSound() {
	var sound = new SoundSource;
	sound.sound = "[contract-accepted]";
	sound.play();
}