Back to Index Page generated: May 8, 2024, 6:16:03 AM

Expansion CommsLog MFD

Content

Warnings

  1. http://wiki.alioth.net/index.php/CommsLog%20MFD -> 404 Not Found
  2. Low hanging fuit: Information URL exists...
  3. Unknown key 'upload_date' at https://wiki.alioth.net/img_auth.php/e/e4/CommsLogMFD.oxz!manifest.plist

Manifest

from Expansion Manager's OXP list from Expansion Manifest
Description Stores all communications received by the player and displays them in a multi-function display. Stores all communications received by the player and displays them in a multi-function display.
Identifier oolite.oxp.phkb.CommsLogMFD oolite.oxp.phkb.CommsLogMFD
Title CommsLog MFD CommsLog MFD
Category HUDs HUDs
Author phkb phkb
Version 1.7.10 1.7.10
Tags mfd, comms mfd, comms
Required Oolite Version
Maximum Oolite Version
Required Expansions
Optional Expansions
Conflict Expansions
Information URL https://wiki.alioth.net/index.php/CommsLogMFD n/a
Download URL https://wiki.alioth.net/img_auth.php/e/e4/CommsLogMFD.oxz n/a
License CC-BY-NC-SA 4.0 CC-BY-NC-SA 4.0
File Size n/a
Upload date 1697012082

Documentation

readme.txt

Comms Log MFD
By Nick Rogers

Overview
========
Communication messages can sometimes happen very quickly. When this happens, the messages may scroll off the screen before they are viewed by the player. Or the player might be busy doing something else (like fighting off pirates) and miss seeing a request for help from his escort. This addon aims to solve these problems.

This oxp stores all communications received by the player and then displays them in an MFD which can be scrolled during flight with primable equipment. Press "B" to select the function and "N" to activate it. The available functions are:
	Scroll back mode:               Message list will be scrolled backwards.
	Scroll forward mode:            Message list will be scrolled forwards.
	Scroll to previous launch:      Message list will be scrolled back to your previous launch message.
	Scroll to next launch:          Message list will be scrolled forward to the next launch message.
	View current:                   Message list will be scrolled to the most current message.
	View oldest:                    Message list will be scrolled to the oldest message in the list.
	Clear log mode:                 This will clear all messages from the log.
	Write comms to lastest log:     This will write all messages to the "latest.log" file.
	Switch to message scrolling:    This will switch the list to scroll by messages.
	Switch to line scrolling:       This will switch the list to scroll by line.

When docked, a "Communications Log" item will appear in the Interfaces (F4) screen. When opened, all messages received by the player will be listed, in date order (oldest first, newest last). From this screen there are several options:
	Previous Page:                  Move the list back one page.
	Next Page:                      Move the list forward one page.
	First Page:                     Go to the first page of the list.
	Last Page:                      Go to the last page of the list.
	Write comms to latest log:      Write all messages to the latest log file.
	Clear the log:                  Clears all messages from the log.
	Set log size:                   Allows the user to define how big the log file will be. You will be asked to enter a number between 1 and 32.
	MFD currently in Active mode:   This means the MFD will be scrollable during flight using the primable equipment. Select this item to change the mode to Passive (see next item).
	MFD currently in Passive mode:  This means the MFD will not be scrollable during flight. There will be no primable equipment. Select this item to change the mode to Active (see previous item).

Only works with Oolite 1.79 or greater.

Installation
============
Install the OXP by copying CommsLogMFD.oxz to your AddOns folder, or downloading via the Expansion Manager in the game itself.

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/

Image from http://simpleicon.com/radar.html

Version history
===============
1.7.10
- Updated background overlay image for Oolite 1.91.

1.7.9
- Made the current mode value readable by other OXP's.

1.7.8
- Fixed some potential referencing errors on mission screens.
- Fixed integration issue with "Interface Reordering OXP".
- Fixed some UI glitches when the log is empty.

1.7.7
- Code refactoring.

1.7.6
- Using an escape pod was showing "(adjusting)" in the log.

1.7.5
- Launching when the clock shows "(adjusting)" would include the incorrect launch date/time and this text would be included in the log.
- Code refactoring.

1.7.4
- Better handling of null values being passed to the commsMessageReceived event.

1.7.3
- Fixed error when attempting to remove the comms log MFD.
- Changed "==" comparisons to "===" for performance improvements.
- Sound effects added for mode/activate functions of MFD.

1.7.2
- Fixed Javascript bug called by mis-named function call.
- Updated check for "Allow Big GUI".

1.7.1
- Internal improvements to save game format (using JSON), and other code improvements.
- If the font is changed between saves, the MFD will have its text reformatted to suit.
- Added escape pod usage notification to comms log history.
- Updated screenID's to enable BGS background sounds.
- Renamed background overlay image to prevent possibility of future duplication.
- Toned down overlay image.
- Trimmed unnecessary items out of equipment.plist.

1.7.0
- Added overlay background image to interface screen.
- Changed color of menu items on interface screen, so it's less yellow.
- Fixed issue where last page of log was not visible on the interface screen.

1.6.1
- Added routine to use 1.83/4 code to check for big GUI HUD's.
- Fixed issue where "(adjusting)" was appearing in messages when docking.

1.6.0
- MFD can now be removed.
- MFD can be switched between active (MFD can be scrolled with primable equipment) and passive (MFD can't be scrolled - no primable equipment) via the "Communication Log" interface screen

1.5.2
- Code improvements as suggested by Wildeblood.

1.5.1
- You can now exit the comms log interface screen at any time using a function key. For real this time.

1.5
- Added ability to set the log file size.
- You can now exit the comms log interface screen at any time using a function key.

1.4
- Comms messages are now viewable in the dockside F4 Communications log, even if the scroller hasn't been purchased.
- Fixed bug where clearing the log while docked would hide the HUD.
- Fixed JS bug when clearing the log in flight.
- F4 comms log options will now show all options, with various items disabled based on the log viewing position and content.
- Code refactoring and cleanup.

1.3
- Whole MFD now puchasable equipment, not just the scroller.
- New price of 250cr, which, along with the previous point, make it feel more "real" in game
- New modes
	* Dump messages to log, which sends all current messages to the lastest log file
	* switch scroll mode to be either by message (default) or by line
- Added "Communications log" to the system interfaces (f4) screen, so you can view your comms log while docked.
- Code refactoring

1.2.1
- no functional changes, just removed some debug code

1.2
- Mode selection now changes scroll function between scroll back, scroll forward, scroll to previous launch, scroll to next launch, view current, view oldest, and clear log.
- If log is scrolled and left for 90 seconds, log will automatically revert to viewing the current message
- updated license reference in manifest.plist to match with the license specified in "comms_mfd.js"
- split up the js files, putting the MFD code in one, and the scroller code in another.
- fixed case when first line of first message might not be viewable in some circumstances
- improved speed and memory management

1.1
- Scrolling by message, rather than by line (can be changed to line by line if you change a setting)
- Better handling of local variables
- Fixed the name of the MFD used to be consistent with the OXP name
- Log is kept between launches (can be changed to clear on launch if you change a setting)
- Log size limit added
- Added "Launch" and "Dock" messages to the comms log to help delineate between message sections
- Corrected the readme file license link to match with the license specified in "comms_mfd.js"
- Adjusted the width setting to leave more of a margin on the right side of the box

Equipment

Name Visible Cost [deci-credits] Tech-Level
CommsLog MFD yes 2500 3+
CommsLog MFD yes 2500 3+
Remove CommsLog MFD no 500 3+
Remove CommsLog MFD no 500 3+

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/commslog_conditions.js
"use strict";
this.name        = "CommsLogMFD_Conditions";
this.author      = "phkb";
this.copyright   = "2016 phkb";
this.description = "Condition script for the CommsLog MFD (Passive Mode)";
this.licence     = "CC BY-NC-SA 4.0";

//-------------------------------------------------------------------------------------------------------------
this.allowAwardEquipment = function(equipment, ship, context) {
	if (equipment === "EQ_COMMSLOGMFD_PASSIVE") {
		if (context != "scripted") return false;
	}
	if (equipment === "EQ_COMMSLOGMFD") {
		var p = player.ship;
		if (p.equipmentStatus("EQ_COMMSLOGMFD_PASSIVE") === "EQUIPMENT_OK" || p.equipmentStatus("EQ_COMMSLOGMFD_PASSIVE") === "EQUIPMENT_DAMAGED") return false;
	}
	return true
}
Scripts/commslog_mfd.js
"use strict";
this.name        = "CommsLogMFD";
this.author      = "phkb";
this.copyright   = "2016 phkb";
this.description = "Stores all communications received by the player and displays them in a multi-function display.";
this.licence     = "CC BY-NC-SA 4.0";

this._debug = false;		    // change to false for less logging
this._scrollTimeout = 90;		// number of seconds it will take for a scrolled view to revert to viewing current message
this._clearOnLaunch = 0;	   	// 0 = message log kept indefinitely, 1 = messages clear on launch
this._logSize = 32000;			// maximum length of log (in bytes) when clear on launch is off. Should not be set over 32k which is the maximum string length
this._lineLength = 14.2;        // horizontal measure of the max width of a line in the MFD (15 is the width of an MFD, minus some margin)
this._commsMFD_maxlines = 9;	// maximum number of lines visible in the MFD (10 - 1 for the header line)
this._syslines = [];
this._msRows = 18;				// rows to display on the mission screen
this._msCols = 32;				// columns to display on the mission screen
this._menuColor = "orangeColor";
this._exitColor = "yellowColor";

//-------------------------------------------------------------------------------------------------------------
this.startUp = function() {
	this._scrollMethod = 1;			// 0 = by line, 1 = by message;
	this._hudHidden = false;
	this._commsLogOpen = false;
	this._commsMFD_lines = [];
	this._commsMFD_log = "";
	if (missionVariables.CommsLogMFD_logsize) this._logSize = missionVariables.CommsLogMFD_logsize;

	if (this._clearOnLaunch === 0) {
		if (missionVariables.CommsLogMFD_log == null) {
			missionVariables.CommsLogMFD_log = "";
			missionVariables.CommsLogMFD_maxscroll = 0;
		}
		if (missionVariables.CommsLogMFD_scrollmethod == null) missionVariables.CommsLogMFD_scrollmethod = 1;

		if (missionVariables.CommsLogMFD_data) {
			var dta = JSON.parse(missionVariables.CommsLogMFD_data);
			this.$reformatData(dta, this._commsMFD_lines, this._lineLength);
			dta = [];
		}
		if (missionVariables.CommsLogMFD_log) {
			this.$convertLogToArray(missionVariables.CommsLogMFD_log);		// storage string of messages
			delete missionVariables.CommsLogMFD_log;
			var dta = [];
			this.$reformatData(this._commsMFD_lines, dta, this._lineLength);
			this._commsMFD_lines = dta;
			dta = [];
		}
		// make sure we're only holding one set of data
		delete missionVariables.CommsLogMFD_data;
		delete missionVariables.CommsLogMFD_log;

		this._commsMFD_maxscroll = missionVariables.CommsLogMFD_maxscroll;	// storage of maximum scroll amount
		this._scrollMethod = missionVariables.CommsLogMFD_scrollmethod;
	} else {
		this._commsMFD_maxscroll = 0;	// storage of maximum scroll amount
	}
	this._commsMFD_scroll = 0;		// storage scroll amount
	this._scrollLaunch = 0;			// used to scroll to paricular launch record, 1 = most recent, 2 = second most recent, etc
	this.$commsMFD_updateview();
}

//-------------------------------------------------------------------------------------------------------------
this.startUpComplete = function() {
	if (player.ship.docked) this.$initInterface(player.ship.dockedStation);
}

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

//-------------------------------------------------------------------------------------------------------------
this.shipDockedWithStation = function(station) {
	this.$initInterface(station);
}

//-------------------------------------------------------------------------------------------------------------
this.playerWillSaveGame = function() {
	// only save if we aren't clearing every launch
	if (this._clearOnLaunch === 0) {
		missionVariables.CommsLogMFD_data = JSON.stringify(this._commsMFD_lines);
		missionVariables.CommsLogMFD_maxscroll = this._commsMFD_maxscroll;
		missionVariables.CommsLogMFD_scrollmethod = this._scrollMethod;
		missionVariables.CommsLogMFD_logsize = this._logSize;
		this._commsMFD_log = "";
	}
}

//-------------------------------------------------------------------------------------------------------------
this.shipWillDockWithStation = function(station) {
	this.commsMessageReceived("Docked in " + system.name + " system at " + clock.clockStringForTime(clock.adjustedSeconds), station);
	// force the log to be resized to prevent it becoming too large between save games
	this.$convertArrayToString();
	this.$convertLogToArray(this._commsMFD_log);
	this._commsMFD_log = "";
}

//-------------------------------------------------------------------------------------------------------------
this.shipWillLaunchFromStation = function() {
	if (missionVariables.CommsLogMFD_data) delete missionVariables.CommsLogMFD_data;
	// reset comms log settings
	if (this._clearOnLaunch === 1) {
		this._commsMFD_log = "";
		this._commsMFD_lines = [];
		this._commsMFD_maxscroll = 0;
	}
	this._commsMFD_scroll = 0;
}

//-------------------------------------------------------------------------------------------------------------
this.shipLaunchedFromStation = function(station) {
	// we're doing this here so that the "(adjusting)" text doesn't appear in the time stamp
	this._syslines = [];
	this.commsMessageReceived("Launched in " + system.name + " system at " + clock.clockStringForTime(clock.adjustedSeconds), station);
	this.$commsMFD_updateview();
}

//-------------------------------------------------------------------------------------------------------------
this.shipLaunchedEscapePod = function(escapepod, passengers) {
     this.$addCommsMessage("Escape pod activated in " + system.name + " system at " + clock.clockString.replace("(adjusting)", ""), "GalCop Emergency Beacon");
}

//-------------------------------------------------------------------------------------------------------------
this.guiScreenChanged = function(to, from) {
	if (from === "GUI_SCREEN_MISSION" && this._commsLogOpen) {
		this._commsLogOpen = false;
		player.ship.hudHidden = this.$hudHidden;
	}
}

//-------------------------------------------------------------------------------------------------------------
this.playerBoughtEquipment = function(equipment) {
	if (equipment === "EQ_COMMSLOGMFD_REMOVAL") {
		var p = player.ship;
		p.removeEquipment("EQ_COMMSLOGMFD");
		p.removeEquipment("EQ_COMMSLOGMFD_REMOVAL");
	}
	if (equipment === "EQ_COMMSLOGMFD_PASSIVE_REMOVAL") {
		var p = player.ship;
		p.removeEquipment("EQ_COMMSLOGMFD_PASSIVE");
		p.removeEquipment("EQ_COMMSLOGMFD_PASSIVE_REMOVAL");
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$initInterface = function(station) {
	station.setInterface(this.name,{
		title:"Communications log",
		category:expandDescription("[interfaces-category-logs]"),
		summary:"Views all entries in the communications log",
		callback:this.$showCommLog.bind(this)
	});
}

//=============================================================================================================
// General functions

//-------------------------------------------------------------------------------------------------------------
// converts a single string to an array
this.$convertLogToArray = function(commLog) {
	this._commsMFD_lines = commLog.split("\n");
}

//-------------------------------------------------------------------------------------------------------------
// converts the comm log array to a single string
this.$convertArrayToString = function() {
	var log = "";
	var nl = "";
	for (var i = this._commsMFD_lines.length - 1; i >= 0; i--) {
		// check for log file size limit
		if ((log + this._commsMFD_lines[i]).length > this._logSize) {
			// remove the top message
			// look for "\n[A-Z]" (which should indicate the start of the second message in the log
			var re = new RegExp("\\n[A-Z]", "i");
			var iSec = log.search(re);
			// cut out the first message
			log = log.substring(iSec + 1);
			break;
		}
		log = this._commsMFD_lines[i] + nl + log;
		nl = "\n";
	}
	this._commsMFD_log = log;
}

//-------------------------------------------------------------------------------------------------------------
// custom handler for special comms messages (that don't get raised through 'commsMessageReceived')
this.$addCommsMessage = function(message, altSender) {
	var msg = "";
	if (altSender && altSender != "") {
		msg = altSender + ": " + message;
	} else {
		msg = "{unidentified source}: " + message;
	}
	this.$appendMessage(msg);
}

//-------------------------------------------------------------------------------------------------------------
// worldscript handler for comms messages
this.commsMessageReceived = function(message, sender) {
	var msg = "";
	if (sender) {
		msg = sender.displayName + ": " + message;
	} else {
		msg = "{unidentified source}: " + message;
	}
	this.$appendMessage(msg);
}

//-------------------------------------------------------------------------------------------------------------
// adds message to the comm log
this.$appendMessage = function(message) {
	if (this._debug === true) log(this.name, "comms::" + message);

	// add the message to the list
	this.$addMsgToArray(message, this._commsMFD_lines, this._lineLength);

	var p = player.ship;
	// only update MFD if the equipment is installed
	if(p.equipmentStatus("EQ_COMMSLOGMFD") === "EQUIPMENT_OK" || p.equipmentStatus("EQ_COMMSLOGMFD_PASSIVE") === "EQUIPMENT_OK") {
		this.$commsMFD_updateview();
	}	
}

//-------------------------------------------------------------------------------------------------------------
this.$reformatData = function(srcArray, destArray, lineWidth) {
	//rebuild comms into wide screen format
	var msg = "";
	var output = "";
	for (var i = 0; i < srcArray.length; i++) {
		if (srcArray[i].substring(0,1) != " " && msg != "") {
			// this is a new message, so output the current one and reset
			//output = output + "> " + msg + "\n";
			this.$addMsgToArray(msg, destArray, lineWidth);
			msg = "";
		}
		// add the message line to the current message
		// we'll trim off any leading and trailing spaces and add them back manually
		msg += srcArray[i].trim() + " ";
	}
	if (msg != "") {
		// output the remaining message
		//output = output + "> " + msg;
		this.$addMsgToArray(msg, destArray, lineWidth);
	}
}

//-------------------------------------------------------------------------------------------------------------
// displays a part of the comms log in the MFD
this.$commsMFD_updateview = function() {

	var p = player.ship;
	if (p.equipmentStatus("EQ_COMMSLOGMFD") === "EQUIPMENT_OK" || p.equipmentStatus("EQ_COMMSLOGMFD_PASSIVE") === "EQUIPMENT_OK") {

		var output = "";
		var iStart = 0;
		var iEnd = 0;
		var iCount = 0;
		var iLast = 0;

		// if we have lines, work out what to display, based on the current scroll position
		if ((this._commsMFD_lines != null) && (this._commsMFD_lines.length != 1 || this._commsMFD_lines[0] != "")) {

			// determine the maximum amount of scroll
			this._commsMFD_maxscroll = this._commsMFD_lines.length - this._commsMFD_maxlines;

			// if the max scroll is less than 0, reset to 0
			if (this._commsMFD_maxscroll < 0) this._commsMFD_maxscroll = 0;


			// work out the start and end points of the log to view
			// scroll line by line
			iEnd = this._commsMFD_lines.length;
			if (this._scrollMethod === 0) iEnd -= this._commsMFD_scroll;

			// scroll message by message
			if (this._scrollMethod === 1 && this._commsMFD_scroll > 0 && this._scrollLaunch === 0) {
				this._holdscrollLaunch = 0;
				// cycle back through the lines, from the bottom up
				for (var i = iEnd - 1; i >= 0; i--) {
					// look for a line that doesn't start with a space character
					if (this._commsMFD_lines[i].substring(0,1) != " ") {
						// when found, increment our message counter
						iCount += 1;
						// and make a note of the position of this message start
						iLast = i;
						// if our counter equals our scroll value, we're done
						if (iCount === this._commsMFD_scroll) {
							// set the end point to be the point we are in the loop
							iEnd = i;
							// jump out of the loop
							break;
						}
					}
					// this check is to, essentially, stop the last page of messages from being scrolled.
					// check if we're inside the last 9 lines of the log
					//   AND the log length is greater than 9 lines
					//   AND the position of the last message we found is greater than 0 (that is, we found one)
					if (i < 9 && this._commsMFD_lines.length >= 9 && iLast > 0) {
						// don't scroll the last page of messages
						iEnd = 9;
						this._commsMFD_scroll = iCount;
						break;
					}
				}
			}
			// scroll launch by launch
			if (this._scrollLaunch != 0) {
				var newScroll = 0;
				var check = 0;
				// when the scroll method is by line and we're scrolling to the previous launch,
				// we need to pad out the current scroll amount otherwise we'll end up going to the same spot over and over
				var addln = 3;
				// however, going in the other direction we wont' need this
				if (this._scrollLaunch === -1) addln = 0;

				for (var i = this._commsMFD_lines.length - 1; i >= 0; i--) {
					// look for a line that doesn't start with a space character
					if (this._commsMFD_lines[i].substring(0,1) != " ") {
						// is this message a launch message?
						if ((this._commsMFD_lines[i].indexOf(": Launched in") > 0) ||
							(this._commsMFD_lines[i].indexOf(": Launched") > 0 && this._commsMFD_lines[i + 1].indexOf(" in") > 0) ||
							(this._commsMFD_lines[i].indexOf(":") > 0 && this._commsMFD_lines[i + 1].indexOf(" Launched in") > 0)) {

							// only used for line-by-line scrolling
							check = this._commsMFD_lines.length - i;
							// if we scrolling to the next launch, and we've passed the current scroll point, then we can break out of the loop
							// because we should now have the right values
							if (this._scrollLaunch === -1 && ((this._scrollMethod === 1 && iCount > this._commsMFD_scroll) || (this._scrollMethod === 0 && check > this._commsMFD_scroll + addln))) {
								break;
							}

							// look for a launch, that is, before or after the current scroll position (depanding on the scroll direction)
							if ((this._scrollLaunch === 1 && ((this._scrollMethod === 1 && iCount > this._commsMFD_scroll) || (this._scrollMethod === 0 && check > this._commsMFD_scroll + addln))) ||
								(this._scrollLaunch === -1 && ((this._scrollMethod === 1 && iCount < this._commsMFD_scroll) || (this._scrollMethod === 0 && check < this._commsMFD_scroll + addln)))) {
								// we've found it
								// we need to move forward 1 message so the full launch message is in the view
								iEnd = this._commsMFD_lines.length;
								for (var j = i + 1; j < this._commsMFD_lines.length - 1; j++) {
									if (this._commsMFD_lines[j].substring(0,1) != " ") {
										// this is it
										iEnd = j;
										break;
									}
								}
								// set the new scroll value
								if (this._scrollMethod === 1) {
									newScroll = iCount;
								} else {
									newScroll = this._commsMFD_lines.length - iEnd;
								}
								// if we're scrolling to the previous launch we can break out now, because we have all the info we need
								if (this._scrollLaunch === 1) break;
							}
						}
						// when found, increment our message counter
						iCount += 1;
						// and make a note of the position of this message start
						iLast = i;
					}
					// this check is to, essentially, stop the last page of messages from being scrolled.
					// check if we're inside the last 9 lines of the log
					//   AND the log length is greater than 9 lines
					//   AND the position of the last message we found is greater than 0 (that is, we found one)
					if (this._scrollLaunch === 1 && i < 9 && this._commsMFD_lines.length >= 9 && iLast > 0) {
						// don't scroll the last page of messages
						iEnd = 9;
						newScroll = iCount - 1;
						// edge case, where the last message found is outside the 9 line limit, which would leave the first line of the message invisible
						if (iLast >= 9) {
							iEnd = 9;
							if (this._scrollMethod === 1) {
								newScroll = iCount;
							} else {
								newScroll = this._commsMFD_lines.length - 9;
							}
						}
						break;
					}
				}
				this._commsMFD_scroll = newScroll;
			}

			// the start position is always 9 lines less than the end point
			iStart = iEnd - this._commsMFD_maxlines;
			// this condition should never happen, but just in case...
			// make sure our end point is inside the bounds of the array of lines
			if (iEnd < 0) {
				iEnd = this._commsMFD_lines.length;
				this._commsMFD_scroll = 0;
			}
			// make sure our start point is inside the bounds of the array
			if (iStart < 0) {
				iStart = 0;
			}

			// build the header
			output = "COMMS LOG: ";
			if (this._commsMFD_scroll != 0) {
				output = output + "Scroll " + this._commsMFD_scroll.toString() + "\n";
			} else {
				output = output + "\n";
			}

			// add the lines
			if (iEnd > 0) {
				for (var i = iStart; i < iEnd; i++) {
					output = output + this._commsMFD_lines[i] + "\n";
				}
			} else {
				output = output + "(empty)";
			}
		} else {
			output = "COMMS LOG:\n(empty)";
		}

		// send output to MFD
		p.setMultiFunctionText("CommsLogMFD", output, false);
	}
}

//-------------------------------------------------------------------------------------------------------------
// output the comms to the latest log
this.$dumpLog = function() {
	var msg = "";
	for (var i = 0; i < this._commsMFD_lines.length; i++) {
		if (this._commsMFD_lines[i].substring(0,1) != " " && msg != "") {
			// this is a new message, so output the current one and reset
			log(this.name, msg);
			msg = "";
		}
		// add the message line to the current message
		// we'll trim off any leading and trailing spaces and add them back manually
		// we'll also replace any instances of "…" with "..."
		msg += this._commsMFD_lines[i].trim().replace("…", "...").replace("–", "-") + " ";
	}
	if (msg != "") {
		// output the remaining message
		log(this.name, msg);
	}
}

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

	this._syslines = [];
	this.$reformatData(this._commsMFD_lines, this._syslines, this._msCols);

	var p = player.ship;
	this._msRows = 18;
	// one less row if the comms log MFD is installed
	if (p.equipmentStatus("EQ_COMMSLOGMFD") === "EQUIPMENT_OK" || p.equipmentStatus("EQ_COMMSLOGMFD_PASSIVE") === "EQUIPMENT_OK") {
		this._msRows = 17;
	}

	this._maxpage = Math.ceil(this._syslines.length / this._msRows);
	this._curpage = 0;
	this.$showPage();
}

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

	var prt = "";

	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);
				// subsequent lines of a message will be indented slightly
				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);
		words = [];
	} else {
		ary.push(msg);
	}
}

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

	var iStart = 0;
	var iEnd = 0;
	var textCount = 0;

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

	for (var i = iStart; i < iEnd; i++) {
		textCount += this._syslines[i].trim().length;
		output += this._syslines[i] + "\n";
	}

	if (cpage === 0 && iStart === 0 && iEnd === 0 && (this._syslines[0] == null || textCount == 0)) output = "(empty)";

	return output;
}

//-------------------------------------------------------------------------------------------------------------
this.$showPage = function $showPage() {
	var p = player.ship;

	this._hudHidden = p.hudHidden;
	if (this.$isBigGuiActive() === false) p.hudHidden = true;

	this._commsLogOpen = true;
	this._msRows = 18;
	// one less row if the comms log MFD is installed
	if (p.equipmentStatus("EQ_COMMSLOGMFD") === "EQUIPMENT_OK" || p.equipmentStatus("EQ_COMMSLOGMFD_PASSIVE") === "EQUIPMENT_OK") this._msRows = 17;

	var text = this.$commLogPage(this._curpage,this._msRows);
	var opts;
	var curChoices = {};
	var def = "09_NEXT";

	if (this._maxpage <= 1) {
		def = "99_EXIT";
		curChoices["01_PREV"] = {text:"Previous page", color:"darkGrayColor", unselectable:true};
		curChoices["09_NEXT"] = {text:"Next page", color:"darkGrayColor", unselectable:true};
		curChoices["10_FIRST"] = {text:"First page", color:"darkGrayColor", unselectable:true};
		curChoices["11_LAST"] = {text:"Last page",color:"darkGrayColor", unselectable:true};
	} else {
		if (this._curpage === 0) {
			curChoices["01_PREV"] = {text:"Previous page", color:"darkGrayColor", unselectable:true};
			curChoices["09_NEXT"] = {text:"Next page", color:this._menuColor};
			curChoices["10_FIRST"] = {text:"First page", color:"darkGrayColor", unselectable:true};
			curChoices["11_LAST"] = {text:"Last page",color:"orangeColor"};
		} else {
			curChoices["01_PREV"] = {text:"Previous page", color:this._menuColor};
			if (this._curpage + 1 === this._maxpage) {
				def = "01_PREV";
				curChoices["09_NEXT"] = {text:"Next page", color:"darkGrayColor", unselectable:true};
				curChoices["10_FIRST"] = {text:"First page", color:this._menuColor};
				curChoices["11_LAST"] = {text:"Last page",color:"darkGrayColor", unselectable:true};
			} else {
				curChoices["09_NEXT"] = {text:"Next page", color:this._menuColor};
				curChoices["10_FIRST"] = {text:"First page", color:this._menuColor};
				curChoices["11_LAST"] = {text:"Last page",color:this._menuColor};
			}
		}
	}

	if (text != "(empty)") {
		curChoices["15_DUMPLOG"] = {text:"Write comms to latest log",color:this._menuColor};
		curChoices["20_CLEARLOG"] = {text:"Clear the log", color:this._menuColor};
	} else {
		curChoices["15_DUMPLOG"] = {text:"Write comms to latest log",color:"darkGrayColor", unselectable:true};
		curChoices["20_CLEARLOG"] = {text:"Clear the log", color:"darkGrayColor", unselectable:true};
	}
	curChoices["30_SETLOGSIZE"] = {text:"Set log size", color:this._menuColor};
	if (p.equipmentStatus("EQ_COMMSLOGMFD") === "EQUIPMENT_OK") {
		curChoices["80_TYPE1"] = {text:"MFD currently in Active Mode", color:this._menuColor};
	}
	if (p.equipmentStatus("EQ_COMMSLOGMFD_PASSIVE") === "EQUIPMENT_OK") {
		curChoices["80_TYPE2"] = {text:"MFD currently in Passive Mode", color:this._menuColor};
	}
	curChoices["99_EXIT"] = {text:"Exit communications log", color:this._exitColor};

	var opts = {
		screenID: "oolite-communicationslog-main-map",
		title: "Communications log" + (this._maxpage == 0 ? "" : " - page " + (this._curpage + 1).toString() + " of " + this._maxpage.toString()),
		allowInterrupt: true,
		choices: curChoices,
		exitScreen: "GUI_SCREEN_INTERFACES",
		overlay: {name:"commlog-radar.png", height:546},
		initialChoicesKey: this._lastchoice?this._lastchoice:def,
		message: text
	};
	mission.runScreen(opts,this.$logHandler.bind(this));
}

//-------------------------------------------------------------------------------------------------------------
this.$logHandler = function $logHandler(choice) {
	delete this._lastchoice;

	var newChoice = null;

	if (!choice) {
		if (this._debug) log(this.name, "no choice");
		return; // launched while reading?
	} else if (choice === "01_PREV") {
		if (this._curpage > 0) this._curpage -= 1;
	} else if (choice === "09_NEXT") {
		if (this._curpage < this._maxpage - 1) this._curpage += 1;
	} else if (choice === "10_FIRST") {
		this._curpage = 0;
	} else if (choice === "11_LAST") {
		this._curpage = this._maxpage - 1;
	} else if (choice === "15_DUMPLOG") {
		this.$dumpLog();
	} else if (choice === "20_CLEARLOG") {
		this._scrollLaunch = 0;
		this._commsMFD_log = "";
		this._commsMFD_lines = [];
		this._syslines = [];
		this._maxpage = 1;
		this._curpage = 0;
	} else if (choice === "30_SETLOGSIZE") {
		this.$setLogSize();
	} else if (choice === "80_TYPE1") {
		this.$changeMFDType("TYPE2");
		newChoice = "80_TYPE2";
	} else if (choice === "80_TYPE2") {
		this.$changeMFDType("TYPE1");
		newChoice = "80_TYPE1";
	}
	this._lastchoice = choice;
	if (newChoice) this._lastchoice = newChoice;

	if (choice != "99_EXIT" && choice != "30_SETLOGSIZE") {
		this.$showPage();
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$changeMFDType = function(mfdType) {
	var p = player.ship;
	switch (mfdType) {
		case "TYPE2":
			if (p.equipmentStatus("EQ_COMMSLOGMFD") != "EQUIPMENT_OK") return;
			if (p.equipmentStatus("EQ_COMMSLOGMFD_PASSIVE") === "EQUIPMENT_OK") return;
			p.removeEquipment("EQ_COMMSLOGMFD");
			p.awardEquipment("EQ_COMMSLOGMFD_PASSIVE");
			break;
		case "TYPE1":
			if (p.equipmentStatus("EQ_COMMSLOGMFD_PASSIVE") != "EQUIPMENT_OK") return;
			if (p.equipmentStatus("EQ_COMMSLOGMFD") === "EQUIPMENT_OK") return;
			p.removeEquipment("EQ_COMMSLOGMFD_PASSIVE");
			p.awardEquipment("EQ_COMMSLOGMFD");
			break;
	}
}

//-------------------------------------------------------------------------------------------------------------
// prompts the user for a new log file size
this.$setLogSize = function() {

	var text = "Enter the desired size of the log file, in kilobytes. This must be a whole number (no decimals) between 1 and 32.\n\n" +
		"The current setting is " + parseInt(this._logSize / 1000) + "k.\n\n" +
		"Press enter to leave this value unchanged.\n\n" +
		"Changes to the log size will be reflected the next time the log file is opened, or when your ship next launches.";

	var opts = {
		screenID: "oolite-communicationslog-logsize-map",
		title: "Set Log File Size",
		allowInterrupt: false,
		exitScreen: "GUI_SCREEN_INTERFACES",
		overlay: {name:"commlog-radar.png", height:546},
		message: text,
		textEntry: true
	};
	mission.runScreen(opts,this.$getNewLogSize, this);
}

//-------------------------------------------------------------------------------------------------------------
// parses the input from the setLogSize screen
this.$getNewLogSize = function(param) {
	if (parseInt(param) >= 1 && parseInt(param) <= 32) {
		this._logSize = parseInt(param) * 1000;
		this.$convertArrayToString()
		this.$convertLogToArray(this._commsMFD_log);
	}
	this.$showPage();
}

//-------------------------------------------------------------------------------------------------------------
// 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/commslog_scroller.js
"use strict";
this.name        = "CommsLogMFD_Equipment";
this.author      = "phkb";
this.copyright   = "2016 phkb";
this.description = "Allows the communications log multi-function display to be scrolled and cleared.";
this.licence     = "CC BY-NC-SA 4.0";

//-------------------------------------------------------------------------------------------------------------
this.activated = function () {
	var w = worldScripts.CommsLogMFD;
	if (w._currMode == null) {
		w._currMode = 0;
	}
	w._nextMode = -1;
	var bResetTimer = false;
	var p = player;
	switch(w._currMode) {
		case 0:
			// scroll further into history (increase the amount of scroll)
			w._scrollLaunch = 0;
			if (w._commsMFD_scroll < w._commsMFD_maxscroll) {
				w._commsMFD_scroll += 1;
				w.$commsMFD_updateview();
				bResetTimer = true;
			}
			break;
		case 1:
			// scroll back towards current (reduce the amount of scroll)
			w._scrollLaunch = 0;
			if (w._commsMFD_scroll > 0) {
				w._commsMFD_scroll -= 1;
				// set the next mode to be mode 0, so we can quickly go back and forwards between messages
				this._nextMode = 0;
				w.$commsMFD_updateview();
				if (w._commsMFD_scroll != 0) {
					bResetTimer = true;
				}
			}
			break;
		case 2:
			// scroll back to previous launch
			w._scrollLaunch = 1;
			w.$commsMFD_updateview();
			bResetTimer = true;
			break;
		case 3:
			// scroll to next launch
			w._scrollLaunch = -1;
			// set the next mode to be mode 2, so we can quickly go back and forwards between launches
			w._nextMode = 2;
			w.$commsMFD_updateview();
			bResetTimer = true;
			break;
		case 4:
			// view current
			w._commsMFD_scroll = 0;
			w._scrollLaunch = 0;
			w.$commsMFD_updateview();
			w._currMode = 0;
			this.$stoptimer();
			p.consoleMessage("Scroll back mode autoselected");
			break;
		case 5:
			// view oldest
			w._scrollLaunch = 0;
			w._commsMFD_scroll = w._commsMFD_maxscroll;
			w.$commsMFD_updateview();
			w._currMode = 1;
			p.consoleMessage("Scroll forward mode autoselected");
			bResetTimer = true;
			break;
		case 6:
		    // clear log
			w._scrollLaunch = 0;
			w._commsMFD_log = "";
			w._commsMFD_lines = [];
			w._commsMFD_scroll = 0;
			w.$commsMFD_updateview();
			w._currMode = 0;
			this.$stoptimer();
			p.consoleMessage("Log cleared");
			break;
		case 7:
			// write comms to log
			w.$dumpLog();
			p.consoleMessage("Output of comms to log complete");
			w._currMode = 0;
			this.$stoptimer();
			p.consoleMessage("Scroll back mode autoselected");
			break;
		case 8:
			if (w._scrollMethod === 0) {
				p.consoleMessage("Scroll mode = By message");
				w._scrollMethod = 1;
			} else {
				p.consoleMessage("Scroll mode = By line");
				w._scrollMethod = 0;
			}
			w._currMode = 0;
			w._scrollLaunch = 0;
			w._commsMFD_scroll = 0;
			w.$commsMFD_updateview();
			this.$stoptimer();
			break;
	}
	if (bResetTimer === true) this.$resettimer();
	this.$playSound("activate");
}

//-------------------------------------------------------------------------------------------------------------
this.$stoptimer = function() {
	if (this._timeout && this._timeout.isRunning) this._timeout.stop();
}

//-------------------------------------------------------------------------------------------------------------
this.mode = function () {
	var w = worldScripts.CommsLogMFD;
	// change the mode
	// if the nextMode value has been set, make this the next mode
	if (w._nextMode != null && w._nextMode != -1) {
		w._currMode = w._nextMode;
		w._nextMode = -1;
	} else {
		// if the value hasn't been set yet, start at default mode (0)
		if (w._currMode == null) {
			w._currMode = 0;
		} else {
			// otherwise, increment the mode
			w._currMode += 1;
			// once we get to 9, switch back to 0
			if (w._currMode === 9) {
				w._currMode = 0;
			}
		}
	}

	var p = player;

	// display a console message about which mode we're in now
	switch(w._currMode) {
		case 0:
			p.consoleMessage("Scroll back mode");
			break;
		case 1:
			p.consoleMessage("Scroll forward mode");
			break;
		case 2:
			p.consoleMessage("Scroll to previous launch");
			break;
		case 3:
			p.consoleMessage("Scroll to next launch");
			break;
		case 4:
			p.consoleMessage("View current");
			break;
		case 5:
			p.consoleMessage("View oldest");
			break;
		case 6:
			p.consoleMessage("Clear log mode");
			break;
		case 7:
			p.consoleMessage("Write comms to lastest log");
			break;
		case 8:
			if (worldScripts.CommsLogMFD._scrollMethod === 0) {
				p.consoleMessage("Switch to message scrolling");
			} else {
				p.consoleMessage("Switch to line scrolling");
			}
	}
	this.$playSound("mode");
}

//-------------------------------------------------------------------------------------------------------------
this.$resettimer = function() {
	this.$stoptimer();
	this._timeout = new Timer(this, this.$timerdone, worldScripts.CommsLogMFD._scrollTimeout, 0);
}

//-------------------------------------------------------------------------------------------------------------
this.$timerdone = function $timerdone() {
	var w = worldScripts.CommsLogMFD;
	w._currMode = 0;
	w._commsMFD_scroll = 0;
	w._scrollLaunch = 0;
	w.$commsMFD_updateview();
}

//-------------------------------------------------------------------------------------------------------------
// play sound effects
this.$playSound = function(soundtype) {
	var mySound = new SoundSource;

	switch (soundtype) {
		case "mode":
			mySound.sound = "[@click]";
			break;
		case "activate":
			mySound.sound = "[@beep]";
			break;
			
	}
	mySound.loop = false;
	mySound.play();
}