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

Expansion Email System

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 Adds an email system to the system interfaces screen. Adds an email system to the system interfaces screen.
Identifier oolite.oxp.phkb.EmailSystem oolite.oxp.phkb.EmailSystem
Title Email System Email System
Category Equipment Equipment
Author phkb phkb
Version 1.7.9 1.7.9
Tags email, comms email, comms
Required Oolite Version
Maximum Oolite Version
Required Expansions
Optional Expansions
Conflict Expansions
Information URL n/a
Download URL https://wiki.alioth.net/img_auth.php/f/f1/EmailSystem.oxz n/a
License CC-BY-NC-SA 4.0 CC-BY-NC-SA 4.0
File Size n/a
Upload date 1613077187

Documentation

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

readme.txt

================================================================================
Introduction

For a long time Oolite players have been receiving mission screens that array themselves as an email message. This makes sense in-game, as you would imagine a lot of communication between the player and NPC's in the game world would take place inside the bounds of an email system. However, while the conceit of an email system has been present, many other aspects of a true email system are lacking. In particular:

1. The ability to re-read emails after you receive them
2. The ability to analyse the trace of an email (ie, the path the email took to reach the player).

The Email System OXP attempts to address these shortcomings and provide (as much as possible) a true, in-game email system.

Big thanks go to Wildeblood who pushed me not to compromise on functionality. Thanks to Norby who is always willing to contribute valuable ideas to any project. Thanks to Disembodied for some wonderful suggestions on new parts of the system. Thanks to Astrobe for his help with the escape pod emails. And huge thanks to cim, who is an amazing help with any and all technical questions relating to Oolite.

********************************************************************************
Standard Operations

The OXP adds a new option on the "Interfaces" (F4) screen called "Email system". The number of unread emails will be displayed here.

After opening the email system, the player will see their inbox, sorted with the most recently received email at the top. The inbox view shows the senders name or email address, the date the email was sent, and the subject line of the email. A "!" symbol beside the email indicates it is unread.

At the bottom of the screen are the functions the player can select. They are:

	Go to next page: If the inbox flows to multiple pages, this option will go to the next page.
	Go to previous page: If the inbox flows to multiple pages, this option will go to the next page.
	Mark all items read: This option will remove the unread "!" flag from all emails in your inbox.
	Delete all read items: This option will delete all emails that have been read and are not waiting for a response from the player.
	Delete all items: This will delete all emails that are not waiting for a response from the player.
	Exit email system: This will exit the email system and return the player to the Interfaces F4 screen.

To open an email, use the up and down arrow keys to move the highlight bar to the desired email and press enter. Opening an email will automatically flag it as read.

When an email is opened, at the top of the screen will be the senders name or email address, the date the email was sent, and the subject line of the email.

Below this is the content of the email.

At the bottom of the screen are functions the player can perform on this email. They are:
	Close email: This will close the email and return the player to the inbox. The email will now be flagged as "read".
	Close email and open next: This will close the current email and open the next email in the inbox.
	Close and delete email: This will close the email and delete it from the inbox if there are no response required by the player.
	Show trace: This will open the email trace, which will display the path the email took to reach the player.

Some emails can require the player to make a response. When an email requires a response, there will be additional options below the "Show trace" option. The format of these options will be "Send 'option' response." For instance, an email could ask the player "Do you want to join our team?". In this case the options might be:

	Send 'Yes' response
	Send 'No' response

The player can select either of these options and press enter to send the response.

When an email has previously had a response sent, additional text will be added to the email body, similar to an email trail. Continuing the example above, if the player has response "Yes", the email display might end up looking like this:

	From:	   Commander Curruthers
	Sent:	   2084504:05:16:02
	Subject:   Request for assistance
	-----------------------------------------------------------------------
	Commander Jameson,

	Her Majesty is in need of your assistance. Thargoid incursions are on
	the increase, and we need your help in the battle. Would you be willing
	to join us in the fight against this rising tide of evil?

	-----------------------------------------------------------------------
	Reply sent: 2084504:05:29:42
	Reply:

	Commander Curruthurs, it would be an honour.

********************************************************************************
External Interfaces

Other OXP's can make use of the email system by calling the $createEmail function.

================================================================================
** $createEmail**
The $createEmail function accepts a single object, that can have the following properties:
Required fields:
	sender				(text) Name of person sending email
	subject				(text) Subject line of email
	date				(time in seconds) the global.clock time the email was sent
Optional fields:
	message				(text) Body text of email
	sentFrom			(int) ID of Planet that is the source of the email. Defaults to the current planet.
						If the optional stopTrace flag is set, this ID will be the last planet in the trace.
	isRead				(boolean) Setting this to true will add the email to the inbox as if its been read by the player.
						This might be useful if you have displayed the message to the  player using "addMessageToArrivalReport" or a mission screen,
						and want to add  a corresponding email, which, because the player has seen it, should be flagged as read.
	expiryDate			(time in seconds) The time this email will expire and be deleted (if no expiryText is set). Alternatively, use the following params
	expiryDays			(int) number of days past the current date when the email will expire
	expiryHours			(int) number of hours past the current date when the email will expire
	expiryMinutes		(int) number of minutes past the current date when the email will expire
	expirySeconds		(int) number of seconds past the current date when the email will expire
						Note: the above expiry options can be combined: to expire an email 2 hours and 30 minutes in the future, use expiryHours:2, expiryMinutes:30
	expiryText			(text) Text to display in the header when the email expired. If this text is not supplied, the email will be deleted when it expires.
	expiryOptions		(csv text) The response option numbers to display when the email expires.  eg "3,4" would mean that option numbers 3 and 4 will be visible when the email expires.
	allowExpiryCancel	(boolean) Indicates whether the option to cancel the expiry notice will be avaiable to the player. Defaults to true if not supplied.
	stopTrace			(boolean) Indicates that the routing ticket is corrupt and a trace is only partial. The trace will terminate at the "sentFrom" planet. Defaults to false.
	traceRoute			(csv text) Allows the trace route information to be fully specified. If not specified, the route will be all planets between sentFrom and the current planet using the fastest route.
	forceResponse		(boolean) Indicates that the player must select a response before the email can be closed. Defaults to false.
	option1				(object) Response option 1
	option2				(object) Response option 2
	option3				(object) Response option 3
	option4				(object) Response option 4

The Response options have the following format:
Required fields:
	display				(text) Text to display to the user. Shown as "Send 'displayText' response".
	reply				(text) The text to be appended to the body of the email as the reply from the player.
	script				(text) The name of the worldScript where the callback function resides
	callback			(text) The name of the function to call
Optional fields:
	parameter			(object) The object to pass to the callback function.

********************************************************************************
Examples

In is most simplest form, sending an email is done as follows

	var w = worldScripts.EmailSystem;
	w.$createEmail(
		{sender:"Captain Solo",					// senders name or email address
		subject:"I've got a bad feeling about this",		// subject line
		date:global.clock.seconds,				// the time the email was sent
		message:"I thought they smelled bad on the outside."	// body text of the email
		});

In this form, there is no response required by the player. Also, the email trace will show that the email was sent from the current planet. To expand this example, lets add a longer trace.

	var w = worldScripts.EmailSystem;
	w.$createEmail(
		{sender:"Captain Solo",					// senders name or email address
		subject:"I've got a bad feeling about this",		// subject line
		date:global.clock.seconds,				// the time the email was sent
		message:"I thought they smelled bad on the outside.",	// body text of the email
		sentFrom:14						// id of planet from which the email was sent.
		});

Now, when the player opens the email trace, the system will show all the planets between planet ID 14 and the players current planet. We can force a particular trace on this email by adding in a comma-separated list of items into the traceRoute parameter.

	var w = worldScripts.EmailSystem;
	w.$createEmail(
		{sender:"Captain Solo",					// senders name or email address
		subject:"I've got a bad feeling about this",		// subject line
		date:global.clock.seconds,				// the time the email was sent
		message:"I thought they smelled bad on the outside.",	// body text of the email
		traceRoute:"Lave,Isinor,Endor,Tatooine,Hoth"		// csv list of items to appear in the email trace.
		});

The trace is ordered from the last to the first. That is, the end point of the trace (where the player receives the message) is at the start of the list. Note that we don't have to include planets - we can include anything we like in the trace when entered like this.

We might want to make it appear as though the trace is corrupted, which might be useful in some circumstances. To do this, set the stopTrace flag.

	var w = worldScripts.EmailSystem;
	w.$createEmail(
		{sender:"Captain Solo",					// senders name or email address
		subject:"I've got a bad feeling about this",		// subject line
		date:global.clock.seconds,				// the time the email was sent
		message:"I thought they smelled bad on the outside.",	// body text of the email
		sentFrom:14,						// id of planet from which the email was sent.
		stopTrace:true						// the trace is now corrupt
		});

What this will do is show the trace from the players current planet to planet ID 14, but the message "Routing ticket corrupt" will be displayed, which would indicate to the player that there was more to the trace but it's now hidden.

All of these examples will add an email to the inbox, but there are no options attached. They are simple emails the player can view and delete as required. An example of an email with some options is below:

	var w = worldScripts.EmailSystem;
	w.$createEmail(
		{sender:"The Chaser",				// senders name or email address
		subject:"Something for you do look at...",	// subject line
		date:global.clock.seconds,			// the time the email was sent
		message:"Would you like to play a game?",	// body text of the email
		option1:{display:"Yes", reply:"Sure. Why not?", script:"EmailSystemDemo", callback:"$AcceptChallenge"},
		option2:{display:"No", reply:"Sorry. Too busy right now.", script:"EmailSystemDemo", callback:"$DeclineChallenge"}
		});

In this example, the player is asked if they want to play a game in the body of the email. Then, two options are configured. The first option will be shown to the player as "Yes", the second as "No."

If the player selects the "Yes" option, the text "Sure. Why not?" will be appended to the body of the email as the reply, and the $AcceptChallenge function will be called in the EmailSystemDemo worldScript.

If the player selects the "No" option, the text "Sorry. Too busy right now. And I don't play games." will be appended to the body of the email as the reply, and the $DeclineChallenge function will be called in the EmailSystemDemo worldScript.

A paramater can be passed to the callback function by using the "param" parameter:

	var w = worldScripts.EmailSystem;
	w.$createEmail(
		{sender:"The Chaser",				// senders name or email address
		subject:"Something for you do look at...",	// subject line
		date:global.clock.seconds,			// the time the email was sent
		message:"Would you like to play a game?",	// body text of the email
		option1:{display:"Yes", reply:"Sure. Why not?", script:"EmailSystemDemo", callback:"$AcceptChallenge", parameter:"mydata"},
		option2:{display:"No", reply:"Sorry. Too busy right now.", script:"EmailSystemDemo", callback:"$DeclineChallenge"}
		});

Emails can be set to expire by using expiryDate, expiryDays, expiryHours, expiryMinutes and expirySeconds. In this example, the email will expire in 2 minutes and 30 seconds. When the email expires it will simply be deleted from the users inbox.

	var w = worldScripts.EmailSystem;
	w.$createEmail(
		{sender:"The Chaser",				// senders name or email address
		subject:"Something for you do look at...",	// subject line
		date:global.clock.seconds,			// the time the email was sent
		message:"Would you like to play a game?",	// body text of the email
		expiryMinutes:2,
		expirySeconds:30,
		option1:{display:"Yes", reply:"Sure. Why not?", script:"EmailSystemDemo", callback:"$AcceptChallenge", parameter:"mydata"},
		option2:{display:"No", reply:"Sorry. Too busy right now.", script:"EmailSystemDemo", callback:"$DeclineChallenge"}
		});

An email with an expiry date will present the user with an option to "Cancel expiry". That will remove the expiry date from the email. If you want to remove that option from the player, use the allowExpiryCancel flag,

	var w = worldScripts.EmailSystem;
	w.$createEmail(
		{sender:"The Chaser",				// senders name or email address
		subject:"Something for you do look at...",	// subject line
		date:global.clock.seconds,			// the time the email was sent
		message:"Would you like to play a game?",	// body text of the email
		expiryMinutes:2,
		expirySeconds:30,
		allowExpiryCancel:false,
		option1:{display:"Yes", reply:"Sure. Why not?", script:"EmailSystemDemo", callback:"$AcceptChallenge", parameter:"mydata"},
		option2:{display:"No", reply:"Sorry. Too busy right now.", script:"EmailSystemDemo", callback:"$DeclineChallenge"}
		});

You might not want the email to be deleted, but to remain in an expired state. To do this, set the expiryText option:

	var w = worldScripts.EmailSystem;
	w.$createEmail(
		{sender:"The Chaser",				// senders name or email address
		subject:"Something for you do look at...",	// subject line
		date:global.clock.seconds,			// the time the email was sent
		message:"Would you like to play a game?",	// body text of the email
		expiryMinutes:2,
		expirySeconds:30,
		allowExpiryCancel:false,
		expiryText:"This email has expired",
		option1:{display:"Yes", reply:"Sure. Why not?", script:"EmailSystemDemo", callback:"$AcceptChallenge", parameter:"mydata"},
		option2:{display:"No", reply:"Sorry. Too busy right now.", script:"EmailSystemDemo", callback:"$DeclineChallenge"}
		});

If you have options on the un-expired email, those options will be hidden once the email expires. However, you can add response options that are only available when the email expires using the "expiryOptions" parameter. In this expiry, option 3 has been defined as an expiry option, which will only be visible after the email expires:

	var w = worldScripts.EmailSystem;
	w.$createEmail(
		{sender:"The Chaser",				// senders name or email address
		subject:"Something for you do look at...",	// subject line
		date:global.clock.seconds,			// the time the email was sent
		message:"Would you like to play a game?",	// body text of the email
		expiryMinutes:2,
		expirySeconds:30,
		allowExpiryCancel:false,
		expiryText:"This email has expired",
		expiryOptions:"3",
		option1:{display:"Yes", reply:"Sure. Why not?", script:"EmailSystemDemo", callback:"$AcceptChallenge", parameter:"mydata"},
		option2:{display:"No", reply:"Sorry. Too busy right now.", script:"EmailSystemDemo", callback:"$DeclineChallenge"}
		option3:{display:"Too late", reply:"Sorry I didn't respond to your email in time. Can I still join in?", script:"EmailSystemDemo", callback:"$TooLate"}}
		});

================================================================================
Why should you use this system?

Oolite already has mission screens with selectable options and callbacks, so why use this system?

This OXP is *not* designed to replace mission screens and their associated methods, although it can certainly do many of the same things. Instead, the intention is to provide a way to add extra realism to the game. If your mission screen sends information to the user and formats it like an email, keep doing this. But consider adding a couple of lines of code to check for the email system and add a new email item so the player has a record of the emails they have received. They can view their historic emails for reminders of mission parameters, or to find hidden clues. They can view the email trace that could help track down a target.

The Email System can provide an additional option for OXP developers for enhancing the realism in their missions.

Third Party Equipment in Maintenance Overhaul Emails
====================================================
In general, any third party equipment items will have a "Perform diagnostics" item in the maintenance overhaul email. But it is possible to include special maintenance description items for third party equipment by following these steps.

1. Include an item in your descriptions.plist file similar to this:
	maint_EQ_YOUR_EQUIPMENT_ID = ("One or more maintenance description items");

2. Include the following code in your startUp or startUpComplete scripts:
	var g = worldScripts.GalCopAdminServices;
	if (g) g._maint_known_equip.push("EQ_YOUR_EQUIPMENT_ID");

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/

Sound files from "Robot Blip Sound" WAV file, created by Marianne Gagnon and sourced from soundbible.com. Licenced under Creative Commons Attribution 3.0.
Images from http://simpleicon.com/message_8.html, http://simpleicon.com/message.html

Discussion
==========
This OXP is discussed at this forum link: http://aegidian.org/bb/viewtopic.php?f=4&t=17216

Version History
===============
1.7.9
- Fixed issue with NaN showing in some purchase emails.
- Fixed issue with Repair Bots recharge items showing as removal emails.
- Fixed issue with IronHide Military upgrades showing as removal emails.
- Fixed issue with Passenger berth purchases showing as removal emails.

1.7.8
- Message ID now returned to caller from $createEmail function.
- Fines processing only occurs at main system station (not at secondary stations, even those with police and with allegiance of "galcop"). 
- Fix for Target Autolock Plus purchase email.

1.7.7
- Bug fixes.

1.7.6
- Better handling of purchasing equipment emails when equipment item is for the removal of something.
- Better handling of purchasing equipment emails when price is calculated via a condition script.

1.7.5
- Spelling corrections.
- New ship email had additional zero on cost of new ship.

1.7.4
- Cost of new ship in the new ship email now formatted to include cr symbol.
- Small change to format of all script and text files.
- Included check for a new Elite rank of "Harmless".
- Code refactoring.

1.7.3
- When future dated emails arrive in player's inbox, the trace route will now reflect the system they receive the email in, not the system where the email was generated.
- A console message will appear when future dated emails arrive in the player's inbox.

1.7.2
- Fixed issue where missile kills were not being noted in bounty emails.

1.7.1
- Remove debug message in new ship email.
- Added missing Elite Federation email subject for Above Average.
- Fixed repair email, which was not being triggered under certain conditions.

1.7.0
- Fixed issue where docking fine email was being sent after using an escape pod.
- Added new email for when the player purchases, uses or sells an escape pod.
- Better handling of sale of items via Ship Configuration.

1.6.8
- Player will now be notified of unread emails that require a response whenever they dock at a station.
- If player has unread emails requiring a response, the interface screen entry will include a token in its text.
- Fixed issue where emails having expired responses could not be deleted.
- Fixed issue with new ship email, where "Remove laser" was appearing in laser mounts that didn't have anything (or really, had EQ_WEAPON_NONE installed).
- Fixed issue where the sales rep name was missing from new ship emails.
- Fixed issue where equipment repair emails had "NaN.NaN" for the repair cost.
- Fixed issue where bounty payments were being overstated for Pirate coves (or any piloted entity defined with scanClass "CLASS_ROCK").
- Added some configuration items to Library Config to control what emails are sent.

1.6.7
- Updated check for "Allow Big GUI".
- Fixed Javascript error when setting up rep names.
- Switched name generator to use "randomName".
- Updated maintenance overhaul email so that it won't try to lookup equipment keys that aren't known to have a "maint_" item in descriptions. This should prevent a lot of unnecessary Javascript error messages about expansion keys not found.
- Updated output of all credit amounts to use the formatCredits function.
- Changed "==" comparisons to "===" for performance improvements.
- Better handling of interstellar space conditions.
- Better menu handling, where the current email will still be selected in the list when returning from viewing it.
- Fixed issue with HUD not becoming visible again when launching while viewing the Email list.
- Code cleanup.

1.6.6
- Updated screenID's to enable BGS background sounds.
- Renamed background overlay images to prevent possibility of future duplication.
- Toned down overlay images.
- Bug fixes.

1.6.5
- Further attempts to fix Javascript timeouts on Mac.
- Code refactoring.

1.6.4
- Really fixed the Javascript timeout issue.
- New ship email was including equipment items that weren't visible.

1.6.3
- Added check to fine email for a zero calculation.
- Added check to repair email for a zero payment.
- Fixed issue where launching within 1 second of purchasing a new item would generate a Javascript error.
- Fixed variable naming issue.
- Fixed Javascript timeout issue.
- Bug fixes and code cleanup.

1.6.2
- Improvements to the "marked for fines" email - added additional checks for bounty threshold, suppression of arrival reports and sun going nova.
- Improvements to the "docking fine" email - added additional check for sun going nova.
- Switched to use "clock.adjustedSeconds" as the date sent in most GalCop emails.

1.6.1
- Spelling corrections.
- Small tweaks to the IronHide repair email.
- Tweaks to the repair email, so items that have "repair" in the name don't get "Repair of" at the beginning of the item (eg "Repair of Emergency Hull Repairs").

1.6.0
- Added overlay background image to interface screens.
- Changed color of menu items on interface screens, so it's less yellow.
- Really fixed issue with maintenance email and no weapon mounts.
- Other maintenance email bug fixes.

1.5.2
- Fixed issue with maintenance email, where maintenance was said to be performed on weapon mounts with no weapons installed.

1.5.1
- Added more equipment exclusions for the Smuggling OXP.
- Fixed a small bug in the inbox compilation routine that would only have reared it's head if your inbox was ever empty.
- Fixed issue where the routine to clear out old emails was removing the most recent first instead of the oldest.
- Added extra process to the routine to clear out old emails to remove expired items first.

1.5.0
- Added some equipment exclusions for the Smuggling OXP.
- Moved lists of equipment items from descriptions and into proper arrays.
- Added routine to use 1.83/4 code to check for big GUI HUD's.

1.4.20
- Fixed issue with new rank email referencing incorrect array variable.

1.4.19
- Added specific docking fine email for Black Monks monestary.
- Moved list of Federation HQ planet ID's to be accessible to external OXP's (via worldScripts.GalCopAdminServices._fedHQ array), in case it's ever needed.
- Switched back to "shipKilledOther" so untargeted ships destroyed by the player (eg via missile) are included in the kill count and email.

1.4.18
- Checks for the "allow_big_gui" HUD option.
- Escape pod recovery emails now turn up immediately.
- Added RRS refueling to list of equipment items excluded from purchase emails.
- Reworked the procedure for when the player buys equipment, to ensure the email process happens after any other OXP process.
- Added condition for when the purchase price of an item is 0 (zero) credits (eg removing lasers).

1.4.17
- Fixed the fine amount calculation on the fine email (really totally for sure this time).
- Put option text into descriptions.plist file.

1.4.16
- Added ellipsis to columns when text is truncated
- Slight adjustment to column widths to cater for wider fonts
- Removed seconds component from email items on the inbox display. The full sent date/time, including seconds is still visible when you open an email.

1.4.15
- Fixed an issue with the new parcel and passenger contracts, where the contract name wasn't being added to the email correctly.

1.4.14
- Fixed an issue with purchasing passenger berths, where an incorrect email was being sent.

1.4.13
- Fixed an issue with the rescued escape pod email, where a substitution was not entered correctly.
- Fixed issue with maintenance email. Report will now only list items that have "isVisible = true".

1.4.12
- Code improvements as suggested by Wildeblood.

1.4.11
- Speed improvements as suggested by Norby.

1.4.10
- Small bug fix when checking Combat Simulator
- Added exclusion for extra ship respray equipment item

1.4.9
- Fixed bug where the new pilot registration email was not being sent for new pilots. Instead, the late notice email was being sent.

1.4.8
- Corrected a minor bug with calculating the route to another system. Had "OPTIMISED_BY_TIME" instead of "OPTIMIZED_BY_TIME".
- Faster sorting algorithm
- Added exclusion for "Ship Respray" OXP.

1.4.7
- Fixed issue where using the Combat Simulator OXP would generate invalid emails

1.4.6
- Better error handling of maint_itemname code.
- Fixed manifest version number issue

1.4.5
- Added some more equipment key exclusions for the maintenance email (ShipVersion OXP items)
- Added some special cases for ShipVersion repair equipment items.
- Switched from shipKilledOther to shipTargetDestroyed to better monitor for ships destroyed by the player
- Small bug fixes and tweaks.

1.4.4
- Fixed grammar issue with bounty emails in Interstellar space.
- Added some more exams to the new pilot registration email
- Fixed decimal number issue with docking fines and other places.
- Fixed issue with docking fines at some stations, where the wrong name was showing in the duty officers email signature.

1.4.3
- You can now exit from the email system just by pressing another function key.

1.4.2
- Attempt to fix issue when the number of unread emails on the interfaces screen was not updating in some circumstances

1.4.1
- Made the licence number longer (it's a big galaxy after all)
- Moved the licence email out of the block that checks for a new ship. This is so users of the Hardships OXP will at least get a pilot's licence when they start.
- Fixed a bug with saving and loading to mission variables.

1.4.0
- Changed the inbox UI to be similar to contract interfaces, where you highlight the desired item and press enter to open the email.
- Added a couple of other GalCop admin-related emails, relating to transfer of ownership of ships, and pilot licensing.
- Layout tweaks to maintenance email, adding some headings
- Small tweaks to the text of bounty emails.
- Small grammar corrections.

1.3.0
- Improved the purchase equipment email to handle removal of equipment better.
- Added equipment description to the purchase equipment email.
- Made the random names a little less random. Now, multiple purchases of equipment will have the same sales rep name while you are docked at that station.
- Improved compatibility with Hardships. You won't get an initial "Welcome to your new ship" with Hardship's start choices, but you also won't get spammed either.
- Included no bounty kills in the bounty email, with appropriate notices
- Added the number of kills to the bounty email
- Future-dated emails are now hidden until their sent date is in the past. This means you can send emails at any time, but with a future date, and player won't see them until the right time.
- ExpiryDays, ExpiryHours, ExpiryMinutes and ExpirySeconds are now linked to the sent date, rather than to the current date. So if you future date an email and give it an expiry of 2 days, that will be 2 days after the future date, not the current date.
- Added a tone for when future dated emails become current. Will only play when docked.
- Code cleanup.

1.2.0
- Fixed bug with the equipment purchase email, when the wrong price was being shown when equipment was bought in any non-main station.
- Added emails from the Elite Federation for changes in player rank
- Improved the new ship email content, including the method of calculating the cost of the new ship.
- Improved default option selection on multi-page emails
- Improved the bounty email. If there are several systems between dockings, the email will include the system name the bounties were collected in.
- Added more maintenance items for overhaul email, including notation for damaged items
- New ship email will now be sent whenever a new game is started (hopefully this will catch most scenarios as well as the standard ones)
- Fixed the calculation of the fine amount
- Fixed bug with failed passenger contracts subject line
- Fixed issue with overhaul email and ships without hyperspace capability (email might have included witchdrive maintenance items)
- Removed expiry date from new contract emails
- Spelling corrections

1.1.5
- Fixed problem with recursive function on Macs. Removed recursion.

1.1.4
- Added GalCop emails for purchasing equipment, new ships, and maintenance overhauls.

1.1.3
- Remove the %R special expansion and replaced with [nom] for better random name generation.

1.1.2
- Removed arrival report message. If you really want it, you can turn it on with a setting.
- Fixed issue with the bounty report email if the player destroys a ship with no bounty. No entry will be made in this case now.
- Added an archive limit, so that emails will automatically be start to be deleted if they have 100 emails or more.
- Added an expiry date to all the standard galcop notices.
- Simplified $createEmail object parameters.
- added a zero check on expiry time
- fixed bug where number of pages wasn't updating when deleting emails.
- spelling corrections

1.1.1
- Removed unnecessary OXP folders, combines GALCOP Admin services into main OXP, ready for OXZ publication
- Fixed bug when calling $createEmail during flight (thanks to Wildeblood for the heads up)
- Removed unnecessary option on the $createMail function
- Added message to arrival report if the player has unread emails.

1.1.0
- Fixed bug with closing a long email on a page greater than 1 and opening the next email. Thanks to Wildeblood for the bug report.
- Added ExpiryDate option (thanks to Wildeblood for the suggestion)
- Reworked the response options so that there is no need for OXP's to reconnect callback functions.
- Changed the save format in mission variables to use a single value, rather than multiple values, for emails.
- Code cleanup (thanks to Wildeblood for the pointers)

1.0.1
- Changed createEmail function call to use an object, rather than parameters.
- Added galaxy number to stored emails, which will be displayed when the player is in a galaxy other than the one they received the email in.
- fixed bug that was preventing bounty email from being sent
- fixed bug that was sending a docking clearance penalty notice when docking clearance was about to expire
- Spelling fixes
- Code cleanup

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/emailsystem_base.js
"use strict";
this.name        = "EmailSystem";
this.author      = "phkb";
this.copyright   = "2017 phkb";
this.description = "Implements a simple email system via the interfaces screen.";
this.licence     = "CC BY-NC-SA 4.0";

// specific settings for this oxp
this._maxpage = 0;              		// total number of pages of inbox to display
this._curpage = 1;              		// the current page of the inbox being displayed
this._msRows = 18; //15					// rows to display on the mission screen
this._msCols = 32;						// columns to display on the mission screen
this._itemsOnPage = 0;					// number of emails being displayed on a page
this._displayType = 0;					// controls the view. 0 = inbox, 1 = email, 2 = trace
this._emailPage = 0;					// which page of the email are we viewing.
this._emailMaxPages = 0;				// total number of pages in the email we are viewing
this._emails = [];						// main array of emails
this._emailID = 0;						// id of the email we are viewing
this._disableEmailNotification = false;	// disables unread email notification when docking
this._notifyMainStationOnly = false; 	// unread email notification at main stations only
this._archiveLimit = 100;				// number of emails to keep before they start getting deleted automatically
this._updateRequired = true;			// flag to indicate the interface screen needs updating because a new email was added
this._newMailAlert = null				// timer for future dated new emails arriving
this._pageItems = [];
this._lastChoice = ["", "", ""];
this._emailOpen = false;
this._itemColor = "yellowColor";
this._menuColor = "orangeColor";
this._exitColor = "yellowColor";
this._disabledColor = "darkGrayColor";
this._readColumn = 0.4;

//=============================================================================================================
// set up initial variables
//-------------------------------------------------------------------------------------------------------------
this.startUp = function() {
	this._hudHidden = false;
	this._emailOpen = false;
}

//-------------------------------------------------------------------------------------------------------------
this.startUpComplete = function() {
	// add the welcome email here. If this is the first time we've used the system, there won't be any data to load from mission variables
	// so it will not be overwritten by restoring saved values
	// once there are saved emails to restore, this email will disappear
	this.$createEmail({sender:"Xenon Industries",
		subject:"Welcome to your inbox!",
		date:clock.seconds - 100,
		message:"Xenon Industries is proud to welcome you to your new inbox. We hope you enjoy the facilities this system provides.\n\n"
			+ "Xenon Industries have been working behind the scenes for many years providing message facilities to spacers from "
			+ "all walks of life. However, it was only after GalCop regulation 3827B (Sub-section A) was passed that we "
			+ "have been allowed to provide a fully functional email system to any space going vessel. Even with this "
			+ "regulation, email facilities are only operational while dockside, as GalCop regulations make it clear than "
			+ "emailing while flying is a serious offence.\n\nDespite this, we know you will love all the benefits a good email "
			+ "client can provide, and we believe we have all of this and more.\n\n"
			+ "Thank you for using Xenon Industries's email system, and we trust it will perform flawlessly for you for years to come."}, false);

	if (defaultFont.measureString("!") > 0.4) this._readColumn = defaultFont.measureString("!") + 0.1;

	// restore saved data if any exists
	if (missionVariables.EmailSystem_Emails) {
        this._emails = JSON.parse(missionVariables.EmailSystem_Emails);
        delete missionVariables.EmailSystem_Emails;
    }

	this.$checkForFutureDatedEmails();

	var p = player.ship;
	if(p.docked) this.$initInterface(p.dockedStation);
}

//=============================================================================================================
//public interfaces
//-------------------------------------------------------------------------------------------------------------

//-------------------------------------------------------------------------------------------------------------
// Create a new email
// REQUIRED
// emailObj.sender					(text) Name of person sending email
// emailObj.subject					(text) Subject line of email
// emailObj.date					time in seconds
// OPTIONAL
// emailObj.message					(text) Body text of email
// emailObj.sentFrom				(int) ID of Planet that is the source of the email. Defaults to the current planet.
//										If the optional stopTrace flag is set, this ID will be the last planet in the trace.
// emailObj.isRead					(boolean) setting this to true will add the email to the inbox as if it's been read by the player.
//										this might be useful if you have displayed the message to the player using "addMessageToArrivalReport",
//										and want to add a corresponding email, which, because the player has seen it, should be flagged as read. Default false
// emailObj.stopTrace				(boolean) Indicates that the routing ticket is corrupt and a trace is only partial. The trace will terminate at the "sentFrom" planet. Default false.
// emailObj.traceRoute				(csv text) Allows the trace route information to be fully specified. If not specified, the route will be all planets between sentFrom and the current planet
//										using the fastest route.
// emailObj.expiryDate		    	time in seconds when email will expire
// emailObj.expiryDays				Number of days added to current date
// emailObj.expiryHours				Number of hours added to current date
// emailObj.expiryMinutes			Number of minutes added to current date
// emailObj.expirySeconds			Number of seconds added to current date
// emailObj.expiryText				Text to display in header when email expires
// emailObj.allowExpiryCancel		(boolean) indicates whether the expiry date of the email can be cancelled. (default true)
// emailObj.expiryOptions			(csv text) csv list of which options to display after expiry (possible values in list are 1,2,3 or 4) eg "3,4"
// emailObj.forceResponse			(boolean) Indicates that the player must select a response before the email can be closed. Default false
// emailObj.option1					(ResponseOption) Response option 1
// emailObj.option2					(ResponseOption) Response option 2
// emailObj.option3					(ResponseOption) Response option 3
// emailObj.option4					(ResponseOption) Response option 4
//
// Response options are an object:
// response.display					(text) Text to display on email. Will be slotted into "Send 'xxx' response"
// response.reply					(text) Text to be appended to the email as a reply.
// response.script					(text) the name of the worldScript where the callback function is located
// response.callback				(text) the name of the function to call when the reply is selected
// response.parameter				(object) object to pass to the function when it is called
this.$createEmail = function(emailObj) {
	// using parameters
	if (emailObj.hasOwnProperty("sender") === false || emailObj.sender === "") {
		throw "Invalid settings: sender must be supplied.";
	}
	if (emailObj.hasOwnProperty("subject") === false || emailObj.subject === "") {
		throw "Invalid settings: subject must be supplied.";
	}
	if (emailObj.hasOwnProperty("date") === false || emailObj.date === 0) {
		throw "Invalid settings: date must be supplied.";
	}
	if (emailObj.hasOwnProperty("sentFrom") === true) {
		if (emailObj.sentFrom < 0 || emailObj.sentFrom > 255) {
			throw "Invalid settings: sentFrom (planet ID) must be supplied and be in range 0-255.";
		}
	}

	var id = this.$nextID();

	var ebody = "";
	if (emailObj.hasOwnProperty("message") === true) ebody = emailObj.message;

	var planet = system.ID;
	if (emailObj.hasOwnProperty("sentFrom") === true) planet = emailObj.sentFrom;

	var stoptr = false;
	if (emailObj.hasOwnProperty("stopTrace") === true) stoptr = emailObj.stopTrace;

	var read = false;
	if (emailObj.hasOwnProperty("isRead") === true) read = emailObj.isRead;

	var trc = "";
	if (emailObj.hasOwnProperty("traceRoute") === true) {
		trc = emailObj.traceRoute;
	} else {
		trc = this.$populateTrace(planet);			// text: CSV list of planets in the trace
	}

	var forcersp = false;
	if (emailObj.hasOwnProperty("forceResponse") === true) forcersp = emailObj.forceResponse;

	var expDate = 0;
	if (emailObj.hasOwnProperty("expiryDate") === true) expDate = emailObj.expiryDate;
	if (emailObj.hasOwnProperty("expiryDays") === true || emailObj.hasOwnProperty("expiryHours") === true || emailObj.hasOwnProperty("expiryMinutes") === true || emailObj.hasOwnProperty("expirySeconds") === true) {
		var addsec = 0;
		// convert all values to seconds
		if (emailObj.hasOwnProperty("expiryDays") === true) addsec += emailObj.expiryDays * 24 * 60 * 60;
		if (emailObj.hasOwnProperty("expiryHours") === true) addsec += emailObj.expiryHours * 60 * 60;
		if (emailObj.hasOwnProperty("expiryMinutes") === true) addsec += emailObj.expiryMinutes * 60;
		if (emailObj.hasOwnProperty("expirySeconds") === true) addsec += emailObj.expirySeconds; // don't know why you'd want to, but just in case
		// add the expiry amount to the transmit date, so a future dated email with expiry will not expire before it's visible
		if (addsec > 0) expDate = emailObj.date + addsec;
	}

	var expText = "";
	if (emailObj.hasOwnProperty("expiryText") === true) expText = emailObj.expiryText;

	var expOptions = "";
	if (emailObj.hasOwnProperty("expiryOptions") === true) expOptions = emailObj.expiryOptions;
	if (expOptions != "" && expText === "") {
		throw "Invalid settings: expiryOptions have been defined but no expiryText has been set. Options will never be available to player.";
	}

	var expCancel = true;
	if (emailObj.hasOwnProperty("allowExpiryCancel") === true) expCancel = emailObj.allowExpiryCancel;

	var opt1 = {DisplayText:"", ReplyText:"", WorldScriptsName:"", CallbackFunction:"", FunctionParam:{}};
	if (emailObj.hasOwnProperty("option1") === true) {
		opt1.DisplayText = emailObj.option1.display;
		opt1.ReplyText = emailObj.option1.reply;
		opt1.WorldScriptsName = emailObj.option1.script;
		opt1.CallbackFunction = emailObj.option1.callback;
		if (emailObj.option1.hasOwnProperty("parameter") === true) opt1.FunctionParam = emailObj.option1.parameter;
	}
	var opt2 = {DisplayText:"", ReplyText:"", WorldScriptsName:"", CallbackFunction:"", FunctionParam:{}};
	if (emailObj.hasOwnProperty("option2") === true) {
		opt2.DisplayText = emailObj.option2.display;
		opt2.ReplyText = emailObj.option2.reply;
		opt2.WorldScriptsName = emailObj.option2.script;
		opt2.CallbackFunction = emailObj.option2.callback;
		if (emailObj.option2.hasOwnProperty("parameter") === true) opt2.FunctionParam = emailObj.option2.parameter;
	}
	var opt3 = {DisplayText:"", ReplyText:"", WorldScriptsName:"", CallbackFunction:"", FunctionParam:{}};
	if (emailObj.hasOwnProperty("option3") === true) {
		opt3.DisplayText = emailObj.option3.display;
		opt3.ReplyText = emailObj.option3.reply;
		opt3.WorldScriptsName = emailObj.option3.script;
		opt3.CallbackFunction = emailObj.option3.callback;
		if (emailObj.option3.hasOwnProperty("parameter") === true) opt3.FunctionParam = emailObj.option3.parameter;
	}
	var opt4 = {DisplayText:"", ReplyText:"", WorldScriptsName:"", CallbackFunction:"", FunctionParam:{}};
	if (emailObj.hasOwnProperty("option4") === true) {
		opt4.DisplayText = emailObj.option4.display;
		opt4.ReplyText = emailObj.option4.reply;
		opt4.WorldScriptsName = emailObj.option4.script;
		opt4.CallbackFunction = emailObj.option4.callback;
		if (emailObj.option4.hasOwnProperty("parameter") === true) opt4.FunctionParam = emailObj.option4.parameter;
	}

	if (expOptions != "" &&
		((expOptions.indexOf("1") >=0 && opt1.DisplayText === "") ||
		(expOptions.indexOf("2") >=0 && opt2.DisplayText === "") ||
		(expOptions.indexOf("3") >=0 && opt3.DisplayText === "") ||
		(expOptions.indexOf("4") >=0 && opt4.DisplayText === ""))) {
		throw "Invalid settings: expiryOptions have been defined but the response option has not been set.";
	}

	var msg = {ID:id,
			Sender:emailObj.sender,
			Subject:emailObj.subject,
			TransmitDate:emailObj.date,
			EmailBody:ebody,
			OriginatingPlanet:System.systemNameForID(planet),
			ReceivedPlanet:System.systemNameForID(system.ID),
			Read:read,
			CorruptTrace:stoptr,
			Trace:trc,
			ExpiryDate:expDate,
			ExpiryText:expText,
			ExpiryOptions:expOptions,
			AllowExpiryCancel:expCancel,
			ChosenOption:0,
			Marked:false,
			GalaxyNum:galaxyNumber,
			ForceResponse:forcersp,
			ResponseOption1:opt1,
			ResponseOption2:opt2,
			ResponseOption3:opt3,
			ResponseOption4:opt4};

	this._emails.push(msg);

	if (emailObj.date > clock.seconds && player.ship.docked === true) {
		// set a timer to ding when the email "appears" in the inbox - only while docked
		if (!this._newMailAlert || this._newMailAlert.isRunning === false) {
			this._newMailAlert = new Timer(this, this.$newMailArrived, (emailObj.date - clock.seconds), 0);
		}
	}

	// make sure we keep our array inside the bounds of the archive limit
	this.$deleteOldest();
	this._updateRequired = true;

	// return the msg id back to the caller, just in case they need it.
	return id;
}

//=============================================================================================================
// ship interfaces

//-------------------------------------------------------------------------------------------------------------
this.shipDockedWithStation = function(station) {

	if (this._disableEmailNotification === false && ((this._notifyMainStationOnly === true && station.isMainStation) || this._notifyMainStationOnly === false)) {
		var unread = this.$totalUnreadItemsRequiringResponse();
		if(unread != 0) {
			var addText = "emails";
			var respText = "require";
			if (unread === 1) { addText = "email"; respText = "requires"; }
			player.addMessageToArrivalReport("You have " + unread + " unread " + addText + " that " + respText + " a response.");
		}
	}

	this.$checkForFutureDatedEmails();
	this.$initInterface(station);
}

//-------------------------------------------------------------------------------------------------------------
this.shipLaunchedFromStation = function(station) {
	delete missionVariables.EmailSystem_Emails;
	if (this._newMailAlert && this._newMailAlert.isRunning) this._newMailAlert.stop();
}

//-------------------------------------------------------------------------------------------------------------
this.playerWillSaveGame = function() {
	// save email array
	missionVariables.EmailSystem_Emails = JSON.stringify(this._emails);
}

//-------------------------------------------------------------------------------------------------------------
this.guiScreenChanged = function(to, from) {

	if (guiScreen === "GUI_SCREEN_INTERFACES" || this._updateRequired === true) {
		// update the interfaces screen
		this._updateRequired = false;
		var p = player.ship;
		if (p.dockedStation) this.$initInterface(p.dockedStation);
	}

	if (from === "GUI_SCREEN_MISSION" && this._emailOpen) {
		this._emailOpen = false;
		if (this._hudHidden === false && player.ship.hudHidden === true) player.ship.hudHidden = false;
	}
}

//=============================================================================================================
// general functions

//-------------------------------------------------------------------------------------------------------------
// check for any future dated emails, and start a timer to play a ding noise
this.$checkForFutureDatedEmails = function() {
	// when will the next ding happen?
	if (player.ship.docked) {
		var next = 0;
		for (var i = 0; i < this._emails.length; i++) {
			if (this._emails[i].TransmitDate > clock.seconds && (this._emails[i].TransmitDate - clock.seconds) > next) {
				next = (this._emails[i].TransmitDate - clock.seconds);
				// update the receipt planet and the trace of all future dated emails, so when they arrive
				// they have the correct values
				if (this._emails[i].GalaxyNum === galaxyNumber) {
					this._emails[i].ReceivedPlanet = System.systemNameForID(system.ID);
					this._emails[i].Trace = this.$populateTrace(System.systemIDForName(this._emails[i].OriginatingPlanet));
				}
			}
		}
		if (next > 0) {
			if (!this._newMailAlert || this._newMailAlert.isRunning === false)
				this._newMailAlert = new Timer(this, this.$newMailArrived, next, 0);
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
// play the new mail message noise
this.$newMailArrived = function $newMailArrived() {
	this._updateRequired = true;
	var mySound = new SoundSource;
	mySound.sound = "ding.ogg";
	mySound.loop = false;
	mySound.play();
	player.consoleMessage(expandDescription("[emailnewitem]"));
	// check for any more emails
	this.$checkForFutureDatedEmails();
}

//-------------------------------------------------------------------------------------------------------------
// returns the next ID number for new emails
this.$nextID = function() {
	var iMax = 0;
	if (this._emails != null && this._emails.length > 0) {
		for (var i = 0; i < this._emails.length; i++) {
			if (this._emails[i].ID > iMax) iMax = this._emails[i].ID;
		}
	}
	return (iMax + 1);
}

//-------------------------------------------------------------------------------------------------------------
// this function should only be called when an email is created. Once created, it will be populated from the missionvariables store
this.$populateTrace = function(startPoint) {
	// get list of planets between start and end points
	var sysID = system.ID;
	if (system.ID === -1) sysID = player.ship.targetSystem;
	var myRoute = System.infoForSystem(galaxyNumber, sysID).routeToSystem(System.infoForSystem(galaxyNumber, startPoint), "OPTIMIZED_BY_TIME");
	// expand and reverse list for storage
	var ret = "";
	if (myRoute != null) {
		for (var i = 0; i < myRoute.route.length; i++) {
			if (i > 0) {
				ret += ",";
			}
			ret = ret + System.systemNameForID(myRoute.route[i]);
		}
	} else {
		ret = "<< Error: Trace unavailable >>";
	}
	return ret;
}

//-------------------------------------------------------------------------------------------------------------
// deletes oldest emails from array until the count is less than archive total
this.$deleteOldest = function() {
	function compare(a,b) {
		return a.TransmitDate - b.TransmitDate;
	}

	// look for and delete any expired emails first
	for (var i = this._emails.length - 1; i >= 0; i--) {
		if (this._emails[i].ExpiryDate != 0 && clock.seconds > this._emails[i].ExpiryDate && this._emails[i].ExpiryText === "") {
			this._emails.splice(i, 1);
		}
	}

	if (this._emails.length > this._archiveLimit) {
		// sort emails oldest to newest
		this._emails.sort(compare);
		var idx = 0;
		do {
			// only delete if there isn't a response required
			if (this.$requiresResponse(this._emails[idx]) === false) this._emails.splice(idx, 1);
			idx += 1;
			// if the player has 100 emails that require a response, we might still end up with an array greater than 100
			// so watch for that case here
			if (idx >= this._emails.length) break;

		} while (this._emails.length > this._archiveLimit);
	}

}

//-------------------------------------------------------------------------------------------------------------
// returns the email based on ID number
this.$getEmailByID = function(id) {
	for (var i = 0; i < this._emails.length; i++) {
		if (this._emails[i].ID === id) return this._emails[i];
	}
	return null;
}

//-------------------------------------------------------------------------------------------------------------
// returns the email based on ID number
this.$getEmailIndexByID = function(id) {
	for (var i = 0; i < this._emails.length; i++) {
		if (this._emails[i].ID === id) return i;
	}
	return -1;
}

//-------------------------------------------------------------------------------------------------------------
this.$findNextEmailID = function(id) {
	var ret = -1;
	for (var i = 0; i < this._emails.length; i++) {
		if (this._emails[i].ID === id && i < (this._emails.length - 1)) ret = this._emails[i + 1].ID;
	}
	return ret;
}

//-------------------------------------------------------------------------------------------------------------
// executes the callback function of the selected response
this.$executeCallback = function(id, optionNumber) {
	var email = this.$getEmailByID(id);

	switch (optionNumber) {
		case 1:
			if (email.ResponseOption1.CallbackFunction != "") {
				var w = worldScripts[email.ResponseOption1.WorldScriptsName];
				if (w) w[email.ResponseOption1.CallbackFunction](email.ResponseOption1.FunctionParam);

			}
			break;
		case 2:
			if (email.ResponseOption2.CallbackFunction != "") {
				var w = worldScripts[email.ResponseOption2.WorldScriptsName];
				if (w) w[email.ResponseOption2.CallbackFunction](email.ResponseOption2.FunctionParam);
			}
			break;
		case 3:
			if (email.ResponseOption3.CallbackFunction != null) {
				var w = worldScripts[email.ResponseOption3.WorldScriptsName];
				if (w) w[email.ResponseOption3.CallbackFunction](email.ResponseOption3.FunctionParam);
			}
			break;
		case 4:
			if (email.ResponseOption4.CallbackFunction != null) {
				var w = worldScripts[email.ResponseOption4.WorldScriptsName];
				if (w) w[email.ResponseOption4.CallbackFunction](email.ResponseOption4.FunctionParam);
			}
			break;
	}
}

//-------------------------------------------------------------------------------------------------------------
// counts the total number of emails viewable in the inbox
this.$totalItems = function() {
	var itms = 0;
	if (this._emails.length > 0) {
		for (var i = 0; i < this._emails.length; i++) {
			if (this._emails[i].TransmitDate <= clock.seconds) itms += 1;
		}
	}
	return itms;
}

//-------------------------------------------------------------------------------------------------------------
// counts the total number of marked emails
this.$totalItemsMarked = function() {
	var itms = 0;
	if (this._emails.length > 0) {
		for (var i = 0; i < this._emails.length; i++) {
			if (this._emails[i].Marked === true) itms += 1;
		}
	}
	return itms;
}

//-------------------------------------------------------------------------------------------------------------
// counts how many read items are in the inbox
this.$totalReadItems = function() {
	var itms = 0;
	if (this._emails != null && this._emails.length > 0) {
		for (var i = 0; i < this._emails.length; i++) {
			if (this._emails[i].Read === true && this._emails[i].TransmitDate <= clock.seconds) itms += 1;
		}
	}
	return itms;
}

//-------------------------------------------------------------------------------------------------------------
// counts how many unread items are in the inbox
this.$totalUnreadItems = function() {
	var itms = 0;
	if (this._emails != null && this._emails.length > 0) {
		for (var i = 0; i < this._emails.length; i++) {
			if (this._emails[i].Read === false && this._emails[i].TransmitDate <= clock.seconds) itms += 1;
		}
	}
	return itms;
}

//-------------------------------------------------------------------------------------------------------------
// counts how many unread items requiring a response are in the inbox
this.$totalUnreadItemsRequiringResponse = function() {
	var itms = 0;
	if (this._emails != null && this._emails.length > 0) {
		for (var i = 0; i < this._emails.length; i++) {
			if (this._emails[i].Read === false && this._emails[i].TransmitDate <= clock.seconds && this._emails[i].ExpiryDate > clock.adjustedSeconds && this.$requiresResponse(this._emails[i]) === true) itms += 1;
		}
	}
	return itms;
}

//-------------------------------------------------------------------------------------------------------------
// marks all items read
this.$markAllItemsRead = function() {
	if (this._emails != null && this._emails.length > 0) {
		for (var i = 0; i < this._emails.length; i++) {
			if (this._emails[i].Read === false && this._emails[i].TransmitDate <= clock.seconds) this._emails[i].Read = true;
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
// checks whether the current item is marked
this.$isItemMarked = function(id) {
	var email = this.$getEmailByID(id);
	if (email.Marked === true) {
		return true;
	} else {
		return false;
	}
}

//-------------------------------------------------------------------------------------------------------------
// marks the currently selected item
this.$markItem = function(id) {
	var email = this.$getEmailByID(id);
	email.Marked = true;
}

//-------------------------------------------------------------------------------------------------------------
// unmarks the currently selected item
this.$unmarkItem = function(id) {
	var email = this.$getEmailByID(id);
	email.Marked = false;
}

//-------------------------------------------------------------------------------------------------------------
// deletes the currently selected item
this.$deleteSelectedItem = function(id) {
	if (this._emails.length > 0 && id > 0) {
		var idx = this.$getEmailIndexByID(id);
		if (this.$requiresResponse(this._emails[idx]) === false || this._emails[idx].ExpiryDate < clock.adjustedSeconds) {
			this._emails.splice(idx, 1);
			this._maxpage = Math.ceil(this.$totalItems() / this._msRows);
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
// deletes all marked emails that don't have a pending response
this.$deleteMarkedItems = function() {
	if (this._emails.length > 0 && this.$totalItemsMarked() > 0) {
		for (var i = this._emails.length - 1; i >= 0; i--) {
			if (this._emails[i].Marked === true && this.$requiresResponse(this._emails[i]) === false) {
				this._emails.splice(i, 1);
			}
		}
		this._curpage = 0;
		this._maxpage = Math.ceil(this.$totalItems() / this._msRows);
	}
}

//-------------------------------------------------------------------------------------------------------------
// deletes all marked emails that don't have a pending response
this.$deleteReadItems = function() {
	if (this._emails.length > 0 && this.$totalReadItems() > 0) {
		for (var i = this._emails.length - 1; i >= 0; i--) {
			if (this._emails[i].Read === true && this.$requiresResponse(this._emails[i]) === false) {
				this._emails.splice(i, 1);
			}
		}
		this._curpage = 0;
		this._maxpage = Math.ceil(this.$totalItems() / this._msRows);
	}
}

//-------------------------------------------------------------------------------------------------------------
// deletes all emails that don't have a pending response.
this.$deleteAllItems = function() {
	for (var i = this._emails.length - 1; i >= 0; i--) {
		// only delete emails that are in the past
		if (this.$requiresResponse(this._emails[i]) === false && this._emails[i].TransmitDate <= clock.seconds) {
			this._emails.splice(i, 1);
		}
	}
	this._curpage = 0;
	this._maxpage = 1;
}

//-------------------------------------------------------------------------------------------------------------
// records the index of the response against the selected email
this.$recordResponse = function(id, option) {
	var email = this.$getEmailByID(id);
	email.ChosenOption = option;
	email.ResponseDate = clock.seconds;
}

//-------------------------------------------------------------------------------------------------------------
// Returns the header details of the email formatted for display
this.$header = function(email) {

	var text = "";

	text += this.$padTextRight("From:", 5) + email.Sender + "\n";
	text += this.$padTextRight("Sent:", 5) + clock.clockStringForTime(email.TransmitDate)
	// add the galaxy number if the email was received in a different galaxy to the one the player is in now.
	if (galaxyNumber != email.GalaxyNum) text += " (G" + (email.GalaxyNum + 1) + ")";
	text += "\n";
	// expiry date
	if (email.ExpiryDate != 0) {
		text += this.$padTextRight("Expires:", 5);
		if (email.ExpiryDate > clock.seconds) {
			text += clock.clockStringForTime(email.ExpiryDate) + "\n";
		} else {
			text += email.ExpiryText + "\n";
		}
	}

	text += this.$padTextRight("Subject:", 5) + email.Subject + "\n";
	text += this.$duplicate("-", 32) + "\n";

	return text;
}

//-------------------------------------------------------------------------------------------------------------
// checks whether a response is required for this email.
// Returns true if there is a response and none has been selected, otherwise false.
this.$requiresResponse = function(email) {
	var ret = false;
	if ((email.ResponseOption1.DisplayText != "" || email.ResponseOption2.DisplayText != "" || email.ResponseOption3.DisplayText != "" || email.ResponseOption4.DisplayText != "") && email.ChosenOption === 0) {
		ret = true;
	}
	return ret;
}

//-------------------------------------------------------------------------------------------------------------
// returns the inbox line for this email
this.$inboxDisplay = function(email) {

	var ret = "";

	if (email.Read === false) {
		ret += this.$padTextRight("!", this._readColumn);
	} else {
		ret += this.$padTextRight("", this._readColumn);
	}

	ret += this.$padTextRight(email.Sender, 10);
	ret += this.$padTextRight(this.$inboxTime(clock.clockStringForTime(email.TransmitDate)), 7.6);
	ret += this.$padTextRight(email.Subject, 14);

	return ret;
}

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

//-------------------------------------------------------------------------------------------------------------
// works out whether a particular option should be displayed or not
this.$displayResponseOption = function(email, itemNumber) {

	var bShow = false;

	switch (itemNumber) {
		case 1:
			if (email.ResponseOption1 != null && email.ResponseOption1.DisplayText != "") bShow = true;
			// if the expiry is turned off but this option was included as an expiry option, don't show it
			if (email.ExpiryDate === 0 && email.ExpiryOptions.indexOf("1") >= 0) bShow = false;
			if (email.ExpiryDate != 0) {
				// don't show this option if it's an expiry option
				if (email.ExpiryDate > clock.seconds && email.ExpiryOptions.indexOf("1") >= 0) bShow = false;
				// don't show this option if the email has expired and it's not included in the expiry options
				if (email.ExpiryDate <= clock.seconds && email.ExpiryOptions.indexOf("1") < 0) bShow = false;
			}
			break;
		case 2:
			if (email.ResponseOption2 != null && email.ResponseOption2.DisplayText != "") bShow = true;
			// if the expiry is turned off but this option was included as an expiry option, don't show it
			if (email.ExpiryDate === 0 && email.ExpiryOptions.indexOf("2") >= 0) bShow = false;
			if (email.ExpiryDate != 0) {
				// don't show this option if it's an expiry option
				if (email.ExpiryDate > clock.seconds && email.ExpiryOptions.indexOf("2") >= 0) bShow = false;
				// don't show this option if the email has expired and it's not included in the expiry options
				if (email.ExpiryDate <= clock.seconds && email.ExpiryOptions.indexOf("2") < 0) bShow = false;
			}
			break;
		case 3:
			if (email.ResponseOption3 != null && email.ResponseOption3.DisplayText != "") bShow = true;
			// if the expiry is turned off but this option was included as an expiry option, don't show it
			if (email.ExpiryDate === 0 && email.ExpiryOptions.indexOf("3") >= 0) bShow = false;
			if (email.ExpiryDate != 0) {
				// don't show this option if it's an expiry option
				if (email.ExpiryDate > clock.seconds && email.ExpiryOptions.indexOf("3") >= 0) bShow = false;
				// don't show this option if the email has expired and it's not included in the expiry options
				if (email.ExpiryDate <= clock.seconds && email.ExpiryOptions.indexOf("3") < 0) bShow = false;
			}
			break;
		case 4:
			if (email.ResponseOption4 != null && email.ResponseOption4.DisplayText != "") bShow = true;
			// if the expiry is turned off but this option was included as an expiry option, don't show it
			if (email.ExpiryDate === 0 && email.ExpiryOptions.indexOf("4") >= 0) bShow = false;
			if (email.ExpiryDate != 0) {
				// don't show this option if it's an expiry option
				if (email.ExpiryDate > clock.seconds && email.ExpiryOptions.indexOf("4") >= 0) bShow = false;
				// don't show this option if the email has expired and it's not included in the expiry options
				if (email.ExpiryDate <= clock.seconds && email.ExpiryOptions.indexOf("4") < 0) bShow = false;
			}
			break;
	}

	return bShow;
}

//-------------------------------------------------------------------------------------------------------------
// duplicates text until it is just less than the desired length;
this.$duplicate = function(text, desiredLength) {
	var res = "";

	do {
		res += text;
	} while (defaultFont.measureString(res) < desiredLength);

	res = res.substring(1, res.length);
	return res;
}

//-------------------------------------------------------------------------------------------------------------
// adds a message to an array, splitting the lines based on a line width
this.$addMsgToArray = function(msg, ary, linewidth) {

	var prt = " ";
	if (msg.substring(0,1) === "~" || msg.substring(0,1) === "-") {
		// don't indent this one
		prt = "";
		// remove the "~" char
		if (msg.substring(0,1) === "~") msg = msg.substring(1, msg.length);
	}

	if (defaultFont.measureString(msg) > linewidth) {
		var words = msg.split(" ");
		var iPoint = 0
		var nextWord = "";
		do {
			// add the space in (after the first word)
			if (prt != "")  prt += " ";

			// add the word on
			prt = prt + words[iPoint];
			// get the next word in the array
			if (iPoint < words.length - 1) {
				nextWord = " " + words[iPoint + 1];
			} else {
				nextWord = ""
			}
			// check the width of the next with the nextword added on
			if (defaultFont.measureString(prt + nextWord) > linewidth) {
				// hit the max width - add the line and start again
				ary.push(prt);
				prt = "";
			}
			iPoint += 1;
			// check for the end of the array, and output remaining text
			if ((iPoint >= words.length) && (prt.trim() != "")) ary.push(prt);

		} while (iPoint < words.length);
	} else {
		ary.push(prt + msg);
	}
}

//=============================================================================================================
// screen interfaces
this.$initInterface = function(station) {

	var unread = "";
	var itms = this.$totalUnreadItems();
	if (itms > 0) unread = " (" + itms.toString() + " unread" + (this.$totalUnreadItemsRequiringResponse() > 0 ? " •" : "") + ")"

	station.setInterface(this.name,{
		title:"Email system" + unread,
		category:expandDescription("[interfaces-category-logs]"),
		summary:"Opens your email inbox",
		callback:this.$showInbox.bind(this)
	});

}

//=============================================================================================================
// inbox
this.$showInbox = function() {
	this._emailID = 0;
	this._emailIndex = 0;
	this._maxpage = Math.ceil(this.$totalItems() / this._msRows);
	this._curpage = 0;
	this._displayType = 0;
	this.$showPage();
}

//-------------------------------------------------------------------------------------------------------------
this.$showPage = function() {
	function compare(a,b) {
		return b.TransmitDate - a.TransmitDate;
	}

	this._hudHidden = player.ship.hudHidden;
	if (this.$isBigGuiActive() === false) player.ship.hudHidden = true;
	this._emailOpen = true;

	var text = "";
	var opts;
	var curChoices = {};
	var def = "";

	//=============================================================================================================
	// inbox view
	if (this._displayType === 0) {
		// sort the inbox by date, newest to oldest
		this._emails.sort(compare);
		this._emailPage = 0;

		var expired = false;
		// look for and delete any expired emails
		for (var i = this._emails.length - 1; i >= 0; i--) {
			if (this._emails[i].ExpiryDate != 0 && clock.seconds > this._emails[i].ExpiryDate && this._emails[i].ExpiryText === "") {
				this._emails.splice(i, 1);
				expired = true;
			}
		}

		// adjust the max page if we remove any expired emails
		if (expired) this._maxpage = Math.ceil(this.$totalItems() / this._msRows);

		curChoices = this.$inboxPageChoices(this._curpage, this._msRows);

		// make sure the selected email stays selected, in case new emails arrived while we were viewing another email
		if (this._emailID != 0 && this._emailIndex != this.$getChoiceItemIndex(this._emailID)) {
			var old = this._emailIndex;
			this._emailIndex = this.$getChoiceItemIndex(this._emailID);
			if (this._emailIndex === 0 && old === this._msRows) {
				// we've been bumped to a new page, so switch to it
				this._curpage += 1;
				curChoices = this.$inboxPageChoices(this._curpage, this._msRows);
				this._emailIndex = this.$getChoiceItemIndex(this._emailID);
			}
			this._lastChoice[this._displayType] = "01_email-" + (this._emailIndex < 10 ? "0" : "") + this._emailIndex + "+" + this._emailID;
		}

		this._emailID = 0;

		text = this.$padTextRight("", this._readColumn + 0.15);
		text += this.$padTextRight("From", 10);
		text += this.$padTextRight("Date", 7.6);
		text += "Subject";
		if (this._itemsOnPage === 0) text += "\n\n(no items to display)";

		for (var i = 0; i < (this._msRows + 1) - this._itemsOnPage; i++) {
			curChoices["02_SPACER_" + i] = "";
		}

		var def = "99_EXIT";

		if (this._emails != null && this._emails.length != 0) {
			if (this._curpage < this._maxpage - 1) {
				curChoices["10_GOTONEXT"] = {text:"[emailopt_nextpage]", color:this._menuColor};
			} else {
				curChoices["10_GOTONEXT"] = {text:"[emailopt_nextpage]", color:this._disabledColor, unselectable:true};
			}
			if (this._curpage > 0) {
				curChoices["11_GOTOPREV"] = {text:"[emailopt_prevpage]", color:this._menuColor};
			} else {
				curChoices["11_GOTOPREV"] = {text:"[emailopt_prevpage]", color:this._disabledColor, unselectable:true};
			}
			if (this.$totalReadItems() > 0) {
				curChoices["42_MARKALLREAD"] = {text:"[emailopt_markallread]", color:this._menuColor};
				curChoices["45_DELREAD"] = {text:"[emailopt_deleteread]", color:this._menuColor};
			} else {
				curChoices["42_MARKALLREAD"] = {text:"[emailopt_markallread]", color:this._disabledColor, unselectable:true};
				curChoices["45_DELREAD"] = {text:"[emailopt_deleteread]", color:this._disabledColor, unselectable:true};
			}
			curChoices["50_DELALL"] = {text:"[emailopt_deleteall]", color:this._menuColor};
		} else {
			curChoices["10_GOTONEXT"] = {text:"[emailopt_nextpage]", color:this._disabledColor, unselectable:true};
			curChoices["11_GOTOPREV"] = {text:"[emailopt_prevpage]", color:this._disabledColor, unselectable:true};
			curChoices["42_MARKALLREAD"] = {text:"[emailopt_markallread]", color:this._disabledColor, unselectable:true};
			curChoices["45_DELREAD"] = {text:"[emailopt_deleteread]", color:this._disabledColor, unselectable:true};
			curChoices["50_DELALL"] = {text:"[emailopt_deleteall]", color:this._disabledColor, unselectable:true};
		}

		curChoices["99_EXIT"] = {text:"[emailopt_exit]", color:this._exitColor};

		var opts = {
			screenID: "oolite-emailsystem-main-map",
			title: "Inbox - page " + (this._curpage + 1).toString() + " of " + this._maxpage.toString(),
			allowInterrupt: true,
			exitScreen: "GUI_SCREEN_INTERFACES",
			overlay: {name:"email-message.png", height:546},
			choices: curChoices,
			initialChoicesKey: (this._lastChoice[this._displayType] ? this._lastChoice[this._displayType] : def),
			message: text
		};
	}

	//=============================================================================================================
	// email item
	if (this._displayType === 1) {
		var email = this.$getEmailByID(this._emailID);

		var headerlen = 4;
		if (email.ExpiryDate !=0 && email.ExpiryDate > clock.seconds) {
			headerlen += 1; // extra line if expiry date is still current
			if (email.allowExpiryCancel) headerlen +=1; // another line for the "Cancel expiry" option
		}

		email.Read = true;

		text += this.$header(email);

		var lines = email.EmailBody.split("\n");
		var dest = [];

		for (var i = 0; i < lines.length; i++) {
			this.$addMsgToArray(lines[i], dest, this._msCols);
		}

		// append any response to the body of the email
		if (email.ChosenOption > 0) {
			var resp = "";
			resp += "\n";
			resp += "~" + this.$duplicate("-", 32) + "\n";
			resp += "~" + this.$padTextRight("Reply sent:", 5) + clock.clockStringForTime(email.ResponseDate) + "\n";
			resp += "~Reply:\n\n";

			switch (email.ChosenOption) {
				case 1:
					resp += email.ResponseOption1.ReplyText;
					break;
				case 2:
					resp += email.ResponseOption2.ReplyText;
					break;
				case 3:
					resp += email.ResponseOption3.ReplyText;
					break;
				case 4:
					resp += email.ResponseOption4.ReplyText;
					break;
			}
			var respLines = resp.split("\n");
			for (var i = 0; i < respLines.length; i++) {
				this.$addMsgToArray(respLines[i], dest, this._msCols);
			}
		}

		var addText = "";
		var iPageHeight = 16 + (4 - headerlen);
		this._emailMaxPages = 0;

		// check if
		if (dest.length > iPageHeight && this.$requiresResponse(email) === true) {
			// how many options are there?
			var iRespCount = 0;
			if ($displayResponseOption(email, 1)) iRespCount += 1;
			if ($displayResponseOption(email, 2)) iRespCount += 1;
			if ($displayResponseOption(email, 3)) iRespCount += 1;
			if ($displayResponseOption(email, 4)) iRespCount += 1;

			// reduce the page height by the number of responses
			iPageHeight -= iRespCount;
		}

		if (dest.length > iPageHeight) {
			this._emailMaxPages = Math.ceil(dest.length / iPageHeight);
			addText = " (Page " + (this._emailPage + 1).toString() + " of " + this._emailMaxPages.toString() + ")";
		}
		var iStart = this._emailPage * iPageHeight; // when 0 then 0, when 1 then 16
		var iEnd = this._emailPage * iPageHeight + iPageHeight; // when 0 then 16, when 1 then 32 etc
		if (iEnd > dest.length) {
			iEnd = dest.length;
		}
		for (var i = iStart; i <= iEnd - 1; i++) {
			text += dest[i] + "\n";
		}

		def = "23_CLOSE";

		if (this._emailMaxPages != 0) {
			if (this._emailPage + 1 < this._emailMaxPages) {
				curChoices["21_NEXTPAGE"] = {text:"[emailitem_nextpage]", color:this._menuColor};
				def = "21_NEXTPAGE";
			} else {
				curChoices["21_NEXTPAGE"] = {text:"[emailitem_nextpage]", color:this._disabledColor, unselectable:true};
			}
			if (this._emailPage > 0) {
				curChoices["22_PREVPAGE"] = {text:"[emailitem_prevpage]", color:this._menuColor};
			} else {
				curChoices["22_PREVPAGE"] = {text:"[emailitem_prevpage]", color:this._disabledColor, unselectable:true};
			}
		}
		// if the force response option is turned on, disable all the close email options, so the player has to make a choice
		if (email.ForceResponse === true && email.ChosenOption === 0) {
			curChoices["23_CLOSE"] = {text:"[emailitem_close]", color:this._disabledColor, unselectable:true};
			curChoices["23A_CLOSEOPEN"] = {text:"[emailitem_closeopen]", color:this._disabledColor, unselectable:true};
			curChoices["24_CLOSEDEL"] = {text:"[emailitem_closedelete]", color:this._disabledColor, unselectable:true};
		} else {
			curChoices["23_CLOSE"] = {text:"[emailitem_close]", color:this._menuColor};
			if (this.$findNextEmailID(this._emailID) >= 0) {
				curChoices["23A_CLOSEOPEN"] = {text:"[emailitem_closeopen]", color:this._menuColor};
			} else {
				curChoices["23A_CLOSEOPEN"] = {text:"[emailitem_closeopen]", color:this._disabledColor, unselectable:true};
			}
			if (this.$requiresResponse(email) === true && email.ExpiryDate > clock.seconds && email.ChosenOption === 0) {
				curChoices["24_CLOSEDEL"] = {text:"[emailitem_closedelete]", color:this._disabledColor, unselectable:true};
			} else {
				curChoices["24_CLOSEDEL"] = {text:"[emailitem_closedelete]", color:this._menuColor};
			}
		}
		curChoices["25_TRACE"] = {text:"[emailitem_trace]", color:this._menuColor};
		if (email.ExpiryDate != 0 && email.ExpiryDate > clock.seconds && email.AllowExpiryCancel === true) {
			curChoices["25A_CANCELEXPIRY"] = {text:"[emailitem_cancelexpiry]", color:this._menuColor};
		}

		if (email.ChosenOption === 0) {
			if (this.$displayResponseOption(email, 1)) curChoices["26_OPT1"] = {text:"Send '" + email.ResponseOption1.DisplayText + "' response", color:this._menuColor};
			if (this.$displayResponseOption(email, 2)) curChoices["26_OPT2"] = {text:"Send '" + email.ResponseOption2.DisplayText + "' response", color:this._menuColor};
			if (this.$displayResponseOption(email, 3)) curChoices["26_OPT3"] = {text:"Send '" + email.ResponseOption3.DisplayText + "' response", color:this._menuColor};
			if (this.$displayResponseOption(email, 4)) curChoices["26_OPT4"] = {text:"Send '" + email.ResponseOption4.DisplayText + "' response", color:this._menuColor};
		}

		var opts = {
			screenID: "oolite-emailsystem-item-map",
			title: "Message" + addText,
			allowInterrupt: false,
			exitScreen: "GUI_SCREEN_INTERFACES",
			overlay: {name:"email-message_open.png", height:546},
			choices: curChoices,
			initialChoicesKey: (this._lastChoice[this._displayType] != "" ? this._lastChoice[this._displayType] : def),
			message: text
		};
	}

	//=============================================================================================================
	// trace
	if (this._displayType === 2) {
		var email = this.$getEmailByID(this._emailID);

		var initText = "- Received: ";

		text += this.$header(email);

		text += "Trace:\n\n";
		var planets = email.Trace.split(",");
		var bEnd = false;
		var bCrpt = false;
		var colHeight = 16;
		if (planets.length < colHeight) {
			for (var i = 0; i < planets.length; i++) {
				if (planets[i] != "") {
					text += initText + planets[i] + "\n";
					initText = "- Sent from: ";
				}
			}
		} else {
			for (var i = 0; i < colHeight; i++) {
				if (planets[i] != "") {
					text += this.$padTextRight(initText + planets[i], 10);
					initText = "- Sent from: ";
					if (i + colHeight < planets.length) {
						if (planets[i + colHeight] != "") {
							text += this.$padTextRight("- Sent from: " + planets[i + colHeight], 10);
						}
					}
					if (i + colHeight === planets.length) {
						if (email.CorruptTrace === true) {
							text += "- Routing ticket corrupt";
							bCrpt = true;
						}
					}
					if (i + (colHeight * 2) < planets.length) {
						if (planets[i + (colHeight * 2)] != "") {
							text += this.$padTextRight("- Sent from: " + planets[i + (colHeight * 2)], 10);
						}
					}
					if (i + (colHeight * 2) === planets.length) {
						if (email.CorruptTrace === true) {
							text += "- Routing ticket corrupt";
							bCrpt = true;
						}
					}
					text += "\n";
				}
			}
		}
		if (email.CorruptTrace === true) {
			if (bCrpt === false) {
				text += "- Routing ticket corrupt\n";
			}
			text += "\nTrace incomplete.";
		} else if (bEnd === false){
			text += "\nTrace complete.";
		}
		def = "27_CLOSE";

		curChoices["27_CLOSE"] = {text:"[emailtrace_close]", color:this._exitColor};

		var opts = {
			screenID: "oolite-emailsystem-trace-summary",
			title: expandDescription("[emailtrace_title]"),
			allowInterrupt: false,
			overlay: {name:"email-message_open.png", height:546},
			exitScreen: "GUI_SCREEN_INTERFACES",
			choices: curChoices,
			initialChoicesKey: (this._lastChoice[this._displayType] != "" ? this._lastChoice[this._displayType] : def),
			message: text
		};
		planets = [];
	}

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

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

	var curdisplay = this._displayType;
	var newChoice = "";
	var newChoicePage = 0;
	
	this._lastChoice[this._displayType] = choice;

	if (!choice) {
		return; // launched while reading?
	} else if (choice.indexOf("01_email") >= 0) {
		this._emailID = parseInt(choice.substring(choice.indexOf("+") + 1));
		this._emailIndex = parseInt(choice.substring(choice.indexOf("-") + 1, choice.indexOf("+")));
		this._emailPage = 0;
		this._displayType = 1;
	} else if (choice === "11_GOTOPREV") {
		this._curpage -= 1;
		if (this._curpage === 0) {
			newChoice = "10_GOTONEXT";
			newChoicePage = this._displayType;
		}
	} else if (choice === "10_GOTONEXT") {
		this._curpage += 1;
		if (this._curpage === this._maxpage - 1) {
			newChoice = "11_GOTOPREV";
			newChoicePage = this._displayType;
		}
	} else if (choice === "42_MARKALLREAD") {
		this.$markAllItemsRead();
	} else if (choice === "45_DELREAD") {
		this.$deleteReadItems();
	} else if (choice === "50_DELALL") {
		this.$deleteAllItems();
	} else if (choice === "21_NEXTPAGE") {
		this._emailPage += 1;
		if ((this._emailPage - 1) === this._emailMaxPages) {
			newChoice = "22_PREVPAGE";
			newChoicePage = this._displayType;
		}
	} else if (choice === "22_PREVPAGE") {
		this._emailPage -= 1;
		if (this._emailPage === 0) {
			newChoice = "21_NEXTPAGE";
			newChoicePage = this._displayType;
		}
	} else if (choice === "23_CLOSE") {
		this._displayType = 0;
	} else if (choice === "23A_CLOSEOPEN") {
		this._emailPage = 0;
		// get next email id
		this._emailID = this.$findNextEmailID(this._emailID);
		// if this the last item on the page?
		if (this._emailIndex === this._msRows && (this._curpage + 1) < this._maxpage) {
			// we've flipped over to the next page
			this._curpage += 1;
			this.$inboxPageChoices(this._curpage, this._msRows);
		}
		this._emailIndex = this.$getChoiceItemIndex(this._emailID)
		newChoice = "01_email-" + (this._emailIndex < 10 ? "0" : "") + this._emailIndex + "+" + this._emailID;
		newChoicePage = 0;
	} else if (choice === "24_CLOSEDEL") {
		this._displayType = 0;
		this.$deleteSelectedItem(this._emailID);
	} else if (choice === "25_TRACE") {
		this._displayType = 2;
	} else if (choice === "25A_CANCELEXPIRY") {
		this._emails[this.$getEmailIndexByID(this._emailID)].ExpiryDate = 0;
	} else if (choice === "26_OPT1") {
		// make sure the email hasn't expired while being viewed
		if (this.$displayResponseOption(this.$getEmailByID(this._emailID), 1)) {
			this.$recordResponse(this._emailID, 1);
			this.$executeCallback(this._emailID, 1);
		}
	} else if (choice === "26_OPT2") {
		// make sure the email hasn't expired while being viewed
		if (this.$displayResponseOption(this.$getEmailByID(this._emailID), 2)) {
			this.$recordResponse(this._emailID, 2);
			this.$executeCallback(this._emailID, 2);
		}
	} else if (choice === "26_OPT3") {
		// make sure the email hasn't expired while being viewed
		if (this.$displayResponseOption(this.$getEmailByID(this._emailID), 3)) {
			this.$recordResponse(this._emailID, 3);
			this.$executeCallback(this._emailID, 3);
		}
	} else if (choice === "26_OPT4") {
		// make sure the email hasn't expired while being viewed
		if (this.$displayResponseOption(this.$getEmailByID(this._emailID), 4)) {
			this.$recordResponse(this._emailID, 4);
			this.$executeCallback(this._emailID, 4);
		}
	} else if (choice === "27_CLOSE") {
		this._displayType = 1;
	}
	
	if (newChoice != "") this._lastChoice[newChoicePage] = newChoice;
	
	if (choice != "99_EXIT") {
		this.$showPage();
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$inboxPageChoices = function(cpage, lines) {
	//var output = "";

	var iStart = 0;
	var iEnd = 0;
	var offset = 0;
	var choices = {};

	// work out start point - start after any future dated emails
	for (var i = 0; i < this._emails.length; i++) {
		if (this._emails[i].TransmitDate <= clock.seconds) {
			offset = i;
			break;
		}
	}

	// set out initial end point
	iStart = offset;
	iEnd = iStart + lines;
	if (cpage != 0) {
		iStart = (cpage * lines) + offset;
		iEnd = iStart + lines;
	}
	if (iEnd > this._emails.length) {
		iEnd = this._emails.length;
	}

	this._itemsOnPage = 0;

	this._pageItems.length = 0;

	if (this.$totalItems() != 0) {
		for (var i = iStart; i < iEnd; i++) {
			// put an index in the key for sorting, and add the id for selecting
			this._itemsOnPage += 1;
			choices["01_email-" + (this._itemsOnPage < 10 ? "0" : "") + this._itemsOnPage + "+" + this._emails[i].ID] = {text:this.$inboxDisplay(this._emails[i]), alignment:"LEFT", color:this._itemColor};
			this._pageItems.push({ID:this._emails[i].ID, index:this._itemsOnPage});
		}
	}
	return choices;
}

//-------------------------------------------------------------------------------------------------------------
this.$getChoiceItemIndex = function(id) {
	var result = 0;
	if (this._pageItems && this._pageItems.length > 0) {
		for (var i = 0; i < this._pageItems.length; i++) {
			if (this._pageItems[i].ID === id) result = this._pageItems[i].index;
		}
	}
	return result;
}

//-------------------------------------------------------------------------------------------------------------
this.$inboxTime = function(emailTime) {
	return emailTime.substring(0, emailTime.length - 3);
}

//-------------------------------------------------------------------------------------------------------------
// 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 {
		var bigGuiHUD = ["XenonHUD.plist", "coluber_hud_ch01-dock.plist"]; 	// until there is a property we can check, I'll be listing HUD's that have the allow_big_gui property set here
		if (bigGuiHUD.indexOf(player.ship.hud) >= 0) {
			return true;
		} else {
			return false;
		}
	}
}
Scripts/galcopadmin.js
"use strict";
this.name        = "GalCopAdminServices";
this.author      = "phkb";
this.copyright   = "2017 phkb";
this.description = "Transmits emails to player from GalCop admin relating to bounty notifications and other admin processes.";
this.licence     = "CC BY-NC-SA 4.0";

/*
	Todo:
	Check for docking fines on other station types (UPS Courier, Jaguar company, The collector, Aquatics, Resistance Commander, Lave Acedemy, FTZ, Planetfall);
	Check for purchase equip on Black Monks Monestary (need different voice for email).

	Ideas:
	Welcome to new galaxy emails
		- Tech Level 15 planets -- G1 Ceesxe, G2 Tezaeded, G3 none (14: LEZAER,BIRERA,LEORENDI,ORZAEDVE,TEEDUS,TIERA,CEDILE), G4 none (14: GEBIISSO,DICEBE),
			G5 Xevera, G6 Diesanen, G7 Quandixe and Maraus, G8 none (14: ESUSALE).
		- List of galactic regions
		- List of galactic routes
		- GalCop HQ's
		- RRS station HQ locations
		- Superhub locations
		- Gal Navy sector commands

	Constore emails about special deals
	Space bar emails
	Emails from revolutionists, saying "Here's the details of the conspiracy"
	Emails from Hognose Evangelists telling player about their religion
	Emails containing bargains for trade (ie "trade in Furs between X and Y for a super bargain. Offer won't last!" X will be system within 7 LY of player, Y will be within 10 LY of X.)
		maybe two different emails: 1 saying nnn commodity is really cheap at X, 1 saying demand for nnn commodity is high at Y
		have an opt in/opt out functionality
		-- seems to be similar to function in NewCargoes OXP, but might be useful as an alternative (ie. if you don't want NewCargos, you can still get market bargins)
		-- also similar to functionality in BlOomberg markets
*/

this._debug = true;
this._bountyEmailBody = "";										// holds details of next bounty email
this._bountyEmailTime = 0;										// the time of the last kill
this._bountyEmailTotalCR = 0;									// total amount of bounty
this._boughtEquipTimer = null;
this._killCount = 0;											// total number of kills
this._killAssortedCount = 0;									// total number of non-ship kills (eg missiles)
this._holdBountyEmail = [];										// holds bounty emails across hyperspace jumps
this._disableBounty = false;									// controls whether bounty emails are sent
this._disableDocking = false;									// controls whether docking violation emails are sent
this._disableEscapePods = false;								// controls whether escape pod rescue emails are sent
this._disableContracts = false;									// controls whether contract emails are sent
this._disableFines = false;										// controls whether fine processing emails are sent
this._disableEquipPurchase = false;								// controls whether equipment purchase emails are sent
this._disableMaintenance = false;								// controls whether repair and overhaul emails are sent
this._disableNewShip = false;									// controls whether new ship emails are sent
this._disableEliteFederation = false;							// controls whether changes to player rank are sent
this._defaultExpiryDays = 5;									// default expiry period for emails that expire
this._repairHullDamage = false;									// keeps track of hull damage email (Battle Damage OXP)
this._repairIronHide = false;									// keeps track of iron hide repair emails (IronHide OXP)
this._sendMaintEmail = false;									// keeps track of when we are sending maintenance emails (relates to IronHide OXP)
this._sendRepairEmail = false;									// keeps track of when we are sending repair emails (relates to IronHide OXP)
this._maintCost = 0;
this._maintCostInit = 0;
this._equipItems = [];											// list of equipment items, held in order to determine diff between purchase and repair
this._equipSalesRep = "";										// name of sales assistant for equipment
this._shipSalesRep = "";										// name of sales assistant for ships
this._maintRep = "";											// name of maintenance rep for repair or overhaul emails
this._dutyOfficer = "";											// station duty officer/flight controller (for docking fines)
this._galCopBountyOfficer = "";									// name of galcop bounty processor
this._insuranceOfficer = "";									// name of claims administrator (for escape pod insurance claims)
this._namesLocation = "";										// keeps track of the system/dock the names are associated with
this._galCopHQ = [7, 33, 26, 49, 102, 85, 202, 184];			// galcop hq locations in each sector
this._fedHQ = [131, 167, 58, 65, 230, 35, 181, 130];			// location of elite federation hq in each sector
this._playerEnteringShipyard = false;							// keeps track of when the player enters the shipyard at a station, so we can check for buying a new ship
this._licenceNumber = "";										// holds the pilot licence number, in case it's ever needed
this._maintItemNameReplacements = "";							// holds list of maint item replacement name keys
this._trueValues = ["yes", "1", 1, "true", true];
this._playerEjected = false;
this._maint_ignore_equip = []; // no longer used, but kept for backwards compatibility.
this._oldPassengerCount = 0;

/* equipment known to have description items for an overhaul email */
this._maint_known_equip = ["EQ_CARGO_BAY", "EQ_ECM", "EQ_FUEL_SCOOPS", "EQ_ESCAPE_POD", "EQ_ENERGY_UNIT", "EQ_NAVAL_ENERGY_UNIT", "EQ_DOCK_COMP", "EQ_GAL_DRIVE",
	"EQ_WEAPON_PULSE_LASER", "EQ_WEAPON_BEAM_LASER", "EQ_WEAPON_MINING_LASER", "EQ_WEAPON_MILITARY_LASER", "EQ_CLOAKING_DEVICE", "EQ_PASSENGER_BERTH", "EQ_FUEL_INJECTION",
	"EQ_SCANNER_SHOW_MISSILE_TARGET", "EQ_MULTI_TARGET", "EQ_ADVANCED_COMPASS", "EQ_ADVANCED_NAVIGATIONAL_ARRAY", "EQ_TARGET_MEMORY", "EQ_INTEGRATED_TARGETING_SYSTEM",
	"EQ_SHIELD_BOOSTER", "EQ_NAVAL_SHIELD_BOOSTER", "EQ_HEAT_SHIELD", "EQ_WORMHOLE_SCANNER", "EQ_WEAPON_TWIN_PLASMA_CANNON",
	"EQ_LMSS_FRONT", "EQ_LMSS_AFT", "EQ_LMSS_PORT", "EQ_LMSS_STARBOARD", "EQ_LMSS_ACTUATOR"];

/* equipment items devoted to repair (not equipment in themselves) */
this._maint_repair_equip = ["EQ_HULL_REPAIR", "EQ_IRONHIDE_REPAIR", "EQ_TURRET_RECOVER", "EQ_SHIP_VERSION_REPAIR", "EQ_SERVICE_LEVEL_SMALL_FIX_1",
	"EQ_SERVICE_LEVEL_SMALL_FIX_2", "EQ_SERVICE_LEVEL_SMALL_FIX_3", "EQ_SERVICE_LEVEL_SMALL_FIX_4"];

/* equipment items devoted to removal of equipment */
this._maint_remove_equip = ["EQ_PASSENGER_BERTH_REMOVAL", "EQ_MISSILE_REMOVAL", "EQ_WEAPON_NONE"];

/* equipment items to be ignored by the email system */
this._purchase_ignore_equip = ["EQ_FUEL", "EQ_MISSILE", "EQ_TRUMBLE", "EQ_RRS_FUEL", "EQ_SHIP_RESPRAY", "EQ_SHIP_RESPRAY_180", "EQ_SMUGGLING_COMPARTMENT", "EQ_IMPORT_PERMIT"];

// equipment items that will be immediately removed after purchase
this._purchase_removed = ["EQ_IRONHIDE_MIL", "EQ_REPAIRBOTS_RECHARGE_10", "EQ_REPAIRBOTS_RECHARGE_5"];

//======================================================================================================================
// Library config
this._galCopAdminConfig = {Name:this.name, Alias:"GalCop Admin Services", Display:"Config", Alive:"_galCopAdminConfig",
	Bool:{
		B0:{Name:"_disableBounty", Def:false, Desc:"Bounty emails"},
		B1:{Name:"_disableDocking", Def:false, Desc:"Docking fine emails"},
		B2:{Name:"_disableEscapePods", Def:false, Desc:"Escape pod emails"},
		B3:{Name:"_disableContracts", Def:false, Desc:"Contract emails"},
		B4:{Name:"_disableFines", Def:false, Desc:"Fine penalty emails"},
		B5:{Name:"_disableEquipPurchase", Def:false, Desc:"Equip purchase emails"},
		B6:{Name:"_disableNewShip", Def:false, Desc:"New ship emails"},
		B7:{Name:"_disableMaintenance", Def:false, Desc:"Maintenance emails"},
		B8:{Name:"_disableEliteFederation", Def:false, Desc:"Elite Fed emails"},
		Info:"Set item to true to disable those emails from being transmitted."},
};

//=============================================================================================================
// ship interfaces
//-------------------------------------------------------------------------------------------------------------
this.startUp = function() {
	// delete the missionScreenEnded routine if IronHide OXP is not installed
	if (!worldScripts["IronHide Armour Script"]) delete this.missionScreenEnded;
}

//-------------------------------------------------------------------------------------------------------------
this.startUpComplete = function() {
	// register our settings, if Lib_Config is present
	if (worldScripts.Lib_Config) worldScripts.Lib_Config._registerSet(this._galCopAdminConfig);

	// load in the licence number if it's been saved
	if (missionVariables.GalCopAdmin_licenceNumber) this._licenceNumber = missionVariables.GalCopAdmin_licenceNumber;
	if (missionVariables.GalCopAdmin_DisableBounty) this._disableBounty = (this._trueValues.indexOf(missionVariables.GalCopAdmin_DisableBounty) >= 0 ? true : false);
	if (missionVariables.GalCopAdmin_DisableEscapePods) this._disableEscapePods = (this._trueValues.indexOf(missionVariables.GalCopAdmin_DisableEscapePods) >= 0 ? true : false);
	if (missionVariables.GalCopAdmin_DisableContracts) this._disableContracts = (this._trueValues.indexOf(missionVariables.GalCopAdmin_DisableContracts) >= 0 ? true : false);
	if (missionVariables.GalCopAdmin_DisableFines) this._disableFines = (this._trueValues.indexOf(missionVariables.GalCopAdmin_DisableFines) >= 0 ? true : false);
	if (missionVariables.GalCopAdmin_DisableEquipPurchase) this._disableEquipPurchase = (this._trueValues.indexOf(missionVariables.GalCopAdmin_DisableEquipPurchase) >= 0 ? true : false);
	if (missionVariables.GalCopAdmin_DisableNewShip) this._disableNewShip = (this._trueValues.indexOf(missionVariables.GalCopAdmin_DisableNewShip) >= 0 ? true : false);
	if (missionVariables.GalCopAdmin_DisableMaintenance) this._disableMaintenance = (this._trueValues.indexOf(missionVariables.GalCopAdmin_DisableMaintenance) >= 0 ? true : false);
	if (missionVariables.GalCopAdmin_DisableEliteFed) this._disableEliteFederation = (this._trueValues.indexOf(missionVariables.GalCopAdmin_DisableEliteFed) >= 0 ? true : false);
	if (missionVariables.GalCopAdmin_DisableDocking) this._disableDocking = (this._trueValues.indexOf(missionVariables.GalCopAdmin_DisableDocking) >= 0 ? true : false);

	this._maintItemNameReplacements = expandDescription("[maint_itemname_list]");

	// send a late notice pilot licence email
	if (global.clock.clockStringForTime(global.clock.seconds).indexOf("2084004:20") === -1 && this._licenceNumber === "") {
		this._licenceNumber = this.$generateLicenceNumber();
		// pilot licence notification arrives shortly after the start
		var lgl = expandDescription("[legalstatuswarnings_" + expandDescription("[commander_legal_status]") + "]");
		var w = worldScripts.EmailSystem;
		w.$createEmail({sender:expandDescription("[new-licence-sender]"),
			subject:expandDescription("Re: New Pilot Registration"),
			date:global.clock.adjustedSeconds,
			message:expandDescription("[latenotice-licence]", {licenceno:this._licenceNumber, insertlegalstatus:lgl}),
			sentFrom:this._galCopHQ[galaxyNumber]
		});
	}

}

//-------------------------------------------------------------------------------------------------------------
this.playerWillSaveGame = function() {
	// save the licence number in worldscripts
	//if (this._licenceNumber === "") this._licenceNumber = this.$generateLicenceNumber();
	missionVariables.GalCopAdmin_licenceNumber = this._licenceNumber;
	missionVariables.GalCopAdmin_DisableBounty = this._disableBounty;
	missionVariables.GalCopAdmin_DisableEscapePods = this._disableEscapePods;
	missionVariables.GalCopAdmin_DisableContracts = this._disableContracts;
	missionVariables.GalCopAdmin_DisableFines = this._disableFines;
	missionVariables.GalCopAdmin_DisableEquipPurchase = this._disableEquipPurchase;
	missionVariables.GalCopAdmin_DisableNewShip = this._disableNewShip;
	missionVariables.GalCopAdmin_DisableMaintenance = this._disableMaintenance;
	missionVariables.GalCopAdmin_DisableEliteFed = this._disableEliteFederation;
	missionVariables.GalCopAdmin_DisableDocking = this._disableDocking;
}

//-------------------------------------------------------------------------------------------------------------
this.shipWillDockWithStation = function(station) {

	var w = worldScripts.EmailSystem;
	this.$setupRepNames(station);

	// have we changed ranking?
	var newrank = expandDescription("[commander_rank]");
	if (this._oldrank != newrank && newrank != "Harmless" && this._disableEliteFederation === false) {
		// send an email from the Elite Federation
		// get clean version of rank
		if (newrank.indexOf("Deadly") >=0) newrank = "Deadly";
		if (newrank.indexOf("E L I T E") >=0 || newrank.indexOf("ELITE") >= 0) newrank = "Elite";

		var pre = "a";
		if ("AEIOU".indexOf(player.ship.shipClassName.substring(0,1)) >= 0) {
			pre = "an";
		}
		w.$createEmail({sender:expandDescription("[elite-fed-sender]"),
			subject:expandDescription("[elite-fed-subject-" + newrank + "]"),
			date:global.clock.adjustedSeconds,
			message:expandDescription("[elite-fed-body-" + newrank + "]", {shipnamepre:pre, realshipname:player.ship.shipUniqueName, galnum:(galaxyNumber + 1)}),
			sentFrom:this._fedHQ[galaxyNumber]
		});
	}

	// do we have any pending bounty notifications?
	if (this._disableBounty === false && (this._bountyEmailBody != "" || (this._holdBountyEmail && this._holdBountyEmail.length > 0))) {
		var include = "";
		// have we got any bounty reports in the hold queue
		if (this._holdBountyEmail && this._holdBountyEmail.length > 0) {
			for (var i = 0; i < this._holdBountyEmail.length; i++) {
				if (this._holdBountyEmail[i].bountySystem === "Interstellar space") {
					include += "~In " + this._holdBountyEmail[i].bountySystem + ":\n" + this._holdBountyEmail[i].bountyEmail + "\n";
				} else {
					include += "~In the " + this._holdBountyEmail[i].bountySystem + " system:\n" + this._holdBountyEmail[i].bountyEmail + "\n";
				}
			}
			// any stuff left to add? add another system marker
			if (this._bountyEmailBody != "") {
				if (this._bountySystem === "Interstellar space") {
					include += "~In " + this._bountySystem + ":\n";
				} else {
					include += "~In the " + this._bountySystem + " system:\n";
				}
			}
			// clean up
			while(this._holdBountyEmail.length > 0) {
				this._holdBountyEmail.pop();
			}
		}
		// any emailbody holding info? add it to the include var
		if (this._bountyEmailBody != "") include += this._bountyEmailBody;

		// add warning for no-bounty kills
		if (this._bountyEmailTotalCR > 0 && this._bountyEmailBody.indexOf("(no bounty)") >= 0) include += "\nPlease note that GalCop does not condone the killing of innocent pilots.\n";

		// number of kills
		var kills = "A total of " + this._killCount + " ships were destroyed";
		if (this._killCount === 1) kills = "A single ship was destroyed";
		var killasrt = "";
		if (this._killAssortedCount > 0) {
			killasrt = " (plus " + this._killAssortedCount + " other items)";
			if (this._killAssortedCount === 1) killasrt = " (plus 1 other item)";
		}
		kills += killasrt;

		var emailType = "[galcop-bounty-admin-body]";
		// if we didn't have any bounty, switch to the no-bounty email type
		if (this._bountyEmailTotalCR === 0) emailType = "[galcop-nobounty-admin-body]";

		w.$createEmail({sender:expandDescription("[galcop-bounty-sender]"),
			subject:"Bounty confirmation",
			date:this._bountyEmailTime,
			message:expandDescription(emailType, {bounty:formatCredits(this._bountyEmailTotalCR, false, true), bountybody:include, totalkills:kills, sender:this._galCopBountyOfficer}),
			sentFrom:this._galCopHQ[galaxyNumber],
			expiryDays:this._defaultExpiryDays}
		);
		this._bountyEmailBody = "";
		this._bountyEmailTotalCR = 0;
		this._bountyEmailTime = 0;
		this._killCount = 0;
		this._killAssortedCount = 0;
	}

	// player ejected
	if (this._disableEscapePods === false && this._playerEjected === true) {
		var msg = expandDescription("[escapepod-rescue-body]", {sender:randomName() + " " + randomName()});
		w.$createEmail({sender:expandDescription("[escapepod-insurance-sender]"),
			subject:"Rescue",
			date:global.clock.adjustedSeconds,
			message:msg,
			expiryDays:this._defaultExpiryDays
		});
	}

	// docking fine
	if (this._disableDocking === false && station.requiresDockingClearance === true &&
		player.dockingClearanceStatus != "DOCKING_CLEARANCE_STATUS_GRANTED" && player.dockingClearanceStatus != "DOCKING_CLEARANCE_STATUS_TIMING_OUT" &&
		this._playerEjected === false && system.sun.isGoingNova === false) {

		// what type of station is this?
		var bSent = false;
		// calculate fine
		var fine = player.credits * 0.05;
		if (fine > 5000) fine = 5000;

		// main station
		if (station.isMainStation === true) {
			var msg = expandDescription("[galcop-docking-fine-body]", {fine:formatCredits(fine, true, true), stationname:station.displayName, sender:this._dutyOfficer});

			w.$createEmail({sender:expandDescription("[galcop-docking-fine-sender]"),
				subject:"Docking Penalty",
				date:global.clock.adjustedSeconds,
				message:msg,
				expiryDays:this._defaultExpiryDays
			});
			bSent = true;
		}
		// seedy space bar
		if (bSent === false && station.hasRole("random_hits_any_spacebar")) {
			var msg = expandDescription("[ssb-docking-fine-body]", {fine:formatCredits(fine, true, true), stationname:station.displayName, sender:this._dutyOfficer});

			w.$createEmail({sender:expandDescription("[ssb-docking-fine-sender]"),
				subject:"Docking Penalty",
				date:global.clock.adjustedSeconds,
				message:msg,
				expiryDays:this._defaultExpiryDays
			});
			bSent = true;
		}
		// blackmonk monestary
		if (bSent === false && station.hasRole("blackmonk_monastery")) {
			var msg = expandDescription("[bmm-docking-fine-body]", {fine:formatCredits(fine, true, true), stationname:station.displayName, sender:this._dutyOfficer});

			w.$createEmail({sender:expandDescription("[bmm-docking-fine-sender]"),
				subject:"Docking Penalty",
				date:global.clock.adjustedSeconds,
				message:msg,
				expiryDays:this._defaultExpiryDays
			});
			bSent = true;
		}
		// constore
		if (bSent === false && station.hasRole("constore")) {
			var msg = expandDescription("[constore-docking-fine-body]", {fine:formatCredits(fine, true, true), stationname:station.displayName, sender:this._dutyOfficer});

			w.$createEmail({sender:expandDescription("[constore-docking-fine-sender]"),
				subject:"Docking Penalty",
				date:global.clock.adjustedSeconds,
				message:msg,
				expiryDays:this._defaultExpiryDays
			});
			bSent = true;
		}
		// rescue station
		if (bSent === false && station.hasRole("rescue_station")) {
			var msg = expandDescription("[rs-docking-fine-body]", {fine:formatCredits(fine, true, true), stationname:station.displayName, sender:this._dutyOfficer});

			w.$createEmail({sender:expandDescription("[rs-docking-fine-sender]"),
				subject:"Docking Penalty",
				date:global.clock.adjustedSeconds,
				message:msg,
				expiryDays:this._defaultExpiryDays
			});
			bSent = true;
		}
		// other?
		if (bSent === false) {
			var msg = expandDescription("[generic-docking-fine-body]", {fine:formatCredits(fine, true, true), stationname:station.name, sender:this._dutyOfficer});

			w.$createEmail({sender:expandDescription("[generic-docking-fine-sender]"),
				subject:"Docking Penalty",
				date:global.clock.adjustedSeconds,
				message:msg,
				expiryDays:this._defaultExpiryDays
			});
		}
	}

	// general fine notification/payment
	if (this._disableFines === false && player.ship.markedForFines === true && station.isMainStation && station.suppressArrivalReports === false && system.sun.isGoingNova === false && player.bounty < (50 - (system.info.government * 6))) {
		// calculate what the fine is
		var gov = 0;
		var calc_fine = 0;
		if (global.system.ID === -1) {
			gov = 1;
		} else {
			gov = system.government;
		}
		calc_fine = 50 + ((gov < 2 || gov > 5) ? 50 : 0);
		calc_fine *= player.bounty;
		if (calc_fine > player.credits) {
			calc_fine = player.credits;
		}

		if (calc_fine > 0) {
			var msg = expandDescription("[marked-for-fines-intro]", {fine:formatCredits(calc_fine, true, true)});
			if (player.bounty >= 50) {
				msg += expandDescription("[marked-for-fines-fugitive]");
			}
			msg += expandDescription("[marked-for-fines-end]");
			w.$createEmail({sender:expandDescription("[marked-for-fines-sender]"),
				subject:"Fine payment",
				date:global.clock.adjustedSeconds,
				message:msg,
				expiryDays:this._defaultExpiryDays
			});
		}
	}

	this._playerEjected = false;
}

//-------------------------------------------------------------------------------------------------------------
this.guiScreenChanged = function(to, from) {

	// make a note of when we reach the shipyafrds screen
	if (guiScreen === "GUI_SCREEN_SHIPYARD" && this._disableNewShip === false) {
		var p = player.ship;
		this._playerEnteringShipyard = true;
		this._oldcredits = player.credits;

		// calculate price of current ship
		// reproduction of core code
		this._storedShipCost = this.$cunningFee((((p.price - (p.price * 0.006 * this.$missingSubEntitiesAdjustment())) * 75 * p.serviceLevel) + 5000) / 10000 , 0.005);

		// calculate price of stock in hold
		this._storedManifestCost = 0;
		for (var i = 0; i < manifest.list.length; i++) {
			this._storedManifestCost += Math.round(manifest.list[i].quantity * (p.dockedStation.market[manifest.list[i].commodity].price / 10) * 100) / 100;
		}
	}
	if (guiScreen != "GUI_SCREEN_SHIPYARD" && guiScreen != "GUI_SCREEN_STATUS" && this._playerEnteringShipyard === true) {
		this._playerEnteringShipyard = false;
	}

	// sending an email at the start of a new game, welcome player to their just-purchased ship
	// note, because of the way Hardships works, it's hard to know when the player has finished looking at ships and testing them
	// so, if you have hardships installed, you won't get a welcome to your new ship email. Standard purchasing will still give an email
	if (guiScreen === "GUI_SCREEN_STATUS" && player.name === "Jameson" && !worldScripts.hardships) {
		if (global.clock.clockStringForTime(global.clock.seconds).indexOf("2084004:20") === 0 && !missionVariables.EmailSystem_newGame) {
			// this is a new game, send new ship email
			missionVariables.EmailSystem_newGame = 1;
			this.$sendNewShipEmail(player.ship);
		}
		if (global.clock.clockStringForTime(global.clock.seconds).indexOf("2084004:20") === -1) {
			// clean up
			delete missionVariables.EmailSystem_newGame;
		}
	}

	// send a pilot licence email
	if (guiScreen === "GUI_SCREEN_STATUS" && player.name === "Jameson") {
		if (global.clock.clockStringForTime(global.clock.seconds).indexOf("2084004:20") === 0 && this._licenceNumber === "") {
			this._licenceNumber = this.$generateLicenceNumber();
			// pilot licence notification arrives shortly after the start
			var w = worldScripts.EmailSystem;
			w.$createEmail({sender:expandDescription("[new-licence-sender]"),
				subject:expandDescription("New Pilot Registration"),
				date:global.clock.seconds + 40,
				message:expandDescription("[new-licence]", {licenceno:this._licenceNumber}),
				sentFrom:this._galCopHQ[galaxyNumber]
			});
		}
	}

	// make a note of the credit balance
	if(to && to === "GUI_SCREEN_EQUIP_SHIP") {
		this._oldcredits = player.credits;
		this._oldPassengerCount = player.ship.passengerCapacity;
		// if the Battle damage OXP is present, check for the HULL REPAIR item. If it's not on the player ship, there is hull damage to repair
		var w = worldScripts["Battle Damage"];
		if (w && missionVariables.BattleDamage_status === "DAMAGED") {
			// there is hull damage to repair
			this._repairHullDamage = true;
		}
		// if the Iron Hide OXP is present, check for damage so we can include an item in the email
		w = worldScripts["IronHide Armour Script"];
		if(w) {
			if ((player.ship.equipmentStatus("EQ_IRONHIDE") === "EQUIPMENT_OK" || player.ship.equipmentStatus("EQ_IRONHIDE_MIL") === "EQUIPMENT_OK") && missionVariables.ironHide_percentage < 100) {
				this._repairIronHide = true;
			}
		}

		// store equipment status of all items so we can tell when it's a repair job
		var p = player.ship;
		var eq = p.equipment;
		for (var i = 0; i < eq.length; i++) {
			var q = eq[i];
			this._equipItems.push({key:q.equipmentKey, status:p.equipmentStatus(q.equipmentKey)});
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
// special case for IronHide
this.missionScreenEnded = function() {
	// if the ironhide armour gets repairs during the maintenance overhaul, it calls a mission screen
	// this will run when the mission screen ends
	if (this._sendMaintEmail === true) {
		this._sendMaintEmail = false;
		this._maintCost = this._maintCostInit - player.credits;
		this.$sendMaintenanceEmail()
	}
	// if the ironhide armour is repaired on its own, it still calls a mission screen
	// this will run when that screen ends
	if (this._sendRepairEmail === true) {
		this._sendRepairEmail = false;
		var msg = expandDescription("[purchase-maintenance]",
			{shipname:player.ship.shipClassName,
			maintenanceitems:"\n\n- Repair of IronHide armour",
			servicecost:formatCredits(this._maintCostInit - player.credits, true, true),
			costnote:"",
			sender:this._maintRep
		});

		var w = worldScripts.EmailSystem;
		w.$createEmail({sender:expandDescription("[purchase-maintenance-sender]"),
			subject:"Repair (Invoice #" + (this.$rand(500000) + 100000).toString() + ")",
			date:global.clock.adjustedSeconds,
			message:msg,
			expiryDays:this._defaultExpiryDays
		});
	}
}

//-------------------------------------------------------------------------------------------------------------
this.shipLaunchedEscapePod = function(pod, pass) {
	this._playerEjected = true;
}

//-------------------------------------------------------------------------------------------------------------
this.playerBoughtEquipment = function(equipment) {
	if (this._debug) log(this.name, "equipment " + equipment);
	// start a timer to do all the bought equipment work
	// this is to ensure any OXP processes have completed (like refunding) before we do out calculations
	this._boughtKey = equipment
	this._boughtStn = player.ship.dockedStation;
	// make sure the timer isn't already running
	if (this._boughtEquipTimer && this._boughtEquipTimer.isRunning) this._boughtEquipTimer.stop();
	this._boughtEquipTimer = new Timer(this, this.$playerBoughtEquip, 1, 0);
}

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

	var equipment = this._boughtKey;
	if (this._debug) log(this.name, "bought key = " + equipment);
	// work out where the purchase occurred
	var stn = null;
	if (this._boughtStn) {
		stn = this._boughtStn;
	} else {
		if (player.ship.dockedStation) {
			stn = player.ship.dockedStation;
		} else {
			stn = system.mainStation;
		}
	}
	this._boughtStn = null;

	// don't do anything for these items
	if (this._purchase_ignore_equip.indexOf(equipment) >= 0) return;

	var w = worldScripts.EmailSystem;

	if (this._shipSalesRep === "") {
		this.$setupRepNames(stn);
	}

	if (equipment !== "EQ_RENOVATION") {
		// purchasing items
		var etype = "purchase";
		var subtype = 0;
		var eq = EquipmentInfo.infoForKey(equipment);

		// work out what sort of purchase this was: standard purchase, repair or removal

		// we could be repairing something so check the statuses we saved when we switched to the equip ship screen
		if (this._equipItems && this._equipItems.length > 0) {
			for (var i = 0; i < this._equipItems.length; i++) {
				if (this._equipItems[i].key === equipment && this._equipItems[i].status === "EQUIPMENT_DAMAGED") etype = "repair";
			}
		}
		// if the equipment key itself has "REPAIR" in it, switch to repair mode
		if (this._maint_repair_equip.indexOf(equipment) >= 0 || (equipment.indexOf("REPAIR") >= 0 && equipment.indexOf("REPAIRBOTS") === 0)) etype = "repair";
		// if the equipment key itself has "REMOVAL" in it, switch to removal mode
		var desc = eq.description.toLowerCase();
		if (this._maint_remove_equip.indexOf(equipment) >= 0 || 
			(desc.indexOf("remove") >= 0 || 
				(desc.indexOf("sell ") >= 0 && desc.indexOf("buy ") === -1) || 
				desc.indexOf("unmount") >= 0 || 
				desc.indexOf("undo ") >= 0 || 
				equipment.indexOf("REFUND") >= 0))
			{etype = "remove"; subtype = 1;}
		// if the player doesn't have the equipment anymore, it must be a removal
		// this should really only be triggered by an external pack calling the playerBoughtEquipment routine manually (see Ship Configuration for an example)
		if (etype === "purchase") {
			if (equipment != "EQ_PASSENGER_BERTH" && player.ship.equipmentStatus(equipment) !== "EQUIPMENT_OK" && this._purchase_removed.indexOf(equipment) === -1) etype = "remove";
		}

		var extra = "";
		var msg = "";
		var itemname = "";

		// see if there is a replacement description for this piece of equipment
		if (this._maintItemNameReplacements.indexOf(equipment) >= 0) {
			itemname = expandDescription("[maint_itemname_" + equipment + "]");
			if (!itemname || itemname === "" || itemname.indexOf("[maint_") >= 0) {
				itemname = eq.name;
			}
		} else {
			itemname = eq.name;
		}

		if (this._debug) {
			log(this.name, "etype = " + etype);
			log(this.name, "subtype = " + subtype);
			log(this.name, "old creds = " + this._oldcredits);
			log(this.name, "curr creds = " + player.credits);
		}

		switch (etype) {
			case "remove":
				if (this._disableEquipPurchase === false) {
					var cost = "";
					// were we charged something, or did we get a refund?
					if (eq.price != 0) {
						if ((this._oldcredits - player.credits) < 0) {
							if (subtype === 1) {
								cost = "The cost of this service was " + formatCredits(eq.price / 10, true, true) + ", but all together you were refunded " + formatCredits(player.credits - this._oldcredits, true, true) + ".";
							} else {
								cost = "In total you were refunded " + formatCredits(player.credits - this._oldcredits, true, true) + ".";
							}
						} else {
							cost = "You were charged " + formatCredits(this._oldcredits - player.credits, true, true) + ".";
						}
					} else {
						if ((this._oldcredits - player.credits) < 0) {
							cost = "In total you were refunded " + formatCredits(player.credits - this._oldcredits, true, true) + ".";
						} else {
							cost = "There was no charge for this service.";
						}
					}

					msg = expandDescription("[remove-equip]",
						{stationname:stn.displayName,
						equipname:itemname,
						equipcost:cost,
						sender:this._equipSalesRep
					});

					w.$createEmail({sender:expandDescription("[purchase-equip-sender]"),
						subject:"Removing equipment",
						date:global.clock.adjustedSeconds,
						message:msg,
						expiryDays:this._defaultExpiryDays
					});

					// special case for escape pod
					if (equipment === "EQ_ESCAPE_POD") {
						msg = expandDescription("[escapepod-sold-body]",
							{sender:randomName() + " " + randomName()
						});

						w.$createEmail({sender:expandDescription("[escapepod-insurance-sender]"),
							subject:"Contract termination",
							date:global.clock.adjustedSeconds + 150,
							message:msg,
							expiryDays:this._defaultExpiryDays
						});
					}
				}
				break;
			case "purchase":
				if (this._disableEquipPurchase === false) {
					var paid = ((eq.price / 10) * stn.equipmentPriceFactor);
					if (this._oldcredits != player.credits + ((eq.price / 10) * stn.equipmentPriceFactor)) {
						paid = (this._oldcredits - player.credits);
						// did we get a refund for an existing weapon?
						if (equipment.indexOf("EQ_WEAPON") >= 0) {
							extra = "(Note: You were refunded the cost of any laser that was removed in order to install your new one.)";
							// switch back to the direct equipment price, otherwise the email text gets confusing
							paid = ((eq.price / 10) * stn.equipmentPriceFactor);
						}
					}

					if (this._debug) log(this.name, "paid = " + paid);

					msg = expandDescription("[purchase-equip]",
						{stationname:stn.displayName,
						extranote:extra,
						equipname:itemname + ": " + eq.description,
						equipcost:"The cost of this item was " + formatCredits(paid, true, true),
						sender:this._equipSalesRep
					});

					// make the missle purchase email more reasonable.
					if (equipment.indexOf("MISSILE") >= 0) {
						msg = msg.replace("We hope you will enjoy many years of trouble-free use of this item.", "We hope this item performs flawlessly for you when you need it.");
					}

					w.$createEmail({sender:expandDescription("[purchase-equip-sender]"),
						subject:"Purchasing equipment",
						date:global.clock.adjustedSeconds,
						message:msg,
						expiryDays:this._defaultExpiryDays
					});

					// special case for escapepods - send insurance application notification
					if (equipment === "EQ_ESCAPE_POD") {
						msg = expandDescription("[escapepod-purchase-body]",
							{sender:randomName() + " " + randomName()
						});

						w.$createEmail({sender:expandDescription("[escapepod-insurance-sender]"),
							subject:"Insurance Application",
							date:global.clock.adjustedSeconds + 300,
							message:msg,
							expiryDays:this._defaultExpiryDays
						});
					}
				}
				break;

			case "repair":
				if (this._disableMaintenance === false) {
					if (equipment === "EQ_IRONHIDE_REPAIR") {
						this._sendRepairEmail = true;
						this._maintCostInit = this._oldcredits;
					} else {
						if ((this._oldcredits - player.credits) > 0) {
							var maint = "";
							if (eq.name.toLowerCase().indexOf("repair") >= 0) {
								maint = "\n\n- " + eq.name;
							} else {
								maint = "\n\n- Repair of " + eq.name;
							}

							// special cases - ShipVersion OXP
							if (equipment === "EQ_TURRET_RECOVER") maint = "\n\n- Recovery of destroyed turrets";
							if (equipment === "EQ_SHIP_VERSION_REPAIR") maint = "\n\n- Restoring the recharge bonuses provided by Ship Version equipment.";

							msg = expandDescription("[purchase-maintenance]",
								{shipname:player.ship.shipClassName,
								maintenanceitems:maint,
								servicecost:formatCredits(this._oldcredits - player.credits, true, true),
								costnote:"",
								sender:this._maintRep
							});

							var w = worldScripts.EmailSystem;
							w.$createEmail({sender:expandDescription("[purchase-maintenance-sender]"),
								subject:"Repair (Invoice #" + (this.$rand(500000) + 100000).toString() + ")",
								date:global.clock.adjustedSeconds,
								message:msg,
								expiryDays:this._defaultExpiryDays
							});
						}
					}
				}
				break;
		}

		if (equipment === "EQ_IRONHIDE_REPAIR") this._repairIronHide = false;
		if (equipment === "EQ_HULL_REPAIR") this._repairHullDamage = false;

	} else {
		if (this._disableMaintenance === false) {
			if (this._repairIronHide === false) {
				// if there's no ironhide armour repair, send the email immediately
				this._maintCost = this._oldcredits - player.credits;
				this.$sendMaintenanceEmail();
			} else {
				// otherwise set up params for sending email later
				this._sendMaintEmail = true;
				this._maintCostInit = this._oldcredits;
			}
		}
	}

	this._oldcredits = player.credits;
}

//-------------------------------------------------------------------------------------------------------------
this.playerBoughtNewShip = function(ship, price) {
	if (this._playerEnteringShipyard === true && this._disableNewShip === false) {
		this._newShipCost = (this._oldcredits + this._storedShipCost + this._storedManifestCost) - player.credits;
		this._playerEnteringShipyard = false;
		this.$sendNewShipEmail(ship);
	}
	this._oldcredits = player.credits;
}

//-------------------------------------------------------------------------------------------------------------
this.shipLaunchedFromStation = function(station) {
	// make a note of the rank we have on launch
	this._oldrank = expandDescription("[commander_rank]");
	// make a note of the system we launch in
	this._bountySystem = system.name;
}

//-------------------------------------------------------------------------------------------------------------
this.shipKilledOther = function(whom, damageType) {
	// don't record anything if this is a siumulator run
	if (this.$simulatorRunning()) return;
	if (whom.isMissile) {
		this._killAssortedCount += 1;
		this._bountyEmailTime = global.clock.seconds;
	}
	if (!whom.hasRole("escape-capsule") && (whom.isPiloted || whom.isDerelict || whom.hasRole("tharglet") || whom.hasRole("thargon"))) {
		// increment the kill counter
		this._killCount += 1;
		this._bountyEmailTime = global.clock.seconds;
		// but was their a bounty?
		if (whom.bounty > 0) {
			// add a new item to the bounty email body
			var true_bounty = whom.bounty;
			if (whom.isCargo || whom.scanClass == "CLASS_BUOY" || whom.scanClass == "CLASS_ROCK") true_bounty = whom.bounty / 10;
			this._bountyEmailBody += "~- Destruction of " + whom.displayName + " for " + formatCredits(true_bounty, false, true) + ".\n";
			this._bountyEmailTotalCR += true_bounty;
		} else {
			// add a no bounty item to the email body
			this._bountyEmailBody += "~- Destruction of " + whom.displayName + " (no bounty).\n";
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.shipExitedWitchspace = function() {
	// if we enter a system while there is some bounty email body hanging around, put it into an array so we can itemise it later
	if (this._bountyEmailBody != "") {
		if (!this._holdBountyEmail) {
			this._holdBountyEmail = [];
		}
		this._holdBountyEmail.push({bountyEmail:this._bountyEmailBody, bountySystem:this._bountySystem});
		this._bountyEmailBody = "";
	}
	this._bountySystem = system.name;
}

//-------------------------------------------------------------------------------------------------------------
// bounty on scooped escape capsule (1.81 only)
this.playerRescuedEscapePod = function(fee, reason, occupant) {

	var w = worldScripts.EmailSystem;

	var msg = "";
	var subj = "";
	var sndr = "";
	switch (reason) {
		case "insurance":
			sndr = expandDescription("[escapepod-insurance-sender]");
			subj = "Salvage fee for escape pod recovery";
			msg = expandDescription("[escapepod-insurance-body]",
				{occupant:occupant.name,
				description:occupant.description,
				fee:formatCredits(fee / 10, true, true),
				sender:this._insuranceOfficer
			});
			break;
		case "bounty":
			sndr = expandDescription("[galcop-bounty-sender]");
			subj = "Bounty for escape pod recovery";
			msg = expandDescription("[escapepod-bounty-body]",
				{occupant:occupant.name,
				description:occupant.description,
				fee:formatCredits(fee / 10, true, true),
				sentFrom:this._galCopHQ[galaxyNumber],
				sender:this._galCopBountyOfficer
			});
			break;
		//case "slave":
	}
	if (sndr != "" && this._disableEscapePods === false) {
		w.$createEmail({sender:sndr,
			subject:subj,
			date:global.clock.seconds,
			message:msg,
			expiryDays:this._defaultExpiryDays
		});
	}
}

//-------------------------------------------------------------------------------------------------------------
// send email to player when a contract is started. (1.81 only)
this.playerEnteredContract = function(type, contract) {

	var w = worldScripts.EmailSystem;

	var msg = "";
	var subj = "";
	var sndr = "";

	switch (type) {
		case "cargo":
			sndr = expandDescription("[cargo-contract-sender]");
			subj = "Accepted contract: " + contract.cargo_description;
			msg = expandDescription("[cargo-contract-start]",
				{description:contract.cargo_description,
				systemname:System.systemNameForID(contract.destination),
				time:global.clock.clockStringForTime(contract.arrival_time),
				fee:formatCredits(contract.fee, true, true)
			});
			break;
		case "passenger":
			sndr = expandDescription("[passenger-contract-sender]");
			subj = "Accepted contract: " + contract.name;
			msg = expandDescription("[passenger-contract-start]",
				{contractname:contract.name,
				systemname:System.systemNameForID(contract.destination),
				time:global.clock.clockStringForTime(contract.arrival_time),
				fee:formatCredits(contract.fee, true, true)
			});
			break;
		case "parcel":
			sndr = expandDescription("[parcel-contract-sender]");
			subj = "Accepted contract: " + contract.name;
			msg = expandDescription("[parcel-contract-start]",
				{contractname:contract.name,
				systemname:System.systemNameForID(contract.destination),
				time:global.clock.clockStringForTime(contract.arrival_time),
				fee:formatCredits(contract.fee, true, true)
			});
			break;
	}

	if (sndr != "" && this._disableContracts === false) {
		w.$createEmail({sender:sndr,
			subject:subj,
			date:global.clock.adjustedSeconds,
			message:msg
		});
	}
}

//-------------------------------------------------------------------------------------------------------------
// result of parcel/passenger/cargo contract (1.81 only)
this.playerCompletedContract = function(type, result, fee, contract) {

	var w = worldScripts.EmailSystem;

	if (this._disableContracts === false) {
		var msg = "";
		var subj = "";
		var sndr = "";

		sndr = expandDescription("[" + type + "-contract-sender]");
		msg = expandDescription("[" + type + "-contract-" + result + "]",
			{description:contract.cargo_description,
			contractname:contract.name,
			systemname:System.systemNameForID(contract.destination),
			time:global.clock.clockStringForTime(contract.arrival_time),
			fee:formatCredits(fee / 10, true, true)
		});
		subj = expandDescription("[" + type + "-contract-" + result + "-subject]");

		w.$createEmail({sender:sndr,
			subject:subj,
			date:global.clock.adjustedSeconds,
			message:msg,
			expiryDays:this._defaultExpiryDays
		});
	}
}

//-------------------------------------------------------------------------------------------------------------
// sends the maintenance email
this.$sendMaintenanceEmail = function() {

	var maint = "";
	var item = "";
	var count = 0;
	var hasLCB = false;

	var extra = "";
	if (this._repairIronHide === true) extra = " (includes cost of IronHide armour repair)";

	// add common items
	maint += "\n\n~General ship maintenance tasks\n";
	maint += expandDescription("[maintitem_all]");

	// add general items
	do {
		item = expandDescription("[maintitem_general]");
		if (item.trim() != "") {
			// because the first word of the item can be random, check if everything after the first word exists in our maintenance list yet
			// only add it if it doesn't exist
			if (maint.indexOf(item.substring(item.indexOf(" ")).trim()) === -1) {
				maint += "\n- " + item;
				count += 1;
			}
		}
	} while (count < system.info.techlevel);

	var p = player.ship;
	var itemname = "";
	var diagtype = expandDescription("[maintperform]");

	maint += "\n\n~Equipment specific maintenance tasks"
	// if the player ship hyperspace capable?
	if (p.hasHyperspaceMotor) {
		maint += "\n- " + diagtype + " diagnostics on Witchspace Drive";
		maint += "\n- " + expandDescription("[maint_Hyperspace]");
	}

	// get list of equipment keys to ignore (hull damage equipment item from the Battle Damage OXP, IronHide item, and any cats/dogs from Ships Cat OXP)
	// add equipment specific items;
	for (var i = 0; i < p.equipment.length; i++) {
		var e_item = p.equipment[i];
		if (this._maint_known_equip.indexOf(e_item.equipmentKey) >= 0) {
			// can this item be repaired in this system?
			if ((e_item.techLevel - 1) <= system.info.techlevel && e_item.isVisible) {
				// is this piece of equipment OK (don't do maintenance on damaged equipment)
				if (p.equipmentStatus(e_item.equipmentKey) === "EQUIPMENT_OK") {
					// add a diagnostic entry for all items
					itemname = e_item.name;

					// check if we have a large cargo bay - we'll use this later on
					if (e_item.equipmentKey === "EQ_CARGO_BAY") hasLCB = true;

					// special case for passenger berth, as its name has extraneous text in it
					if (e_item.equipmentKey === "EQ_PASSENGER_BERTH") itemname = "Passenger Berth";

					maint += "\n- " + diagtype + " diagnostics on " + itemname;

					// get a maintenance item for this equipment
					item = expandDescription("[maint_" + e_item.equipmentKey + "]");
					// do we have a real value? if so, add it to the list
					if (item.indexOf("[") === -1 && item.trim() != "") maint += "\n- " + item;
				} else if (p.equipmentStatus(e_item.equipmentKey) === "EQUIPMENT_DAMAGED") {
					// this item is damaged - just note it in the message
					// add a diagnostic entry for all items
					itemname = e_item.name;

					// special case for passenger berth, as its name has extraneous text in it
					if (e_item.equipmentKey === "EQ_PASSENGER_BERTH") itemname = "Passenger Berth";

					item = "Identified repairs required for " + itemname;
					maint += "\n- " + item;
				}
			}
		}
	}

	// did the player repair the ironhide armour?
	if ((p.equipmentStatus("EQ_IRONHIDE") === "EQUIPMENT_OK" || p.equipmentStatus("EQ_IRONHIDE_MIL") === "EQUIPMENT_OK") && missionVariables.ironHide_percentage === 100 && this._repairIronHide === true) {
		// player repaired ironhide armour
		maint += "\n- Repair IronHide armour";
		this._repairIronHide = false;
	}

	// for battle damage OXP, add an item to show that the damage has been repaired
	if (this._repairHullDamage === true) {
		maint += "\n- Repair hull damage";
		this._repairHullDamage = false;
	}

	// do we have any cargo space but no LCB?
	if (p.cargoSpaceCapacity > 0 && hasLCB === false) {
		maint += "\n- " + expandDescription("[maint_CargoBay]");
	}

	// do we have any missiles?
	if (p.missileCapacity > 0) {
		maint += "\n- " + diagtype + " diagnostics on missile system";
		maint += "\n- " + expandDescription("[maint_MissileSystem]");
	}

	// weapon equipment items don't appear in the ship.equipment array, so we need to do an individual check for each one
	if (p.forwardWeapon && p.forwardWeapon.equipmentKey != "EQ_WEAPON_NONE" && this._maint_known_equip.indexOf(p.forwardWeapon.equipmentKey) >= 0) {
		if (p.forwardWeapon.techLevel <= (system.info.techlevel - 1)) {
			maint += "\n- " + diagtype + " diagnostics on front " + p.forwardWeapon.name;
			item = expandDescription("[maint_" + p.forwardWeapon.equipmentKey + "]", {pos:"front"});
			maint += "\n- " + item;
		}
	}
	if (p.aftWeapon && p.aftWeapon.equipmentKey != "EQ_WEAPON_NONE" && this._maint_known_equip.indexOf(p.aftWeapon.equipmentKey) >= 0) {
		if (p.aftWeapon.techLevel <= (system.info.techlevel - 1)) {
			maint += "\n- " + diagtype + " diagnostics on aft " + p.aftWeapon.name;
			item = expandDescription("[maint_" + p.aftWeapon.equipmentKey + "]", {pos:"aft"});
			maint += "\n- " + item;
		}
	}
	if (p.portWeapon && p.portWeapon.equipmentKey != "EQ_WEAPON_NONE" && this._maint_known_equip.indexOf(p.portWeapon.equipmentKey) >= 0) {
		if (p.portWeapon.techLevel <= (system.info.techlevel - 1)) {
			maint += "\n- " + diagtype + " diagnostics on port " + p.portWeapon.name;
			item = expandDescription("[maint_" + p.portWeapon.equipmentKey + "]", {pos:"port"});
			maint += "\n- " + item;
		}
	}
	if (p.starboardWeapon && p.starboardWeapon.equipmentKey != "EQ_WEAPON_NONE" && this._maint_known_equip.indexOf(p.starboardWeapon.equipmentKey) >= 0) {
		if (p.starboardWeapon.techLevel <= (system.info.techlevel - 1)) {
			maint += "\n- " + diagtype + " diagnostics on starboard " + p.starboardWeapon.name;
			item = expandDescription("[maint_" + p.starboardWeapon.equipmentKey + "]", {pos:"starboard"});
			maint += "\n- " + item;
		}
	}

	maint += "\n- Ship detailing and valet service\n- Parts and labour";

	var msg = expandDescription("[purchase-maintenance]",
		{shipname:p.shipClassName,
		maintenanceitems:maint,
		servicecost:formatCredits(this._maintCost, true, true),
		costnote:extra,
		sender:this._maintRep
	});

	var w = worldScripts.EmailSystem;
	w.$createEmail({sender:expandDescription("[purchase-maintenance-sender]"),
		subject:"Overhaul (Invoice #" + (this.$rand(500000) + 100000).toString() + ")",
		date:global.clock.adjustedSeconds,
		message:msg,
		expiryDays:this._defaultExpiryDays
	});
}

//-------------------------------------------------------------------------------------------------------------
// sends new ship email
this.$sendNewShipEmail = function(ship) {

	// make sure we have created the rep names
	if (this._shipSalesRep === "") this.$setupRepNames();

	var equip = "";
	var specs = "";
	var inj = 7;
	if (0 >= oolite.compareVersion("1.81")) inj = ship.injectorSpeedFactor;

	var wpn = "\n     Front - ";
	if (ship.forwardWeapon && ship.forwardWeapon.equipmentKey != "EQ_WEAPON_NONE") {
		wpn += ship.forwardWeapon.name;
	} else {
		wpn += "None";
	}

	if (" 3 7 11 15 ".indexOf(" " + ship.weaponFacings.toString() + " ") >= 0) {
		wpn += "\n     Aft - ";
		if (ship.aftWeapon && ship.aftWeapon.equipmentKey != "EQ_WEAPON_NONE") {
			wpn += ship.aftWeapon.name;
		} else {
			wpn += "None";
		}
	}
	if (" 5 7 13 15 ".indexOf(" " + ship.weaponFacings.toString() + " ") >= 0) {
		wpn += "\n     Port - ";
		if (ship.portWeapon && ship.portWeapon.equipmentKey != "EQ_WEAPON_NONE") {
			wpn += ship.portWeapon.name;
		} else {
			wpn += "None";
		}
	}
	if (" 9 11 13 15 ".indexOf(" " + ship.weaponFacings.toString() + " ") >= 0) {
		wpn += "\n     Starboard - ";
		if (ship.starboardWeapon && ship.starboardWeapon.equipmentKey != "EQ_WEAPON_NONE") {
			wpn += ship.starboardWeapon.name;
		} else {
			wpn += "None";
		}
	}

	specs = "- Max speed/thrust: " + (ship.maxSpeed / 1000).toFixed(3) + "/" + (ship.maxThrust / 1000).toFixed(3) + " LS\n" +
		"- Injector speed: " + ((ship.maxSpeed * inj) / 1000).toFixed(3) + " LS\n" +
		"- Max pitch/roll/yaw: " + ship.maxPitch.toFixed(2) + "/" + ship.maxRoll.toFixed(2) + "/" + ship.maxYaw.toFixed(2) + "\n" +
		"- Laser mounts: " + wpn + "\n" +
		"- Missile pylons: " + ship.missileCapacity + "\n" +
		"- Cargo capacity: " + ship.cargoSpaceCapacity + " tons\n" +
		"- Max energy: " + (ship.maxEnergy) + " units\n" +
		"- Energy recharge rate: " + ship.energyRechargeRate.toFixed(2) + " units/sec\n" +
		"- Hyperspace capable: " + ship.hasHyperspaceMotor + "\n";

	if (ship.equipment.length > 0) {
		equip = "Your ship also came equipped with the following items:\n";
		var added = false;
		var e = ship.equipment;
		for (var i = 0; i < e.length; i++) {
			var q = e[i];
			if ("EQ_HULL_REPAIR".indexOf(q.equipmentKey) === -1) {
				if (q.isPortableBetweenShips === false && q.isVisible === true) {
					equip += "- " + q.name + "\n";
					added = true
				}
			}
		}
		if (added === false) {
			equip = "";
		} else {
			equip += "\n";
		}
	}

	var cost = ship.price;
	var extra = "";

	if (this._newShipCost) {
		cost = this._newShipCost;
		extra = " As part of this transaction your old ship was traded for " + formatCredits(this._storedShipCost, false, true);
		if (this._storedManifestCost > 0) extra += ", and the content of your hold was traded for " + formatCredits(this._storedManifestCost, true, true);
		extra += ".";
	}

	var msg = expandDescription("[purchase-ship]",
		{stationname:player.ship.dockedStation.displayName,
		shipclass:ship.shipClassName,
		shipspecs:specs,
		shipequip:equip,
		shipcost:formatCredits(cost, false, true),
		extracost:extra,
		rndsystem:System.systemNameForID(this.$rand(256)-1),
		sender:this._shipSalesRep
	});

	var w = worldScripts.EmailSystem;
	w.$createEmail({sender:expandDescription("[purchase-ship-sender]"),
		subject:"Purchase of new ship",
		date:global.clock.adjustedSeconds,
		message:msg
	});

	// transfer of ownership email arrives shortly after the first one.
	w.$createEmail({sender:"GalCop Ship Registry",
		subject:"Transfer of ownership",
		date:global.clock.adjustedSeconds + 25,
		message:expandDescription("[new-ship-transfer]")
	});
}

//-------------------------------------------------------------------------------------------------------------
// return a random number between 1 and max
this.$rand = function(max) {
	return Math.floor((Math.random() * max) + 1)
}

//-------------------------------------------------------------------------------------------------------------
// generates a random licence number in the format "GPL000000-XX000-XX0000"
this.$generateLicenceNumber = function() {

	var ln = "GPL";

	ln += (this.$rand(800000) + 100000);
	ln += "-" + expandDescription("[licencecodes]");
	ln += (this.$rand(500) + 200);
	ln += "-" + expandDescription("[licencecodes]");
	ln += (this.$rand(8000) + 1000);

	return ln;
}

//-------------------------------------------------------------------------------------------------------------
// set up and store various names for inclusion in emails
this.$setupRepNames = function(station) {
	if (!station && !player.ship.dockedStation) station = system.mainStation;
	if (!station && player.ship.dockedStation) station = player.ship.dockedStation;

	// if we just redocked at the same station, don't change anything
	if (this._namesLocation === system.name + "-" + station.displayName) return;

	this._namesLocation = system.name + "-" + station.displayName;

	this._shipSalesRep = randomName() + " " + randomName();
	this._equipSalesRep = randomName() + " " + randomName();
	this._maintRep = randomName() + " " + randomName();
	this._dutyOfficer = randomName() + " " + randomName();
	this._insuranceOfficer = randomName() + " " + randomName();
	// bounty processing occurs in a central location, so only update the officer occasionally
	if (this._galCopBountyOfficer === "") {
		this._galCopBountyOfficer = randomName() + " " + randomName();
	} else {
		if (this.$rand(20) > 15) this._galCopBountyOfficer = randomName() + " " + randomName();
	}
}

//-------------------------------------------------------------------------------------------------------------
// reproductions of core code to try and work out an accurate trade in value for the player's ship
this.$cunningFee = function(value, precision) {
	var fee = value;
	var superfee = 100000.0;
	var max = 1 + precision;
	var min = 1 - precision;
	var rounded_fee = superfee * Math.floor(0.5 + fee / superfee);
	if (rounded_fee === 0) rounded_fee = 1;
	var ratio = fee / parseFloat(rounded_fee);
	while ((ratio < min || ratio > max) && superfee > 1)
	{
		rounded_fee = superfee * Math.floor(0.5 + fee / superfee);
		if (rounded_fee === 0) rounded_fee = 1;
		ratio = fee / parseFloat(rounded_fee);
		superfee /= 10.0;
	}
	if (ratio > min && ratio < max) fee = rounded_fee;
	return fee;
}

//-------------------------------------------------------------------------------------------------------------
this.$missingSubEntitiesAdjustment = function() {
	// each missing subentity depreciates the ship by 5%, up to a maximum of 35% depreciation.
	var percent = 5 * (player.ship.subEntityCapacity - player.ship.subEntities.length);
	return (percent > 35 ? 35 : percent);
}

//-------------------------------------------------------------------------------------------------------------
this.$simulatorRunning = function() {
	var w = worldScripts["Combat Simulator"];
	if (w && w.$checkFight && w.$checkFight.isRunning) return true;
	return false;
}