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

Expansion Gallery

Content

Manifest

from Expansion Manager's OXP list from Expansion Manifest
Description Show and zoom ships, meet Exhibitions and gain Visitor Levels. Show and zoom ships, meet Exhibitions and gain Visitor Levels.
Identifier oolite.oxp.Norby.Gallery oolite.oxp.Norby.Gallery
Title Gallery Gallery
Category Activities Activities
Author Norby Norby
Version 1.21 1.21
Tags
Required Oolite Version
Maximum Oolite Version
Required Expansions
Optional Expansions
Conflict Expansions
Dependent Expansions
  • oolite.oxp.Norby.Ambience_Collection:1.3
  • Information URL http://wiki.alioth.net/index.php/Gallery n/a
    Download URL https://wiki.alioth.net/img_auth.php/7/71/Gallery_1.21.oxz n/a
    License CC-BY-NC-SA 3.0 CC-BY-NC-SA 3.0
    File Size n/a
    Upload date 1610873334

    Relationships Diagram

    Documentation

    Also read http://wiki.alioth.net/index.php/Gallery

    Gallery_readme.txt

    Gallery OXP
    
    Show encountered ships and other space objects in Interfaces (F4).
    Extend your gallery by flying around and targeting or fighting with ships and other space objects.
    
    If you are a new pilot then you will only see purchasable ships from the station shipyards.
    
    Press F4 when you are docked, then choose "Gallery of encounters" to see ship statistics and rotating models.
    
    You will see how many ships and other space objects you have currently gathered and the list of last encounters.
    
    Only the first name from each initials listed here to fit into the screen when you will have one in each letter also.
    
    
    Gallery Menu
    
     "Name"
     Previous
     Search
     Rotate X+ / Stop / X- / Stop / Y+ / Stop / Y- / Stop
     Move X+ / Stop / X- / Stop / Y+ / Stop / Y- / Stop
     Zoom + / Stop / Zoom - / Stop
     Exit
     Hide Menu
    
    The first line shows name of the current object displayed, select it to see the next rotating model.
    You can step back, search, rotate, move, zoom and hide the menu to get a better view of the rotating model.
    
    The Search menu uses text entry in Oolite 1.79. Oolite 1.77 uses a selection list.
    
    
    Exhibitions
    
    Check incoming ship meetings in Exhibitions Interface (F4) where you can add rare ships into your Gallery.
    Type specific exhibitions contain ships which not in largest ones.
    Beware of Pirate Meetings, there are aggressive guards out there!
    
    Plan your route to arrive to the advertised systems in time. Exhibitions are organized near witchpoints.
    
    You can request permission to rotate exhibition ships for viewing, after you have received permission it you can rotate the ship by locking target on it.
    To help find which ship is missing from your Gallery these start rotating initially.
    Select each rotating ship with a target lock to add these into your Gallery.
    Some ships may only differ in colour (colouring need shaders, if your system does not aupport shaders then these ships will look identical).
    After 5 minutes you must join the exhibition queue again so that other exhibition visitors can have their turn.
    
    
    Visitor levels
    
    Your visit to an exhibition will be registered when you target at least one ship in an exhibition.
    You can increase your Visitor Level if you check more exhibitions.
    You will receive messages when you reach the following levels of appreciation:
    
     Visit	Level
     1	Novice Visitor
     2	Unexpected Visitor
     4	Poor Visitor
     8	Below Average Visitor
     16	Average Visitor
     32	Above Average Visitor
     64	Competent Visitor
     100	Trustworthy Visitor!
     300	Expected Visitor!
     1000	Elite Visitor!
    
    
    Private Exhibition Control equipment
    
    You can order a private exhibition of purchasable ships before the dock where you are by either pay out this equipment or order in Exhibitions Interface.
    A few days will elapse until all ships arrive.
    You will be fined if you destroy any of them.
    
    You can start and stop all rotation in Private Exhibition by priming (Shift+N) and activating (n) the Private Exhibition Control equipment.
    To rotate one ship simply target it. Target a rotating ship to stop it rotating.
    
    The mode (b) of the primed equipment shows how many ships there are in the Private Exhibition.
    Some parts of the Private Exhibition will be hidden if too many ships causes display problems on your system. If this happens, select the mode (b) key to show the next part.
    
    The Private Exhibition will be closed if you dock with another station (you can order a new Private Exhibition before the new station) or you jump into another system. The Private Exhibition will remain active if you take a rest (save then load game).
    
     Cost: 100.0 Cr.
     Techlevel: 1
    
    
    Instructions:
    
    In Oolite v1.79 or later do not unzip the .oxz file, just move into the AddOns folder of your Oolite installation.
    In Oolite v1.77 make a Gallery.oxp subfolder in your AddOns folder and unzip the .oxz file into the newly created subfolder.
    
    
    Limitations:
    
    Depends on Oolite v1.77 but v1.79 or later recommended.
    
    Every menu selection redraws the ship model, so shader colours and decals will change by when you select Rotate, Move, etc. 
    
    Ship details are hidden based on the following (maybe need to extend):
    * Technical Reference Library OXP (validator copied from v1.0.1 but call the original if TRL OXP installed),
    * ccl_missionShip in scriptInfo,
    * "stealth" word within dataKey or primaryRole.
    
    This OXP use Ship.keys(), Ship.roles() and Ship.keysForRole("role") methods in Oolite v1.79.
    There is a slow and inaccurate method for Oolite v1.77 which tries to get all ships, but a few ships are randomly left out. This OXP can cause a crash at game start there is if not enough memory (if spinning cobras appear then you have enough memory). You can reduce the $GalleryMaxIt variable in gallery.js to avoid the problem if needed, but more ships will be left out.
    
    Can show ship.energyRechargeRate and ship.extraCargo in v1.79 only.
    
    Ships with "subent" word in dataKey are removed to skip subentities in Griff_Shipset_Replace_v1.34.oxp.
    
    Turrets, docks and other subentities without "subent" rule will appear in v1.79 if all objects mode enabled in gallery.js, there is no exact method to skip all of these.
    In v1.77 it is not a problem due to the fallback method can include ships with given roles only (see in gallery.js, you can extend if you want) so all OXP ships without standard roles left out completely.
    
    
    Setings in Scripts/gallery.js:
    
     this.$GalleryAll = false; //Gallery of all objects (cheat)
     this.$GalleryDefaultZoom = 1.5; //default size of ships
     this.$GalleryMaxIt = 5; //max. iteration in Oolite v1.77, reduce if cause problems
     this.$GalleryLog = false; //verbose log
     this.$GalleryRoles = ["all", "player", ... ]; //main roles, you can add more
     this.$GallerySpeed = 2; //move and zoom speed
    
    Setings in Scripts/exhibitions.js:
    
    this.$ExhibitionsAllShipsInPrivateExhibition = false; //show NPC ships in Private Exhibition (cheat)
    this.$ExhibitionsLog = false; //verbose log
    this.$ExhibitionsMaxShipsInLargeExhibitions = 144; //should be square number and min. 64
    this.$ExhibitionsMinFPS = 20; //split Private Exhibition to get more than this frames per second
    this.$ExhibitionsSpace = 150; //m space between ships in exhibition matrix
    
    
    License:
    
    This work is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike License version 4.0.
    If you are re-using any piece of this OXP, please let me know by sending an e-mail to norbylite@gmail.com.
    
    Background image source: Start Choices OXP by spara.
    
    
    Changelog:
     2015.04.25. v1.21 Many small fixes, thanks to QCS.
     2014.07.04. v1.20 Fix for seedy space bar and ships with ship script.
     2014.07.03. v1.19 Exhibitioins are moved forward to 10km from the player's exit point.
                       Oversized ships in Exhibitions are moved farther from others.
                       Small fix in gallery search.
     2014.05.19. v1.18 Fast startup in Oolite 1.79 by the new shipDataForKey.
     2014.05.18. v1.17 Back to the previous startup method after a fix in Oolite.
     2014.05.18. v1.16 Faster startup in Oolite 1.79 if many OXZs are installed.
     2014.01.17. v1.15 Small fix against "Unknown expansion key [Exhibition Guard]" log messages.
     2013.12.28. v1.14 Further proofreading by Keeper and Salon renamed to Private Exhibition.
     2013.12.15. v1.12 Proofreading by Ranthe.
     2013.12.13. v1.1  Salon split into more parts if needed to reach $ExhibitionsMinFPS.
     2013.12.06. v1.0  Exhibitions and Visitor levels.
                       List of last encounters.
     2013.11.27. v0.9  Salon Control equipment: exhibition of buyable ships at Navigation Buoy.
     2013.11.26. v0.8  Show shields from NPC Shields OXP and CustomShields OXP.
                       Fixed empty and duplicated names.
     2013.11.25. v0.7  Show encountered and playable ships only by default.
                       Show max.energy in banks, recharge in words and name instead of dataKey.
                       Details of some ships are hidden using Technical Reference Library OXP by spara.
                       Must set $GalleryAll to true if wanted all ships and details.
                       Search menu with text entry in Oolite 1.79 and selection list in 1.77.
     2013.11.23. v0.6  Works with Oolite v1.77 also (but slow and limited).
     2013.11.22. v0.5  First usable version.
     2013.11.21. v0.1  First test files.
    

    Equipment

    Name Visible Cost [deci-credits] Tech-Level
    Private Exhibition Control yes 1000 1+

    Ships

    This expansion declares no ships.

    Models

    This expansion declares no models.

    Scripts

    Path
    Scripts/exhibitions-ship-script.js
    "use strict";
    this.name        = "exhibitions-ship-script";
    this.author      = "Norby";
    this.copyright   = "2013 Norbert Nagy";
    this.licence     = "CC BY-NC-SA 3.0";
    this.description = "Deactivated ship script placeholder";
    
    //do nothing but must exist to attach to ships in exhibition to prevent original scripts
    //working but keep the ship.script object in exist for sure,
    //for example deterctors oxp insert variables into the ship script
    
    Scripts/exhibitions.js
    "use strict";
    this.name        = "exhibitions";
    this.author      = "Norby";
    this.copyright   = "2013 Norbert Nagy";
    this.licence     = "CC BY-NC-SA 3.0";
    this.description = "Ship exhibitions near witchpoints where player can earn Visitor Levels.";
    
    //customizable properties
    this.$ExhibitionsAllShipsInPrivateExhibition = false; //show NPC ships in Private Exhibition (cheat)
    this.$ExhibitionsLog = false; //verbose log
    this.$ExhibitionsMaxShipsInLargeExhibitions = 144; //should be square number and min. 64
    this.$ExhibitionsMinFPS = 20; //split Private Exhibition to get more than this frames per second
    this.$ExhibitionsSpace = 150; //m space between ships in exhibition matrix
    
    //internal properties, should not touch
    this.$ExhibitionsEndDate = []; //store the last valid date of exhibitions
    this.$ExhibitionsFCB = null; //FrameCallBack to fill up and rotate exhibition
    this.$ExhibitionsRoles = //for large exhibitions, without escort, hunter, thargoid, trader, player and pirate
    	["interceptor","miner","police","scavenger","shuttle","buoy","cargopod","missile"];
    this.$ExhibitionsJ = 0; //index in player ships during exhibition creation, 0 mean no active exhibition
    this.$ExhibitionsLastMenu = 0; //set marker to the lastly used exhibition menu line
    this.$ExhibitionsLastStation = null; //Private Exhibition will before this station, update when dock into a new one
    this.$ExhibitionsMenuFCB = null; //FrameCallBack for exhibitions menu
    this.$ExhibitionsMO = null; //store initial orientation in menu
    this.$ExhibitionsPerm = false; //permission to rotate ships in current exhibition
    this.$ExhibitionsPermReqSent = false; //permission request sent
    this.$ExhibitionsPosition = [null, null, null, null]; //pos, ori, vR, vU of exhibitions, will adjusted to player
    this.$ExhibitionsSalon = 0; //Salon mean Private Exhibition in short, this variable avoid recreating ships after jump
    this.$ExhibitionsSalonCost = 100; //must match with the cost of Private Exhibition Control in equipment.plist
    this.$ExhibitionsSalonDelta = 0; //count delta to determine FPS in Private Exhibition
    this.$ExhibitionsSalonDeltaC = 0; //counter to determine FPS in Private Exhibition
    this.$ExhibitionsSalonJ = 0; //index in player ships during Private Exhibition creation
    this.$ExhibitionsSalonO = null; //store Salon initial orientation
    this.$ExhibitionsSalonP = null; //store Salon central position
    this.$ExhibitionsSalonR = null; //store Salon initial vectorRight
    this.$ExhibitionsSalonRot = false; //rotating Salon
    this.$ExhibitionsSalonU = null; //store Salon initial vectorUp
    this.$ExhibitionsSalonFCB = null; //FrameCallBack to fill up and rotate Salon
    this.$ExhibitionsSalonShips = []; //store salon ships to fine if destroyed by player
    this.$ExhibitionsSalonPart = 1; //actual part in splitted salon
    this.$ExhibitionsSalonPartMax = 1; //salon splitted into how many parts
    this.$ExhibitionsShips = []; //store ships to fine if destroyed by player
    this.$ExhibitionsShipKeys = []; //store ship keys
    this.$ExhibitionsSystems = []; //store system IDs of exhibitions
    this.$ExhibitionsStartDate = []; //store start date of exhibitions
    this.$ExhibitionsType = []; //store types of introduced exhibitions
    this.$ExhibitionsTypes = [["All exhibitions","ship"], //0. line in this array mean all exhibitions
    	["Large exhibitions","many"], //special type for $ExhibitionsRoles
    	["Escort exhibitions","escort"], //type-role pairs, max. about 10 fit into the menu
    	["Hunter exhibitions","hunter"],
    	["Trader exhibitions","trader"],
    	["Pirate meetings","pirate"]];
    this.$ExhibitionsTimer = null; //wait for Gallery startUp finished
    this.$ExhibitionsTimerPerm = null; //timer for permission
    this.$ExhibitionsU = null; //store initial vectorUp
    this.$ExhibitionsVisited = []; //store of the visited exhibitions
    this.$ExhibitionsVisKey = []; //startdate+systemid of visited exhibitions (unique key)
    this.$ExhibitionsVisitor = ["a Novice Visitor.", "an Unexpected Visitor.", "a Poor Visitor.",
    	"a Below Average Visitor.", "an Average Visitor.", "an Above Average Visitor.",
    	"a Competent Visitor.", "a Trustworthy Visitor!", "an Expected Visitor!",
    	"the only Elite Visitor in this Ooniverse!"]; //string of visitor level
    
    
    //equipment events
    this.activated = function () { //attached to Private Exhibition Control equipment
    	var e = worldScripts["exhibitions"];
    	if( e.$ExhibitionsSalonRot ) {
    		e.$ExhibitionsSalonRot = false;
    		player.consoleMessage("Stop Private Exhibition rotation");
    	} else {
    		e.$ExhibitionsSalonRot = true;
    		player.consoleMessage("Start Private Exhibition rotation");
    	}
    }
    
    this.mode = function () { //attached to Private Exhibition Control equipment
    	var e = worldScripts["exhibitions"];
    	if( e.$ExhibitionsSalonPartMax > 1 ) { //step to the next part in splitted salon
    		var p = e.$Exhibitions_SalonShips();
    		if( e.$ExhibitionsSalonPart < e.$ExhibitionsSalonPartMax )
    			e.$ExhibitionsSalonPart++; //next part
    		else e.$ExhibitionsSalonPart = 1; //first part
    		
    		var s = e.$ExhibitionsSalonShips;
    		for( var i = 0; i < s.length; i++ ) {
    			if( s[i] && s[i].isValid ) s[i].remove(true); //remove other parts of salon
    		}
    		delete e.$ExhibitionsSalonShips;
    		e.$ExhibitionsSalonShips = [];
    		var sp = Math.floor( p.length / e.$ExhibitionsSalonPartMax ); ///ships in one part
    		var imin = 0;
    		if( e.$ExhibitionsSalonPart > 1 )
    			imin = Math.floor( ( e.$ExhibitionsSalonPart - 1 ) * sp );
    		var imax = p.length; //last part show last ship also
    		if( e.$ExhibitionsSalonPart < e.$ExhibitionsSalonPartMax )
    			imax = Math.floor( e.$ExhibitionsSalonPart * sp ) - 1;
    		if( e.$ExhibitionsLog )
    			log("Exhibitions", "Private Exhibition ships:"+p.length+" part:"+e.$ExhibitionsSalonPart
    				+"/"+e.$ExhibitionsSalonPartMax+" sp:"+sp+" imin:"+imin+" imax:"+imax);
    		for( var i = imin; i <= imax && i < p.length; i++ ) {
    			e.$Exhibitions_Salon2( i );  //create ships in actual part
    		}
    
    		player.consoleMessage("Private Exhibit "+e.$ExhibitionsSalonPart //+"/"+e.$ExhibitionsSalonPartMax
    			+": Showing "+(i-imin)+" of "+p.length+" ships");
    	} else player.consoleMessage("Private Exhibition show "+e.$ExhibitionsSalonShips.length+" ships");
    }
    
    //world script events
    this.startUp = function() {
    	this.$ExhibitionsJ = 0; //index in player ships during exhibition creation
    	this.$ExhibitionsLastMenu = 0; //set marker to the lastly used exhibition menu line
    	if( player.ship && player.ship.docked )
    		this.$ExhibitionsLastStation = player.ship.dockedStation;
    	this.$ExhibitionsPerm = false; //permission to rotate ships in current exhibition
    	this.$ExhibitionsPermReqSent = false; //permission request sent
    	this.$ExhibitionsSalon = missionVariables.$ExhibitionsSalon; //recreate ships after load game if ordered before
    	this.$ExhibitionsSalonDelta = 0; //count delta to determine FPS in Salon
    	this.$ExhibitionsSalonDeltaC = 0; //counter to determine FPS in Salon
    	this.$ExhibitionsSalonJ = 0; //index in player ships during Salon creation
    	this.$ExhibitionsSalonRot = false; //rotating Salon
    	this.$ExhibitionsSalonShips = []; //store ships to fine if destroyed by player
    	this.$ExhibitionsSalonPartMax = 1;
    	this.$ExhibitionsShips = []; //store ships to fine if destroyed by player
    	this.$ExhibitionsSystems = []; //store system IDs of exhibitions
    
    	var mv = missionVariables.$ExhibitionsSystems;//load advertised exhibitions from savegame
    	if( !mv ) this.$ExhibitionsSystems = []; else this.$ExhibitionsSystems = mv.split(",");
    	mv = missionVariables.$ExhibitionsStartDate;
    	if( !mv ) this.$ExhibitionsStartDate = []; else this.$ExhibitionsStartDate = mv.split(",");
    	mv = missionVariables.$ExhibitionsEndDate;
    	if( !mv ) this.$ExhibitionsEndDate = []; else this.$ExhibitionsEndDate = mv.split(",");
    	mv = missionVariables.$ExhibitionsType;
    	if( !mv ) this.$ExhibitionsType = []; else this.$ExhibitionsType = mv.split(",");
    
    	if( !(this.$ExhibitionsSystems.length > 0) ) //make new list if empty (first run)
    		this.$Exhibitions_Create(clock.days);
    
    	mv = missionVariables.$ExhibitionsVisited;
    	if( !mv ) this.$ExhibitionsVisited = []; 
    	else if( (mv+"").indexOf(",") ) this.$ExhibitionsVisited = (mv+"").split(","); //need +"" for bugfix
    	else this.$ExhibitionsVisited = [mv];
    	
    	mv = missionVariables.$ExhibitionsVisKey;
    	if( !mv ) this.$ExhibitionsVisKey = []; 
    	else if( (mv+"").indexOf(",") ) this.$ExhibitionsVisKey = (mv+"").split(","); //need +"" for bugfix
    	else this.$ExhibitionsVisKey = [mv];
    
    	if( this.$ExhibitionsLog ) { //log first list also
    		log("Exhibitions", "ExhibitionsSystems ("+this.$ExhibitionsSystems.length+"): "+this.$ExhibitionsSystems);
    		log("Exhibitions", "ExhibitionsStartDate ("+this.$ExhibitionsStartDate.length+"): "+this.$ExhibitionsStartDate);
    		log("Exhibitions", "ExhibitionsEndDate ("+this.$ExhibitionsEndDate.length+"): "+this.$ExhibitionsEndDate);
    		log("Exhibitions", "ExhibitionsType ("+this.$ExhibitionsType.length+"): "+this.$ExhibitionsType);
    		log("Exhibitions", "ExhibitionsVisited ("+this.$ExhibitionsVisited.length+"): "+this.$ExhibitionsVisited);
    		log("Exhibitions", "ExhibitionsVisKey ("+this.$ExhibitionsVisKey.length+"): "+this.$ExhibitionsVisKey);
    	}
    	this.$ExhibitionsTimer = new Timer(this, this.$Exhibitions_Timed, 1);//need wait for gallery startUp finished
    
    	if( player.ship && player.ship.docked ) this.shipDockedWithStation( player.ship.dockedStation ); //set interface
    	else if( system ) this.shipDockedWithStation( system.mainStation );//fallback	
    }
    
    this.dayChanged = function(newday) {
    	if(!system || system.isInterstellarSpace) return;
    	this.$Exhibitions_Create(newday); //make new ones
    	var openex = false;
    	if( this.$ExhibitionsJ > 0 ) openex = true;
    	if( this.$Exhibitions_CheckSystem() ) //create ships if there are an exhibition in this system today
    		if( !openex ) player.consoleMessage("Exhibition open at witchpoint!",5);
    	
    	for( var x = 0; x < this.$ExhibitionsEndDate.length; x++) { //remove expired items from all array
    		if( this.$ExhibitionsEndDate[ x ] < newday ) {
    			this.$ExhibitionsSystems.splice(x,1);
    			this.$ExhibitionsStartDate.splice(x,1);
    			this.$ExhibitionsEndDate.splice(x,1);
    			this.$ExhibitionsType.splice(x,1);
    			x--;
    		}
    	}
    }
    
    this.playerBoughtEquipment = function(equipmentKey) {
    	if(equipmentKey === ("EQ_SALON_CONTROL")) {
    		this.$ExhibitionsSalon = 1; //recreate ships after load game
    		clock.addSeconds(86400*(1+Math.random()));
    		this.$Exhibitions_Salon();
    	}
    }
    
    this.playerEnteredNewGalaxy = function(galaxyNumber) { //make new exhibitions
    	this.$ExhibitionsEndDate = []; //store the last valid date of exhibitions
    	this.$ExhibitionsSystems = []; //store system IDs of exhibitions
    	this.$ExhibitionsStartDate = []; //store start date of exhibitions
    	this.$ExhibitionsType = []; //store types of introduced exhibitions
    	this.$Exhibitions_Create(clock.days); //recreate exhibitions
    }
    
    this.playerWillSaveGame = function(message) {
    	missionVariables.$ExhibitionsVisited = this.$ExhibitionsVisited;
    	missionVariables.$ExhibitionsVisKey = this.$ExhibitionsVisKey;
    	missionVariables.$ExhibitionsSalon = this.$ExhibitionsSalon;
    	missionVariables.$ExhibitionsSystems = this.$ExhibitionsSystems;
    	missionVariables.$ExhibitionsStartDate = this.$ExhibitionsStartDate;
    	missionVariables.$ExhibitionsEndDate = this.$ExhibitionsEndDate;
    	missionVariables.$ExhibitionsType = this.$ExhibitionsType;
    }
    
    this.shipDockedWithStation = function(station)
    {
    	if( this.$ExhibitionsLastStation != station ) {
    		this.$ExhibitionsLastStation = station; //save lastly docked station to check if changed
    		this.$Exhibitions_SalonRemove();//to can buy new salon placed before this new station
    	}
    	if( isValidFrameCallback( this.$ExhibitionsFCB ) )
    		removeFrameCallback( this.$ExhibitionsFCB );
    		
    	this.$ExhibitionsPerm = false; //permission to rotate ships in current exhibition
    	this.$ExhibitionsPermReqSent = false; //permission request sent
    	
    	var len = 0;
    	if( this.$ExhibitionsVisited ) len = this.$ExhibitionsVisited.length;
    	var exs = len;
    	var exv = "";
    	if( len > 0 ) exv += " - you are "+(this.$ExhibitionsVisitor[ this.$Exhibitions_VisitorLevel() ]);
    	
    	station.setInterface("Exhibitions",{
    		title: "Exhibitions ("+exs+" visited)"+exv,
    		category: "Ship Systems",
    		summary: "Check incoming ship meetings to extend your Gallery with new ships and increase your Visitor Level.",
    		callback: this.$Exhibitions_Interface.bind(this)
    		});
    }
    
    this.shipKilledOther = function(whom, damageType) {
    	if( this.$ExhibitionsShips && this.$ExhibitionsShips.indexOf(whom) > -1 ) {
    		player.consoleMessage("GalCop fined you "+formatCredits(10000, false, true)
    			+" for destroyng an Exhibition ship", 10);
    		player.credits -= 100000;
    		player.score--;
    	}
    	if( this.$ExhibitionsSalonShips && this.$ExhibitionsSalonShips.indexOf(whom) > -1 ) {
    		player.consoleMessage("GalCop fined you "+formatCredits(10000, false, true)
    			+" for destroying a Private Exhibition ship", 10);
    		player.credits -= 10000;
    		player.score--;
    	}
    }
    
    this.shipTargetAcquired = function(target) { //flag
    	var e = worldScripts["exhibitions"];
    	if( e.$ExhibitionsJ > 0 && e.$ExhibitionsShips && e.$ExhibitionsShips.indexOf(target) > -1 ) {
    		var si = e.$ExhibitionsSystems.indexOf(system.ID+"");//need +"" for bugfix
    		if( si > -1 ) {
    		    var key = e.$ExhibitionsStartDate[si]+""+galaxyNumber+""+system.ID; //unique for each exhibitions
    		    if( e.$ExhibitionsVisKey.indexOf(key) == -1 ) { //new visit
    			var rep = e.$Exhibitions_VisitorLevel();
    			if( !e.$ExhibitionsVisited ) this.$ExhibitionsVisited = []; //for sure
    			if( !e.$ExhibitionsVisKey ) this.$ExhibitionsVisKey = []; //for sure
    			var len = e.$ExhibitionsVisited.length;
    			e.$ExhibitionsVisited.push(clock.days+": "+system.name+" in "+(galaxyNumber+1)+" galaxy");
    			e.$ExhibitionsVisKey.push(key);
    			var msg = "You visited your "+(len+1)+" exhibition.";
    			player.consoleMessage(msg, 10);
    			if( e.$ExhibitionsLog ) log("Exhibitions", msg+" "+key);
    			var rep2 = e.$Exhibitions_VisitorLevel();
    			if( rep2 > rep ) {
    				msg = "Congratulations. You are "+e.$ExhibitionsVisitor[ rep2 ];
    				player.commsMessage(msg, 10);
    				if( e.$ExhibitionsLog ) log("Exhibitions", msg);
    			}
    		    }
    		}
    		//request permission for rotate ships
    		if( !this.$ExhibitionsPerm ) {
    			if( !this.$ExhibitionsTimerPerm && !this.$ExhibitionsPermReqSent ) {
    				this.$ExhibitionsPermReqSent = true;
    				player.consoleMessage("Permission requested to rotate ships in this exhibition", 10);
    				var t = Math.ceil(Math.random() * 30); //max. 30 seconds
    				this.$ExhibitionsTimerPerm = new Timer(this, this.$Exhibitions_TimedPerm, t);
    			} else {
    				player.consoleMessage("Please wait. You will get permission soon.", 5);
    			}
    		}
    	}
    }
    
    this.shipWillEnterWitchspace = function() {
    	delete this.$ExhibitionsShips;
    	this.$ExhibitionsShips = [];
    	if( isValidFrameCallback( this.$ExhibitionsFCB ) )
    		removeFrameCallback( this.$ExhibitionsFCB );//stop rotate
    	
    	this.$ExhibitionsLastStation = null; //clear last station
    	this.$ExhibitionsSalon = false; //do not recreate ships in salon after hyperjump
    	this.$ExhibitionsSalonDeltaC = 0; //count delta to determine FPS in Salon
    	delete this.$ExhibitionsSalonShips;
    	this.$ExhibitionsSalonShips = [];
    	this.$ExhibitionsSalonPartMax = 1;
    	if( isValidFrameCallback( this.$ExhibitionsSalonFCB ) )
    		removeFrameCallback( this.$ExhibitionsSalonFCB );//stop rotate
    	player.ship.removeEquipment("EQ_SALON_CONTROL");
    }
    
    this.shipWillExitWitchspace = function() { //use this event due to shipExitedWitchspace is not working in v1.77
    	this.$ExhibitionsPerm = false; //permission to rotate ships in current exhibition
    	this.$ExhibitionsPermReqSent = false;
    	if(system && !system.isInterstellarSpace) this.$Exhibitions_CheckSystem(); //create ships after hyperjump
    }
    
    this.shipWillLaunchFromStation = function() {
    	player.ship.hudHidden = false;
    	this.$ExhibitionsPerm = false; //permission to rotate ships in current exhibition
    	this.$ExhibitionsPermReqSent = false; //permission request sent
    	if( this.$ExhibitionsJ > 0 ) {
    		if( !isValidFrameCallback( this.$ExhibitionsFCB ) )
    			this.$ExhibitionsFCB = addFrameCallback( this.$Exhibitions_FCB ); //add ships and rotate
    	} else this.$Exhibitions_CheckSystem(); //recreate ships after load game
    }
    
    
    //Exhibitions methods
    this.$Exhibitions_CheckSystem = function() {
    	var e = worldScripts["exhibitions"];
    	var x = e.$ExhibitionsSystems.indexOf( system.ID+"" );//need +"" for bugfix
    	if( e.$ExhibitionsLog ) log("Exhibitions", " CheckSystem:"+system.ID+" "+x+" "+e.$ExhibitionsJ+" "
    		+clock.days+" "+e.$ExhibitionsStartDate[ x ]+"-"+e.$ExhibitionsEndDate[ x ]+" s"+e.$ExhibitionsSystems[x]);
    	if( x != -1 && clock.days >= e.$ExhibitionsStartDate[ x ]
    		&& clock.days <= e.$ExhibitionsEndDate[ x ] ) {
    		if( e.$ExhibitionsJ == 0 ) e.$Exhibitions_Show( x ); //only if not shown before
    		return( true );
    	} else e.$Exhibitions_Remove(); //remove if last day elapsed
    	return( false ); //no exhibition
    }
    
    this.$Exhibitions_Create = function( days ) {
    	if(!system || system.isInterstellarSpace) return;
    	
    	var tlen = this.$ExhibitionsTypes.length; 
    	var dlen = this.$ExhibitionsStartDate.length; 
    	var maxstartdate = 0;
    	if( dlen > 0 ) maxstartdate = this.$ExhibitionsStartDate.sort()[dlen-1];
    	if( maxstartdate == 0 ) {
    		maxstartdate = 2084002;
    		if( galaxyNumber == 0 ) {
    			this.$ExhibitionsSystems.push("55"); //long exhibition in Leesti for demonstration, need in "" for bugfix
    			this.$ExhibitionsStartDate.push( 2084001 );
    			this.$ExhibitionsEndDate.push( 2084060 );
    			this.$ExhibitionsType.push( 1 ); //large exhibition
    		}
    	}
    	var maxdate = days + 100; //new exhibitions introduced 100 days before
    	if( !( dlen > 0 && maxstartdate > maxdate ) ) {
    		for( var i = maxstartdate; i < maxdate; i++ ) {
    			var r = system.scrambledPseudoRandomNumber(i);
    			var t = Math.floor(r * (tlen)); //one open in every day, equal odds for each type
    			var sy = Math.floor(r * 256) + ""; //need +"" for bugfix, 1/tlen odds for no opening
    			if( t > 0 && this.$ExhibitionsSystems.indexOf(sy) == -1 ) {
    				this.$ExhibitionsSystems.push( sy );
    				this.$ExhibitionsStartDate.push( i );
    				this.$ExhibitionsEndDate.push( i + 1 + Math.floor(r * 60) );//open max. 2 months
    				this.$ExhibitionsType.push( t );
    			}
    		}
    	}
    }
    
    this.$Exhibitions_FCB = function( delta ) {
    	var e = worldScripts["exhibitions"];
    	var p = e.$ExhibitionsShipKeys;
    	if( e.$ExhibitionsJ < p.length ) {
    		e.$Exhibitions_Show2( e.$ExhibitionsJ++ ); //spawn Salon ships in cycle
    		if( e.$ExhibitionsJ >= p.length ) {
    			var x = e.$ExhibitionsSystems.indexOf( system.ID+"" );//need +"" for bugfix
    			var type = e.$ExhibitionsTypes[ e.$ExhibitionsType[ x ] ][1]; //escort, hunter, trader, pirate
    			player.consoleMessage("Exhibition of "+type+" ships is open at witchpoint", 10);
    		}
    	} else {
    		if( e.$ExhibitionsPerm ) {
    			var enc = worldScripts["gallery"].$GalleryEncounters;
    			var s = e.$ExhibitionsShips;
    			var m = null;
    			for( var i = 0; i < s.length; i++ ) {
    				m = s[i];
    				if( enc.indexOf(m.dataKey) == -1 && m.orientation  //rotate new ship
    					|| player.ship && player.ship.target == s[i] ) {//or current target
    					m.orientation = m.orientation.rotateY( delta ).rotateZ( delta );
    				}	
    			}
    		}
    	}
    }
    
    this.$Exhibitions_Interface = function() {
    	player.ship.hudHidden = true;//to shift down the menu
    	this.$ExhibitionsLastMenu = 0;
    	var g = worldScripts["gallery"];
    	if( g && isValidFrameCallback( g.$GalleryFCB ) ) removeFrameCallback( g.$GalleryFCB ); //need for bugfix
    	if( !isValidFrameCallback( this.$ExhibitionsMenuFCB ) )
    		this.$ExhibitionsMenuFCB = addFrameCallback( this.$Exhibitions_MenuFCB ); //will call ex.menu
    }
    
    this.$Exhibitions_Menu = function() { //exhibitions menu
    	var e = worldScripts["exhibitions"];
    	var g = worldScripts["gallery"];
    	var c = [];
    	if(e.$ExhibitionsLog) log("Exhibitions", "Visitor:"+e.$Exhibitions_VisitorLevel()
    		+" "+e.$ExhibitionsVisitor[e.$Exhibitions_VisitorLevel()]);
    
    	var len = 0;
    	if( e.$ExhibitionsVisited ) len = e.$ExhibitionsVisited.length;
    	var exs = "You have not visited any exhibitions yet.";
    	if( len > 0 ) {
    		exs = "You visited "+len+" exhibition";
    		if( len > 1 ) exs += "s";
    		exs += ". You are "+(e.$ExhibitionsVisitor[ e.$Exhibitions_VisitorLevel() ]);
    	}
    	
    	var msg = [];
    
    	for( var i = 0; i < e.$ExhibitionsTypes.length; i++ ) { //make menu for each type, start from 1 (not from 0!)
    		c.push(e.$ExhibitionsTypes[i][0]);
    		var ex = "";
    		var k = 0;
    		for( var j = 0; j < e.$ExhibitionsType.length && k < 23 - e.$ExhibitionsTypes.length ; j++ ) {
    			if( i == 0 || i == e.$ExhibitionsType[j] ) {//list one type into type specific menus
    				k++;
    				ex += e.$ExhibitionsStartDate[j] + " - " + e.$ExhibitionsEndDate[j] + "  "
    					+ System.infoForSystem(galaxyNumber, e.$ExhibitionsSystems[j]).name;
    				if( i == 0 ) ex += " - " + e.$ExhibitionsTypes[e.$ExhibitionsType[j]][1] + " ships\n";
    				else ex += "\n";
    					
    			}
    		}
    		msg.push(ex);
    	}
    	var pur = "purchasable ships";
    	if( e.$ExhibitionsAllShipsInPrivateExhibition ) pur = "all ships";
    	c.push("Private Exhibition of "+pur);
    	msg.push("You need "+formatCredits(e.$ExhibitionsSalonCost, false, true)+" to order your private exhibition.");
    	c.push("Gallery of encounters"); 
    	msg.push("Go to Gallery");
    	c.push("Exit"); 
    	msg.push("Bye");
    
    	var startori = 0;
    	var ti = "Exhibitions";
    	if( e.$ExhibitionsLastMenu > 0 ) ti = c[e.$ExhibitionsLastMenu-10]+" "+clock.days+"-";
    	else { e.$ExhibitionsMO = null; startori = 1; }
    	msg[-10] = [ exs+"\n\nYou can choose exhibitions below. You will find them near witchpoints."
    			+" Target at least one ship within to increase your Visitor Level."
    			+" If you are lucky, you will find new ship types. You can add these to your Gallery by targeting them."
    			+"\n\nIn the Private Exhibition menu, you can order an exhibition of "+pur+"."
    			+"\nYou can go to the Gallery or exit when finished."];
    	var ms = msg[e.$ExhibitionsLastMenu-10];
    	if( e.$ExhibitionsLastMenu == 0 ) e.$ExhibitionsLastMenu = 10;
    	var ch = {	"_10" : c[0],
    		    	"_11" : c[1],
    		    	"_12" : c[2],
    		    	"_13" : c[3],
    		    	"_14" : c[4],
    		    	"_15" : c[5],
    		    	"_16" : c[6],
    		    	"_17" : c[7],
    		    	"_18" : c[8],
    		    	"_19" : c[9],
    		    	"_20" : c[10],
    		    	"_21" : c[11],
    		    	"_22" : c[12],
    		    	"_23" : c[13],
    		    	"_24" : c[14],
    		    	"_25" : c[15],
    		    	"_26" : c[16],
    		    	"_27" : c[17],
    		    	"_28" : c[18],
    		    	"_29" : c[19],
    		    	"_30" : c[20],
    		    	"_31" : c[21],
    		    	"_32" : c[22],
    		    	"_33" : c[23],
    		    	"_34" : c[24],
    		    	"_35" : c[25]
    		};
    	if( isValidFrameCallback( e.$ExhibitionsMenuFCB ) ) removeFrameCallback( e.$ExhibitionsMenuFCB );
    	mission.runScreen({
    		title: ti,
    		message: ms,
    		model: "["+player.ship.dataKey+"]",
    		modelPersonality: 0,
    		spinModel:false,
    		background: "gallery_bg.png",
    		initialChoicesKey: "_"+e.$ExhibitionsLastMenu,
    		choices: ch
    	},function(choice) {
    		var ch = 10;
    		if( choice ) ch = choice.substr(1); //cut starting "_"
    		e.$ExhibitionsLastMenu = 1 * ch;
    		if(e.$ExhibitionsLog) log("Exhibitions", "LastMenu: "+e.$ExhibitionsLastMenu);
    		if( e.$ExhibitionsLastMenu - 10 < c.length - 3 ) {  //exhibition types, draw menu again
    			if( !isValidFrameCallback( e.$ExhibitionsMenuFCB ) )
    				e.$ExhibitionsMenuFCB = addFrameCallback( e.$Exhibitions_MenuFCB );
    		} else switch( e.$ExhibitionsLastMenu - 10 ) {
    			case (c.length - 3) : //Salon
    				e.$Exhibitions_SalonMenu();
    				break;
    			case (c.length - 2) : //Go to Gallery
    				g.$Gallery_Interface2( g.$GalleryShowAll ); //show from the lastly viewed ship
    				break;
    			case (c.length - 1) : //Exit
    				break;
    		}
    	});
    	var m = mission.displayModel;
    	if( m ) {
    		if( !e.$ExhibitionsMO ) e.$ExhibitionsMO = m.orientation; //save orientation in opening screen
    		var w = oolite.gameSettings.gameWindow; //correction if not in widescreen
    		var wide = Math.max( 0.01, ( w.width / w.height ) / g.$GalleryDefaultZoom );
    		m.position = Vector3D(m.position.x, m.position.y, m.position.z * wide * 0.9); //zoom more closer
    		m.orientation = e.$ExhibitionsMO.rotateZ( -Math.PI/2 ).rotateX( -Math.PI/2 )
    			.rotateY( - Math.PI/4 * ( e.$ExhibitionsLastMenu + 5 - startori ) );
    	}
    }
    
    this.$Exhibitions_MenuFCB = function( delta ) { //FrameCallBack for exhibitions menu
    	var e = worldScripts["exhibitions"];
    	if( isValidFrameCallback( e.$ExhibitionsMenuFCB ) ) removeFrameCallback( e.$ExhibitionsMenuFCB );
    	e.$Exhibitions_Menu();
    }
    
    
    this.$Exhibitions_Remove = function() { //remove ships
    	this.$ExhibitionsJ = 0; //index in ships during creation
    	var s = this.$ExhibitionsShips;
    	for( var i = 0; i < s.length; i++ ) {
    		if( s[i] && s[i].isValid ) s[i].remove(true);
    	}
    	this.$ExhibitionsPosition = [null, null, null, null];
    	if( isValidFrameCallback( this.$ExhibitionsFCB ) )
    		removeFrameCallback( this.$ExhibitionsFCB );
    }
    
    
    this.$Exhibitions_Salon = function() {
    	if(!system || system.isInterstellarSpace) return;
    
    	this.$ExhibitionsSalonJ = 0; //index in player ships during Salon creation
    	var st = player.ship; //at load game or in-fly debug
    	if( player.ship.docked ) st = player.ship.dockedStation;
    	this.$ExhibitionsSalonO = st.orientation;
    	this.$ExhibitionsSalonP = st.position.add(st.vectorForward.multiply( 10300 )); //Salon central position
    	this.$ExhibitionsSalonR = st.vectorRight; //store initial vectorRight
    	this.$ExhibitionsSalonU = st.vectorUp; //store initial vectorUp
    	player.ship.awardEquipment("EQ_SALON_CONTROL");//need after load game
    	if( !isValidFrameCallback( this.$ExhibitionsSalonFCB ) )
    		this.$ExhibitionsSalonFCB = addFrameCallback( this.$Exhibitions_SalonFCB );
    }
    
    this.$Exhibitions_Salon2 = function( j ) {
    	if(!system || system.isInterstellarSpace) return;
    
    	var e = worldScripts["exhibitions"];
    	var g = worldScripts["gallery"];
    	var p = e.$Exhibitions_SalonShips();
    	var space = e.$ExhibitionsSpace;//between ships
    	var st = player.ship; //at load game or in-fly debug
    	if( player.ship.docked ) st = player.ship.dockedStation;
    	var sq = Math.ceil(Math.sqrt(p.length));
    //	for( var j = 0; j < p.length; j++ ) { //SalonFCB make this cycle to allow fast forwarding game clock parallelly
    		var key = p[j];
    		var row = Math.floor(j/sq);
    		var pos = e.$ExhibitionsSalonP.add( e.$ExhibitionsSalonR.multiply( space * ( j - sq * row - sq / 2 + 0.5 ) ) )
    				.add( e.$ExhibitionsSalonU.multiply( space * ( row - sq / 2 + 0.5 ) ) );//square align
    		var ships = system.addShips("["+key+"]", 1, pos, 5);
    		if( ( !ships || !ships[0] ) && key.indexOf("-player") > -1 ) { //handle unsuccesful ship creation
    			key = key.slice(0, key.indexOf("-player")); //remove -player
    			ships = system.addShips("["+key+"]", 1, pos, 5);
    			if( ships ) e.$Exhibitions_SalonAdd( ships[0], key, st, e, space, j );
    		} else if( ships && ships[0] ) e.$Exhibitions_SalonAdd( ships[0], key, st, e, space, j );
    //	}
    }
    
    this.$Exhibitions_SalonAdd = function( ship, key, st, e, space, j ) {
    	if( !ship ) { //handle unsuccesful ship creation
    		if(e.$ExhibitionsLog) log("Exhibitions", "Failed addShips ["+key+"] to Private Exhibition.");
    	} else {
    		if(e.$ExhibitionsLog) log("Exhibitions", "["+key+"] added to Private Exhibition.");
    		if( ship.escorts && ship.escorts.length > 0 ) 
    			for( var i = 0; i < ship.escorts.length; i++ ) ship.escorts[i].remove(true);
    		ship.beaconCode = null; //fix for escorted mothership in escort contracts
    		if(ship.beaconLabel) ship.beaconLabel = "";
    		ship.bounty = 0;
    		ship.displayName = "[Private Exhibition] " + ship.displayName;
    		ship.setAI("nullAI.plist");
    		ship.setScript("exhibitions-ship-script.js"); //must deactivate the original script
    		ship.target = null;
    		ship.clearDefenseTargets();
    //		ship.scanClass = "CLASS_CARGO";
    		if( st ) ship.orientation = e.$ExhibitionsSalonO.rotateX(3*Math.PI/2);
    		var cr = ship.collisionRadius; //shift out large ships from the square plane
    		if( cr > space ) {
    			var c2 = 1; //prevent collision of two neighbour baseship
    			if( Math.floor( j / 2 ) == j / 2 ) c2 = -1;
    			var mul =  c2 * (cr + 4 * space);
    			if(e.$ExhibitionsLog) log("Exhibitions", ship.displayName+" too large, shifted out "+j+" "+c2+" "+mul);
    			ship.position = ship.position.add( st.vectorForward.multiply( mul ) );
    		}
    		e.$ExhibitionsSalonShips.push(ship);
    	}
    }
    
    this.$Exhibitions_SalonFCB = function( delta ) {
    	var e = worldScripts["exhibitions"];
    	var p = e.$Exhibitions_SalonShips();
    	if( e.$ExhibitionsSalonJ < p.length ) {
    		if( e.$ExhibitionsSalonJ == 0 ) { //add active police as defenders of salon
    			var role = "police";
    			if( 6 <=  system.techLevel ) role = "interceptor";
    			var eg = system.addShips(role, 1+Math.floor(system.government/2), e.$ExhibitionsSalonP, 2000);
    			if( eg ) for( var i = 0; i < eg.length; i++ ) {
    				if( eg[i] && eg[i].isValid ) eg[i].displayName = "[Private Exhibition Guard] "+eg[i].displayName;
    			}
    		}
    		e.$Exhibitions_Salon2( e.$ExhibitionsSalonJ++ ); //spawn Salon ships in cycle
    	} else {
    		if( e.$ExhibitionsSalonDeltaC > 0 ) {
    			if(e.$ExhibitionsLog) log("Exhibitions", "C:"+e.$ExhibitionsSalonDeltaC+" delta:"+delta
    				+" deltasum:"+e.$ExhibitionsSalonDelta);
    			e.$ExhibitionsSalonDeltaC++;
    			e.$ExhibitionsSalonDelta += delta;
    		}
    		if( e.$ExhibitionsSalonJ == p.length ) {
    			e.$ExhibitionsSalonJ++; //send message once only
    			player.consoleMessage(p.length+" ships arrived. Take off to view Private Exhibition.",10);
    			e.$ExhibitionsSalonRot = false; //rotating Salon
    		}
    		if( e.$ExhibitionsSalonJ > p.length && e.$ExhibitionsSalonDeltaC == 0
    			&& player.ship && !player.ship.docked ) { //wait for undock
    			e.$ExhibitionsSalonDelta = delta; //start count delta to determine FPS for Salon split
    			e.$ExhibitionsSalonDeltaC = 1;
    		}
    		if( e.$ExhibitionsSalonPartMax == 1 && e.$ExhibitionsSalonDelta > 2 ) { //after 2 seconds of measurement
    			var fps = e.$ExhibitionsSalonDeltaC / e.$ExhibitionsSalonDelta; ///frames per second
    			e.$ExhibitionsSalonDelta = delta;
    //			e.$ExhibitionsSalonDeltaC = 1;//restart measurement
    			e.$ExhibitionsSalonDeltaC = 0;//stop measurement
    			var split = "";
    			if( fps < e.$ExhibitionsMinFPS //too few FPS
    				&& player.ship && player.ship.position && e.$ExhibitionsSalonP
    				&& player.ship.position.distanceTo(e.$ExhibitionsSalonP) < 25000 ) { //not so far
    				e.$ExhibitionsSalonPartMax = Math.ceil( e.$ExhibitionsMinFPS / fps ); ///round up
    				var sp = Math.floor( p.length / e.$ExhibitionsSalonPartMax ); ///ships in one part
    				var sq = Math.ceil(Math.sqrt(p.length));
    				if( sp < sq ) //at least one row of ships in one part
    					e.$ExhibitionsSalonPartMax = Math.ceil( p.length / sq ); ///
    				e.$ExhibitionsSalonPart = 0; //will show the first part
    				split = ", split into "+e.$ExhibitionsSalonPartMax+" parts";
    				var button = "b";
    				if( oolite.gameSettings.keyConfig ) //from Oolite v.1.77.1
    					button = String.fromCharCode( oolite.gameSettings.keyConfig.key_mode_equipment );
    				player.consoleMessage("Private Exhibition split into "+e.$ExhibitionsSalonPartMax
    					+" parts. Prime Private Exhibition Control and press '"+button+"' to show next part", 10);
    				e.mode(); //hide parts of salon
    			}
    			if(e.$ExhibitionsLog)
    				log("Exhibitions", "Private Exhibition "+p.length+" ships, "+fps+" FPS"+split);
    		}
    
    		var s = e.$ExhibitionsSalonShips;
    		var m = null;
    		for( var i = 0; i < s.length; i++ ) {
    			m = s[i];
    			if( m && m.isValid && m.orientation && player.ship && //rotate all ships in salon
    				( e.$ExhibitionsSalonRot && player.ship.target != s[i] //or current target only
    				|| !e.$ExhibitionsSalonRot && player.ship.target == s[i] ) )
    				m.orientation = m.orientation.rotateZ( delta/1.5 );
    		}
    	}
    }
    
    this.$Exhibitions_SalonMenu = function() {
    	var e = worldScripts["exhibitions"];
    	var g = worldScripts["gallery"];
    	var pur = "purchasable ships";
    	if( e.$ExhibitionsAllShipsInPrivateExhibition ) pur = "all ships";
    
    	var msg = "You can order a Private Exhibition of "+pur
    		+" before this station for "+formatCredits(e.$ExhibitionsSalonCost, false, true)
    		+", but you need to allow 1-2 days for this to be set up. If you have an existing Private Exhibition,"
    		+" it will be renewed with this purchase."
    		+"\n\nYou will get a Private Exhibition Control primable equipment to start and stop rotation of ships"
    		+" in your Private Exhibition, or you can start and stop rotation of a single ship by targeting it."
    		+" Do not destroy ships in your Private Exhibition. If you do, you will be fined."
    		+" Private Exhibition will be closed if you dock with another station or jump into another system.";
    	mission.runScreen({
    		title: "Order a Private Exhibition",
    		message: msg,
    		model: "["+player.ship.dataKey+"]",
    		modelPersonality: 0,
    		spinModel:false,
    		background: "gallery_bg.png",
    		choices: {"_0":"No thanks","_1":"OK. I will pay and wait for ships!"}
    	},function(choice) {
    		e.$ExhibitionsLastMenu = 0; //will display initial message
    		switch(choice) {
    			case "_0":
    				if( !isValidFrameCallback( e.$ExhibitionsMenuFCB ) )
    					e.$ExhibitionsMenuFCB = addFrameCallback( e.$Exhibitions_MenuFCB );
    				break;
    			case "_1":
    				var saloncost = e.$ExhibitionsSalonCost; //must match with the cost in equipment.plist
    				if( player.ship.equipmentStatus("EQ_SALON_CONTROL") == "EQUIPMENT_OK" ) {
    					player.consoleMessage("Old Private Exhibition closed.",3);
    					e.$Exhibitions_SalonRemove();
    				}
    				if( player.credits >= saloncost ) {
    					player.credits -= saloncost;
    					player.ship.awardEquipment("EQ_SALON_CONTROL");
    					e.playerBoughtEquipment("EQ_SALON_CONTROL"); //do as if just bought one
    					//no FCBE setup so will exit from ex.menu
    				} else {
    					//display not enough money message
    					mission.runScreen({
    						title: "Order a Private Exhibition",
    						message: "You need "+formatCredits(e.$ExhibitionsSalonCost, false, true)
    							+" to order your Private Exhibition.",
    						model: "["+player.ship.dataKey+"]",
    						modelPersonality: 0,
    						spinModel:false,
    						background: "gallery_bg.png",
    						choices: {"_0":"Ok"}
    						},function(choice) {
    							//back to the exhibitions menu
    							if( !isValidFrameCallback( e.$ExhibitionsMenuFCB ) )
    							    e.$ExhibitionsMenuFCB = addFrameCallback(e.$Exhibitions_MenuFCB);
    						});
    				}
    				break;
    		}
    	});
    	var w = oolite.gameSettings.gameWindow;
    	var wide = Math.max( 0.01, ( w.width / w.height ) / g.$GalleryDefaultZoom ); //correction if not in widescreen
    	var m = mission.displayModel;
    	if( m ) {
    		m.position = Vector3D(m.position.x, m.position.y, m.position.z * wide * 0.9); //zoom more closer
    		m.orientation = m.orientation.rotateZ( Math.PI/2 );
    	}
    }
    
    this.$Exhibitions_SalonShips = function() {
    	var g = worldScripts["gallery"];
    	if( worldScripts["exhibitions"].$ExhibitionsAllShipsInPrivateExhibition ) 
    		return( g.$GalleryAllObjects ); //cheat: all existing ships
    	else return( g.$GalleryKeysForRole[ 1 ] );  //player ships
    }
    
    this.$Exhibitions_SalonRemove = function() {
    	this.$ExhibitionsSalon = 0; //do not recreate ships after docked into a new station
    	this.$ExhibitionsSalonDeltaC = 0; //count delta to determine FPS in Salon
    	this.$ExhibitionsSalonJ = 0; //index in player ships during Salon creation
    	this.$ExhibitionsSalonO = null; //store Salon initial orientation
    	this.$ExhibitionsSalonP = null; //store Salon central position
    	this.$ExhibitionsSalonR = null; //store Salon initial vectorRight
    	this.$ExhibitionsSalonRot = false; //rotating Salon
    	this.$ExhibitionsSalonU = null; //store Salon initial vectorUp
    	var s = this.$ExhibitionsSalonShips;
    	for( var i = 0; i < s.length; i++ ) {
    		if( s[i] && s[i].isValid ) s[i].remove(true); //remove previous salon
    	}
    	delete this.$ExhibitionsSalonShips;
    	this.$ExhibitionsSalonShips = [];
    	this.$ExhibitionsSalonPartMax = 1;
    	if( isValidFrameCallback( this.$ExhibitionsSalonFCB ) )
    		removeFrameCallback( this.$ExhibitionsSalonFCB );
    	player.ship.removeEquipment("EQ_SALON_CONTROL"); //to can buy again
    }
    
    this.$Exhibitions_Show = function( x ) { //create ships
    	if(!system || system.isInterstellarSpace) return;
    
    	var e = worldScripts["exhibitions"];
    	var type = e.$ExhibitionsTypes[ e.$ExhibitionsType[ x ] ][1]; //escort, hunter, trader, pirate
    	var enc = worldScripts["gallery"].$GalleryEncounters;
    	e.$ExhibitionsShips = [];
    	e.$ExhibitionsShipKeys = [];
    
    	var sdk = [];
    	if (0 < oolite.compareVersion("1.79")) { //slow and limited method, use before Oolite v1.79
    		if( type == "many" ) { //from $ExhibitionsRoles
    			for( var i = 0; i < e.$ExhibitionsRoles.length; i++ ) {
    				var ships = system.addShips( e.$ExhibitionsRoles[i], //sum of 64 ship created
    					Math.ceil( 64 / e.$ExhibitionsRoles.length ), [0,0,0], 1000000 );
    				if( ships ) for( var i = 0; i < ships.length; i++ )
    					if( ships[i] && ships[i].isValid && sdk.indexOf(ships[i].dataKey) == -1 )
    						sdk.push( ships[i].dataKey );
    			}
    			if(e.$ExhibitionsLog) log("Exhibitions", type+": "+sdk);
    			sdk = sdk.concat(enc); //player and encountered ships at the end, show if anothers is not enough
    		} else {
    			var ships = system.addShips(type, 64, [0,0,0], 1000000);
    			if( ships ) for( var i = 0; i < ships.length; i++ )
    				if( ships[i] && ships[i].isValid && sdk.indexOf(ships[i].dataKey) == -1 )
    					sdk.push( ships[i].dataKey );
    			if(e.$ExhibitionsLog) log("Exhibitions", type+": "+sdk);
    		}
    		if( !sdk || sdk.length < 1 ) { //fallback to player ships
    			var ships = system.addShips("player", 64, [0,0,0], 1000000);
    			if( ships ) for( var i = 0; i < ships.length; i++ )
    				if( ships[i] && ships[i].isValid && sdk.indexOf(ships[i].dataKey) == -1 )
    					sdk.push( ships[i].dataKey );
    			if(e.$ExhibitionsLog) log("Exhibitions", "player: "+sdk);
    		}
    	} else { // Oolite v1.79 or later
    		if( type == "many" ) { //from $ExhibitionsRoles
    			for( var i = 0; i < e.$ExhibitionsRoles.length; i++ ) {
    				sdk = sdk.concat(Ship.keysForRole( e.$ExhibitionsRoles[i] ));
    			}
    			if(e.$ExhibitionsLog) log("Exhibitions", type+": "+sdk);
    			sdk = sdk.concat(enc); //player and encountered ships at the end, show if anothers is not enough
    		} else {
    			sdk = Ship.keysForRole( type );
    			if(e.$ExhibitionsLog) log("Exhibitions", type+": "+sdk);
    		}
    		if( !sdk || sdk.length < 1 ) {
    			sdk = Ship.keysForRole( "player" );//fallback to player ships
    			if(e.$ExhibitionsLog) log("Exhibitions", "player: "+sdk);
    		}
    	}
    	if( (!sdk || sdk.length < 1 ) && player.ship ) sdk = player.ship.dataKey;//last resort
    	if( !sdk || sdk.length < 1 ) { //should not happed
    		log("Exhibitions", "$Exhibitions_Show() failed. Can not find any dataKey to show");
    		return; //give up and exit to prevent errors
    	}
    
    	var maxnewship = 2; //a few new ship added into every exhibition only else reserves run out too early
    	var shipno = 12 + 52 * system.scrambledPseudoRandomNumber(e.$ExhibitionsType[ x ]); //max. 64 = 8*8
    	if( type == "many" ) shipno = Math.max(64, system.pseudoRandomNumber * e.$ExhibitionsMaxShipsInLargeExhibitions);
    	for( var i = 0; e.$ExhibitionsShipKeys.length < shipno && i < sdk.length; i++ ) {
    		var key = sdk[i];
    		if( ( key && enc.indexOf(key) != -1 || maxnewship-- > 0 ) 
    			&& key.indexOf("station") == -1 && key.indexOf("rock-hermit") == -1 ) {
    			//still not sure if this is a ship, further checking
    			if( Ship.shipDataForKey ) { //new fast method in 1.79 since 2014.05.19
    				var s = Ship.shipDataForKey(key);
    				if(this.$ExhibitionsLog) log("Exhibitions", j+". "+key+" shipDataForKey "+s);
    				if( s && s.max_flight_speed && s.max_flight_speed > 0 )
    					e.$ExhibitionsShipKeys.push(key); //ok, add it
    			} else { //old way for Oolite 1.77
    				if( key.indexOf("spacebar") == -1 ) //must check every problematic key
    					e.$ExhibitionsShipKeys.push(key); //maybe ok, add it
    			}
    		}
    	}
    
    	if( sdk && sdk.length < shipno ) //duplicate until contain enough ships
    		while( e.$ExhibitionsShipKeys.length < shipno && e.$ExhibitionsShipKeys.length < 1000 )
    			e.$ExhibitionsShipKeys = e.$ExhibitionsShipKeys.concat(e.$ExhibitionsShipKeys);
    	e.$ExhibitionsShipKeys.splice( shipno ); //shrink before short, can appear ships from the end of the alphabet also
    	e.$ExhibitionsShipKeys.sort();
    	if(e.$ExhibitionsLog) log("Exhibitions", "ExhibitionsShipKeys: "+e.$ExhibitionsShipKeys);
    
    
    	this.$ExhibitionsJ = 0; //index in player ships during exhibition creation
    	if( !isValidFrameCallback( this.$ExhibitionsFCB ) )
    		this.$ExhibitionsFCB = addFrameCallback( this.$Exhibitions_FCB ); //add ships and rotate
    
    	//add active police/pirate as defenders of exhibition
    	var role = "police";
    	if( 6 <=  system.techLevel ) role = "interceptor";
    	if(type == "pirate") role = "pirate";
    	var base = 1;
    	if(type == "many") base += 1;
    
    	var pos = Vector3D(0,0,10000);
    	var ori = Quaternion(0, 0, 0, 0);
    	var vr = Vector3D(1, 0, 0);
    	var vu = Vector3D(0, 1, 0);
    	if( player.ship && player.ship.position ) {
    		if( player.ship.position.distanceTo([0,0,0]) < 25600 ) 
    			pos = player.ship.position.add(player.ship.vectorForward.multiply( 10000 ));
    		ori = player.ship.orientation;
    		vr = player.ship.vectorRight; //store initial vectorRight
    		vu = player.ship.vectorUp; //store initial vectorUp
    	}
    	this.$ExhibitionsPosition = [pos, ori, vr, vu]; 
    	
    	var eg = system.addShips(role, base+Math.floor(system.government/4), pos, 2000);
    	if( eg ) for( var i = 0; i < eg.length; i++ ) {
    		if( eg[i] && eg[i].isValid ) eg[i].displayName = "[Exhibition Guard] "+eg[i].displayName;
    	}
    }
    
    this.$Exhibitions_Show2 = function( j ) {
    	if(!system || system.isInterstellarSpace) return;
    
    	var e = worldScripts["exhibitions"];
    	var p = e.$ExhibitionsShipKeys;
    	var space = e.$ExhibitionsSpace;//between ships
    	var sq = Math.ceil(Math.sqrt(p.length));
    //	for( var j = 0; j < p.length; j++ ) { //FCB make this cycle
    		var key = p[j];
    		var row = Math.floor(j/sq);
    		var pos = null;
    		
    		if( e.$ExhibitionsPosition && e.$ExhibitionsPosition[0] ) pos = e.$ExhibitionsPosition[0]
    			.add( e.$ExhibitionsPosition[2].multiply( space * ( j - sq * row - sq / 2 + 0.5 ) ) )
    			.add( e.$ExhibitionsPosition[3].multiply( space * ( row - sq / 2 + 0.5 ) ) );//square align
    		else pos = [ space * ( j - sq * row - sq / 2 + 0.5 ), space * ( row - sq / 2 + 0.5 ), 10000 ];//fallback
    		
    		var ships = system.addShips("["+key+"]", 1, pos, 5);
    		if( ( !ships || !ships[0] ) && key.indexOf("-player") > -1 ) { //handle unsuccesful ship creation
    			key = key.slice(0, key.indexOf("-player")); //remove -player
    			ships = system.addShips("["+key+"]", 1, pos, 5);
    			if( ships ) e.$Exhibitions_ShowAdd( ships[0], key, e, space, j );
    		} else if( ships && ships[0] ) e.$Exhibitions_ShowAdd( ships[0], key, e, space, j );
    //	}
    }
    
    this.$Exhibitions_ShowAdd = function( ship, key, e, space, j ) {
    	if( !ship ) { //handle unsuccesful ship creation
    		if(e.$ExhibitionsLog) log("Exhibitions", "Failed addShips ["+key+"] to the Exhibition.");
    	} else {
    		if(e.$ExhibitionsLog) log("Exhibitions", "["+key+"] added to the Exhibition.");
    		if( ship.escorts && ship.escorts.length > 0 ) 
    			for( var i = 0; i < ship.escorts.length; i++ ) ship.escorts[i].remove(true);
    		ship.beaconCode = null; //fix for escorted mothership in escort contracts
    		if(ship.beaconLabel) ship.beaconLabel = "";
    		ship.bounty = 0;
    		ship.displayName = "[Exhibition] " + ship.displayName;
    		ship.setAI("nullAI.plist");
    		ship.setScript("exhibitions-ship-script.js"); //must deactivate the original script
    		ship.target = null;
    		ship.clearDefenseTargets();
    //		ship.scanClass = "CLASS_CARGO";
    		var cr = ship.collisionRadius; //shift out large ships from the square plane
    		if( cr > space ) {
    			var c2 = 1; //prevent collision of two neighbour baseship
    			if( Math.floor( j / 2 ) == j / 2 ) c2 = -1;
    			var mul =  c2 * (cr + 4 * space);
    			if(e.$ExhibitionsLog) log("Exhibitions", ship.displayName+" too large, shifted out "+j+" "+c2+" "+mul);
    			ship.position = ship.position.add( [0, 0, mul ] );
    		}
    //		if(e.$ExhibitionsLog) log("Exhibitions", "playerori:"+player.ship.orientation);
    		if( e.$ExhibitionsPosition && e.$ExhibitionsPosition[1] ) 
    			ship.orientation = e.$ExhibitionsPosition[1].rotateX( Math.PI/2 );
    		else if( player.ship && player.ship.orientation ) //fallback
    			ship.orientation = player.ship.orientation.rotate( Vector3D(1,0,0), Math.PI/2 );
    
    		if( !e.$ExhibitionsShips ) e.$ExhibitionsShips = [ship];
    		else e.$ExhibitionsShips.push(ship);
    	}
    }
    
    this.$Exhibitions_Timed = function() { //need wait for Gallery startUp finished to get filled ship dataKey arrays
    	var e = worldScripts["exhibitions"];
    	if( e.$ExhibitionsTimer ) {
    		e.$ExhibitionsTimer.stop();
    		delete e.$ExhibitionsTimer;
    	}
    	if(e.$ExhibitionsSalon) e.$Exhibitions_Salon(); //recreate after load game if ordered before
    	e.$Exhibitions_CheckSystem(); //recreate exhibition after load game if any
    }
    
    this.$Exhibitions_TimedPerm = function() { //
    	var e = worldScripts["exhibitions"];
    	if( e.$ExhibitionsTimerPerm ) {
    		e.$ExhibitionsTimerPerm.stop();
    		delete e.$ExhibitionsTimerPerm;
    	}
    	if( e.$ExhibitionsPerm ) { //end of permitted time for rotation
    		e.$ExhibitionsPerm = false;
    		e.$ExhibitionsPermReqSent = false;
    		player.consoleMessage("Permission revoked. You can request again by targeting another ship in the exhibition", 10);
    	} else if( e.$ExhibitionsPermReqSent ) { //end of wait for permission
    		e.$ExhibitionsPerm = true;
    		e.$ExhibitionsPermReqSent = false;
    		player.consoleMessage("Permission granted. You can rotate ships in the exhibition for 5 minutes", 10);
    		e.$ExhibitionsTimerPerm = new Timer(e, e.$Exhibitions_TimedPerm, 300);
    	}
    }
    
    this.$Exhibitions_VisitorLevel = function() {
    	var e = worldScripts["exhibitions"];
    	var len = 0;
    	if( e.$ExhibitionsVisited ) len = e.$ExhibitionsVisited.length;
    	var rep = 0;
    	if( len < 2 ) rep = 0;
    	else if( len < 4 ) rep = 1;
    	else if( len < 8 ) rep = 2;
    	else if( len < 16 ) rep = 3;
    	else if( len < 32 ) rep = 4;
    	else if( len < 64 ) rep = 5;
    	else if( len < 100 ) rep = 6;
    	else if( len < 300 ) rep = 7;
    	else if( len < 1000 ) rep = 8;
    	else rep = 9; //Elite Visitor
    	return( rep );
    }
    Scripts/gallery.js
    "use strict";
    this.name        = "gallery";
    this.author      = "Norby";
    this.copyright   = "2013 Norbert Nagy";
    this.licence     = "CC BY-NC-SA 3.0";
    this.description = "Show installed ships and objects in an interface screen.";
    
    //customizable properties
    this.$GalleryAll = true; //Gallery of all objects (cheat)
    this.$GalleryDefaultZoom = 1.5; //default size of ships
    this.$GalleryMaxIt = 5; //max. iteration in Oolite v1.77, reduce if cause "Universe is full" or crash to desktop
    this.$GalleryLog = false; //verbose log
    this.$GalleryRoles = ["all","player", //main roles, you can add more but must leave these first two untouched
    	"escort","hunter","interceptor","miner","pirate","police","trader","scavenger","shuttle","thargoid",
    	"asteroid","boulder","buoy","cargopod","missile","station"];
    this.$GallerySpeed = 2; //move and zoom speed
    
    //internal properties, should not touch
    this.$GalleryAllObjects = []; //store all ship and object keys
    this.$GalleryAllNames = []; //all ship names for search
    this.$GalleryEncounters = []; //store encountered ship keys for savegame also
    this.$GalleryEncNames = []; //encountered ship names for search
    this.$GalleryFCB = null; //FrameCallBack for rotating ship model
    this.$GalleryFCB2 = null; //FrameCallBack for menu
    this.$GalleryFCB3 = null; //FrameCallBack to fill up other role
    this.$GalleryKey = null; //store last dataKey
    this.$GalleryKeyi = []; //selected key indexes in the GalleryKeysForRole array
    this.$GalleryKeysi = 0; //actual index in all Keys
    this.$GalleryKeysForRole = []; //dataKeys for the selected role
    this.$GalleryLastMenu = 0; //set marker to the lastly used menu line
    this.$GalleryLastEnc = []; //lastly encountered dataKeys
    this.$GalleryMore = true; //show more menu
    this.$GalleryMove = 0; //move showed ship
    this.$GalleryOri = null; //store last orientation
    this.$GalleryOthers = [];
    this.$GalleryPrevZ = 0; //store previous z position
    this.$GalleryRole = 0; //selected role
    this.$GalleryRotate = 0; //rotating showed ship around Y axis
    this.$GalleryShift = 0; //store move multiplier calculated from ship size
    this.$GalleryShiftX = 0; //store move position
    this.$GalleryShiftY = 0; //store move position
    this.$GalleryShiftZ = 0; //store move position
    this.$GalleryShowAll = false; //show all ships and objects selected in search
    this.$GalleryTimer = null; //help avoid timelimit
    this.$GalleryTRole = 0; //actual role in timer
    this.$GalleryTRI = 0; //Timer Role Iteration
    this.$GalleryTrash = []; //store created ships to remove later
    this.$GalleryZoom = 0; //zoom showed ship
    
    //following arrays comes from Technical Reference Library OXP by spara
    this.$GalleryClassifiedScanClasses = ["CLASS_MILITARY", "CLASS_POLICE", "CLASS_THARGOID"];
    this.$GalleryClassifiedRoles = ["constrictor", "kephalan", "odonatean", "scorpax", "bigTrader", "monkpatrol", "griff_blackmonk_defenceship", "monkhit1", "monkhit2", "monkhold1", "monkhold2", "ev_green_gecko", "sin-pirate"];//classify constrictor, aliens oxp, bigTrader ships, black monks, green gecko oxp and capisastra oxp.
    
    
    //world script events
    this.startUp = function() {
    	this.$GalleryAllObjects = []; //store all ship and object keys
    	this.$GalleryAllNames = []; //all ship names for search
    	var ge = missionVariables.$GalleryEncounters; //load encountered ships from savegame
    	if( !ge ) this.$GalleryEncounters = []; else this.$GalleryEncounters = ge.split(",");//need at least 2 item exists
    	if(this.$GalleryLog)
    		log("Gallery", "Loaded Encounters ("+this.$GalleryEncounters.length+"): "+this.$GalleryEncounters);
    	this.$GalleryEncNames = []; //encountered ship names for search
    	this.$GalleryFCB = null; //FrameCallBack for rotating ship model
    	this.$GalleryLastMenu = 0; //set marker to the lastly used menu line
    	
    	var ge = missionVariables.$GalleryLastEnc; //load order of encounters from savegame
    	if( !ge ) this.$GalleryLastEnc = []; 
    	else if( (ge+"").indexOf(",") ) this.$GalleryLastEnc = (ge+"").split(","); //need +"" for bugfix of a single item
    	else this.$GalleryLastEnc = [ge];
    	if(this.$GalleryLog)
    		log("Gallery", "Loaded LastEnc ("+this.$GalleryLastEnc.length+"): "+this.$GalleryLastEnc);
    		
    	this.$GalleryMore = true; //show more menu
    	this.$GalleryMove = 0; //move showed ship
    	this.$GalleryRole = 0; //starting role in Roles array
    	this.$GalleryRotate = 0; //rotating showed ship around Y axis
    	this.$GallerySet = false; //settings menu
    	this.$GalleryShiftX = 0; //store move position
    	this.$GalleryShiftY = 0; //store move position
    	this.$GalleryShiftZ = 0; //store move position
    	this.$GalleryShowAll = false; //show all ships and objects selected in search
    	this.$GalleryTrash = []; //store created ships to remove later
    	this.$GalleryZoom = 0; //zoom showed ship
    	
    	if (0 < oolite.compareVersion("1.79")) { //slow and limited method, use before Oolite v1.79
    //can cause long wait in startUp, "Universe is full" or crash to desktop before the spinning cobra if not enough memory
    		log("Gallery", "Fallback to a slow and inaccurate method without Oolite v1.79");
    		this.$GalleryKeysForRole[ 0 ] = [];
    		this.$GalleryKeysForRole[ 0 ] = this.$GalleryKeysForRole[ 0 ].concat(this.$GalleryEncounters);
    		var all = false;
    		if( this.$GalleryAll || worldScripts["exhibitions"] 
    			&& worldScripts["exhibitions"].$ExhibitionsAllShipsInPrivateExhibition ) all = true;
    		for( var i = 1; i < this.$GalleryRoles.length && all || i == 1; i++ ) { //main roles
    			var maxk = this.$GalleryMaxIt; //how many iteration max., reduce if cause "Universe is full" or crash to desktop
    			var prevlen = 0;
    			var num = 16; //how many ships created at one time
    			if( i == 1 ) num = 64; //more if player role
    			for( var k = 1; k <= maxk; k++ ) {
    				if( this.$GalleryKeysForRole[ i ] ) prevlen = this.$GalleryKeysForRole[ i ].length;
    				var ships = system.addShips(this.$GalleryRoles[i], num, [0,0,0], 1000000);
    //				this.$GalleryTrash = this.$GalleryTrash.concat(ships);
    				if(ships) for( var j = 0; j < ships.length; j++ ) { //created ships
    					if( ships[j] ) {
    						var key = ships[j].dataKey;
    						if( !this.$GalleryKeysForRole[ i ] ) {
    							this.$GalleryKeysForRole[ i ] = [key];
    						} else if( this.$GalleryKeysForRole[ i ].indexOf(key) == -1 )
    							this.$GalleryKeysForRole[ i ].push(key);//add new key
    						ships[j].remove(true);//can not remove everything and reach Universe limit
    						//all ship remove must be called with true parameter to avoid call shipDied
    						//event which will drop the following error in the case of constrictor!
    						//TypeError: killer is null in oolite-constrictor.js, line 72
    //					if(this.$GalleryLog) log("Gallery", i + ". main role: " + this.$GalleryRoles[i]
    //							+" "+k+". try, "+(j+1)+". key: "+key
    //							+" GalleryKeysForRole["+i+"]:"+this.$GalleryKeysForRole[ i ]);
    					}
    				}
    				if(this.$GalleryLog) log("Gallery", i + ". main role: " + this.$GalleryRoles[i]
    					+" "+k+". try: "+this.$GalleryKeysForRole[ i ] );
    				if( //k == 1 && this.$GalleryKeysForRole[ i ].length < num / 3 ||//too few object in this role
    					prevlen == this.$GalleryKeysForRole[ i ].length) maxk = 0;//exit from cycle
    			}
    			if(this.$GalleryKeysForRole[ i ]) {
    				this.$GalleryKeysForRole[ i ].sort();
    				if(this.$GalleryLog) log("Gallery", i + ". role after "+(k-1)+" try: " + this.$GalleryRoles[i]
    					+ " ("+this.$GalleryKeysForRole[ i ].length+"): " + this.$GalleryKeysForRole[ i ] );
    				this.$GalleryKeysForRole[ 0 ] = this.$GalleryKeysForRole[ 0 ].concat(this.$GalleryKeysForRole[ i ]);
    			}
    		}
    		this.$GalleryKeysForRole[ 0 ].sort();
    		for( var i = 1; i < this.$GalleryKeysForRole[ 0 ].length; i++ ) { //remove duplicated keys
    			if(this.$GalleryKeysForRole[0][i] == this.$GalleryKeysForRole[0][i-1]) {
    				this.$GalleryKeysForRole[0].splice(i,1); //remove this item
    				i--;
    			}
    		}
    		if(this.$GalleryLog) log("Gallery", "All dataKeys ("+this.$GalleryKeysForRole[ 0 ].length
    			+"): " + this.$GalleryKeysForRole[ 0 ] );
    
    //		this.$GalleryTRole = 1;
    //		this.$GalleryTRI = 1;
    //		this.$GalleryTimer = new Timer(this, this.$Gallery_Timed, 1); //cycle in timer
    		if( !this.$GalleryEncounters || this.$GalleryEncounters.length == 0 )
    			this.$GalleryEncounters = this.$GalleryKeysForRole[ 1 ]; //store playable ships
    		else {
    			for( var i = 1; i < this.$GalleryKeysForRole[ 1 ].length; i++ ) {
    				var key = this.$GalleryKeysForRole[ 1 ][i];
    				if( this.$GalleryEncounters.indexOf( key ) == -1 )
    					this.$GalleryEncounters.push( key );
    			}
    		}
    	
    	} else { //Oolite v1.79 or later
    		var sdk = Ship.keysForRole( "player" );  //playable and encountered ships only
    		if(this.$GalleryEncounters && this.$GalleryEncounters.length > 0) {
    			for( var i = 1; i < this.$GalleryEncounters.length; i++ ) {
    				var key = this.$GalleryEncounters[i];
    				if( sdk.indexOf( key ) == -1 ) sdk.push( key );
    			}
    		}
    		this.$GalleryEncounters = sdk;
    		if( this.$GalleryAll ) var sdk = Ship.keys(); //all dataKeys
    
    	    this.$GalleryKeysForRole[ 0 ] = sdk.sort(); //all or playable+encountered dataKeys
    	    this.$GalleryKeyi[ 0 ] = 0;
    	    if(this.$GalleryLog) log("Gallery", "All dataKeys ("+this.$GalleryKeysForRole[ 0 ].length+"): "
    		    + this.$GalleryKeysForRole[ 0 ] );
    	
    	    for( var i = 1; i < this.$GalleryRoles.length; i++ ) { //main roles
    		var roleKeys = Ship.keysForRole( this.$GalleryRoles[i] ).sort();
    		
    		if( !roleKeys || !roleKeys.length || roleKeys.length < 1 )
    			this.$GalleryRoles.splice(i,1); //remove empty role
    		else {
    
    /* - validate check removed (too slow), check when try to show the item only
    		    for( var j = 0; j < roleKeys.length; j++ ) {
    			var key = roleKeys[j];
    			var ships = system.addShips("["+key+"]", 1, [0,0,0], 25000);
    			if( ( !ships || !ships[0] ) && key.indexOf("-player") > -1 ) { //handle unsuccesful ship creation
    				key = key.slice(0, key.indexOf("-player")); //remove -player
    				ships = system.addShips("["+key+"]", 1, [0,0,0], 25000);
    				if( !ships || !ships[0] ) { //handle unsuccesful ship creation
    					roleKeys.splice(j,1); //remove item
    					j--;
    					if(this.$GalleryLog) log("Gallery", "Failed addShips ["+key+"], removed.");
    				} else ships[0].remove(true); //ok without -player
    			} else {
    				if( ships && ships[0] ) {
    					var ship = ships[0];
    					if( ship.roles ) for( var k = 0; k < ship.roles.length; k++ ) {
    						if( ship.roles[k] && ship.roles[k].indexOf("subent") > -1 ) {
    							//skip subentities in Griff_Shipset_Replace_v1.34.oxp
    							roleKeys.splice(j,1); //remove item
    							j--;
    							k = ship.roles.length;//exit from cycle
    							var msg = "Subentity ["+key+"] removed.";
    							if(this.$GalleryLog) log("Gallery", msg);
    							if(ship != player.ship) ship.remove(true);
    						}
    					}
    					ship.remove(true); //ok
    				} else if(this.$GalleryLog) log("Gallery", "Failed addShips ["+key+"], removed.");
    			}
    		    }
    */		
    		    if( this.$GalleryAll ) this.$GalleryKeysForRole[ i ] = roleKeys;
    		    else {
    			this.$GalleryKeysForRole[ i ] = [];
    			for( var k = 1; k < roleKeys.length; k++ ) {
    				var key = roleKeys[k];
    				if( this.$GalleryKeysForRole[ 0 ].indexOf( key ) != -1 )
    					this.$GalleryKeysForRole[ i ].push( key ); //get playable or encountered keys only
    			}
    		    }
    		    this.$GalleryKeyi[ i ] = 0;
    		    if(this.$GalleryLog) log("Gallery", i + ". role: " + this.$GalleryRoles[i] 
    			    + " ("+this.$GalleryKeysForRole[ i ].length+"): " + this.$GalleryKeysForRole[ i ] );
    		}
    	    }
    	
    	    if(this.$GalleryLog) var roles = Ship.roles(); //all roles
    	    if(this.$GalleryLog) for( var j = 0; j < roles.length; j++ ) {
    //		if( roles[j] && roles[j][0] != "[" ) { //many roles start with "["
    			var roleKeys = Ship.keysForRole( roles[j] );
    			if(this.$GalleryLog) log("Gallery", (j+1)+". "+roles[j]+" ("+roleKeys.length+"): "+roleKeys);
    //		}
    	    }
    //	    if( !isValidFrameCallback( this.$GalleryFCB3 ) )
    //		this.$GalleryFCB3 = addFrameCallback( this.$Gallery_OtherRole );//bug: give all keys, not others only
    
    	}
    	
    	this.$GalleryAllObjects = this.$GalleryKeysForRole[ 0 ].sort(); //store all ships
    //	this.$GalleryEncounters.sort();//Gallery_EncSort() is better after GalleryEncNames is filled
    
    		
    	//get encountered ship names
    	for( var j = 0; j < this.$GalleryEncounters.length; j++ ) {
    		var key = this.$GalleryEncounters[j];
    		if( Ship.shipDataForKey ) { //new fast method in 1.79 since 2014.05.19
    			var s = Ship.shipDataForKey(key);
    			if(this.$GalleryLog) log("Gallery", j+". "+key+" shipDataForKey "+s);//debug
    			if( s && s.name && s.name.length > 0 ) this.$Gallery_AddEncName(j, s.name); //check and store name
    		} else { //old slower way to get ship names, need validate check also
    			var ships = system.addShips("["+key+"]", 1, [0,0,0], 1000000);
    			if( ( !ships || !ships[0] ) && key.indexOf("-player") > -1 ) { //handle unsuccesful ship creation
    				key = key.slice(0, key.indexOf("-player")); //remove -player
    				ships = system.addShips("["+key+"]", 1, [0,0,0], 1000000);
    				if( !ships || !ships[0] ) { //handle unsuccesful ship creation
    					this.$GalleryEncounters.splice(j,1); //remove item
    					j--;
    //					if(this.$GalleryLog) 
    						log("Gallery", "Failed addShips ["+key+"], removed.");
    				} else {
    					this.$Gallery_AddEncName(j, ships[0].name); //check and store name
    					ships[0].remove(true); //ok without -player
    				}
    			} else {
    				if( ships && ships[0] ) {
    					var ship = ships[0];
    					if( ship.roles ) for( var k = 0; k < ship.roles.length; k++ ) {
    						if( ship.roles[k] && ship.roles[k].indexOf("subent") > -1 ) {
    							//skip subentities in Griff_Shipset_Replace_v1.34.oxp
    							this.$GalleryEncounters.splice(j,1); //remove item
    							j--;
    							k = ship.roles.length;//exit from cycle
    							var msg = "Subentity ["+key+"] removed.";
    //							if(this.$GalleryLog) 
    								log("Gallery", msg);
    							if(ship != player.ship) ship.remove(true);
    						}
    					}
    					this.$Gallery_AddEncName(j, ship.name); //check and store name
    					ship.remove(true); //ok
    				} else //if(this.$GalleryLog)
    					log("Gallery", "Failed addShips ["+key+"], removed.");
    			}
    		}
    	}
    
    	if(this.$GalleryLog) log("Gallery", " Gallery ("+this.$GalleryEncounters.length+"): "+this.$GalleryEncounters);//debug
    	this.$Gallery_EncSort();
    	this.$GalleryKeysForRole[ 0 ] = this.$GalleryEncounters;//always start in enc. array
    	if(this.$GalleryLog) log("Gallery", " Gallery ("+this.$GalleryEncounters.length+"): "+this.$GalleryEncounters);//debug
    
    	if( player.ship && player.ship.docked ) this.shipDockedWithStation( player.ship.dockedStation ); //set interface
    	else if( system ) this.shipDockedWithStation( system.mainStation );//fallback
    }
    
    this.commsMessageReceived = function(message, sender) {
    	this.$Gallery_Encounter( sender );
    }
    
    this.distressMessageReceived = function(aggressor, sender) {
    	this.$Gallery_Encounter( aggressor );
    	this.$Gallery_Encounter( sender );
    }
    	
    this.playerTargetedMissile = function(missile) {
    	this.$Gallery_Encounter( missile );
    }
    
    this.playerWillSaveGame = function(message) {
    	missionVariables.$GalleryEncounters = this.$GalleryEncounters;  //store encountered ships into savegame
    	missionVariables.$GalleryLastEnc = this.$GalleryLastEnc;  //store order of encounters into savegame
    }
    
    this.shipAttackedOther = function(other) {
    	this.$Gallery_Encounter( other );
    }
    
    this.shipAttackedWithMissile = function(missile, whom) {
    	this.$Gallery_Encounter( missile );
    	this.$Gallery_Encounter( whom );
    }
    
    this.shipBeingAttacked = function(whom) {
    	this.$Gallery_Encounter( whom );
    }
    
    this.shipBeingAttackedUnsuccessfully = function(whom) {
    	this.$Gallery_Encounter( whom );
    }
    
    this.shipCloseContact = function(otherShip) {
    	this.$Gallery_Encounter( otherShip );
    }
    
    this.shipCollided = function(otherShip)
    {
    	this.$Gallery_Encounter( otherShip );
    }
    
    this.shipDockedWithStation = function(station)
    {
    	if( isValidFrameCallback( this.$GalleryFCB ) )
    		removeFrameCallback( this.$GalleryFCB );
    	var len = this.$GalleryLastEnc.length;
    	var s = "";
    	if( len > 1 ) s = "s";
    	station.setInterface("Gallery",{
    		title: "Gallery of encounters ("+this.$GalleryLastEnc.length+" item"+s+" stored)",
    		category: "Ship Systems",
    		summary: "Shows a gallery of ships and other space objects that you have encountered, with public technical data where available.",
    		callback: this.$Gallery_Interface.bind(this)
    		});
    	if( this.$GalleryAll ) {
    	    station.setInterface("GalleryAll",{
    		title: "Gallery of all objects ("+this.$GalleryAllObjects.length+" objects)",
    		category: "Ship Systems",
    		summary: "Shows a gallery of all ships and other space objects, with public technical data where available.",
    		callback: this.$Gallery_InterfaceAll.bind(this)
    	    });
    	}
    }
    
    this.shipEnteredStationAegis = function(station) {
    	this.$Gallery_Encounter( station );
    }
    
    this.shipFiredMissile = function(missile, target) {
    	this.$Gallery_Encounter( missile );
    	this.$Gallery_Encounter( target );
    }
    
    this.shipKilledOther = function(whom, damageType) {
    	this.$Gallery_Encounter( whom );
    }
    
    this.shipScoopedOther = function(whom) {
    	this.$Gallery_Encounter( whom );
    }
    
    this.shipTakingDamage = function(amount, whom, type) {
    	this.$Gallery_Encounter( whom );
    }
    
    this.shipTargetAcquired = function(target) {
    	this.$Gallery_Encounter( target );
    }
    
    this.shipWillLaunchFromStation = function() {
    	player.ship.hudHidden = false;
    	if( isValidFrameCallback( worldScripts["gallery"].$GalleryFCB ) )
    		removeFrameCallback( worldScripts["gallery"].$GalleryFCB );
    }
    
    
    //Gallery methods
    this.$Gallery_AddEncName = function( j, n ) { //save name for search, check for empty and duplicated name
    	if( n && n.length > 0 ) {
    		var x = this.$GalleryEncNames.indexOf(n);
    		if( x > -1 ) { //duplicated name, add number in parenthesis to the name
    			for( var i = 1; i < 10000; i++ ) {
    				var n2 = n + " (" + i + ")";
    				if( this.$GalleryEncNames.indexOf(n2) == -1 ) i = 10000;//exit
    			}
    			this.$GalleryEncNames[j] = n2;
    		} else this.$GalleryEncNames[j] = n;//save name
    	} else this.$GalleryEncNames[j] = this.$GalleryEncounters[j];//datakey if name is empty
    	if(this.$GalleryLog) log("Gallery", j+". "+this.$GalleryEncounters[j]+": "+this.$GalleryEncNames[j]);
    }
    
    this.$Gallery_Encounter = function( ship ) { //save the keys of encountered ships
    	if( !ship || !ship.isValid || !player.ship || !player.ship.isValid ) return;
    	var key = ship.dataKey;
    	if( key && key != "telescopemarker" && key.indexOf("customshields") == -1
    		&& ( !this.$GalleryEncounters || this.$GalleryEncounters.indexOf( key ) == -1 ) ) {
    		if(this.$GalleryLog) log("Gallery", key+" encountered.");
    		if( !this.$GalleryEncounters ) this.$GalleryEncounters = [ key ];
    		else this.$GalleryEncounters.push( key ); //do not sort: EncNames array will be in wrong order!
    		if( !this.$GalleryLastEnc ) this.$GalleryLastEnc = [ key ];
    		else this.$GalleryLastEnc.push( key ); //do not sort at all: this array hold the order of encounters
    		var j = this.$GalleryEncounters.length - 1; //last item
    		this.$Gallery_AddEncName( j, ship.name ); //do not use displayName to avoid randomshipnames OXP
    //		this.$GalleryEncNames.push( ship.name );
    		player.consoleMessage(this.$GalleryEncNames[j]+" is now added to your ship's Gallery.",10); //as the "+(j+1)+". space object
    		this.$Gallery_EncSort();//sort after consoleMessage only!
    //		if( !this.$GalleryShowAll ) this.$GalleryKeysForRole[0] = this.$GalleryEncounters;//needed but EncSort do it
    
    
    		if( this.$GalleryAll ) for( var i = 2; i < this.$GalleryRoles.length; i++ ) { //main roles
    			if( ship.roles && ship.roles.indexOf( this.$GalleryRoles[i] ) != -1 ) {
    				if( !this.$GalleryKeysForRole[ i ] ) this.$GalleryKeysForRole[ i ] = [key];
    				else this.$GalleryKeysForRole[ i ].push( key );
    				this.$GalleryKeysForRole[ i ].sort();
    			}
    		}
    	}
    }
    
    this.$Gallery_EncSort = function() {
    	var g = worldScripts["gallery"];
    	var e = g.$GalleryEncounters;
    	var n = g.$GalleryEncNames;
    	var k = []; //dataKeys for names
    	for( var i = 0; i < n.length; i++ ) {
    		if( n[i] && n[i].length > 0 ) k[ n[i] ] = e[i];
    		else {
    			e[i] = null;
    			n[i] = null;
    		}
    	}
    	g.$GalleryEncNames.sort();
    	n = g.$GalleryEncNames;
    	var e2 = [];
    	for( var i = 0; i < n.length; i++ ) e2[i] = k[ n[i] ];
    	g.$GalleryEncounters = e2;
    	if( !g.$GalleryShowAll ) g.$GalleryKeysForRole[ 0 ] = g.$GalleryEncounters;
    }
    
    this.$Gallery_FCB = function( delta ) { //FrameCallBack updating ship in sell salvage screen
    	var m = mission.displayModel;
    	if( m && m.isValid ) {
    		var g = worldScripts["gallery"];
    		
    		switch( g.$GalleryRotate ) {
    			case 0:
    				m.orientation = m.orientation.rotateY( delta/1.5 );
    				break;
    			case 2:
    				m.orientation = m.orientation.rotateY( -delta/1.5 );
    				break;
    			case 4:
    				m.orientation = m.orientation.rotateX( delta/1.5 );
    				break;
    			case 6:
    				m.orientation = m.orientation.rotateX( -delta/1.5 );
    				break;
    		}
    		g.$GalleryOri = mission.displayModel.orientation;
    		
    		//Move and zoom, camera is at [0,0,0] facing [1,0,0,0]
    		switch( g.$GalleryMove ) {
    			case 1:
    				g.$GalleryShiftX += g.$GallerySpeed * g.$GalleryShift * delta;
    				break;
    			case 3:
    				g.$GalleryShiftX -= g.$GallerySpeed * g.$GalleryShift * delta;
    				break;
    			case 5:
    				g.$GalleryShiftY += g.$GallerySpeed * g.$GalleryShift * delta;
    				break;
    			case 7:
    				g.$GalleryShiftY -= g.$GallerySpeed * g.$GalleryShift * delta;
    				break;
    		}
    		if( g.$GalleryZoom == 1 ) g.$GalleryShiftZ -= 3 * g.$GallerySpeed * g.$GalleryShift * delta;
    		else if( g.$GalleryZoom == 3 ) g.$GalleryShiftZ += 3 * g.$GallerySpeed * g.$GalleryShift * delta;
    		
    		var w = oolite.gameSettings.gameWindow;
    		var wide = Math.max( 0.01, ( w.width / w.height ) / g.$GalleryDefaultZoom ); //correction if not in widescreen
    		mission.displayModel.position = Vector3D(g.$GalleryShiftX, g.$GalleryShiftY, g.$GalleryShiftZ * wide);
    	}
    }
    
    this.$Gallery_FCB2 = function( delta ) { //FrameCallBack for menu
    	var g = worldScripts["gallery"];
    	if( isValidFrameCallback( g.$GalleryFCB2 ) ) removeFrameCallback( g.$GalleryFCB2 );
    	g.$Gallery_Interface2( g.$GalleryShowAll );
    }
    
    this.Gallery_GetShipData = function(ship, all) {
    	var equip="";
    	var s="";
    	s=ship.forwardWeapon;
    	if( s!=null && s.equipmentKey != "EQ_WEAPON_NONE" ) equip+="Forward "+s.name;
    	var shipno = 0;
    	var r =  worldScripts["rocketmenu"];
    	if( r ) {
    		shipno = r.$RocketMenu_Name.indexOf(ship.dataKey); //RocketShip with extra weapons
    	}
    	if( shipno > 0 ) {
    		var pul = r.$RocketMenu_Pulse[shipno];
    		if( pul > 0 ) {
    			if( equip.length > 0 ) equip+="\n";
    			if( pul > 1) equip+=pul+" ";
    			equip+="Auto Pulse Laser";
    			if( pul > 1) equip+="s";
    		}
    		var ham = r.$RocketMenu_Hammer[shipno];
    		if( ham > 0 ) {
    			if( equip.length > 0 ) equip+="\n";
    			if( ham > 1) equip+=ham+" ";
    			equip+="Hammer Laser";
    			if( ham > 1) equip+="s";
    		}
    		var Sniper = r.$RocketMenu_Sniper[shipno];
    		if( Sniper > 0 ) {
    			if( equip.length > 0 ) equip+="\n";
    			if( Sniper > 1) equip+=Sniper+" ";
    			equip+="Sniper Laser";
    			if( Sniper > 1) equip+="s";
    		}
    		var pha = r.$RocketMenu_Sapper[shipno];
    		if( pha > 0 ) {
    			if( equip.length > 0 ) equip+="\n";
    			if( pha > 1) equip+=pha+" ";
    			equip+="Sapper";
    			if( pha > 1) equip+="s";
    		}
    	}
    	s=ship.aftWeapon;
    	if( s!=null && s.equipmentKey != "EQ_WEAPON_NONE" ) {
    		if( equip.length > 0 ) equip+="\n"; //Anaconda has aft weapon only
    		equip+="Aft "+s.name;
    	}
    	s=ship.portWeapon;
    	if( s!=null && s.equipmentKey != "EQ_WEAPON_NONE" ) equip+="\nPort "+s.name;
    	s=ship.starboardWeapon;
    	if( s!=null && s.equipmentKey != "EQ_WEAPON_NONE" ) equip+="\nStarboard "+s.name;
    	if( shipno > 0 ) {
    		var bt = r.$RocketMenu_Turrets[shipno];
    		if( bt > 0) {  //RocketShip with turrets
    			if( equip.length > 0 && !ship.portWeapon && !ship.starboardWeapon) equip+="\n";
    			else equip+=", ";
    			if(bt < 1) equip += (bt*10)+" Mini";
    			else if(bt > 99) equip += Math.floor(bt/100)+" Strong";
    			else equip += bt+" Ball";
    			equip += " Turret";
    			if( bt > 1) equip+="s";
    		}
    		if(ship.dataKey.indexOf("rocketcruiser") > -1) equip+=", 2 Main Turrets"; //rocketcruiser-player
    	}
    	if(equip.length > 0) equip+="\n";
    	else if( ship.isPiloted || all ) equip+="No Laser\n";
    	var eqs = "";
    /*	var n = ""; //equipment list removed to make space
    	var eq = ship.equipment;
    	var i=-1;
    	while(eq[++i]) {
    		n = eq[i].name;
    		if( n.length > 0 ) eqs += n + ", ";
    		else {	//skip noname eqs awarded by NumericHUD to avoid timeLimit error
    			for( var j = i + 10; j < eq.length; j+=10 ) {
    				if( eq[j].name.length != 0 ) {
    					i = j - 10; //end of skip
    					j = eq.length;
    				}
    			}
    			while( i < eq.length && eq[i].name.length == 0 ) i++;
    			i--;
    		}
    	}
    	eqs = eqs.substr( 0, eqs.length - 2 ); //cut last comma
    	if( eqs.length > 0 ) equip+=eqs+"\n\n";
    	else equip+="\n";//empty line before desc if no eqs
    */	var desc = "";
    	if( ship.scriptInfo && ship.scriptInfo.buydesc )//show ship description if any
    		desc="\""+ship.scriptInfo.buydesc+"\"\n";
    	var sh = 0;
    	if( ship == player.ship ) sh += ship.maxForwardShield;
    	else {
    		var w = worldScripts["NPC-shields"];
    		if( w && ship.script ) {
    			if( !ship.script.maxShieldStrength ) w.shipSpawned(ship); //add npc shield
    			sh += ship.script.maxShieldStrength;
    		}
    		var w = worldScripts["customshields"];
    		if( w && ship.script ) {
    			if( !ship.script.customshieldsmaxforwardshieldlevel ) w.shipSpawned(ship); //add customshield
    			sh += ship.script.customshieldsmaxforwardshieldlevel;
    		}
    	}
    	if( ship.dataKey.indexOf("escape-capsule") > -1 ) sh = 0; //no shield on escape pod
    	else if( worldScripts["shieldequalizercapacitors"] ) {
    		if( ship.equipmentStatus("EQ_BIGSHCAP") == "EQUIPMENT_OK"
    			&& ship.equipmentStatus("EQ_FORWARD_SHIELD_CAPACITOR") == "EQUIPMENT_OK" ) sh += 256;
    		else if( ship.equipmentStatus("EQ_FORWARD_SHIELD_CAPACITOR") == "EQUIPMENT_OK" ) sh += 64;
    	}
    	var psd = "";//ship.displayName+
    	var hd = "";
    	if(!ship.hasHyperspaceMotor ) hd = "  No Hyperdrive";
    	var speed = ship.maxSpeed;
    	if( ship.script && ship.script.name === "rocketships" )
    		speed = Math.round(ship.maxSpeed/3*2)+"+"+Math.round(ship.maxSpeed/3);
    	if( speed > 0 ) psd += "Speed: "+speed+" mLS  Thrust: "+ship.maxThrust+"  ";
    	if( all || speed > 0 && ship.isPiloted ) {
    		psd += "\nPitch: "+Math.round(ship.maxPitch*100)/100+
    			"  Roll: "+Math.round(ship.maxRoll*100)/100+
    			"  Yaw: "+Math.round(ship.maxYaw*100)/100+hd+
    //			"  Version: "+this.$shipVersion(ship)+
    //			"  Service "+ship.serviceLevel+
    			"\nCargo: "+ship.cargoSpaceCapacity;
    		if( ship.extraCargo > 0 ) psd += "+"+parseInt(ship.extraCargo); //need Oolite v1.79
    		else if( ship.scriptInfo && ship.scriptInfo.cargoext && parseInt(ship.scriptInfo.cargoext) > 0 )
    			psd += "+"+parseInt(ship.scriptInfo.cargoext);
    		psd += "t  ";
    	}
    	var sp = "";
    	var sph = Math.round(ship.collisionRadius*ship.collisionRadius*Math.PI);
    	if( sph < 1000000 ) sp = sph;
    	else sp = Math.round(sph/100000)/10+"k";//base sphere too large to fit in the line so show in km^2
    
    	var en = "";
    	var r = "";
    	if(all) en = "Energy: "+ship.maxEnergy;
    	else {
    		if( ship.isPiloted ) {
    			var eb = Math.max(1, Math.floor(ship.maxEnergy/64));
    			en = "EBank";
    			if( eb > 1 ) en += "s";
    			en += ": "+eb;
    		}
    	}
    
    	if( ship.isPiloted || all ) {
    		var er = 0;
    		if( ship.energyRechargeRate > 0 ) er = parseInt(ship.energyRechargeRate); //need Oolite v1.79
    		else if( ship.scriptInfo && ship.scriptInfo.recharge && parseInt(ship.scriptInfo.recharge) > 0 )
    			er = parseInt(ship.scriptInfo.recharge);
    		if( er > 0 ) {
    			var ers = "";
    			if( er < 2.5 ) ers = "Poor";
    			else if( er < 3.5 ) ers = "Medium";
    			else if( er < 4.5 ) ers = "Good";
    			else if( er < 10 ) ers = "Excellent";
    			else ers = "Extreme";
    			if( all ) ers += " ("+er+")";
    			r = "  Recharge: "+ers;
    		}
    		if( sh > 0 ) r += "\nShields: "+Math.max(2, Math.floor(sh/64));
    		if( ship.missileCapacity > 0 ) {
    			if( sh > 0 ) r += "  "; else r += "\n";
    			r += "Missile";
    			if( ship.missileCapacity > 1 ) r += "s";
    			r += ": "+ship.missileCapacity;
    		}
    	}
    
    	psd += "Mass: "+Math.max(Math.round(ship.mass)/1000, 1)+ //min.1t
    		"t\nSize: "+Math.round(ship.boundingBox.x)+"*"+
    		Math.round(ship.boundingBox.y)+"*"+Math.round(ship.boundingBox.z)+
    		"m Radius: "+Math.round(ship.collisionRadius)+"m"+//" Sphere: "+sp+"m^2"+
    		"\n"+en+r;
    //	if( ship.scriptInfo && ship.scriptInfo.hardarmour && parseInt(ship.scriptInfo.hardarmour) > 0 )
    //		psd += "\nHard Armour deflect "+parseInt(ship.scriptInfo.hardarmour)+" points from any damage";
    		//removed, redundant with Hard Armour Equipment and give more free space
    	psd += "\n"+equip;
    	if( worldScripts["gallery"].$GalleryMore ) psd = desc + psd;
    	if( ship.price > 0 ) psd += "\nPrice: "+formatCredits(ship.price, false, true);
    	return(psd);
    }
    
    this.$Gallery_Interface = function() { //encounters only
    	player.ship.hudHidden = true;//to shift down the menu
    	var g = worldScripts["gallery"];
    	g.$GalleryShowAll = false;
    	g.$GalleryKeysForRole[ 0 ] = g.$GalleryEncounters; //fill up default key array
    	
    	var e = worldScripts["exhibitions"];
    	if( e && isValidFrameCallback( e.$ExhibitionsMenuFCB ) )
    		removeFrameCallback( e.$ExhibitionsMenuFCB );  //need for bugfix
    	
    	this.$Gallery_LastEncounters( false, true ); //not all, init
    //	this.$Gallery_Search( false, true ); //not all, init
    //	this.$Gallery_Interface2( false );
    }
    
    this.$Gallery_InterfaceAll = function() { //all ships
    	player.ship.hudHidden = true;//to shift down the menu
    	var g = worldScripts["gallery"];
    	g.$GalleryShowAll = true;
    	g.$GalleryKeysForRole[ 0 ] = g.$GalleryAllObjects; //fill up default key array
    	this.$Gallery_Search( true, true ); //all, init
    //	this.$Gallery_Interface2( true );
    }
    
    this.$Gallery_Interface2 = function( all ) {
    	if( player.ship && player.ship.docked && system ) {
    		player.ship.hudHidden = true;//to shift down the menu
    		var g = worldScripts["gallery"];
    		if( !g.$GalleryKeyi[g.$GalleryRole] ) g.$GalleryKeyi[g.$GalleryRole] = 0; //prevent a bug
    		var key = g.$GalleryKeysForRole[g.$GalleryRole][g.$GalleryKeyi[g.$GalleryRole]];
    		if(!key) {
    			g.$GalleryKeyi[g.$GalleryRole] = 0;
    			key = g.$GalleryKeysForRole[g.$GalleryRole][g.$GalleryKeyi[g.$GalleryRole]];
    			if(!key) {
    				g.$GalleryRole = 0;
    				key = g.$GalleryKeysForRole[g.$GalleryRole][g.$GalleryKeyi[g.$GalleryRole]];
    				if(!key) {
    					g.$GalleryKeyi[g.$GalleryRole] = 0;
    					key = g.$GalleryKeysForRole[g.$GalleryRole][g.$GalleryKeyi[g.$GalleryRole]];
    					if(!key) return; //no key found
    				}
    			}
    		}
    		if( key == player.ship.dataKey ) var ship = player.ship;
    		else {
    			if( !all && 
    				( g.$GalleryPlayerShips && g.$GalleryPlayerShips.indexOf(key) == -1 ) &&
    				( g.$GalleryEncounters && g.$GalleryEncounters.indexOf(key) == -1 ) ) {
    				if(this.$GalleryLog) log("Gallery", key+" not encountered so removed. ");
    				this.$Gallery_RemoveCurrentShip(g, all); //will retry also
    				return;
    			}
    			var ships = system.addShips("["+key+"]", 1, [0,0,0], 25000);
    			if( !ships ) { //need to handle unsuccesful ship creation
    				var p = key.indexOf("-player");
    				if( p > -1 ) var key = key.slice(0, p); //remove -player part
    				ships = system.addShips("["+key+"]", 1, [0,0,0], 25000);
    				if( !ships ) { //need to handle unsuccesful ship creation
    					var msg = "Failed addShips with role ["+key+"], removed.";
    					if(this.$GalleryLog) log("Gallery", msg);
    					this.$Gallery_RemoveCurrentShip(g, all); //will retry also
    					return;
    				}
    			}
    			var ship = ships[0];
    			if( ship.roles ) for( var i = 0; i < ship.roles.length; i++ ) {
    				if( ship.roles[i] && ship.roles[i].indexOf("subent") > -1 ) {
    					i = ship.roles.length; //found, will exit from cycle
    					//skip subentities in Griff_Shipset_Replace_v1.34.oxp
    					var msg = "Subentity ["+key+"] removed.";
    					if(this.$GalleryLog) log("Gallery", msg);
    					if(ship != player.ship) ship.remove(true);
    					this.$Gallery_RemoveCurrentShip(g, all); //will retry also
    					return;
    				}
    			}
    		}
    		if(this.$GalleryLog) log("Gallery", "dataKey: "+key+ " Keyi:"+g.$GalleryKeyi[g.$GalleryRole]+
    			" Gallery ("+g.$GalleryKeysForRole.length+"): "+g.$GalleryKeysForRole );
    
    		var c = [key,
    			"Previous",
    			"Role: "+g.$GalleryRoles[g.$GalleryRole]+
    				" ("+g.$GalleryKeysForRole[g.$GalleryRole].length+")",
    			"Previous role",
    			"Search",
    			["Rotate Y+","Rotate stop","Rotate Y-","Rotate stop","Rotate X+","Rotate stop","Rotate X-","Rotate stop"],
    			["Move stop","Move X+","Move stop","Move X-","Move stop","Move Y+","Move stop","Move Y-"],
    			["Zoom stop","Zoom +","Zoom stop","Zoom -"],
    			"Exit",
    			"Hide Menu",
    			"Menu"];
    			
    		if( !all && !g.$GalleryValidateTarget(ship) ) {
    			var msg = "No public details.";
    			if(this.$GalleryLog) log("Gallery", key+" showed without data.");
    		} else {
    			var msg = g.Gallery_GetShipData(ship, all);
    			if(this.$GalleryLog) log("Gallery", msg);
    		}
    		if(ship != player.ship) ship.remove(true); //will really disappear after the end of the function
    		
    		var title = ship.displayName;
    		var x = g.$GalleryEncounters.indexOf(key);
    		if( x > -1 ) title = g.$GalleryEncNames[x]; //show (1), (2), etc. after duplicated names
    		if( all ) name = key;
    		else name = title;
    		if( !name || name.length < 1) name = "..."; //for sure
    		
    		var ch = {	"_0" : name,
    				"_10" : c[10]
    		};
    		if( g.$GalleryMore ) {
    			if( all ) ch = {	"_0" : name,
    		    			"_1" : c[1],
    		    			"_2" : c[2],
    		    			"_3" : c[3],
    		    			"_4" : c[4],
    		    			"_5" : c[5][g.$GalleryRotate],
    		    			"_6" : c[6][g.$GalleryMove],
    		    			"_7" : c[7][g.$GalleryZoom],
    		    			"_8" : c[8],
    		    			"_9" : c[9]
    				};
    			else ch = {	"_0" : name,
    		    			"_1" : c[1],
    //		    			"_2" : c[2], //without role menus
    //		    			"_3" : c[3],
    		    			"_4" : c[4],
    		    			"_5" : c[5][g.$GalleryRotate],
    		    			"_6" : c[6][g.$GalleryMove],
    		    			"_7" : c[7][g.$GalleryZoom],
    		    			"_8" : c[8],
    		    			"_9" : c[9]
    				};
    		} 
    		mission.runScreen({
    			title: title,
    			message: msg,
    			model: "["+key+"]",
    			modelPersonality: 0,
    			spinModel:false,
    			background: "gallery_bg.png",
    			initialChoicesKey: "_"+g.$GalleryLastMenu,
    			choices: ch
    		},function(choice) {
    			switch(choice) {
    				case "_0":
    					if( ++g.$GalleryKeyi[g.$GalleryRole] >= g.$GalleryKeysForRole[g.$GalleryRole].length
    						|| !g.$GalleryKeysForRole[g.$GalleryRole][g.$GalleryKeyi[g.$GalleryRole]] ) 
    						g.$GalleryKeyi[g.$GalleryRole] = 0;
    					g.$GalleryLastMenu = 0;
    					if( !isValidFrameCallback( g.$GalleryFCB2 ) )
    						g.$GalleryFCB2 = addFrameCallback( g.$Gallery_FCB2 );
    				break;
    				case "_1":
    					if( --g.$GalleryKeyi[g.$GalleryRole] < 0 )
    						g.$GalleryKeyi[g.$GalleryRole] = 
    							g.$GalleryKeysForRole[g.$GalleryRole].length - 1;
    					g.$GalleryLastMenu = 1;
    					if( !isValidFrameCallback( g.$GalleryFCB2 ) )
    						g.$GalleryFCB2 = addFrameCallback( g.$Gallery_FCB2 );
    				break;
    				case "_2":
    					if( ++g.$GalleryRole >= g.$GalleryRoles.length )
    //						|| !g.$GalleryKeysForRole[g.$GalleryRole][g.$GalleryKeyi[g.$GalleryRole]] )
    						g.$GalleryRole = 0;
    					while( g.$GalleryRole > 0 &&
    						!g.$GalleryKeysForRole[g.$GalleryRole]
    						|| g.$GalleryKeysForRole[g.$GalleryRole].length < 1 ) { //skip empty roles
    						if( ++g.$GalleryRole >= g.$GalleryRoles.length ) g.$GalleryRole = 0;
    					}
    					g.$GalleryLastMenu = 2;
    					if( !isValidFrameCallback( g.$GalleryFCB2 ) )
    						g.$GalleryFCB2 = addFrameCallback( g.$Gallery_FCB2 );
    				break;
    				case "_3":
    					if( --g.$GalleryRole < 0 ) g.$GalleryRole =  g.$GalleryRoles.length - 1;
    					while( g.$GalleryRole > 0 &&
    						!g.$GalleryKeysForRole[g.$GalleryRole]
    						|| g.$GalleryKeysForRole[g.$GalleryRole].length < 1 ) { //skip empty roles
    						g.$GalleryRole--;
    					}
    					g.$GalleryLastMenu = 3;
    					if( !isValidFrameCallback( g.$GalleryFCB2 ) )
    						g.$GalleryFCB2 = addFrameCallback( g.$Gallery_FCB2 );
    				break;
    				case "_4":
    					g.$GalleryLastMenu = 0;
    					g.$Gallery_Search( all, false ); //use textEntry from Oolite v1.79
    				break;
    				case "_5":
    					if( ++g.$GalleryRotate >= c[5].length ) g.$GalleryRotate = 0;
    					g.$GalleryLastMenu = 5;
    					mission.displayModel.orientation = g.$GalleryOri;
    					if( !isValidFrameCallback( g.$GalleryFCB2 ) )
    						g.$GalleryFCB2 = addFrameCallback( g.$Gallery_FCB2 );
    				break;
    				case "_6":
    					if( ++g.$GalleryMove >= c[6].length ) g.$GalleryMove = 0;
    					g.$GalleryLastMenu = 6;
    					if( !isValidFrameCallback( g.$GalleryFCB2 ) )
    						g.$GalleryFCB2 = addFrameCallback( g.$Gallery_FCB2 );
    				break;
    				case "_7":
    					if( ++g.$GalleryZoom >= c[7].length ) g.$GalleryZoom = 0;
    					g.$GalleryLastMenu = 7;
    					if( !isValidFrameCallback( g.$GalleryFCB2 ) )
    						g.$GalleryFCB2 = addFrameCallback( g.$Gallery_FCB2 );
    				break;
    				case "_8": //exit
    				break;
    				case "_9": //Hide Menu
    					g.$GalleryMore = false;
    					g.$GalleryLastMenu = 10;
    					if( !isValidFrameCallback( g.$GalleryFCB2 ) )
    						g.$GalleryFCB2 = addFrameCallback( g.$Gallery_FCB2 );
    				break;
    				case "_10": //Menu
    					g.$GalleryMore = true;
    					g.$GalleryLastMenu = 9;
    					if( !isValidFrameCallback( g.$GalleryFCB2 ) )
    						g.$GalleryFCB2 = addFrameCallback( g.$Gallery_FCB2 );
    				break;
    			}
    		});
    		if( g.$GalleryLog ) log("Gallery", key +" "+ g.$GalleryKey);
    		if( key != g.$GalleryKey ) { //save position if new model
    			g.$GalleryKey = key;
    			g.$GalleryShiftX = mission.displayModel.position.x;
    			g.$GalleryShiftY = mission.displayModel.position.y;
    			if( g.$GalleryPrevZ < 1 || g.$GalleryShiftZ < 1 )
    				g.$GalleryShiftZ = mission.displayModel.position.z;
    			else {
    				g.$GalleryShiftZ = mission.displayModel.position.z * g.$GalleryShiftZ / g.$GalleryPrevZ; ///
    				g.$GalleryPrevZ = mission.displayModel.position.z;
    			}
    //			g.$GalleryOri = mission.displayModel.orientation;
    		} else mission.displayModel.orientation = g.$GalleryOri;
    		
    		g.$GalleryShift = Math.max( ship.collisionRadius * 0.7 ,  //fix for adder and moray
    					0.5 * mission.displayModel.position.z
    					- Math.max( ship.boundingBox.x, ship.boundingBox.y ) ) / 5;
    		if( g.$GalleryLog ) log("Gallery", mission.displayModel.position+ " shift:"+ g.$GalleryShift);
    		// camera is at [0,0,0] facing [1,0,0,0]
    		mission.displayModel.orientation = //fix flashing shaders in 1.81
    				mission.displayModel.orientation.rotateX( 0.001 );
    
    		if( !isValidFrameCallback( g.$GalleryFCB ) )
    			g.$GalleryFCB = addFrameCallback( g.$Gallery_FCB );
     		g.$Gallery_FCB(0);//needed to set position immediately
    	}
    }
    
    this.$Gallery_LastEncounters = function( all, init ) {
    	var g = worldScripts["gallery"];
    //	g.$GalleryLastEnc = g.$GalleryEncounters;//debug
    
    	var bgkey = player.ship.dataKey;
    	var c = [];
    	var title = g.$Gallery_SearchHeadTitle( all );
    	if( !g.$GalleryLastEnc || g.$GalleryLastEnc.length == 0 ) { //skip last encounters menu if none
    		var msg = "You have not found any object yet, but you can search in purchasable ships.";
    //		g.$Gallery_Search( false, true ); //not all, init
    	} else {
    		var len = g.$GalleryLastEnc.length;
    		var s = "";
    		if( len > 1 ) s = "s";
    		var msg = "You have so far encountered a total of "+len+" space object"+s
    			+". Below are the latest items added to your gallery. Select the gallery item that you wish to view.";
    //		var msg = g.$Gallery_SearchHeadMsg( all )+"   Last encounters:";
    		var i = len - 1;
    		bgkey = g.$GalleryLastEnc[ i ];
    		for( var j = 0; i >= 0 && j < 23; j++ ) { //in reverse order
    			var ei = g.$GalleryEncounters.indexOf( g.$GalleryLastEnc[ i ] );
    			if( ei > -1 ) {
    				c[ j ] = g.$GalleryEncNames[ ei ]; 
    			} else j--;
    			i--;
    		}
    	}
    	
    		var ch = {
    			"_10" : c[0],
    		    	"_11" : c[1],
    		    	"_12" : c[2],
    		    	"_13" : c[3],
    		    	"_14" : c[4],
    		    	"_15" : c[5],
    		    	"_16" : c[6],
    		    	"_17" : c[7],
    		    	"_18" : c[8],
    		    	"_19" : c[9],
    		    	"_20" : c[10],
    		    	"_21" : c[11],
    		    	"_22" : c[12],
    		    	"_23" : c[13],
    		    	"_24" : c[14],
    		    	"_25" : c[15],
    		    	"_26" : c[16],
    		    	"_27" : c[17],
    		    	"_28" : c[18],
    		    	"_29" : c[19],
    		    	"_30" : c[20],
    		    	"_31" : c[21],
    		    	"_32" : c[22],
    			"_S" : "Search" //max. 23 llines fit into the screen after two line msg and one empty line
    		};
    		
    		if( isValidFrameCallback( worldScripts["gallery"].$GalleryFCB ) )
    			removeFrameCallback( worldScripts["gallery"].$GalleryFCB );
    		mission.runScreen({
    			title: title,
    			message: msg,
    			background: "gallery_bg.png",
    			model: "["+bgkey+"]",
    			modelPersonality: 0,
    			spinModel:false,
    //			initialChoicesKey: "_"+g.$GalleryLastMenu,
    			choices: ch
    		},function( choice ) {
    			g.$GalleryRole = 0;
    			if( choice == "_S" ) {
    				g.$Gallery_Search( all, false ); //use textEntry from Oolite v1.79
    				return;
    			}
    			var text = "";
    			if( choice ) text = c[ choice.substr(1) - 10 ]; //cut starting "_"
    			g.$Gallery_SearchKey(text.toLowerCase(), all);
    		});
    		var m = mission.displayModel;
    		if( m ) {
    			var w = oolite.gameSettings.gameWindow; //correction if not in widescreen
    			var wide = Math.max( 0.01, ( w.width / w.height ) / g.$GalleryDefaultZoom );
    			m.position = Vector3D(m.position.x, m.position.y, m.position.z * wide );
    		}
    //	}
    }
    
    this.$Gallery_OtherRole = function() { //separated from startUp for faster boot
    	var g = worldScripts["gallery"];
    	var k = g.$GalleryKeysForRole[ 0 ];  //do one check/frame = 60 check/sec, need about 10 sec to finish
    	var i = g.$GalleryKeysi++;
    	var o = g.$GalleryOthers;
    	
    //	var k = Ship.keys(); //all dataKeys
    //	var o = [];
    //	for( var i = 0; i < k.length; i++ ) { //find keys not in main rules
    		var ki = k[i];
    		var ok = true;
    		for( var j = 0; j < g.$GalleryKeysForRole.length; j++ ) {
    			if( g.$GalleryKeysForRole.indexOf( ki ) > -1 ) { //bug: can not filter anything
    				ok = false;
    				j = g.$GalleryKeysForRole.length; //exit from cycle
    			}
    		}
    		if( ok ) {//found new key
    //			var key = ki;
    //			var ships = system.addShips("["+key+"]", 1, [0,0,0], 25000);
    //			if( ( !ships || !ships[0] ) && key.indexOf("-player") > -1 ) { //handle unsuccesful ship creation
    //				key = key.slice(0, key.indexOf("-player")); //remove -player
    //				ships = system.addShips("["+key+"]", 1, [0,0,0], 25000);
    //				if( !ships || !ships[0] ) { //handle unsuccesful ship creation
    //					if(g.$GalleryLog) log("Gallery", "Failed addShips ["+key+"], skipped.");
    //				} else {  //ok without -player
    //					ships[0].remove(true);
    //					o.push( ki );//add to others
    //				}
    //			} else {
    //				if( ships && ships[0] ) { //ok
    //					ships[0].remove(true);
    					o.push( ki );//add to others
    //				} else if(g.$GalleryLog) log("Gallery", "Failed addShips ["+key+"], skipped.");
    //			}
    		}
    //	}
    	i++;
    	if( i >=  k.length ) {
    		o.sort();
    		if( g.$GalleryLog) log("Gallery", "All other dataKeys: " + o );
    		if( o.length > 0 ) {
    			g.$GalleryKeysForRole.push( o );
    			g.$GalleryRoles.push("all others");
    		}
    		if( isValidFrameCallback( g.$GalleryFCB3 ) )
    			removeFrameCallback( g.$GalleryFCB3 );
    	}
    	g.$GalleryOthers = o;
    }
    
    this.$Gallery_RemoveCurrentShip = function(g, all) {
    	g.$GalleryKeysForRole[g.$GalleryRole].splice(g.$GalleryKeyi[g.$GalleryRole],1); //remove from the current list
    	if(!g.$GalleryKeysForRole[g.$GalleryRole]
    		|| g.$GalleryKeysForRole[g.$GalleryRole].length < 1 ) //none left in this rule
    		g.$GalleryRole = 0;
    	else if(g.$GalleryKeyi[g.$GalleryRole] > g.$GalleryKeysForRole[g.$GalleryRole].length)
    		g.$GalleryKeyi[g.$GalleryRole] ==
    			g.$GalleryKeysForRole[g.$GalleryRole].length - 1; //previous one
    	g.$Gallery_Interface2( all ); //try again with another ship
    }
    	
    this.$Gallery_Search = function( all, init ) {
    	var g = worldScripts["gallery"];
    	var title = this.$Gallery_SearchHeadTitle( all );
    	var msg = this.$Gallery_SearchHeadMsg( all );
    
    	if (init || 0 < oolite.compareVersion("1.79")) { //no textEntry before Oolite v1.79
    		msg += " Select the gallery item that you wish to view.";
    
    		var k = [];
    		if( g.$GalleryShowAll ) k = g.$GalleryKeysForRole[ 0 ];
    		else k = g.$GalleryEncNames;
    		var k1 = [];
    		var l = ["a","b","c","d","e","f","g","h","i","j","k","l","m","n","o",
    			"p","q","r","s","t","u","v","w","x","y","z"];
    		var z = [];
    		for( var j = 0; j < k.length; j++ ) 
    			if(k[j]) k1[j] = k[j].charAt(0).toLowerCase(); 
    			else if(g.$GalleryKeysForRole[ 0 ][j]) k1[j] = g.$GalleryKeysForRole[ 0 ][j].charAt(0).toLowerCase();
    			else k1[j] = "?";
    		for( var i = 0; i < l.length; i++ ) {
    			var x = k1.indexOf( l[i] );
    			if( x != -1 ) z[ l[i] ] = k[x];
    		}
    		var last = "";
    		if( g.$GalleryLastEnc && g.$GalleryLastEnc.length > 0 ) last = "Last encounters";
    		var ch = {	"_" : last,
    				"A" : z.a,
    				"B" : z.b,
    				"C" : z.c,
    				"D" : z.d,
    				"E" : z.e,
    				"F" : z.f,
    				"G" : z.g,
    				"H" : z.h,
    				"I" : z.i,
    				"J" : z.j,
    				"K" : z.k,
    				"L" : z.l,
    				"M" : z.m,
    				"N" : z.n,
    				"O" : z.o,
    				"P" : z.p,
    //				"Q" : z.q,//removed to give a line to the exhibitions menu
    				"R" : z.r,
    				"S" : z.s,
    				"T" : z.t,
    				"U" : z.u,
    				"V" : z.v,
    				"W" : z.w,
    				"X" : z.x,
    				"Y" : z.y,
    				"Z" : z.z
    		};
    //		if( g.$GalleryAll ) {
    //			if( all ) {
    //				ch += {"ZZEnc" : "Encounters"};
    //				msg += "\nSelect \"Encounters\" to see Gallery of encounters.";
    //			} else {
    //				ch += {"ZZAll" : "All"};
    //				msg += "\nSelect \"All\" to see Gallery of all objects.";
    //			}
    //		}
    		if( isValidFrameCallback( g.$GalleryFCB ) ) removeFrameCallback( g.$GalleryFCB );
    		mission.runScreen({
    			title: title,
    			message: msg,
    			model: "["+player.ship.dataKey+"]",
    			modelPersonality: 0,
    			spinModel:false,
    			background: "gallery_bg.png",
    //			initialChoicesKey: "_"+g.$GalleryLastMenu,
    			choices: ch
    		},function(text) {
    			if( text == "_" ) { //Last encounters menu
    				g.$Gallery_LastEncounters( all, init );
    				return;
    			}
    //			if( text == "__" ) { //exhibitions menu
    //				var e = worldScripts["exhibitions"];
    //				e.$ExhibitionsLastMenu = 0;
    //				if( g.$GalleryLog) log("Gallery", "exhibitions menu");
    //				if( !isValidFrameCallback( e.$ExhibitionsMenuFCB ) )
    //					e.$ExhibitionsMenuFCB = addFrameCallback( e.$Exhibitions_MenuFCB ); //will call ex.menu
    //			} else { //selected a ship
    				g.$GalleryRole = 0;
    //			if( g.$GalleryAll && !g.$GalleryShowAll && text == "ZZAll") {
    //				g.$GalleryShowAll = true;
    //				g.$GalleryKeysForRole[ 0 ] = g.$GalleryAllObjects; //fill up default key array
    //			} else if( g.$GalleryAll && g.$GalleryShowAll && text == "ZZEnc") {
    //				g.$GalleryShowAll = false;
    //				g.$GalleryKeysForRole[ 0 ] = g.$GalleryEncounters; //fill up default key array
    //			} else 
    				g.$Gallery_SearchKey(text.toLowerCase(), all);
    //			}
    		});
    		var m = mission.displayModel;
    		if( m ) {
    			var w = oolite.gameSettings.gameWindow; //correction if not in widescreen
    			var wide = Math.max( 0.01, ( w.width / w.height ) / g.$GalleryDefaultZoom );
    			m.position = Vector3D(m.position.x, m.position.y, m.position.z * wide );
    			m.orientation = m.orientation.rotateY( Math.PI );
    		}
    	} else { //textEntry need Oolite v1.79
    		msg += "\nType any part of a ship name or press enter for a list.\nDetails of your ship:\n\n";
    //		if( g.$GalleryAll ) {
    //			if( all ) msg += "\nType \"enc\" to switch back to the Gallery of encounters.";
    //			else msg += "\nType \"all\" to switch to the Gallery of all objects.";
    //		}
    		msg += g.Gallery_GetShipData(player.ship, g.$GalleryShowAll);
    		if( isValidFrameCallback( worldScripts["gallery"].$GalleryFCB ) )
    			removeFrameCallback( worldScripts["gallery"].$GalleryFCB );
    		mission.runScreen({
    			title: title,
    			message: msg,
    			background: "gallery_bg.png",
    			model: "["+player.ship.dataKey+"]",
    			modelPersonality: 0,
    			spinModel:true,
    			screenID: "gallery-search",
    			textEntry: true
    		},function(text) {
    			g.$GalleryRole = 0;
    			if( !text || text.length == 0 ) {
    				g.$Gallery_Search( all, true );
    				return;
    			}
    			text = text.toLowerCase();
    //			if( g.$GalleryAll && !g.$GalleryShowAll && text == "all") {
    //				g.$GalleryShowAll = true;
    //				g.$GalleryKeysForRole[ 0 ] = g.$GalleryAllObjects; //fill up default key array
    //			} else if( g.$GalleryAll && g.$GalleryShowAll && text == "enc") {
    //				g.$GalleryShowAll = false;
    //				g.$GalleryKeysForRole[ 0 ] = g.$GalleryEncounters; //fill up default key array
    //			} else 
    				g.$Gallery_SearchKey(text, all);
    		});
    	}
    }
    
    this.$Gallery_SearchHeadMsg = function( all ) {
    	var g = worldScripts["gallery"];
    	var len = g.$GalleryEncounters.length;
    	if( all ) len = len + " of "+g.$GalleryAllObjects.length;
    	return( "Your "+player.ship.displayName+" has recorded "+len+" ship and space object types." );
    }
    
    this.$Gallery_SearchHeadTitle = function( all ) {
    	var title = "Gallery of ";
    	if( all ) title += "all objects";
    	else title += "encounters";
    	return( title );
    }
    
    this.$Gallery_SearchKey = function( text, all ) {
    	var g = worldScripts["gallery"];
    	var k = g.$GalleryKeysForRole[ 0 ];
    	var found = false;
    	if( !text || text.length == 0 ) found = true; //no search, show the last item
    	else if( k ) {
    		if( !g.$GalleryShowAll ) { //search in name, not in dataKey
    			var n = g.$GalleryEncNames; 
    			if(g.$GalleryLog) log("Gallery",n);
    			for( var i = 0; i < n.length; i++ ) {
    				if ( n[i] && n[i].toLowerCase().indexOf(text) == 0 ) { //find text from begin
    					found = true;
    					g.$GalleryKeyi[ 0 ] = i;//set to the found item
    					i = n.length; //exit
    				}
    			}
    			if( !found ) {
    				for( var i = 0; i < n.length; i++ ) {
    					if ( n[i] && n[i].toLowerCase().indexOf(text) != -1 ) { //find text within
    						found = true;
    						g.$GalleryKeyi[ 0 ] = i;//set to the found item
    						i = n.length; //exit
    					}
    				}
    			}
    		}
    		if( !found ) for( var i = 0; i < k.length; i++ ) { //search in dataKey
    			if ( k[i] && k[i].toLowerCase().indexOf(text) == 0 ) { //find text from begin
    				found = true;
    				g.$GalleryKeyi[ 0 ] = i;//set to the found item
    				i = k.length; //exit
    			}
    		}
    		if( !found ) {
    			for( var i = 0; i < k.length; i++ ) {
    				if ( k[i] && k[i].toLowerCase().indexOf(text) != -1 ) { //find text within
    					found = true;
    					g.$GalleryKeyi[ 0 ] = i;//set to the found item
    					i = k.length; //exit
    				}
    			}
    		}
    		if( !found && text.length > 1 ) {
    			var text1 = text.charAt(0); //find starting letter only
    			for( var i = 0; i < k.length; i++ ) {
    				if ( k[i] && k[i].charAt(0).toLowerCase() == text1 ) {
    					found = true;
    					g.$GalleryKeyi[ 0 ] = i;//set to the found item
    					i = k.length; //exit
    				}
    			}
    		}
    	}
    	if( found ) g.$Gallery_Interface2( all ); //show it
    	else g.$Gallery_Search( all, false );//try again
    }
    
    /*
    this.$Gallery_Timed = function() { //extra ship.remove can not help avoid "Universe is full" due to the remaining entities
    	for( var i = 0; i < g.$GalleryTrash.length; i++ ) {
    		g.$GalleryTrash[i].remove(true);
    	}
    	if( g.$GalleryLog) log("Gallery", "Removed "+g.$GalleryTrash.length+" tried ship.");
    	delete g.$GalleryTrash;
    }
    */	
    
    this.$Gallery_Timed = function() { //cause extreme memory usage and crash when out of allocation space
    	var g = worldScripts["gallery"];
    	var maxk = 10; //how many iteration max., very slow and cause "Universe is full" if many OXP ships
    	var num = 64; //how many ships created at one time
    	var i = g.$GalleryTRole;//actual role in this timed function
    	var k = g.$GalleryTRI;//actual TRole Iteration
    	var prevlen = 0;
    
    	if( g.$GalleryTimer ) { //remove old timer
    		g.$GalleryTimer.stop();
    		delete g.$GalleryTimer;
    	}
    
    	if( g.$GalleryKeysForRole[ i ] ) prevlen = g.$GalleryKeysForRole[ i ].length;
    	var ships = system.addShips(g.$GalleryRoles[i], num, [0,0,0], 1000000);
    	if(ships) {
    		for( var j = 0; j < ships.length; j++ ) { //created ships
    			if( ships[j] ) {
    				var key = ships[j].dataKey;
    				if( !g.$GalleryKeysForRole[ i ] )
    					g.$GalleryKeysForRole[ i ] = [key];
    				else if( g.$GalleryKeysForRole[ i ].indexOf(key) == -1 )
    					g.$GalleryKeysForRole[ i ].push(key);//add new key
    				ships[j].remove(true);//can not remove everything, can reach Universe limit
    //				if(g.$GalleryLog) log("Gallery", i + ". main role: " + g.$GalleryRoles[i]
    //					+" "+k+". try, "+(j+1)+". key: "+key
    //					+" GalleryKeysForRole["+i+"]:"+g.$GalleryKeysForRole[ i ]);
    			}
    		}
    		if(g.$GalleryLog) log("Gallery", i + ". main role: " + g.$GalleryRoles[i]
    			+" "+k+". try: "+g.$GalleryKeysForRole[ i ].length+" object." );
    		if(prevlen == g.$GalleryKeysForRole[ i ].length) maxk = 0;//exit from iteration cycle
    	}
    	var end = false;
    	g.$GalleryTRI++;
    	if( g.$GalleryTRI > maxk ) {
    	
    		if(g.$GalleryKeysForRole[ i ]) {
    			g.$GalleryKeysForRole[ i ].sort();
    			if(g.$GalleryLog) log("Gallery", i + ". role after "+k+" try: " + g.$GalleryRoles[i]
    				+ " ("+g.$GalleryKeysForRole[ i ].length+"): " + g.$GalleryKeysForRole[ i ] );
    			g.$GalleryKeysForRole[ 0 ] = g.$GalleryKeysForRole[ 0 ].concat(g.$GalleryKeysForRole[ i ]);
    		}
    
    		g.$GalleryTRI = 1;
    		g.$GalleryTRole++;
    		if( g.$GalleryTRole >= g.$GalleryRoles.length ) {
    			
    			g.$GalleryKeysForRole[ 0 ].sort();
    			for( var i = 1; i < g.$GalleryKeysForRole[ 0 ].length; i++ ) { //remove duplicated keys
    				if(g.$GalleryKeysForRole[0][i] == g.$GalleryKeysForRole[0][i-1]) {
    					g.$GalleryKeysForRole[0].splice(i,1); //remove this item
    					i--;
    				}
    			}
    			if(g.$GalleryLog) log("Gallery", "All dataKeys ("+g.$GalleryKeysForRole[ 0 ].length
    				+"): " + g.$GalleryKeysForRole[ 0 ] );
    			end = true; //dataKey search finished, no more timer start
    		}
    	}
    
    	if( !end ) g.$GalleryTimer = new Timer(g, g.$Gallery_Timed, 2.3); //will make the next step
    }
    
    
    this.$GalleryValidateTarget = function(target) {
    
    	if( target.dataKey.indexOf("stealth") > -1 || target.primaryRole.indexOf("stealth") > -1
    		|| target.scriptInfo && target.scriptInfo.ccl_missionShip //skip mission ships
    		|| target.name == "Constrictor"
    		|| target.dataKey == "vector_arn" //mission ship in Vector OXP
    		|| target.primaryRole.indexOf("rescue_blackbox") > -1 ) //mission ships in Rescue Stations OXP
    		return false;
    	
    	if( !target.roles ) return true; //bugfix for entities without any role
    		
    	var lib = worldScripts["tech_ref_lib"];
    	if( lib ) return( lib.$validateTarget(target) ); //call the original function if available
    	
    	//following lines comes from Technical Reference Library OXP by spara
    	//check scanclasses
    	if (this.$GalleryClassifiedScanClasses.indexOf(target.scanClass) != -1) return false;
    	//check roles
    	var targetRoles = target.roles;
    	var i;
    	for (i = 0; i < targetRoles.length; i++) {
    		if (this.$GalleryClassifiedRoles.indexOf(targetRoles[i]) != -1) {
    			return false;
    		}
    	}
    	//check for script_info key
    	if (target.scriptInfo && target.scriptInfo.classifiedShip) return false;
    	return true;
    }