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