Back to Index Page generated: Jun 13, 2026, 7:54:52 PM

Expansion Ship's Library

Content

Manifest

from Expansion Manager's OXP list from Expansion Manifest
Description An e-reader for your ship which can read various books while docked - or with a purchasable upgrade, also while in flight. It comes with a ship's manual as an initial book, with other books available as separate expansion packs. An e-reader for your ship which can read various books while docked - or with a purchasable upgrade, also while in flight. It comes with a ship's manual as an initial book, with other books available as separate expansion packs.
Identifier oolite.oxp.cim.ships-library oolite.oxp.cim.ships-library
Title Ship's Library Ship's Library
Category Equipment Equipment
Author cim cim
Version 0.14 0.14
Tags
Required Oolite Version
Maximum Oolite Version
Required Expansions
Optional Expansions
Conflict Expansions
Dependent Expansions
  • oolite.oxp.Cholmondeley.Addons_for_Beginners_(Vital_Statistics):1.2
  • oolite.oxp.Norby.Ambience_Collection:1.3
  • oolite.oxp.Ramen.Mutabilis-Ships_Library:1.1.1
  • oolite.oxp.Ramen.Status_Quo-Ship's_Library:1.0.0
  • oolite.oxp.Ramirez.IronRaven:2.0.2
  • oolite.oxp.captsolo.solos-good-fortune:1.05
  • oolite.oxp.cim.extracts-tre-clan:1.2
  • oolite.oxp.phkb.RoughGuideToTheOoniverse:1.0
  • oolite.oxp.phkb.ShipConfiguration:1.5.6
  • Information URL https://wiki.alioth.net/index.php/Ship%27s_Library_OXP n/a
    Download URL https://wiki.alioth.net/img_auth.php/0/05/Ships_Library_0.14.oxz n/a
    License GPL v2+ / CC-BY-NC-SA 3.0 GPL v2+ / CC-BY-NC-SA 3.0
    File Size n/a
    Upload date 1770412318

    Relationships Diagram

    Documentation

    Also read http://wiki.alioth.net/index.php/Ship's%20Library

    Equipment

    Name Visible Cost [deci-credits] Tech-Level
    Ship's Library Flight Interface yes 0 5+

    Ships

    This expansion declares no ships.

    Models

    This expansion declares no models.

    Scripts

    Path
    Scripts/ingame-manual.js
    "use strict";
    this.author = "cim";
    this.copyright = "� 2011-2014 cim.";
    this.licence = "CC-BY-SA 3.0";
    this.version = "0.14";
    this.name = "Ships Library";
    this.description = "An e-book library readable on the F4 screen";
    
    // https://www.freepik.com/icon/open-book_5638999 Icon by Freepik
    this._bookListOverlay = { name: "ingame-manual-book.png", height: 546 };
    // https://www.freepik.com/icon/guidebook_11940154 Icon by Anggara
    this._chapterListOverlay = { name: "ingame-manual-list.png", height: 546 };
    
    /* Public */
    this._registerBook = function (key, title, contents, pageHeight) {
    	if (!pageHeight) pageHeight = 15;
    	this.$books[key] = { title: title, contents: contents, pageHeight: pageHeight };
    }
    
    this._unregisterBook = function (key) {
    	if (key == "ingame-manual") {
    		return false; // can't remove this one
    	}
    	if (this.$currentBook == key) {
    		if (mission.screenID == "ships-library") {
    			// book is currently loaded and mission screen active
    			return false;
    		}
    		this._setBook("ingame-manual");
    	}
    	delete this.$books[key];
    	return true;
    }
    
    this.startUp = function () {
    	delete this.startUp;
    
    	this.$fcbM = false;
    	this.$hudHidden = false;
    	this.$manualActive = false;
    	this.$currentBook = false;
    	this.$spaceWidth = defaultFont.measureString(" ");
    
    	// settings for mission screen reading
    	this.$chapter = 0;
    	this.$offset = 0;
    	this.$msStore = "offsets";
    	this.$msRows = 15;
    	this.$msCols = 32;
    
    	// settings for MFD in-flight reading
    	this.$mfdChapter = 0;
    	this.$mfdOffset = 0;
    	this.$mfdStore = "miniOffsets";
    	this.$mfdRows = 10;
    	this.$mfdCols = 14.75;
    
    	this.$mfdMode = 0;
    	this.$mfdOn = false;
    
    	// initialise
    	this.$minblock = 3;
    	this.$chaptersperpage = 20;
    
    	this.$books = new Object;
    	this._registerIngameManual();
    	this.$mode = "firstrun";
    
    	if (missionVariables.ships_library_bookmark) {
    		this.$bookmark = JSON.parse(missionVariables.ships_library_bookmark);
    	} else {
    		this.$bookmark = { "ingame-manual": [0, 0, 0, 0] };
    	}
    
    	this._setBook("ingame-manual");
    
    }
    
    this.startUpComplete = function () {
    	this._initInterface(system.mainStation);
    	this._initInterface(player.ship.dockedStation);
    	if (oolite.compareVersion("1.91") <= 0) {
    		setExtraGuiScreenKeys(this.name, {
    			guiScreen: "GUI_SCREEN_INTERFACES",
    			registerKeys: { "key1": [{ key: "l", mod1: true }] },
    			callback: this._showLibrary.bind(this)
    		});
    		
    		if (worldScripts.ContextualHelp) {
    			var ch = worldScripts.ContextualHelp;
    			ch.$addHelpTextToGuiScreen(this.name, "GUI_SCREEN_INTERFACES", expandDescription("[ships-library-shortcut]"));
    		}
    	}	
    }
    
    
    /* Private and handlers */
    
    this.playerWillSaveGame = function () {
    	missionVariables.ships_library_bookmark = JSON.stringify(this.$bookmark);
    }
    
    this.shipDockedWithStation = function (station) {
    	this._initInterface(station);
    }
    
    this._initInterface = function (station) {
    	station.setInterface(this.name, {
    		title: expandMissionText("ships-library-title"),
    		category: expandDescription("[interfaces-category-ship-systems]"),
    		summary: expandMissionText("ships-library-summary"),
    		callback: this._showLibrary.bind(this)
    	});
    }
    
    this._registerIngameManual = function () {
    	if (oolite.compareVersion("1.91") <= 0) {
    		this._anaKey = expandMissionText("ingame-manual-ana-key-191");
    		this._primeKey = expandMissionText("ingame-manual-prime-key-191");
    		this._cycleMFDKey = expandMissionText("ingame-manual-cyclemfd-key-191");
    		this._switchMFDKey = expandDescription("ingame-manual-switchmfd-key-191");
    	} else {
    		this._anaKey = expandMissionText("ingame-manual-ana-key-190");
    		this._primeKey = expandMissionText("ingame-manual-prime-key-190");
    		this._cycleMFDKey = expandMissionText("ingame-manual-cyclemfd-key-190");
    		this._switchMFDKey = expandMissionText("ingame-manual-switchmfd-key-190");
    	}
    
    	var contents = [
    		{ level: 0, key: "ingame-manual-frontpage", params: [this._getTip], opts: { model: "[" + player.ship.dataKey + "]", spinModel: false } },
    		{ level: 0, key: "ingame-manual-welcome" },
    		{ level: 0, key: "ingame-manual-controls" },
    		{ level: 1, key: "ingame-manual-ship-systems" },
    		{ level: 2, key: "ingame-manual-ship-systems-status", backgrounds: ["ingame-manual-f5.png", ""] },
    		{ level: 2, key: "ingame-manual-ship-systems-manifest", backgrounds: ["ingame-manual-f5f5.png", ""] },
    		{ level: 2, key: "ingame-manual-ship-systems-src", backgrounds: ["ingame-manual-f6.png", ""] },
    		{ level: 2, key: "ingame-manual-ship-systems-lrc", backgrounds: ["ingame-manual-f6f6.png", ""], params: [function () { return worldScripts["Ships Library"]._anaKey; }] },
    		{ level: 2, key: "ingame-manual-ship-systems-planet", backgrounds: ["ingame-manual-f7.png", ""] },
    		{ level: 2, key: "ingame-manual-ship-systems-commodity", params: [function () { return player.ship.cargoSpaceCapacity; }], backgrounds: ["ingame-manual-f8.png", ""] },
    		{ level: 1, key: "ingame-manual-station-systems" },
    		{ level: 2, key: "ingame-manual-station-systems-launch", backgrounds: ["ingame-manual-f1.png"] },
    		{ level: 2, key: "ingame-manual-station-systems-equipment", backgrounds: ["ingame-manual-f3.png", ""] },
    		{ level: 2, key: "ingame-manual-station-systems-shipyard", backgrounds: ["ingame-manual-f3f3.png", ""] },
    		{ level: 2, key: "ingame-manual-station-systems-interfaces", backgrounds: ["ingame-manual-f4.png", ""] },
    		{ level: 1, key: "ingame-manual-inflight" },
    		{ level: 2, key: "ingame-manual-inflight-view" },
    		{ level: 2, key: "ingame-manual-inflight-hud", params: [function () { return Math.max(1, Math.floor(player.ship.maxEnergy / 64)); }], backgrounds: ["ingame-manual-hud.png"] },
    		{ level: 3, key: "ingame-manual-inflight-hud-scanner", backgrounds: ["ingame-manual-hud.png"] },
    		{ level: 2, key: "ingame-manual-inflight-flight" },
    		{ level: 2, key: "ingame-manual-inflight-docking", backgrounds: ["ingame-manual-dock0.png", "ingame-manual-dock1.png", "ingame-manual-dock2.png", "ingame-manual-dock3.png", "ingame-manual-dock4.png"] },
    		{ level: 2, key: "ingame-manual-inflight-navigation", params: [function () { if (player.ship.hasHyperspaceMotor) { return expandDescription("[ingame-manual-hyperspaceinfo]", { "spintime": player.ship.hyperspaceSpinTime }); } else { return "" } }, function () { if (player.ship.hasHyperspaceMotor) { return expandDescription("[ingame-manual-outoffuel]"); } else { return expandDescription("[ingame-manual-nohyperdrive]") } }] },
    		{ level: 2, key: "ingame-manual-inflight-combat" },
    		{ level: 3, key: "ingame-manual-inflight-combat-laser", params: [function () { var m = player.ship.weaponFacings; return (m & 1) + ((m & 2) / 2) + ((m & 4) / 4) + ((m & 8) / 8); }], backgrounds: ["ingame-manual-laser0.png"] },
    		{ level: 3, key: "ingame-manual-inflight-combat-missile", params: [function () { return player.ship.missileCapacity; }], backgrounds: ["ingame-manual-missile0.png"] },
    		{ level: 0, key: "ingame-manual-equipment" },
    		{ level: 1, key: "ingame-manual-equipment-standard" },
    		{ level: 2, key: "ingame-manual-equipment-standard-hull" },
    		{ level: 2, key: "ingame-manual-equipment-standard-generators" },
    		{ level: 2, key: "ingame-manual-equipment-standard-capacitors", params: [function () { return Math.ceil(player.ship.maxEnergy / 64); }] },
    		{ level: 2, key: "ingame-manual-equipment-standard-support" },
    		{ level: 2, key: "ingame-manual-equipment-standard-crew" },
    		{ level: 2, key: "ingame-manual-equipment-standard-engines" },
    		{ level: 2, key: "ingame-manual-equipment-standard-shields" },
    		{ level: 2, key: "ingame-manual-equipment-standard-sensors" },
    		{ level: 2, key: "ingame-manual-equipment-standard-cargo" },
    		{ level: 2, key: "ingame-manual-equipment-standard-witchdrive" },
    		{ level: 1, key: "ingame-manual-equipment-trading" },
    		{ level: 2, key: "ingame-manual-equipment-trading-cargo" },
    		{ level: 2, key: "ingame-manual-equipment-trading-scoops", backgrounds: ["ingame-manual-scoops0.png", "ingame-manual-scoops1.png"] },
    		{ level: 2, key: "ingame-manual-equipment-trading-heat", backgrounds: ["ingame-manual-heat.png"] },
    		{ level: 2, key: "ingame-manual-equipment-trading-passenger" },
    		{ level: 2, key: "ingame-manual-equipment-trading-ana", backgrounds: ["ingame-manual-ana.png"], params: [function () { return worldScripts["Ships Library"]._anaKey; }] },
    		{ level: 2, key: "ingame-manual-equipment-trading-asc", backgrounds: ["ingame-manual-asc.png"] },
    		{ level: 2, key: "ingame-manual-equipment-trading-docking" },
    		{ level: 2, key: "ingame-manual-equipment-trading-galactic" },
    		{ level: 1, key: "ingame-manual-equipment-combat" },
    		{ level: 2, key: "ingame-manual-equipment-combat-lasers", backgrounds: ["ingame-manual-laser1.png"] },
    		{ level: 3, key: "ingame-manual-equipment-combat-lasers-pulse" },
    		{ level: 3, key: "ingame-manual-equipment-combat-lasers-beam" },
    		{ level: 3, key: "ingame-manual-equipment-combat-lasers-mining" },
    		{ level: 3, key: "ingame-manual-equipment-combat-lasers-military" },
    		{ level: 2, key: "ingame-manual-equipment-combat-pylons", backgrounds: ["ingame-manual-pylons0.png"] },
    		{ level: 3, key: "ingame-manual-equipment-combat-pylons-missile", backgrounds: ["ingame-manual-missile1.png"] },
    		{ level: 3, key: "ingame-manual-equipment-combat-pylons-hardhead", opts: { model: "[ecm-proof-missile]" } },
    		{ level: 3, key: "ingame-manual-equipment-combat-pylons-qcm", opts: { model: "[qbomb]" } },
    		{ level: 2, key: "ingame-manual-equipment-combat-ecm" },
    		{ level: 2, key: "ingame-manual-equipment-combat-mts", backgrounds: ["ingame-manual-pylons1.png"] },
    		{ level: 2, key: "ingame-manual-equipment-combat-escape", opts: { model: "[escape-capsule]" } },
    		{ level: 2, key: "ingame-manual-equipment-combat-eeu" },
    		{ level: 2, key: "ingame-manual-equipment-combat-tsme" },
    		{ level: 2, key: "ingame-manual-equipment-combat-inject", backgrounds: ["ingame-manual-inject.png"] },
    		{ level: 2, key: "ingame-manual-equipment-combat-wormhole", backgrounds: ["ingame-manual-wormhole.png"] },
    		{ level: 2, key: "ingame-manual-equipment-combat-ste", backgrounds: ["ingame-manual-ste.png"] },
    		{ level: 2, key: "ingame-manual-equipment-combat-its" },
    		{ level: 2, key: "ingame-manual-equipment-combat-shield" },
    		{ level: 1, key: "ingame-manual-equipment-extra", params: [function () { return worldScripts["Ships Library"]._primeKey; }] },
    		{ level: 2, key: "ingame-manual-equipment-extra-displays", params: [function () { return worldScripts["Ships Library"]._cycleMFDKey; }, function () { return worldScripts["Ships Library"]._switchMFDKey; }] },
    		{ level: 0, key: "ingame-manual-sociology" },
    		{ level: 1, key: "ingame-manual-sociology-galcop" },
    		{ level: 2, key: "ingame-manual-sociology-galcop-facilities", backgrounds: ["ingame-manual-station.png"] },
    		{ level: 2, key: "ingame-manual-sociology-galcop-legal", backgrounds: ["ingame-manual-police0.png", "ingame-manual-police1.png", "ingame-manual-police0.png"] },
    		{ level: 2, key: "ingame-manual-sociology-galcop-combat", backgrounds: ["ingame-manual-combat0.png"] },
    		{ level: 1, key: "ingame-manual-sociology-pilots" },
    		{ level: 2, key: "ingame-manual-sociology-pilots-traders", backgrounds: ["ingame-manual-shiptrader.png"] },
    		{ level: 2, key: "ingame-manual-sociology-pilots-hunters", backgrounds: ["ingame-manual-shiphunter.png"] },
    		{ level: 2, key: "ingame-manual-sociology-pilots-pirates", backgrounds: ["ingame-manual-shippirate.png"] },
    		{ level: 2, key: "ingame-manual-sociology-pilots-miners", backgrounds: ["ingame-manual-shipminer.png"] },
    		{ level: 2, key: "ingame-manual-sociology-pilots-military" },
    		{ level: 2, key: "ingame-manual-sociology-pilots-deep" },
    		{ level: 2, key: "ingame-manual-sociology-pilots-salvage" },
    		{ level: 1, key: "ingame-manual-sociology-govt" },
    		{ level: 2, key: "ingame-manual-sociology-govt-corp" },
    		{ level: 2, key: "ingame-manual-sociology-govt-demo" },
    		{ level: 2, key: "ingame-manual-sociology-govt-conf" },
    		{ level: 2, key: "ingame-manual-sociology-govt-comm" },
    		{ level: 2, key: "ingame-manual-sociology-govt-dict" },
    		{ level: 2, key: "ingame-manual-sociology-govt-mult" },
    		{ level: 2, key: "ingame-manual-sociology-govt-feud" },
    		{ level: 2, key: "ingame-manual-sociology-govt-anar" },
    		{ level: 1, key: "ingame-manual-sociology-econ" },
    		{ level: 2, key: "ingame-manual-sociology-econ-agri" },
    		{ level: 2, key: "ingame-manual-sociology-econ-ind" },
    		{ level: 1, key: "ingame-manual-sociology-species" },
    		{ level: 1, key: "ingame-manual-sociology-thargoids" },
    		{ level: 1, key: "ingame-manual-sociology-history" },
    		{ level: 2, key: "ingame-manual-sociology-history-before" },
    		{ level: 2, key: "ingame-manual-sociology-history-eight" },
    		{ level: 2, key: "ingame-manual-sociology-history-interspecies" },
    		{ level: 2, key: "ingame-manual-sociology-history-founding" },
    		{ level: 2, key: "ingame-manual-sociology-history-thargoids" },
    		{ level: 2, key: "ingame-manual-sociology-history-modern" },
    		{ level: 0, key: "ingame-manual-ships" },
    		{ level: 1, key: "ingame-manual-ships-adder", opts: { model: "[adder]" } },
    		{ level: 1, key: "ingame-manual-ships-anaconda", opts: { model: "[anaconda]" } },
    		{ level: 1, key: "ingame-manual-ships-asp", opts: { model: "[asp]" } },
    		{ level: 1, key: "ingame-manual-ships-boa", opts: { model: "[boa]" } },
    		{ level: 1, key: "ingame-manual-ships-boa2", opts: { model: "[boa-mk2]" } },
    		{ level: 1, key: "ingame-manual-ships-cobra", opts: { model: "[cobramk1]" } },
    		{ level: 1, key: "ingame-manual-ships-cobra3", opts: { model: "[cobra3-trader]" } },
    		{ level: 1, key: "ingame-manual-ships-fdl", opts: { model: "[ferdelance]" } },
    		{ level: 1, key: "ingame-manual-ships-gecko", opts: { model: "[gecko]" } },
    		{ level: 1, key: "ingame-manual-ships-krait", opts: { model: "[krait]" } },
    		{ level: 1, key: "ingame-manual-ships-mamba", opts: { model: "[mamba]" } },
    		{ level: 1, key: "ingame-manual-ships-moray", opts: { model: "[moray]" } },
    		{ level: 1, key: "ingame-manual-ships-shuttle", opts: { model: "[shuttle]" } },
    		{ level: 1, key: "ingame-manual-ships-python", opts: { model: "[python]" } },
    		{ level: 1, key: "ingame-manual-ships-sidewinder", opts: { model: "[sidewinder]" } },
    		{ level: 1, key: "ingame-manual-ships-transporter", opts: { model: "[transporter-miner]" } },
    		{ level: 1, key: "ingame-manual-ships-viper", opts: { model: "[viper]" } },
    		{ level: 1, key: "ingame-manual-ships-interceptor", opts: { model: "[viper-interceptor]" } },
    		{ level: 1, key: "ingame-manual-ships-worm", opts: { model: "[worm]" } },
    		{ level: 1, key: "ingame-manual-ships-thargoid", opts: { model: "[thargoid]" } },
    		{ level: 1, key: "ingame-manual-ships-tharglet", opts: { model: "[tharglet]" } },
    		{ level: 0, key: "ingame-manual-end" } // temp entry
    	];
    
    	this._registerBook("ingame-manual", expandMissionText("ingame-manual-title"), contents);
    
    }
    
    this._setBook = function (key) {
    	if (!this.$books[key]) {
    		return;
    	}
    	this.$contents = this.$books[key].contents;
    	this.$msRows = this.$books[key].pageHeight;
    	if (!this.$fcb) {
    		this.$fcb = addFrameCallback(this._precalculateOffsets.bind(this));
    	}
    
    	if (this.$bookmark[key]) {
    		while (this.$bookmark[key].length < 4) {
    			this.$bookmark[key].push(0);
    		}
    		this.$chapter = this.$bookmark[key][0];
    		this.$offset = this.$bookmark[key][1];
    		this.$mfdChapter = this.$bookmark[key][2];
    		this.$mfdOffset = this.$bookmark[key][3];
    	} else {
    		this.$chapter = 0;
    		this.$offset = 0;
    		this.$mfdChapter = 0;
    		this.$mfdOffset = 0;
    		this.$bookmark[key] = [0, 0, 0, 0];
    	}
    	this.$currentBook = key;
    
    	// reset MFD if active
    	if (!player.ship.docked) {
    		// resetting appears to be unnecessary.
    		//this.$mfdOn = false;
    		//this.$mfdMode = 0;
    		player.consoleMessage(expandMissionText("ships-library-mfd-reset"), 6);
    		//this._hideMFD();
    	}
    }
    
    this._getBookListAlpha = function () {
    	var keys = Object.keys(this.$books);
    	keys.sort(function (a, b) {
    		return this.$books[a].title.localeCompare(this.$books[b].title);
    	}.bind(this));
    	return keys;
    }
    
    /** Mission screen management **/
    
    this._showLibrary = function () {
    	this.$mode = "firstrun";
    	// init and enter loop
    	this._showManual();
    }
    
    this._showManual = function () {
    	this.$hudHidden = player.ship.hudHidden;
    	this.$manualActive = true;
    	player.ship.hudHidden = true;
    	if (this.$mode == "firstrun") {
    		if (Object.keys(this.$books).length > 1) {
    			this.$mode = "book";
    			this.$offset = 0;
    		} else {
    			this.$mode = "read";
    		}
    	}
    	if (this.$mode == "contents") {
    		this._showChapterList();
    	} else if (this.$mode == "read") {
    		this._showPage();
    	} else if (this.$mode == "book") {
    		this._showBookList();
    	}
    }
    
    this.guiScreenChanged = function (to, from) {
    	if (from == "GUI_SCREEN_MISSION" && this.$manualActive) {
    		this.$manualActive = false;
    		player.ship.hudHidden = this.$hudHidden;
    		if (this.$fcbM) {
    			removeFrameCallback(this.$fcbM);
    			delete this.$fcbM;
    		}
    	}
    }
    
    this._showBookList = function () {
    	var blank = { unselectable: true, text: "" };
    	var listchoices = new Object;
    	if (this.$offset >= this.$chaptersperpage) {
    		listchoices["51_BOOK_BACK"] = {
    			text: expandMissionText("ships-library-contents-back"),
    			alignment: "LEFT",
    			color: "orangeColor"
    		}
    	} else {
    		listchoices["51_BOOK_BACK"] = blank;
    	}
    	listchoices["53_SPACER"] = blank;
    	var books = this._getBookListAlpha();
    	var start = this.$offset - (this.$offset % this.$chaptersperpage);
    	for (var i = start; i < start + this.$chaptersperpage; i++) {
    		var istr = String(i);
    		if (i < 10) {
    			istr = "0" + String(i);
    		}
    		if (books.length > i) {
    			listchoices["55_BOOK_SELECT_" + istr + "_" + books[i]] = {
    				text: this.$books[books[i]].title,
    				alignment: "LEFT"
    			}
    		} else {
    			listchoices["55_BOOK_SELECT_" + istr + "_"] = blank;
    		}
    	}
    	listchoices["57_SPACER"] = blank;
    	if (start + this.$chaptersperpage < books.length) {
    		listchoices["59_BOOK_FORWARD"] = {
    			text: expandMissionText("ships-library-contents-forward"),
    			alignment: "RIGHT",
    			color: "orangeColor"
    		}
    	} else {
    		listchoices["59_CONTENTS_FORWARD"] = blank;
    	}
    	for (var i = 24; i < 26; i++) {
    		listchoices["98_SPACER" + i] = blank;
    	}
    	//		listchoices["50_BOOK"] = expandMissionText("ships-library-changebook");
    	listchoices["99_EXIT"] = expandMissionText("ships-library-exit");
    
    	mission.runScreen({
    		screenID: "ships-library",
    		titleKey: "ships-library-bookselect",
    		allowInterrupt: true,
    		exitScreen: "GUI_SCREEN_INTERFACES",
    		choices: listchoices,
    		initialChoicesKey: this.$lastchoice ? this.$lastchoice : "55_CONTENTS_SELECT_" + this.$offset
    	}, this._manualHandler, this);
    
    	if (this._bookListOverlay != "") {
    		setScreenOverlay(this._bookListOverlay);
    	}
    }
    
    this._showChapterList = function () {
    	// 0 = back in list (?)
    	// 2-11 = chapter headings (read chapter)
    	// 13 = forward in list (?)
    	// 20 = exit
    	var blank = { unselectable: true, text: "" };
    	var listchoices = new Object;
    	if (this.$chapter >= this.$chaptersperpage) {
    		listchoices["11_CONTENTS_BACK"] = {
    			text: expandMissionText("ships-library-contents-back"),
    			alignment: "LEFT",
    			color: "orangeColor"
    		}
    	} else {
    		listchoices["11_CONTENTS_BACK"] = blank;
    	}
    	listchoices["13_SPACER"] = blank;
    	var start = this.$chapter - (this.$chapter % this.$chaptersperpage);
    	for (var i = start; i < start + this.$chaptersperpage; i++) {
    		var istr = String(i);
    		if (i < 10) {
    			istr = "0" + String(i);
    		}
    		if (this.$contents.length > i) {
    			var indent = "";
    			if (this.$contents[i].level > 0) {
    				indent += Array(this.$contents[i].level + 1).join("... ");
    			}
    			listchoices["15_CONTENTS_SELECT_" + istr] = {
    				text: indent + expandMissionText(this.$contents[i].key + "-title"),
    				alignment: "LEFT"
    			}
    		} else {
    			listchoices["15_CONTENTS_SELECT_" + istr] = blank;
    		}
    	}
    	listchoices["17_SPACER"] = blank;
    	if (start + this.$chaptersperpage < this.$contents.length) {
    		listchoices["19_CONTENTS_FORWARD"] = {
    			text: expandMissionText("ships-library-contents-forward"),
    			alignment: "RIGHT",
    			color: "orangeColor"
    		}
    	} else {
    		listchoices["19_CONTENTS_FORWARD"] = blank;
    	}
    	for (var i = 24; i < 25; i++) {
    		listchoices["48_SPACER" + i] = blank;
    	}
    	listchoices["50_BOOK"] = expandMissionText("ships-library-changebook");
    	listchoices["99_EXIT"] = expandMissionText("ships-library-exit");
    
    	mission.runScreen({
    		screenID: "ships-library",
    		titleKey: "ships-library-chapterselect",
    		allowInterrupt: true,
    		exitScreen: "GUI_SCREEN_INTERFACES",
    		choices: listchoices,
    		initialChoicesKey: this.$lastchoice ? this.$lastchoice : "15_CONTENTS_SELECT_" + this.$chapter
    	}, this._manualHandler, this);
    
    	if (this._chapterListOverlay != "") {
    		setScreenOverlay(this._chapterListOverlay);
    	}
    }
    
    this._showPage = function () {
    	var chapter = this.$contents[this.$chapter];
    	this.$msRows = this.$books[this.$currentBook].pageHeight;
    	if (chapter.hasOwnProperty("pageHeight")) this.$msRows = chapter.pageHeight;
    	var text = this._textFromOffset(this.$chapter, this.$offset, this.$msStore, this.$msRows, this.$msCols);
    
    	var opts = {
    		screenID: "ships-library",
    		titleKey: chapter.key + "-title",
    		allowInterrupt: true,
    		exitScreen: "GUI_SCREEN_INTERFACES",
    		choices: {
    			"01_PREV": expandMissionText("ships-library-page-back"),
    			"09_NEXT": expandMissionText("ships-library-page-next"),
    			"10_CONTENTS": expandMissionText("ships-library-contents"),
    			"99_EXIT": expandMissionText("ships-library-exit"),
    		},
    		initialChoicesKey: this.$lastchoice ? this.$lastchoice : "09_NEXT",
    		message: text
    	};
    	if (chapter.opts) {
    		var extras = Object.keys(chapter.opts)
    		for (var k = 0; k < extras.length; k++) {
    			opts[extras[k]] = chapter.opts[extras[k]];
    		}
    	}
    	if (chapter.backgrounds) {
    		var page = this._pageOfOffset(chapter, this.$offset, this.$msStore);
    		if (page < chapter.backgrounds.length) {
    			var backg = chapter.backgrounds[page];
    		} else {
    			var backg = chapter.backgrounds[chapter.backgrounds.length - 1];
    		}
    		if (backg != "") {
    			opts.overlay = backg;
    		}
    	}
    
    	mission.runScreen(opts, this._manualHandler, this);
    	this.$fcbM = addFrameCallback(this._moveShip.bind(this));
    
    }
    
    this._moveShip = function (delta) {
    	if (mission.displayModel) {
    		if (mission.displayModel.position.y >= -1) {
    			mission.displayModel.position = new Vector3D(0, -mission.displayModel.position.z / 2.5, mission.displayModel.position.z * 2.5);
    		}
    	}
    }
    
    this._setBookmark = function () {
    	this.$bookmark[this.$currentBook][0] = this.$chapter;
    	this.$bookmark[this.$currentBook][1] = this.$offset;
    	this.$bookmark[this.$currentBook][2] = this.$mfdChapter;
    	this.$bookmark[this.$currentBook][3] = this.$mfdOffset;
    }
    
    this._manualHandler = function (choice) {
    	//		log(this.name,choice);
    	delete this.$lastchoice;
    	if (!choice) {
    		return; // launched while reading?
    	} else if (choice == "01_PREV") {
    		if (this.$offset == 0) {
    			if (this.$chapter == 0) {
    				this.$mode = "contents";
    			} else {
    				this.$mode = "read";
    				this.$chapter--;
    				this.$offset = this._lastPageOffset(this.$chapter, this.$msStore);
    				this._setBookmark();
    			}
    		} else {
    			this.$mode = "read";
    			this.$offset = this._previousOffset(this.$chapter, this.$offset, this.$msStore, this.$msRows, this.$msCols);
    			this._setBookmark();
    		}
    		this.$lastchoice = choice;
    	} else if (choice == "05_READ") {
    		this.$mode = "read";
    	} else if (choice == "09_NEXT") {
    		this.$offset = this._nextOffset(this.$chapter, this.$offset, this.$msStore, this.$msRows, this.$msCols);
    		if (this.$offset == -1) {
    			this.$offset = 0;
    			this.$chapter++;
    			if (this.$chapter >= this.$contents.length) {
    				this.$mode = "contents";
    				this.$chapter = 0;
    			} else {
    				this._setBookmark();
    				this.$mode = "read";
    			}
    		} else {
    			this._setBookmark();
    		}
    	} else if (choice == "10_CONTENTS") {
    		this.$mode = "contents";
    		this.$offset = 0;
    	} else if (choice == "11_CONTENTS_BACK") {
    		this.$mode = "contents";
    		this.$offset = 0;
    		this.$chapter -= 1 + (this.$chapter % this.$chaptersperpage);
    		this.$lastchoice = choice;
    	} else if (choice.match(/^15_CONTENTS_SELECT_/)) {
    		this.$chapter = parseInt(choice.slice(19), 10);
    		this.$offset = 0;
    		this._setBookmark();
    		this.$mode = "read";
    	} else if (choice == "19_CONTENTS_FORWARD") {
    		this.$mode = "contents";
    		this.$offset = 0;
    		this.$chapter += this.$chaptersperpage - (this.$chapter % this.$chaptersperpage);
    		this.$lastchoice = choice;
    	} else if (choice == "50_BOOK") {
    		this.$mode = "book";
    		this.$offset = 0;
    	} else if (choice == "51_BOOK_BACK") {
    		this.$offset -= this.$chaptersperpage;
    	} else if (choice == "59_BOOK_FORWARD") {
    		this.$offset += this.$chaptersperpage;
    	} else if (choice.match(/^55_BOOK_SELECT_/)) {
    		this._setBook(choice.slice(18));
    		this.$mode = "read";
    	}
    
    	// 01_PREV: reduce offset to prev page, or last page of prev chapter
    	// 05_READ: switch to read mode at current chapter + offset
    	// 09_NEXT: advance offset to next page, or next chapter if no more
    	// 10_CONTENTS: switch to contents mode at current chapter, zero offset
    	// 11_CONTENTS_BACK: up one page in chapter list
    	// 15_CONTENTS_SELECT_[0-9]: switch to read mode in chapter at offset
    	// 19_CONTENTS_FORWARD: down one page in chapter list
    	// 99_EXIT: quit
    
    
    	if (choice != "99_EXIT") {
    		this._showManual();
    	}
    }
    
    /** MFD functions **/
    
    this._equipPressMode = function () {
    	this.$mfdMode = (this.$mfdMode + 1) % 7;
    	player.consoleMessage(expandDescription("[ships-library-mfd-mode][ships-library-mfd-mode" + this.$mfdMode + "]"));
    }
    
    this._equipPressActivate = function () {
    	if (player.ship.multiFunctionDisplays == 0) {
    		player.consoleMessage(expandDescription("[ships-library-mfd-unavailable]"));
    		return;
    	}
    	if (this.$mfdMode == 0) // power
    	{
    		this.$mfdOn = !this.$mfdOn;
    		if (this.$mfdOn) {
    			// out of range value picks first free MFD, if there is one
    			player.ship.setMultiFunctionDisplay(player.ship.multiFunctionDisplays, "ships-library")
    			player.consoleMessage(expandDescription("[ships-library-mfd-on]"));
    		}
    	}
    	else if (this.$mfdMode == 1) // next page
    	{
    		this.$mfdOffset = this._nextOffset(this.$mfdChapter, this.$mfdOffset, this.$mfdStore, this.$mfdRows, this.$mfdCols);
    		if (this.$mfdOffset == -1) {
    			this.$mfdOffset = 0;
    			this.$mfdChapter++;
    			if (this.$mfdChapter >= this.$contents.length) {
    				this.$mfdChapter = 0;
    			} else {
    				this._setBookmark();
    			}
    		} else {
    			this._setBookmark();
    		}
    	}
    	else if (this.$mfdMode == 2) // prev page
    	{
    		if (this.$mfdOffset == 0) {
    			if (this.$mfdChapter == 0) {
    				this.$mfdChapter = this.$contents.length - 1;
    			} else {
    				this.$mfdChapter--;
    				this.$mfdOffset = this._lastPageOffset(this.$mfdChapter, this.$mfdStore);
    				this._setBookmark();
    			}
    		} else {
    			this.$mfdOffset = this._previousOffset(this.$mfdChapter, this.$mfdOffset, this.$mfdStore, this.$mfdRows, this.$mfdCols);
    			this._setBookmark();
    		}
    	}
    	else if (this.$mfdMode == 3) // next chapter
    	{
    		this.$mfdOffset = 0;
    		this.$mfdChapter++;
    		if (this.$mfdChapter >= this.$contents.length) {
    			this.$mfdChapter = 0;
    		}
    	}
    	else if (this.$mfdMode == 4) // prev chapter
    	{
    		this.$mfdOffset = 0;
    		this.$mfdChapter--;
    		if (this.$mfdChapter < 0) {
    			this.$mfdChapter = this.$contents.length - 1;
    		}
    	}
    	else if (this.$mfdMode == 5) // next book
    	{
    		var booklist = Object.keys(this.$books);
    		var curIndex = booklist.indexOf(this.$currentBook);
    		var newIndex = curIndex + 1;
    		if (curIndex == booklist.length - 1) newIndex = 0;
    		this._setBook(booklist[newIndex]);
    		player.consoleMessage(expandDescription("[ships-library-mfd-book-selected]", { selected_book: this.$books[booklist[newIndex]].title }), 6);
    	}
    	else if (this.$mfdMode == 6) // previous book
    	{
    		var booklist = Object.keys(this.$books);
    		var curIndex = booklist.indexOf(this.$currentBook);
    		var newIndex = curIndex - 1;
    		if (curIndex == 0) newIndex = booklist.length - 1;
    		this._setBook(booklist[newIndex]);
    		player.consoleMessage(expandDescription("[ships-library-mfd-book-selected]", { selected_book: this.$books[booklist[newIndex]].title }), 6);
    	}
    
    	if (this.$mfdOn) {
    		this._showMFD();
    	}
    	else {
    		this._hideMFD();
    	}
    }
    
    this._showMFD = function () {
    	var text = this._textFromOffset(this.$mfdChapter, this.$mfdOffset, this.$mfdStore, this.$mfdRows, this.$mfdCols);
    	text = this._mfdBreak(text);
    	player.ship.setMultiFunctionText("ships-library", text);
    }
    
    this._hideMFD = function () {
    	player.ship.setMultiFunctionText("ships-library");
    }
    
    this._mfdBreak = function (text) {
    	if (text == String.fromCharCode(30)) {
    		return expandDescription("[ingame-library-imageunavailable]");
    	}
    	var linewidth = this.$mfdCols;
    	var lines = [];
    	var paras = text.split("\n");
    
    	var pl = paras.length;
    	for (var i = 0; i < pl; i++) {
    		var accum = "";
    		var words = paras[i].split(" ");
    		var widthuse = 0;
    		var wl = words.length;
    		for (var j = 0; j < wl; j++) {
    			var word = (accum == "" ? "" : " ") + words[j];
    			var wordwidth = defaultFont.measureString(word);
    			if (widthuse + wordwidth < linewidth) {
    				accum += word;
    				widthuse += wordwidth;
    			} else {
    				lines.push(accum);
    				accum = word.slice(1);
    				widthuse = wordwidth - this.$spaceWidth;
    			}
    		}
    		lines.push(accum);
    	}
    	return lines.join("\n");
    }
    
    /** Utility functions **/
    
    this._blockFromOffset = function (chapternum, offset, lines, linewidth) {
    	//		log(this.name,"Calculating block: "+key+","+offset);
    	var pagebreak = String.fromCharCode(30);
    	//	var lines = this.$rows;
    	//	var linewidth = this.$cols;
    
    	var fulltext = this._getText(chapternum);
    
    	var text = fulltext.slice(offset, offset + (lines * linewidth * 4));
    	var paras = text.split("\n");
    	var accum = "";
    	var lineuse = 0;
    	var pl = paras.length;
    	for (var i = 0; i < pl; i++) {
    		var words = paras[i].split(" ");
    		var widthuse = 0;
    		var wl = words.length;
    		for (var j = 0; j < wl; j++) {
    			if (words[j] == pagebreak) {
    				lineuse = lines;
    				accum += pagebreak + "\n";
    				break; // page break character
    			}
    			var word = (j == 0 ? "" : " ") + words[j];
    			var wordwidth = defaultFont.measureString(word);
    			if (widthuse + wordwidth < linewidth) {
    				accum += word;
    				widthuse += wordwidth;
    			} else {
    				widthuse = 0;
    				lineuse++;
    				if (lineuse >= lines) {
    					accum += "\n";
    					break;
    				} else {
    					accum += word;
    					if (j == 0) {
    						widthuse += wordwidth;
    					}
    					else {
    						widthuse += wordwidth - this.$spaceWidth;
    					}
    				}
    			}
    		}
    		if (lineuse >= lines) {
    			break;
    		}
    		lineuse++;
    		accum += "\n";
    		if (lineuse >= lines) {
    			break;
    		}
    	}
    
    	if (lineuse < this.$minblock && lines == this.$msRows) {
    		this.$short = true;
    	} else {
    		this.$short = false;
    	}
    	//		log(this.name,"Calculated block: "+key+","+offset);
    	return accum;
    }
    
    // offset calculation can be fairly slow for large text blocks
    // this calculates it in the background a page at a time
    // while the player is docked and frame rate is less crucial
    this._precalculateOffsets = function () {
    	if (!player.ship || !player.ship.docked) {
    		return;
    	}
    	for (var i = 0; i < this.$contents.length; i++) {
    		if (this._precalculateOffset(i)) {
    			return;
    		}
    	}
    	removeFrameCallback(this.$fcb);
    	delete this.$fcb;
    }
    
    this._precalculateOffset = function (chapternum) {
    	var chapter = this.$contents[chapternum];
    	if (chapter.offsets && chapter.miniOffsets) {
    		return false;
    	}
    	if (!chapter._offsets) {
    		chapter._offsets = [0];
    	}
    	if (!chapter._miniOffsets) {
    		chapter._miniOffsets = [0];
    	}
    
    	var fulltext = this._getText(chapternum);
    	var offset = chapter._offsets[chapter._offsets.length - 1];
    	var block = this._blockFromOffset(chapternum, offset, this.$msRows, this.$msCols);
    	offset += block.length;
    	if (offset < fulltext.length) {
    		// add next block
    		chapter._offsets.push(offset);
    	} else {
    		// done
    		if (this.$short && chapter._offsets.length > 1) {
    			chapter._offsets.pop(); // merge last block into previous
    		}
    		chapter._offsets.push(-1);
    		chapter.offsets = chapter._offsets;
    		delete chapter._offsets;
    	}
    
    	offset = chapter._miniOffsets[chapter._miniOffsets.length - 1];
    	block = this._blockFromOffset(chapternum, offset, this.$mfdRows, this.$mfdCols);
    	offset += block.length;
    	if (offset < fulltext.length) {
    		// add next block
    		chapter._miniOffsets.push(offset);
    	} else {
    		// done
    		if (this.$short && chapter._miniOffsets.length > 1) {
    			chapter._miniOffsets.pop(); // merge last block into previous
    		}
    		chapter._miniOffsets.push(-1);
    		chapter.miniOffsets = chapter._miniOffsets;
    		delete chapter._miniOffsets;
    	}
    
    	return true;
    }
    
    
    this._calculateOffsets = function (chapternum, rows, cols) {
    	var offsets = new Array;
    	var fulltext = this._getText(chapternum);
    
    	var offset = 0;
    	this.$short = false;
    	while (offset < fulltext.length && !this.$short) {
    		offsets.push(offset);
    		var block = this._blockFromOffset(chapternum, offset, rows, cols);
    		offset += block.length;
    	}
    	if (this.$short && offsets.length > 1) {
    		offsets.pop(); // merge last block into previous
    	}
    
    	offsets.push(-1);
    
    	return offsets;
    }
    
    
    this._textFromOffset = function (chapternum, fromoffset, otype, rows, cols) {
    	var fulltext = this._getText(chapternum);
    
    	var tooffset = this._nextOffset(chapternum, fromoffset, otype, rows, cols);
    	if (tooffset == -1) {
    		var slice = fulltext.slice(fromoffset);
    	} else {
    		var slice = fulltext.slice(fromoffset, tooffset);
    	}
    	return slice; //.trim();
    }
    
    this._getText = function (chapternum) {
    	var chapter = this.$contents[chapternum];
    	var params = new Object;
    	params["ships-library-page-break"] = String.fromCharCode(30) + "\n";
    	if (!chapter.params) {
    		return expandMissionText(chapter.key + "-text", params);
    	} else {
    		for (var i = 0; i < chapter.params.length; i++) {
    			params["ships_library_param_" + i] = chapter.params[i]();
    		}
    		return expandMissionText(chapter.key + "-text", params);
    	}
    }
    
    this._pageOfOffset = function (chapter, offset, otype) {
    	// only call this function when chapter.offsets must have been
    	// calculated already
    
    	for (var i = 0; i < chapter[otype].length; i++) {
    		if (offset <= chapter[otype][i]) {
    			return i;
    		}
    	}
    	return chapter[otype].length;
    }
    
    this._nextOffset = function (chapternum, fromoffset, otype, rows, cols) {
    	var chapter = this.$contents[chapternum];
    	if (!chapter[otype]) {
    		chapter[otype] = this._calculateOffsets(chapternum, rows, cols);
    		delete chapter._offsets;
    	}
    
    	for (var i = 0; i < chapter[otype].length; i++) {
    		if (fromoffset <= chapter[otype][i]) {
    			if (i == chapter[otype].length - 1) {
    				return -1;
    			}
    			return chapter[otype][i + 1];
    		}
    	}
    	return -1;
    }
    
    this._previousOffset = function (chapternum, fromoffset, otype, rows, cols) {
    	var offcount = 0;
    	do {
    		var next = this._nextOffset(chapternum, offcount, otype, rows, cols);
    		if (next < fromoffset) {
    			offcount = next;
    		}
    	} while (next < fromoffset);
    	return offcount;
    }
    
    this._lastPageOffset = function (chapternum, otype, rows, cols) {
    	var offcount = 0;
    	do {
    		var next = this._nextOffset(chapternum, offcount, otype, rows, cols);
    		if (next != -1) {
    			offcount = next;
    		}
    	} while (next != -1);
    	return offcount;
    }
    
    this._getTip = function () {
    	var numtips = 12;
    
    
    	var tipn = (clock.days - 2084004) % numtips;
    	if (tipn <= 9) {
    		var tips = "00" + tipn.toString();
    	} else if (tipn <= 99) {
    		var tips = "0" + tipn.toString();
    	} else {
    		var tips = tipn.toString();
    	}
    	return expandMissionText("ingame-manual-tip-" + tips);
    }
    
    Scripts/ships_library_mfd.js
    "use strict";
    this.author      = "cim"; 
    this.copyright   = "� 2011-2014 cim."; 
    this.licence = "CC-BY-SA 3.0";
    this.version     = "0.8"; 
    this.name = "Ships Library equipment script";
    
    this.activated = function() {
    		worldScripts["Ships Library"]._equipPressActivate.bind(worldScripts["Ships Library"])();
    }
    
    this.mode = function() {
    		worldScripts["Ships Library"]._equipPressMode.bind(worldScripts["Ships Library"])();
    }