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

Expansion Enhanced Passenger Contracts

Content

Warnings

  1. Information URL mismatch between OXP Manifest and Expansion Manager string length at character position 0

Manifest

from Expansion Manager's OXP list from Expansion Manifest
Description Expansion to passenger contracts system. Expansion to passenger contracts system.
Identifier oolite.oxp.phkb.EnhancedPassengerContracts oolite.oxp.phkb.EnhancedPassengerContracts
Title Enhanced Passenger Contracts Enhanced Passenger Contracts
Category Missions Missions
Author phkb phkb
Version 0.4 0.4
Tags
Required Oolite Version
Maximum Oolite Version
Required Expansions
  • oolite.oxp.phkb.BulletinBoardSystem:1.6
  • oolite.oxp.phkb.BulletinBoardSystem:1.6
  • Optional Expansions
  • oolite.oxp.phkb.BroadcastCommsMFD:1.2.8
  • oolite.oxp.phkb.BountySystem:0.4.1
  • oolite.oxp.phkb.BroadcastCommsMFD:1.2.8
  • oolite.oxp.phkb.BountySystem:0.4.1
  • Conflict Expansions
    Information URL n/a
    Download URL https://wiki.alioth.net/img_auth.php/d/d9/EnhancedPassengerContracts.oxz n/a
    License CC-BY-NC-SA 4.0 CC-BY-NC-SA 4.0
    File Size n/a
    Upload date 1670936999

    Documentation

    Also read http://wiki.alioth.net/index.php/Enhanced%20Passenger%20Contracts

    readme.txt

    Enhanced Passenger Contracts
    by Nick Rogers
    
    Overview
    ========
    This OXP aims to expand on passenger contracts in a number of ways. 
    
    1. The player will occasionally receive comms messages from passengers in a variety of situations.
    
    2. Some passengers will now have a stipulation that they do not want a rough ride. Get involved in too much fighting, and throw the ship around too much, and your final payment will be reduced by 50%.
    
    3. Some passengers will have a stipulation that they do not want to dock at GalCop stations in systems with a particular government (eg Anarchies). If you do, the contract will be cancelled.
    
    4. Some passengers will have a stipulation that they do not want to dock at GalCop stations in systems where a particular species (eg Rodents) are the primary inhabitant. If you do, the contract will be cancelled.
    
    5. Some passengers will have a stipulation that they do not want to dock at a GalCop station in a particular system (eg Lave, Tionisla). If you do, the contract will be cancelled.
    
    6. Some passengers will offer a bonus amount if the player docks at GalCop stations in particular systems before reaching the final destination. 
    
    7. If you have the Broadcast Comms MFD available, you might find that sometimes passengers will change their minds. You might get a request from a passenger to take them to a different destination to the one you signed up for. You'll have the option of accepting the new destination (which comes with a bonus payment), or to keep the contract the way it was.
    
    8. If you have the Bounty System installed, some passengers might have a stipulation that they do not want to be scanned by police or bounty hunters at any time during transit. If you get scanned, the final payment will be reduced by 75%. 
    
    9. Again, if you have the Bounty System installed, passengers can have a bounty amount, and if police discover that bounty during a scan in certain systems, you may receive a small penalty to your offender status.
    
    Configuration
    =============
    You can control what types of contracts will be created, and whether any of the comms messages will be sent, by changing settings made available through Library Config. The settings are:
    
    	A:Government-type restrictions - passengers indicating they don't want to visit systems of a particular government type.
    	B:Species-type restrictions - passengers indicating they don't want to visit systems with particular inhabitants.
    	C:System restrictions - passengers indicating they don't want to visit particular systems.
    	D:Bonus systems - passengers offering bonuses for visiting particular systems before the final destination.
    	E:Passenger bounties - passenger can have bounties (visible or hidden) which can be picked up by police warrant scanners.
    	F:No bounty scan restrictions - passengers indicating they do not want your ship scanned with a warrant scanner.
    	G:Smooth flight restrictions - passengers indicating they want their journey to be without any combat in it.
    	H:Mid-flight changes - passengers changing their minds about their destination somewhere along the way.
    	I:Passenger comms - passengers will send comms messages to the player in certain conditions.
    
    Setting a value to "1" turns the option on. Setting it to "0" turns it off. Turning all values to 0 will render the OXP inoperative, and the types of passenger contracts generated will be identical to the core passenger contract system.
    
    Once a contract setting is turned off, it will take until your next jump for available contracts to reflect the changes. However, the setting will have an immediate impact on existing contracts (ie if you turn off the "Government-type restrictions" after accepting a contract of this type, no government-type checking will take place).
    
    Required OXP's
    ==============
    The following OXP's are listed as required:
    
    - Bulletin Board System (1.6)
    	Required as all passenger missions are accessed through this interface.
    
    Recommended OXP's
    =================
    The following OXP's are listed as recommended, in that additional text and/or gameplay options will become available if they are installed.
    
    - Broadcast Comms MFD (v1.2.8)
    - Bounty System (v0.4.1)
    - Email System (v1.7.4)
    - Library (v1.4)
    
    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/
    
    Version History
    ===============
    0.4
    - Fixed JS referencing error.
    
    0.3
    - Removed PhraseGen code, reverted to original name generation code, to reduce the size of the code base.
    
    0.2
    - Better timer handling.
    
    0.1
    - Initial release.
    

    Equipment

    This expansion declares no equipment. This may be related to warnings.

    Ships

    This expansion declares no ships. This may be related to warnings.

    Models

    This expansion declares no models. This may be related to warnings.

    Scripts

    Path
    Scripts/epc_passenger_contracts.js
    "use strict";
    this.name					= "EnhancedPassengerContracts";
    this.author					= "phkb";
    this.copyright              = "2018 phkb";
    this.description			= "Expands the passenger contracts system";
    this.licence                = "CC BY-NC-SA 4.0";
    
    /*
        todo:
            passengers who get injured during battles and need medical treatment at the next station
            passengers who steal cargo and disappear
            passengers who steal parcels and disappear
    */
    //
    
    this._flags = [1,1,1,1,1,1,1,1,1];
    this._settings = 511;
    this._simulator = false;
    this._internal = false;
    this._loading = true;
    this._emailDisable = false;
    this._sendEmailType = 0;
    this._informPlayerOfSystem = false;
    
    this._avoidGovTypeLoading = 1.2; // 20% increase
    this._avoidSpeciesLoading = 1.3; // 30% increase
    this._avoidSystemLoading = 1.05; // 5% for each system
    this._smoothFlightLoading = 2.0; // 100% increase
    this._bountyLoading = 1.6; // 60% increase
    this._noScanLoading = 1.6; // 60% increase
    this._bonusSystemLoading = 0.1; // 10%
    
    this._changeTimer = null;
    this._monitorWildMovementTimer = null;
    this._chatTimer = null;
    this._complimentTimer = null;
    this._damageTimer = null;
    this._impatientTimer = null;
    this._interstellarSpaceTimer = null;
    this._transmissions = [];
    
    this._govTypeNames = ["Anarchy","Feudal","Multi-Government","Dictatorship","Communist","Confederate","Democratic","Corporate State"];
    this._inhabitantTypes = ["Lobsters","Felines","Humanoids","Insects","Frogs","Lizards","Rodents","Birds","Colonials"];
    
    this._config = {
        Name:this.name, 
        Display:"Config", 
        Alias:"Enh. Passenger Contracts", 
        Alive:"_config", 
        Notify:"$onChange",
        EInt:{
            E0:{
                Name:"_settings", 
                Def:511, 
                Min:0, 
                Max:511, 
                Desc:[
                    "A:Gov type restr.",
                    "B:Species type restr.",
                    "C:System restr.",
                    "D:Bonus systems",
                    "E:Passenger bounties",
                    "F:No bounty scan restr.",
                    "G:Smooth flight restr.",
                    "H:Mid-flight changes",
                    "I:Passenger comms"
                ]
            },
            Info:"Options A-H control what type of enhanced contracts will be generated. Option I controls whether passengers will send the player any comms messages."
        },
    };
    
    //-------------------------------------------------------------------------------------------------------------
    this.$onChange = function() {
    	var set = 1;
    	for (var i = 0; i < this._flags.length; i++) {
    		if (this._settings & set) {
    			this._flags[i] = 1;
    		} else {
    			this._flags[i] = 0;
    		}
    		set *= 2;
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.startUp = function() {
        // turn off standard passenger contract system
        // disable script and empty its dataset
        this.$disableCorePassengerContracts();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.startUpComplete = function() {
        if (worldScripts.BountySystem_Core) {
            // tell the bounty system about the new offense type
            var b = worldScripts.BountySystem_Core;
            b.$uncoverBounties_endpoint = b.$uncoverBounties;
            b.$uncoverBounties = this.$ovr_bountySystem_uncoverBounties;
    		b._offenceTypes["transporting offenders"] = {description:"Transporting and associating with known offenders.", severity:1};
        }
        if (worldScripts.AutoPrimeEquipment) {
            // sync up autoprime equip so selecting our MFD automatically primes broadcast comms
            worldScripts.AutoPrimeEquipment.$addConfig("EnhancedPassengerMFD", "EQ_BROADCASTCOMMSMFD");
        }
        if (missionVariables.EnhancedPassengers_Settings) {
            // pull out the stored value
            this._settings = parseInt(missionVariables.EnhancedPassengers_Settings);
        } else {
            // build settings from flags array (in case a user has manually changed the defaults)
            var set = 1;
            this._settings = 0;
            for (var i = 0; i < this._flags.length; i++) {
                if (this._flags[i] === 1) {
                    this._settings += set;
                }
                set *= 2;
            }
        }
        
        // register our settings, if Lib_Config is present
    	if (worldScripts.Lib_Config) worldScripts.Lib_Config._registerSet(this._config);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.systemWillPopulate = function() {
        // add our station key to the main station, so the contracts are only offered there
        if (system.mainStation) {
            worldScripts.BulletinBoardSystem.$addStationKey(this.name, system.mainStation, "mainStation");        
        }
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.missionScreenOpportunity = function() {
        if (this._loading === true) {
            this._loading = false;
            this.$removeExistingContracts();
            if (this.$countAvailableContracts() === 0) this.$createPassengerContracts();
            // force the interface entry to update
            worldScripts.BulletinBoardSystem.$initInterface(player.ship.dockedStation);
            delete this.missionScreenOpportunity;
        }
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.playerEnteredContract = function(type, contract) {
        if (this._internal === true) {
            if (this._sendEmailType > 0) {
                switch (this._sendEmailType) {
                    case 1: 
                        this.$customPassengerContractEmail(contract);
                        break;
                    default:
                        this.$contractAdjustmentEmail(contract);
                        break;
                }
            }
        }
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.playerCompletedContract = function(type, result, fee, contract) {
        if (type === "passenger") {
            var bb = worldScripts.BulletinBoardSystem;
            for (var i = 0; i < bb._data.length; i++) {
                var itm = bb._data[i];
                if (itm != null && itm.accepted === true) {
                    // is this item the one we're dealing with?
                    if (itm.destination === contract.destination && itm.payment === contract.fee && itm.source === contract.start) {
                        bb.$removeBBMission(itm.ID);
                        break;
                    }
                }
            }
        }
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$epc_shipBeingAttacked = function(whom) {
        player.ship.script._epc_attacked += 1;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$epc_shipTargetDestroyed = function(target) {
        if (Math.random() < 0.4) {
            if (!this._complimentTimer || this._complimentTimer.isRunning === false) {
                this._complimentTimer = new Timer(this, this.$niceShotMessage, 2, 0);
            }
        }
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$epc_shipTakingDamage = function(amount, whom, type) {
        if (amount > 0 && (player.ship.energy - amount) > 0 && Math.random() < 0.6) {
            if (!this._damageTimer || this._damageTimer.isRunning === false) {
                this._damageTimer = new Timer(this, this.$damageMessage, 2, 0);
            }
        }
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipDied = function(whom, why) {
        if (this._simulator === true) return;
        this.$stopTimers();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipLaunchedFromStation = function(station) {
        if (this.$simulatorRunning() === true) {
            this._simulator = true;
            return;
        }
        this.$startTimers();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipWillDockWithStation = function(station) {
    	if (this._simulator === true) return;
        this.$stopTimers();
        
        // check if we have any contracts to terminate
        var bb = worldScripts.BulletinBoardSystem;
        var data = bb._data;
        for (var i = data.length - 1; i >= 0; i--) {
            if (data[i].accepted === true && data[i].worldScript === this.name) {
                var contract = data[i].data.details;
                var terminate = false;
                if (station.allegiance === "galcop") {
                    if (this._flags[0] === 1 && contract.avoidGovType >= 0 && contract.avoidGovType === system.government) {
                        player.addMessageToArrivalReport(expandDescription("[epc_avoid-government-fail]", {name:contract.name, govType:system.governmentDescription}));
                        terminate = true;
                    }
                    if (this._flags[1] === 1 && contract.avoidSpecies != "" && system.info.inhabitants.indexOf(contract.avoidSpecies) >= 0) {
                        player.addMessageToArrivalReport(expandDescription("[epc_avoid-species-fail]", {name:contract.name, species:contract.avoidSpecies}));
                        terminate = true;
                    }
                    if (this._flags[2] === 1 && contract.avoidSysList && contract.avoidSysList.length > 0) {
                        if (contract.avoidSysList.indexOf(system.ID) >= 0) {
                            player.addMessageToArrivalReport(expandDescription("[epc_avoid-system-fail]", {name:contract.name, system:system.name}));
                            terminate = true;
                        }
                    }
                    if (this._flags[3] === 1 && contract.bonusSysList && contract.bonusSysList.length > 0) {
                        var idx = contract.bonusSysList.indexOf(system.ID);
                        if (idx >= 0) {
                            // remove the system now, so the player can't get the bonus twice
                            contract.bonusSysList.splice(idx, 1);
                            // remove this system from the map
                            for (var j = 0; j < data[i].additionalMarkers.length; j++) {
                                if (data[i].additionalMarkers[j].destination === system.ID) {
                                    data[i].additionalMarkers.splice(j, 1);
                                    break;
                                }
                            }
                            // because we are still mid-mission at this point, the BB system will not automatically remove
                            // any chart markers (it will wait until the mission is finished to do this). 
                            /// So we'll do it now manually.
                            mission.unmarkSystem({system:system.ID, name:this.name + "_" + data[i].ID});
                            // add an arrival report
                            player.addMessageToArrivalReport(expandDescription("[epc_bonus-system-success]", {name:contract.name, bonus:formatCredits(data[i].payment * this._bonusSystemLoading, true, true)}));
                            // pay the player
                            player.credits += Math.floor((data[i].payment * this._bonusSystemLoading) * 10) / 10;
                        }
                    }
                }
                if (terminate === true) {
                    // send an email message for our special case
                    var w = worldScripts.EmailSystem;
                    if (w && this._emailDisable === false) {
                        var sndr = expandDescription("[passenger-contract-sender]");
                        var msg = expandDescription("[passenger-contract-terminated]",
                            {contractname:contract.name,
                            systemname:System.systemNameForID(contract.destination),
                            time:global.clock.clockStringForTime(data[i].expiry)
                        });
                        var subj = expandDescription("[passenger-contract-terminated-subject]");
                
                        w.$createEmail({sender:sndr,
                            subject:subj,
                            date:global.clock.adjustedSeconds,
                            message:msg,
                            expiryDays:worldScripts.GalCopAdminServices._defaultExpiryDays
                        });
                    }
    
                    player.ship.removePassenger(contract.name);
                    player.decreasePassengerReputation();
                    bb.$removeBBMission(data[i].ID);
                    break;
                }
            }
        }
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipWillExitWitchspace = function() {
        if (!system.isInterstellarSpace && !system.sun.hasGoneNova && system.mainStation) {
            // must be a regular system with a main station
            this.$createPassengerContracts();
        }
        var p = player.ship;
        p.script._epc_wildMovement = 0;
        p.script._epc_attacked = 0;
    
        // only try and do a change if broadcast comms is installed
        if (p.passengerCount > 0 && p.equipmentStatus("EQ_BROADCASTCOMMSMFD") === "EQUIPMENT_OK") {
            if (system.isInterstellarSpace) {
                if (this._flags[8] === 1) this._interstellarSpaceTimer = new Timer(this, this.$interstellarSpaceMessage.bind(this), 7, 0);
            } else {
                if (this._flags[8] === 1) {
                    if (this._impatientTimer && this._impatientTimer.isRunning) this._impatientTimer.stop();
                    this._impatientTimer = new Timer(this, this.$impatientMessage.bind(this), (60 * 10), (60 * 5)); // after 10 minutes, then every 5 mins after that
                }
                if (this._flags[7] === 1) {
                    if (this._changeTimer && this._changeTimer.isRunning) this._changeTimer.stop();
                    this._changeTimer = new Timer(this, this.$doContractChange.bind(this), Math.random() * 60 + 30, 0);
                }
            }
        }
    
        this.$resetFoundFlag();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.guiScreenChanged = function(to, from) {
        if (this._informPlayerOfSystem === true) {
            player.consoleMessage("New destination marked with white 'X'", 6);
            this._informPlayerOfSystem = false;
        }
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$createPassengerContracts = function() {
        var bountySys = false;
        if (worldScripts.BountySystem_Core) bountySys = true;
    
    	// no point in generating too many, but generally want 5 or more
    	// some of them will be discarded later
    	var numContracts = Math.floor(5 * Math.random() + 5 * Math.random() + 5 * Math.random() + (player.passengerReputationPrecise * Math.random()));
    	if (player.passengerReputationPrecise >= 0 && numContracts < 5)	{
    		numContracts += 5;
    	}
    	if (numContracts > 16) {
    		numContracts = 16;
    	} else if (numContracts < 0) {
    		numContracts = 0;
    	}
    
    	// some of these possible contracts may be discarded later on
    	for (var i = 0; i < numContracts; i++) {
    		// pick a random system to take the passenger to
    		var destination = Math.floor(Math.random() * 256);
    		// discard if chose the current system
    		if (destination === system.ID) continue;
    		// get the SystemInfo object for the destination
    		var destinationInfo = System.infoForSystem(galaxyNumber, destination);
            // skip nova systems
    		if (destinationInfo.sun_gone_nova) continue;
    
            // create a passenger contract record
            this.$createRecord(destinationInfo, bountySys);
        }
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$createRecord = function(sys, bountySystem) {
        // this part is the same as core
        var passenger = {};
        var daysUntilDeparture = 1 + (Math.random() * (7 + player.passengerReputationPrecise - sys.government));
        if (daysUntilDeparture <= 0) { 
            // loses some more contracts if reputation negative
            return;
        }
       
        var route = system.info.routeToSystem(sys);
        if (!route || route.route.length === 0) return;
    
        passenger.payment = 0;
        passenger.destination = sys.systemID;
        passenger.route = route;
        if (Math.random() < 0.5) {// 50% local inhabitant
            passenger.species = system.info.inhabitant;
        } else { // 50% random species (which will be 50%ish human)
            passenger.species = System.infoForSystem(galaxyNumber, Math.floor(Math.random() * 256)).inhabitant;
        }
    
        if (passenger.species.match(new RegExp(expandDescription("[human-word]"), "i"))) {
            passenger.name = expandDescription("%N ") + expandDescription("[nom]");
        } else {
            passenger.name = randomName() + " " + randomName();
        }
    
        /* Because passengers with duplicate names won't be accepted,
         * check for name duplication with either other passengers
         * here or other passengers carried by the player, and adjust
         * this passenger's name a little if there's a match */
        do {
            var okay = true;
            for (var j = 0; j < player.ship.passengers.length; j++) {
                if (player.ship.passengers[j].name == passenger.name) {
                    okay = false;
                    break;
                }
            }
            if (okay) {
                var bb = worldScripts.BulletinBoardSystem;
                var data = bb._data;
                for (var j = 0; j < data.length; j++) {
                    var item = data[j];
                    if (item.data && item.data.hasOwnProperty("type") && item.data.type === "enhanced_passenger" && item.data.details.name == passenger.name) {
                        okay = false;
                        break;
                    }
                }
            }
            if (!okay) passenger.name += "a";
        } while (!okay);
    
        // risk
        passenger.risk = Math.floor(Math.random()*3);
        passenger.species = expandDescription("[passenger-description-risk" + passenger.risk + "]") + " " + passenger.species;
        
        // time allowed for delivery is time taken by "fewest jumps"
        // route, plus timer above. Higher reputation makes longer
        // times available.
        var d_time = Math.floor(daysUntilDeparture * 86400) + (passenger.route.time * 3600);
        passenger.deadline = clock.adjustedSeconds + d_time;
        if (passenger.risk < 2 && sys.government <= 1 && Math.random() < 0.5) {
            passenger.risk++;
        }
    
        // ok, we have a valid target - what sort of destination is this?
        // total payment is:
        passenger.payment = Math.floor(
            // payment per hop (higher at rep > 5)
            5 * Math.pow(route.route.length - 1, (passenger.risk * 0.2) + (player.passengerReputationPrecise > 5 ? 2.45 : 2.3)) +
                // payment by route length
                route.distance * (8 + (Math.random() * 8)) +
                // premium for delivery to more dangerous systems
                (5 * (7 - sys.government) * (7 - sys.government))
        );
        passenger.payment *= (Math.random() + Math.random() + Math.random() + Math.random()) / 2;
    
        var prudence = (2 * Math.random()) - 1;
        var desperation = (Math.random() * (0.5 + passenger.risk)) * (1 + 1 / (Math.max(0.5, d_time - (route.time * 3600))));
        var competency = Math.max(50, (route.route.length - 1) * (0.5 + (passenger.risk * 2)));
        if (passenger.risk == 0) competency -= 30;
    
        passenger.payment = Math.floor(passenger.payment * (1 + (0.4 * prudence)));
        passenger.payment += (passenger.risk * 200);
        passenger.skill = Math.min(60, competency + 20 * (prudence - desperation));
    
        // this is the enhanced stuff
        passenger.changed = false;
    
        var typeCheck = Math.random() * (10 + (7 - sys.economy));
        var avoidGov = 0.2;
        var avoidSpecies = 0.2;
        var avoidSystem = 0.2;
        var bonusSystem = 0.2; 
        var smooth = 0.1;
        var noScan = 0.2;
        if (typeCheck >= 9) {
            smooth = 0.5;
            noScan = 0.6;
        } else if (typeCheck >= 7) {
            smooth = 0.2;
            noScan = 0.4;
        }
        //log(this.name, "Passenger " + passenger.name);
    
        passenger.smooth = false;
        // only low-risk passengers want smooth flights, on shorter trips only
        if (this._flags[6] === 1 && passenger.risk === 0 && route.route.length < 10 && Math.random() < smooth) {
            // this passenger wants a smooth flight
            passenger.smooth = true;
            // make the payment a bit larger 
            //log(this.name, "pre smooth loading  = " + passenger.payment);
            passenger.payment = Math.floor(passenger.payment * this._smoothFlightLoading);
            //log(this.name, "post smooth loading = " + passenger.payment);
            //passenger.changed = true; // we'll prevent these passengers from making mid-route changes, though
        }
    
        passenger.avoidGovType = -1;
        if (this._flags[0] === 1 && Math.random() < avoidGov) {
            do {
                passenger.avoidGovType = Math.floor(Math.random() * 8);
            } while (passenger.avoidGovType === sys.government || passenger.avoidGovType === system.government);
            //log(this.name, "pre avoidGov loading  = " + passenger.payment);
            passenger.payment = Math.floor(passenger.payment * this._avoidGovTypeLoading);
            //log(this.name, "post avoidGov loading = " + passenger.payment);
        }
    
        passenger.avoidSpecies = "";
        if (this._flags[1] === 1 && passenger.avoidGovType === -1 && Math.random() < avoidSpecies) {
            do {
                passenger.avoidSpecies = this._inhabitantTypes[Math.floor(Math.random() * this._inhabitantTypes.length)];
            } while (sys.inhabitants.indexOf(passenger.avoidSpecies) >= 0 || passenger.species.indexOf(passenger.avoidSpecies) >= 0);
            //log(this.name, "pre avoidSpecies loading  = " + passenger.payment);
            passenger.payment = Math.floor(passenger.payment * this._avoidSpeciesLoading);
            //log(this.name, "post avoidSpecies loading = " + passenger.payment);
        }
    
        var rt = system.info.routeToSystem(sys, "OPTIMIZED_BY_TIME");
        
        passenger.avoidSysList = [];
        if (this._flags[2] === 1 && Math.random() < avoidSystem) {
            // how many will we have to avoid?
            var num = Math.floor(route.route.length * 0.2);
            if (num > 3) num = 3;
            if (num < 1) num = 1;
            //log(this.name, "number to avoid " + num + ", routeLen = " + route.route.length);
            for (var i = 0; i < num; i++) {
                // sometime pick a system on the quick route
                if (Math.random() > 0.6) {
                    var tgt = System.infoForSystem(galaxyNumber, rt.route[Math.floor(Math.random() * (rt.route.length - 1))]);
                } else {
                    var tgt = System.infoForSystem(galaxyNumber, route.route[Math.floor(Math.random() * (route.route.length - 1))]);
                }
                // make sure we avoid any conflicts with other clauses
                if ((passenger.avoidGovType === -1 || passenger.avoidGovType != tgt.government) && 
                    (passenger.avoidSpecies === "" || tgt.inhabitants.indexOf(passenger.avoidSpecies) === -1) &&
                    tgt.systemID != system.ID && 
                    passenger.avoidSysList.indexOf(tgt.systemID) === -1) {
                    passenger.avoidSysList.push(tgt.systemID);
                    passenger.payment = Math.floor(passenger.payment * this._avoidSystemLoading);
                }
            }
            //log(this.name, "avoid sysList = " + passenger.avoidSysList.length);
        }
    
        passenger.bonusSysList = [];
        if (this._flags[3] === 1 && Math.random() < bonusSystem) {
            // how many extra will we have?
            var num = Math.floor(route.route.length * 0.2);
            if (num > 3) num = 3;
            if (num < 1) num = 1;
            // get the fast route as well
            //log(this.name, "potential bonus systems " + num + ", routeLen = " + route.route.length);
            for (var i = 0; i < num; i++) {
                // make sure the point we pick along the route is not the start or end point
                var point = Math.floor(Math.random() * (route.route.length - 2)) + 1;
                var tgtList = System.infoForSystem(galaxyNumber, route.route[point]).systemsInRange(6);
                var tgt = tgtList[Math.floor(Math.random() * tgtList.length)];
                // make sure we avoid any conflicts with other clauses
                if ((passenger.avoidGovType === -1 || passenger.avoidGovType != tgt.government) && 
                    (passenger.avoidSpecies === "" || tgt.inhabitants.indexOf(passenger.avoidSpecies) === -1) &&
                    route.route.indexOf(tgt.systemID) === -1 && 
                    rt.route.indexOf(tgt.systemID) === -1 && 
                    passenger.avoidSysList.indexOf(tgt.systemID) === -1 &&
                    passenger.bonusSysList.indexOf(tgt.systemID) === -1) 
                    passenger.bonusSysList.push(tgt.systemID);
            }
            //log(this.name, "bonus sysList = " + passenger.bonusSysList.length);
        }
    
        passenger.noScan = false;
        passenger.bounty = 0;
        passenger.hiddenBounty = 0;
        passenger.found = false;
        var custMenu = null;
        if (bountySystem) {
            // warrant check only available if player is clean
            if (player.bounty === 0 && this._flags[4] === 1) {
                custMenu = [];
                custMenu.push({text:"Request GalCop Warrant Information", worldScript:this.name, callback:"$doWarrantCheck", condition:"$warrantCheckAvailable", autoRemove:true});
            }
    
            if (this._flags[4] === 1 && Math.random() < ((8 - system.government) / 8)) {
                if (Math.random() > 0.8) { 
                    // this passenger has a bounty
                    passenger.bounty += Math.floor(Math.random() * (10 - system.government) + 5);
                    // make the payment a bit larger - this passenger knows it's riskier
                    //log(this.name, "pre bounty loading  = " + passenger.payment);
                    passenger.payment = Math.floor(passenger.payment * this._bountyLoading);
                    //log(this.name, "post bounty loading = " + passenger.payment);
                } else {
                    // this passenger has a hidden bounty
                    passenger.hiddenBounty += Math.floor(Math.random() * (10 - system.government) + 5);
                    //log(this.name, passenger.name + " - " + passenger.hiddenBounty);
                }
            }
            if (this._flags[5] === 1 && Math.random() > 0.8) { 
                // but only some will care, even those who don't have a bounty
                if (passenger.smooth === false && Math.random() < noScan) {
                    passenger.noScan = true;
                    // make the payment a bit larger 
                    //log(this.name, "pre noscan loading  = " + passenger.payment);
                    passenger.payment = Math.floor(passenger.payment * this._noScanLoading);
                    //log(this.name, "post noscan loading = " + passenger.payment);
                }
            }
        }
    
        passenger.advance = Math.min(passenger.payment * 0.9, Math.max(0, Math.floor(passenger.payment * (0.05 + (0.1 * desperation) + (0.02 * player.passengerReputationPrecise))))); // some% up front
        passenger.payment -= passenger.advance;
    
        this.$ovr_addPassengerToSystem(passenger, custMenu);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$disableCorePassengerContracts = function() {
        var psngr = worldScripts["oolite-contracts-passengers"];
        // remove the event hooks
        delete psngr.shipWillExitWitchspace;
        delete psngr.playerWillSaveGame;
        delete psngr.shipWillLaunchFromStation;
        delete psngr.guiScreenWillChange;
        delete psngr.guiScreenChanged;
        // intercept the _addPassengerToSystem function
        psngr._addPassengerToSystem = this.$ovr_addPassengerToSystem;
    
        // if contracts on BB is installed, we don't need this function
        if (worldScripts.ContractsOnBB) {
            delete this.playerCompletedContract;
        }
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$removeExistingContracts = function() {
        // clear the array
        var psngr = worldScripts["oolite-contracts-passengers"];
        if (psngr.$passengers && psngr.length > 0) psngr.$passengers.length = 0;
        system.mainStation.setInterface("oolite-contracts-passengers", null);
        // remove any pre-existing BB passenger contracts
        if (worldScripts.ContractsOnBB) {
            var cobb = worldScripts.ContractsOnBB;
            var bb = worldScripts.BulletinBoardSystem;
            // remove anything already there
            var curr = cobb.$countContracts(32000);
            //log(this.name, "current " + curr);
            if (curr > 0) {
                for (var i = 0; i < curr; i++) {
                    bb.$removeBBMission(32000 + i);
                }
            }
        }
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$ovr_addPassengerToSystem = function(passenger, custMenu) {
        var xui = false;
        var ovr = "";
        if (worldScripts.XenonReduxUI) {
            xui = true;
            ovr = {name:"xrui-boardingpass.png", height:546};
        }
        if (worldScripts.XenonUI) {
            xui = true;
            ovr = {name:"xui-boardingpass.png", height:546};
        } 
    
        // we won't attempt to change any other passenger - just the ones we create
        // so we'll flag any contract that arrives here as having been changed
        if (passenger.hasOwnProperty("changed") === false) {
            passenger.changed = true;
        }
    
        var cust = [];
        if (passenger.advance > 0) cust.push({heading:expandDescription("[epc_contract-advance]"), value:formatCredits(passenger.advance, true, true)});
        cust.push({heading:expandDescription("[epc_contract-clientname]"), value:passenger.name + ", a " + passenger.species});
    
        if (passenger.hasOwnProperty("bounty") && passenger.bounty > 0) {
            cust.push({heading:expandDescription("[epc_contract-bounty]"), value:(passenger.bounty >= 50 ? "Fugitive" : "Offender") + " (" + passenger.bounty + ")"});
        }
        var additional = ".";
        var addCount = 0;
    
        var smooth_text = "";
        if (passenger.hasOwnProperty("smooth") === true && passenger.smooth === true) {
            smooth_text = expandDescription("[epc_contract-no-wildness]");
            addCount += 1;
        } else {
            passenger.smooth = false;
        }
        var scan_text = "";
        if (passenger.hasOwnProperty("noScan") === true && passenger.noScan === true) {
            scan_text = expandDescription("[epc_contract-no-scan]");
            addCount += 1;
        } else {
            passenger.noScan = false;
        }
        var avoidGov_text = "";
        if (passenger.hasOwnProperty("avoidGovType") === true && passenger.avoidGovType >= 0) {
            avoidGov_text = expandDescription("[epc_contract-avoid-gov]", {govType:this._govTypeNames[passenger.avoidGovType]});
            addCount += 1;
        } else {
            passenger.avoidGovType = -1;
        }
        var avoidSpecies_text = "";
        if (passenger.hasOwnProperty("avoidSpecies") === true && passenger.avoidSpecies != "") {
            avoidSpecies_text = expandDescription("[epc_contract-avoid-species]", {species:passenger.avoidSpecies});
            addCount += 1;
        } else {
            passenger.avoidSpecies = ""
        }
        var addMarkers = [];
        var avoidSysList_text = "";
        var govs = new Array();
        for (var i = 0; i < 8 ; i++) govs.push(String.fromCharCode(i));
        if (passenger.hasOwnProperty("avoidSysList") === true && Array.isArray(passenger.avoidSysList) === true && passenger.avoidSysList.length > 0) {
            var listText = "";
            for (var i = 0; i < passenger.avoidSysList.length; i++) {
                var sys = System.infoForSystem(galaxyNumber, passenger.avoidSysList[i]);
                listText += "\n -- " + sys.name + " (" + govs[sys.government] + " TL" + (sys.techlevel + 1) + ")";
                addMarkers.push({destination:sys.systemID, markerColor:"yellowColor", markerShape:"MARKER_X"});
            }
            avoidSysList_text = expandDescription("[epc_contract-avoid-systems]", {syslist:listText});
            addCount += 1;
        }
        var bonusSysList_text = "";
        if (passenger.hasOwnProperty("bonusSysList") === true && Array.isArray(passenger.bonusSysList) === true && passenger.bonusSysList.length > 0) {
            var listText = "";
            for (var i = 0; i < passenger.bonusSysList.length; i++) {
                var sys = System.infoForSystem(galaxyNumber, passenger.bonusSysList[i]);
                listText += "\n -- " + sys.name + " (" + govs[sys.government] + " TL" + (sys.techlevel + 1) + ")";
                addMarkers.push({destination:sys.systemID, markerColor:"greenColor", markerShape:"MARKER_X"});
            }
            bonusSysList_text = expandDescription("[epc_contract-bonus-systems]", {bonus:formatInteger(this._bonusSystemLoading * 100) + "% of payment", syslist:listText});
            addCount += 1;
        }
        if (addCount > 0) additional = expandDescription("[epc_additional-clauses]", {multiChange:(addCount === 1 ? "" : "s")});
    
        var customMenu = "";
        if (custMenu != undefined) customMenu = custMenu;
    
        var bb = worldScripts.BulletinBoardSystem;
        bb.$addBBMission({
            source:system.ID,
            destination:passenger.destination,
            stationKey:"mainStation",
            description:expandDescription("[epc_contract-passenger-title]"),
            details:expandDescription("[epc_contract-passenger-description]", {client:passenger.name, destination:System.systemNameForID(passenger.destination), extra:(additional + smooth_text + avoidGov_text + avoidSpecies_text + avoidSysList_text + bonusSysList_text + scan_text)}),
            payment:passenger.payment,
            allowTerminate:false,
            completionType:"IMMEDIATE",
            stopTimeAtComplete:true,
            allowPartialComplete:false,
            expiry:passenger.deadline,
            disablePercentDisplay:true,
            noEmails:true,
            playAcceptedSound:false,
            markerShape:"NONE", // marker control will be handled by contracts system
            overlay:(xui === true ? ovr : ""),
            customDisplayItems:cust,
            customMenuItems:customMenu,
            additionalMarkers:addMarkers,
            initiateCallback:"$acceptPassengerContract",
            availableCallback:"$passengerContractAvailable",
            worldScript:this.name,
            data:{type:"enhanced_passenger", details:passenger}
        });
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$acceptPassengerContract = function(missID) {
        //log(this.name, "accepted contract " + missID);
        var bb = worldScripts.BulletinBoardSystem;
        var item = bb.$getItem(missID);
        this.$acceptContract(item.data.details);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // perform checks to see if the passenger contract can be accepted by the player
    this.$passengerContractAvailable = function(missID) {
        var pc = worldScripts["oolite-contracts-passengers"];
        var item = worldScripts.BulletinBoardSystem.$getItem(missID);
    	// temp variable to simplify code
        var passenger = item.data.details;
        if (passenger) {
            var playerrep = worldScripts["oolite-contracts-helpers"]._playerSkill(player.passengerReputationPrecise);
            if (player.ship.passengerCapacity <= player.ship.passengerCount) {
                return expandMissionText("oolite-contracts-passengers-command-unavailable").replace("(", "").replace(")", "");
            } 
            else if (playerrep < passenger.skill) {
                var utype = "both";
                if (player.passengerReputationPrecise*10 >= passenger.skill) {
                    utype = "kills";
                } else if (Math.sqrt(player.score) >= passenger.skill) {
                    utype = "rep";
                }
                return expandMissionText("oolite-contracts-passengers-command-unavailable-"+utype).replace("(", "").replace(")", "");
            }
        }
        return "";
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$acceptContract = function(passenger) {
        // give the passenger to the player
        this._internal = true;
        this.$disableEmails();
        this._sendEmailType = 1;
        var result = player.ship.addPassenger(passenger.name, system.ID, passenger.destination, passenger.deadline, passenger.payment, passenger.advance, passenger.risk);
        this.$enableEmails();
        this._sendEmailType = 0;
        this._internal = false;
    	var helpers = worldScripts["oolite-contracts-helpers"];
    	if (result)	{
            // pay the advance
    		player.credits += passenger.advance;
    
    		helpers._soundSuccess();
    
    		if (passenger.risk > 0) {
    			// once for medium risk
    			helpers._setClientName(passenger.name);
    			if (passenger.risk > 1) {
    				// three times for high risk
    				helpers._setClientName(passenger.name);
    				helpers._setClientName(passenger.name);
    			}
    		}
    	} else {
    		// else must have had another passenger board recently
    		// (unlikely, but another OXP could have done it)
    		helpers._soundFailure();
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$countAvailableContracts = function() {
        var count = 0;
        var bb = worldScripts.BulletinBoardSystem;
        var data = bb._data;
        for (var i = 0; i < data.length; i++) {
            var item = data[i];
            if (item.accepted === false && item.data && item.data.hasOwnProperty("type") && item.data.type === "enhanced_passenger") count += 1;
        }
        //log(this.name, "count contracts " + count);
        return count;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$doContractChange = function $doContractChange() {
        // go through all the accepted passenger contracts and see if any of them want to change their destination
        var p = player.ship;
        if (player.alertCondition === 3) {
            if (Math.random() > 0.0) {
                this._changeTimer = new Timer(this, this.$doContractChange.bind(this), Math.random() * 60 + 30, 0);
            }
            return; // maybe try again shortly
        }
        // make sure we still have broadcast comms (might have been damaged)
        if (p.equipmentStatus("EQ_BROADCASTCOMMSMFD") !== "EQUIPMENT_OK") return;
        for (var i = 0; i < p.passengers.length; i++) {
            var passenger = p.passengers[i];
            if (Math.random() > 0.0 && passenger.destination != system.ID) {  // 0.8
                // how far away is the destination
                var sys = System.infoForSystem(galaxyNumber, passenger.destination);
                var rt = system.info.routeToSystem(sys, "OPTIMIZED_BY_TIME");
                // if there's no route to the system, or there's more than 2 jumps left, we're good to go
                if (!rt || rt.route.length > 2) {
                    var sysList = system.info.systemsInRange(30);
                    var newsys = sysList[Math.floor(Math.random() * sysList.length)];
                    var newrt = system.info.routeToSystem(newsys, "OPTIMIZED_BY_TIME");
                    if (newrt && newrt.route.length > 1 && newrt.route.length > rt.route.length) {
                        var bb = worldScripts.BulletinBoardSystem;
                        var data = bb._data;
                        var bbitem = null;
                        for (var j = 0; j < data.length; j++) {
                            var item = data[j];
                            if (item.accepted === true && item.worldScript === this.name) {
                                if (item.data.details.name === passenger.name && item.data.details.destination === passenger.destination) {
                                    bbitem = item;
                                    break;
                                }
                            }
                        }
                        // if this passenger has already done a mid-flight change, don't do another one
                        if (!bbitem) continue;
                        var dtls = bbitem.data.details;
                        if (dtls.changed === true) continue;
                        // make sure we dont conflict with other types
                        if (dtls.avoidGovType >= 0 && newsys.government === dtls.avoidGovType) continue;
                        if (dtls.avoidSpecies != "" && newsys.inhabitants.indexOf(dtls.avoidSpecies) >= 0) continue;
                        if (dtls.avoidSysList.length > 0 && dtls.avoidSysList.indexOf(newsys.systemID) >= 0) continue;
                        if (dtls.bonusSysList.length > 0 && dtls.bonusSysList.indexOf(newsys.systemID) >= 0) continue;
                        // found a target!
                        // time to power up the MFD
                        var diff = Math.abs(newrt.route.length - rt.route.length);
                        //log(this.name, "diff " + diff + ", risk " + passenger.risk + ", gov " + newsys.government);
                        var extra = Math.pow(diff, (passenger.risk + 1) * 1.5) * (14 - newsys.government);
                        if (extra < 50) extra = 50;
                        var txt = expandDescription("[epc_new-destination]", {client:passenger.name, original_system:sys.name, new_system:newsys.name, extra:formatCredits(extra, false, true)});
                        // store some data for quick pickup later
                        this._newDest = {
                            name:passenger.name, 
                            original:sys.systemID, 
                            source:item.source,
                            newDest:newsys.systemID, 
                            deadline:clock.adjustedSeconds + (newrt.time * 3600),
                            species:passenger.species,
                            risk:passenger.risk,
                            skill:dtls.skill,
                            advance:passenger.premium,
                            payment:passenger.fee,
                            newRoute:newrt,
                            smooth:dtls.smooth,
                            noScan:dtls.noScan,
                            bounty:dtls.bounty,
                            avoidGovType:dtls.avoidGovType,
                            avoidSpecies:dtls.avoidSpecies,
                            // next two should always be empty (because we are preventing those types from changing their minds)
                            // but in case /we/ ever change our minds...
                            avoidSysList:(dtls.hasOwnProperty("avoidSysList") ? dtls.avoidSysList : []),
                            bonusSysList:(dtls.hasOwnProperty("bonusSysList") ? dtls.bonusSysList : []),
                            hiddenBounty:dtls.hiddenBounty,
                            found:dtls.found,
                            bbID:bbitem.ID,
                            extra:extra
                        };
    
                        mission.markSystem({system:newsys.systemID, name:"epc_newDest", markerColor:"whiteColor", markerShape:"MARKER_X"});
                        this._informPlayerOfSystem = true;
                        // set this value so if the player declines, we won't keep asking on this passenger
                        bbitem.data.details.changed = true;
                        // communicate with player about the new mission
                        worldScripts.EnhancedPassengerMFD.$updateMFD(txt);
                        break;
                    }
                }
            }
        }
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // routine to check the combat simulator worldscript, to see if it's running or not
    this.$simulatorRunning = function() {
    	var w = worldScripts["Combat Simulator"];
    	if (w && w.$checkFight && w.$checkFight.isRunning) return true;
    	return false;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$acceptPendingChange = function() {
        if (!this._newDest || this._newDest == null) return;
        // get a copy of the passenger details
        var p = player.ship;
        // remove the system mark
        mission.unmarkSystem({system:this._newDest.newDest, name:"epc_newDest"});
        this._informPlayerOfSystem = false;
        // remove the old passenger
        p.removePassenger(this._newDest.name)
        // add them back in with the new details
        this._internal = true;
        this.$disableEmails();
        this._sendEmailType = 2;
        var result = p.addPassenger(this._newDest.name, this._newDest.source, this._newDest.newDest, this._newDest.deadline, parseInt(this._newDest.payment) + parseInt(this._newDest.extra), this._newDest.advance, this._newDest.risk);
        this._sendEmailType = 0;
        this.$enableEmails();
        this._internal = false;
        // update the bb
        var bb = worldScripts.BulletinBoardSystem;
        var item = bb.$getItem(this._newDest.bbID);
        item.destination = this._newDest.newDest;
        item.destinationName = System.systemNameForID(this._newDest.newDest);
        item.expiry = this._newDest.deadline;
        item.payment = this._newDest.payment + this._newDest.extra;
        item.data.details = {
            payment: this._newDest.payment + this._newDest.extra,
            destination: this._newDest.newDest,
            route: this._newDest.newRoute,
            species: this._newDest.species,
            name: this._newDest.name,
            risk: this._newDest.risk,
            deadline: this._newDest.deadline,
            skill: this._newDest.skill,
            advance: this._newDest.advance,
            smooth: this._newDest.smooth,
            noScan: this._newDest.noScan,
            bounty: this._newDest.bounty,
            avoidGovType: this._newDest.avoidGovType,
            avoidSpecies: this._newDest.avoidSpecies,
            avoidSysList: this._newDest.avoidSysList,
            bonusSysList: this._newDest.bonusSysList,
            hiddenBounty: this._newDest.hiddenBounty,
            found: this._newDest.found,
            changed: true
        };
    
        delete this._newDest;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$declinePendingChange = function() {
        // remove the system mark
        mission.unmarkSystem({system:this._newDest.newDest, name:"epc_newDest"});
        this._informPlayerOfSystem = false;
        // just delete the details
        delete this._newDest;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$startTimers = function() {
        // check if we have any need to monitor for movement/attacks
        var bb = worldScripts.BulletinBoardSystem;
        var data = bb._data;
        var start = false;
        var found = false;
        // look for any passengers, and if we need to monitor for wild movement
        for (var i = 0; i < data.length; i++) {
            if (data[i].accepted === true && data[i].worldScript === this.name) {
                found = true;
                if (data[i].data.details.smooth === true) {start = true; break;}
            }
        }
        // set up the chat timer
        if (found === true && this._flags[8] === 1) {
            this._transmissions.length = 0;
            this._chatTimer = new Timer(this, this.$sendChatMessage, Math.floor(Math.random() * 120 + 60), Math.floor(Math.random() * 120) + 60);
            this.shipTargetDestroyed = this.$epc_shipTargetDestroyed;
            this.shipTakingDamage = this.$epc_shipTakingDamage;
        }
        // if we don't have a need for a wild movement monitor, exit here.
        if (start === false) return;
        // otherwise, set up the properties, events, fcb and monitor timer
        var p = player.ship;
        p.script._epc_wildMovement = 0;
        p.script._epc_wildMovementWarning = false;
        p.script._epc_attacked = 0;
        p.script._epc_stable = 0.0;
        // some ships will be sluggish, so "wild movement" isn't really possible for them
        // so max sure than wild movement will only be calculated for ships that have the possibility of it
        // for those with pitch/roll stats lower than these values, it won't trigger
        p.script._maxPitch = Math.max(0.75, p.maxPitch);
        p.script._maxRoll = Math.max(1, p.maxRoll);
        this.shipBeingAttacked = this.$epc_shipBeingAttacked;
        this._frameCallbackID = addFrameCallback(this.$checkForWildMovement.bind(this));
        this._monitorWildMovementTimer = new Timer(this, this.$monitorWildMovement.bind(this), 5, 5);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$stopTimers = function() {
        if (this._chatTimer && this._chatTimer.isRunning) {
            this._chatTimer.stop();
            this._chatTimer = null;
        }
    	if (this._changeTimer && this._changeTimer.isRunning) {
            this._changeTimer.stop();
            this._changeTimer = null;
        }
        if (this._monitorWildMovementTimer && this._monitorWildMovementTimer.isRunning) {
            this._monitorWildMovementTimer.stop();
            this._monitorWildMovementTimer = null;
        }
        if (this._complimentTimer && this._complimentTimer.isRunning) {
            this._complimentTimer.stop();
            this._complimentTimer = null;
        }
        if (this._damageTimer && this._damageTimer.isRunning) {
            this._damageTimer.stop();
            this._damageTimer = null;
        }
        if (this._impatientTimer && this._impatientTimer.isRunning) {
            this._impatientTimer.stop();
            this._impatientTimer = null;
        }
        delete this.shipTargetDestroyed;
        delete this.shipTakingDamage;
        this.$stopFrameCallback();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$stopFrameCallback = function() {
    	if (this._frameCallbackID && isValidFrameCallback(this._frameCallbackID)) {
    		removeFrameCallback(this._frameCallbackID);
    	}
        delete this._frameCallbackID;
        delete this.shipBeingAttacked;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$checkForWildMovement = function $checkForWildMovement(delta) {
        var p = player.ship;
        var ps = p.script;
        if (!p || p.isValid === false || !ps) return;
        // are we moving around a lot?
        if (ps._maxPitch - Math.abs(p.pitch) < 0.0001) {ps._epc_wildMovement += delta; ps._epc_stable = 0; return;}
        if (ps._maxRoll - Math.abs(p.roll) < 0.0001) {ps._epc_wildMovement += delta; ps._epc_stable = 0; return;}
        // monitor how long we've been flying in a stable way
        ps._epc_stable += delta;
        // every 30 seconds or so, bring down our wild movement amount
        if (ps._epc_stable > 15) {
            ps._epc_stable = 0;
            if (ps._epc_wildMovement > 0) {
                ps._epc_wildMovement -= 1;
            }
            if (ps._epc_attacked > 0) {
                ps._epc_attacked -= 1;
            }
            // reset the warning flag if we get back to 0 again
            if (ps._epc_wildMovement <= 0 || ps._epc_attacked <= 0) {
                ps._epc_wildMovementWarning = false;
                ps._epc_wildMovement = 0;
                ps._epc_attacked = 0;
            }
        }
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$monitorWildMovement = function $monitorWildMovement() {
        var p = player.ship;
        var ps = p.script;
        var ding = false;
        if (!p || p.isValid === false || !ps) {
            this._monitorWildMovementTimer.stop();
            return;
        }
        //log(this.name, "wm = " + ps._epc_wildMovement + ", lh " + ps._epc_attacked);
        // send warning after 15 seconds of wild movement or 20 laser hits
        if ((ps._epc_wildMovement > 15 || ps._epc_attacked > 20) && ps._epc_wildMovementWarning === false) {
            // find a contract who can act as spokesperson for anyone else.
            var bb = worldScripts.BulletinBoardSystem;
            var data = bb._data;
            for (var i = 0; i < data.length; i++) {
                var item = data[i];
                if (item.accepted === true && item.worldScript === this.name) {
                    var passenger = item.data.details;
                    if (passenger.smooth === true) {
                        //log(this.name, "sending wild movement warning for " + passenger.name);
                        this.$transmitMessageFromPassenger(passenger, "[passenger-response-not-comfortable-warning]");
                        break;
                    }
                }
            }
            ps._epc_wildMovementWarning = true;
            player.consoleMessage("");
            return;
        }
        // 30 seconds of wild movement or 40 hits
        if (ps._epc_wildMovement > 30 || ps._epc_attacked > 40) {
            ding = true;
            // reset the values so we don't keep sending the same message
            ps._epc_wildMovement = 0;
            ps._epc_attacked = 0;
        }
    
        if (ding === true) {
            // stop the frame callback - we're done
            this.$stopFrameCallback();
    
            var bb = worldScripts.BulletinBoardSystem;
            var data = bb._data;
            for (var i = 0; i < data.length; i++) {
                var item = data[i];
                if (item.accepted === true && item.worldScript === this.name) {
                    var passenger = item.data.details;
                    if (passenger.smooth === true) {
                        // oh dear
                        this.$transmitMessageFromPassenger(passenger, "[passenger-response-not-comfortable]");
    
                        item.payment = item.payment * 0.5; // 50% reduction
                        passenger.payment = item.payment;
                        passenger.smooth = false; // turn this off now, so it can't happen twice
                        item.data.details = passenger;
                        // find and update the actual passenger record
                        for (var j = 0; j < p.passengers.length; j++) {
                            var psngr = p.passengers[j];
                            if (psngr.name === passenger.name && psngr.destination === item.destination) {
                                //log(this.name, "failed for " + psngr.name);
                                // remove the old passenger
                                p.removePassenger(passenger.name)
                                // add them back in with the new details
                                this._internal = true;
                                this.$disableEmails();
                                this._sendEmailType = 3;
                                var result = p.addPassenger(passenger.name, item.source, item.destination, passenger.deadline, passenger.payment, passenger.advance, passenger.risk);
                                this._sendEmailType = 0;
                                this.$enableEmails();
                                this._internal = false;
                                break;
                            }
                        }
                    }
                }
            }
        }
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$ovr_bountySystem_uncoverBounties = function(source) {
        var p = player.ship;
        var bb = worldScripts.BulletinBoardSystem;
        var data = bb._data;
        var done = false;
        // only do the check if the source of the scan is a police vessel, and we're in a high-level government system
        // (never in an anarchy, but more and more likely the higher the government level)
        if ((Math.random() * 7) < system.government) {
            for (var i = 0; i < data.length; i++) {
                var item = data[i];
                if (item.accepted === true && item.worldScript === "EnhancedPassengerContracts") {
                    var passenger = item.data.details;
                    // check for any passengers carrying a bounty - and potentially add a bounty to the player
                    if (passenger.hiddenBounty > 0 || passenger.bounty > 0) {
                        // found one!
                        // ding the player and inform them
                        if (done === false && passenger.found === false && source.isPolice) {
                            done = true;
                            player.ship.setBounty(player.bounty + Math.floor(Math.random() * 5 + 2), "transporting offenders");
                            source.commsMessage(expandDescription("[police-comms-offender-passenger]"), player.ship);
                        }
                        passenger.found = true;
                        // add a custom display item to display the passenger bounty
                        if (passenger.bounty === 0) {
                            passenger.bounty = passenger.hiddenBounty;
                            passenger.hiddenBounty = 0;
                            item.customDisplayItems.push({heading:expandDescription("[epc_contract-bounty]"), value:passenger.bounty});
                            item.data.details = passenger;
                        }
                    }
                    // check for any passengers who don't want to be scanned
                    if (passenger.noScan === true) {
                        // oh dear
                        var epc = worldScripts.EnhancedPassengerContracts;
                        epc.$transmitMessageFromPassenger(passenger, "[passenger-response-got-scanned]");
    
                        item.payment = item.payment * 0.25; // 75% reduction
                        passenger.payment = item.payment;
                        passenger.noScan = false; // turn this off now, so it can't happen twice
                        for (var j = 0; j < p.passengers.length; j++) {
                            var psngr = p.passengers[j];
                            if (psngr.name === passenger.name && psngr.destination === item.destination) {
                                // remove the old passenger
                                p.removePassenger(passenger.name)
                                // add them back in with the new details
                                epc._internal = true;
                                epc.$disableEmails();
                                epc._sendEmailType = 3;
                                var result = p.addPassenger(passenger.name, item.source, item.destination, passenger.deadline, passenger.payment, passenger.advance, passenger.risk);
                                epc._sendEmailType = 0;
                                epc.$enableEmails();
                                epc._internal = false;
                                break;
                            }
                        }
                        item.data.details = passenger;
                    }
                }
            }
        }
    
        // do the standard uncover bounties
        this.$uncoverBounties_endpoint(source);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$doWarrantCheck = function(missID) {
        var p = player.ship;
        var bb = worldScripts.BulletinBoardSystem;
        var item = bb.$getItem(missID);
        var found = false;
    
        var passenger = item.data.details;
        if (passenger.hiddenBounty > 0) {
            passenger.bounty = passenger.hiddenBounty;
            passenger.hiddenBounty = 0;
            item.customDisplayItems.push({heading:expandDescription("[epc_contract-bounty]"), value:passenger.bounty});
            player.consoleMessage(expandDescription("[epc_reveal-bounty]", {name:passenger.name, bounty:passenger.bounty}), 5);
            found = true;
        } else {
            player.consoleMessage(expandDescription("[epc_reveal-no-bounty]", {name:passenger.name}), 5);
        }
        item.payment = item.payment * 0.8; // 20% reduction
        passenger.payment = item.payment;
        item.data.details = passenger;
        for (var j = 0; j < p.passengers.length; j++) {
            var psngr = p.passengers[j];
            if (psngr.name === passenger.name && psngr.destination === item.destination) {
                // remove the old passenger
                p.removePassenger(passenger.name)
                // add them back in with the new details
                this._internal = true;
                this.$disableEmails();
                if (found === false) this._sendEmailType = 4;
                var result = p.addPassenger(passenger.name, item.source, item.destination, passenger.deadline, passenger.payment, passenger.advance, passenger.risk);
                this._sendEmailType = 0;
                this.$enableEmails();
                this._internal = false;
                break;
            }
        }
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$warrantCheckAvailable = function(missID) {
        var bb = worldScripts.BulletinBoardSystem;
        var item = bb.$getItem(missID);
        if (item.accepted === false) return "only when contract accepted";
        if (item.data.details.bounty > 0) return "bounty already visible";
        return "";
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$resetFoundFlag = function() {
        var bb = worldScripts.BulletinBoardSystem;
        var data = bb._data;
        for (var i = 0; i < data.length; i++) {
            var item = data[i];
            if (item.accepted === true && item.worldScript === this.name) {
                item.data.details.found = false;
            }
        }    
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // turns off contract emails, if they aren't already
    this.$disableEmails = function() {
        if (worldScripts.GalCopAdminServices) {
            var ga = worldScripts.GalCopAdminServices;
            this._emailDisable = ga._disableContracts;
            ga._disableContracts = true;
        }
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // turns on contracts emails, if it was previously
    this.$enableEmails = function() {
        if (worldScripts.GalCopAdminServices) {
            var ga = worldScripts.GalCopAdminServices;
            ga._disableContracts = this._emailDisable;
        }
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$customPassengerContractEmail = function(contract) {
    	var w = worldScripts.EmailSystem;
        if (!w || this._emailDisable === true) return;
    
        // look up the BB item
        var bb = worldScripts.BulletinBoardSystem;
        var data = bb._data;
        var item = null;
        for (var i = 0; i < data.length; i++) {
            if (data[i].accepted === true && 
                data[i].worldScript === this.name && 
                data[i].destination === contract.destination && 
                data[i].payment === contract.fee && 
                data[i].source === contract.start) {
    
                item = data[i].data.details;
            }
        }
        if (item === null) return;
    
        var extras = this.$buildExtrasText(item);
        var sndr = expandDescription("[passenger-contract-sender]");
        var subj = "Accepted contract: " + contract.name;
        var msg = expandDescription("[epc_email-passenger-contract-start]",
            {contractname:contract.name,
            systemname:System.systemNameForID(contract.destination),
            time:global.clock.clockStringForTime(contract.arrival_time),
            fee:formatCredits(contract.fee, true, true),
            extras:extras
        });
    
    	if (sndr != "") {
    		w.$createEmail({sender:sndr,
    			subject:subj,
    			date:global.clock.adjustedSeconds,
    			message:msg
    		});
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$contractAdjustmentEmail = function(contract) {
    	var w = worldScripts.EmailSystem;
        if (!w || this._emailDisable === true) return;
    
        // look up the BB item
        var bb = worldScripts.BulletinBoardSystem;
        var data = bb._data;
        var item = null;
        for (var i = 0; i < data.length; i++) {
            if (data[i].accepted === true && 
                data[i].worldScript === this.name && 
                data[i].destination === contract.destination && 
                data[i].payment === contract.fee && 
                data[i].source === contract.start) {
    
                item = data[i].data.details;
            }
        }
        if (item === null) return;
    
        var adjType = "";
        switch (this._sendEmailType) {
            case 2:
                adjType = "[epc_email-contract-amend-destination]";
                break;
            case 3:
                adjType = "[epc_email-contract-amend-payment]";
                break;
            case 4:
                adjType = "[epc_email-contract-amend-privacy]";
                break;
        }
        var extras = this.$buildExtrasText(item);
        var sndr = expandDescription("[passenger-contract-sender]");
        var subj = "Amended contract: " + contract.name;
        var msg = expandDescription(adjType,
            {contractname:contract.name,
            systemname:System.systemNameForID(contract.destination),
            time:global.clock.clockStringForTime(contract.arrival_time),
            fee:formatCredits(contract.fee, true, true),
            extras:extras
        });
    
    	if (sndr != "") {
    		w.$createEmail({sender:sndr,
    			subject:subj,
    			date:global.clock.adjustedSeconds,
    			message:msg
    		});
    	}
        
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$buildExtrasText = function(item) {
        var govs = new Array();
        for (var i = 0; i < 8 ; i++) govs.push(String.fromCharCode(i));
    
        var extras = "";
        if (item.hasOwnProperty("smooth") && item.smooth === true) {
            extras += expandDescription("[epc_contract-no-wildness]");
        }
        if (item.hasOwnProperty("noScan") && item.noScan === true) {
            extras += expandDescription("[epc_contract-no-scan]");
        }
        if (item.hasOwnProperty("avoidGovType") && item.avoidGovType >= 0) {
            extras += expandDescription("[epc_contract-avoid-gov]", {govType:this._govTypeNames[item.avoidGovType]});
        }
        if (item.hasOwnProperty("avoidSpecies") && item.avoidSpecies != "") {
            extras += expandDescription("[epc_contract-avoid-species]", {species:item.avoidSpecies});
        }
        if (item.hasOwnProperty("avoidSysList") && item.avoidSysList.length > 0) {
            var listText = "";
            for (var i = 0; i < item.avoidSysList.length; i++) {
                var sys = System.infoForSystem(galaxyNumber, item.avoidSysList[i]);
                listText += "\n -- " + sys.name + " (" + govs[sys.government] + " TL" + (sys.techlevel + 1) + ")";
            }
            extras += expandDescription("[epc_contract-avoid-systems]", {syslist:listText});
        }
        if (item.hasOwnProperty("bonusSysList") && item.bonusSysList.length > 0) {
            var listText = "";
            for (var i = 0; i < item.bonusSysList.length; i++) {
                var sys = System.infoForSystem(galaxyNumber, item.bonusSysList[i]);
                listText += "\n -- " + sys.name + " (" + govs[sys.government] + " TL" + (sys.techlevel + 1) + ")";
            }
            extras += expandDescription("[epc_contract-bonus-systems]", {bonus:formatInteger(this._bonusSystemLoading * 100) + "% of payment", syslist:listText});
        }
        return extras;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$sendChatMessage = function $sendChatMessage() {
        // don't send any messages if there is a pending contract change
        if (this._newDest) return;
        // don't sent chat messages in Interstellar space
        if (system.isInterstellarSpace) return;
        // overheating
        if (player.ship.temperature >= 0.65) {
            if (Math.random() < 0.5) {
                this.$transmitMessageFromPassenger(this.$pickRandomPassenger(), "[passenger-overheating]");
            }
            // if the ship is overheating, don't continue and potentially send a small talk message
            return;
        }
        // low energy
        if (player.ship.energy < 32) {
            if (Math.random() < 0.5) {
                this.$transmitMessageFromPassenger(this.$pickRandomPassenger(), "[passenger-low-energy-concern]");
            }
            return;
        }
        // low altitude
        if (player.alertAltitude) {
            // altitude could be low near the sun, too, but hopefully the temperature message above will prevent conflicts
            if (Math.random() < 0.5) {
                this.$transmitMessageFromPassenger(this.$pickRandomPassenger(), "[passenger-low-altitude-concern]");
            }
            return;
        }
        // don't send small talk during read alert
        if (player.alertCondition === 3) return;
        // small talk
        if (Math.random() < 0.2) {
            // pick a passenger
            var passenger = this.$pickRandomPassenger();
            if (passenger) {
                // set up any mission variables that might be used by the chat text
                var ds = this.$getSystemsByDescription("disease");
                missionVariables.diseaseSystem = ds[Math.floor(Math.random() * ds.length)].name;
                var es = this.$getSystemsByDescription("earthquake");
                missionVariables.earthquakeSystem = es[Math.floor(Math.random() * es.length)].name;
                missionVariables.finalDestination = System.systemNameForID(passenger.destination);
                var shipTypes = ["Cobra Mark III","Boa","Boa Class Cruiser","Anaconda","Fer-de-Lance"];
                for (var i = 0; i < shipTypes.length; i++) {
                    if (player.ship.name !== shipTypes[i]) {
                        missionVariables.altShipName = shipTypes[i];
                        break;
                    }
                }
                var data = expandDescription("[passenger-chat]");
                var idx = parseInt(data.split("|")[0]);
                var msg = data.split("|")[1];
                // clean up the mission variables we added
                delete missionVariables.altShipName;
                delete missionVariables.finalDestination;
                delete missionVariables.diseaseSystem;
                delete missionVariables.earthquakeSystem;
                if (this._transmissions.indexOf(idx) === -1) {
                    this.$transmitMessageFromPassenger(passenger, msg);
                    this._transmissions.push(idx);
                }
            }
        }
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$niceShotMessage = function $niceShotMessage() {
        // pick a passenger
        this.$transmitMessageFromPassenger(this.$pickRandomPassenger(), "[passenger-nice-shooting]");
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$damageMessage = function $damageMessage() {
        this.$transmitMessageFromPassenger(this.$pickRandomPassenger(), "[passenger-damage-concern]");
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$impatientMessage = function $impatientMessage() {
        this.$transmitMessageFromPassenger(this.$pickRandomPassenger(), "[passenger-impatient]");
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$interstellarSpaceMessage = function $interstellarSpaceMessage() {
        this.$transmitMessageFromPassenger(this.$pickRandomPassenger(), "[passenger-interstellar]");
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$pickRandomPassenger = function() {
        var p = player.ship;
        return p.passengers[Math.floor(Math.random() * p.passengers.length)];
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$transmitMessageFromPassenger = function(passenger, message) {
        if (!passenger) return;
        if (message.indexOf("[") === 0) message = expandDescription(message);
    
        var notifySound = new SoundSource;
    	notifySound.sound = "[@beep]";
    	notifySound.play();
    
        player.commsMessage("Message from passenger " + passenger.name + ": " + message, 5);
        // add to comms log (if installed) so that it doesn't disappear with other messages
        if (worldScripts.CommsLogMFD) {
            worldScripts.CommsLogMFD.commsMessageReceived(message, null, "Passenger " + passenger.name);
        }
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$getSystemsByDescription = function(options) {
    	var planets = SystemInfo.filteredSystems(this, function(other) {
    		return (other.description.indexOf(options) >= 0 && other.systemID != system.ID);
    	});
    	return planets;
    }
    
    Scripts/epc_passenger_mfd.js
    "use strict";
    this.name        = "EnhancedPassengerMFD";
    this.author      = "phkb";
    this.copyright   = "2018 phkb";
    this.description = "Controls the output the MFD";
    this.licence     = "CC BY-NC-SA 4.0";
    
    this._lineLength = 14;
    this._mfdID = -1;
    this._offerTimeout = 180;		// number of seconds to wait for response (3 mins, to allow for player research time)
    this._offerTimer = null;		// timer for mission offer
    this._timeoutCounter = 0;
    this._currentMFDText = "";
    this._displayPoint = 0;
    this._holdData = {};
    this._subMessageTimer = null;
    this._originalMsg = "";
    this._final = false;
    this._declineMessageTimer = null;
    this._declineType = false;
    this._acceptMessageTimer = null;
    this._disableReplyOptions = false;
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipWillEnterWitchspace = function(cause, destination) {
    	if (this._offerTimer && this._offerTimer.isRunning) {
    		this.$stopTimers();
    		worldScripts.EnhancedPassengerContracts.$declinePendingChange();
    		this.$removeBCReplyOptions();
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipWillDockWithStation = function(station) {
    	if (this._simulator === true) return;
    	if (this._offerTimer && this._offerTimer.isRunning) {
    		// make sure we don't leave any half-accepted change hanging around
    		this.$stopTimers();
    		worldScripts.EnhancedPassengerContracts.$declinePendingChange();
    		epc.$removeBCReplyOptions();
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.shipDied = function(whom, why) {
    	this.$stopTimers();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$stopTimers = function $stopTimers() {
    	if (this._offerTimer && this._offerTimer.isRunning) {
    		this._offerTimer.stop();
    		this._offerTimer = null;
    	}
    	if (this._subMessageTimer && this._subMessageTimer.isRunning) {
    		this._subMessageTimer.stop();
    		this._subMessageTimer = null;
    	}
    	if (this._declineMessageTimer && this._declineMessageTimer.isRunning) {
    		this._declineMessageTimer.stop();
    		this._declineMessageTimer = null;
    	}
    	if (this._acceptMessageTimer && this._acceptMessageTimer.isRunning) {
    		this._acceptMessageTimer.stop();
    		this._acceptMessageTimer = null;
    	}
    	this._declineType = false;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // breaks text up into appropriate lengths for an MFD
    this.$processText = function $processText(msg) {
    	var line = "";
    	// the replace here should ensure all newline commands are at the end of a word, and not in the middle of one
    	var words = msg.replace(new RegExp("\n", 'g'), "\n ").split(" ");
    	var word = "";
    	var output = "";
    
    	for (var i = 0; i < words.length; i++) {
    		word = words[i].trim();
    		// make sure we have a word to add before trying to add it
    		if (word !== "") {
    			if (defaultFont.measureString(line + " " + word) <= this._lineLength) {
    				line += (line === "" ? "" : " ") + word;
    				// if the word ended in a newline command, add the line to the output and reset
    				if (word.indexOf("\n", word.length - 2) !== -1) {
    					output += line;
    					line = "";
    				}
    			} else {
    				output += line + "\n";
    				line = word;
    				// if the word ended in a newline command, add the line to the output and reset
    				if (word.indexOf("\n", word.length - 2) !== -1) {
    					output += line;
    					line = "";
    				}
    			}
    		}
    	}
    	output += line;
    	
    	// update MFD so that new text goes on the bottom.
    	var lines = this._currentMFDText.split("\n");
    	if (this._currentMFDText === "") lines = [];
    	var newlines = output.split("\n");
    	
    	for (var i = 0; i < newlines.length; i++) {
    		lines.push(newlines[i]);
    	}
    	
    	var final = "";
    	if (lines.length <= 10) {
    		final = lines.join("\n");
    	} else {
    		for (var i = lines.length - 10; i < lines.length; i++) {
    			final += (final === "" ? "" : "\n") + lines[i];
    		}
    	}
    	
    	this._currentMFDText = final;
    	
    	return final;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // updates the text of the MFD
    this.$updateMFD = function $updateMFD(msg) {
    	var p = player.ship;
    
    	if (this._originalMsg === "") this._originalMsg = msg;
    	
    	var msglist = msg.split("\n");
    	var currmsg = msglist[this._displayPoint];
    	// if this is the final message, just select the last one
    	if (this._final === true) {
    		currmsg = msglist[msglist.length - 1];
    	}
    	
    	// if the hud is hidden don't try an update - it will get confusing as to which mfd slot is open or not.
    	if (p.hudHidden === false) {
            // find the slot currently set for this MFD
            this.$findMFDID();
            // if we haven't got a set slot (this._mfdID === -1) or the set slot we had is now unavailable...
            if (this._mfdID === -1 ||
                (p.multiFunctionDisplayList[this._mfdID] && p.multiFunctionDisplayList[this._mfdID] !== "" && p.multiFunctionDisplayList[this._mfdID] !== this.name)) {
                // find a free slot
                // first, make sure we reset our slot id marker (in the case where the previous one is in use)
                this._mfdID = -1;
                // search for a free slot
                for (var i = 0; i < p.multiFunctionDisplayList.length; i++) {
                    if (!p.multiFunctionDisplayList[i] || p.multiFunctionDisplayList[i] === "") {
                        this._mfdID = i;
                        break;
                    }
                }
            }
    		
    		// we have a free slot, so force the mfd to display
    		if (this._mfdID !== -1) {
    			// set the text in the MFD
    			var output = this.$processText(currmsg);
    			p.setMultiFunctionText(this.name, output, false);
    
    			p.setMultiFunctionDisplay(this._mfdID, this.name);
    		} else {
    			if (useComms === true && src && src.isInSpace && player.ship.position.distanceTo(src) < player.ship.scannerRange) {
    				// yay! comms is working!
    				src.commsMessage(currmsg, p);
    			} else {
    				// oh well, make it a console message
    				player.consoleMessage(currmsg, 20);
    			}
    		}
    	}
    	
    	if (this._final === false) {
    		this._displayPoint += 1;
    		if (this._displayPoint < msglist.length) {
    			this._holdData.msg = msg;
    			this._subMessageTimer = new Timer(this, this.$processNextMessage, 5, 0);
    			// return at this point so the accept/reject items only appear when all the messages are displayed.
    			return;
    		}
    	}
    
    	this._holdData = {};
    	if (this._disableReplyOptions === false) this.$displayBCReplyOptions();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // re-run the update function so the next message in the list can be displayed
    this.$processNextMessage = function $processNextMessage() {
    	this.$updateMFD(this._holdData.msg);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // records the index of the MFD that currently holds the damage report mfd
    this.$findMFDID = function $findMFDID() {
    	var p = player.ship;
    	for (var i = 0; i < p.multiFunctionDisplayList.length; i++) {
    		if (p.multiFunctionDisplayList[i] === this.name) this._mfdID = i;
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // hides all instances of the damage report MFD
    this.$autoHideMFD = function $autoHideMFD() {
    	var p = player.ship;
    	if (p && p.multiFunctionDisplayList) {
    		for (var i = 0; i < p.multiFunctionDisplayList.length; i++) {
    			if (p.multiFunctionDisplayList[i] === this.name) {
    				p.setMultiFunctionDisplay(i, "");
    			}
    		}
    	}
    }
    
    //-------------------------------------------------------------------------------------------------------------
    // add the secondary mission accept/decline options to broadcast comms
    this.$displayBCReplyOptions = function $displayBCReplyOptions() {
    	var bc = worldScripts.BroadcastCommsMFD;
    	if (bc.$checkMessageExists("epc_accept_change") === true || bc.$checkMessageExists("epc_decline_change") === true) return;
    	bc.$createMessage({messageName:"epc_accept_change", callbackFunction:this.$acceptContractChange.bind(this), displayText:"(* Accept Contract Change *)", messageText:"", transmissionType:"broadcast", deleteOnTransmit:true, delayCallback:1});
    	bc.$createMessage({messageName:"epc_decline_change", callbackFunction:this.$declineContractChange.bind(this), displayText:"(* Decline Contract Change *)", messageText:"", transmissionType:"broadcast", deleteOnTransmit:true, delayCallback:1});
    	// reset the timer counter
    	this._timeoutCounter = 0;
    	// start a timer to wait for player response
    	this._offerTimer = new Timer(this, this.$removeOffer, 1, 1);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$acceptContractChange = function $acceptContractChange() {
        player.consoleMessage("Contract change accepted");
        if (this._mfdID !== -1) {
            this.$updateMFD(this._originalMsg + "\n>> " + expandDescription("[epc_accept-change]"));
        }
    	worldScripts.EnhancedPassengerContracts.$acceptPendingChange();
    	this.$removeBCReplyOptions();
    	this._acceptMessageTimer = new Timer(this, this.$sendAcceptComms, 5, 0);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$sendAcceptComms = function $sendAcceptComms() {
        if (this._mfdID !== -1) {
            this._final = true;
            this._disableReplyOptions = true;
            this.$updateMFD(this._originalMsg + "\n" + expandDescription("[epc_accept-response]"));
        }
    	// turn off the MFD (if it was used)
    	this.$turnOffMFD();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$declineContractChange = function $declineContractChange(send_message) {
        this._declineType = false;
        if (send_message == null) {
            this._declineType = true;
            player.consoleMessage("Contract change declined");
            if (this._mfdID !== -1) {
                this.$updateMFD(this._originalMsg + "\n>> " + expandDescription("[epc_decline-change]"));
            }
        }
    	this.$removeBCReplyOptions();
    	this._declineMessageTimer = new Timer(this, this.$sendDeclineComms, 5, 0);
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$sendDeclineComms = function $sendDeclineComms() {
        if (this._declineType === true) {
            if (this._mfdID !== -1) {
                this._final = true;
    			this._disableReplyOptions = true;
                this.$updateMFD(this._originalMsg + "\n" + expandDescription("[epc_decline-response]"));
            }
        }
    	worldScripts.EnhancedPassengerContracts.$declinePendingChange();
    	// turn off the MFD (if it was used)
    	this.$turnOffMFD();
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$turnOffMFD = function $turnOffMFD() {
    	// turn off the MFD (if it was used)
    	this._currentMFDText = "";
    	this._originalMsg = "";
    	this._final = false;
    	this._displayPoint = 0;
    	if (this._mfdID !== -1) {
    		if (this._subMessageTimer && this._subMessageTimer.isRunning) this._subMessageTimer.stop();
    		this._subMessageTimer = new Timer(this, this.$autoHideMFD, 3, 0);
    	}
    	this._disableReplyOptions = false;
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$removeBCReplyOptions = function $removeBCReplyOptions() {
    	if (this._offerTimer && this._offerTimer.isRunning) {
    		this._offerTimer.stop();
    		this._offerTimer = null;
    	}
    	var bc = worldScripts.BroadcastCommsMFD;
    	if (bc.$checkMessageExists("epc_accept_change") === true) bc.$removeMessage("epc_accept_change");
    	if (bc.$checkMessageExists("epc_decline_change") === true) bc.$removeMessage("epc_decline_change");
    }
    
    //-------------------------------------------------------------------------------------------------------------
    this.$removeOffer = function $removeOffer() {
    	this._timeoutCounter += 1;
    	// have we timed out
    	if (this._timeoutCounter >= this._offerTimeout) {
    		this.$declineContractChange(true);
    		return;
    	}
    }