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

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
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

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;
}