Back to Index Page generated: Jun 25, 2025, 7:13:19 AM

Expansion Home System

Content

Manifest

from Expansion Manager's OXP list from Expansion Manifest
Description Adds some flavour text to a variety of messages received by the player in systems designated as their home. As well, there are some other benefits achievable the more players visit their home system. Adds some flavour text to a variety of messages received by the player in systems designated as their home. As well, there are some other benefits achievable the more players visit their home system.
Identifier oolite.oxp.phkb.HomeSystem oolite.oxp.phkb.HomeSystem
Title Home System Home System
Category Ambience Ambience
Author phkb, UK_Eliter, Cody, Diziet Sma phkb, UK_Eliter, Cody, Diziet Sma
Version 0.15 0.15
Tags
Required Oolite Version
Maximum Oolite Version
Required Expansions
Optional Expansions
Conflict Expansions
Information URL https://wiki.alioth.net/index.php/Home_System n/a
Download URL https://wiki.alioth.net/img_auth.php/f/fb/HomeSystem.oxz n/a
License CC-BY-NC-SA 4.0 CC-BY-NC-SA 4.0
File Size n/a
Upload date 1748848528

Documentation

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

readme.txt

Home System
By phkb (Nick Rogers), UK_Eliter, Diziet Sma and Cody.

Overview
========
This OXP seeks to add some flavour to systems that the player has made their home. Various entities in the system will now communicate spontaneously with the player, enhancing the idea that the player is well-known to system inhabitants. Benefits will also be given to the player the more they visit the system.

Operation
=========
Up to 3 systems in each sector can be declared as "home". To make a system home, purchase the "Home System Privileges" item from the F3 Equip Ship screen. If you have already purchased 3 Home System Privileges and decide later on to move homes, you can either purchase the "Remove Home System Privileges" item in one of the systems where you have already made your home, or you can simply buy a new Home System Privileges in the new system, and if your maximum has been reached, you will be asked to select one of your current home systems to remove.

Once set, additional messages will be sent to the player by the station (when you are granted docking clearance, after you dock, and after you launch), police vessels, and even pirates.

There are some benefits from making a system your home that will come into play the more you visit your home system:

1. Police will always side with the player in clean-v-clean fights.
2. If you have the "Docking Fees" OXP installed, fees will be waived at all stations in your home system.
3. Purchasing goods from the commodities market will return a small bonus.
4. Purchasing equipment will give you a small rebate.
5. Purchasing a new ship will give you a small rebate.
6. Clearing of any offender status.
7. Auto-refuelling at aligned stations.
8. Additional bonus amounts on missions from "GalCop Missions" OXP (if installed).
9. Special parcel and passenger contracts.

More benefits may be added in future versions.

You also have the opportunity to invest in your home system, which can accelerate your standing with the system and provide access to more home system benefits faster. Investment options can be purchased for 10000cr.

Home systems will be marked on the galactic chart with a purple diamond shape. There will also be a note on the F7 System Data screen.

Note: Firing on stations or police vessels in a system designated as your home will result in all your home system privileges being revoked.

Investment level
----------------
The more you invest in a Home System, the fast you will achieve higher levels of rewards. To see your current level of investment, go to the F7 System Information screen for your Home System, and the investment level will be shown. There are 8 levels of investment:

    Donor           Achieved when 1 investment package has been purchased.
    Backer          Achieved when up to 3 investment packages have been purchased.
    Supporter       Achieved when up to 5 investment packages have been purchased.
    Contributor     Achieved when up to 8 investment packages have been purchased.
    Sponsor         Achieved when up to 11 investment packages have been purchased.
    Benefactor      Achieved when up to 14 investment packages have been purchased.
    Patron          Achieved when up to 18 investment packages have been purchased.
    Champion        Achieved when more than 18 investment packages have been purchased.

Each investment package you purchase counts as 2 docks. That is, it's as if you have docked 2 more times in that system.

Diplomacy
=========
If the Diplomacy OXP is installed, purchasing Investment options for your home system will eventually grant you citizenship.

License
=======
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/

Thanks to UK_Eliter for his code analysis and adding his ideas into the mix to make the pack better.
Thanks to Diziet Sma for getting the team together.
With special thanks to Cody for the original idea. You rock, El Viejo!

Home image from http://simpleicon.com/home-1.html.
Question mark image from http://simpleicon.com/question_mark_1.html

Version History
===============
0.15
- Moved all text into descriptions.plist for easier localisation.
- Added the "Home System Handbook" to the Ship's Library (if installed).

0.14
- Small code tweaks.

0.13
- Investments now have a bigger impact on access to benefits.
- The level of investment is now visible of the F7 system info screen, as well as in the "Reputation and Awards" Interface screen (if the "GalCop Missions" OXP is installed).
- (If the "Diplomacy" OXP is installed) Purchasing investment options for a home system will eventually grant you citizenship.
- Home Systems are now marked on the GalCop Station Docking Counts screen.
- Fixed issue where Home System Investment could be offered for sale when the player has no home system chosen.
- Code refactoring.

0.12
- *Really* fixed issue where reply messages with BroadcastComms could end up with a duplicated ID.

0.11
- Fixed issue where reply messages with BroadcastComms could end up with a duplicated ID.

0.10
- Benefit levels are now adjusted based on a systems government, economy and tech level. 
- Investment purchase notification emails for systems other than the current system will now arrive later, rather than instantly.

0.9
- Fixed issue with incorrect emails being sent when purchasing investment option.
- Split out investment option counters, so it's independent of dock counts.

0.8
- Added "Invest in home system" item, to allow players to accelerate their status with their home system.

0.7
- Code changes as suggested by UK_Eliter.
- Fixed issue with home system notification not appearing on F7 screen in all setups.
- Fixed broken priority docking system.
- Fixed issue with offender clearance message being shown when player is not an offender.

0.6
- Disabled docking and launching messages when using the combat simulator.
- Disabled docking messages after using an escape pod.
- Added an F4 screen showing the players number of dockings at GalCop stations.

0.5
- Home systems will now be marked with a purple diamond shape on the galactic chart, and a note will be added to the System Data screen.
- Added link to GalCop Missions to allow an increase in mission bonus value as a new benefit.
- Added special, high-value parcel and passenger contracts as a new benefit.
- Added free, automatic refuelling as a new benefit.
- Police and pirate comms messages now shouldn't happen when they're in the middle of a fight.
- Fixed errors resulting from not having Email System OXP installed.
- Farewell emails will now have their point-of-origin for the email trace set to the appropriate system.
- Set maximum number of home systems to 3.

0.4
- Added some additional benefits (increased rebate on equipment and ship purchases).
- Added email notification of new benefits achieved.
- If the player commits a serious offence in a home system, as well as their privileges being revoked, they will also have their reputation removed (so if they repurchase the privileges, they will have to build up their reputation to recover their benefits).
- Added a farewell email message, for when the player decides to remove their home system privileges.
- Fixed bug where an attack on police or stations when the player doesn't have home system privileges would still send a message about it being revoked.

0.3
- Changed installation time for equipment items to be 15 minutes.
- Fixed issue where the system was not recording the docking counts correctly.
- Added new benefit: clearing offender status (but not fugitive status).

0.2
- Reduced maximum number of home systems per sector to 2.
- Added functionality to ensure police will always side with the player in clean-v-clean fights.
- Added equipment and ship rebates.
- Added levels of benefit, requiring the player to visit the system a certain number of times before benefits come online.
- Added extra text variations.

0.1
- Initial alpha release.

Equipment

Name Visible Cost [deci-credits] Tech-Level
Home System Privileges no 150000 1+
Home System Privileges no 500000 1+
Invest in Home System no 100000 1+
Home System Privileges no 250000 1+
Remove Home System Privileges no 500 1+

Ships

This expansion declares no ships.

Models

This expansion declares no models.

Scripts

Path
Scripts/home_system.js
(
    function () {

        "use strict";
        this.name = "HomeSystem";
        this.author = "phkb";
        this.copyright = "2017 phkb";
        this.description = "Controls the output of home system messages to the player, as well as applying home system benefits to the player.";
        this.license = "CC BY-NC-SA 4.0";

        /*
            TODO:
                Areas to add text to:
                    - Traders? probably not
                Additional expenses:
                    - ongoing maintenance if visits per month is less than X?
                        slugging the player at random intervals seems cheap
                    - or reduce loyalty value if it doesn't increase steadily? (ie degrade reputation)
                    - sponsoring convoys heading to system by providing seed capital
                        force player to be involved or decline with a mission screen, or leave it as a mission via BB?
                        random/semi-random result?
                        could player be employed as an escort to ensure mission success
                    - 
                Special missions
                    Collect specialty from another system and bring it back?
                    Transport trade delegation to nearby system
                        Passenger cabin?
                Additional benefits:
                    bonus equipment
                    ?
        */

        this._maxHomeSystems = 3;
        this._homeSystems = [[], [], [], [], [], [], [], []];
        this._dockCounts = [{}, {}, {}, {}, {}, {}, {}, {}];
        this._investCounts = [{}, {}, {}, {}, {}, {}, {}, {}];
        this._messageNotification = {};
        this._messageTime = {};
        this._dockingStation = null;
        this._messageTimer = null;
        this._dockTimer = null;
        this._dockAvail = false;
        this._dockMessage = 0;
        this._systemCounted = false;
        this._playerRoles = [];
        this._purchasePrice = 0;
        this._simulator = false;
        this._stationTypes = ["galcop"]; // just for galcop stations, or for any non-pirate station?
        // this controls how many times the player must visit the system (and dock at one of the station types above) in order to get that benefit
        this._levels = {
            base: 0,
            police: 2,
            missions: 5,
            pirate: 6,
            dock_fee: 10,
            dock_priority: 15,
            clear_offender: 20,
            fuel: 25,
            trade: 30,
            equip_rebate: 40,
            ship_rebate: 50,
            contracts: 60,
            missions_2: 75,
            equip_rebate_2: 100,
            ship_rebate_2: 150
        };

        this._trueValues = ["yes", "1", 1, "true", true];

        //-------------------------------------------------------------------------------------------------------------
        this.startUpComplete = function () {
            if (worldScripts["oolite-system-data-config"] && worldScripts["CommpressedF7Layout"]) {
                worldScripts["oolite-system-data-config"].addChangeCallback(this.name, "$addInfoToSystemDataScreen");
                delete this.guiScreenWillChange;
                delete this.guiScreenChanged;
                delete this.infoSystemWillChange;
            }

            if (missionVariables.HomeSystems) this._homeSystems = JSON.parse(missionVariables.HomeSystems);
            if (missionVariables.HomeSystem_DockCounts) this._dockCounts = JSON.parse(missionVariables.HomeSystem_DockCounts);
            if (missionVariables.HomeSystem_InvestCounts) this._investCounts = JSON.parse(missionVariables.HomeSystem_InvestCounts);
            if (missionVariables.HomeSystem_Messages) this._messageNotification = JSON.parse(missionVariables.HomeSystem_Messages);
            if (missionVariables.HomeSystem_Counted) this._systemCounted = (this._trueValues.indexOf(missionVariables.HomeSystem_Counted) >= 0 ? true : false);
            if (missionVariables.HomeSystem_PlayerRoles) this._playerRoles = JSON.parse(missionVariables.HomeSystem_PlayerRoles);

            // testing
            //this._homeSystems[galaxyNumber].push(240);
            //this._dockCounts[galaxyNumber][240] = 20;
            //this._investCounts[galaxyNumber][240] = 4;
            //this._homeSystems[galaxyNumber].push(1);
            //this._homeSystems[galaxyNumber].push(2);
            //this._homeSystems[galaxyNumber].push(3);
            //player.ship.fuel = 0;
            //this._homeSystems[galaxyNumber].push(system.ID);
            //this._dockCounts[galaxyNumber][2] = 10;
            //this._dockCounts[galaxyNumber][3] = 20;
            //this._dockCounts[galaxyNumber][system.ID] = 100;
            //this._dockCounts[galaxyNumber] = {};
            //for (let i = 0; i < 256; i++) {
            //    this._dockCounts[galaxyNumber][i] = Math.floor(Math.random() * 1000 + 100);
            //}

            // make sure the current system gets logged if this is the first time we've been run
            this.shipDockedWithStation(player.ship.dockedStation);

            if (worldScripts.EmailSystem) {
                // stop the normal purchase emails from being sent for these items
                worldScripts.GalCopAdminServices._purchase_ignore_equip.push("EQ_HOMESYSTEM_CLEAN");
                worldScripts.GalCopAdminServices._purchase_ignore_equip.push("EQ_HOMESYSTEM_OFFENDER");
                worldScripts.GalCopAdminServices._purchase_ignore_equip.push("EQ_HOMESYSTEM_FUGITIVE");
                worldScripts.GalCopAdminServices._purchase_ignore_equip.push("EQ_HOMESYSTEM_REMOVE");
                worldScripts.GalCopAdminServices._purchase_ignore_equip.push("EQ_HOMESYSTEM_INVEST");
            }

            this.$checkForMarkedSystems();
            this.$addInvestmentLevelToGalCopReputations();
            this.$initInterface(player.ship.dockedStation);
        }

        //-------------------------------------------------------------------------------------------------------------
        this.playerWillSaveGame = function () {
            missionVariables.HomeSystems = JSON.stringify(this._homeSystems);
            missionVariables.HomeSystem_DockCounts = JSON.stringify(this._dockCounts);
            missionVariables.HomeSystem_InvestCounts = JSON.stringify(this._investCounts);
            missionVariables.HomeSystem_Messages = JSON.stringify(this._messageNotification);
            missionVariables.HomeSystem_Counted = this._systemCounted;
            missionVariables.HomeSystem_PlayerRoles = JSON.stringify(this._playerRoles);
        }

        //-------------------------------------------------------------------------------------------------------------
        this.playerBoughtEquipment = function (equipmentKey) {
            let equip = null;
            let stn = player.ship.dockedStation;
            if (equipmentKey === "EQ_HOMESYSTEM_CLEAN" || equipmentKey === "EQ_HOMESYSTEM_OFFENDER" || equipmentKey === "EQ_HOMESYSTEM_FUGITIVE") {
                player.ship.removeEquipment(equipmentKey);
                let tot = this.$homeSystemCount();
                if (tot >= this._maxHomeSystems) {
                    equip = EquipmentInfo.infoForKey(equipmentKey);
                    this._purchaseEquipKey = equipmentKey
                    stn = player.ship.dockedStation;
                    this._purchasePrice = ((parseInt(equip.price) / 10) * stn.equipmentPriceFactor).toFixed(1);
                    this.$askPlayerAboutRemoval();
                } else {
                    this.$setHomeSystem(equipmentKey);
                }
                return;
            }
            if (equipmentKey === "EQ_HOMESYSTEM_REMOVE") {
                this.$removeHomeSystem(system.ID);
                this.$sendGoodbyeEmail(system.ID);
                this.$swapRoles(false);
                player.ship.removeEquipment(equipmentKey);
                // send email
                return;
            }
            if (equipmentKey === "EQ_HOMESYSTEM_INVEST") {
                player.ship.removeEquipment(equipmentKey);
                this.$askPlayerAboutInvestment();
                return;
            }
            // equipment rebate?
            if (this.$isHomeSystem(system.ID) === true && this.$checkLevel(system.ID, "equip_rebate") === true) {
                if (this._stationTypes.indexOf(stn.allegiance) === -1) return;
                let pct = 0.05;
                if (this.$checkLevel(system.ID, "equip_rebate_2") === true) pct = 0.1;
                equip = EquipmentInfo.infoForKey(equipmentKey);
                if (parseInt(equip.price) <= 0) return;

                let cost = (parseInt(equip.price) / 10) * stn.equipmentPriceFactor;
                let refund = Math.floor(cost * pct);
                if (refund > 0) {
                    player.credits += refund;
                    player.consoleMessage(expandDescription("[homesystem_rebate]", { amount: formatCredits(refund, false, true) }));
                }
            }
        }

        //-------------------------------------------------------------------------------------------------------------
        this.playerBoughtNewShip = function (ship, price) {
            if (this.$isHomeSystem(system.ID) === true && this.$checkLevel(system.ID, "ship_rebate") === true) {
                let stn = player.ship.dockedStation;
                if (this._stationTypes.indexOf(stn.allegiance) === -1) return;

                let pct = 0.05;
                if (this.$checkLevel(system.ID, "ship_rebate_2") === true) pct = 0.1;
                let refund = Math.floor(price * pct);
                if (refund > 0) {
                    player.credits += refund;
                    player.consoleMessage(expandDescription("[homesystem_rebate]", { amount: formatCredits(refund, false, true) }));
                }
            }
        }

        //-------------------------------------------------------------------------------------------------------------
        this.shipWillEnterWitchspace = function () {
            // clear out any message info
            this._messageNotification = {};
            this._systemCounted = false;
            this.$stopTimers();
        }

        //-------------------------------------------------------------------------------------------------------------
        this.playerEnteredNewGalaxy = function () {
            if (this._homeSystems[galaxyNumber].length > 0) {
                let hs = this._homeSystems[galaxyNumber];
                for (let i = 0; i < hs.length; i++) {
                    mission.markSystem({ system: hs[i], name: this.name, markerShape: "MARKER_DIAMOND", markerColor: "purpleColor", markerScale: 2 });
                }
            }
        }

        //-------------------------------------------------------------------------------------------------------------
        this.shipExitedWitchspace = function () {
            if (!this._dockCounts[galaxyNumber][system.ID]) this._dockCounts[galaxyNumber][system.ID] = 0;

            if (this.$isHomeSystem(system.ID) === true && this.$checkLevel(system.ID, "contracts") === true && !system.isInterstellarSpace && !system.sun.hasGoneNova && system.mainStation) {
                // must be a regular system with a main station
                this.$addSpecialParcelContracts();
                worldScripts["oolite-contracts-parcels"]._updateMainStationInterfacesList();
                this.$addSpecialPassengerContracts();
                worldScripts["oolite-contracts-passengers"]._updateMainStationInterfacesList();
            }

            this.$startTimer();
            this.$swapRoles(this.$isHomeSystem(system.ID));
        }

        //-------------------------------------------------------------------------------------------------------------
        this.escapePodSequenceOver = function () {
            this._simulator = true;
        }

        //-------------------------------------------------------------------------------------------------------------
        this.shipWillDockWithStation = function (station) {
            if (this._simulator === true) return;
            this.$stopTimers();
            this._dockAvail = false;
            if (this.$isHomeSystem(system.ID) === true) {
                if (this.$checkLevel(system.ID, "dock_fee") === true) {
                    let df = worldScripts["Docking Fees"];
                    // turn off the docking fees for stations in home system
                    if (df) df._simulator = true;
                }
                if (this._stationTypes.indexOf(station.allegiance) >= 0 && player.ship.bounty < 50) {
                    let txt = expandDescription("[homesystem_dockingreport]"); // *** expansions required
                    if (this.$checkLevel(system.ID, "clear_offender") === true && player.ship.bounty > 0) {
                        txt += expandDescription("[homesystem_clearoffender]");
                        player.bounty = 0;
                    }
                    player.addMessageToArrivalReport(txt);
                }
            }
            this._dockingStation = null;

            // add the f4 interface
            this.$initInterface(station);

        }

        //-------------------------------------------------------------------------------------------------------------
        this.shipDockedWithStation = function (station) {
            this._tellPlayerAboutFuel = false;
            if (this._simulator === true) return;
            if (this._stationTypes.indexOf(station.allegiance) >= 0) {
                if (this._systemCounted === false) {
                    let oldLevel = this.$baseLevel(system.ID);
                    let current = 0;
                    if (!this._dockCounts[galaxyNumber]) this._dockCounts[galaxyNumber] = {};
                    if (this._dockCounts[galaxyNumber][system.ID]) current = parseInt(this._dockCounts[galaxyNumber][system.ID]);
                    current += 1;
                    this._dockCounts[galaxyNumber][system.ID] = current;
                    if (this.$isHomeSystem(system.ID) === true) this.$newBenefitEmail(system.ID, oldLevel, false);
                    this._systemCounted = true;
                }
                if (this.$isHomeSystem(system.ID) && this.$checkLevel(system.ID, "fuel") === true && player.ship.fuel < 7) {
                    player.ship.fuel = 7;
                    player.consoleMessage(expandDescription("[homesystem_refuelled]"));
                    let fe = worldScripts.FuelTweaks_FuelEconomy
                    if (fe && (fe.$isFuelAvailable(system.ID) === false || fe.$fuelRationsInUse(system.ID) === true)) {
                        this._tellPlayerAboutFuel = true;
                    }
                }
            }
            this.$initInterface(station);
        }

        //-------------------------------------------------------------------------------------------------------------
        this.missionScreenOpportunity = function () {
            if (this._tellPlayerAboutFuel === true) {
                this._tellPlayerAboutFuel = false;
                mission.runScreen({
                    screenID: "oolite-homesystem-fuel-map",
                    title: expandDescription("[homesystem_refuelled_title]"),
                    overlay: { name: "hs-home.png", height: 546 },
                    message: expandDescription("[homesystem_fuelmessage]"),
                    exitScreen: "GUI_SCREEN_INTERFACES"
                });
            }
        }

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

        //-------------------------------------------------------------------------------------------------------------
        this.shipLaunchedFromStation = function (station) {
            if (this.$simulatorRunning()) { this._simulator = true; this.$stopTimers(); return; }
            this._simulator = false;
            if (this.$isHomeSystem(system.ID) === true) {
                if (this._stationTypes.indexOf(station.allegiance) >= 0 && player.ship.bounty < 50 && (!this._messageNotification[station.displayName + "_departure"] || this._messageNotification[station.displayName + "_departure"] === false)) {
                    let txt = expandDescription("[homesystem_departurecomms]");
                    station.commsMessage(txt, player.ship);
                    this._messageNotification[station.displayName + "_departure"] = true;
                }
            }
        }

        //-------------------------------------------------------------------------------------------------------------
        this.playerRequestedDockingClearance = function (message) {
            /*switch (message) {
                case "DOCKING_CLEARANCE_DENIED_TRAFFIC_OUTBOUND":
                case "DOCKING_CLEARANCE_DENIED_TRAFFIC_INBOUND":
                    if (this.$isHomeSystem(system.ID) === true && this.$checkLevel(system.ID, "dock_priority") === true) {
                        this._dockTimer = new Timer(this, this.$autoDock, 5, 5);
                    }
                case "DOCKING_CLEARANCE_GRANTED":
                case "DOCKING_CLEARANCE_NOT_REQUIRED":
                    this._dockAvail = true;
                    let stn = player.ship.target;
                    if (stn.isStation && this._dockingStation == null) this._dockingStation = stn;
                    break;
                case "DOCKING_CLEARANCE_DENIED_SHIP_FUGITIVE":
                case "DOCKING_CLEARANCE_CANCELLED":
                    this._dockingStation = null;
                    break;
            }*/
            // Rather than try to get linter to ignore the use of the switch statement above, i've re-written the routine
            // to hopefully make it easier to understand
            // if we've been told we have to wait for incoming or outgoing ships, then start a timer for an autodock
            if (message === "DOCKING_CLEARANCE_DENIED_TRAFFIC_OUTBOUND" ||
                message === "DOCKING_CLEARANCE_DENIED_TRAFFIC_INBOUND") {
                this._dockAvail = false;
                if (this.$isHomeSystem(system.ID) === true && this.$checkLevel(system.ID, "dock_priority") === true) {
                    this._dockTimer = new Timer(this, this.$autoDock, 5, 5);
                }
            }
            // what station is this?
            if (message === "DOCKING_CLEARANCE_DENIED_TRAFFIC_OUTBOUND" ||
                message === "DOCKING_CLEARANCE_DENIED_TRAFFIC_INBOUND" ||
                message === "DOCKING_CLEARANCE_GRANTED" ||
                message === "DOCKING_CLEARANCE_NOT_REQUIRED") {
                let stn = player.ship.target;
                if (stn.isStation && this._dockingStation == null) this._dockingStation = stn;
            }
            // clear out the stored station reference
            if (message === "DOCKING_CLEARANCE_DENIED_SHIP_FUGITIVE" ||
                message === "DOCKING_CLEARANCE_CANCELLED") {
                this._dockingStation = null;
            }
        }

        //-------------------------------------------------------------------------------------------------------------
        this.playerDockingClearanceGranted = function () {
            if (this.$isHomeSystem(system.ID) === true) {
                this._dockAvail = true;
                if (!this._messageNotification[this._dockingStation.displayName + "_docking"] || this._messageNotification[this._dockingStation.displayName + "_docking"] === false) {
                    let txt = expandDescription("[homesystem_dockcomms]");
                    this._dockingStation.commsMessage(txt, player.ship); // *** expansions required
                    this._messageNotification[this._dockingStation.displayName + "_docking"] = true;
                }
            }
        }

        //-------------------------------------------------------------------------------------------------------------
        this.playerBoughtCargo = function (commodity, units, price) {
            if (this.$isHomeSystem(system.ID) === true && this.$checkLevel(system.ID, "trade") === true && units > 0 && this._stationTypes.indexOf(player.ship.dockedStation.allegiance) >= 0) {
                let tot = 0;
                if (this._messageNotification["cargo_bought"]) tot = parseInt(this._messageNotification["cargo_bought"]);
                let start = tot;
                tot += units;
                this._messageNotification["cargo_bought"] = tot;
                let bonus = 0;
                if (tot >= 10 && start < 10) bonus = 5;
                if (tot >= 50 && start < 50) bonus = 10;
                if (tot >= 100 && start < 100) bonus = 20;
                if (tot >= 200 && start < 200) bonus = 50;
                if (tot >= 500 && start < 500) bonus = 100;
                if (bonus > 0) {
                    player.credits += bonus;
                    player.consoleMessage(expandDescription("[homesystem_tradebonus]", { amount: formatCredits(bonus, true, false) }));
                }
                // apply this to the sold setting, so the player doesn't get awarded twice
                //this._messageNotification["cargo_sold"] = tot;
            }
        }

        //-------------------------------------------------------------------------------------------------------------
        this.playerSoldCargo = function (commodity, units, price) {
            if (this.$isHomeSystem(system.ID) === true && this.$checkLevel(system.ID, "trade") === true && units > 0 && this._stationTypes.indexOf(player.ship.dockedStation.allegiance) >= 0) {
                let tot = 0;
                if (this._messageNotification["cargo_bought"]) tot = parseInt(this._messageNotification["cargo_bought"]);
                let start = tot;
                tot -= units;
                // mke sure the player can't just ossillate across one of the bonus points by buying and selling without moving
                if (tot < 500 && start >= 500) tot = 500;
                if (tot < 200 && start >= 200) tot = 200;
                if (tot < 100 && start >= 100) tot = 100;
                if (tot < 50 && start >= 50) tot = 50;
                if (tot < 10 && start >= 10) tot = 10;
                if (tot < 0) tot = 0;
                this._messageNotification["cargo_bought"] = tot;
            }
        }

        //-------------------------------------------------------------------------------------------------------------
        this.shipBountyChanged = function (delta, reason) {
            // remove any home system link if the player does something bad to make themselves a fugitive in the system
            switch (reason) {
                case "killed innocent":
                case "killed police":
                case "attacked police":
                case "attacked main station":
                    if (delta > 0 && player.ship.bounty > 50 && this.$isHomeSystem(system.ID) === true) {
                        player.consoleMessage(expandDescription("[homesystem_revoked]"));
                        this.$removeHomeSystem(system.ID);
                    }
                    // reset the player's reputation
                    this._dockCounts[galaxyNumber][system.ID] = 0
                    this._investCounts[galaxyNumber][system.ID] = 0;
                    break;
            }
        }

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

        //-------------------------------------------------------------------------------------------------------------
        this.guiScreenWillChange = function (to, from) {
            if (to === "GUI_SCREEN_SYSTEM_DATA") this.$addInfoToSystemDataScreen();
        }

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

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

        //-------------------------------------------------------------------------------------------------------------
        this.$askPlayerAboutRemoval = function () {
            let curChoices = {};
            let txt = "";
            let count = this.$homeSystemCount();
            if (count >= this._maxHomeSystems) {
                if (count === 1) {
                    txt = expandDescription("[homesystem_letting_go_single]", { sys: System.systemNameForID(this._homeSystems[galaxyNumber][0]) });
                    curChoices["0_UNSET"] = expandDescription("[homesystem_yes]");
                    curChoices["9_NO"] = expandDescription("[homesystem_no]");
                } else {
                    txt = expandDescription("[homesystem_letting_go_multi]");
                    let set = this._homeSystems[galaxyNumber];
                    for (let i = 0; i < set.length; i++) {
                        curChoices[i + "_UNSET"] = System.systemNameForID(set[i]);
                    }
                    curChoices["9_NO"] = expandDescription("[homesystem_cancel]");
                }
                let opts = {
                    screenID: "oolite-homesystem-question-map",
                    title: expandDescription("[homesystem_screen_title]"),
                    overlay: { name: "hs-question.png", height: 546 },
                    allowInterrupt: false,
                    exitScreen: "GUI_SCREEN_EQUIP_SHIP",
                    choices: curChoices,
                    initialChoicesKey: "9_NO",
                    message: txt
                };
                mission.runScreen(opts, this.$questionHandler, this);
            } else {
                return;
            }
        }

        //-------------------------------------------------------------------------------------------------------------
        this.$questionHandler = function (choice) {
            if (choice == null) return;

            if (choice === "9_NO") {
                // refund the player
                player.credits += Math.floor(parseFloat(this._purchasePrice) * 10) / 10;
                return;
            }
            if (choice.indexOf("_UNSET") >= 0) {
                // unset the system
                let idx = parseInt(choice.substring(0, 1), 10);
                let sys = this._homeSystems[galaxyNumber][idx];
                this.$removeHomeSystem(sys);
                this.$sendGoodbyeEmail(sys);
                player.consoleMessage(expandDescription("[homesystem_removed]", { sys: System.systemNameForID(sys) }));
                // check the count again -- if we're still over, go back and ask again
                let count = this.$homeSystemCount();
                if (count >= this._maxHomeSystems) {
                    this.$askPlayerAboutRemoval();
                    return;
                }
                this.$setHomeSystem(this._purchaseEquipKey);
            }
        }

        //-------------------------------------------------------------------------------------------------------------
        this.$askPlayerAboutInvestment = function () {
            // if there's only one home system to invest in, force that choice
            if (this.$homeSystemCount() === 1) {
                this.$questionHandler2("0_INVEST");
                return;
            }
            let curChoices = {};
            let txt = "";
            txt = expandDescription("[homesystem_transfer]");
            let set = this._homeSystems[galaxyNumber];
            for (let i = 0; i < set.length; i++) {
                curChoices[i + "_INVEST"] = System.systemNameForID(set[i]);
            }
            curChoices["9_NO"] = expandDescription("[homesystem_cancel]");
            let opts = {
                screenID: "oolite-homesystem-question-map",
                title: expandDescription("[homesystem_screen_title]"),
                overlay: { name: "hs-question.png", height: 546 },
                allowInterrupt: false,
                exitScreen: "GUI_SCREEN_EQUIP_SHIP",
                choices: curChoices,
                initialChoicesKey: "9_NO",
                message: txt
            };
            mission.runScreen(opts, this.$questionHandler2, this);
        }

        //-------------------------------------------------------------------------------------------------------------
        this.$questionHandler2 = function (choice) {
            if (choice == null) return;
            if (choice === "9_NO") {
                // refund the player
                player.credits += 10000;
                return;
            }
            if (choice.indexOf("_INVEST") >= 0) {
                let idx = parseInt(choice.substring(0, 1), 10);
                let sys = this._homeSystems[galaxyNumber][idx];

                player.consoleMessage(expandDescription("[homesystem_transferred]", { sys: System.systemNameForID(sys) }), 5);

                if (!this._investCounts[galaxyNumber]) this._investCounts[galaxyNumber] = {};
                let oldLevel = this.$baseLevel(sys);
                let current = this.$investmentCount(galaxyNumber, sys);
                current += 1;
                this._investCounts[galaxyNumber][sys] = current;
                // only send a new benefit email if the player is at that system
                this.$newBenefitEmail(sys, oldLevel, false);

                if (worldScripts.DayDiplomacy_060_Citizenships) {
                    let c = worldScripts.DayDiplomacy_060_Citizenships;
                    let s = System.infoForSystem(galaxyNumber, sys);
                    /*
                        According to the Diplomacy OXP, a citizenship costs somewhere between 900cr and 9000cr.
                        By contrast investment package costs 10000cr.
                        To bring these values into some form of alignment, I'm taking the number of investment packages purchased
                        and multiplying it by 500, then comparing that to the citizenship price for that system.
                        For example: if I've purchased 3 investment options, my equivalent amount would be 3 * 500 = 1500cr
                        That's enough to get you citizenship in some of the lower productivity systems, but not the higher ones.
        
                        The explanation for this is that the investment purchase is largely a commercial transaction, whereas
                        "citizenship" is more of a governmental transaction. However, a portion of the commercial transaction 
                        is allocated to government, so, over time, the government will recognise the value of your commercial
                        input and award the citizenship.
        
                        Note: this is mucking around with the internals of Diplomacy and is liable to break in future versions
                        Note: the citizenship option will only be awarded once (when the equivalent investment amount surpasses
                        the citizenship price). Subsequent investment purchases will not award the citizenship, and if the
                        player revokes their citizenship, it will not be re-awarded. Also, if the player has previously had
                        citizenship, then revokes it, then purchases investment options that ultimately grant citizenship,
                        the previous revoking will not be remembered and citizenship will get purchased again.
                    */
                    let inv = current * 500;
                    let oldinv = (current - 1) * 500;
                    let price = c.$getCitizenshipPrice(s);
                    if (c.$hasPlayerCitizenship(galaxyNumber, sys) === false && (oldinv < price && inv >= price)) {
                        // make sure we have enough credits to buy the citizenship
                        player.credits += price;
                        c._buyCitizenship(galaxyNumber, sys);
                        player.consoleMessage(expandDescription("[homesystem_citizen]", { sys: System.systemNameForID(sys) }), 5);
                    }
                }
            }
        }

        //-------------------------------------------------------------------------------------------------------------
        this.$checkForShips = function $checkForShips() {
            // after transmitting message, turn off "Greet ship" message in BroadcastComms, and add a "Reply" message instead
            let ships = player.ship.checkScanner(true);
            let rnd = Math.random;
            let pirateLevel = this.$checkLevel(system.ID, "pirate");
            if (ships && ships.length > 0) {
                for (let i = 0; i < ships.length; i++) {
                    let shp = ships[i];
                    // police ship
                    if (shp.isPolice && shp.isStation === false && player.ship.bounty === 0 && shp.hasHostileTarget === false && (!shp.script._welcomed || shp.script._welcomed === false)) {
                        // only send a police welcome message every 10 minutes at most
                        if (rnd() > 0.5 && (!this._messageNotification["police"] || clock.adjustedSeconds - this._messageNotification["police"] > 600)) {
                            let txt = expandDescription("[homesystem_policegreeting]");
                            shp.commsMessage(txt, player.ship); // ** expansions required
                            shp.script._welcomed = true;
                            // note the time we did this, so we can avoid spamming the player
                            this._messageNotification["police"] = clock.adjustedSeconds;
                            let bcc = worldScripts.BroadcastCommsMFD;
                            if (bcc) {
                                // turn off the greet ship message
                                bcc.$addShipToArray(shp, bcc._greeted);
                                // add a reply option
                                bcc.$createMessage({
                                    messageName: "hs_transmit_police_reply_" + shp.entityPersonality.toString(),
                                    displayText: expandDescription("[homesystem_reply]"),
                                    messageText: expandDescription("[homesystem_policereply]"), // ** expansions required
                                    ship: shp,
                                    transmissionType: "target",
                                    callbackFunction: this.$messageReplyPolice,
                                    deleteOnTransmit: true,
                                    delayCallback: 0,
                                    hideOnConditionRed: true
                                }
                                );
                            }
                            break;
                        }
                    }
                    // trader
                    // pirate
                    if (pirateLevel === true && shp.bounty >= 0 && shp.hasHostileTarget === false && (Ship.roleIsInCategory(shp.primaryRole, "oolite-pirate") || (shp.AI && shp.AI.toLowerCase().indexOf("pirate") >= 0) || (shp.AIScript && shp.AIScript.name.toLowerCase().indexOf("pirate") >= 0))) {
                        // only send a pirate "welcome" message every 5 minutes at most
                        if ((!shp.script._welcomed || shp.script._welcomed === false) && rnd() > 0.5 && (!this._messageNotification["pirate"] || clock.adjustedSeconds - this._messageNotification["pirate"] > 300)) {
                            let txt = expandDescription("[homesystem_pirategreeting]");
                            shp.commsMessage(txt, player.ship); // ** expansions required
                            shp.script._welcomed = true;
                            // note the time we did this, so we can avoid spamming the player
                            this._messageNotification["pirate"] = clock.adjustedSeconds;
                            let bcc = worldScripts.BroadcastCommsMFD;
                            if (bcc) {
                                // turn off the greet ship message
                                bcc.$addShipToArray(shp, bcc._greeted);
                                // add a reply option
                                bcc.$createMessage({
                                    messageName: "hs_transmit_pirate_reply_" + shp.entityPersonality.toString(),
                                    displayText: expandDescription("[homesystem_reply]"),
                                    messageText: expandDescription("[homesystem_piratereply]"), // ** expansions required
                                    ship: shp,
                                    transmissionType: "target",
                                    callbackFunction: this.$messageReplyPirate,
                                    deleteOnTransmit: true,
                                    delayCallback: 0,
                                    hideOnConditionRed: true
                                }
                                );
                                // ideally, we need a way of disabling this response if any other type is sent by the player to this ship
                            }
                            break;
                        }
                    }
                }
            }
        }

        //-------------------------------------------------------------------------------------------------------------
        this.$messageReplyPolice = function () {
        }

        //-------------------------------------------------------------------------------------------------------------
        this.$messageReplyPirate = function () {
        }

        //-------------------------------------------------------------------------------------------------------------
        this.$startTimer = function () {
            if (this.$isHomeSystem(system.ID) === true && this.$checkLevel(system.ID, "police") === true) {
                // start a timer to being monitoring the scanner for ships that might want to say hi to the player
                this._messageTimer = new Timer(this, this.$checkForShips, 10, 10);
            }
        }

        //-------------------------------------------------------------------------------------------------------------
        this.$stopTimers = function () {
            if (this._messageTimer && this._messageTimer.isRunning) this._messageTimer.stop();
            if (this._dockTimer && this._dockTimer.isRunning) this._dockTimer.stop();
        }

        //-------------------------------------------------------------------------------------------------------------
        this.$autoDock = function () {
            if (this._dockingStation == null) return;
            if (this._dockAvail === true) {
                if (this._dockMessage >= 3) {
                    this._dockingStation.commsMessage(expandDescription("[homesystem_dock_delay]"), player.ship);
                }
                this._dockTimer.stop();
                return;
            }
            this._dockMessage += 1;
            switch (this._dockMessage) {
                case 1:
                    this._dockingStation.commsMessage(expandDescription("[homesystem_dock_clearing]"), player.ship);
                    break;
                case 3:
                    this._dockingStation.commsMessage(expandDescription("[homesystem_dock_auto]"), player.ship);
                    break;
                case 7:
                    this._dockingStation.commsMessage(expandDescription("[homesystem_dock_final]"), player.ship);
                    break;
                case 8:
                    this._dockingStation.dockPlayer();
                    // only a 5 minute time delay
                    clock.addSeconds(300);
            }
        }

        //-------------------------------------------------------------------------------------------------------------
        this.$setHomeSystem = function (equipmentKey) {
            this._homeSystems[galaxyNumber].push(system.ID);
            mission.markSystem({ system: system.ID, name: this.name, markerShape: "MARKER_DIAMOND", markerColor: "purpleColor", markerScale: 2 });
            this.$swapRoles(true);
            // send email welcoming the player "home"
            if (worldScripts.EmailSystem) {
                let ga = worldScripts.GalCopAdminServices;
                let e = worldScripts.EmailSystem;
                e.$createEmail(
                    {
                        sender: expandDescription("[homesystem_station_manager]"),
                        subject: expandDescription("[homesystem_privileges]"),
                        date: clock.adjustedSeconds,
                        sentFrom: system.ID,
                        message: expandDescription("[homesystem_email_" + equipmentKey + "]"),
                        expiryDays: ga._defaultExpiryDays
                    });
                this.$newBenefitEmail(system.ID, this.$baseLevel(system.ID) - 1, true);
            }
            this.$addInvestmentLevelToGalCopReputations();
        }

        //-------------------------------------------------------------------------------------------------------------
        this.$isHomeSystem = function (sysID) {
            return (parseInt(sysID) >= 0 && this._homeSystems[galaxyNumber].indexOf(parseInt(sysID)) >= 0);
        }

        //-------------------------------------------------------------------------------------------------------------
        this.$homeSystemCount = function () {
            return this._homeSystems[galaxyNumber].length;
        }

        //-------------------------------------------------------------------------------------------------------------
        this.$baseLevel = function (sysID) {
            return (parseInt(this._dockCounts[galaxyNumber][parseInt(sysID)]) + (this.$investmentCount(galaxyNumber, parseInt(sysID)) * 2));
        }

        //-------------------------------------------------------------------------------------------------------------
        this.$checkLevel = function (sysID, lvl) {
            let min = this.$levelCalc(sysID, lvl);
            return (this.$baseLevel(sysID) >= min);
        }

        //-------------------------------------------------------------------------------------------------------------
        this.$checkLevelValue = function (sysID, lvl, value) {
            let min = this.$levelCalc(sysID, lvl);
            return (value >= min);
        }

        //-------------------------------------------------------------------------------------------------------------
        this.$levelCalc = function (sysID, lvl) {
            let min = this._levels[lvl];
            let sys = (parseInt(sysID) === system.ID ? system.info : System.infoForSystem(galaxyNumber, parseInt(sysID)));
            let factor = 1 + (sys.government + (7 - sys.economy) * 2 + sys.techlevel * 2) / 55;
            return parseInt(min * factor);
        }

        //-------------------------------------------------------------------------------------------------------------
        this.$removeHomeSystem = function (sysID) {
            let set = this._homeSystems[galaxyNumber];
            let idx = set.indexOf(parseInt(sysID));
            if (idx >= 0) set.splice(idx, 1);
            this._investCounts[galaxyNumber][parseInt(sysID)] = 0;
            mission.unmarkSystem({ system: sysID, name: this.name });
            this.$removeGalCopReputation(sysID);
        }

        //-------------------------------------------------------------------------------------------------------------
        // make all the player's roles "player-home" (or swap them back to what they were originally)
        // by doing this we can ensure a consistent view of the player while they're "home"
        this.$swapRoles = function (swapIn) {
            if (swapIn === true) {
                // copy all existing roles to holding array
                if (player.roleWeights[0] === "player-home" || player.roleWeights[1] === "player-home" || player.roleWeights[2] === "player-home") return; // looks like we've already swapped things in
                this._playerRoles.length = 0;
                for (let i = 0; i < player.roleWeights.length; i++) {
                    this._playerRoles.push(player.roleWeights[i]);
                    player.setPlayerRole("player-home", i);
                }
            } else {
                if (this._playerRoles.length === 0) return; // no data to swap back
                if (player.roleWeights[0] !== "player-home" || player.roleWeights[1] !== "player-home" || player.roleWeights[2] !== "player-home") return; // looks like we haven't swapped anything out yet
                for (let i = 0; i < this._playerRoles.length; i++) {
                    // only update the ones that are still "player-home"
                    // just in case any player action insystem causes a change.
                    if (player.roleWeights[i] === "player-home") player.setPlayerRole(this._playerRoles[i], i);
                }
                this._playerRoles.length = 0;
            }
        }

        //-------------------------------------------------------------------------------------------------------------
        this.$newBenefitEmail = function (sysID, oldLevel, initial) {
            let txt = "";
            let types = "a new benefit";
            let count = 0;

            let e = worldScripts.EmailSystem;
            if (!e) return;

            if (this.$checkLevelValue(sysID, "dock_fee", oldLevel) === false && this.$checkLevel(sysID, "dock_fee") === true && worldScripts["Docking Fee"]) {
                txt += "\n" + expandDescription("[homesystem_benefit_dock_fee]");
                count += 1;
            }
            if (this.$checkLevelValue(sysID, "dock_priority", oldLevel) === false && this.$checkLevel(sysID, "dock_priority") === true) {
                txt += "\n" + expandDescription("[homesystem_benefit_dock_priority]");
                count += 1;
            }
            if (this.$checkLevelValue(sysID, "clear_offender", oldLevel) === false && this.$checkLevel(sysID, "clear_offender") === true) {
                txt += "\n" + expandDescription("[homesystem_benefit_clear_offender]");
                count += 1;
            }
            if (this.$checkLevelValue(sysID, "missions", oldLevel) === false && this.$checkLevel(sysID, "missions") === true && worldScripts.GalCopBB_Missions) {
                txt += "\n" + expandDescription("[homesystem_benefit_missions]");
                count += 1;
            }
            if (this.$checkLevelValue(sysID, "missions_2", oldLevel) === false && this.$checkLevel(sysID, "missions_2") === true && worldScripts.GalCopBB_Missions) {
                txt += "\n" + expandDescription("[homesystem_benefit_missions_2]");
                count += 1;
            }
            if (this.$checkLevelValue(sysID, "contracts", oldLevel) === false && this.$checkLevel(sysID, "contracts") === true) {
                txt += "\n" + expandDescription("[homesystem_benefit_contracts]");
                count += 1;
            }
            if (this.$checkLevelValue(sysID, "fuel", oldLevel) === false && this.$checkLevel(sysID, "fuel") === true) {
                txt += "\n" + expandDescription("[homesystem_benefit_fuel]");
                count += 1;
            }
            if (this.$checkLevelValue(sysID, "trade", oldLevel) === false && this.$checkLevel(sysID, "trade") === true) {
                txt += "\n" + expandDescription("[homesystem_benefit_trade]");
                count += 1;
            }
            if (this.$checkLevelValue(sysID, "equip_rebate", oldLevel) === false && this.$checkLevel(sysID, "equip_rebate") === true) {
                txt += "\n" + expandDescription("[homesystem_benefit_equip_rebate]");
                count += 1;
            }
            if (this.$checkLevelValue(sysID, "ship_rebate", oldLevel) === false && this.$checkLevel(sysID, "ship_rebate") === true) {
                txt += "\n" + expandDescription("[homesystem_benefit_ship_rebate]");
                count += 1;
            }
            if (this.$checkLevelValue(sysID, "equip_rebate_2", oldLevel) === false && this.$checkLevel(sysID, "equip_rebate_2") === true) {
                txt += "\n" + expandDescription("[homesystem_benefit_equip_rebate_2]");
                count += 1;
            }
            if (this.$checkLevelValue(sysID, "ship_rebate_2", oldLevel) === false && this.$checkLevel(sysID, "ship_rebate_2") === true) {
                txt += "\n" + expandDescription("[homesystem_benefit_ship_rebate_2]");
                count += 1;
            }

            if (txt !== "") {
                if (count > 1) types = expandDescription("[homesystem_new_benefits]");
                // something's changed, so send the email
                let ga = worldScripts.GalCopAdminServices;

                let msg = expandDescription("[homesystem_benefit_email]", { benefits: txt, types: types, display_initial: (initial === true ? expandDescription("[homesystem_so_glad]") : "") });
                let sndr = expandDescription("[homesystem_station_manager]");

                let delay = 0;

                if (system.ID != parseInt(sysID)) {
                    sndr = expandDescription("[homesystem_email_stnmgr]", { sys: System.systemNameForID(sysID) });
                    msg = expandDescription("[homesystem_benefit_remote_email]", { benefits: txt, types: types, system: System.systemNameForID(sysID) });
                    let dist = system.info.distanceToSystem(System.infoForSystem(galaxyNumber, sysID));
                    delay = dist * 3600; // 1 hour for every ly
                }

                e.$createEmail({
                    sender: sndr,
                    subject: expandDescription("[homesystem_privileges]"),
                    date: clock.adjustedSeconds + delay,
                    sentFrom: sysID,
                    message: msg,
                    expiryDays: ga._defaultExpiryDays
                });
            }
        }

        //-------------------------------------------------------------------------------------------------------------
        this.$sendGoodbyeEmail = function (sysID) {
            let sysname = System.systemNameForID(sysID);
            let ga = worldScripts.GalCopAdminServices;
            let e = worldScripts.EmailSystem;
            if (!e) return;
            e.$createEmail(
                {
                    sender: expandDescription("[homesystem_email_stnmgr]", { sys: sysname }),
                    subject: expandDescription("[homesystem_privileges]"),
                    date: clock.adjustedSeconds,
                    sentFrom: sysID,
                    message: expandDescription("[homesystem_leaving_email]", { system_name: sysname }),
                    expiryDays: ga._defaultExpiryDays
                });
        }

        //-------------------------------------------------------------------------------------------------------------
        this.$addSpecialParcelContracts = function () {
            // possibly add some high-value contracts
            let numContracts = Math.floor(Math.random() * 3);

            for (let i = 0; i < numContracts; i++) {
                let parcel = {};
                // pick a random system to take the parcel to
                let destination = Math.floor(Math.random() * 256);
                // discard if chose the current system
                if (destination === system.ID) continue;
                // get the SystemInfo object for the destination
                let destinationInfo = System.infoForSystem(galaxyNumber, destination);
                if (destinationInfo.sun_gone_nova) continue;
                // check that a route to the destination exists
                let routeToDestination = system.info.routeToSystem(destinationInfo);
                // if the system cannot be reached, discard the parcel
                if (!routeToDestination) continue;

                // we now have a valid destination, so generate the rest of
                // the parcel details
                parcel.destination = destination;
                // we'll need this again later, and route calculation is slow
                parcel.route = routeToDestination;
                parcel.sender = randomName() + " " + randomName();

                // time allowed for delivery is time taken by "fewest jumps"
                // route, plus 10-110%, plus four hours to make sure all routes
                // are "in time" for a reasonable-length journey in-system.
                let dtime = Math.floor((routeToDestination.time * 3600 * (1.1 + (Math.random())))) + 14400;
                parcel.deadline = clock.adjustedSeconds + dtime;

                parcel.risk = Math.floor(Math.random() * 3);
                if (parcel.risk < 2 && destinationInfo.government <= 1 && Math.random() < 0.5) parcel.risk++;
                parcel.description = expandDescription("[hs_parcel-description-risk" + parcel.risk + "]");

                // total payment is small for these items.
                parcel.payment = Math.floor(
                    // 2-3 credits per LY of route
                    (routeToDestination.distance * (2 + Math.random())) +
                    // additional income for route length based on reputation
                    (Math.pow(routeToDestination.route.length, 1 + (parcel.risk * 0.4) + (0.2 * player.parcelReputationPrecise))) +
                    // small premium for delivery to more dangerous systems
                    (2 * Math.pow(7 - destinationInfo.government, 1.5))
                );

                parcel.payment *= (Math.random() + Math.random() + Math.random() + Math.random()) / 2;

                let prudence = (2 * Math.random()) - 1;

                let desperation = (Math.random() * (0.5 + parcel.risk)) * (1 + 1 / (Math.max(0.5, dtime - (routeToDestination.time * 3600))));
                let competency = Math.max(50, (routeToDestination.route.length - 1) * (0.5 + (parcel.risk * 2)));
                if (parcel.risk == 0) competency -= 30;
                parcel.payment = Math.floor(parcel.payment * (1 + (0.4 * prudence)));
                parcel.payment += (parcel.risk * 200);

                // anywhere from 2 to 5 times the normal payment
                parcel.payment *= Math.floor(Math.random() * (parcel.risk + 1) + 2);
                // paying this little probably can't ask for anyone good
                if (parcel.payment < 100) competency -= 15;

                parcel.skill = competency + 20 * (prudence - desperation);

                // upper limit on skill
                if (parcel.skill > 60) parcel.skill = 60;

                //log(this.name, "parcel " + parcel.payment, parcel.skill, parcel.risk);

                // add parcel to contract list
                worldScripts["oolite-contracts-parcels"]._addParcelToSystem(parcel);
            }

        }

        //-------------------------------------------------------------------------------------------------------------
        this.$addSpecialPassengerContracts = function () {
            let numContracts = Math.floor(Math.random * 4 - 1);
            if (numContracts < 0) numContracts = 0;

            // some of these possible contracts may be discarded later on
            for (let i = 0; i < numContracts; i++) {
                let passenger = {};
                // pick a random system to take the passenger to
                let destination = Math.floor(Math.random() * 256);
                // discard if chose the current system
                if (destination === system.ID) continue;
                // get the SystemInfo object for the destination
                let destinationInfo = System.infoForSystem(galaxyNumber, destination);
                if (destinationInfo.sun_gone_nova) continue;

                let daysUntilDeparture = 1 + (Math.random() * (7 + player.passengerReputationPrecise - destinationInfo.government));
                // loses some more contracts if reputation negative
                if (daysUntilDeparture <= 0) continue;

                // check that a route to the destination exists
                let routeToDestination = system.info.routeToSystem(destinationInfo);
                // if the system cannot be reached, ignore this contract
                if (!routeToDestination) continue;

                // we now have a valid destination, so generate the rest of the passenger details
                passenger.destination = destination;
                // we'll need this again later, and route calculation is slow
                passenger.route = routeToDestination;

                // 50% local inhabitant
                if (Math.random() < 0.5) {
                    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 {
                    let okay = true;
                    for (let j = 0; j < player.ship.passengers.length; j++) {
                        if (player.ship.passengers[j].name == passenger.name) {
                            okay = false;
                            break;
                        }
                    }
                    if (okay) {
                        for (let j = 0; j < worldScripts["oolite-contracts-passengers"].$passengers.length; j++) {
                            if (worldScripts["oolite-contracts-passengers"].$passengers[j].name == passenger.name) {
                                okay = false;
                                break;
                            }
                        }
                    }
                    if (!okay) passenger.name += "a";
                } while (!okay);

                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.
                let dtime = Math.floor(daysUntilDeparture * 86400) + (passenger.route.time * 3600);
                passenger.deadline = clock.adjustedSeconds + dtime;
                if (passenger.risk < 2 && destinationInfo.government <= 1 && Math.random() < 0.5) passenger.risk++;

                // total payment is:
                passenger.payment = Math.floor(
                    // payment per hop (higher at rep > 5)
                    5 * Math.pow(routeToDestination.route.length - 1, (passenger.risk * 0.2) + (player.passengerReputationPrecise > 5 ? 2.45 : 2.3)) +
                    // payment by route length
                    routeToDestination.distance * (8 + (Math.random() * 8)) +
                    // premium for delivery to more dangerous systems
                    (5 * (7 - destinationInfo.government) * (7 - destinationInfo.government))
                );
                passenger.payment *= (Math.random() + Math.random() + Math.random() + Math.random()) / 2;

                let prudence = (2 * Math.random()) - 1;
                let desperation = (Math.random() * (0.5 + passenger.risk)) * (1 + 1 / (Math.max(0.5, dtime - (routeToDestination.time * 3600))));
                let competency = Math.max(50, (routeToDestination.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));

                // anywhere from 2 to 5 times the normal payment
                passenger.payment *= Math.floor(Math.random() * (passenger.risk + 1) + 2);

                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;

                log(this.name, "pass " + passenger.payment, passenger.skill, passenger.risk);

                // add passenger to contract list
                worldScripts["oolite-contracts-passengers"]._addPassengerToSystem(passenger);
            }
        }

        //-------------------------------------------------------------------------------------------------------------
        // makes sure all our home systems are marked on the chart
        this.$checkForMarkedSystems = function () {
            let hs = this._homeSystems[galaxyNumber];
            let ms = mission.markedSystems;
            if (hs.length > 0) {
                // look for any marked system matches
                if (ms.length > 0) {
                    for (let i = 0; i < hs.length; i++) {
                        let found = false;
                        // loop through all the marked systems
                        for (let j = 0; j < ms.length; j++) {
                            if (ms[j].system === hs[i] && ms[j] === this.name) found = true;
                        }
                        // if we didn't get a match, mark this one
                        if (found === false) {
                            mission.markSystem({ system: hs[i], name: this.name, markerShape: "MARKER_DIAMOND", markerColor: "purpleColor", markerScale: 2 });
                        }
                    }
                } else {
                    // if there aren't any marked systems at all, just add all our home systems
                    for (let i = 0; i < hs.length; i++) {
                        mission.markSystem({ system: hs[i], name: this.name, markerShape: "MARKER_DIAMOND", markerColor: "purpleColor", markerScale: 2 });
                    }
                }
            }
        }

        //-------------------------------------------------------------------------------------------------------------
        this.$addInvestmentLevelToGalCopReputations = function $addInvestmentLevelToGalCopReputations() {
            if (!worldScripts.GalCopBB_Reputation) return
            for (let gal = 0; gal <= 7; gal++) {
                let hs = this._homeSystems[gal];
                for (let i = 0; i < hs.length; i++) {
                    let hsID = parseInt(hs[i]);
                    // make sure we don't already have the custom function
                    if (this["$returnInvestmentLevel_" + gal + "_" + hsID]) continue;

                    let sysname = System.infoForSystem(gal, hsID).name;
                    let rep = worldScripts.GalCopBB_Reputation;
                    rep._entities[sysname + " (G" + (gal + 1) + ") Investment Level"] = {
                        name: expandDescription("[homesystem_invest_level]", { sys: sysname }),
                        scope: "system",
                        display: true,
                        regionSystems: [hsID],
                        regionGalaxy: gal,
                        getValueWS: "HomeSystem",
                        getValueFN: "$returnInvestmentLevel_" + gal + "_" + hsID,
                        rewardGrid: [{
                            value: 0,
                            description: ""
                        },
                        {
                            value: 1,
                            description: expandDescription("[hs_investment_level_1]")
                        },
                        {
                            value: 2,
                            description: expandDescription("[hs_investment_level_2]")
                        },
                        {
                            value: 3,
                            description: expandDescription("[hs_investment_level_3]")
                        },
                        {
                            value: 4,
                            description: expandDescription("[hs_investment_level_4]")
                        },
                        {
                            value: 5,
                            description: expandDescription("[hs_investment_level_5]")
                        },
                        {
                            value: 6,
                            description: expandDescription("[hs_investment_level_6]")
                        },
                        {
                            value: 7,
                            description: expandDescription("[hs_investment_level_7]")
                        },
                        {
                            value: 8,
                            description: expandDescription("[hs_investment_level_8]")
                        }]
                    };
                    eval("this.$returnInvestmentLevel_" + gal + "_" + hsID + " = function() { return this.$investmentLevel(" + gal + ", " + hsID + "); }");
                    rep._reputations.push({
                        entity: sysname + " (G" + (gal + 1) + " Investment Level",
                        galaxy: galaxyNumber,
                        system: hsID,
                        reputation: this.$investmentLevel(gal, hsID)
                    });
                }
            }
        }

        //-------------------------------------------------------------------------------------------------------------
        this.$removeGalCopReputation = function $removeGalCopReputation(sysID) {
            if (!worldScripts.GalCopBB_Reputation) return;
            let rep = worldScripts.GalCopBB_Reputation;
            let arr = rep._reputations;
            for (let i = arr.length - 1; i >= 0; i++) {
                if (arr[i].galaxy === galaxyNumber && arr[i].system === parseInt(sysID)) {
                    arr.splice(i, 1);
                    return;
                }
            }
            delete this["$returnInvestmentLevel_" + galaxyNumber + "_" + sysID];
        }

        //-------------------------------------------------------------------------------------------------------------
        this.$getInvestmentLevelDescription = function $getInvestmentLevelDescription(sysID) {
            return expandDescription("[homesystem_level_reached]") + " '" + expandDescription("[hs_investment_level_" + this.$investmentLevel(sysID).toString() + "]") + "'."
        }

        //-------------------------------------------------------------------------------------------------------------
        this.$investmentLevel = function $investmentLevel(galID, sysID) {
            let ic = this.$investmentCount(galID, sysID);
            let result = 0;
            if (ic > 0) {
                if (ic == 1) result = 1;
                else if (ic >= 2 && ic <= 3) result = 2;
                else if (ic >= 4 && ic <= 5) result = 3;
                else if (ic >= 6 && ic <= 8) result = 4;
                else if (ic >= 9 && ic <= 11) result = 5;
                else if (ic >= 12 && ic <= 14) result = 6;
                else if (ic >= 15 && ic <= 18) result = 7;
                else if (ic >= 19) result = 8;
            }
            return result;
        }

        //-------------------------------------------------------------------------------------------------------------
        this.$addInfoToSystemDataScreen = function $addInfoToSystemDataScreen() {
            let sysID = player.ship.targetSystem;
            if (player.ship.hasOwnProperty("infoSystem")) sysID = player.ship.infoSystem;
            // build message
            let msg = "";
            // add the investment level
            if (this.$isHomeSystem(sysID) === true) {
                msg = expandDescription("[homesystem_home]");
                let result = this.$investmentLevel(sysID);
                if (result > 0) msg += " " + expandDescription("[homesystem_level_reached]") + " '" + expandDescription("[hs_investment_level_" + result.toString() + "]") + "'.";
            }
            // add message to top part of screen, if possible
            let added = false;
            if (worldScripts["oolite-system-data-config"] && worldScripts["CompressedF7Layout"]) {
                let ln = 0;
                let dc = worldScripts["oolite-system-data-config"];
                for (let i = 16; i >= 1; i--) {
                    if (ln === 0 && dc.systemDataLineText(i) != "") {
                        ln = i + 1;
                    }
                    if (dc.systemDataLineOwner(i) === this.name) {
                        ln = i;
                        break;
                    }
                }
                if (ln <= 16 && ln > 0) {
                    added = true;
                    dc.setSystemDataLine(ln, msg, (msg != "" ? this.name : ""));
                }
            }
            // if none of the above worked, then just add the message to the bottom part of the screen
            if (added === false && msg != "") mission.addMessageText(msg);
        }

        //-------------------------------------------------------------------------------------------------------------
        // routine to check the combat simulator worldscript, to see if it's running or not
        this.$simulatorRunning = function () {
            let w = worldScripts["Combat Simulator"];
            if (w && w.$checkFight && w.$checkFight.isRunning) return true;
            return false;
        }

        //-------------------------------------------------------------------------------------------------------------
        this.$initInterface = function (station) {
            if (this._stationTypes.indexOf(station.allegiance) >= 0) {
                station.setInterface(this.name, {
                    title: expandDescription("[homesystem_station_title]"),
                    category: expandDescription("[interfaces-category-logs]"),
                    summary: expandDescription("[homesystem_station_summary]"),
                    callback: this.$displayDockCountList.bind(this)
                });
            } else {
                station.setInterface(this.name, null);
            }
        }

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

            this._pageLength = 19;
            if (this.$isBigGuiActive() === true) this._pageLength = 25;

            // build sortable dataset
            let src = this._dockCounts[galaxyNumber];
            let keys = Object.keys(src);
            let data = [];
            for (let i = 0; i < keys.length; i++) {
                data.push({
                    id: keys[i],
                    name: (this.$isHomeSystem(keys[i]) ? "•" : "") + System.systemNameForID(keys[i]),
                    count: src[keys[i]]
                }
                );
            }
            data.sort(compare);

            let text = this.$formatSystemList(data);

            mission.runScreen({
                screenID: "oolite-homesystem-dockcounts-map",
                title: expandDescription("[homesystem_screen_dockcounts]"),
                overlay: { name: "hs-home.png", height: 546 },
                message: text,
                exitScreen: "GUI_SCREEN_INTERFACES"
            });
        }

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

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

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

        //-------------------------------------------------------------------------------------------------------------
        // returns a 4-column list of planets, formatted for display
        this.$formatSystemList = function (list) {
            let lines = [];
            for (let i = 0; i < this._pageLength; i++) lines.push("");
            let point = 0;

            let colwidth = 32;
            let colcount = 1;
            if (list.length > this._pageLength) { colwidth = 16; colcount = 2; }
            if (list.length > (this._pageLength * 2)) { colwidth = 10.3; colcount = 3; }
            if (list.length > (this._pageLength * 3)) { colwidth = 8; colcount = 4; }

            for (let i = 0; i < list.length; i++) {
                lines[point] += this.$padTextRight(list[i].name, colwidth - 3) +
                    this.$padTextLeft((list[i].count <= 999 ? list[i].count.toString() : "999"), 2) +
                    this.$padTextRight(" ", 1);
                point += 1;
                if (point === lines.length) point = 0;
                if ((i + 1) >= (this._pageLength * colcount)) break;
            }
            let result = "";
            for (let i = 0; i < lines.length; i++) {
                result += lines[i] + "\n";
            }
            return result;
        }

        //-------------------------------------------------------------------------------------------------------------
        this.$investmentCount = function (galID, sysID) {
            let amt = 0;
            if (this._investCounts[galID][sysID]) amt = parseInt(this._investCounts[galID][sysID]);
            return amt;
        }

    }).call(this);
Scripts/home_system_book.js
"use strict";
this.name = "HomeSystem_Book";
this.author = "phkb";
this.description = "Adds the Home System Handbook to the Ship's Library";

//-------------------------------------------------------------------------------------------------------------
this.startUpComplete = function () {
    if (worldScripts["Ships Library"]) this._registerBook();
}

//-------------------------------------------------------------------------------------------------------------
this._registerBook = function () {
    var contents = [
        { level: 0, key: "homesystem_intro" },
        { level: 1, key: "homesystem_chap1" },
        { level: 1, key: "homesystem_chap2", 
            params: [
                function() {return (worldScripts.DayDiplomacy_060_Citizenships ? expandMissionText("homesystem_diplomacy") : "")}
            ]
        },
        { level: 1, key: "homesystem_chap3", 
            params: [
                function () { return (worldScripts["Docking Fees"] ? expandMissionText("homesystem_dockingfees") : ""); }, 
                function () { return (worldScripts.GalCopBB_Missions ? expandMissionText("homesystem_galcopmissions") : ""); }
            ]
        },
        { level: 1, key: "homesystem_chap4" },
        { level: 1, key: "homesystem_chap5" },
        { level: 0, key: "homesystem_conclusion" },
    ];

    worldScripts["Ships Library"]._registerBook("home_system", expandMissionText("homesystem_title"), contents, 21);
}
Scripts/home_system_conditions.js
(
function(){

"use strict";
this.name        = "HomeSystem_Conditions";
this.author      = "phkb";
this.copyright   = "2017 phkb";
this.description = "Condition script for Home System equipment";
this.license     = "CC BY-NC-SA 4.0";

//-------------------------------------------------------------------------------------------------------------
this.allowAwardEquipment = function(equipment, ship, context) {
    if (context == "scripted") return true;
    let w = worldScripts.HomeSystem;
    if (equipment === "EQ_HOMESYSTEM_INVEST") return (w.$homeSystemCount() > 0)
    if (w._stationTypes.indexOf(player.ship.dockedStation.allegiance) >= 0) {
        if (w.$isHomeSystem(system.ID) === false) {
            if (equipment === "EQ_HOMESYSTEM_CLEAN" && player.ship.bounty === 0) return true;
            if (equipment === "EQ_HOMESYSTEM_OFFENDER" && player.ship.bounty > 0 && player.ship.bounty < 50) return true;
            if (equipment === "EQ_HOMESYSTEM_FUGITIVE" && player.ship.bounty >= 50) return true;
        }
        if (equipment === "EQ_HOMESYSTEM_REMOVE" && w.$isHomeSystem(system.ID) === true) return true; 
    }
	return false;
}

}).call(this);