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

Expansion Library

Content

Manifest

from Expansion Manager's OXP list from Expansion Manifest
Description Library is a collection of useful snippets, helpers and resources to simplify some common tasks used by AddOns. Library is a collection of useful snippets, helpers and resources to simplify some common tasks used by AddOns.
Identifier oolite.oxp.Svengali.Library oolite.oxp.Svengali.Library
Title Library Library
Category Miscellaneous Miscellaneous
Author Svengali & BlackWolf Svengali & BlackWolf
Version 1.9.1 1.9.1
Tags config, configuration, effects, helper, library, misc, parent, tools, sanity, shaders config, configuration, effects, helper, library, misc, parent, tools, sanity, shaders
Required Oolite Version
Maximum Oolite Version
Required Expansions
Optional Expansions
Conflict Expansions
Dependent Expansions
  • oolite.oxp.Arquebus.ContextualJukebox:0.1
  • oolite.oxp.EricWalch.DeepSpacePirates:1.9
  • oolite.oxp.Lone_Wolf.ShieldCyclerNext:2.1.1
  • oolite.oxp.Svengali.BGS:2.5.4
  • oolite.oxp.Svengali.GNN:1.2
  • oolite.oxp.Svengali.HyperRadio2:2.0
  • oolite.oxp.Thargoid.Planetfall:2.32
  • oolite.oxp.phkb.XenonHUD:3.8.10
  • Information URL https://wiki.alioth.net/index.php/Library n/a
    Download URL https://wiki.alioth.net/img_auth.php/0/04/Library_1.9.1.oxz n/a
    License CC-by-nc-sa 4.0, partly MIT CC-by-nc-sa 4.0, partly MIT
    File Size n/a
    Upload date 1778224578

    Relationships Diagram

    Documentation

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

    Readme.txt

    Library 1.9 for Oolite
    Copyright 2016-2018 by Svengali, BlackWolf, & phkb.
    Licences: see below
    April 2026
    
    REQUIREMENTS:
    - Oolite v1.88
    
    DOCUMENTATION:
    - http://wiki.alioth.net/index.php/Library
    
    
    PROBLEMS:
    In case of problems, please report it: http://bb.oolite.space/viewtopic.php?f=4&t=18074
    
    Please include the following infos:
    - Oolite version (and if trunk or nightly is used the revision number)
    - OS, Graphics card (and driver version)
    - Fullscreen/Windowed mode
    - Shader mode
    - List of used OXPs (incl. versions)
    
    Licences:
    Creative Commons Attribution-NonCommercial-ShareAlike 4.0 Unported License.
    To view a copy of this license, visit http://creativecommons.org/licenses/by-nc-sa/4.0/ or
     send a letter to Creative Commons, 444 Castro Street, Suite 900, Mountain View, California, 94041, USA.
    
    The avatar pics and most of the character pics have been created by BlackWolf!
    
    Many thanks to:
    Ahruman, Cody, Commander McLane, Dr.J R Stockton, Eric Walch, Getafix, Michael Werle, Lone_Wolf, Nicholas Zakas, Nikos Barkas, Norbert Nagy, Paul Bourke, phkb, Smivs and Terry Yuen.
    
    Updates by phkb:
    V1.7.2: Added function names from Oolite 1.91.
    Tweaked layouts in Config to be more generous with text.
    V1.8: Moved all text into missiontext.plist for easier localisation.
    V1.8.1: Moved additional text into missiontext.plist.
    Added "News History" to record all news items received via GNN.
    V1.8.2: Fixed an invalid missiontext lookup for the log book.
    V1.8.3: Moved additional text into missiontext.plist.
    V1.8.4: Made sure display keys are used on initial PAD display.
    V1.8.5: Added Lib_Music code changes as suggested by tsoj here: https://bb.oolite.space/viewtopic.php?p=280702#p280702
            Moved the Commander Name and Ship Name data entries to Library PAD, suppressed the "Edit Ship Registration" F4 screen.
            Fixed some missing text expansions used for localisation.
    V1.9:   Fixed some more missing text expansions used for localisation.
            Added "quitGame" to global namespace exclusions (for Oolite 1.93).
            Removed the "Select item/Next Item" data entry method, now can select whole lines and press enter (except for EInt values).
    V1.9.1: Prevent removal of "Edit Ship Registration" if "Random Player/Ship name" OXP is installed.

    Equipment

    This expansion declares no equipment.

    Ships

    Name
    lib_ms
    lib_ms_cube_a
    lib_ms_cubes
    lib_ms_exhaust
    lib_ms_helper
    lib_ms_helper12
    lib_ms_helper12x
    lib_ms_helper3x
    lib_ms_helper3y
    lib_ms_helper4y
    lib_ms_helper6x
    lib_ms_helper6y
    lib_ms_helper_cut
    lib_ms_laser
    lib_ms_laserDist
    lib_ms_moon
    lib_ms_ov
    Boa

    Models

    This expansion declares no models.

    Scripts

    Path
    Scripts/Lib_2DCollision.js
    /* jshint bitwise:false, forin:false */
    /* (C) Svengali 2016-2018, License CC-by-nc-sa-4.0 */
    (function(){
    "use strict";
    this.name = "Lib_2DCollision";
    
    /** _inBox() - Checks if point is inside the bounding box
    	@box - Array. 4 positions in a.x,a.y,b.x,b.y
    	@pos - Vector. Position to be checked
    	@return - Boolean. True if inside
    */
    this._inBox = function(box,pos){
    	if(pos.x<box[0] || pos.x>box[1] || pos.y<box[2] || pos.y>box[3]) return false;
    	return true;
    };
    
    /** _inPoly() - Checks if a point is inside a polygon
    	@nvert - Number of vertices in the polygon
    	@vertx, verty - Arrays containing the x- and y-coordinates of the polygon's vertices
    	@pos - Vector. x- and y-coordinate of the test point
    	@con - Boolean. Concave shapes may need to set it
    	@return - Boolean. True if inside
    */
    this._inPoly = function(nvert,vertx,verty,pos,con){
    	var i,j,c = false,r1=0;
    	for(i=0,j=nvert-1;i<nvert;j=i++){
    		if(((verty[i]>pos.y)!==(verty[j]>pos.y)) && (pos.x<(vertx[j]-vertx[i])*(pos.y-verty[i])/(verty[j]-verty[i])+vertx[i])){
    			c = !c;
    			r1++;
    		}
    		if(!con && r1>1) break;
    	}
    	return c;
    };
    
    /** _onLine() - Checks if point is on a specified line
    	@pa - Vector. Starting point of the line
    	@pb - Vector. End point of the line
    	@pc - Vector. Position to be checked
    	@tol - Number. Tolerance. Default 0.01
    	@return - Boolean. True if on line (within tolerance)
    */
    this._onLine = function(pa,pb,pc,tol){
    	var s,rnum,denom,check,distLine,distSegment,dist1,dist2;
    	if(typeof(tol)!=='number') tol = 0.01;
    	rnum = (pc.x-pa.x)*(pb.x-pa.x)+(pc.y-pa.y)*(pb.y-pa.y);
    	denom = (pb.x-pa.x)*(pb.x-pa.x)+(pb.y-pa.y)*(pb.y-pa.y);
    	check = rnum/denom;
    	s = ((pa.y-pc.y)*(pb.x-pa.x)-(pa.x-pc.x)*(pb.y-pa.y))/denom;
    	distLine = Math.abs(s)*Math.sqrt(denom);
    	if(check>=0 && check<=1) distSegment = distLine;
    	else {
    		dist1 = (pc.x-pa.x)*(pc.x-pa.x)+(pc.y-pa.y)*(pc.y-pa.y);
    		dist2 = (pc.x-pb.x)*(pc.x-pb.x)+(pc.y-pb.y)*(pc.y-pb.y);
    		if(dist1<dist2) distSegment = Math.sqrt(dist1);
    		else distSegment = Math.sqrt(dist2);
    	}
    	return (distSegment<tol);
    };
    }).call(this);
    
    Scripts/Lib_Animator.js
    /* jshint bitwise:false, forin:false */
    /* global _Animator,Sound,SoundSource,Timer,Vector3D,addFrameCallback,clock,isValidFrameCallback,mission,player,removeFrameCallback,setScreenBackground,setScreenOverlay,worldScripts */
    /* (C) Svengali 2016-2018, License CC-by-nc-sa-4.0 */
    (function(){
    "use strict";
    this.name = "Lib_Animator";
    /** 
    * TODO:
    * 	Flags - append
    *	Fixme - modifyMaterials
    */
    this._start = function(ani){
    	if(!player.ship.docked || !ani || !ani.model || !ani.flow || (this._a && !ani.replace && !ani.capture)) return false;
    	if((this._a && (!ani.replace || !ani.capture) ) && (!mission.displayModel || mission.displayModel.dataKey!==ani.model)) return false;
    	var obj = worldScripts.Lib_Main._lib.objClone(ani),hud,hudHide,i,msc;
    	if(this._a && (obj.replace || obj.capture)){
    		this._a.reset();
    		if(this.$anim){
    			hud = this.$anim.prevHUD;
    			hudHide = this.$anim.prevHUDHide;
    		}
    		this._cleanUp(1);
    	}
    	this._a = new this._Animator();
    	if(!obj.capture){
    		msc = {
    			background: null,
    			choices: (obj.choices?obj.choices:null),
    			choicesKey: (obj.choicesKey?obj.choicesKey:null),
    			model: (obj.model?obj.model:null),
    			music: (obj.music?obj.music:null),
    			overlay: null,
    			screenID: (obj.screenID?obj.screenID:"Lib_Animator"),
    			spinModel: false,
    			title: (obj.title?obj.title:"")
    		};
    		if(obj.text) msc.message = obj.text;
    		if(obj.background){
    			msc.background = obj.background;
    			this._a.bg.name = obj.background;
    		}
    		if(obj.overlay){
    			msc.overlay = obj.overlay;
    			this._a.ov.name = obj.overlay;
    		}
    		if(obj.fadeIn) msc.overlay = {name:"lib_blend10.png",width:1024,height:512};
    	}
    	this.$anim = {
    		count: 0,
    		index: 0,
    		flow: obj.flow,
    		caller: (obj.caller?obj.caller:null),
    		callback: (obj.callback?obj.callback:null),
    		checkpoint: (obj.checkpoint?obj.checkpoint:null),
    		prevHUD: (hud?hud:player.ship.hud),
    		prevHUDHide: (typeof hudHide!=="undefined"?hudHide:player.ship.hudHidden),
    		custom: obj.custom,
    		capture: obj.capture?obj.capture:null,
    		md: obj.model
    	};
    	if(!obj.capture){
    		if(obj.hud){
    			player.ship.hud = obj.hud;
    			player.ship.hudHidden = false;
    		} else player.ship.hudHidden = true;
    		mission.runScreen(msc,this._choiceEval);
    	}
    	if(!mission.displayModel) return false;
    	this._a.init(mission.displayModel);
    	this.$waitUntil = -1;
    	if(obj.corner) this._a.screenCornerPos(obj.corner);
    	if(obj.aPos) for(i=0;i<obj.aPos.length;i++) this._a.setPosition(obj.aPos[i]);
    	if(obj.aExh) for(i=0;i<obj.aExh.length;i++) this._a.setExhaust(obj.aExh[i]);
    	if(obj.aOris) for(i=0;i<obj.aOris.length;i++) this._a.setOrientation(obj.aOris[i]);
    	if(obj.aBinds) for(i=0;i<obj.aBinds.length;i++) this._a.setBinding(obj.aBinds[i]);
    	if(obj.aProps) for(i=0;i<obj.aProps.length;i++) this._a.setProp(obj.aProps[i]);
    	if(obj.bPos) for(i=0;i<obj.bPos.length;i++) this._a.setPosition(obj.bPos[i]);
    	if(obj.bOris) for(i=0;i<obj.bOris.length;i++) this._a.setOrientation(obj.bOris[i]);
    	if(obj.bBinds) for(i=0;i<obj.bBinds.length;i++) this._a.setBinding(obj.bBinds[i]);
    	if(obj.bProps) for(i=0;i<obj.bProps.length;i++) this._a.setProp(obj.bProps[i]);
    	if(obj.shadeMaps) for(i=0;i<obj.shadeMaps.length;i++) this._a.setShaderProp(obj.shadeMaps[i]);
    	if(obj.pilots) for(i=0;i<obj.pilots.length;i++) this._a._setPilot(obj.pilots[i]);
    	if(obj.subRots) for(i=0;i<obj.subRots.length;i++) this._a.subs[obj.subRots[i].e].subEntityRotation = obj.subRots[i].r;
    	if(obj.subTex) for(i=0;i<obj.subTex.length;i++) this._a.setMaterials(obj.subTex[i]);
    	this.$sndA = new SoundSource();
    	this.$sndB = new SoundSource();
    	if(obj.delta) this._a.defDelta = obj.delta;
    	else this._a.defDelta = 0.005;
    	if(!this.$animTimer) this.$animTimer = new Timer(this,this._doAnimTimer,0,0.25);
    	else this.$animTimer.start();
    	return true;
    };
    this._cleanUp = function(all){
    	if(this.$animTimer) this.$animTimer.stop();
    	delete this.$animTimer;
    	if(all){
    		delete this.$anim;
    		delete this.$sndA;
    		delete this.$sndB;
    		delete this._a;
    	}
    };
    this._choiceEval = function(choice){worldScripts.Lib_Animator._choice(choice); return;};
    this._choice = function(choice){
    	this._a.reset();
    	player.ship.hud = this.$anim.prevHUD;
    	player.ship.hudHidden = this.$anim.prevHUDHide;
    	var a = this.$anim.caller, b = this.$anim.callback;
    	this._cleanUp(1);
    	if(a && b) worldScripts[a][b](choice);
    	return;
    };
    this._doAnimTimer = function _doAnimTimer(){
    	if(!this.$anim.flow.length || !mission.displayModel || !mission.displayModel.isValid || this.$anim.index>=this.$anim.flow.length){
    		this._a.reset();
    		this._cleanUp();
    		return;
    	}
    	var cur = this.$anim.flow[this.$anim.index],i,p;
    	if(this.$anim.count >= cur[0]){
    		// [time,cmd,obj,other]
    		switch(cur[1]){
    			case "reset": this._a.reset(); break;
    			case "clr": this._a.clear(); break;
    			case "clrMov": this._a.clearMoves(); break;
    			case "kill": // Get CPU usage down
    				if(this.$anim.index===this.$anim.flow.length-1){
    					mission.displayModel.remove();
    					if(cur[2] && cur[2].txt) mission.addMessageText(cur[2].txt);
    				}
    				break;
    			case "rotw": this._a.rotateW(cur[2]); break;
    			case "rot": this._a.rotate(cur[2]); break;
    			case "rotTo": this._a.rotateTo(cur[2]); break;
    			case "rotX": this._a.rotateXDeg(cur[2]); break;
    			case "rotY": this._a.rotateYDeg(cur[2]); break;
    			case "rotZ": this._a.rotateZDeg(cur[2]); break;
    			case "stopRot": this._a.stopRotate(cur[2]); break;
    			case "fly": this._a.flight(cur[2]); break;
    			case "flyTo": this._a.flightTo(cur[2]); break;
    			case "stopFly": this._a.stopFlight(cur[2]); break;
    			case "velo": this._a.velocity(cur[2]); break;
    			case "veloTo": this._a.velocityTo(cur[2]); break;
    			case "veloSet": this._a.setVelocity(cur[2]); break;
    			case "stopVelo": this._a.stopVelocity(cur[2]); break;
    			case "walk": this._a.walk(cur[2]); break;
    			case "walkTo": this._a.walkTo(cur[2]); break;
    			case "stopWalk": this._a.stopWalk(cur[2]); break;
    			case "aiDest": this._a.aiDestination(cur[2]); break;
    			case "aiFace": this._a.aiFace(cur[2]); break;
    			case "aiFly": this._a.aiFly(cur[2]); break;
    			case "aiHold": this._a.aiHold(cur[2]); break;
    			case "aiRange": this._a.aiRange(cur[2]); break;
    			case "aiSpeed": this._a.aiSpeed(cur[2]); break;
    			case "aiStop": this._a.aiStop(cur[2]); break;
    			case "zoom": this._a.zoom(cur[2]); break;
    			case "zoomTo": this._a.zoomTo(cur[2]); break;
    			case "stopZoom": this._a.stopZoom(cur[2]); break;
    			case "prop": this._a.setProp(cur[2]); break;
    			case "matSet": this._a.setMaterials(cur[2]); break;
    			case "matMod": this._a.modifyMaterials(cur[2]); break;
    			case "lit": this._a.highlight(cur[2]); break;
    			case "tex": this._a.setTextures(cur[2]); break;
    			case "shadeSet": this._a.setShader(cur[2]); break;
    			case "shadeMod": this._a.setShaderProp(cur[2]); break;
    			case "sun": this._a.sun(cur[2]); break;
    			case "twinkle": this._a.twinkle(cur[2]); break;
    			case "moon": this._a.moon(cur[2]); break;
    			case "boom": this._a.explosion(cur[2]); break;
    			case "hyper":
    				this._a.hyper(cur[2]);
    				this.$sndB.playSound("[bgs_fxWitch]");
    				break;
    			case "shoot":
    				var d = [0.00001,clock.absoluteSeconds,0];
    				//if(cur[2].rear) d[2] = cur[2].rear;
    				if(this._a.cur[cur[2].tg].ang>0.098){
    					this.$sndA.playSound("[player-laser-miss]");
    				} else {
    					d[0] = 1/this._a.cur[cur[2].tg].mag;
    					this.$sndA.playSound("[player-laser-hit]");
    				}
    				this._a.setProp({e:cur[2].e,p:"destination",v:d});
    				break;
    			case "laser":
    				var act, tg, mag, ang, v;
    				var d = [0.00001,clock.absoluteSeconds,0];
    				if(cur[2].e>-1) act = this._a.subs[cur[2].e];
    				else act = this._a.ent;
    				if(cur[2].tg>-1) tg = this._a.subs[cur[2].tg];
    				else tg = this._a.ent;
    				v = act.position.subtract(tg.position).direction().multiply(-1);
    				mag = act.position.subtract(tg.position).magnitude();
    				ang = act.heading.angleTo(v);
    				if(ang>0.098){
    					this.$sndA.playSound("[player-laser-miss]");
    				} else {
    					d[0] = 1/mag;
    					this.$sndA.playSound("[player-laser-hit]");
    				}
    				this._a.setProp({e:cur[2].e,p:"destination",v:d});
    				break;
    			case "bind": this._a.setBinding(cur[2]); break;
    			case "stopBind": this._a.stopBinding(cur[2]); break;
    			case "pos": this._a.setPosition(cur[2]); break;
    			case "posTo": this._a.setPositionTo(cur[2]); break;
    			case "posFl": this._a.setFlasherPositionTo(cur[2]); break;
    			case "ori": this._a.setOrientation(cur[2]); break;
    			case "snap": this._a.snapIn(cur[2]); break;
    			case "bg": this._a.setBackground(cur[2]); break;
    			case "bgZoom": this._a.zoomBackground(cur[2]); break;
    			case "bgZoomTo": this._a.zoomBackgroundTo(cur[2]); break;
    			case "bgStop": this._a.stopBackground(); break;
    			case "bgClr": this._a.clearBackground(); break;
    			case "bgRes": this._a.resetBackground(cur[2]); break;
    			case "ov": this._a.setOverlay(cur[2]); break;
    			case "ovZoom": this._a.zoomOverlay(cur[2]); break;
    			case "ovStop": this._a.stopOverlay(); break;
    			case "ovClr": this._a.clearOverlay(); break;
    			case "ovFadeIn": this._a.fadeIn(cur[2]); break;
    			case "ovFadeOut": this._a.fadeOut(cur[2]); break;
    			case "corner": this._a.screenCornerPos(cur[2]); break;
    			case "txt": mission.addMessageText(cur[2]); break;
    			case "snd1": this.$sndA.playSound(cur[2].snd); break;
    			case "snd2": this.$sndB.playSound(cur[2].snd); break;
    			case "mus": Sound.playMusic(cur[2].snd); break;
    			case "custom": this.$anim.custom[cur[2]](); break;
    			case "goto": this._goto(cur[2]); break;
    			case "check":
    				var ch = false;
    				if(this.$anim.caller && this.$anim.checkpoint) ch = worldScripts[this.$anim.caller][this.$anim.checkpoint](cur[0]);
    				if(typeof ch==='number') this._goto(ch);
    				else {
    					if(!ch){this._a.reset(); this._cleanUp();}
    				}
    				break;
    			case "waitExt":
    				if(this.$waitUntil>-1){
    					this._goto(this.$waitUntil);
    					this.$waitUntil = -1;
    				} else {
    					this.$anim.index--;
    					this.$anim.count--;
    				}
    				break;
    			case "speak":
    				this._a._pilotSpeak(cur[2]);
    				if(cur[2].snd) this.$sndB.playSound(cur[2].snd);
    				break;
    		}
    		if(cur[3]){
    			for(i=0;i<cur[3].length;i++){
    				p = cur[3][i];
    				switch(p.id){
    					case "txt": mission.addMessageText(p.txt); break;
    					case "snd1": this.$sndA.playSound(p.snd); break;
    					case "snd2": this.$sndB.playSound(p.snd); break;
    					case "mus": Sound.playMusic(p.snd); break;
    				}
    			}
    		}
    		this.$anim.index++;
    	}
    	this.$anim.count++;
    	return;
    };
    this._goto = function(cmp){
    	var from,to;
    	if(cmp>this.$anim.count){
    		from = this.$anim.index;
    		to = this.$anim.flow.length;
    	} else {
    		from = 0;
    		to = this.$anim.index;
    	}
    	for(var i=from;i<to;i++){
    		if(this.$anim.flow[i][0]===cmp){
    			this.$anim.index = i-1;
    			this.$anim.count = cmp-1;
    			break;
    		}
    	}
    };
    /** Animation API
    */
    this._Animator = function(){};
    _Animator.prototype = {
    	constructor: _Animator,
    	w: 1024,
    	h: 512,
    	fcb: 0,
    	fcbs: [],
    	bg: {name:null,width:this.w,height:this.h,mw:1,mh:1},
    	ov: {name:null,width:this.w,height:this.h,mw:1,mh:1},
    	ovFade: {n:10,dir:1,t:2,cur:0},
    	defDelta: 0.005,
    	ent: null,
    	subs: null,
    	flow: {
    		bind:{},
    		flight:{},
    		props:{},
    		rotate:{},
    		velocity:{},
    		walk:{},
    		zoom:{},
    		bg:{},
    		ov:{},
    		aiDest:{}
    	},
    	cur: {},
    	/** ent = Entity. Must be called before anything else!
    	*/
    	init: function(ent){
    		this.cur = {"-1":{mf:0,ang:0,mag:0,tOri:null}};
    		this.ent = ent;
    		if(ent.subEntities && ent.subEntities.length){
    			this.subs = ent.subEntities;
    			for(var i=0;i<ent.subEntities.length;i++) this.cur[i] = {mf:0,ang:0,mag:0,tOri:null};
    		}
    		else this.subs = null;
    		this.w = 1024;
    		this.h = 512;
    		this.bg.width = this.w;
    		this.bg.height = this.h;
    		this.ov.width = this.w;
    		this.ov.height = this.h;
    	},
    	reset: function(){
    		this.clear();
    		this.bg = {name:null,width:this.w,height:this.h,mw:1,mh:1};
    		this.ov = {name:null,width:this.w,height:this.h,mw:1,mh:1};
    		this.ovFade = {n:10,dir:1,t:2,cur:0};
    		this.ent = null;
    		this.subs = null;
    		this.defDelta = 0.005;
    	},
    	clear: function(){
    		this._removeFCBs();
    		this.flow = {
    			bind:{},
    			flight:{},
    			props:{},
    			rotate:{},
    			velocity:{},
    			walk:{},
    			zoom:{},
    			bg:{},
    			ov:{},
    			aiDest:{}
    		};
    	},
    	clearMoves: function(){
    		var m = ["flight","rotate","velocity","walk","zoom","aiDest"],k;
    		for(var o=0;o<m.length;o++){
    			k = Object.keys(this.flow[m[o]]);
    			for(var i=0;i<k.length;i++){
    				this._stopIt({e:k[i]},m[o]);
    			}
    		}
    	},
    	/** e, t, rx,ry,rz [, z, da, db]		World!
    	*/
    	rotateW: function(obj){
    		var c = [this.ent,this.subs,0,this.defDelta,obj];
    		this.fcb = addFrameCallback(function(delta){
    			if(!delta || !c[0] || !c[0].isValid) return;
    			var f=1, bz=1, da=1, db=1, act;
    			if(c[3]>delta) f = 1/(c[3]/delta);
    			else f = 1-(c[3]-delta);
    			c[2] += delta;
    			if(c[2]>=c[4].t) return;
    			if(c[4].e>-1) act = c[1][c[4].e];
    			else act = c[0];
    			if(c[4].z) bz = Math.sqrt(Math.abs(act.position.z));
    			if(c[4].da && c[2]<c[4].da) da = 1-((c[4].da-c[2])/c[4].da);
    			if(c[4].db && c[2]>c[4].t-c[4].db) db = (c[4].t-c[2])/c[4].db;
    			if(c[4].rx) act.orientation = act.orientation.rotateX(c[4].rx*da*db*bz*f);
    			if(c[4].ry) act.orientation = act.orientation.rotateY(c[4].ry*da*db*bz*f);
    			if(c[4].rz) act.orientation = act.orientation.rotateZ(c[4].rz*da*db*bz*f);
    			return;
    		});
    		this._addTo("rotate",this.fcb,obj);
    		return true;
    	},
    	/** e, t, rx,ry,rz [, z, da, db]		Model!
    	*/
    	rotate: function(obj){
    		var c = [this.ent,this.subs,0,this.defDelta,obj];
    		this.fcb = addFrameCallback(function(delta){
    			if(!delta || !c[0] || !c[0].isValid) return;
    			var f=1, bz=1, da=1, db=1, act;
    			if(c[3]>delta) f = 1/(c[3]/delta);
    			else f = 1-(c[3]-delta);
    			c[2] += delta;
    			if(c[2]>=c[4].t) return;
    			if(c[4].e>-1) act = c[1][c[4].e];
    			else act = c[0];
    			if(c[4].z) bz = Math.sqrt(Math.abs(act.position.z));
    			if(c[4].da && c[2]<c[4].da) da = 1-((c[4].da-c[2])/c[4].da);
    			if(c[4].db && c[2]>c[4].t-c[4].db) db = (c[4].t-c[2])/c[4].db;
    			if(c[4].rx) act.orientation = act.orientation.rotate(act.vectorRight,c[4].rx*da*db*bz*f);
    			if(c[4].ry) act.orientation = act.orientation.rotate(act.vectorUp,c[4].ry*da*db*bz*f);
    			if(c[4].rz) act.orientation = act.orientation.rotate(act.vectorForward,c[4].rz*da*db*bz*f);
    			return;
    		});
    		this._addTo("rotate",this.fcb,obj);
    		return true;
    	},
    	/** e, t, tg [, z, da, db, s]
    	*/
    	rotateTo: function(obj){
    		this._stopIt(obj,"rotate");
    		if(!obj.tg.isShip){
    			if(typeof obj.tg==='number') obj.tg = this.subs[obj.tg];
    			else if(obj.tg.constructor.name==="Array") obj.tgv = new Vector3D(obj.tg);
    		}
    		var c = [this.ent,this.subs,0,this.defDelta,obj,this.cur[obj.e]];
    		this.fcb = addFrameCallback(function(delta){
    			if(!delta || !c[0] || !c[0].isValid || !c[4].tg || (!c[4].tg.isValid && !c[4].tgv)) return;
    			var f=1, bz=1, da=1, db=1, act, v, a, cr, steps = 0.01;
    			if(c[3]>delta) f = 1/(c[3]/delta);
    			else f = 1-(c[3]-delta);
    			c[2] += delta;
    			if(c[2]>=c[4].t) return;
    			if(c[4].e>-1) act = c[1][c[4].e];
    			else act = c[0];
    			if(c[4].tgv){
    				v = act.position.subtract(c[4].tgv).direction().multiply(-1);
    				c[5].mag = act.position.subtract(c[4].tgv).magnitude();
    			} else {
    				v = act.position.subtract(c[4].tg.position).direction().multiply(-1);
    				c[5].mag = act.position.subtract(c[4].tg.position).magnitude();
    			}
    			a = act.heading.angleTo(v);
    			c[5].ang = a;
    			if(c[4].z) bz = 1-1/Math.sqrt(Math.max(Math.abs(act.position.z),0.001));
    			if(c[4].da && c[2]<c[4].da) da = 1-((c[4].da-c[2])/c[4].da);
    			if(c[4].db && c[2]>c[4].t-c[4].db) db = (c[4].t-c[2])/c[4].db;
    			if(c[4].s) steps = c[4].s*da;
    			cr = act.heading.cross(v);
    			act.orientation = act.orientation.rotate(cr,-a*steps*f*bz*da*db);
    			return;
    		});
    		this._addTo("rotate",this.fcb,obj);
    		return true;
    	},
    	/** e, t, deg [, z, da, db, inf]		World!
    	*/
    	rotateXDeg: function(obj){
    		if(obj.e>-1) obj.nt = this.subs[obj.e].orientation.rotateX(0.017453293916206696*obj.deg);
    		else obj.nt = this.ent.orientation.rotateX(0.017453293916206696*obj.deg);
    		if(obj.inf) this.cur[obj.e].tOri = obj.nt;
    		obj.st = obj.deg/(obj.t*1000);
    		var c = [this.ent,this.subs,0,this.defDelta,obj];
    		this.fcb = addFrameCallback(function(delta){
    			if(!delta || !c[0] || !c[0].isValid) return;
    			var f=1, bz=1, da=1, db=1, act, sp;
    			if(c[3]>delta) f = 1/(c[3]/delta);
    			else f = 1-(c[3]-delta);
    			c[2] += delta;
    			if(c[2]>=c[4].t) return;
    			if(c[4].e>-1) act = c[1][c[4].e];
    			else act = c[0];
    			sp = act.orientation.dot(c[4].nt);
    			if(sp>0.999) return;
    			if(c[4].z) bz = Math.sqrt(Math.abs(act.position.z));
    			if(c[4].da && c[2]<c[4].da) da = 1-((c[4].da-c[2])/c[4].da);
    			if(c[4].db && c[2]>c[4].t-c[4].db) db = (c[4].t-c[2])/c[4].db;
    			if(c[4].deg) act.orientation = act.orientation.rotateX(c[4].st*da*db*bz*f);
    			return;
    		});
    		this._addTo("rotate",this.fcb,obj);
    		return true;
    	},
    	/** e, t, deg [, z, da, db, inf]		World!
    	*/
    	rotateYDeg: function(obj){
    		if(obj.e>-1) obj.nt = this.subs[obj.e].orientation.rotateY(0.017453293916206696*obj.deg);
    		else obj.nt = this.ent.orientation.rotateY(0.017453293916206696*obj.deg);
    		if(obj.inf) this.cur[obj.e].tOri = obj.nt;
    		obj.st = obj.deg/(obj.t*1000);
    //		obj.st = ((0.017453293916206696*obj.deg)/obj.t)*this.defDelta;
    		var c = [this.ent,this.subs,0,this.defDelta,obj];
    		this.fcb = addFrameCallback(function(delta){
    			if(!delta || !c[0] || !c[0].isValid) return;
    			var f=1, bz=1, da=1, db=1, act, sp;
    			if(c[3]>delta) f = 1/(c[3]/delta);
    			else f = 1-(c[3]-delta);
    			c[2] += delta;
    			if(c[2]>=c[4].t) return;
    			if(c[4].e>-1) act = c[1][c[4].e];
    			else act = c[0];
    			sp = act.orientation.dot(c[4].nt);
    			if(sp>0.9999) return;
    			if(c[4].z) bz = Math.sqrt(Math.abs(act.position.z));
    			if(c[4].da && c[2]<c[4].da) da = 1-((c[4].da-c[2])/c[4].da);
    			if(c[4].db && c[2]>c[4].t-c[4].db) db = (c[4].t-c[2])/c[4].db;
    			if(c[4].deg) act.orientation = act.orientation.rotateY(c[4].st*da*db*bz*f);
    			return;
    		});
    		this._addTo("rotate",this.fcb,obj);
    		return true;
    	},
    	/** e, t, deg [, z, da, db, inf]		World!
    	*/
    	rotateZDeg: function(obj){
    		if(obj.e>-1) obj.nt = this.subs[obj.e].orientation.rotateZ(0.017453293916206696*obj.deg);
    		else obj.nt = this.ent.orientation.rotateZ(0.017453293916206696*obj.deg);
    		if(obj.inf) this.cur[obj.e].tOri = obj.nt;
    		obj.st = obj.deg/(obj.t*1000);
    		var c = [this.ent,this.subs,0,this.defDelta,obj];
    		this.fcb = addFrameCallback(function(delta){
    			if(!delta || !c[0] || !c[0].isValid) return;
    			var f=1, bz=1, da=1, db=1, act, sp;
    			if(c[3]>delta) f = 1/(c[3]/delta);
    			else f = 1-(c[3]-delta);
    			c[2] += delta;
    			if(c[2]>=c[4].t) return;
    			if(c[4].e>-1) act = c[1][c[4].e];
    			else act = c[0];
    			sp = act.orientation.dot(c[4].nt);
    			if(sp>0.999) return;
    			if(c[4].z) bz = Math.sqrt(Math.abs(act.position.z));
    			if(c[4].da && c[2]<c[4].da) da = 1-((c[4].da-c[2])/c[4].da);
    			if(c[4].db && c[2]>c[4].t-c[4].db) db = (c[4].t-c[2])/c[4].db;
    			if(c[4].deg) act.orientation = act.orientation.rotateZ(c[4].st*da*db*bz*f);
    			return;
    		});
    		this._addTo("rotate",this.fcb,obj);
    		return true;
    	},
    	/** e
    	*/
    	stopRotate: function(obj){
    		this._stopIt(obj,"rotate");
    		return true;
    	},
    	/** e, t, mu,mr,mf, rx,ry,rz [,da, db, z, fu, en]
    	*/
    	flight: function(obj){
    		this.rotate(obj);
    		this.velocity(obj);
    	},
    	/** e, t, tg, s, mu,mr,mf [, da, db, z, fu, en]
    	*/
    	flightTo: function(obj){
    		this.rotateTo(obj);
    		this.velocityTo(obj);
    	},
    	/** e
    	*/
    	stopFlight: function(obj){
    		this.stopVelocity(obj);
    		this.stopRotate(obj);
    		return true;
    	},
    	aiDestination: function(obj){
    		var act;
    		if(obj.e>-1) act = this.subs[obj.e];
    		else act = this.ent;
    		this._stopIt(obj,"aiDest");
    		if(obj.dest){
    			if(typeof obj.dest==='number') act.destination = this.subs[obj.dest];
    			else act.destination = obj.dest;
    		} else {
    			var c = [this.ent,this.subs,0,this.defDelta,obj,0];
    			this.fcb = addFrameCallback(function(delta){
    				if(!delta || !c[0] || !c[0].isValid) return;
    				var act, st;
    				c[2] += delta;
    				c[5]++;
    				// Updating only every 50. framecallback!!!
    				if(c[2]>=c[4].t || c[5]<50) return;
    				if(c[4].e>-1) act = c[1][c[4].e];
    				else act = c[0];
    				act.destination = c[1][c[4].tg].position;
    				c[5] = 0;
    				return;
    			});
    			this._addTo("aiDest",this.fcb,obj);
    		}
    	},
    	aiFace: function(obj){
    		this.aiDestination(obj);
    		if(obj.e>-1) this.subs[obj.e].performFaceDestination();
    		else this.ent.performFaceDestination();
    	},
    	aiFly: function(obj){
    		this.aiSpeed(obj);
    		this.aiRange(obj);
    		this.aiDestination(obj);
    		var act;
    		if(obj.e>-1) act = this.subs[obj.e];
    		else act = this.ent;
    		act.performFlyToRangeFromDestination();
    	},
    	aiHold: function(obj){
    		if(obj.e>-1) this.subs[obj.e].performHold();
    		else this.ent.performHold();
    		this._stopIt(obj,"aiDest");
    	},
    	aiRange: function(obj){
    		if(obj.e>-1) this.subs[obj.e].desiredRange = obj.rng;
    		else this.ent.desiredRange = obj.rng;
    	},
    	aiSpeed: function(obj){
    		if(obj.e>-1) this.subs[obj.e].desiredSpeed = obj.spd;
    		else this.ent.desiredSpeed = obj.spd;
    	},
    	aiStop: function(obj){
    		if(obj.e>-1) this.subs[obj.e].performStop();
    		else this.ent.performStop();
    		this._stopIt(obj,"aiDest");
    	},
    	/** e, t, mu,mr,mf [,da, db, z, fu, en]
    	*/
    	velocity: function(obj){
    		var c = [this.ent,this.subs,0,this.defDelta,obj];
    		this.fcb = addFrameCallback(function(delta){
    			if(!delta || !c[0] || !c[0].isValid) return;
    			var f=1, bz=1, da=1, db=1, act;
    			if(c[3]>delta) f = 1/(c[3]/delta);
    			else f = 1-(c[3]-delta);
    			c[2] += delta;
    			if(c[2]>=c[4].t) return;
    			if(c[4].e>-1) act = c[1][c[4].e];
    			else act = c[0];
    			if(c[4].z) bz = Math.sqrt(Math.abs(act.position.z));
    			if(c[4].da && c[2]<c[4].da) da = 1-((c[4].da-c[2])/c[4].da);
    			if(c[4].db && c[2]>c[4].t-c[4].db) db = (c[4].t-c[2])/c[4].db;
    			if(c[4].mu) act.velocity = act.vectorUp.multiply(c[4].mu*bz*da*db*f);
    			if(c[4].mr) act.velocity = act.vectorRight.multiply(c[4].mr*bz*da*db*f);
    			if(c[4].mf) act.velocity = act.vectorForward.multiply(c[4].mf*bz*da*db*f);
    			if(c[4].fu) act.fuel = (act.velocity.magnitude()/act.maxSpeed)*7;
    			if(c[4].en) act.energy = (act.velocity.magnitude()/act.maxSpeed)*act.maxEnergy;
    			return;
    		});
    		this._addTo("velocity",this.fcb,obj);
    		return true;
    	},
    	/** e, t, tg, mu,mr,mf [, da, db, z, fu, en]
    	*/
    	velocityTo: function(obj){
    		this._stopIt(obj,"velocity");
    		var x;
    		if(!obj.tg.isShip){
    			if(typeof obj.tg==='number') obj.tg = this.subs[obj.tg];
    			else if(obj.tg.constructor.name==="Array") obj.tgv = new Vector3D(obj.tg);
    		} else if(obj.tg.isInSpace) obj.tgv = obj.tg.position;
    		if(obj.e>-1) x = this.subs[obj.e].velocity.magnitude();
    		else x = this.ent.velocity.magnitude();
    		if(x>1) obj.da = obj.da/x;
    		var c = [this.ent,this.subs,0,this.defDelta,obj,this.cur[obj.e],0];
    		this.fcb = addFrameCallback(function(delta){
    			if(!delta || !c[0] || !c[0].isValid || c[6]) return;
    			var f=1, bz=1, da=1, db=1, act, col, v, dist, mul,slow, check;
    			if(c[3]>delta) f = 1/(c[3]/delta);
    			else f = 1-(c[3]-delta);
    			c[2] += delta;
    			if(c[2]>=c[4].t) return;
    			if(c[4].e>-1) act = c[1][c[4].e];
    			else act = c[0];
    			col = act.collisionRadius;
    			if(c[4].tgv) v = c[4].tgv;
    			else v = c[4].tg;
    			if(c[4].tg.isInSpace) col += c[4].tg.collisionRadius;
    			dist = act.position.distanceTo(v);
    			if(dist<col*4){
    				f *= 1/(col*4/dist);
    				slow = 1;
    			}
    			if(dist<col){
    				if(!c[6]){
    					act.velocity = [0,0,0];
    					c[6] = 1;
    					if(c[4].fu) act.fuel = 0;
    					if(c[4].en) act.energy = 0;
    					c[5].mf = 0;
    				}
    				return;
    			}
    			if(c[4].z) bz = Math.sqrt(Math.abs(act.position.z));
    			if(c[4].da && c[2]<c[4].da) da = 1-((c[4].da-c[2])/c[4].da);
    			if(c[4].db && c[2]>c[4].t-c[4].db) db = (c[4].t-c[2])/c[4].db;
    			mul = bz*da*db*f;
    			if(c[4].mu) act.velocity = act.vectorUp.multiply(c[4].mu*mul);
    			if(c[4].mr) act.velocity = act.vectorRight.multiply(c[4].mr*mul);
    			if(c[4].mf){
    				check = c[4].mf*mul;
    				if(!slow && c[5].mf>check) check = c[5].mf;
    				act.velocity = act.vectorForward.multiply(check);
    				c[5].mf = check;
    			}
    			if(c[4].fu) act.fuel = (act.velocity.magnitude()/act.maxSpeed)*7;
    			if(c[4].en) act.energy = (act.velocity.magnitude()/act.maxSpeed)*act.maxEnergy;
    			return;
    		});
    		this._addTo("velocity",this.fcb,obj);
    		return true;
    	},
    	/** e, velo
    	*/
    	setVelocity: function(obj){
    		this._stopIt(obj,"velocity");
    		if(obj.e>-1) this.ent.subEntities[obj.e].velocity = obj.velo;
    		else this.ent.velocity = obj.velo;
    		return true;
    	},
    	/** e
    	*/
    	stopVelocity: function(obj){
    		this._stopIt(obj,"velocity");
    		if(obj.e>-1) this.ent.subEntities[obj.e].velocity = [0,0,0];
    		else this.ent.velocity = [0,0,0];
    		return true;
    	},
    	/** e, t, st [,z, da, db]
    	*/
    	walk: function(obj){
    		var c = [this.ent,this.subs,0,this.defDelta,obj];
    		this.fcb = addFrameCallback(function(delta){
    			if(!delta || !c[0] || !c[0].isValid) return;
    			var f=1, bz=1, da=1, db=1, act, up,rol, mul, steps;
    			if(c[3]>delta) f = 1/(c[3]/delta);
    			else f = 1-(c[3]-delta);
    			c[2] += delta;
    			steps = Math.floor(c[2]);
    			if(c[2]>=c[4].t || steps>=c[4].st) return;
    			if(c[4].e>-1) act = c[1][c[4].e];
    			else act = c[0];
    			if(c[4].z) bz = Math.sqrt(Math.abs(act.position.z));
    			if(c[4].da && c[2]<c[4].da) da = 1-((c[4].da-c[2])/c[4].da);
    			if(c[4].db && c[2]>c[4].t-c[4].db) db = (c[4].t-c[2])/c[4].db;
    			mul = Math.PI*bz*da*db*f;
    			rol = Math.cos(c[2]*mul);
    			up = Math.cos(c[2]*3*mul);
    			act.position = act.position.subtract([rol*0.05,up*0.15,mul*0.15]);
    			return;
    		});
    		this._addTo("walk",this.fcb,obj);
    		return true;
    	},
    	walkTo: function(){},
    	/** e
    	*/
    	stopWalk: function(obj){
    		this._stopIt(obj,"walk");
    		return true;
    	},
    	/** e, t, mz [, z, da, db]
    	*/
    	zoom: function(obj){
    		var c = [this.ent,this.subs,0,this.defDelta,obj];
    		this.fcb = addFrameCallback(function(delta){
    			if(!delta || !c[0] || !c[0].isValid) return;
    			var f=1, bz=1, da=1, db=1, act, mf=1;
    			if(c[3]>delta) f = 1/(c[3]/delta);
    			else f = 1-(c[3]-delta);
    			c[2] += delta;
    			if(c[2]>=c[4].t) return;
    			if(c[4].e>-1) act = c[1][c[4].e];
    			else act = c[0];
    			if(c[4].z) bz = Math.sqrt(Math.abs(act.position.z));
    			if(c[4].da && c[2]<c[4].da) da = 1-((c[4].da-c[2])/c[4].da);
    			if(c[4].db && c[2]>c[4].t-c[4].db) db = (c[4].t-c[2])/c[4].db;
    			if(c[4].mz>1) mf = 1+(da*db*bz*(c[4].mz%1));
    			else mf = 1-(f*da*db*bz*(1%c[4].mz));
    			act.position = act.position.multiply(mf);
    			return;
    		});
    		this._addTo("zoom",this.fcb,obj);
    		return true;
    	},
    	zoomTo: function(){},
    	/** e
    	*/
    	stopZoom: function(obj){
    		this._stopIt(obj,"zoom");
    		return true;
    	},
    	/** e, p, v [, t, da, db]
    	*/
    	setProp: function(obj){
    		if(!obj.t){
    			if(obj.e>-1) this.subs[obj.e][obj.p] = obj.v;
    			else this.ent[obj.p] = obj.v;
    		} else {
    			var c = [this.ent,this.subs,0,this.defDelta,obj];
    			this.fcb = addFrameCallback(function(delta){
    				if(!delta || !c[0] || !c[0].isValid) return;
    				var f=1, da=1, db=1, act;
    				if(c[3]>delta) f = 1/(c[3]/delta);
    				else f = 1-(c[3]-delta);
    				c[2] += delta;
    				if(c[2]>=c[4].t) return;
    				if(c[4].e>-1) act = c[1][c[4].e];
    				else act = c[0];
    				if(c[4].da && c[2]<c[4].da) da = 1-((c[4].da-c[2])/c[4].da);
    				if(c[4].db && c[2]>c[4].t-c[4].db) db = (c[4].t-c[2])/c[4].db;
    				if(typeof act[c[4].p]==='number') act[c[4].p] += c[4].v*f*da*db/c[4].t;
    				else act[c[4].p] = act[c[4].p].multiply(c[4].v*f*da*db/c[4].t); // TODO check
    				return;
    			});
    			this._addTo("props",this.fcb,obj);
    		}
    		return true;
    	},
    	/** e, mat, sha
    	*/
    	setMaterials: function(obj){
    		var act;
    		if(obj.e>-1) act = this.subs[obj.e];
    		else act = this.ent;
    		if(obj.mat && obj.sha) act.setMaterials(obj.mat,obj.sha);
    		else if(obj.mat) act.setMaterials(obj.mat,{});
    		else if(obj.sha) act.setMaterials({},obj.sha);
    		return true;
    	},
    	/** e, prop
    	* i, p, v
    	*/
    	modifyMaterials: function(obj,prop){
    		var m = this._getMaterials(obj),k = Object.keys(m),w;
    		if(!k.length) return;
    		for(var o=0;o<obj.prop.length;o++){
    			w = obj.prop[o];
    			m[k[w.i]][w.p] = w.v;
    		}
    		this._setMaterials(obj,m);
    	},
    	/** e, mat, tex, col
    	*/
    	highlight: function(obj){
    		var act,m;
    		if(obj.e>-1) act = this.subs[obj.e];
    		else act = this.ent;
    		m = act.getMaterials();
    		m[obj.mat].emission_map = obj.tex;
    		m[obj.mat].emission_modulate_color = obj.col;
    		act.setMaterials(m);
    	},
    	setTextures: function(obj){},
    	/** e, sha [, rep]
    	*/
    	setShader: function(obj){
    		if(obj.rep){
    			var k = Object.keys(obj.sha),w,h,v;
    			for(var o=0;o<k.length;o++){
    				w = obj.sha[k[o]].uniforms;
    				h = Object.keys(w);
    				for(var i=0;i<h.length;i++){
    					v = obj.sha[k[o]].uniforms[h[i]];
    					if(v.value==="p1") v.value = clock.absoluteSeconds-1;
    				}
    			}
    		}
    		if(obj.e>-1) this.subs[obj.e].setShaders(obj.sha);
    		else this.ent.setShaders(obj.sha);
    		return true;
    	},
    	/** e, tex (Array), uni (Array), val (Array)
    	*/
    	setShaderProp: function(obj){
    		var a,b,flagM,i;
    		a = this._getShaders(obj);
    		b = Object.keys(a);
    		if(!b.length){
    			a = this._getMaterials(obj);
    			flagM = 1;
    			b = Object.keys(a);
    		}
    		if(!b.length) return false;
    		if(obj.tex) for(i=0;i<obj.tex.length;i++) a[b[0]].textures = obj.tex;
    		if(obj.uni) for(i=0;i<obj.uni.length;i++) a[b[0]].uniforms[obj.uni[i]].value = obj.val[i];
    		if(flagM){
    			if(obj.e>-1) this.subs[obj.e].setMaterials(a);
    			else this.ent.setMaterials(a);
    		} else {
    			if(obj.e>-1) this.subs[obj.e].setShaders(a);
    			else this.ent.setShaders(a);
    		}
    		return true;
    	},
    	/** e, id, scale, pos
    	*/
    	explosion: function(obj){
    		var act,sha = {};
    		sha[obj.id] = {
    			fragment_shader:"lib_ms_whexit.fs",
    			vertex_shader:"lib_ms_bbT.vs",
    			textures:["lib_explode2.png"],
    			uniforms:{
    				colorMap:{type:"texture",value:0},
    				Time:{binding:"universalTime"},
    				Dest:{binding:"destination",bindToSubentity:true,normalized:false}}};
    		if(obj.e>-1) act = this.subs[obj.e];
    		else act = this.ent;
    		act.destination = [2,clock.absoluteSeconds-1,obj.scale];
    		act.setShaders(sha);
    		if(obj.pos) act.position = obj.pos;
    	},
    	/** e, id, scale, pos
    	*/
    	hyper: function(obj){
    		var act,sha = {};
    		sha[obj.id] = {
    			fragment_shader:"lib_ms_whexit.fs",
    			vertex_shader:"lib_ms_bbT.vs",
    			textures:["lib_hyper.png"],
    			uniforms:{
    				colorMap:{type:"texture",value:0},
    				Time:{binding:"universalTime"},
    				Dest:{binding:"destination",bindToSubentity:true,normalized:false}}};
    		if(obj.e>-1) act = this.subs[obj.e];
    		else act = this.ent;
    		act.destination = [6,clock.absoluteSeconds-1,obj.scale];
    		act.setShaders(sha);
    		if(obj.pos) act.position = obj.pos;
    	},
    	/** e, id, scale, pos, col
    	*/
    	sun: function(obj){
    		var act,sha = {};
    		sha[obj.id] = {
    			fragment_shader:"lib_ms_sun.fs",
    			vertex_shader:"lib_ms_bbT.vs",
    			uniforms:{
    				Suncolor:{type:"vector",value:obj.col},
    				Time:{type:"float",value:0},
    				Dest:{binding:"destination",bindToSubentity:true,normalized:false}}};
    		if(obj.e>-1) act = this.subs[obj.e];
    		else act = this.ent;
    		act.destination = [1,1,obj.scale];
    		act.setShaders(sha);
    		if(obj.pos) act.position = obj.pos;
    	},
    	/** e, id, scale, pos, col
    	*/
    	twinkle: function(obj){
    		var act,sha = {};
    		sha[obj.id] = {
    			fragment_shader:"lib_ms_bg_twinkle.fs",
    			vertex_shader:"lib_ms_bbT.vs",
    			uniforms:{
    				Time:{binding:"universalTime"},
    				Dest:{binding:"destination",bindToSubentity:true,normalized:false}}};
    		if(obj.e>-1) act = this.subs[obj.e];
    		else act = this.ent;
    		act.destination = [99999999,0,obj.scale];
    		act.setShaders(sha);
    		if(obj.pos) act.position = obj.pos;
    	},
    	/** e, id, scale, pos, col
    	*/
    	moon: function(obj){
    		var act,sha = {};
    		sha[obj.id] = {
    			fragment_shader:"lib_ms_moon.fs",
    			vertex_shader:"lib_ms_moon.vs",
    			textures:[{name:obj.tex,cube_map:true}],
    			uniforms:{
    				colorMap:{type:"texture",value:0},
    				Suncolor:{type:"vector",value:obj.col},
    				Scale:{type:"float",value:obj.scale}}};
    		if(obj.e>-1) act = this.subs[obj.e];
    		else act = this.ent;
    		act.setShaders(sha);
    		if(obj.pos) act.position = obj.pos;
    	},
    	/** e, t, tg, [exh, fe, dist, swap]
    	*/
    	setBinding: function(obj){
    		if(typeof obj.tg==='number'){
    			if(obj.tg>-1) obj.tg = this.subs[obj.tg];
    			else obj.tg = this.ent;
    		}
    		if(obj.dist){
    			if(obj.e>-1) obj.distv = this.subs[obj.e].position.subtract(obj.tg.position);
    			else obj.distv = obj.tg.position;
    		}
    		var c = [this.ent,this.subs,0,this.defDelta,obj];
    		this.fcb = addFrameCallback(function(delta){
    			if(!delta || !c[0] || !c[0].isValid) return;
    			var f=1, act, p, o, s = 0;
    			if(c[3]>delta) f = 1/(c[3]/delta);
    			else f = 1-(c[3]-delta);
    			c[2] += delta;
    			if(c[2]>=c[4].t) return;
    			if(c[4].e>-1) act = c[1][c[4].e];
    			else act = c[0];
    			if(c[4].fe){
    				act.fuel = c[4].tg.fuel;
    				act.energy = c[4].tg.energy;
    			}
    			if(c[4].exh){
    				if(c[4].tg.desiredSpeed) s = c[4].tg.speed+1;
    				act.fuel = (s/c[4].tg.maxSpeed)*0.7;
    				act.energy = (s/c[4].tg.maxSpeed)*c[4].tg.maxEnergy;
    			}
    			if(c[4].dsor){
    				act.destination = c[4].tg.position;
    				act.energy = 0;
    			}
    			if(c[4].distv){
    				o = c[4].tg.orientation;
    				if(c[4].swap) o = o.rotate(c[4].tg.vectorUp,-Math.PI);
    				act.orientation = o;
    				p = c[4].tg.position.add(c[4].distv.rotateBy(o));
    				act.position = p;
    			}
    			return;
    		});
    		this._addTo("bind",this.fcb,obj);
    		return true;
    	},
    	/** e
    	*/
    	stopBinding: function(obj){
    		this._stopIt(obj,"bind");
    		return true;
    	},
    	/** e, pos
    	*/
    	setPosition: function(obj){
    		if(obj.e>-1) this.subs[obj.e].position = obj.pos;
    		else this.ent.position = obj.pos;
    		return true;
    	},
    	/** e, tg, ex, p
    	*/
    	setExhaust: function(obj){
    		if(obj.e>-1){
    			this.subs[obj.e].position = this.subs[obj.tg].exhausts[obj.ex].position;
    			this.subs[obj.e][obj.p] = this.subs[obj.tg].exhausts[obj.ex].size.multiply(0.11764); // .dat 1/8.5
    		}
    		return true;
    	},
    	/** e, tg [, off]
    	*/
    	setPositionTo: function(obj){
    		var pos;
    		if(obj.tg>-1) pos = this.subs[obj.tg].position;
    		else pos = this.ent.position;
    		if(obj.off) pos.add(obj.off);
    		if(obj.e>-1) this.subs[obj.e].position = pos;
    		else this.ent.position = pos;
    		return true;
    	},
    	/** e, f, tg [, off]
    	*/
    	setFlasherPositionTo: function(obj){
    		var pos;
    		if(obj.tg>-1) pos = this.subs[obj.tg].position;
    		else pos = this.ent.position;
    		if(obj.off) pos = pos.add(obj.off);
    		if(obj.e>-1) this.subs[obj.e].flashers[obj.f].position = pos;
    		else this.ent.flashers[obj.f].position = pos;
    		return true;
    	},
    	/** e, ori
    	*/
    	setOrientation: function(obj){
    		if(obj.e>-1) this.subs[obj.e].orientation = obj.ori;
    		else this.ent.orientation = obj.ori;
    		return true;
    	},
    	/** e, inf
    	*/
    	snapIn: function(obj){
    		this._stopIt(obj,"rotate");
    		var act,ori;
    		if(obj.e>-1) act = this.subs[obj.e];
    		else act = this.ent;
    		ori = act.orientation;
    		ori.w = this._snapIn(ori.w);
    		ori.x = this._snapIn(ori.x);
    		ori.y = this._snapIn(ori.y);
    		ori.z = this._snapIn(ori.z);
    		act.orientation = ori;
    	},
    	/** n
    	*/
    	setBackground: function(obj){
    		this.bg.name = obj.n;
    		setScreenBackground(this.bg);
    	},
    	/** mw, mh [, n]
    	*/
    	zoomBackground: function(obj){
    		this.bg.mw = obj.mw;
    		this.bg.mh = obj.mh;
    		if(obj.n) this.bg.name = obj.n;
    		this.fcb = addFrameCallback(this._zoomBG.bind(this));
    		this.fcbs.push(this.fcb);
    		this.flow.bg["-1"] = [this.fcb];
    	},
    	zoomBackgroundTo: function(){},
    	/**
    	*/
    	stopBackground: function(){
    		this._stopIt({e:-1},"bg");
    	},
    	/**
    	*/
    	clearBackground: function(){
    		this.stopBackground();
    		setScreenBackground(null);
    	},
    	resetBackground: function(obj){
    		this.stopBackground();
    		this.bg = {name:null,width:this.w,height:this.h,mw:1,mh:1};
    		if(obj && obj.n) this.setBackground(obj);
    	},
    	/** n
    	*/
    	setOverlay: function(obj){
    		this.ov.name = obj.n;
    		setScreenOverlay(this.ov);
    	},
    	/** mw, mh [, n]
    	*/
    	zoomOverlay: function(obj){
    		this.ov.mw = obj.mw;
    		this.ov.mh = obj.mh;
    		if(obj.n) this.ov.name = obj.n;
    		this.fcb = addFrameCallback(this._zoomOV.bind(this));
    		this.fcbs.push(this.fcb);
    		this.flow.ov["-1"] = [this.fcb];
    	},
    	/**
    	*/
    	stopOverlay: function(){
    		this._stopIt({e:-1},"ov");
    	},
    	/**
    	*/
    	clearOverlay: function(){
    		this.stopOverlay();
    		setScreenOverlay(null);
    	},
    	/** t
    	*/
    	fadeIn: function(obj){
    		this._stopIt({e:-1},"ov");
    		this.ov = {name:"lib_blend0.png",width:this.w,height:this.h,mw:1,mh:1};
    		this.ovFade = {n:0,dir:1,t:obj.t};
    		this.fcb = addFrameCallback(this._fadeOV.bind(this));
    		this.fcbs.push(this.fcb);
    		this.flow.ov["-1"] = [this.fcb];
    	},
    	/** t
    	*/
    	fadeOut: function(obj){
    		this._stopIt({e:-1},"ov");
    		this.ov = {name:"lib_blend10.png",width:this.w,height:this.h,mw:1,mh:1};
    		this.ovFade = {n:0,dir:0,t:obj.t};
    		this.fcb = addFrameCallback(this._fadeOV.bind(this));
    		this.fcbs.push(this.fcb);
    		this.flow.ov["-1"] = [this.fcb];
    	},
    	/** e, lv, x, y, mz
    	*/
    	screenCornerPos: function(obj){
    		var pos,
    			w = 1024/this.w,
    			h = 768/this.h;
    		if(obj.e>-1) pos = this.subs[obj.e].position;
    		else pos = this.ent.position;
    		pos.x = obj.lv*pos.z*obj.x;
    		pos.y = obj.lv*pos.z*0.75*obj.y;
    		if(obj.mz) pos.z *= obj.mz;
    		else pos.z *= 1.3;
    		var wh = w/h;
    		pos = pos.add([w*wh-1,h*wh-1,0]);
    		this.ent.position = pos;
    		return true;
    	},
    	// Sub functions
    	_addTo: function(w,id,obj){
    		this.fcbs.push(id);
    		if(this.flow[w][obj.e]) this.flow[w][obj.e].push(id);
    		else this.flow[w][obj.e] = [id];
    	},
    	/** e, what
    	*/
    	_stopIt: function(obj,what){
    		var f = this.flow[what][obj.e],ind;
    		if(!f) return false;
    		for(var i=0;i<f.length;i++){
    			if(isValidFrameCallback(f[i])) removeFrameCallback(f[i]);
    			ind = this.fcbs.indexOf(f[i]);
    			if(ind!==-1) this.fcbs.splice(ind,1);
    		}
    		this.flow[what][obj.e] = [];
    		return true;
    	},
    	/**
    	*/
    	_zoomBG: function(delta){
    		if(!delta) return;
    		this.bg.width *= this.bg.mw;
    		this.bg.height *= this.bg.mh;
    		setScreenBackground(this.bg);
    	},
    	/**
    	*/
    	_zoomOV: function(delta){
    		if(!delta) return;
    		this.ov.width *= this.ov.mw;
    		this.ov.height *= this.ov.mh;
    		setScreenOverlay(this.ov);
    	},
    	/**
    	*/
    	_fadeOV: function(delta){
    		if(!delta) return;
    		var f = Math.floor(this.ovFade.n*10/this.ovFade.t);
    		if(this.ovFade.dir) f = 10-f;
    		else f++;
    		if(f<0) f = 0;
    		if(f>10) f = 10;
    		this.ov.name = "lib_blend"+f+".png";
    		setScreenOverlay(this.ov);
    		this.ovFade.n += delta;
    	},
    	_removeFCBs: function(){
    		if(!this.fcbs.length) return false;
    		for(var i=0;i<this.fcbs.length;i++){
    			if(isValidFrameCallback(this.fcbs[i])) removeFrameCallback(this.fcbs[i]);
    		}
    		this.fcb = 0;
    		this.fcbs = [];
    		return true;
    	},
    	_getMaterials: function(obj){
    		if(obj.e>-1) return this.subs[obj.e].getMaterials();
    		return this.ent.getMaterials();
    	},
    	_setMaterials: function(obj,m){
    		if(obj.e>-1) return this.subs[obj.e].setMaterials(m);
    		return this.ent.setMaterials(m);
    	},
    	_getShaders: function(obj){
    		if(obj.e>-1) return this.subs[obj.e].getShaders();
    		return this.ent.getShaders();
    	},
    	_setPilot: function(obj){
    		var act;
    		this.setShaderProp(obj);
    		if(obj.e>-1) act = this.subs[obj.e];
    		else act = this.ent;
    		act.destination = obj.scpos;
    		act.lightsActive = obj.init;
    	},
    	_pilotSpeak: function(obj){
    		var act;
    		if(obj.e>-1) act = this.subs[obj.e];
    		else act = this.ent;
    		act.energy = obj.fade;
    		if(obj.txt) mission.addMessageText(obj.txt);
    	},
    	_sRND: function(seed,max){
    		return ((0x731753*seed)>>16)%max;
    	},
    	_snapIn: function(e){
    		if(e>0.9) e = 1;
    		if(e>0.6 && e<0.8) e = 0.707107;
    		if(e>0.4 && e<0.6) e = 0.5;
    		if(e>-0.1 && e<0.1) e = 0.0;
    		if(e>-0.6 && e<-0.4) e = -0.5;
    		if(e>-0.8 && e<-0.6) e = -0.707107;
    		if(e<-0.9) e = -1;
    		return e;
    	}
    };
    }).call(this);
    Scripts/Lib_Animator.txt
    Lib_Animator
    
    The Animator is a helper for animations on missionscreens.
    
    Methods:
    
    _start(obj)
    worldScripts.Lib_Animator._start(obj);
    
    Required members:
    obj.model		- String. Role
    obj.flow		- Array. Animation flow object.
    
    Optional members:
    obj.background	- String.
    obj.choices		- Object.
    obj.choicesKey	- String.
    obj.music		- String
    obj.overlay		- String.
    obj.screenID	- String.
    obj.title		- String.
    
    obj.fadeIn		- Bool. If used replaces obj.overlay.
    obj.corner		- Object. See below.
    obj.aPos		- Array.
    obj.aOris		- Array.
    obj.aBinds		- Array.
    obj.aProps		- Array.
    obj.bPos		- Array.
    obj.bOris		- Array.
    obj.bBinds		- Array.
    obj.bProps		- Array.
    obj.pilots		- Array. Sets texture, scale, position and display for character overlay.
    
    obj.caller		- String. worldScript name for callback and checkpoints.
    obj.callback	- String. Function to be called when user  
    obj.checkpoint	- String. Function to be called when 'check' gets processed.
    obj.custom		- Object. Holds custom Functions to be called when 'custom' gets processed.
    obj.hud			- String.
    
    obj.aSnd		- String.
    obj.bSnd		- String.
    obj.delta		- Number.
    
    
    obj.flow
    	
    	Array containing Arrays with at least 2 members (frame and cmd).
    		frame	- Number.
    		cmd		- String. Action command.
    		obj		- Object or String.
    		subCMD	- Array.
    
    Action commands:
    
    reset
    	Removes all Framecallbacks
    clr
    	Removes all Framecallbacks and resets 
    clrMov
    	Removes all object movement Framecallbacks (flight, rotate, speed, velocity, walk and zoom).
    kill
    	Removes mission.displayModel.
    
    rotw
    	Rotate entity in world space.
    	e, t, rx,ry,rz [, z, da, db]
    rot
    	Rotate entity in model space
    	e, t, rx,ry,rz [, z, da, db]
    rotTo
    	Rotate entity to target. Clears other rotations for this entity.
    	e, t, tg [, z, da, db, s]
    stopRot
    	Clear rotations for this entity.
    	e
    fly
    	e, t, mu,mr,mf, rx,ry,rz [,da, db, z, fu, en]
    flyTo
    	e, t, tg, s, mu,mr,mf [, da, db, z, fu, en]
    stopFly
    	e
    KI
    stopKI
    
    spd
    spdTo
    stopSpd
    velo
    	e, t, mu,mr,mf [,da, db, z, fu, en]
    veloTo
    	e, t, tg, mu,mr,mf [, da, db, z, fu, en]
    veloSet
    	e, velo
    stopVelo
    	e
    walk
    	e, t, st [,z, da, db]
    walkTo
    	
    stopWalk
    	e
    zoom
    	e, t, mz [, z, da, db]
    zoomTo
    	
    stopZoom
    	e
    prop
    	e, t, p, v [, da, db]
    matSet
    	e, mat, sha
    matMod
    	e, prop
    		i, p, v
    tex
    shadeSet
    	e, sha, rep
    shadeMod
    	e, tex, uni, val
    sun
    	Replaces shader with textureless sun shader.
    	e, id, scale, pos, col
    moon
    	Replaces shader with cubemapped moon shader.
    	e, id, scale, pos, col
    boom
    	Replaces shader with explosion shader.
    	e, id, scale, pos
    hyper
    	Replaces shader with hyperjump exit shader and plays sound.
    	e, id, scale, pos
    shoot
    	Sets energy for laser shader and plays sound.
    speak
    	Sets energy for character overlay and plays sound or adds message text.
    bind
    	e, t, tg [,fe, dist, swap]
    stopBind
    	e
    pos
    	e, pos
    posTo
    	e, tg [, off]
    posFl
    	e, f, tg [, off]
    ori
    	e, ori
    bg
    	n
    bgZoom
    	mw, mh [, n]
    bgZoomTo
    
    bgStop
    	none
    bgClr
    	none
    ov
    	n
    ovZoom
    	mw, mh [, n]
    ovStop
    	none
    ovClr
    	none
    ovFadeIn
    	t
    ovFadeOut
    	t
    corner
    	e, lv, x, y [, mz]
    txt
    	String.
    snd1
    	snd
    snd2
    	snd
    mus
    	snd
    custom
    	Calls custom function declared in the passed obj.custom.
    	String.
    goto
    	Goto animation frame time.
    	Number.
    check
    	Request action from obj.caller. Passes current frame time.
    	If returned value is a Number goto frame.
    	If no returned value (or false) stops the animation.
    
    Scripts/Lib_BinSearch.js
    /* jshint bitwise:false, forin:false */
    /* global _lib_BinSearch */
    /* (C) Svengali 2016-2018, License CC-by-nc-sa-4.0 */
    (function(){
    "use strict";
    this.name = "Lib_BinSearch";
    
    /** This search tree uses two corresponding entries.
    *	Must be instantiated! 
    *	this.$myTree = new worldScripts.Lib_BinSearch._lib_BinSearch();
    */
    this._lib_BinSearch = function(){this._lbs = null;};
    _lib_BinSearch.prototype = {
    	constructor: _lib_BinSearch,
    
    	/** add() - Adds nodes to the search tree with the corresponding entry.
    	@key - String. Add node with key.
    	@val - Object. Corresponding entry
    	*/
    	add: function(key,val){
    		var node = {key: key,left: null,right: null, val: val},cur;
    		if(this._lbs === null) this._lbs = node;
    		else {
    			cur = this._lbs;
    			while(true){
    				if(key < cur.key){
    					if(cur.left === null){
    						cur.left = node;
    						break;
    					} else cur = cur.left;
    				} else if(key > cur.key){
    					if(cur.right === null){
    						cur.right = node;
    						break;
    					} else cur = cur.right;
    				} else break;
    			}
    		}
    		return;
    	},
    
    	/** contains() - Returns corresponing entry or false.
    	@key - String. Search for entry.
    	@return - Object. Corresponding entry or false.
    	*/
    	contains: function(key){
    		var found = false,cur = this._lbs;
    		while(!found && cur){
    			if(key < cur.key) cur = cur.left;
    			else if(key > cur.key) cur = cur.right;
    			else found = true;
    		}
    		if(!found) return false;
    		return cur.val;
    	},
    
    	traverse: function(process){
    		function inOrder(node){
    			if(node){
    				if(node.left !== null) inOrder(node.left);
    				process.call(this,node);
    				if(node.right !== null) inOrder(node.right);
    			}
    		}
    		inOrder(this._lbs);
    	},
    
    	/** size() - Returns the number of nodes in the search tree.
    	@return - Number. Number of nodes.
    	*/
    	size: function(){
    		var length = 0;
    		this.traverse(function(node){length++;});
    		return length;
    	},
    
    	/** toArray() - Returns an array containing all nodes in the search tree. The nodes are processed in-order.
    	@return - Array. Entries.
    	*/
    	toArray: function(){
    		var result = [];
    		this.traverse(function(node){result.push(node.key,node.val);});
    		return result;
    	},
    
    	/** toString() - Returns a comma-separated string consisting of all entries. The nodes are processed in-order.
    	@separator - String. Optional. If specified separates the elements.
    	@return - String. Concatenation of all entries.
    	*/
    	toString: function(separator){
    		if(separator) return this.toArray().join(separator);
    		else return this.toArray().toString();
    	}
    };
    }).call(this);
    
    Scripts/Lib_Config.js
    /* jshint bitwise:false, forin:false */
    /* global expandMissionText,log,mission,player,worldScripts */
    /* (C) Svengali 2016-2018, License CC-by-nc-sa-4.0 */
    (function () {
    	"use strict";
    	this.name = "Lib_Config";
    	this.description = expandMissionText("LIB_CONFIG_DESCRIPTION");
    
    	this.$sets = {}; // Settings objects
    	this.$setNames = []; // Sorting
    	this.$setProbs = {}; // Error codes
    	this.$curSet = null;
    	this.$defCHC = {
    		allowInterrupt: false,
    		choices: {
    			ZZZ: expandMissionText("LIB_CONFIG_EXIT")
    		},
    		initialChoicesKey: null,
    		message: "",
    		screenID: this.name,
    		textEntry: false,
    		title: expandMissionText("LIB_CONFIG_TITLE")
    	};
    	this.$oxpc = {
    		curBit: 0,
    		curInd: 0,
    		curKeys: [],
    		curMSB: 0,
    		curOpt: 0,
    		curPage: 0,
    		curTyp: 0,
    		div: "",
    		entry: 0,
    		intern: 0,
    		resort: 0,
    		scr: ['Bool', 'SInt', 'EInt']
    	};
    	/** _registerSet(obj) - Use on .startUpComplete() or later.
    		@obj - Settings object
    		@return Number. Error code. 0 - ok, >0 - error (see missiontext.plist)
    	*/
    	this._registerSet = function (obj) {
    		if (this.startUp) return 99;
    		if (!obj || typeof obj !== 'object') return 101;
    		if (!Object.keys(obj).length) return 102;
    		var e = this._checkSet(obj);
    		if (e) return e;
    		var id = obj.Name + obj.Display,
    			ind = this.$setNames.indexOf(id);
    		this.$sets[id] = obj; // Store or update
    		if (ind === -1) { // No double entries
    			this.$setNames.push(id);
    			this.$oxpc.entry++;
    			this.$oxpc.resort = 1;
    			this.$setProbs[id] = e;
    		}
    		return 0;
    	};
    	/** _unregisterSet(obj)
    		@obj - Settings object
    		@return Number. Error code. 0 - ok, >0 - error (see missiontext.plist)
    	*/
    	this._unregisterSet = function (obj) {
    		if (typeof obj !== 'object') return 501;
    		var id = obj.Name + obj.Display,
    			ind = this.$setNames.indexOf(id);
    		if (!this.$sets[id]) return 502;
    		if (ind === -1) return 503;
    		delete this.$sets[id];
    		this.$oxpc.entry--;
    		this.$oxpc.resort = 1;
    		this.$setNames.splice(ind, 1);
    		return 0;
    	};
    	this._updSet = function (obj) {
    		if (typeof obj !== 'object') return 601;
    		var ind = -1,
    			tmp, tmps,
    			e = this._checkSet(obj);
    		if (!e || e === 211) {
    			ind = this.$setNames.indexOf(obj.Name + obj.Display);
    			if (ind !== -1) {
    				tmp = this.$setNames[ind];
    				if (tmp) {
    					if (!e) {
    						tmps = this._aid.objGrab(worldScripts[obj.Name], obj.Alive);
    						this.$sets[tmp] = tmps[0][tmps[1]];
    					}
    					this.$curSet = this.$sets[tmp];
    				}
    			} else { // Not registered
    				this.$curSet = obj;
    				this.$oxpc.intern = 0;
    				return e;
    			}
    		} else this.$curSet = obj;
    		return e;
    	};
    	this.startUp = function () {
    		delete this.startUp;
    		this._aid = worldScripts.Lib_Main._lib; // Covered by manifest
    		worldScripts.Lib_GUI.$IDRules.Lib_Config = {
    			pic: 1,
    			mus: 1
    		};
    		this.$oxpc.div = this._aid.scrToWidth("-", 31, "-") + "\n";
    	};
    	this.startUpComplete = function () {
    		delete this.startUpComplete;
    		this.shipDockedWithStation(); // Implement interface
    	};
    	this.shipDockedWithStation = function () {
    		if (!player.ship || !player.ship.isValid || !player.ship.docked) return;
    		player.ship.dockedStation.setInterface(this.name, {
    			title: expandMissionText("LIB_CONFIG_INTERFACE_TITLE"),
    			category: expandMissionText("LIB_CONFIG_INTERFACE_CATEGORY"),
    			summary: this.description,
    			callback: this._showStart.bind(this)
    		});
    	};
    	this._checkSet = function (obj) {
    		var e = 0,
    			t = ['string', 'function', 'object', 'undefined'],
    			noti;
    		if (typeof obj.Name !== t[0]) return 201;
    		if (typeof obj.Display !== t[0]) return 202;
    		if (typeof obj.Alive !== t[0]) return 203;
    		if (obj.Alias && typeof obj.Alias !== t[0]) return 209;
    		if (obj.Notify) {
    			if (typeof obj.Notify !== t[0]) return 204;
    			noti = this._aid.objGrab(worldScripts[obj.Name], obj.Notify);
    			if (typeof noti[0] === 'undefined') return 212;
    			if (typeof noti[0] !== t[1] && typeof noti[0][noti[1]] !== t[1]) return 212;
    		}
    		var k = this.$oxpc.scr,
    			v;
    		for (var i = 0; i < k.length; i++) {
    			v = k[i];
    			if (typeof obj[v] !== t[3]) {
    				if (typeof obj[v] !== t[2]) return 206 + i;
    				if (!obj[v] || !Object.keys(obj[v]).length) return 213 + i;
    			} else e++;
    		}
    		if (e === 3) return 205;
    		if (!this._checkAvail(obj)) return 211;
    		return 0;
    	};
    	this._checkAvail = function (obj) {
    		var ws = worldScripts[obj.Name];
    		if (!ws || ws.$deactivated) return false;
    		var a = this._aid.objGrab(ws, obj.Alive);
    		if (typeof a[0][a[1]] !== 'object' || !Object.keys(a[0][a[1]]).length) return false;
    		return true;
    	};
    	this._checkHeader = function (obj) {
    		var h = {
    			Name: "-",
    			Alias: 0,
    			Display: "-",
    			Mani: "-",
    			Author: "-",
    			Copyright: "-",
    			Licence: "-",
    			Desc: "-",
    			Version: "-",
    			Ref: false,
    			Extern: false
    		},
    			ws = worldScripts[obj.Name];
    		h.Ref = this._checkAvail(obj);
    		if (ws) {
    			if (ws.name) h.Name = ws.name;
    			if (obj.Name) h.Name = obj.Name;
    			if (ws.author) h.Author = ws.author;
    			if (ws.copyright) h.Copyright = ws.copyright;
    			if (ws.licence) h.Licence = ws.licence;
    			else if (ws.license) h.Licence = ws.license;
    			if (ws.description) h.Desc = ws.description;
    			if (ws.version) h.Version = ws.version;
    			if (ws.oolite_manifest_identifier) h.Mani = ws.oolite_manifest_identifier;
    		} else if (obj.Name) h.Name = obj.Name;
    		if (obj.Display) h.Display = obj.Display;
    		if (obj.Alias) h.Alias = obj.Alias;
    		if (!this.$oxpc.intern) h.Extern = true;
    		return h;
    	};
    	this._checkValues = function (obj, typ) {
    		var wc = obj[typ],
    			ws = worldScripts[obj.Name],
    			max = 16777215,
    			min = -16777215,
    			c, ty, de, val;
    		switch (typ) {
    			case 'Bool':
    				ty = 'boolean';
    				de = 'string';
    				break;
    			case 'SInt':
    				ty = 'number';
    				de = 'string';
    				break;
    			case 'EInt':
    				ty = 'number';
    				de = 'object';
    				break;
    		}
    		for (var t in wc) {
    			if (!wc[t]) return 301;
    			if (t === 'Info' || t === 'Notify') continue;
    			c = wc[t];
    			if (typeof c.Name !== 'string') return 302;
    			if (typeof c.Def !== ty) return 303;
    			if (typeof c.Desc !== de) return 304;
    			val = this._aid.objGet(ws, c.Name);
    			if (typeof val !== ty) return 401;
    			if (typ !== 'Bool') {
    				if (typeof c.Max !== ty) return 311;
    				if (typeof c.Min !== ty) return 312;
    				if (typ === 'EInt') min = 0;
    				if (c.Max < min) return 313;
    				if (c.Max > max) return 314;
    				if (c.Min > c.Max) return 315;
    				if (c.Min < min) return 316;
    				if (c.Def > c.Max) return 317;
    				if (c.Def < c.Min) return 318;
    				if (c.Def > max) return 319;
    				if (c.Def < min) return 320;
    				if (val > max || val > c.Max) return 411;
    				if (val < min || val < c.Min) return 412;
    			}
    		}
    		return 0;
    	};
    	this._fillOptions = function (chc, tit, ent) {
    		var obj = JSON.parse(JSON.stringify(this.$defCHC));
    		if (chc) obj.choices = chc;
    		if (tit) obj.title = tit;
    		if (ent) obj.textEntry = true;
    		return obj;
    	};
    	this._resort = function () {
    		this.$setNames = this.$setNames.sort();
    		this.$oxpc.resort = 0;
    	};
    	this._reset = function () {
    		this.$curSet = null;
    		this.$defCHC.initialChoicesKey = null;
    		var o = this.$oxpc;
    		o.curBit = 0;
    		o.curMSB = 0;
    		o.curInd = 0;
    		o.curKeys = [];
    		o.curOpt = 0;
    		o.curPage = 0;
    		o.curTyp = 0;
    		o.intern = 0;
    	};
    	this._showScr = function (obj, mes, mesKey, cb) {
    		if (mesKey) {
    			obj.messageKey = mesKey;
    			obj.message = null;
    		} else if (mes) obj.message = mes;
    		if (cb) mission.runScreen(obj, cb);
    		else mission.runScreen(obj, this._choiceEval);
    	};
    	this._showStart = function () {
    		var ch = {
    			goLI: expandMissionText("LIB_CONFIG_LIST_SETTINGS"),
    			ZZZ: expandMissionText("LIB_CONFIG_EXIT")
    		},
    			chc, txt,
    			o = this.$oxpc;
    		this._reset();
    		if (!o.entry) ch = this._aid.scrChcUnsel(ch, 'goLI');
    		chc = this._fillOptions(ch);
    		chc.exitScreen = "GUI_SCREEN_INTERFACES";
    		this._showScr(chc, 0, 'LIBC_HEAD');
    		txt = o.div;
    		o.intern = 1;
    		txt += expandMissionText("LIB_CONFIG_REGISTERED", { count: o.entry });
    		mission.addMessageText(txt);
    		if (o.resort) this._resort();
    	};
    	this._showList = function () {
    		var ch = {
    			//LINext: expandMissionText("LIB_CONFIG_NEXTSET"),
    			//lioOXP: expandMissionText("LIB_CONFIG_SELECTSET"),
    			LIPNext: expandMissionText("LIB_CONFIG_NEXTPAGE"),
    			LIPPrev: expandMissionText("LIB_CONFIG_PREVPAGE"),
    			LIRet: expandMissionText("LIB_CONFIG_BACK")
    		},
    			chc, txt, tmp, tmq, ind,
    			o = this.$oxpc,
    			max = o.curPage * 10,
    			e = o.entry;
    		var big = false;
    		if (player.ship.hudAllowsBigGui) big = true;
    		txt = this._aid.scrAddLine([
    			["", 1],
    			[expandMissionText("LIB_CONFIG_COL_SETTINGS"), 10],
    			[expandMissionText("LIB_CONFIG_COL_SCRIPT"), 10],
    			[expandMissionText("LIB_CONFIG_COL_VERSION"), 7],
    			[expandMissionText("LIB_CONFIG_COL_REF"), 3]
    		], "", "\n");
    		txt += o.div;
    		var count = 0;
    		for (var i = 0; i < 10; i++) {
    			ind = i + max;
    			if (e <= ind) break;
    			count += 1;
    			tmq = this.$setNames[ind];
    			tmp = this._checkHeader(this.$sets[tmq]);
    			ch["A_" + ind + "_item"] = { text: this._addHeader(tmp, 1), alignment: "LEFT" };
    		}
    		// pad out the entry lines so the general functions are at the bottom of the page
    		var rows = 16;
    		if (big) rows += 6;
    		for (var i = 0; i < rows - count; i++) ch["B_BLANK_" + i] = { text: "", unselectable: true };
    		if (e <= (1 + o.curPage) * 10) ch = this._aid.scrChcUnsel(ch, 'LIPNext');
    		if (!o.curPage) ch = this._aid.scrChcUnsel(ch, 'LIPPrev');
    		chc = this._fillOptions(ch);
    		this._showScr(chc, txt);
    	};
    	/** _showOXPPage(obj) - for direct access
    		@obj - Settings object
    		@return Number. Error code. 0 - ok, >0 - error (see missiontext.plist)
    	*/
    	this._showOXPPage = function (obj) {
    		if (!obj || typeof obj !== 'object') return 101;
    		if (!Object.keys(obj).length) return 102;
    		var e = this._updSet(obj),
    			ch = {
    				goBool: expandMissionText("LIB_CONFIG_SHOW_SWITCH"),
    				goEInt: expandMissionText("LIB_CONFIG_SHOW_FLAGS"),
    				goSInt: expandMissionText("LIB_CONFIG_SHOW_VALUES"),
    				OXPZZZ: expandMissionText("LIB_CONFIG_BACK")
    			},
    			c = this.$curSet,
    			o = this.$oxpc,
    			tmp = this._checkHeader(c),
    			chc, txt;
    		if (e || !tmp.Ref) {
    			ch = this._aid.scrChcUnsel(ch, 'goBool');
    			ch = this._aid.scrChcUnsel(ch, 'goEInt');
    			ch = this._aid.scrChcUnsel(ch, 'goSInt');
    		} else {
    			if (!c.Bool) ch = this._aid.scrChcUnsel(ch, 'goBool');
    			if (!c.EInt) ch = this._aid.scrChcUnsel(ch, 'goEInt');
    			if (!c.SInt) ch = this._aid.scrChcUnsel(ch, 'goSInt');
    			if (c.Reset) ch.OXPDef = expandMissionText("LIB_CONFIG_RESET");
    		}
    		txt = this._addHeader(tmp);
    		txt += o.div;
    		txt += this._aid.scrAddLine([
    			[expandMissionText("LIB_CONFIG_LINE_AUTHOR", { author: tmp.Author }), 29],
    			[expandMissionText("LIB_CONFIG_LINE_MANIFEST", { manifest: tmp.Mani }), 29],
    			[expandMissionText("LIB_CONFIG_LINE_COPYRIGHT", { copy: tmp.Copyright }), 29],
    			[expandMissionText("LIB_CONFIG_LINE_LICENCE", { licence: tmp.Licence }), 29],
    			[expandMissionText("LIB_CONFIG_LINE_DESC", { desc: tmp.Desc }), 29]
    		], "\n");
    		txt += o.div;
    		if (e || !tmp.Ref) txt += "\n" + this._addError(tmp, e, 'obj');
    		else txt += expandMissionText("LIB_CONFIG_SETTINGS_FOUND");
    		chc = this._fillOptions(ch, tmp.Display);
    		this._showScr(chc, txt);
    		o.curBit = 0;
    		o.curMSB = 0;
    		o.curOpt = 0;
    		o.curTyp = 0;
    		return e;
    	};
    	/** _showOXPSub(obj) - for direct access
    		@obj - Settings object
    		@typ - String. Either "Bool","SInt" or "EInt"
    		@return Number. Error code. 0 - ok, >0 - error (see missiontext.plist)
    	*/
    	this._showOXPSub = function (obj, typ) {
    		if (!obj || typeof obj !== 'object') return 101;
    		if (!Object.keys(obj).length) return 102;
    		if (!typ || this.$oxpc.scr.indexOf(typ) === -1) return 701;
    		var big = false;
    		if (player.ship.hudAllowsBigGui) big = true;
    		var ch;
    		switch (typ) {
    			case "Bool":
    				ch = {
    					OXPZZZ: expandMissionText("LIB_CONFIG_BACK")
    				};
    				break;
    			case "SInt":
    				ch = {
    					OXPZZZ: expandMissionText("LIB_CONFIG_BACK")
    				};
    				break;
    			case "EInt":
    				ch = {
    					OXPNext: expandMissionText("LIB_CONFIG_NEXTOPT"),
    					OXPToggle: expandMissionText("LIB_CONFIG_CHANGECURR"),
    					OXPZZZ: expandMissionText("LIB_CONFIG_BACK")
    				};
    				break;
    		}
    		var e = this._updSet(obj),
    			c = this.$curSet,
    			o = this.$oxpc,
    			r, tp, val, inf,
    			z = 0,
    			sh = 1;
    		o.curTyp = typ;
    		var op = Object.keys(c[typ]),
    			opl = op.length,
    			opk = [],
    			pass = this._checkValues(c, typ),
    			tmp = this._checkHeader(c),
    			txt = this._addHeader(tmp);
    		if (e || pass) sh = 0;
    		txt += o.div;
    		if (sh) {
    			for (var x = 0; x < opl; x++) {
    				tp = op[x];
    				if (tp === 'Info' || tp === 'Notify') continue;
    				if (opk.length > 8) break;
    				if (!c[typ][tp].Hide) opk.push(tp);
    			}
    			o.curKeys = opk;
    			if (!opk.length) {
    				txt += expandMissionText("LIB_CONFIG_NOTHING");
    				z++;
    			} else {
    				for (var t in c[typ]) {
    					if (z > 8) break;
    					if (t === 'Info' || t === 'Notify') continue;
    					val = c[typ][t];
    					if (!val || val.Hide) txt += this._aid.scrAddLine([
    						["", 1],
    						["-", 1]
    					], "", "\n");
    					else {
    						if (!o.curOpt) o.curOpt = t;
    						r = this._calcs(val);
    						switch (typ) {
    							case 'Bool':
    								ch["AB_" + z + "_" + t] = {
    									text: this._aid.scrAddLine([
    										[z, 1],
    										[r.Dec, 4],
    										[expandMissionText("LIB_CONFIG_ITEM_DEFAULT", { def: val.Def }), 5],
    										[val.Desc, 19]
    									], "", "\n"), alignment: "LEFT"
    								};
    								txt += "\n";
    								break;
    							case 'SInt':
    								ch["AB_" + z + "_" + t] = {
    									text: this._aid.scrAddLine([
    										[z, 1],
    										[r.Dec, 5],
    										[expandMissionText("LIB_CONFIG_ITEM_DEFAULT", { def: r.Def }), 5],
    										[expandMissionText("LIB_CONFIG_ITEM_RANGE", { min: r.Min, max: r.Max }), 10],
    										[val.Desc, 10]
    									], "", "\n"), alignment: "LEFT"
    								};
    								txt += "\n";
    								break;
    							case 'EInt':
    								o.curMSB = r.msb;
    								txt += this._aid.scrAddLine([
    									["", 1],
    									[z, 10],
    									[r.Dec, 5],
    									[r.str, 14],
    									[r.msb, 0]
    								], "", "\n");
    								var curLen = 0;
    								for (var i = 0; i < r.msb; i++) {
    									var txtbit = this._aid.scrAddLine([
    										[(o.curBit === i ? ">" : ""), 1],
    										[(r.revstr.length > i ? r.revstr[i] : "0"), 1],
    										[(val.Desc.length > i ? val.Desc[i] + "  " : "  "), defaultFont.measureString((val.Desc.length > i ? val.Desc[i] + "  " : "  "))]
    									], ""); // orig 8, new variable
    									// only jump to a new line if we're about to go over
    									if (curLen + defaultFont.measureString(txtbit) > 30) {
    										txt += "\n";
    										curLen = 0;
    									}
    									curLen += defaultFont.measureString(txtbit);
    									txt += txtbit;
    								}
    								txt += "\n";
    								z = Math.ceil(r.msb / 3);
    								break;
    						}
    					}
    					z++;
    				}
    				if (typ == "Bool" || typ == "SInt") {
    					// pad out the entry lines so the general functions are at the bottom of the page
    					var rows = 18;
    					if (big) rows += 6;
    					for (var i = 0; i < rows - z; i++) ch["B_BLANK_" + i] = { text: "", unselectable: true };
    				}
    			}
    		} else {
    			if (e) txt += this._addError(tmp, e, typ);
    			if (pass) txt += this._addError(tmp, pass, typ);
    			z += (e ? 1 : 0) + (pass ? 1 : 0);
    		}
    		txt += this._aid.scrFillLines(z, 9);
    		if (c[typ].Info) {
    			inf = c[typ].Info;
    			if (inf[0] === "^") inf = expandMissionText(inf.substr(1));
    			// this code restricts the info text to 5 lines or 10 lines, based on whether the hud allows Big Gui.
    			var max = 32 * 5;
    			if (big) max = 32 * 10;
    			var final = inf.toString();
    			// start reducing the size of text until it fits in.
    			if (defaultFont.measureString(final) > max) {
    				var ilen = final.length;
    				do {
    					ilen -= 1;
    					final = inf.toString().substr(0, ilen);
    				} while (defaultFont.measureString(final) > max)
    			}
    			txt += "\n" + expandMissionText("LIB_CONFIG_LINE_DESC", { desc: "\n" + final }) + "\n"; // was inf.toString().substr(0, 240) (ie only 240 characters)
    		}
    		if (!sh || (opk.length < 2 && typ !== 'EInt')) ch = this._aid.scrChcUnsel(ch, 'OXPNext');
    		if (!sh || !opk.length) ch = this._aid.scrChcUnsel(ch, 'OXPToggle');
    		var chc = this._fillOptions(ch, tmp.Display);
    		this._showScr(chc, txt);
    		return e;
    	};
    	this._calcs = function (val) {
    		var s = this._aid.objGet(worldScripts[this.$curSet.Name], val.Name);
    		var r = {
    			Dec: s
    		};
    		switch (this.$oxpc.curTyp) {
    			case 'SInt':
    				r.Dec = r.Dec;
    				r.Def = val.Def;
    				r.Max = val.Max;
    				r.Min = val.Min;
    				break;
    			case 'EInt':
    				r.str = this._aid.toBase(r.Dec, 2);
    				r.msb = this._aid.getMSB(val.Max);
    				r.revstr = this._aid.strRev(r.str);
    				break;
    		}
    		return r;
    	};
    	this._showOXPValueEnter = function () {
    		var c = this.$curSet,
    			o = this.$oxpc,
    			tmp = this._checkHeader(c),
    			txt = this._addHeader(tmp) + o.div,
    			val = c[o.curTyp][o.curOpt],
    			r = this._calcs(val);
    		txt += this._aid.scrAddLine([
    			["", 1],
    			["", 1],
    			[r.Dec, 5],
    			["D:" + r.Def, 5],
    			["R:" + r.Min + " - " + r.Max, 10],
    			[val.Desc, 0]
    		], "", "\n");
    		txt += o.div;
    		txt += expandMissionText("LIB_CONFIG_HEX_INFO");
    		if (val.Min < 0) txt += expandMissionText("LIB_CONFIG_NEG_INFO");
    		if (val.Float) txt += expandMissionText("LIB_CONFIG_FLOAT_INFO");
    		txt += expandMissionText("LIB_CONFIG_ENTER_VALUE");
    		var chc = this._fillOptions({}, tmp.Name, 1);
    		this._showScr(chc, txt, null, this._choiceEnter);
    	};
    	this._addHeader = function (tmp, flag) {
    		return this._aid.scrAddLine([
    			[tmp.Display, 10],
    			[(tmp.Alias ? tmp.Alias : tmp.Name), 10],
    			[tmp.Version, 4],
    			[(flag ? "" : "#"), 1],
    			[(flag ? "" : ""), 1],
    			[(flag ? "" : (tmp.Extern ? "Ex" : "")), 1],
    			[(tmp.Ref ? "" : "!") + (tmp.Extern ? "-" : String(this.$setProbs[tmp.Name + tmp.Display])), 2.5]
    		], "", "\n");
    	};
    	this._addError = function (w, e, typ) {
    		this.$setProbs[w.Name + w.Display] = e;
    		log(this.name, "Error: " + w.Name + " " + w.Display + " - (" + typ + ") " + expandMissionText('LIBC_E' + e));
    		return "Error: " + e + " - (" + typ + ") " + expandMissionText('LIBC_E' + e) + "\n";
    	};
    	this._notiError = function (s, w) {
    		log(this.name, expandMissionText("LIB_CONFIG_NOTIFY_FAIL", { s: s, w: w }));
    	};
    	this._choiceEval = function (choice) {
    		worldScripts.Lib_Config._choices(choice);
    		return;
    	};
    	this._choices = function (choice) {
    		if (choice) this.$defCHC.initialChoicesKey = choice;
    		var o = this.$oxpc,
    			c = this.$curSet,
    			p = Math.floor(o.curInd / 10),
    			cur = this.$setNames[o.curInd],
    			a, b, flag, ind, ok, ws;
    		var pp = p * 10;
    		if (choice.indexOf("A_") >= 0) {
    			o.curInd = parseInt(choice.split("_")[1]);
    			cur = this.$setNames[o.curInd];
    			this._showOXPPage(this.$sets[cur]);
    			return;
    		};
    		if (choice.indexOf("AB_") >= 0) {
    			o.curOpt = choice.split("_")[2];
    			choice = "OXPToggle";
    		}
    		switch (choice) {
    			case 'goBool':
    				this._showOXPSub(c, 'Bool');
    				break;
    			case 'goEInt':
    				this._showOXPSub(c, 'EInt');
    				break;
    			case 'goLI':
    				o.intern = 1;
    				this._showList();
    				break;
    			case 'lioOXP':
    				this._showOXPPage(this.$sets[cur]);
    				break;
    			case 'goSInt':
    				this._showOXPSub(c, 'SInt');
    				break;
    			case 'LINext':
    				o.curInd++;
    				if (o.curInd >= pp + 10 || o.curInd < pp || o.curInd >= o.entry) o.curInd = pp;
    				this._showList();
    				break;
    			case 'LIPNext':
    				o.curPage++;
    				o.curInd = pp + 10;
    				this._showList();
    				break;
    			case 'LIPPrev':
    				if (o.curPage > -1) {
    					o.curPage--;
    					o.curInd = pp - 10;
    				}
    				this._showList();
    				break;
    			case 'LIRet':
    				this._showStart();
    				break;
    			case 'OXPDef':
    				ws = worldScripts[c.Name];
    				if (c.Notify) a = 1;
    				ind = o.scr;
    				for (var i = 0; i < 3; i++) {
    					flag = ind[i];
    					if (c[flag]) {
    						if (!a && c[flag].Notify) b = 1;
    						else b = 0;
    						for (var j in c[flag]) {
    							if (typeof c[flag][j] !== 'object') continue;
    							this._aid.objSet(ws, c[flag][j].Name, c[flag][j].Def);
    							if (!a && !b && c[flag][j].Notify) this._aid.objPass(ws, c[flag][j].Notify, j);
    						}
    						if (b) this._aid.objPass(ws, c[flag].Notify, flag);
    					}
    				}
    				if (a) this._aid.objPass(ws, c.Notify, "All");
    				this._showOXPPage(this.$sets[cur]);
    				mission.addMessageText(expandMissionText("LIB_CONFIG_RESET"));
    				break;
    			case 'OXPNext':
    				switch (o.curTyp) {
    					case 'Bool':
    					case 'SInt':
    						ind = o.curKeys.indexOf(o.curOpt) + 1;
    						if (ind > o.curKeys.length - 1) ind = 0;
    						o.curOpt = o.curKeys[ind];
    						break;
    					case 'EInt':
    						o.curBit++;
    						if (o.curBit >= o.curMSB) o.curBit = 0;
    						break;
    				}
    				this._showOXPSub(c, o.curTyp);
    				break;
    			case 'OXPToggle':
    				ws = worldScripts[c.Name];
    				ind = c[o.curTyp][o.curOpt].Name;
    				switch (o.curTyp) {
    					case 'Bool':
    						a = this._aid.objGet(ws, ind);
    						a = !a;
    						this._aid.objSet(ws, ind, a);
    						flag = 1;
    						break;
    					case 'SInt':
    						this._showOXPValueEnter();
    						break;
    					case 'EInt':
    						a = this._aid.objGet(ws, ind);
    						b = Math.pow(2, o.curBit);
    						if (c[o.curTyp][o.curOpt].OneOf) a = b;
    						else if (c[o.curTyp][o.curOpt].OneOfZero) {
    							if (a === b) a = 0;
    							else a = b;
    						} else {
    							if (a & b) a -= b;
    							else a += b;
    						}
    						this._aid.objSet(ws, ind, a);
    						flag = 1;
    						break;
    				}
    				if (flag) {
    					if (c[o.curTyp][o.curOpt].Notify) {
    						ok = this._aid.objPass(ws, c[o.curTyp][o.curOpt].Notify, o.curOpt);
    						if (!ok) this._notiError(c.Name, c[o.curTyp][o.curOpt].Notify);
    					}
    					this._showOXPSub(c, o.curTyp);
    				}
    				break;
    			case 'OXPZZZ':
    				ws = worldScripts[c.Name];
    				if (o.curTyp) {
    					if (c[o.curTyp].Notify) {
    						ok = this._aid.objPass(ws, c[o.curTyp].Notify, o.curTyp);
    						if (!ok) this._notiError(c.Name, c[o.curTyp].Notify);
    					}
    					this._showOXPPage(c);
    				} else {
    					if (c.Notify) {
    						ok = this._aid.objPass(ws, c.Notify, "All");
    						if (!ok) this._notiError(c.Name, c.Notify);
    					}
    					this.$curSet = null;
    					if (o.intern) {
    						this.$defCHC.initialChoicesKey = 'LIRet';
    						this._showList();
    					} else this._reset(); // Bye
    				}
    				break;
    			case 'ZZZ':
    				this._reset();
    				break;
    		}
    	};
    	// Numerical input
    	this._choiceEnter = function (choice) {
    		worldScripts.Lib_Config._choiceEnterB(choice);
    		return;
    	};
    	this._choiceEnterB = function (choice) {
    		var o = this.$oxpc,
    			c = this.$curSet,
    			ws = worldScripts[c.Name],
    			ind = c[o.curTyp][o.curOpt],
    			sign = 1,
    			ok, val;
    		if (choice) {
    			// Store sign
    			if (choice[0] === '-') {
    				choice = choice.substr(1);
    				sign = -1;
    			}
    			if (choice.length > 2 && choice.substr(0, 2) === "0x" && !isNaN(parseInt(choice))) val = parseInt(choice);
    			else if (ind.Float && !isNaN(parseFloat(choice))) val = parseFloat(choice);
    			else if (!isNaN(parseInt(choice))) val = parseInt(choice);
    			if (typeof (val) !== 'undefined') {
    				val *= sign; // Reapply sign
    				val = this._aid.clamp(val, ind.Min, ind.Max);
    				this._aid.objSet(ws, ind.Name, val);
    				if (c[o.curTyp][o.curOpt].Notify) {
    					ok = this._aid.objPass(ws, c[o.curTyp][o.curOpt].Notify, o.curOpt);
    					if (!ok) this._notiError(c.Name, c[o.curTyp][o.curOpt].Notify);
    				}
    			}
    		}
    		this._showOXPSub(c, o.curTyp);
    	};
    
    }).call(this);
    Scripts/Lib_Crypt.js
    /* jshint bitwise:false, forin:false */
    /* global log */
    /* (C) Svengali 2016-2018, License CC-by-nc-sa-4.0 */
    (function(){
    "use strict";
    this.name = "Lib_Crypt";
    
    /** _decrypt() - Decrypts string.
    	@str - String. Minimum length 6 chars.
    	@pwd - String. Password. Minimum length 4 chars.
    	@return - String/Boolean. String or false.
    	Author: Terry Yuen.
    */
    this._decrypt = function(str,pwd){
    	if(!str || !pwd || str.length<6 || pwd.length<4){log(this.name,expandMissionText("LIB_DECRYPT_PARAM_ERROR")); return false;}
    	var prand = "";
    	for(var i=0;i<pwd.length;i++) prand += pwd.charCodeAt(i).toString();
    	var sPos = Math.floor(prand.length/5);
    	var mult = parseInt(prand.charAt(sPos)+prand.charAt(sPos*2)+prand.charAt(sPos*3)+prand.charAt(sPos*4)+prand.charAt(sPos*5),null);
    	var incr = Math.round(pwd.length/2);
    	var modu = Math.pow(2,31)-1;
    	var salt = parseInt(str.substring(str.length-8,str.length),16);
    	str = str.substring(0,str.length-8);
    	prand += salt;
    	while(prand.length>10) prand = (parseInt(prand.substring(0,10),null)+parseInt(prand.substring(10,prand.length),null)).toString();
    	prand = (mult*prand+incr)%modu;
    	var enc_chr = "",dec_str = "";
    	for(var j=0;j<str.length;j+=2){
    		enc_chr = parseInt(parseInt(str.substring(j,j+2),16)^Math.floor((prand/modu)*255),null);
    		dec_str += String.fromCharCode(enc_chr);
    		prand = (mult*prand+incr)%modu;
    	}
    	return dec_str;
    };
    
    /** _encrypt() - Encrypts string.
    	@str - String. Minimum length 6 chars.
    	@pwd - String. Password. Minimum length 4 chars.
    	@return - String/Boolean. String or false.
    	Author: Terry Yuen.
    */
    this._encrypt = function(str,pwd){
    	if(!str || !pwd || str.length<6 || pwd.length<4){log(this.name,expandMissionText("LIB_ENCRYPT_PARAM_ERROR")); return false;}
    	var prand = "";
    	for(var i=0;i<pwd.length;i++) prand += pwd.charCodeAt(i).toString();
    	var sPos = Math.floor(prand.length/5);
    	var mult = parseInt(prand.charAt(sPos)+prand.charAt(sPos*2)+prand.charAt(sPos*3)+prand.charAt(sPos*4)+prand.charAt(sPos*5),null);
    	var incr = Math.ceil(pwd.length/2);
    	var modu = Math.pow(2,31)-1;
    	if(mult<2){log(this.name,expandMissionText("LIB_ENCRYPT_HASH_ERROR")); return false;}
    	var salt = Math.round(Math.random()*1000000000)%100000000;
    	prand += salt;
    	while(prand.length>10) prand = (parseInt(prand.substring(0,10),null)+parseInt(prand.substring(10,prand.length),null)).toString();
    	prand = (mult*prand+incr)%modu;
    	var enc_chr = "",enc_str = "";
    	for(var j=0;j<str.length;j++){
    		enc_chr = parseInt(str.charCodeAt(j)^Math.floor((prand/modu)*255),null);
    		if(enc_chr<16) enc_str += "0"+enc_chr.toString(16);
    		else enc_str += enc_chr.toString(16);
    		prand = (mult*prand+incr)%modu;
    	}
    	salt = salt.toString(16);
    	while(salt.length<8) salt = "0"+salt;
    	enc_str += salt;
    	return enc_str;
    };
    
    /** _getCRC() - Returns simple checksum. Limits every char via &0xff and result via &0x3fff.
    */
    this._getCRC = function(str){
    	var crc = 0, i = str.length;
    	while(i--) crc += (str.charCodeAt(i)&0xff);
    	return crc&0x3fff;
    };
    
    /** _rot5() - Number rotation. Can be paired with _rot13.
    */
    this._rot5 = function(str){
    	return (str+'').replace(/[0-9]/g,function(s){return String.fromCharCode(s.charCodeAt(0)+(s<'5'?5:-5));});
    };
    
    /** _rot13() - Alphabet rotation. Can be paired with _rot5.
    */
    this._rot13 = function(str){
    	return (str+'').replace(/[a-z]/gi,function(s){return String.fromCharCode(s.charCodeAt(0)+(s.toLowerCase()<'n'?13:-13));});
    };
    
    /** _rot513 - Combined rot5 and rot13.
    */
    this._rot513 = function(str){
    	var a = this._rot5(str),b = this._rot13(a);
    	return b;
    };
    
    /** _rot47() - Expanded rotation.
    */
    this._rot47 = function(a,b){return++b?String.fromCharCode((a=a.charCodeAt()+47,a>126?a-94:a)):a.replace(/[^ ]/g,this._rot47);};
    
    /** _Vigenere
    * input   String.
    * key     String. Alphabetical key.
    * forward Bool. Set to true for decryption.
    * $return String.
    */
    this._Vigenere = function(input, key, forward){
    	var alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
    		adjusted_key = "", i, key_char, output = "", key_index = 0, in_tag = false;
    	if(key===null) key = "";
    	key = key.toUpperCase();
    	var key_len = key.length;
    	for(i=0;i<key_len;i++){
    		key_char = alphabet.indexOf(key.charAt(i));
    		if(key_char<0) continue;
    		adjusted_key += alphabet.charAt(key_char);
    	}
    	key = adjusted_key;
    	key_len = key.length;
    	if (key_len===0){
    		key = "a";
    		key_len = 1;
    	}
    	var input_len = input.length;
    	for(i=0;i< input_len;i++){
    		var input_char = input.charAt(i);
    		if(input_char==="<") in_tag = true;
    		else if(input_char===">") in_tag = false;
    		if(in_tag){
    			output += input_char;
    			continue;
    		}
    		var input_char_value = alphabet.indexOf(input_char);
    		if(input_char_value<0){
    			output += input_char;
    			continue;
    		}
    		var lowercase = input_char_value >= 26 ? true : false;
    		if(forward) input_char_value += alphabet.indexOf(key.charAt(key_index));
    		else input_char_value -= alphabet.indexOf(key.charAt(key_index));
    		input_char_value += 26;
    		if(lowercase) input_char_value = input_char_value % 26 + 26;
    		else input_char_value %= 26;
    		output += alphabet.charAt(input_char_value);
    		key_index = (key_index + 1) % key_len;
    	}
    	return output;
    };
    }).call(this);
    
    Scripts/Lib_Cubecode.js
    /* global mission,worldScripts,SoundSource */
    /* (C) Svengali 2016-2018, License CC-by-nc-sa-4.0 */
    (function(){
    "use strict";
    this.name = "Lib_Cubecode";
    
    /** _initCubes
    * @obj.pass - Number
    * @obj.card - Texture
    * @obj.ws - String
    * @obj.path - String
    */
    this._initCubes = function(obj){
    	if(this.$cub.init) return false;
    	var ret,map = "lib_lock.png",lo = 0,ws=null,wsf=null;
    	if(obj){
    		if(obj.card) map = obj.card;
    		if(typeof obj.pass==="number") lo = obj.pass;
    		if(obj.ws && obj.path){
    			ws = obj.ws;
    			wsf = obj.path;
    		}
    	}
    	this.$cub.lock = lo;
    	this.$cub.ws = ws;
    	this.$cub.path = wsf;
    	this.$head.subTex[0].mat["lib_null.png"].emission_map = map;
    	this.$cub.code = -1;
    	this.$cub.init = 1;
    	ret = this._showCubes();
    	return ret;
    };
    this.startUp = function(){
    	this.$head = {
    		model: "lib_ms_cubes",
    		title: expandMissionText("LIB_CUBE_TITLE"),
    		background: "lib_console_bg.png",
    		choices: {NEXT:expandMissionText("LIB_CUBE_NEXT"),ROTX:expandMissionText("LIB_CUBE_ROTATEX"),ROTY:expandMissionText("LIB_CUBE_ROTATEY"),ROTZ:expandMissionText("LIB_CUBE_ROTATEZ"),ZZZ:expandMissionText("LIB_CUBE_QUIT")},
    		text: expandMissionText("LIB_CUBE_SELECT"),
    		caller: this.name,
    		callback: "_choiceEval",
    		checkpoint: "_checkpoints",
    		aOris: [{e:-1,ori:[1,0,0,0]}],
    		aPos: [{e:-1,pos:[0,30,250]}],
    		subTex: [{e:0,mat:{"lib_null.png":{emission_map:"lib_lock.png"}}}],
    		flow:[]
    	};
    	this.$cub = {
    		code:-1,
    		lock:0,
    		cur:1,
    		init:0,
    		ws:null,
    		path:null,
    		curOri:[[1,0,0,0],[1,0,0,0],[1,0,0,0],[1,0,0,0],[1,0,0,0],[1,0,0,0],[1,0,0,0],[1,0,0,0],[1,0,0,0],[1,0,0,0]],
    		flows:{a:[4,"rotX",{e:1,t:2,deg:90,da:1,db:1,inf:1}],b:[4,"rotY",{e:1,t:2,deg:90,da:1,db:1,inf:1}],c:[4,"rotZ",{e:1,t:2,deg:90,da:1,db:1,inf:1}]}
    	};
    	this.$snd = new SoundSource();
    	this._aid = worldScripts.Lib_Main._lib;
    };
    this._showCubes = function(fl,w){
    	var obj = this._aid.objClone(this.$head),ac,c = 0,ret,i,un = ['ROTX','ROTY','ROTZ','ZZZ'],m = "lib_ms_cube_a.png";
    	for(i=1;i<10;i++){obj.aOris.push({e:i,ori:this.$cub.curOri[i]});}
    	if(w) obj.replace = 1;
    	else {
    		obj.fadeIn = true;
    		obj.flow.push([c,"ovFadeIn",{t:1.5}]);
    		c++;
    		obj.flow.push([c,"snd1",{snd:"lib_enter_code.ogg"}]);
    		c++;
    	}
    	if(this.$cub.lock===this.$cub.code){
    		for(i=0;i<4;i++) obj.choices = this._aid.scrChcUnsel(obj.choices,un[i]);
    		obj.flow.push([c,"clr"]);
    		c++;
    		for(i=1;i<10;i++){
    			obj.flow.push([c,"lit",{e:i,mat:m,tex:m,col:[0,0.6,0,1]}]);
    			c++;
    		}
    	} else {
    		obj.flow.push([c,"lit",{e:this.$cub.cur,mat:m,tex:m,col:[0,0.6,0,1]}]);
    		c++;
    		if(fl){
    			ac = this.$cub.flows[fl];
    			ac[0] = c;
    			ac[2].e = this.$cub.cur;
    			obj.flow.push(ac);
    			c++;
    			obj.flow.push([c,"check"]);
    			c+=5;
    			obj.flow.push([c,"snap",{e:this.$cub.cur}]);
    			c++;
    			obj.flow.push([c,"check"]);
    			c++;
    		}
    	}
    	c+=5;
    	obj.flow.push([c,"clr"]);
    	ret = worldScripts.Lib_Animator._start(obj);
    	if(ret && !w && !fl) this._getRes();
    	return ret;
    };
    this._choiceEval = function(choice){worldScripts.Lib_Cubecode._delayChoice(choice); return;};
    this._delayChoice = function(choice){
    	var cb,ws;
    	switch(choice){
    		case "NEXT":
    			this.$cub.cur++;
    			if(this.$cub.cur>9) this.$cub.cur = 1;
    			if(this.$cub.lock===this.$cub.code){
    				cb = 1;
    				this.$cub.init = 0;
    			} else this._showCubes(0,1);
    			break;
    		case "ROTX": this._showCubes("a",1); break;
    		case "ROTY": this._showCubes("b",1); break;
    		case "ROTZ": this._showCubes("c",1); break;
    		case "ZZZ": cb = 1; this.$cub.init = 0; break;
    	}
    	if(cb && this.$cub.ws && this.$cub.path){
    		ws = worldScripts[this.$cub.ws];
    		this._aid.objPass(ws,this.$cub.path,this.$cub.code);
    	}
    	this.$snd.sound = "[changed-option]";
    	this.$snd.play();
    };
    this._checkpoints = function(n){
    	var o;
    	switch(n){
    		case 2:
    			o = worldScripts.Lib_Animator._a;
    			if(o) this.$cub.curOri[this.$cub.cur] = o.cur[this.$cub.cur].tOri;
    			break;
    		case 8:
    			this._getRes();
    			if(this.$cub.lock===this.$cub.code){
    				this.$snd.sound = "lib_code_verified.ogg";
    				this.$snd.play();
    				this._showCubes(0,1);
    			}
    			break;
    	}
    	return true;
    };
    this._getRes = function(){
    	var res = 0,pri = [3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61],a,b,c = mission.displayModel;
    	for(var i=1;i<10;i++){
    		a = pri[i];
    		b = c.subEntities[i].vectorUp;
    		if(b.x>0) res += b.x*11*a;
    		if(b.y>0) res += b.y*13*a;
    		if(b.z>0) res += b.z*17*a;
    		b = c.subEntities[i].vectorRight;
    		if(b.x>0) res += b.x*19*a;
    		if(b.y>0) res += b.y*23*a;
    		if(b.z>0) res += b.z*29*a;
    		b = c.subEntities[i].vectorForward;
    		if(b.x>0) res += b.x*31*a;
    		if(b.y>0) res += b.y*37*a;
    		if(b.z>0) res += b.z*41*a;
    	}
    	this.$cub.code = Math.floor(res);
    };
    }).call(this);
    
    Scripts/Lib_EntityStrength.js
    /* global system,worldScripts */
    /* (C) Svengali 2016-2018, License CC-by-nc-sa-4.0 */
    (function(){
    "use strict";
    this.name = "Lib_EntityStrength";
    
    this.$weapons = ["EQ_WEAPON_NONE","EQ_WEAPON_MINING_LASER","EQ_WEAPON_PULSE_LASER","EQ_WEAPON_BEAM_LASER","EQ_WEAPON_MILITARY_LASER","EQ_WEAPON_THARGOID_LASER"];
    this.$slotsA = ["forwardWeapon","aftWeapon","portWeapon","starboardWeapon"];
    this.$slotsB = ["weaponPositionForward","weaponPositionAft","weaponPositionPort","weaponPositionStarboard"];
    /** Returns strength value for passed entity.
    */
    this._check = function(ent){
    	var w = ent.weaponFacings,
    		eq = ent.equipment,
    		esc = ent.escorts,
    		sec = ent.subEntityCapacity,
    		c = 0, wc = 0, ind = 0,
    		i, l, s, t, tt;
    	c += ent.maxEnergy*(ent.maxPitch+ent.maxRoll+ent.maxYaw)*ent.energyRechargeRate;
    	if(ent.isPlayer) c += (ent.maxAftShield*ent.aftShieldRechargeRate+ent.maxForwardShield*ent.forwardShieldRechargeRate)*0.5;
    	c += ent.maxSpeed*ent.maxThrust;
    	c *= 0.001;
    	if(eq && eq.length){
    		if(ent.isPlayer) c += eq.length;
    		else c += eq.length*5;
    	}
    	if(esc && esc.length){
    		l = esc.length;
    		for(i=0;i<l;i++) c += this._check(esc[i])*0.5;
    	}
    	c += ent.maxEscorts;
    	if(w){
    		for(i=0;i<4;i++){
    			t = ent[this.$slotsA[i]];
    			if(t && t.equipmentKey){
    				ind = this.$weapons.indexOf(t.equipmentKey);
    				if(ind<0) ind = 6;
    				tt = ent[this.$slotsB[i]];
    				if(tt && tt.length) ind += tt.length; // multi
    				wc += ind;
    			}
    		}
    		if(ent.accuracy>1) wc *= ent.accuracy;
    		c += wc*10;
    	}
    	if(sec){
    		s = ent.subEntities;
    		l = (s?s.length:0);
    		for(i=0;i<l;i++) if(s[i].isTurret) c += 50;
    		if(ent.isPlayer && l<sec) c += 50*(sec-l); // no cheats
    	}
    	c += ent.missileCapacity*5;
    	if(!ent.maxSpeed) c /= ent.mass*0.000005;
    	if(ent.isThargoid) c *= 1.5;
    	c = Math.round(c);
    	// Add value to ship script.
    	if(ent.script) ent.script.$libStrength = c;
    	return c;
    };
    /** Returns maximum value of NPC ships in the system.
    	Params:
    		rel - Piloted ships relative to entity.
    		all - Return Array of all numbers.
    		redo - Set script values. Do not use careless.
    */
    this._gather = function(rel,all,redo){
    	var a = [],b,l,i=0,c;
    	if(rel) b = system.filteredEntities(this,function(e){return(e.isShip && e.isValid && e.isPiloted && !e.isPlayer);},rel,52000);
    	else {
    		b = system.allShips;
    		i = 1;
    	}
    	l = b.length;
    	if(l>400) l = 400; // Cap
    	if(redo){
    		for(i;i<l;i++) a.push(this._check(b[i]));
    	} else {
    		for(i;i<l;i++){
    			if(b[i].script){
    				if(typeof b[i].script.$libStrength==='number') c = b[i].script.$libStrength;
    				else c = this._check(b[i]);
    			} else c = -1;
    			a.push(c);
    		}
    	}
    	if(!all){
    		if(a.length) a = Math.max.apply(Math,a);
    		else a = 0;
    		if(!isFinite(a)) a = 999999;
    		if(!rel) worldScripts.Lib_Main._lib.$entLastStrength = a;
    	}
    	return a;
    };
    this.shipSpawned = function(ent){
    	this._check(ent);
    	if(ent.primaryRole==="lib_test" && ent.script.name!=="lib_test") worldScripts.Lib_Main._lib.$entCstRemoved = true;
    };
    this.missionScreenOpportunity = function(){this._gather(0,0,1); delete this.missionScreenOpportunity;};
    this.shipWillDockWithStation = this.shipExitedWitchspace = function(){this._gather(0,0,1);};
    }).call(this);
    
    Scripts/Lib_GUI.js
    /* global addFrameCallback,galaxyNumber,guiScreen,mission,missionVariables,player,removeFrameCallback,setScreenBackground,setScreenOverlay,worldScripts,SoundSource,System */
    /* (C) Svengali 2016-2018, License CC-by-nc-sa-4.0 */
    (function(){
    "use strict";
    this.name = "Lib_GUI";
    this.copyright = "(C)2016-2018";
    this.description = "GUI images and sounds handling.";
    
    // Full Sets
    this.$guis = {};
    this.$IDs = {};
    // Extension - temporary overrides, FIFO
    this.$guisExt = {
    	GUI_SCREEN_EQUIP_SHIP: [],
    	GUI_SCREEN_GAMEOPTIONS: [],
    	GUI_SCREEN_INTERFACES: [],
    	GUI_SCREEN_KEYBOARD: [],
    	GUI_SCREEN_LOAD: [],
    	GUI_SCREEN_LONG_RANGE_CHART: [],
    	GUI_SCREEN_MANIFEST: [],
    	GUI_SCREEN_MARKET: [],
    	GUI_SCREEN_MARKETINFO: [],
    	GUI_SCREEN_OPTIONS: [],
    	GUI_SCREEN_REPORT: [],
    	GUI_SCREEN_SAVE: [],
    	GUI_SCREEN_SAVE_OVERWRITE: [],
    	GUI_SCREEN_SHIPLIBRARY: [],
    	GUI_SCREEN_SHIPYARD: [],
    	GUI_SCREEN_SHORT_RANGE_CHART: [],
    	GUI_SCREEN_STATUS: [],
    	GUI_SCREEN_STICKMAPPER: [],
    	GUI_SCREEN_STICKPROFILE: [],
    	GUI_SCREEN_SYSTEM_DATA: []
    };
    // Extension
    this.$IDsExt = {};
    // screenID rules - expand, but do not change!
    this.$IDRules = {
    	"oolite-contracts-cargo-none": {pic:1,ov:1,snd:1,mus:1},
    	"oolite-contracts-cargo-details": {mpic:1,snd:1,mus:1},
    	"oolite-contracts-cargo-summary": {pic:1,ov:1,snd:1,mus:1},
    	"oolite-contracts-parcels-none": {pic:1,ov:1,snd:1,mus:1},
    	"oolite-contracts-parcels-details": {mpic:1,snd:1,mus:1},
    	"oolite-contracts-parcels-summary": {pic:1,ov:1,snd:1,mus:1},
    	"oolite-contracts-passengers-none": {pic:1,ov:1,snd:1,mus:1},
    	"oolite-contracts-passengers-details": {mpic:1,snd:1,mus:1},
    	"oolite-contracts-passengers-summary": {pic:1,ov:1,snd:1,mus:1},
    	"oolite-primablemanager": {pic:1,ov:1,snd:1,mus:1},
    	"oolite-register": {pic:1,ov:1,snd:1,mus:1}
    };
    this.$ambi = {
    	audio: true,
    	guiFX: true,
    	ex: 0,
    	last: 0,
    	reinit: 0,
    	crowd: "generic",
    	generic: [],
    	redux: [],
    	none: []
    };
    this.$noEx = Object.keys(this.$IDRules);
    
    this.startUp = function(){
    	this._aid = worldScripts.Lib_Main._lib;
    	for(var i=0;i<this.$noEx.length;i++) this._aid.objLock(this.$IDRules[this.$noEx[i]]);
    	this._aid.objLock(this.$IDRules);
    };
    // remove missionVariables.LIB_GUI sometime
    this.startUpComplete = function(){
    	delete this.startUpComplete;
    	var gu,ms = missionVariables.LIB_GUI,ind=0,max=0,mv=missionVariables.LIB_GUI_CONFIG,mvp;
    	this.$s = new SoundSource();
    	this.$amb = new SoundSource();
    	this.$op = null;
    	this.$cur = null;
    	this.$k = Object.keys(this.$guis);
    	if(ms && worldScripts[ms]) this.$cur = ms;
    	if(mv){
    		mvp = JSON.parse(missionVariables.LIB_GUI_CONFIG);
    		if(mvp.cur && worldScripts[mvp.cur]) this.$cur = mvp.cur;
    		this.$ambi.audio = mvp.audio;
    		this.$ambi.guiFX = mvp.guiFX;
    	}
    	gu = this.$k;
    	if(!this.$cur && gu.length) this.$cur = gu[gu.length-1];
    	if(gu && gu.length){
    		ind = gu.indexOf(this.$cur);
    		this.$E0 = 0;
    		if(ind!==-1){
    			ind = Math.pow(2,ind);
    			this.$E0 = ind;
    		}
    		max = Math.pow(2,gu.length)-1;
    		this.$inf = {Name:"Lib_GUI",Alias:"Library",Display:expandMissionText("LIB_GUI_DISPLAY"),Alive:"$inf",
    			Bool:{
    				B0:{Name:"$ambi.audio",Def:true,Desc:expandMissionText("LIB_GUI_AMBIENT")},
    				B1:{Name:"$ambi.guiFX",Def:true,Desc:expandMissionText("LIB_GUI_SFXGUI")}
    			},
    			EInt:{E0:{Name:"$E0",Def:ind,Min:0,Max:max,Desc:gu,Notify:"_chgSet",OneOfZero:1},Info:expandMissionText("LIB_GUI_CHOOSE_BKD")}};
    		worldScripts.Lib_Config._registerSet(this.$inf);
    	}
    	// TODO: Attempt to avoid JS-reset bug - remove when solved.
    	setScreenBackground("lib_black.png");
    	this._setCrowd(player.ship.dockedStation);
    	this._stationAmb();
    	missionVariables.LIB_GUI = null;
    };
    this._chgSet = function(){
    	var gu = this.$k, ind = this._aid.getMSB(this.$E0)-1;
    	if(ind<0) this.$cur = null;
    	else this.$cur = gu[ind];
    };
    this.playerWillSaveGame = function(){
    	var o = {
    		cur: this.$cur,
    		audio: this.$ambi.audio,
    		guiFX: this.$ambi.guiFX
    	};
    	missionVariables.LIB_GUI_CONFIG = JSON.stringify(o);
    };
    this.alertConditionChanged = function(){
    	if(player.ship.isInSpace) this.guiScreenChanged();
    };
    this.gameResumed = function(){
    	this.guiScreenChanged();
    	this._stationAmb();
    };
    this.gamePaused = function(){
    	this._stationAmb(1);
    };
    this.shipDied = function(){
    	if(this.$guiFCB) removeFrameCallback(this.$guiFCB);
    	this._stationAmb(1);
    };
    this.shipDockedWithStation = function(st){
    	this._setCrowd(st);
    	this._stationAmb();
    };
    this._setCrowd = function(st){
    	var inf, p = ["generic","redux","none"];
    	if(st.scriptInfo && st.scriptInfo.lib_crowd && p.indexOf(st.scriptInfo.lib_crowd)>-1) inf = st.scriptInfo.lib_crowd;
    	if(!st.isMainStation && !st.hasNPCTraffic && !inf) this.$ambi.crowd = "none";
    	else {
    		if(inf) this.$ambi.crowd = inf;
    		else if(st.name==="Rock Hermit") this.$ambi.crowd = "redux";
    		else this.$ambi.crowd = "generic";
    	}
    };
    this._stationAmb = function _stationAmb(s){
    	if(player.ship.isInSpace || s){
    		if(this.$AmbTimer) this.$AmbTimer.stop();
    		this.$amb.stop();
    		this.$ambi.last = 0;
    	} else {
    		if(!this.$ambi.audio) return;
    		if(this.$ambi.ex){
    			this.$ambi.last = 0;
    			if(this.$AmbTimer){
    				this.$AmbTimer.stop();
    				this.$AmbTimer.nextTime = clock.absoluteSeconds+1;
    				this.$AmbTimer.start();
    			}
    			return;
    		}
    		var w = this.$ambi[this.$ambi.crowd], i = Math.floor(Math.random()*w.length), d,r;
    		if(w.length){
    			this.$amb.sound = w[i].name;
    			this.$amb.volume = w[i].vol;
    			this.$amb.play();
    			d = w[i].dur-1;
    			if(this.$AmbTimer) this.$AmbTimer.stop();
    			else this.$AmbTimer = new Timer(this,this._stationAmb,-1,d);
    			this.$AmbTimer.nextTime = clock.absoluteSeconds+d;
    			this.$AmbTimer.start();
    			this.$ambi.reinit = 0;
    			this.$ambi.last = clock.absoluteSeconds+d;
    		}
    	}
    };
    this.shipWillLaunchFromStation = function(){
    	this.$s.stop();
    	this._stationAmb(1);
    };
    this.guiScreenChanged = function(){
    	var gu = guiScreen, amb;
    	switch(gu){
    		case "GUI_SCREEN_EQUIP_SHIP":
    		case "GUI_SCREEN_LONG_RANGE_CHART":
    		case "GUI_SCREEN_MANIFEST":
    		case "GUI_SCREEN_MARKET":
    		case "GUI_SCREEN_MARKETINFO":
    		case "GUI_SCREEN_REPORT":
    		case "GUI_SCREEN_SHIPYARD":
    		case "GUI_SCREEN_SHORT_RANGE_CHART":
    		case "GUI_SCREEN_STATUS":
    			this.$op = null;
    			this._setGUI(gu,0);
    			this.$ambi.ex = 0;
    			break;
    		case "GUI_SCREEN_SYSTEM_DATA":
    			var inf = System.infoForSystem(galaxyNumber,player.ship.infoSystem),sp = 0;
    			if(inf.sun_gone_nova || inf.sun_going_nova) sp = 1;
    			this.$op = null;
    			this._setGUI(gu,sp);
    			this.$ambi.ex = 0;
    			break;
    		case "GUI_SCREEN_INTERFACES":
    		case "GUI_SCREEN_SHIPLIBRARY":
    			this.$op = gu;
    			this._setGUI(gu,0);
    			this.$ambi.ex = 0;
    			break;
    		case "GUI_SCREEN_GAMEOPTIONS":
    		case "GUI_SCREEN_KEYBOARD":
    		case "GUI_SCREEN_LOAD":
    		case "GUI_SCREEN_OPTIONS":
    		case "GUI_SCREEN_SAVE":
    		case "GUI_SCREEN_SAVE_OVERWRITE":
    		case "GUI_SCREEN_STICKMAPPER":
    		case "GUI_SCREEN_STICKPROFILE":
    			this.$op = gu;
    			this._setGUI(gu,0);
    			this._stationAmb(1);
    			amb = 1;
    			break;
    		case "GUI_SCREEN_MISSION":
    			this.$op = null;
    			if(mission.screenID) this._setMS(mission.screenID);
    			else {
    				this._stationAmb(1);
    				this.$ambi.reinit = 1;
    				amb = 1;
    			}
    			if(mission.exitScreen) this.$op = gu;
    			break;
    		case "GUI_SCREEN_MAIN":
    			amb = 1;
    			this.$op = null;
    			break;
    		default:
    			this.$op = null;
    	}
    	if(this.$op){
    		if(!this.$guiFCB) this.$guiFCB = addFrameCallback(this._checkGUI.bind(this));
    	} else {
    		if(this.$guiFCB) removeFrameCallback(this.$guiFCB);
    		delete this.$guiFCB;
    	}
    	if(!amb && !this.$ambi.reinit && this.$ambi.last<=clock.absoluteSeconds+2) this._stationAmb();
    	if(amb) this.$ambi.ex = 1;
    };
    this.missionScreenEnded = function(){
    	if(this.$ambi.reinit) this._stationAmb();
    };
    this._checkGUI = function(){
    	if(!player.ship.isValid) return;
    	if(this.$op && guiScreen!==this.$op && guiScreen!=="GUI_SCREEN_MISSION") this.guiScreenChanged();
    };
    this._setGUI = function(gui,spc){
    	if(!this.$cur && !this.$guisExt[gui].length){
    		setScreenBackground("lib_black.png");
    		return;
    	}
    	var g = this.$guis[this.$cur],
    		ex = this.$guisExt[gui],
    		big = (player.ship.hudAllowsBigGui || player.ship.hudHidden),
    		img,ov,ref,snd,sou,w;
    	if(!ex.length && (!g || (!g[gui] && !g.generic))) return;
    	if(ex.length) w = ex[0];
    	else if(g[gui]) w = g[gui];
    	else w = g.generic;
    	if(player.ship.isInSpace){
    		sou = "sndF";
    		if(spc && w.picNova) img = "picNova";
    		else if(w.picFlight) img = "picFlight";
    		else if(w.pic) img = "pic";
    	} else {
    		sou = "snd";
    		if(spc && w.picNova) img = "picNova";
    		else if(player.ship.dockedStation.script.$guiVoid && w.picVoid){
    			img = "picVoid";
    			sou = null;
    		} else if(w.pic) img = "pic";
    	}
    	if(sou){
    		if(w[sou]) snd = w[sou];
    		else if(w.sndRef){
    			ref = this.$guis[w.sndRef];
    			if(ref && ref[gui] && ref[gui][sou]) snd = ref[gui][sou];
    		}
    	}
    	if(img){
    		if(player.alertCondition===3 && w[img+"Red"]) img = img+"Red";
    		if(big && w[img+"Big"]) img = img+"Big";
    		this._applyBG(w[img]);
    	}
    	if(w.ov){
    		ov = "ov";
    		if(player.alertCondition===3 && w[ov+"Red"]) ov = ov+"Red";
    		if(big && w[ov+"Big"]) ov = ov+"Big";
    		this._applyOV(w[ov]);
    	}
    	if(snd && this.$ambi.guiFX) this.$s.playSound(snd);
    	this.$guisExt[gui].shift();
    };
    this._setMS = function(id){
    	if(!this.$cur && !this.$IDsExt[id]) return;
    	var m = this.$IDRules,
    		s = this.$IDs[this.$cur],
    		ex = (this.$noEx.indexOf(id)===-1 && this.$IDsExt[id]?this.$IDsExt[id]:null),
    		big = (player.ship.hudAllowsBigGui || player.ship.hudHidden),
    		sou = "snd",
    		img,ov,w,ref;
    	if(!m[id] || (!ex && !s && (!s[id] && !s.generic))){
    		this.$s.stop();
    		this._stationAmb(1);
    		this.$ambi.reinit = 1;
    		this.$ambi.ex = 1;
    		return;
    	}
    	if(ex) w = ex;
    	else if(s[id]) w = s[id];
    	else w = s.generic;
    	if(player.ship.isInSpace) sou = "sndF";
    	if(m[id].snd && this.$ambi.guiFX){
    		if(w[sou]) this.$s.playSound(w[sou]);
    		else if(w.sndRef){
    			ref = this.$IDs[w.sndRef];
    			if(ref && ref[id] && ref[id][sou]) this.$s.playSound(ref[id][sou]);
    		} else this.$s.stop();
    	} else this.$s.stop();
    	if(m[id].mpic && w.mpic) ov = "mpic";
    	else {
    		if(m[id].pic && w.pic) img = "pic";
    		if(m[id].ov && w.ov) ov = "ov";
    	}
    	if(img){
    		if(big && w.picBig) img = "picBig";
    		this._applyBG(w[img]);
    	}
    	if(ov){
    		if(big && w[ov+"Big"]) ov = ov+"Big";
    		this._applyOV(w[ov]);
    	}
    };
    this._applyBG = function(img){
    	if(typeof img==="object" && img.scale){
    		var sc = this._aid.ooImageScale(img.x,img.y,img.zoom);
    		setScreenBackground({name:img.name,height:sc.height,width:sc.width});
    	} else setScreenBackground(img);
    };
    this._applyOV = function(ov){
    	if(typeof ov==="object" && ov.scale){
    		var sc = this._aid.ooImageScale(ov.x,ov.y,ov.zoom);
    		setScreenOverlay({name:ov.name,height:sc.height,width:sc.width});
    	} else setScreenOverlay(ov);
    };
    }).call(this);
    
    Scripts/Lib_Main.js
    /* jshint bitwise:false, forin:false */
    /* global _libMain,defaultFont,expandMissionText,galaxyNumber,global,log,missionVariables,oolite,player,system,worldScripts,Ship,System */
    /* (C) Svengali 2016-2018, License CC-by-nc-sa-4.0 - Helper functions and data objects */
    (function () {
    	"use strict";
    	this.name = "Lib_Main";
    	this.author = "Svengali";
    
    	/** _libMain - Main class. There's often no need to instantiate it for other AddOns.
    	 * Just create a reference -> this._aid = worldScripts.Lib_Main._lib;
    	 */
    	this._libMain = function () {};
    	_libMain.prototype = {
    		constructor: _libMain,
    		/** Contains connected systems based on players position when
    		 * the game starts or player jumps to a new galaxy.
    		 */
    		$connections: [
    			[],
    			[],
    			[],
    			[],
    			[],
    			[],
    			[],
    			[]
    		],
    		$missionActive: null,
    		/** Contains data keys of all ships.
    		 */
    		$ships: [],
    		spc: String.fromCharCode(31),
    		spcw: defaultFont.measureString(String.fromCharCode(31)),
    		spcs: String.fromCharCode(32),
    		spcsw: defaultFont.measureString(String.fromCharCode(32)),
    		// Sanity
    		$entCstChanged: [],
    		$entCstPatched: false,
    		$entCstRemoved: false,
    		$entLastStrength: 0,
    
    		// ***** Number *****
    		/** Returns clamped Number in range min...max.
    		 */
    		clamp: function (n, min, max) {
    			return ((n > max) ? max : (n < min) ? min : n);
    		},
    		/** Counts set bits in Number.
    		 */
    		cntBits: function (n) {
    			var res = 0;
    			if (n < 0) n = ~n;
    			while (n > 0) {
    				if ((n & 1)) res++;
    				n >>= 1;
    			}
    			return res;
    		},
    		/** Returns greatest common denominator.
    		 */
    		gcd: function (x, y) {
    			return (y === 0 ? x : this.gcd(y, x % y));
    		},
    		/** Returns position of most significant bit of non-negative Number or zero.
    		 */
    		getMSB: function (n) {
    			var res = 15;
    			for (var r = 8; r > 0; r >>= 1) res = (n < 1 << res) ? res - r : res + r;
    			return ((n < 1 << res) ? res : res + 1);
    		},
    		/** Modulo. Differs from JS native for negative values.
    		 * -2%3 = -2 while this._aid.mod(-2,3) = 1
    		 */
    		mod: function (x, y) {
    			return x - Math.floor(x / y) * y;
    		},
    		/** Returns random Number in range 0...n.
    		 */
    		rand: function (n) {
    			return n * (Math.random() % 1) | 0;
    		},
    		/** Returns random Number in range min...max.
    		 */
    		randXY: function (min, max) {
    			return min + this.rand(max - min + 1);
    		},
    		/** Returns String with converted base of Number.
    		 * this._aid.toBase(-29,16) = "-1d"
    		 */
    		toBase: function (n, bas, from) {
    			return parseInt(n, from || 10).toString(bas);
    		},
    		/** Returns String with converted base of Number. Programmers version.
    		 * this._aid.toBaseSign(-29,16) = "-0xffffe3"
    		 */
    		toBaseSign: function (n, bas) {
    			return (n < 0 ? "-" : "") + (bas === 16 ? "0x" : "") + (parseInt((n < 0 ? (0xffffffffffff + n) + 1 : n), 10) & 0xffffff).toString(bas);
    		},
    		/** Returns String with a leading zero if n<10.
    		 */
    		toLZ: function (n) {
    			return ((n !== null && n < 10 && n >= 0 ? "0" : "") + n);
    		},
    		/** Returns String with a leading zeros if n<100.
    		 */
    		toLZZ: function (n) {
    			return (n !== null && n < 100 && n >= 0 ? "0" + this.toLZ(n) : "" + n);
    		},
    		/** Returns String with specified length filled with leading zeros.
    		 */
    		toNLZ: function (n, len) {
    			return ("0000000000000000" + n).substr(-len);
    		},
    		/** Returns rounded Number with specified precision.
    		 */
    		toPrec: function (n, p) {
    			if (!n) return 0;
    			var y = Math.pow(10, p);
    			return Math.round(n * y) / y;
    		},
    
    		// ***** String *****
    		/** Returns reverted String.
    		 */
    		strRev: function (str) {
    			if (!str) return "";
    			var i = str.length,
    				res = "";
    			while (i--) res += str[i];
    			return res;
    		},
    		/** Returns random String with specified length.
    		 */
    		strRND: function (len, chars) {
    			chars = chars || 'abcdefghijklmnopqrstuvwxyz';
    			var res = '',
    				rndPos;
    			for (var i = 0; i < len; i++) {
    				rndPos = Math.floor(Math.random() * chars.length);
    				res += chars.substring(rndPos, rndPos + 1);
    			}
    			return res;
    		},
    		/** Returns random numbered String with specified length.
    		 */
    		strRNDInt: function (len) {
    			var res = String(1000000000 + Math.random() * 1000000000 | 0);
    			return res.substr(0, len);
    		},
    		/** Returns trimmed String. Unlike .trim() it also removes control chars.
    		 */
    		strTrim: function (str) {
    			return str.replace(/[\f\r\n\t\v]/g, "").trim();
    		},
    
    		// ***** Array *****
    		/** Merges nested Arrays
    		 */
    		arrConcat: function (arrA, arrB) {
    			if (this.typeGet(arrB) !== "array") return arrA;
    			var res = arrA.slice(0),
    				na = arrB.slice(0);
    			var la = res.length,
    				lb = na.length;
    			for (var i = 0; i < lb; i++) {
    				if (i > la - 1) break;
    				if (this.typeGet(na[i]) === "array") res[i] = res[i].concat(na[i]);
    			}
    			return res;
    		},
    		/** Returns difference between Arrays. Strict equality.
    		 */
    		arrDiff: function (a, b, oneway) {
    			var res = [],
    				i = a.length;
    			while (i--)
    				if (b.indexOf(a[i]) === -1) res.push(a[i]);
    			if (oneway) return res;
    			i = b.length;
    			while (i--)
    				if (a.indexOf(b[i]) === -1) res.push(b[i]);
    			return res;
    		},
    		/** Returns flattened Array.
    		 */
    		arrFlat: function (arr) {
    			return this._arrFlatten(arr, []);
    		},
    		/** Returns Array with min and max of flat input Array containing only numbers.
    		 */
    		arrMinMax: function (arr) {
    			return [Math.min.apply(Math, arr), Math.max.apply(Math, arr)];
    		},
    		/** Returns Array without specified element. Strict equality.
    		 */
    		arrOmit: function (arr, ele) {
    			var len = arr.length,
    				res = arr.slice(0);
    			for (var i = 0; i < len; i++) {
    				if (res[i] === ele) res.splice(i, 1);
    			}
    			return res;
    		},
    		/** Returns shuffled Array.
    		 */
    		arrShuffle: function (arr) {
    			var j, k, t, res = arr.slice(0);
    			j = res.length;
    			do {
    				k = (Math.random() * (j--)) << 0;
    				t = res[j];
    				res[j] = res[k];
    				res[k] = t;
    			} while (j);
    			return res;
    		},
    		/** Returns sorted Array of Objects. Selectionsort.
    		 */
    		arrSortBy: function (arr, prop) {
    			var res = arr.slice(0),
    				len = res.length,
    				min, i, j;
    			for (i = 0; i < len; i++) {
    				min = i;
    				for (j = i + 1; j < len; j++) {
    					if (j > 0 && j < len && res[j][prop] < res[min][prop]) min = j;
    				}
    				if (i !== min) this._arrSwap(res, i, min);
    			}
    			return res;
    		},
    		/** Merges Arrays.
    		 */
    		arrUnion: function () {
    			var len = arguments.length,
    				res = [];
    			while (len--) {
    				var arg = arguments[len];
    				for (var i = 0; i < arg.length; i++) {
    					var ele = arg[i];
    					if (res.indexOf(ele) === -1) res.push(ele);
    				}
    			}
    			return res;
    		},
    		/** Returns Array with unique entries. Strict equality.
    		 */
    		arrUnique: function (arr) {
    			var res = arr.slice(0),
    				len = res.length,
    				i = -1;
    			while (i++ < len) {
    				var j = i + 1;
    				for (; j < res.length; ++j) {
    					if (res[i] === res[j]) res.splice(j--, 1);
    				}
    			}
    			return res;
    		},
    
    		// ***** Object *****
    		/** Return deep cloned Object.
    		 */
    		objClone: function (obj) {
    			if (typeof obj !== 'object' || obj === null) return obj;
    			var res = obj.constructor();
    			for (var i in obj) res[i] = this.objClone(obj[i]);
    			return res;
    		},
    		/** Deep freeze Object. In-place.
    		 */
    		objFreeze: function (obj) {
    			var prop, propKey;
    			Object.freeze(obj);
    			for (propKey in obj) {
    				prop = obj[propKey];
    				if (!obj.hasOwnProperty(propKey) || typeof prop !== "object" || Object.isFrozen(prop)) continue;
    				this.objFreeze(prop);
    			}
    		},
    		/** Returns value of the Objects member in the specified path or null.
    		 */
    		objGet: function (obj, path) {
    			var a = this.objGrab(obj, path);
    			if (!a[0] || !a[1]) return null;
    			return a[0][a[1]];
    		},
    		/** Returns Array with Object and key following the specified path.
    		 */
    		objGrab: function (obj, path) {
    			var p = path.split("."),
    				len = p.length - 1,
    				last = p.pop(),
    				o = obj;
    			if (len < 1) return [obj, last];
    			for (var i = 0; i < len; i++) o = this._objRet(o, p[i]);
    			return [o, last];
    		},
    		/** Lock object, but leave expandable.
    		 */
    		objLock: function (obj) {
    			var props = Object.getOwnPropertyNames(obj);
    			for (var i = 0; i < props.length; i++) {
    				var desc = Object.getOwnPropertyDescriptor(obj, props[i]);
    				if ("value" in desc) desc.writable = false;
    				desc.configurable = false;
    				Object.defineProperty(obj, props[i], desc);
    			}
    		},
    		/** Returns merged object. In-place.
    		 */
    		objMerge: function (target, obj, omit) {
    			for (var key in obj) {
    				if (!this._objHasOwn(obj, key)) continue;
    				if (omit && !this._objHasOwn(target, key)) continue;
    				var val = obj[key];
    				if (typeof target[key] === "object") {
    					if (!target[key]) target[key] = this.objClone(val);
    					else target[key] = this.objMerge(target[key] || {}, val);
    				} else target[key] = this.objClone(val);
    			}
    			return target;
    		},
    		/** Returns deep merged object. In-place.
    		 */
    		objMergeDeep: function (orig, objects) {
    			if (typeof orig !== "object") orig = {};
    			var len = arguments.length,
    				target = this.objClone(orig);
    			for (var i = 1; i < len; i++) {
    				var val = arguments[i];
    				if (typeof val === "object") this.objMerge(target, val);
    			}
    			return target;
    		},
    		/** Passes value to Function in Object following the specified path. Returns true or false.
    		 */
    		objPass: function (obj, path, val) {
    			var a = this.objGrab(obj, path);
    			if (!a[0] || !a[1]) return false;
    			if (typeof a[0][a[1]] === 'function') {
    				a[0][a[1]](val);
    				return true;
    			}
    			return false;
    		},
    		/** Passes value to Function in Object following the specified path. Returns returned value.-)
    		 */
    		objRequest: function (obj, path, val) {
    			var a = this.objGrab(obj, path);
    			if (!a[0] || !a[1]) return false;
    			if (typeof a[0][a[1]] === 'function') return a[0][a[1]](val);
    			return false;
    		},
    		/** Deep seal Object. In-place.
    		 */
    		objSeal: function (obj) {
    			var prop, propKey;
    			Object.seal(obj);
    			for (propKey in obj) {
    				prop = obj[propKey];
    				if (!obj.hasOwnProperty(propKey) || typeof prop !== "object" || Object.isSealed(prop)) continue;
    				this.objSeal(prop);
    			}
    		},
    		/** Sets value in Object following the specified path. Returns true or false.
    		 */
    		objSet: function (obj, path, val) {
    			var a = this.objGrab(obj, path);
    			if (!a[0] || !a[1]) return false;
    			if (!a[1]) {
    				obj[path] = val;
    				return true;
    			}
    			a[0][a[1]] = val;
    			return true;
    		},
    
    		// ***** Type *****
    		/** Returns lowercase type of Argument for JS native and Oolites classes.
    		 */
    		typeGet: function (obj) {
    			var t = typeof obj;
    			switch (t) {
    				case 'undefined':
    				case 'boolean':
    				case 'number':
    				case 'string':
    				case 'function':
    					return t;
    			}
    			if (!obj) return null;
    			t = Object.prototype.toString.call(obj).substr(8).replace("]", "");
    			return t.toLowerCase();
    		},
    
    		// ***** AddOn *****
    		/** Checks required worldScripts and versions. The Array req is expected to contain two elements
    		 * (name and version) per check and version must be null for legacy scripts or if you want to
    		 * check only for existance!
    		 * Note: Checks property '$deactivated' in scripts.
    		 */
    		addOnVersion: function (who, req, quiet) {
    			var ok, ws, len = req.length,
    				res = [];
    			for (var i = 0; i < len; i += 2) {
    				ok = 1;
    				ws = worldScripts[req[i]];
    				if (typeof ws === 'undefined') ok = 0;
    				else if (ws.$deactivated || (req[i + 1] && !this.cmpVersion(ws.version, req[i + 1]))) ok = 0;
    				if (!ok) {
    					if (!quiet) {
    						if (!req[i + 1]) this._addOnVersion(who, req[i], null);
    						else this._addOnVersion(who, req[i], req[i + 1]);
    					}
    				}
    				res.push(ok);
    			}
    			return res;
    		},
    		/** Clears specified missionVariables.
    		 */
    		clrMVs: function (arr) {
    			var i = 0,
    				l = arr.length;
    			for (i; i < l; i++) missionVariables[arr[i]] = null;
    			return;
    		},
    		/** Delete specified properties.
    		 */
    		clrProps: function (where, arr) {
    			var i = 0,
    				l = arr.length;
    			for (i; i < l; i++) delete worldScripts[where][arr[i]];
    			return;
    		},
    		/** Compares dotted version Strings. Strips pre-/suffixes in first argument.
    		 * Returns 0 if smaller, 1 if equal and 2 if bigger.
    		 * JS native .localeCompare() may be another option.
    		 */
    		cmpVersion: function (act, exp) {
    			var x = act.replace(/[^\d\.]/g, "").split('.'),
    				y = exp.split('.');
    			if (!x.length) return 0;
    			for (var i = 0; i < x.length; i++) {
    				if (i > y.length - 1 || x[i] > y[i]) return 2;
    				if (x[i] < y[i]) return 0;
    			}
    			return 1;
    		},
    
    		// ***** Screens *****
    		/** Returns aligned String.
    		 * e.g. this._aid.scrAddLine([ ["First",8],["Second",8],["Third",8] ],"","\n");
    		 */
    		scrAddLine: function (line, sep, brk) {
    			if (!line) {
    				if (brk) return brk;
    				return "";
    			}
    			var le = line.length,
    				res = "",
    				coun = 0,
    				tmp;
    			for (var i = 0; i < le; i++) {
    				if (coun > 30) break;
    				tmp = this.scrToWidth(line[i][0], line[i][1], "", 1);
    				res += tmp[0] + (sep ? sep : "");
    				coun += tmp[1];
    				if (sep === "\n") coun = 0;
    			}
    			if (brk) res += brk;
    			return res;
    		},
    		/** Sets unselectable flag for missionscreen choices. In-place.
    		 */
    		scrChcUnsel: function (opt, key) {
    			if (typeof opt[key] === 'string') opt[key] = {
    				text: opt[key],
    				unselectable: true
    			};
    			else if (typeof opt[key] === 'object') opt[key].unselectable = true;
    			return opt;
    		},
    		/** Returns String with linebreaks up to max.
    		 */
    		scrFillLines: function (cur, max) {
    			var res = "";
    			for (var i = cur; i < max; i++) res += "\n";
    			return res;
    		},
    		/** Returns String with specified length in em.
    		 * Truncated if width > max, otherwise filled up with space or chr.
    		 */
    		scrToWidth: function (str, max, chr, ret, rgt) {
    			if (typeof str === 'object' && str) str = str.toSource();
    			str = String(str);
    			var l = defaultFont.measureString(str),
    				c, d;
    			if (max < 1) {
    				if (ret) return [str, l];
    				return str;
    			}
    			if (chr) c = defaultFont.measureString(chr);
    			if (l > max) {
    				while (l > max) {
    					if (str && str.length > 1) str = str.substr(0, str.length - 1);
    					d = defaultFont.measureString(str);
    					l = d;
    				}
    			}
    			if (l < max) {
    				while (l < max) {
    					if (chr && c < max - l) {
    						str += chr;
    						l += c;
    					} else {
    						if (max - l > this.spcsw) {
    							if (rgt) str = this.spcs + str;
    							else str += this.spcs;
    							l += this.spcsw;
    						} else {
    							if (max - l > this.spcw) {
    								if (rgt) str = this.spc + str;
    								else str += this.spc;
    								l += this.spcw;
    							} else break;
    						}
    					}
    				}
    			}
    			if (ret) return [str, l];
    			return str;
    		},
    
    		// ***** Oolite *****
    		/** Returns Object with width, height, gcd and ratio of screen window.
    		 */
    		ooScreen: function () {
    			var g = this._ooGame(),
    				w, h, d, a;
    			w = g.gameWindow.width;
    			h = g.gameWindow.height;
    			d = this.gcd(w, h);
    			a = w / h;
    			return {
    				width: w,
    				height: h,
    				gcd: d,
    				ratio: a
    			};
    		},
    		/** Returns shader support level.
    		 */
    		ooShaders: function () {
    			var g = this._ooGame();
    			if (g.wireframeGraphics) return 0;
    			switch (g.detailLevel) {
    				case 'DETAIL_LEVEL_MINIMUM':
    				case 'DETAIL_LEVEL_NORMAL':
    					return 0;
    				case 'DETAIL_LEVEL_SHADERS':
    					return 1;
    				case 'DETAIL_LEVEL_EXTRAS':
    					return 2;
    				default:
    					return 0;
    			}
    		},
    		/** Returns Object with width and height to be used for images
    		 * x and y are width and height in pixels, zoom any number or 0.
    		 * Author: Norbert Nagy.
    		 */
    		ooImageScale: function (x, y, zoom) {
    			var he = 480,
    				wi = 0,
    				o = this.ooScreen();
    			if (x > 0 && y > 0) {
    				if (o.ratio >= x / y) {
    					if (zoom < 0) wi = 0;
    					else {
    						if (zoom) he = 0;
    						wi = o.ratio * 480;
    					}
    				} else if (zoom < 0) {
    					he = 0;
    					wi = o.ratio * 480;
    				} else if (!zoom) wi = o.ratio * 480;
    			}
    			if (!wi && he > 0) wi = null;
    			else if (wi > 0 && !he) he = null;
    			return {
    				height: he,
    				width: wi
    			};
    		},
    
    		// ***** Entities *****
    		/** Spawns VisualEffect in front of (or behind) the player.
    		 */
    		entFXCreate: function (ent, mul) {
    			return system.addVisualEffect(ent, player.ship.position.add(player.ship.vectorForward.multiply(mul)));
    		},
    		/** Sets main texture for entity.
    		 */
    		entSetMainTex: function (ent, tex) {
    			var ta = ent.getMaterials(),
    				tb = Object.keys(ta),
    				tc = ta[tb[0]].textures;
    			if (!tc) return false;
    			if (typeof tc[0] === 'string') ta[tb[0]].textures[0] = tex;
    			else ta[tb[0]].textures[0].name = tex;
    			ent.setMaterials(ta);
    		},
    		setMaterials: function (ent, obj) {
    			if (obj.mat && obj.sha) ent.setMaterials(obj.mat, obj.sha);
    			else if (obj.mat) ent.setMaterials(obj.mat, {});
    			else if (obj.sha) ent.setMaterials({}, obj.sha);
    			return true;
    		},
    
    		// ***** Helper *****
    		_addOnVersion: function (w, r, v) {
    			log("Check", expandMissionText("LIB_MAIN_CHECK_ADDONVER", {w:w, r:r, v:(v ? expandMissionText("LIB_MAIN_MINVER", {v:v}): "")}));
    			return;
    		}, // w + " requirement not matched : " + r + (v ? " required min. version " + v : "") + " not found."
    		_arrFlatten: function (arr, res) {
    			var len = arr.length,
    				i = -1;
    			while (len--) {
    				var cur = arr[++i];
    				if (Array.isArray(cur)) this._arrFlatten(cur, res);
    				else res.push(cur);
    			}
    			return res;
    		},
    		_arrSwap: function (arr, f, s) {
    			var temp = arr[f];
    			arr[f] = arr[s];
    			arr[s] = temp;
    		},
    		_chkGlobal: function () {
    			var a = Object.getOwnPropertyNames(global),
    				warn = [],
    				t, ok = expandMissionText("LIBC_GLOB").split("|");
    			for (var i = 0; i < a.length; i++)
    				if (ok.indexOf(a[i]) === -1) warn.push(a[i]);
    			if (warn.length) {
    				log("WARNING", expandMissionText("LIB_MAIN_GLOBAL_WARNING"));
    				t = JSON.stringify(warn);
    				log("WARNING", t);
    			}
    		},
    		_cmpNum: function (a, b) {
    			return a - b;
    		},
    		_cmpGen: function (a, b) {
    			return (b < a) - (a < b);
    		},
    		_cmpObjKey: function (a, b, k) {
    			return a[k] - b[k];
    		},
    		_cmpArrKey: function (a, b) {
    			return a[0] - b[0];
    		},
    		_getMat: function (ent) {
    			var ta = ent.getMaterials(),
    				tb = Object.keys(ta);
    			return ta[tb[0]];
    		},
    		// Collecting the data + Class A algorithm is expensive.
    		_getConnected: function (gal, sys, coo) {
    			var gn = galaxyNumber,
    				id = system.ID,
    				cpp = player.ship.galaxyCoordinatesInLY,
    				check = [],
    				cur = [],
    				gInfo = [],
    				c, cp, dist, len, i, o, r, s, t, u, w;
    			if (typeof gal === 'number') {
    				gn = gal;
    				cpp = coo;
    				id = sys;
    			}
    			// Collect coordinates
    			for (i = 0; i < 256; i++) {
    				c = System.infoForSystem(gn, i).coordinates;
    				r = {
    					x: c.x,
    					y: c.y,
    					id: i,
    					done: 0
    				};
    				gInfo.push(r);
    			}
    			cp = {
    				x: cpp.x,
    				y: cpp.y,
    				z: 0
    			};
    			gInfo = this.arrSortBy(gInfo, "x");
    			this.$connections[gn] = [];
    			// Start loop
    			for (u = 0; u < 256; u++) {
    				t = gInfo[u];
    				if (t.x > cp.x + 7.15) break;
    				if (t.x < cp.x - 7.15 || t.y < cp.y - 7.15 || t.y > cp.y + 7.15) continue;
    				dist = Math.sqrt((cp.x - t.x) * (cp.x - t.x) + (cp.y - t.y) * (cp.y - t.y));
    				if (dist < 7.341) cur.push(t);
    			}
    			// Grow
    			while (cur.length) {
    				check = check.concat(cur);
    				cur = [];
    				for (o = 0; o < check.length; o++) {
    					cp = check[o];
    					if (cp.id === id || cp.done) continue;
    					for (i = 0; i < 256; i++) {
    						t = gInfo[i];
    						if (t.x > cp.x + 7.15) break;
    						if (t.x < cp.x - 7.15 || t.y < cp.y - 7.15 || t.y > cp.y + 7.15) continue;
    						dist = Math.sqrt((cp.x - t.x) * (cp.x - t.x) + (cp.y - t.y) * (cp.y - t.y));
    						if (dist < 7.341 && check.indexOf(t) === -1 && cur.indexOf(t) === -1) cur.push(t);
    					}
    				}
    				len = check.length;
    				for (w = 0; w < len; w++) check[w].done = 1;
    			}
    			len = check.length;
    			for (s = 0; s < len; s++) this.$connections[gn].push(check[s].id);
    		},
    		_objHasOwn: function (obj, key) {
    			return Object.prototype.hasOwnProperty.call(obj, key);
    		},
    		_objRet: function (obj, prop) {
    			if (obj[prop] && typeof obj[prop] === 'object') return obj[prop];
    			return obj;
    		},
    		_ooGame: function () {
    			return oolite.gameSettings;
    		},
    		_sanity: function () {
    			return {
    				changed: this.$entCstChanged,
    				removed: this.$entCstRemoved,
    				strength: this.$entLastStrength,
    				patched: this.$entCstPatched
    			};
    		},
    		_shuffle: function () {
    			return 0.5 - Math.random();
    		}
    	};
    	/* Do before startUp. These features are useful for other OXPs, but are somewhat slow
    		and it's better to have it done only once instead of slow code in lots of OXPs.
    	*/
    	this._lib = new this._libMain(); // Create instance
    	this._lib.$ships = Ship.keys().sort();
    
    	this.startUp = this.playerEnteredNewGalaxy = function () {
    		this._lib._getConnected();
    	};
    	this.missionScreenOpportunity = function () {
    		delete this.missionScreenOpportunity;
    		this._lib._chkGlobal();
    		this.shipWillExitWitchspace();
    	};
    	this.shipWillExitWitchspace = function () {
    		system.addShips('lib_test', 1, [99999999, 99999999, 99999999], 15000);
    	};
    
    }).call(this);
    Scripts/Lib_MissionCoord.js
    /* jshint bitwise:false, forin:false */
    /* global galaxyNumber,log,system,worldScripts */
    /* (C) Svengali 2016-2018, License CC-by-nc-sa-4.0 */
    (function(){
    "use strict";
    this.name = "Lib_MissionCoord";
    
    // TODO: use $missionActive in Lib_Main
    this.$pool = [];
    this.startUp = function(){
    	delete this.startUp;
    	this._bSearch = new worldScripts.Lib_BinSearch._lib_BinSearch();
    	this.$checked = false;
    	this.$isMission = false;
    	this.$dbg = false;
    };
    this._prepare = function(){
    	var s = [],r = this.$pool.length;
    	if(r){
    		var merged = [],pos=0;
    		for(var m=0;m<r;m++){
    			s = this.$pool[m];
    			if(s.length!==2 || !s[1].name || typeof s[1].name!=='string' || !s[1].func || typeof s[1].func!=='string') continue;
    			if(s[1].hasOwnProperty("incOXP")){
    				var temp = s[1].incOXP;
    				for(var i=0;i<temp.length;i++){
    					if(worldScripts[temp[i]]!=='undefined'){
    						log(this.name,expandMissionText("LIB_MISSCOORD_DROPTOKEN", {s0:s[0], name:s[1].name, func:s[1].func, inc:temp[i]}));
    						continue;
    					}
    				}
    			}
    			if(!s[1].hasOwnProperty("mutalEx") || typeof s[1].mutalEx!=='number') s[1].mutalEx = 0;
    			pos = merged.indexOf(s[0]);
    			if(pos===-1) merged.push(s[0],{0:s[1]});
    			else {
    				var q = merged[pos+1];
    				for(var p=1;p<10;p++){
    					if(q.hasOwnProperty(p)) continue;
    					q[p] = s[1];
    					break;
    				}
    			}
    		}
    		r = merged.length;
    		for(var j=0;j<r;j+=2){
    			this._bSearch.add(merged[j],merged[j+1]);
    		}
    		if(this.$dbg) log(this.name,"merged: "+JSON.stringify(merged));
    	} else {
    		delete this._performCheck;
    		delete this.shipWillExitWitchspace;
    		delete this.shipWillLaunchFromStation;
    		this.$checked = true;
    	}
    	delete this.alertConditionChanged;
    	delete this.guiScreenChanged;
    	delete this._prepare;
    };
    this._performCheck = function(early){
    	this.$checked = true;
    	if(this.$isMission) return;
    	var test = system.description,prop;
    	var pRand = Math.floor(100*system.scrambledPseudoRandomNumber(system.ID));
    	var rest = test.replace(/[^a-zA-Z]/g,' ');
    	rest = rest.split(" ");
    	rest = worldScripts.Lib_Main._lib.arrUnique(rest);
    	var fflag = false, l = rest.length, muteGroup = [];
    	for(var s=0;s<l;s++){
    		fflag = this._bSearch.contains(rest[s]);
    		if(fflag){
    			for(prop in fflag){
    				var c = fflag[prop];
    				if((early && !c.early) || (!early && c.early)) continue;
    				if(muteGroup.indexOf(c.mutalEx)!==-1) continue;
    				if(c.chance && Math.random()>c.chance) continue;
    				if(c.gal && c.gal.indexOf(galaxyNumber)===-1) continue;
    				if(c.ID && c.ID.indexOf(system.ID)===-1) continue;
    				if(c.gov && c.gov.indexOf(system.government)===-1) continue;
    				if(c.seeds && c.seeds.indexOf(pRand)===-1) continue;
    				if(c.exToken && rest.indexOf(c.exToken)!==-1) continue;
    				if(this.$dbg) log(this.name,expandMissionText("LIB_MISSCOORD_CHECK", {name:c.name, muteg:c.mutalEx, rest:rest[s]}));
    				if(worldScripts[c.name]){
    					var act = false;
    					if(c.func && worldScripts[c.name][c.func]) act = worldScripts[c.name][c.func](rest[s]);
    					if(act){
    						muteGroup.push(c.mutalEx);
    						this.$isMission = true;
    					}
    				}
    			}
    		}
    	}
    };
    this.alertConditionChanged = this.guiScreenChanged = function(){
    	if(this._prepare) this._prepare();
    	if(!this.$checked && this._performCheck) this._performCheck();
    };
    this.shipWillExitWitchspace = function(){
    	this.$isMission = false;
    	if(this._performCheck) this._performCheck(1);
    };
    this.shipExitedWitchspace = function(){
    	if(this._performCheck) this._performCheck();
    };
    this.shipWillLaunchFromStation = function(){
    	if(this._prepare) this._prepare();
    	if(!this.$checked && this._performCheck) this._performCheck(1);
    };
    this.shipLaunchedFromStation = function(){
    	if(!this.$checked && this._performCheck) this._performCheck();
    };
    }).call(this);
    
    Scripts/Lib_Music.js
    /* jshint forin:false, bitwise:false */
    /* global Sound,SoundSource,Timer,clock,guiScreen,mission,missionVariables,player,system,worldScripts */
    /* (C) Svengali 2016-2018, License CC-by-nc-sa-4.0 */
    (function () {
    	"use strict";
    	this.name = "Lib_Music";
    	this.copyright = "(C)2016-2018";
    	this.description = "Event-driven and generic music handling.";
    
    	this.$E0 = 0;
    	this.$inf = {
    		Name: "Lib_Music", Display: expandMissionText("LIB_MUSIC_DISPLAY"), Alive: "$inf", Alias: "Library",
    		SInt: { S0: { Name: "$dub.vol", Def: 0.8, Min: 0, Max: 1, Float: true, Desc: expandMissionText("LIB_MUSIC_VOL"), Notify: "_updateVol" } },
    		EInt: { E0: { Name: "$E0", Def: 0, Min: 0, Max: 1, Hide: true, Desc: [" "] }, Info: expandMissionText("LIB_MUSIC_NOADDON"), Notify: "_buildList" }
    	};
    	this.$act = {
    		generic: 0
    	};
    	this.$groupIDs = [];
    	this.$radios = [];
    	this.$media = {
    		aegis: { enter: [], exit: [] },
    		alert: { red: [] },
    		docked: { main: [], any: [] },
    		exitWS: { inter: [], goneNova: [], doNova: [], standard: [] },
    		killed: { any: [] },
    		launch: { inter: [], goneNova: [], doNova: [], main: [], any: [] },
    		planetIn: { enterMain: [], enterSun: [], any: [] },
    		planetOut: { exitMain: [], exitSun: [], any: [] },
    		radio: { generic: [] },
    		rescued: { any: [] },
    		scooped: { any: [] },
    		scoopFuel: { fuel: [] }
    	};
    	this.$dub = {
    		auto: 0,
    		channel: null,
    		cnt: 0,
    		cur: null,
    		dur: 0,
    		ent: null,
    		hand: null,
    		kick: 0,
    		lockAlert: 1,
    		lockDock: 0,
    		lockFuel: 1,
    		lockKill: 1,
    		lockPlanet: 1,
    		now: 0,
    		queue: [],
    		radio: null,
    		reinit: 0,
    		spec: null,
    		vol: 0.8
    	};
    	this.startUp = function () {
    		delete this.startUp;
    		var mus;
    		this._aid = worldScripts.Lib_Main._lib;
    		this.$snd = new SoundSource();
    		this.$snd.sound = "lib_music_fade.ogg";
    		if (missionVariables.LIB_MUSIC) {
    			mus = JSON.parse(missionVariables.LIB_MUSIC);
    			this.$dub.vol = mus.vol;
    			this.$snd.volume = mus.vol;
    		}
    	};
    	this.playerWillSaveGame = function () {
    		var t, s = [], ind, mus = {};
    		if (this.$E0) {
    			t = [].concat(this.$inf.EInt.E0.Desc);
    			for (var i = 0; i < t.length; i++) {
    				ind = Math.pow(2, i);
    				if (this.$E0 & ind) s.push(t[i]);
    			}
    			if (s.length > 24) s.length = 24;
    			missionVariables.LIB_RADIO = JSON.stringify(s);
    		} else missionVariables.LIB_RADIO = null;
    		mus.vol = this.$dub.vol;
    		missionVariables.LIB_MUSIC = JSON.stringify(mus);
    	};
    	/** Expects Object with the structure of this.$media (can be partial),
    	* except radio. The Arrays contain Objects with members:
    	* {snd: <filename>, dur: duration seconds <integer>, vol: optional, volume 0...1 <float>}
    	*/
    	this._addEventMusic = function (obj, ID) {
    		if (!obj || typeof obj !== "object" || typeof ID !== "string") return false;
    		var c = this._aid.objClone(obj);
    		for (var h in c) {
    			if (!this.$media[h] || h === "radio") continue;
    			for (var s in c[h]) {
    				if (s === "any" && !this.$media[h][s]) continue;
    				if (!this.$media[h][s]) this.$media[h][s] = [];
    				for (var i = 0; i < c[h][s].length; i++) {
    					c[h][s][i].act = ID;
    					this.$media[h][s].push(c[h][s][i]);
    				}
    			}
    		}
    		if (this.$actClone) {
    			this.$actClone[ID] = 0;
    			this.$act[ID] = 0;
    		} else if (typeof this.$act[ID] !== "number") this.$act[ID] = 0;
    		if (this.$groupIDs.indexOf(ID) === -1) this.$groupIDs.push(ID);
    		return true;
    	};
    	/** Toggle status for group ID.
    	* - ID: Name <string>
    	*/
    	this._toggleGroup = function (ID) {
    		var a = this.$act;
    		if (typeof a[ID] !== "number") return false;
    		if (this.$actClone) a = this.$actClone;
    		if (!a[ID]) a[ID] = 1;
    		else a[ID] = 0;
    		return a[ID];
    	};
    	/** Take priority for the specified GroupID.
    	* - ID: Name <string>
    	*/
    	this._setPriorityGroup = function (ID) {
    		var g, i, l;
    		if (typeof this.$act[ID] !== "number") return false;
    		this._clrPriorityGroup();
    		this.$actClone = this._aid.objClone(this.$act);
    		g = this.$groupIDs;
    		l = g.length;
    		for (i = 0; i < l; i++) this.$act[g[i]] = 0;
    		this.$act[ID] = 1;
    		return true;
    	};
    	/** Clear priority.
    	*/
    	this._clrPriorityGroup = function () {
    		if (this.$actClone) this.$act = this.$actClone;
    		delete this.$actClone;
    		return;
    	};
    	/** Add radio channel or merges. Expects Object with members:
    	* - name: Channel name <string>. Can be generic, docking, fight, (docked) entity name or a custom channel
    	* - sounds: Array of Objects with {snd: <filename>, dur: duration seconds <integer>, vol: optional, volume <float>}
    	* - radio: If set channel name will be listed in this.$radios
    	*/
    	this._addChannel = function (obj) {
    		var c = this._aid.objClone(obj), i, len = obj.sounds.length;
    		if (!this.$media.radio[c.name]) this.$media.radio[c.name] = [];
    		for (i = 0; i < len; i++) {
    			c.sounds[i].act = c.name;
    			this.$media.radio[c.name].push(c.sounds[i]);
    		}
    		if (this.$actClone) {
    			this.$actClone[c.name] = 1;
    			this.$act[c.name] = 0;
    		} else this.$act[c.name] = 1;
    		if (c.radio && this.$radios.indexOf(c.name) === -1) this.$radios.push(c.name);
    		return true;
    	};
    	/** Sets or clears custom radio channel.
    	* - str: Channel name <string> or null.
    	*/
    	this._setChannel = function (str) {
    		var d = this.$dub;
    		switch (typeof str) {
    			case "string": if (typeof this.$act[str] === "number" && typeof this.$media.radio[str] !== "undefined") d.channel = str; break;
    			case "object": if (!str) d.channel = null; break;
    		}
    		return d.channel;
    	};
    	this._buildList = function () {
    		var c = this.$radios, l = c.length, ind, d = [];
    		for (var i = 0; i < l; i++) {
    			ind = Math.pow(2, i);
    			if (this.$E0 & ind) d = d.concat(this.$media.radio[c[i]]);
    		}
    		if (this.$E0 && d.length) {
    			this._addChannel({ name: "RadioPlaylist", sounds: [] });
    			this.$media.radio.RadioPlaylist = d;
    			this._setChannel("RadioPlaylist");
    		} else this.$dub.channel = null;
    	};
    	this._updateInf = function () {
    		var c = this.$radios, l = c.length, max = Math.pow(2, l) - 1, o = this.$inf.EInt;
    		o.E0.Max = max & 0xffffff;
    		o.E0.Desc = [].concat(c);
    		if (l) {
    			o.E0.Hide = false;
    			o.Info = expandMissionText("LIB_MUSIC_PUSH");
    		}
    	};
    	this._updateVol = function () {
    		var d = this.$dub, mus, vol, cvol = 1, i;
    		if (d.cur) {
    			mus = Sound.musicSoundSource();
    			vol = d.vol;
    			if (mus.isPlaying) {
    				for (i = 0; i < d.queue.length; i++) {
    					if (d.queue[i].snd === d.cur) {
    						if (d.queue[i].vol) cvol = d.queue[i].vol;
    						break;
    					}
    				}
    				mus.volume = vol * cvol;
    			}
    		}
    		this.$snd.volume = d.vol;
    	};
    	this._selectGeneric = function (d) {
    		if (d.channel) {
    			d.queue = this.$media.radio[d.channel];
    			d.spec = "generic";
    		} else {
    			if (d.radio) {
    				d.queue = this.$media.radio[d.radio];
    				d.spec = d.radio;
    			} else {
    				d.queue = this.$media.radio.generic;
    				d.spec = "generic";
    			}
    		}
    		d.hand = "radio";
    		d.now = 0;
    	};
    	this._resetTimer = function (t) {
    		if (this.$mediaTimer) this.$mediaTimer.stop();
    		this.$mediaTimer = null;
    		if (typeof t === "number") this.$mediaTimer = new Timer(this, this._doPlay, t, 1);
    	};
    	this._doPlay = function _doPlay(q, d) {
    		var sel, r, found, vol;
    		if (!d) d = this.$dub;
    		d.cnt++;
    		if (!q) {
    			q = [];
    			if (d.lockDock && player.ship.isValid && player.ship.docked) {
    				d.lockDock = 0;
    				this.shipDockedWithStation(player.ship.dockedStation);
    				return;
    			}
    		}
    		if (d.cnt > d.dur) {
    			if (!d.now && d.hand === "radio") this._selectGeneric(d);
    			if (!q.length) {
    				this._selectGeneric(d);
    				q = d.queue;
    				if (q.length) found = 1;
    			} else found = 1;
    			if (found) {
    				if (d.now && d.kick) this.$snd.play();
    				// TODO: selection
    				r = Math.floor(Math.random() * q.length);
    				sel = q[r];
    				vol = d.vol;
    				if (sel.vol) vol *= sel.vol;
    				Sound.playMusic(sel.snd, false, vol);
    				d.dur = sel.dur;
    				d.cnt = d.dur - 2;
    				d.cur = sel.snd;
    			} else {
    				d.dur = 1;
    				d.cnt = 0;
    			}
    			d.ent = null;
    			d.now = 0;
    			d.kick = 0;
    			//if (found) this._resetTimer(d.dur + this._aid.randXY(13, 22));
    			if (found) {
    				var randomAddition = this._aid.randXY(13, 22);
    				if (d.radio === "fight") randomAddition = 0;
    				this._resetTimer(d.dur + randomAddition);
    			} else this._resetTimer();
    			if (d.hand !== "radio") return d.dur;
    			else return 10;
    		} else d.hand = "radio";
    		return 0;
    	};
    	this._performMedia = function (hand, spec, ent) {
    		var d = this.$dub, i, q = [], len, w, add = this.$media[hand].any, any = 1;
    		if (hand !== "alert" && guiScreen === "GUI_SCREEN_MISSION") return 0;
    		//if (!ent && d.radio === "fight" && player.alertHostiles && hand !== "killed") return 0;
    		if (!ent && d.radio === "fight" && (hand !== "alert" || spec !== "red") && player.alertHostiles && hand !== "killed") return 0;
    		if (ent && this.$media[hand][ent]) {
    			w = this.$media[hand][ent];
    			if (w.length) any = 0;
    		} else w = this.$media[hand][spec];
    		if ((!w || !w.length) && any && add) {
    			if (w) w = w.concat(add);
    			else w = add;
    		}
    		if (w) {
    			len = w.length;
    			// TODO: move the hard work to _toggleGroup and _setPriorityGroup
    			for (i = 0; i < len; i++) {
    				if (this.$act[w[i].act]) q.push(w[i]);
    			}
    			if (q.length) {
    				d.cnt = 1;
    				d.dur = 0;
    				d.hand = hand;
    				d.spec = spec;
    				d.queue = q;
    				if (ent) d.ent = ent;
    				else d.ent = null;
    				d.now = 1;
    				return this._doPlay(q, d);
    			}
    		}
    		d.kick = 0;
    		return 10;
    	};
    	// Handler
    	this.alertConditionChanged = function (to) {
    		var c = clock.absoluteSeconds, d = this.$dub;
    		if (!player.ship.isInSpace) return;
    		switch (to) {
    			case 1:
    			case 2:
    				if (d.radio === "fight") d.radio = "generic";
    				break;
    			case 3:
    				if (this.$media.radio.fight) d.radio = "fight";
    				if (c >= d.lockAlert) {
    					d.kick = 1;
    					d.lockAlert = c + this._performMedia("alert", "red");
    				}
    				if (this.$media.radio.fight)
    					if (!this.$mediaTimer || !this.$media.alert.red.length) this._resetTimer(0);
    				/*if (c >= d.lockAlert) {
    					d.kick = 1;
    					d.lockAlert = c + this._performMedia("alert", "red");
    				}
    				if (this.$media.radio.fight) {
    					d.radio = "fight";
    					if (!this.$mediaTimer || !this.$media.alert.red.length) this._resetTimer(0);
    				}*/
    				break;
    		}
    	};
    	this.playerRescuedEscapePod = function (fee, reason, occupant) {
    		var c = clock.absoluteSeconds;
    		if (!occupant || !occupant.name || c < this.$dub.lockDock) return;
    		this.$dub.lockDock = clock.absoluteSeconds + this._performMedia("rescued", "ent", occupant.name);
    	};
    	this.shipDockedWithStation = function (station) {
    		var c = clock.absoluteSeconds, d = this.$dub;
    		this._clrLock();
    		if (!station) return;
    		if (this.$media.radio[station.name]) d.radio = station.name;
    		else d.radio = "generic";
    		if (c < d.lockDock) return;
    		this._resetTimer(0); // autopilot!
    		if (station.isMainStation) this._performMedia("docked", "main");
    		else this._performMedia("docked", "ent", station.name);
    		d.lockDock = 0;
    	};
    	this.shipEnteredPlanetaryVicinity = function (planet) {
    		var aa, ab, c = clock.absoluteSeconds, d = this.$dub;
    		if (!player.ship.isInSpace || !planet || c < d.lockPlanet) return;
    		if (planet.isMainPlanet) aa = "enterMain";
    		else if (planet.isSun) aa = "enterSun";
    		else {
    			aa = "ent";
    			ab = planet.name;
    		}
    		d.kick = 1;
    		d.lockPlanet = c + this._performMedia("planetIn", aa, ab);
    	};
    	this.shipEnteredStationAegis = function (station) {
    		var c = clock.absoluteSeconds, d = this.$dub;
    		if (!player.ship.isInSpace || !station || c < d.lockPlanet) return;
    		d.kick = 1;
    		d.lockPlanet = c + this._performMedia("aegis", "enter");
    	};
    	this.shipExitedStationAegis = function (station) {
    		var c = clock.absoluteSeconds, d = this.$dub;
    		if (!player.ship.isInSpace || !station || c < d.lockPlanet) return;
    		d.kick = 1;
    		d.lockPlanet = c + this._performMedia("aegis", "exit");
    	};
    	this.shipExitedPlanetaryVicinity = function (planet) {
    		var aa, ab, c = clock.absoluteSeconds, d = this.$dub;
    		if (!player.ship.isInSpace || !planet || c < d.lockPlanet) return;
    		if (planet.isMainPlanet) aa = "exitMain";
    		else if (planet.isSun) aa = "exitSun";
    		else {
    			aa = "ent";
    			ab = planet.name;
    		}
    		d.kick = 1;
    		d.lockPlanet = c + this._performMedia("planetOut", aa, ab);
    	};
    	this.shipExitedWitchspace = function () {
    		this._clrLock();
    		var a;
    		if (!player.ship.isInSpace) return;
    		if (system.isInterstellarSpace) a = "inter";
    		else if (system.sun.hasGoneNova) a = "goneNova";
    		else if (system.sun.isGoingNova) a = "doNova";
    		else a = "standard";
    		this.$dub.kick = 1;
    		this._performMedia("exitWS", a);
    	};
    	this.shipKilledOther = function (whom) {
    		var c = clock.absoluteSeconds, d = this.$dub;
    		if (!player.ship.isInSpace || c < d.lockKill) return;
    		d.kick = 1;
    		if (whom && whom.name) d.lockKill = c + this._performMedia("killed", "ent", whom.name);
    		else d.lockKill = c + this._performMedia("killed", "any");
    	};
    	this.shipScoopedFuel = function () {
    		var c = clock.absoluteSeconds, d = this.$dub;
    		if (!player.ship.isInSpace || c < d.lockFuel) return;
    		d.kick = 1;
    		d.lockFuel = c + this._performMedia("scoopFuel", "fuel");
    	};
    	this.shipScoopedOther = function (whom) {
    		if (!player.ship.isInSpace || !whom || !whom.name) return;
    		this.$dub.kick = 1;
    		this._performMedia("scooped", "ent", whom.name);
    	};
    	this.shipTargetDestroyed = function (target) {
    		var c = clock.absoluteSeconds, d = this.$dub, t;
    		if (!player.ship.isInSpace || c < d.lockKill) return;
    		d.kick = 1;
    		if (target) {
    			t = target.name;
    			if (t && this.$media.killed[t]) {
    				d.lockKill = c + this._performMedia("killed", "ent", t);
    				return;
    			}
    		}
    		d.lockKill = c + this._performMedia("killed", "any");
    	};
    	this.shipWillDockWithStation = function () {
    		var d = this.$dub;
    		d.auto = 0;
    		if (d.cur) Sound.stopMusic(d.cur);
    	};
    	this.shipWillLaunchFromStation = function (station) {
    		var d = this.$dub;
    		if (!station) return;
    		d.radio = "generic";
    		d.lockDock = 0;
    		d.kick = 1;
    		this._resetTimer(0);
    		if (station.isMainStation) this._performMedia("launch", "main");
    		else this._performMedia("launch", "ent", station.name);
    	};
    	// Special cases
    	this.gamePaused = function () {
    		if (this.$dub.cur) Sound.stopMusic(this.$dub.cur);
    		this._clrLock();
    	};
    	this.gameResumed = function () {
    		if (this.$dub.auto && this.$media.radio.docking) {
    			this.playerStartedAutoPilot();
    		} else this._resetTimer(0);
    	};
    	this.guiScreenChanged = function () {
    		var d = this.$dub, k, m;
    		if (guiScreen === "GUI_SCREEN_MISSION") {
    			m = mission.screenID;
    			if (m) k = worldScripts.Lib_GUI.$IDRules[m];
    			if (!m || !k || !k.mus) {
    				if (d.cur) Sound.stopMusic(d.cur);
    				this._resetTimer();
    				d.reinit = 1;
    			}
    		}
    	};
    	this.missionScreenEnded = function () {
    		if (this.$dub.reinit) {
    			this.$dub.reinit = 0;
    			this._resetTimer(0);
    		}
    	};
    	// Start session
    	this.missionScreenOpportunity = function () {
    		delete this.missionScreenOpportunity;
    		if (guiScreen !== "GUI_SCREEN_MISSION") this.shipDockedWithStation(player.ship.dockedStation);
    		else this.$dub.reinit = 1;
    		var c = this.$radios, l = c.length, ind, s;
    		this._updateInf();
    		if (missionVariables.LIB_RADIO) {
    			s = JSON.parse(missionVariables.LIB_RADIO);
    			for (var i = 0; i < l; i++) {
    				ind = c.indexOf(s[i]);
    				if (ind !== -1) this.$E0 += Math.pow(2, ind);
    			}
    			this._buildList();
    		}
    		worldScripts.Lib_Config._registerSet(this.$inf);
    	};
    	this.playerCancelledAutoPilot = function () {
    		var d = this.$dub;
    		if (d.auto && d.cur) Sound.stopMusic(d.cur);
    		d.radio = "generic";
    		d.auto = 0;
    		this._resetTimer(0);
    	};
    	this.playerStartedAutoPilot = function () {
    		if (this.$media.radio.docking) {
    			Sound.stopMusic();
    			this.$dub.radio = "docking";
    			this.$dub.auto = 1;
    			this._resetTimer(0);
    		} else this._resetTimer();
    	};
    	this.shipDied = function () {
    		this._resetTimer();
    		if (this.$dub.cur) Sound.stopMusic(this.$dub.cur);
    	};
    	this.shipLaunchedEscapePod = function () {
    		this.shipDied();
    	};
    	this._clrLock = function () {
    		var d = this.$dub;
    		d.lockAlert = 1;
    		d.lockFuel = 1;
    		d.lockKill = 1;
    		d.lockPlanet = 1;
    	};
    }).call(this);
    
    Scripts/Lib_PAD.js
    /* jshint bitwise:false, forin:false */
    /* global clock,expandMissionText,mission,missionVariables,player,worldScripts */
    /* (C) Svengali & BlackWolf 2016-2018, License CC-by-nc-sa-4.0 */
    (function () {
    	"use strict";
    	this.name = "Lib_PAD";
    
    	this.$data = {
    		categories: {
    			GALCOP: expandMissionText("LIB_PAD_GALCOP"),
    			GUILDS: expandMissionText("LIB_PAD_GUILDS"),
    			INFOS: expandMissionText("LIB_PAD_INFOS"),
    			LOGS: expandMissionText("LIB_PAD_LOGS"),
    			NEWS_HISTORY: expandMissionText("LIB_PAD_NEWS_HISTORY"),
    			PERSONS: expandMissionText("LIB_PAD_PERSONS"),
    			SYSTEMS: expandMissionText("LIB_PAD_SYSTEMS"),
    			ZZX: expandMissionText("LIB_PAD_SEARCH"),
    			ZZY: expandMissionText("LIB_PAD_PLAYER"),
    			ZZZ: expandMissionText("LIB_PAD_EXIT")
    		},
    		GALCOP: {
    			GENERIC: { keyDisplay: "", name: "", entry: "2084004:01:01:01", enlisted: "", kills: 0, missions: [], awards: [], info: [], t0: 11, t1: 0, t2: 0, t3: 0, t4: 0, t5: 0 },
    			NAVY: { keyDisplay: expandMissionText("LIB_PAD_KEY_NAVY"), name: "", entry: "", enlisted: "", kills: 0, missions: [], awards: [], info: [], t0: 21, t1: 0, t2: 0, t3: 0, t4: 0, t5: 0 }
    		},
    		GUILDS: {
    			GENERIC: { keyDisplay: "", name: "", entry: "2084004:01:01:01", enlisted: expandMissionText("LIB_PAD_TRADE_GUILD"), kills: 0, missions: [], awards: [], info: [], t0: 20, t1: 0, t2: 0, t3: 0, t4: 0, t5: 0 }
    		},
    		INFOS: {
    			GENERIC: { keyDisplay: "", name: expandMissionText("LIB_PAD_LAVEACADEMY"), location: expandMissionText("LIB_PAD_LAVE"), beacon: expandMissionText("LIB_PAD_BEACON_NONE"), purpose: expandMissionText("LIB_PAD_PURPOSE"), special: [expandMissionText("LIB_PAD_EXAM_RESULT")], notes: [], t0: 20, t1: 0, t2: 0, t3: 0, t4: 0, t5: 0 }
    		},
    		LOGS: {
    			GENERIC: { keyDisplay: "", list: [expandMissionText("LIB_PAD_LOGS_INIT")] }
    		},
    		NEWS_HISTORY: {
    			GENERIC: { keyDisplay: "", entry: "2084004:01:01:01", text: expandMissionText("LIB_PAD_NEWS_INIT") }
    		},
    		PERSONS: {
    			GENERIC: { keyDisplay: "", name: "", origin: "", species: "", gender: "", age: 0, ship: "", rank: "", info: [], notes: [], t0: 0, t1: 0, t2: 0, t3: 0, t4: 0, t5: 0 }
    		},
    		SYSTEMS: {
    			GENERIC: { keyDisplay: "", name: expandMissionText("LIB_PAD_LAVE"), info: [expandMissionText("LIB_PAD_LAVE1"), expandMissionText("LIB_PAD_LAVE2"), expandMissionText("LIB_PAD_LAVE3")], notes: [], t0: 0, t1: 0, t2: 0, t3: 0, t4: 0, t5: 0 }
    		}
    	};
    	this.$config = {
    		curCat: "GALCOP",
    		def: { XXX: expandMissionText("LIB_PAD_PAGEUP"), YYY: expandMissionText("LIB_PAD_PAGEDOWN"), ZZZ: expandMissionText("LIB_PAD_BACK") },
    		defSpecies: [expandMissionText("LIB_PAD_SPECIES_BIRD"), expandMissionText("LIB_PAD_SPECIES_FELINE"), expandMissionText("LIB_PAD_SPECIES_FROG"), expandMissionText("LIB_PAD_SPECIES_HUMAN"), expandMissionText("LIB_PAD_SPECIES_HUMANOID"), expandMissionText("LIB_PAD_SPECIES_INSECT"), expandMissionText("LIB_PAD_SPECIES_LIZARD"), expandMissionText("LIB_PAD_SPECIES_LOBSTER"), expandMissionText("LIB_PAD_SPECIES_RODENT")],
    		defGender: [expandMissionText("LIB_PAD_GENDER_MALE"), expandMissionText("LIB_PAD_GENDER_FEMALE"), expandMissionText("LIB_PAD_GENDER_NEUTRAL"), expandMissionText("LIB_PAD_GENDER_ANDROID")],
    		defImages: [
    			{ t1: "lib_user.png", s: 3, g: 0, a: [26, 27] },
    			{ t1: "lib_avatar01.png", s: 3, g: 1, a: [23, 36] }, { t1: "lib_avatar02.png", s: 3, g: 1, a: [23, 32] }, { t1: "lib_avatar03.png", s: 3, g: 1, a: [23, 32] },
    			{ t1: "lib_avatar04.png", s: 3, g: 0, a: [26, 37] }, { t1: "lib_avatar05.png", s: 3, g: 0, a: [24, 33] }, { t1: "lib_avatar06.png", s: 3, g: 0, a: [44, 63] },
    			{ t1: "lib_avatar07.png", s: 8, g: 1, a: [23, 43] }, { t1: "lib_avatar08.png", s: 8, g: 0, a: [30, 43] }, { t1: "lib_avatar09.png", s: 0, g: 0, a: [28, 43] },
    			{ t1: "lib_avatar10.png", s: 0, g: 1, a: [28, 43] }, { t1: "lib_avatar11.png", s: 1, g: 1, a: [28, 43] }, { t1: "lib_avatar12.png", s: 1, g: 0, a: [34, 48] },
    			{ t1: "lib_avatar13.png", s: 1, g: 1, a: [24, 41] }, { t1: "lib_avatar14.png", s: 1, g: 0, a: [24, 41] }, { t1: "lib_avatar15.png", s: 2, g: 2, a: [24, 41] },
    			{ t1: "lib_avatar16.png", s: 2, g: 2, a: [24, 41] }, { t1: "lib_avatar17.png", s: 5, g: 2, a: [24, 51] }, { t1: "lib_avatar18.png", s: 5, g: 2, a: [24, 51] },
    			{ t1: "lib_avatar19.png", s: 6, g: 0, a: [24, 151] }, { t1: "lib_avatar20.png", s: 6, g: 1, a: [24, 111] }, { t1: "lib_avatar21.png", s: 8, g: 1, a: [21, 31] },
    			{ t1: "lib_avatar22.png", s: 8, g: 0, a: [24, 35] }
    		],
    		defTemps: {
    			GALCOP: {
    				disp: { name: "", entry: "", enlisted: "", kills: 0, missions: [], awards: [], info: [] },
    				silent: { keyDisplay: expandMissionText("LIB_PAD_GALCOP"), missions: 5, awards: 5, info: 5, t0: 1, t1: 1, t2: 1, t3: 1, t4: 1, t5: 1, model: "lib_ms_helper6y" }
    			},
    			GUILDS: {
    				disp: { name: "", entry: "", enlisted: "", kills: 0, missions: [], awards: [], info: [] },
    				silent: { keyDisplay: expandMissionText("LIB_PAD_GUILDS"), missions: 5, awards: 5, info: 5, t0: 1, t1: 1, t2: 1, t3: 1, t4: 1, t5: 1, model: "lib_ms_helper6y" }
    			},
    			INFOS: {
    				disp: { name: "", location: "", beacon: "", purpose: "", special: [], notes: [], $crypt: "" },
    				silent: { keyDisplay: expandMissionText("LIB_PAD_INFOS"), special: 10, $crypt: 1, notes: 5, t0: 1, t1: 1, t2: 1, t3: 1, t4: 1, t5: 1, model: "lib_ms_helper6y" }
    			},
    			LOGS: {
    				disp: { list: [] }, silent: { keyDisplay: expandMissionText("LIB_PAD_LOGS"), list: 20, model: null }
    			},
    			NEWS_HISTORY: {
    				disp: { entry: "", text: "" },
    				silent: { keyDisplay: expandMissionText("LIB_PAD_NEWS_HISTORY"), entry: 1, text: 1, model: null }
    			},
    			PERSONS: {
    				disp: { name: "", origin: "", species: expandMissionText("LIB_PAD_SPECIES_HUMAN"), gender: expandMissionText("LIB_PAD_GENDER_MALE"), age: 0, rank: "", ship: "", info: [], notes: [] },
    				silent: { keyDisplay: expandMissionText("LIB_PAD_PERSONS"), info: 7, notes: 5, t0: 1, t1: 1, t2: 1, t3: 1, t4: 1, t5: 1, model: "lib_ms_helper6y" }
    			},
    			SYSTEMS: {
    				disp: { name: "", info: [], notes: [], $hide: "" },
    				silent: { keyDisplay: expandMissionText("LIB_PAD_SYSTEMS"), info: 10, notes: 5, $hide: 1, t0: 1, t1: 1, t2: 1, t3: 1, t4: 1, t5: 1, model: "lib_ms_helper6y" }
    			}
    		},
    		defOrgs: [null,
    			"lib_pad_org1.png", "lib_pad_org2.png", "lib_pad_org3.png", "lib_pad_org4.png", "lib_pad_org5.png", "lib_pad_org6.png",
    			"lib_pad_org7.png", "lib_pad_org8.png", "lib_pad_org9.png", "lib_pad_org10.png", "lib_pad_org11.png", "lib_pad_org12.png",
    			"lib_pad_org13.png", "lib_pad_org14.png", "lib_pad_org15.png", "lib_pad_org16.png", "lib_pad_org17.png", "lib_pad_org18.png",
    			"lib_pad_org19.png", "lib_pad_org20.png", "lib_pad_org21.png", "lib_pad_org22.png", "lib_pad_org23.png", "lib_pad_org24.png",
    			"lib_pad_org25.png", "lib_pad_org26.png", "lib_pad_org27.png", "lib_pad_org28.png", "lib_pad_org29.png", "lib_pad_org30.png",
    			"lib_pad_org31.png"
    		],
    		defRanks: [null,
    			"lib_pad_rank1.png", "lib_pad_rank2.png", "lib_pad_rank3.png", "lib_pad_rank4.png", "lib_pad_rank5.png", "lib_pad_rank6.png",
    			"lib_pad_rank7.png", "lib_pad_rank8.png", "lib_pad_rank9.png"
    		],
    		defAwards: [null,
    			"lib_pad_medal1.png", "lib_pad_medal2.png", "lib_pad_medal3.png", "lib_pad_medal4.png", "lib_pad_medal5.png", "lib_pad_medal6.png",
    			"lib_pad_medal7.png", "lib_pad_medal8.png", "lib_pad_medal9.png", "lib_pad_medal10.png", "lib_pad_medal11.png", "lib_pad_medal12.png",
    			"lib_pad_medal13.png", "lib_pad_medal14.png", "lib_pad_medal15.png", "lib_pad_medal16.png", "lib_pad_medal17.png"
    		],
    		defFFF: [null, "lib_pad_rank1.png"],
    		defGGG: [null, "lib_pad_rank1.png"],
    		defTex: ["t0", "t1", "t2", "t3", "t4", "t5"],
    		defTexLookUp: ["defOrgs", "defImages", "defRanks", "defAwards", "defFFF", "defGGG"],
    		defHead: {
    			exitScreen: "GUI_SCREEN_INTERFACES",
    			message: "",
    			background: { name: "lib_pad_text_bg.png", height: 512 },
    			screenID: this.name,
    			spinModel: false
    		},
    		hide: ["$crypt", "$hide"],
    		HUD: 0,
    		last: {
    			news: "",
    			add: "",
    			upd: ""
    		},
    		lastSearch: "",
    		noteAdd: "",
    		page: "INIT",
    		pageInd: 0,
    		setInd: 0,
    		setGal: 0,
    		relInd: 0,
    		ps: {
    			age: 26,
    			gender: expandMissionText("LIB_PAD_GENDER_MALE"),
    			species: expandMissionText("LIB_PAD_SPECIES_HUMAN"),
    			origin: expandMissionText("LIB_PAD_SYSTEM_LAVE"),
    			image: "lib_user.png"
    		},
    		psUpd: 1
    	};
    	this.startUp = function () {
    		var d = this.$data,
    			load;
    		this._aid = worldScripts.Lib_Main._lib;
    		worldScripts.Lib_GUI.$IDRules.Lib_PAD = { mus: 1 };
    		if (missionVariables.LIB_PAD_DATA) {
    			load = JSON.parse(missionVariables.LIB_PAD_DATA);
    			d.GALCOP.GENERIC = load.GALCOP;
    			d.GALCOP.NAVY = load.NAVY;
    			d.GUILDS.GENERIC = load.GUILDS;
    			d.INFOS.GENERIC = load.INFOS;
    			d.LOGS.GENERIC = load.LOGS;
    			d.PERSONS.GENERIC = load.PERSONS;
    			d.SYSTEMS.GENERIC = load.SYSTEMS;
    			if (load.hasOwnProperty("NEWS_HISTORY")) {
    				d.NEWS_HISTORY = load.NEWS_HISTORY;
    			}
    			this._fixData();
    			this.$config.ps = JSON.parse(missionVariables.LIB_PAD_PS);
    			this.$config.last = JSON.parse(missionVariables.LIB_PAD_LAST);
    		}
    		this._updatePlayer(1);
    		this.$Search = {
    			"GALCOP.GENERIC": [0, [], []],
    			"GALCOP.NAVY": [0, [], []],
    			"GUILDS.GENERIC": [0, [], []],
    			"INFOS.GENERIC": [0, [], []],
    			"LOGS.GENERIC": [0, [], []],
    			"PERSONS.GENERIC": [0, [], []],
    			"SYSTEMS.GENERIC": [0, [], []],
    			"NEWS_HISTORY.GENERIC": [0, [], []]
    		};
    		this._fillSearch();
    	};
    	this._fixData = function () {
    		var d = this.$data;
    		if (!d.GALCOP.GENERIC.hasOwnProperty("keyDisplay")) d.GALCOP.GENERIC.keyDisplay = "";
    		if (!d.GALCOP.NAVY.hasOwnProperty("keyDisplay")) d.GALCOP.NAVY.keyDisplay = expandMissionText("LIB_PAD_KEY_NAVY");
    		if (!d.GUILDS.GENERIC.hasOwnProperty("keyDisplay")) d.GUILDS.GENERIC.keyDisplay = "";
    		if (!d.INFOS.GENERIC.hasOwnProperty("keyDisplay")) d.INFOS.GENERIC.keyDisplay = "";
    		if (!d.PERSONS.GENERIC.hasOwnProperty("keyDisplay")) d.PERSONS.GENERIC.keyDisplay = "";
    		if (!d.SYSTEMS.GENERIC.hasOwnProperty("keyDisplay")) d.SYSTEMS.GENERIC.keyDisplay = "";
    	}
    	this._fillSearch = function () {
    		var d = this.$data;
    		this.$Search["GALCOP.GENERIC"][0] = this._getEntries(d.GALCOP.GENERIC);
    		this.$Search["GALCOP.NAVY"][0] = this._getEntries(d.GALCOP.NAVY);
    		this.$Search["GUILDS.GENERIC"][0] = this._getEntries(d.GUILDS.GENERIC);
    		this.$Search["INFOS.GENERIC"][0] = this._getEntries(d.INFOS.GENERIC);
    		this.$Search["LOGS.GENERIC"][0] = this._getEntries(d.LOGS.GENERIC);
    		this.$Search["PERSONS.GENERIC"][0] = this._getEntries(d.PERSONS.GENERIC);
    		this.$Search["SYSTEMS.GENERIC"][0] = this._getEntries(d.SYSTEMS.GENERIC);
    		this.$Search["NEWS_HISTORY.GENERIC"][0] = this._getEntries(d.NEWS_HISTORY.GENERIC);
    	};
    	this.startUpComplete = function () {
    		this._addInterface();
    		// remove register ship interface
    		// but only if random player/ship name is not installed
    		if (!worldScripts["oolite-registership"]._registerShipRandomPilot) {
    			system.mainStation.setInterface("oolite-registership", null);
    		}
    	};
    	// store only GENERIC, NAVY, last and player data
    	this.playerWillSaveGame = function () {
    		var d = this.$data,
    			store = {
    				GALCOP: d.GALCOP.GENERIC,
    				NAVY: d.GALCOP.NAVY,
    				GUILDS: d.GUILDS.GENERIC,
    				INFOS: d.INFOS.GENERIC,
    				LOGS: d.LOGS.GENERIC,
    				PERSONS: d.PERSONS.GENERIC,
    				SYSTEMS: d.SYSTEMS.GENERIC,
    				NEWS_HISTORY: d.NEWS_HISTORY
    			};
    		missionVariables.LIB_PAD_DATA = JSON.stringify(store);
    		missionVariables.LIB_PAD_PS = JSON.stringify(this.$config.ps);
    		missionVariables.LIB_PAD_LAST = JSON.stringify(this.$config.last);
    	};
    	this.shipDockedWithStation = function () {
    		if (!player.ship.docked) return;
    		this._addInterface();
    		// remove register ship interface
    		// but only if random player/ship name is not installed
    		if (!worldScripts["oolite-registership"]._registerShipRandomPilot) {
    			system.mainStation.setInterface("oolite-registership", null);
    		}
    	};
    	this._limit = function (who, how) {
    		for (var k in who) if (typeof (who[k]) === "object" && who[k].length) who[k] = who[k].splice(-how[k]);
    		return who;
    	};
    	this._getEntries = function (obj) {
    		if (obj == null) return "";
    		var ownProps = Object.keys(obj), i = ownProps.length, o, res = "", ex = ["t0", "t1", "t2", "t3", "t4", "t5"];
    		while (i--) {
    			if (ex.indexOf(ownProps[i]) !== -1) continue;
    			o = obj[ownProps[i]];
    			if (typeof (o) === "object") res += this._getEntries(o);
    			else res += "|" + o;
    		}
    		return res;
    	};
    	/** function _addPageInCategory
    	* path    String. Dotted path, e.g. "GALCOP.NAVY".
    	* content Object.
    	* parent  Array. Member of other pages.
    	* sil     Bool. Silent mode. Does not add to latest additions.
    	* $return true on success, otherwise false.
    	*/
    	this._addPageInCategory = function (path, content, parent, sil) {
    		var con = this.$config,
    			d = this.$data,
    			q = this._aid.objGet(d, path),
    			s = path.split("."),
    			obj = this._aid.objClone(content),
    			temp = this._aid.objClone(con.defTemps[s[0]].disp),
    			i;
    		if (q || !temp) return false;
    		for (i = 0; i < 6; i++) temp[con.defTex[i]] = 0;
    		if (!content.hasOwnProperty("keyDisplay")) {
    			temp.keyDisplay = s[s.length - 1];
    		} else {
    			temp.keyDisplay = content.keyDisplay;
    		}
    		temp = this._aid.objMerge(temp, obj, 1);
    		temp = this._limit(temp, con.defTemps[s[0]].silent);
    		this._aid.objSet(d, path, temp);
    		if (!sil) con.last.add = this._buildKey(s); //s.join(":");
    		con.psUpd = 1;
    		this.$Search[path] = [this._getEntries(temp), [], []];
    		if (parent) {
    			this.$Search[path][1] = parent;
    			for (i = 0; i < parent.length; i++) {
    				this.$Search[parent[i]][2].push(path);
    			}
    		}
    		return true;
    	};
    	this._buildKey = function (s) {
    		var txt = "";
    		var d = this.$data;
    		var temp = this.$config.defTemps;
    		for (var i = 0; i < s.length; i++) {
    			txt += (txt == "" ? "" : ":")
    			switch (i) {
    				case 0:
    					if (temp[s[0]].silent.keyDisplay)
    						txt += temp[s[0]].silent.keyDisplay;
    					else
    						txt += s[i];
    					break;
    				case 1:
    					if (d[s[0]][s[1]].keyDisplay)
    						txt += d[s[0]][s[1]].keyDisplay;
    					else
    						txt += s[i];
    					break;
    			}
    			if (i == 1) break;
    		}
    		return txt;
    	}
    	/** _setPageEntry
    	* path   String. Dotted path, e.g. "LOGS.GENERIC.list".
    	* value  Primitive. Value.
    	* sil    Bool. Silent mode. Does not add to latest additions.
    	* $return true on success, otherwise false.
    	*/
    	this._setPageEntry = function (path, value, sil) {
    		var d = this.$data,
    			q = this._aid.objGet(d, path),
    			s = path.split("."),
    			o = {},
    			n = clock.clockString.substr(0, 13),
    			p;
    		if (typeof (q) === "undefined") return false;
    		switch (this._aid.typeGet(q)) {
    			case "array":
    				if (s[2] === "list") q.push(n + " " + value);
    				else if (q.indexOf(value) < 0) q.push(value);
    				o[s[2]] = q;
    				q = this._limit(o, this.$config.defTemps[s[0]].silent)[s[2]];
    				break;
    			case "number":
    				if (value === "++") q++;
    				else q = value;
    				break;
    			default: q = value;
    		}
    		this._aid.objSet(d, path, q);
    		p = this._aid.objGet(d, s[0] + "." + s[1]);
    		if (!sil) this.$config.last.upd = this._buildKey(s) + " - " + n + " " + value; // + s[0] + ":" + s[1]
    		this.$Search[s[0] + "." + s[1]][0] = this._getEntries(p);
    		return true;
    	};
    	/** _getData
    	* path String. Dotted path, e.g. "GALCOP.NAVY".
    	* $return Object or null.
    	*/
    	this._getData = function (path) {
    		var q = this._aid.objGet(this.$data, path);
    		if (q) return this._aid.objClone(q);
    		return null;
    	};
    	this._addInterface = function () {
    		player.ship.dockedStation.setInterface(this.name, {
    			title: expandMissionText("LIB_PAD_TITLE"),
    			category: expandMissionText("LIB_PAD_LOG_CATEGORY"),
    			summary: expandMissionText("LIB_PAD_LOG_SUMMARY"),
    			callback: this._showStart.bind(this)
    		});
    	};
    	this._updatePlayer = function (ex) {
    		var a, b, c, i, ps = this.$config.ps;
    		a = this.$data.PERSONS.GENERIC;
    		a.name = player.name;
    		a.ship = player.ship.displayName;
    		a.species = ps.species;
    		a.origin = ps.origin;
    		a.gender = ps.gender;
    		a.age = ps.age;
    		a.rank = player.rank;
    		a.t1 = ps.image;
    		b = this.$data.GALCOP;
    		c = Object.keys(b);
    		for (i = 0; i < c.length; i++) {
    			b[c[i]].name = player.name;
    			b[c[i]].t1 = ps.image;
    		}
    		b.GENERIC.kills = player.score;
    		b = this.$data.GUILDS;
    		c = Object.keys(b);
    		for (i = 0; i < c.length; i++) {
    			b[c[i]].name = player.name;
    			b[c[i]].t1 = ps.image;
    		}
    		b.GENERIC.kills = player.score;
    		this.$config.psUpd = 0;
    		if (!ex) this._fillSearch();
    	};
    	this._showStart = function (ini) {
    		var c = this.$config;
    		if (ini === "Lib_PAD") {
    			c.psUpd = 1;
    			if (!player.ship.hudHidden) c.HUD = 1;
    		}
    		//player.ship.hudHidden = true;
    		if (c.psUpd) this._updatePlayer();
    		c.curCat = "INIT";
    		var head = this._aid.objClone(c.defHead);
    		head.choices = this.$data.categories;
    		if (!worldScripts.GNN || worldScripts.GNN.$snoop.newsHistory == 0) {
    			delete head.choices["NEWS_HISTORY"];
    		}
    		head.message = expandMissionText("LIB_PAD_INFO_LAST", { news: c.last.news, update: c.last.upd, add: c.last.add });
    		head.background = { name: "lib_pad_bg.png", height: 512 };
    		head.title = expandMissionText("LIB_PAD_LOG_SUMMARY");
    		mission.runScreen(head, this._choices.bind(this));
    	};
    	this._showCategory = function (ick, note) {
    		//player.ship.hudHidden = true;
    		var con = this.$config,
    			cat = con.curCat,
    			d = this.$data[cat],
    			ind = con.pageInd,
    			max = Object.keys(d),
    			p = d[con.page],
    			temps = Object.keys(con.defTemps),
    			i, md, m, min, template,
    			tt = con.defTex,
    			head = this._aid.objClone(con.defHead),
    			pm = this.$Search[cat + "." + con.page];
    		con.relInd = 0;
    		head.choices = this._aid.objClone(con.def);
    		head.initialChoicesKey = ick;
    		head.model = con.defTemps[cat].silent.model;
    		var keyDisp = d[max[ind]].keyDisplay;
    		head.title = expandMissionText("LIB_PAD_" + cat).toUpperCase() + (ind !== 0 ? "-" + (keyDisp != "" ? keyDisp : max[ind]) : "") + " (" + (ind + 1) + "/" + max.length + ")";
    		if (max.length < 2) {
    			head.choices = this._aid.scrChcUnsel(head.choices, 'XXX');
    			head.choices = this._aid.scrChcUnsel(head.choices, 'YYY');
    		}
    		if (p.list) {
    			if (!p.list.length) head.message = expandMissionText("LIB_PAD_NO_ENTRIES");
    			else {
    				min = p.list.length - 1;
    				max = Math.max(0, min - 20);
    				for (i = min; i >= max; i--) head.message += this._aid.scrAddLine([[p.list[i], 30]], "", "\n");
    			}
    		} else {
    			if (temps.indexOf(cat) > -1) {
    				template = con.defTemps[cat];
    				m = this._fillTemplate(p, template);
    				head.message = m[0];
    			} else {
    				head.message = expandMissionText("LIB_PAD_NO_ENTRIES");
    			}
    			if (m[1].silent.notes) {
    				if (con.page === "GENERIC") head.choices.ZZW = expandMissionText("LIB_PAD_ADD_NOTE");
    				else head.choices.ZZW = { text: expandMissionText("LIB_PAD_ADD_NOTE"), unselectable: true };
    			}
    			if (m[1].$crypt) head.choices.ZZV = expandMissionText("LIB_PAD_DECRYPT");
    		}
    		if (pm && (pm[1].length || pm[2].length)) head.choices.ZZU = expandMissionText("LIB_PAD_RELATED");
    		if (note) head.message += note;
    		mission.runScreen(head, this._choices.bind(this));
    		md = mission.displayModel;
    		if (md) {
    			md.orientation = [1, 0, 1, 0]; // invisible
    			md.position = [50, 8, 150];
    			for (i = 0; i < tt.length; i++) {
    				if (m[1][tt[i]]) {
    					md.subEntities[i].setMaterials({ "lib_null.png": { emission_map: m[1][tt[i]] } });
    					md.subEntities[i].orientation = [-1, 0, 1, 0];
    				}
    			}
    			md.subEntities[0].position = [100, 60, 160]; // organisation
    			md.subEntities[2].position = [25, 14, -10]; // rank/enlisted
    			md.subEntities[3].position = [25, -7, -10]; // medal
    			md.subEntities[4].position = [25, -28, -10]; // tex
    			md.subEntities[5].position = [140, 75, -80]; // tex
    		}
    	};
    	this._fillTemplate = function (obj, temp, ps) {
    		var con = this.$config,
    			a = this._aid.objClone(obj),
    			b = this._aid.objClone(temp),
    			ca = Object.keys(b.disp),
    			m = "", i, j, min, max, fill, spec,
    			tt = con.defTex,
    			tts = con.defTexLookUp;
    		for (i = 0; i < ca.length; i++) {
    			if (con.hide.indexOf(ca[i]) !== -1) continue;
    			if (a[ca[i]]) {
    				if (this._aid.typeGet(a[ca[i]]) === "array") {
    					if (ps) continue; // opt out for gallery
    					fill = b.silent[ca[i]] - 1;
    					if (a[ca[i]].length) {
    						min = a[ca[i]].length - 1;
    						max = Math.max(0, a[ca[i]].length - b.silent[ca[i]]);
    						for (j = min; j >= max; j--) {
    							if (j === min) m += this._aid.scrAddLine([[expandMissionText("LIB_PAD_SUB_" + ca[i].toUpperCase()) + ": ", 7], [a[ca[i]][j], 23]], "", "\n");
    							else m += this._aid.scrAddLine([[" ", 7], [a[ca[i]][j], 23]], " ", "\n");
    						}
    						if (fill > min) { // Fill up
    							fill -= min;
    							while (fill--) m += "\n";
    						}
    					} else { // Fill up
    						for (j = fill; j >= 0; j--) {
    							if (j === fill) m += this._aid.scrAddLine([[expandMissionText("LIB_PAD_SUB_" + ca[i].toUpperCase()) + ": ", 7], ["-", 23]], "", "\n");
    							else m += "\n";
    						}
    					}
    				} else if (ca[i] == "text") m += "\n" + a[ca[i]]; // special case for multiline-text items
    				else m += this._aid.scrAddLine([[expandMissionText("LIB_PAD_SUB_" + ca[i].toUpperCase()) + ": ", 7], [a[ca[i]], 23]], "", "\n");
    			} else m += this._aid.scrAddLine([[expandMissionText("LIB_PAD_SUB_" + ca[i].toUpperCase()) + ": ", 7], ["-", 23]], "", "\n");
    		}
    		for (i = 0; i < tt.length; i++) {
    			spec = a[tt[i]];
    			if (spec && b.silent[tt[i]]) {
    				if (typeof (spec) === "number") {
    					if (spec > 0 && spec < con[tts[i]].length) b[tt[i]] = con[tts[i]][spec];
    				} else b[tt[i]] = spec;
    			}
    		}
    		if (a.$crypt && b.silent.$crypt) b.$crypt = a.$crypt;
    		return [m, b];
    	};
    	this._showPlayer = function () {
    		//player.ship.hudHidden = true;
    		this._updatePlayer();
    		var arr = ["LIB_PAD_COMMANDER_NAME", "LIB_PAD_AVATAR", "LIB_PAD_SPECIES", "LIB_PAD_ORIGIN", "LIB_PAD_GENDER", "LIB_PAD_AGE", "LIB_PAD_SHIP_NAME"],
    			c = this.$config, m, md, mdt, min, i,
    			head = this._aid.objClone(c.defHead);
    		head.textEntry = true;
    		head.model = "lib_ms_helper";
    		head.title = expandMissionText("LIB_PAD_LOG_SUMMARY");
    		if (c.setInd === 1) head.model = "lib_ms_helper12x"; // Gallery
    		m = this._fillTemplate(this.$data.PERSONS.GENERIC, c.defTemps.PERSONS, 1);
    		head.message = m[0];
    		head.message += "\n" + expandMissionText(arr[c.setInd]);
    		mission.runScreen(head, this._pSettings.bind(this));
    		md = mission.displayModel;
    		if (md && m && m[1].t1) {
    			md.setMaterials({ "lib_null.png": { emission_map: m[1].t1 } });
    			md.orientation = [1, 0, 0, 0];
    			if (this.$config.setInd === 1) {
    				mdt = c.defImages;
    				min = Math.min(mdt.length, c.setGal + 12);
    				for (i = c.setGal; i < min; i++) md.subEntities[i - c.setGal].setMaterials({ "lib_null.png": { diffuse_map: "lib_pad" + (i - c.setGal + 1) + ".png", emission_map: mdt[i].t1 } });
    				md.position = [0, -12, 150];
    			} else md.position = [50, 40, 150];
    		}
    	};
    	this._pSettings = function (choice) {
    		var c = this.$config, cint = parseInt(choice), mdt, v;
    		if (choice) {
    			switch (c.setInd) {
    				case 0: player.name = choice; break;
    				case 1:
    					if (/\.png/.test(choice)) { // No way to test if image exists. Undocumented.
    						c.ps.image = choice;
    					} else {
    						if (!isNaN(cint)) {
    							mdt = c.defImages;
    							if (cint === 0) { // Next gallery
    								c.setInd--;
    								c.setGal += 12;
    								if (c.setGal > mdt.length - 1) c.setGal = 0;
    							} else {
    								if (cint <= mdt.length && cint > 0) {
    									v = mdt[c.setGal + cint - 1];
    									if (!v) v = { t1: "lib_user.png", s: 3, g: 0, a: [26, 27] };
    									c.ps.image = v.t1;
    									c.ps.species = c.defSpecies[v.s];
    									c.ps.gender = c.defGender[v.g];
    									c.ps.age = this._aid.randXY(v.a[0], v.a[1]);
    								} else c.setInd--;
    							}
    						} else c.setInd--;
    					}
    					break;
    				case 2: c.ps.species = choice; break;
    				case 3: c.ps.origin = choice; break;
    				case 4: c.ps.gender = choice; break;
    				case 5: if (!isNaN(cint) && cint > 0 && cint < 300) c.ps.age = cint; break;
    				case 6: player.ship.shipUniqueName = choice; break;
    			}
    		}
    		c.setInd++;
    		if (c.setInd < 7) this._showPlayer();
    		else {
    			c.curCat = "INIT";
    			c.page = "INIT";
    			c.pageInd = 0;
    			c.psUpd = 1;
    			this._showStart(1);
    		}
    	};
    	this._choices = function (choice) {
    		var con = this.$config,
    			c = Object.keys(this.$data.categories),
    			cat = con.curCat,
    			d = this.$data[cat],
    			ind = con.pageInd,
    			max, p1, p2, act, p3, p4;
    		if (!cat || cat === "INIT") {
    			switch (choice) {
    				case "ZZZ": // Bye
    					if (con.HUD) player.ship.hudHidden = false;
    					con.HUD = 0;
    					con.curCat = "";
    					con.page = "INIT";
    					con.pageInd = 0;
    					break;
    				case "ZZY": // Player settings
    					con.setInd = 0;
    					this._showPlayer();
    					break;
    				case "ZZX": // Search
    					this._showSearch();
    					break;
    				default:
    					ind = c.indexOf(choice);
    					if (ind > -1) {
    						con.curCat = choice;
    						con.page = "GENERIC";
    						con.pageInd = 0;
    						this._showCategory(choice);
    					}
    			}
    		} else {
    			max = Object.keys(d);
    			switch (choice) {
    				case "XXX": // Page up
    					ind++;
    					if (ind >= max.length) ind = 0;
    					con.page = max[ind];
    					con.pageInd = ind;
    					this._showCategory(choice);
    					break;
    				case "YYY": // Page down
    					ind--;
    					if (ind < 0) ind = max.length - 1;
    					con.page = max[ind];
    					con.pageInd = ind;
    					this._showCategory(choice);
    					break;
    				case "ZZU": // Parent/Members
    					this._showRelated();
    					break;
    				case "ZZV": // De-Crypt
    					p1 = d[con.page].$crypt.split(".");
    					p2 = p1.shift();
    					act = this._aid.objGet(worldScripts[p2], p1.join("."), 1);
    					if (act) {
    						p3 = d[con.page].special.join("|");
    						p4 = worldScripts.Lib_Crypt._rot513(p3);
    						d[con.page].special = p4.split("|");
    						d[con.page].$crypt = 0;
    						this.$Search[con.curCat + "." + con.page][0] = this._getEntries(d[con.page]);
    						this._showCategory(choice);
    					} else this._showCategory(choice, expandMissionText("LIB_PAD_NO_DECRYPT"));
    					break;
    				case "ZZW": // Add note
    					this._showAddNote(cat + "." + con.page);
    					break;
    				case "ZZZ": // Back
    					con.curCat = "INIT";
    					con.page = "INIT";
    					con.pageInd = 0;
    					this._showStart(1);
    					break;
    			}
    		}
    	};
    	this._showAddNote = function (path) {
    		var m = this._aid.objGet(this.$data, path + ".notes"),
    			f = m.slice(0).reverse(),
    			head = this._aid.objClone(this.$config.defHead);
    		head.textEntry = true;
    		head.title = expandMissionText("LIB_PAD_ADD_NOTE_PATH", { path: path });
    		this.$config.noteAdd = path;
    		for (var i = 0; i < 10; i++) {
    			if (i < f.length) head.message += this._aid.scrAddLine([[f[i], 23]], "", "\n");
    			else head.message += "\n";
    		}
    		mission.runScreen(head, this._noteAdded.bind(this));
    	};
    	this._noteAdded = function (choice) {
    		if (choice && choice !== "") {
    			this._setPageEntry(this.$config.noteAdd + ".notes", choice);
    			this._showCategory("ZZW", expandMissionText("LIB_PAD_NOTE_ADDED"));
    		} else this._showCategory("ZZW");
    		this.$config.noteAdd = "";
    	};
    	this._showSearch = function () {
    		var c = this.$config,
    			head = this._aid.objClone(c.defHead);
    		if (c.psUpd) this._updatePlayer();
    		head.textEntry = true;
    		head.message = null;
    		head.messageKey = "LIB_PAD_SEARCH";
    		head.title = expandMissionText("LIB_PAD_LOG_SUMMARY");
    		mission.runScreen(head, this._pSearch.bind(this));
    	};
    	this._pSearch = function (choice) {
    		var res = null, i, max,
    			head = this._aid.objClone(this.$config.defHead);
    		head.choices = { ZZZ: expandMissionText("LIB_PAD_BACK") };
    		head.title = expandMissionText("LIB_PAD_SEARCH_RESULT", { result: (choice ? expandMissionText("LIB_PAD_SEARCH_CHOICE", { choice: choice }) : "") });
    		if (choice) {
    			res = this._searchSData(choice);
    			this.$config.lastSearch = choice;
    		}
    		if (res && res.length) {
    			max = Math.min(res.length, 20);
    			for (i = 0; i < max; i++) head.choices[res[i]] = res[i].split(".").join(" : ");
    			for (i = 0; i < 26 - max; i++) head.choices["ZZY" + i] = { text: "", unselectable: true };
    		} else head.message = expandMissionText("LIB_PAD_SEARCH_NOTHING");
    		mission.runScreen(head, this._pSearchRes.bind(this));
    	};
    	this._pSearchRes = function (path) {
    		var con = this.$config,
    			d = this.$data,
    			k, ind;
    		switch (path) {
    			case "ZZZ": this._showStart(1); break;
    			default:
    				path = path.split(".");
    				con.curCat = path[0];
    				con.page = path[1];
    				k = Object.keys(d[path[0]]);
    				ind = k.indexOf(path[1]);
    				con.pageInd = ind;
    				this._showCategory(path[1]);
    		}
    	};
    	this._cullNewsHistory = function (keep) {
    		if (keep == -1) return;
    		var keys = Object.keys(this.$data.NEWS_HISTORY);
    		var num = keys.length;
    		var pointer = 1;
    		// have to keep "GENERIC", so the number we keep is always +1 (ie. GENERIC item + however many we've been told to keep)
    		while (num > (keep + 1)) {
    			var key = keys[pointer];
    			delete this.$data.NEWS_HISTORY[key];
    			delete this.$Search["NEWS_HISTORY." + key];
    			num -= 1;
    			pointer += 1;
    		}
    	}
    	// Returns array with paths
    	this._searchSData = function (word) {
    		var w = new RegExp(word),
    			s = this.$Search,
    			k = Object.keys(s),
    			kl = k.length,
    			d,
    			res = [];
    		for (var i = 0; i < kl; i++) {
    			d = s[k[i]][0];
    			if (w.test(d)) res.push(k[i]);
    			if (res.length > 20) break;
    		}
    		return res;
    	};
    	this._showRelated = function () {
    		var con = this.$config,
    			head = this._aid.objClone(con.defHead),
    			pm = this.$Search[con.curCat + "." + con.page],
    			d = this.$data,
    			pma = pm[1],
    			pmb = pm[2],
    			i;
    		head.choices = this._aid.objClone(con.def);
    		var cat1 = con.defTemps[con.curCat].silent.keyDisplay;
    		if (cat1 == "") cat1 = con.curCat;
    		var page1 = d[con.curCat][con.page].keyDisplay;
    		if (page1 == "") page1 = con.page;
    		head.title = expandMissionText("LIB_PAD_RELATED_TO", { cat: cat1, page: page1 });
    		head.overlay = { name: "lib_pad_text_ovcatmem.png", height: 512 };
    		for (i = 5 * con.relInd; i < 5 + (5 * con.relInd); i++) {
    			if (pma.length > i) {
    				var keys = pma[i].split(".");
    				var key1 = con.defTemps[keys[0]].silent.keyDisplay;
    				if (key1 == "") key1 = keys[0];
    				var key2 = d[keys[0]][keys[1]].keyDisplay;
    				if (key2 == "") key2 = keys[1];
    				head.choices["A" + pma[i]] = key1 + "." + key2;
    			}
    			else head.choices["BZZZ" + i] = { text: "", unselectable: true };
    		}
    		head.choices.CZZZ = { text: "", unselectable: true };
    		for (i = 15 * con.relInd; i < 15 + (15 * con.relInd); i++) {
    			if (pmb.length > i) head.choices["D" + pmb[i]] = pmb[i];
    			else head.choices["EZZZ" + i] = { text: "", unselectable: true };
    		}
    		head.choices.FZZZ = { text: "", unselectable: true };
    		head.choices.GZZZ = { text: "", unselectable: true };
    		head.choices.HZZZ = { text: "", unselectable: true };
    		if (pma.length < 6 && pmb.length < 16) {
    			head.choices = this._aid.scrChcUnsel(head.choices, 'XXX');
    			head.choices = this._aid.scrChcUnsel(head.choices, 'YYY');
    		}
    		mission.runScreen(head, this._related.bind(this));
    	};
    	this._related = function (path) {
    		var con = this.$config,
    			pm = this.$Search[con.curCat + "." + con.page],
    			pma = pm[1],
    			pmb = pm[2],
    			d = this.$data,
    			k, ind;
    		switch (path) {
    			case "XXX":
    				con.relInd++;
    				if (pma.length <= con.relInd * 5 && pmb.length <= con.relInd * 15) con.relInd = 0;
    				this._showRelated();
    				break;
    			case "YYY":
    				con.relInd--;
    				if (con.relInd < 0) con.relInd = Math.max(Math.ceil(pmb.length / 15) - 1, Math.ceil(pma.length / 5) - 1);
    				this._showRelated();
    				break;
    			case "ZZZ": this._showCategory(); break;
    			default:
    				path = path.substr(1);
    				path = path.split(".");
    				con.curCat = path[0];
    				con.page = path[1];
    				k = Object.keys(d[path[0]]);
    				ind = k.indexOf(path[1]);
    				con.pageInd = ind;
    				this._showCategory(path[1]);
    		}
    	};
    	this._Help = function (what) {
    		var h;
    		switch (what) {
    			case "_addPageInCategory": h = "LIB_PAD_HELP_addPageInCategory"; break;
    			case "_setPageEntry": h = "LIB_PAD_HELP_setPageEntry"; break;
    			case "_getData": h = "LIB_PAD_HELP_getData"; break;
    			default: h = "LIB_PAD_HELP";
    		}
    		return expandMissionText(h);
    	};
    }).call(this);
    
    Scripts/Lib_PAD_Events.js
    /* jshint bitwise:false, forin:false */
    /* global clock,galaxyNumber,guiScreen,mission,missionVariables,player,system,worldScripts */
    /* (C) Svengali 2016-2018, License CC-by-nc-sa-4.0 */
    (function () {
    	"use strict";
    	this.name = "Lib_PAD_Events";
    
    	this.$config = {
    		bounty: 0,
    		credits: 0,
    		fined: 0,
    		rank: expandMissionText("LIB_PAD_RANK_HARMLESS"),
    		status: expandMissionText("LIB_PAD_STATUS_CLEAN"),
    		updCR: 0,
    		updNavy: 1,
    		updNova: 1,
    		updTP: 0
    	};
    	this.$delay = [];
    	this.$init = 1;
    	this.startUp = function () {
    		this.$init = 0;
    	};
    	this.startUpComplete = function () {
    		var m = ["PRELUDE", "MISSION_COMPLETE", "STAGE_1", "RUNNING"], mc = -1, mt = -1;
    		if (missionVariables.Lib_PAD_EVENTS) this.$config = JSON.parse(missionVariables.Lib_PAD_EVENTS);
    		if (missionVariables.conhunt) mc = m.indexOf(missionVariables.conhunt);
    		if (missionVariables.thargplans) mt = m.indexOf(missionVariables.thargplans);
    		if (this.$config.updNavy && worldScripts.Lib_PAD.$data.GALCOP.NAVY.entry === "") this._add("GALCOP.NAVY.entry", clock.clockString, 1);
    		this.$config.updNavy = 0;
    		if (mc > -1) this._addCurruthers(1);
    		if (mt > -1) {
    			this._addFortesque(1);
    			if (mt > 0) this._addBlake(1);
    		}
    		if (this.$config.updNova && missionVariables.nova === "NOVA_HERO") {
    			this._add("PERSONS.GENERIC.info", expandMissionText("LIB_EVENTS_NOVA"), 1);
    			this.$config.updNova = 0;
    		}
    		this.$config.bounty = player.bounty;
    		this.$config.credits = player.credits;
    		this.$config.rank = player.rank;
    		this.$config.status = player.legalStatus;
    	};
    	this.playerWillSaveGame = function () {
    		missionVariables.Lib_PAD_EVENTS = JSON.stringify(this.$config);
    	};
    	this.guiScreenChanged = function () {
    		if (!player.ship.docked) return;
    		if (guiScreen === "GUI_SCREEN_MISSION" && mission.screenID) {
    			switch (mission.screenID) {
    				case "oolite-constrictor-hunt-briefing":
    					if (this.$config.updNavy) this._add("GALCOP.NAVY.entry", clock.clockString.substr(0, 13));
    					this.$config.updNavy = 0;
    					this._add("LOGS.GENERIC.list", expandMissionText("LIB_EVENT_CONSTRICTOR_START"));
    					this._addCurruthers();
    					this.$config.updCR = 1;
    					break;
    				case "oolite-constrictor-hunt-debriefing":
    					this._add("LOGS.GENERIC.list", expandMissionText("LIB_EVENT_CONSTRICTOR_WIN1"));
    					this._add("GALCOP.NAVY.missions", expandMissionText("LIB_EVENT_CONSTRICTOR_WIN2"));
    					this._add("GALCOP.NAVY.kills", "++", 1);
    					this.$config.updCR = 0;
    					break;
    				case "oolite-thargoid-plans-briefing1":
    					this._addFortesque();
    					break;
    				case "oolite-thargoid-plans-briefing2":
    					this._add("LOGS.GENERIC.list", expandMissionText("LIB_EVENT_THARPLANS_START"));
    					this._addBlake();
    					this.$config.updTP = 1;
    					break;
    				case "oolite-thargoid-plans-debriefing":
    					this._add("GALCOP.NAVY.missions", expandMissionText("LIB_EVENT_THARPLANS_WIN2"));
    					this._add("LOGS.GENERIC.list", expandMissionText("LIB_EVENT_THARPLANS_WIN1"));
    					this.$config.updTP = 0;
    					break;
    				case "oolite-nova-hero":
    				case "oolite-nova-disappointed":
    				case "oolite-nova-ignored":
    				case "oolite-nova-coward":
    					this._add("PERSONS.GENERIC.info", expandMissionText("LIB_EVENTS_NOVA"));
    					this.$config.updNova = 0;
    					break;
    			}
    		}
    	};
    	this.playerCompletedContract = function (type, result, fee, contract) {
    		var txt, f, prefix;
    		if (!fee) {
    			switch (type) {
    				case "passenger": txt = expandMissionText("LIB_EVENT_CONTRACT_PSNGR_NOFEE", { name: contract.name }); break;
    				case "parcel": txt = expandMissionText("LIB_EVENT_CONTRACT_PARCEL_NOFEE", { name: contract.name }); break;
    				case "cargo": txt = expandMissionText("LIB_EVENT_CONTRACT_CARGO_NOFEE", { name: contract.cargo_description }); break;
    			}
    		} else {
    			// logically, the "failed" result can't get here. in the event of a failed result, the fee would be 0
    			switch(result) {
    				case "success":
    					prefix = expandMissionText("LIB_EVENT_CONTRACT_ARRIVE");
    					break;
    				case "short":
    					prefix = expandMissionText("LIB_EVENT_CONTRACT_SHORT");
    					break;
    				default:
    					prefix = expandMissionText("LIB_EVENT_CONTRACT_LATE");
    			}
    
    			f = formatCredits(fee / 10, 1);
    
    			switch (type) {
    				case "passenger":
    					txt = expandMissionText("LIB_EVENT_CONTRACT_PSNGR_FEE", { prefix: prefix, fee: f, name: contract.name });
    					break;
    				case "parcel":
    					txt = expandMissionText("LIB_EVENT_CONTRACT_PARCEL_FEE", { prefix: prefix, fee: f, name: contract.name });
    					break;
    				case "cargo":
    					txt = expandMissionText("LIB_EVENT_CONTRACT_CARGO_FEE", { prefix: prefix, fee: f, name: contract.cargo_description });
    					break;
    			}
    		}
    		if (txt) this._add("LOGS.GENERIC.list", txt);
    	};
    	this.playerEnteredNewGalaxy = function () {
    		this.$delay.push(["LOGS.GENERIC.list", expandMissionText("LIB_EVENT_JUMP_GAL", { num: +(galaxyNumber + 1) })]);
    	};
    	this.playerRescuedEscapePod = function (fee, reason, pilot) {
    		fee = formatCredits(fee, 1);
    		if (system.isInterstellarSpace) {
    			if (fee) this.$delay.push(["LOGS.GENERIC.list", expandMissionText("LIB_EVENT_RESCUED_POD_INT_FEE", { name: pilot.name, fee: (fee / 10) })]);
    			else this.$delay.push(["LOGS.GENERIC.list", expandMissionText("LIB_EVENT_RESCUED_POD_INT", { name: pilot.name })]);
    		} else {
    			if (fee) this.$delay.push(["LOGS.GENERIC.list", expandMissionText("LIB_EVENT_RESCUED_POD_FEE", { name: pilot.name, sys: system.name, fee: (fee / 10) })]);
    			else this.$delay.push(["LOGS.GENERIC.list", expandMissionText("LIB_EVENT_RESCUED_POD", { name: pilot.name, sys: system.name })]);
    		}
    	};
    	this.shipLaunchedEscapePod = function () {
    		if (system.isInterstellarSpace) this.$delay.push(["LOGS.GENERIC.list", expandMissionText("LIB_EVENT_EJECT_INT")]);
    		else this.$delay.push(["LOGS.GENERIC.list", expandMissionText("LIB_EVENT_EJECT", { sys: system.name })]);
    	};
    	this.shipWillDockWithStation = function () {
    		this.$config.bounty = player.bounty;
    		this.$config.credits = player.credits;
    		if (player.rank !== this.$config.rank) this._add("LOGS.GENERIC.list", expandMissionText("LIB_EVENT_RANK", { rank: player.rank }));
    		this.$config.rank = player.rank;
    		this.$config.status = player.legalStatus;
    		this._addDelayed();
    	};
    	this.shipDockedWithStation = function (st) {
    		if (!player.ship.docked) return;
    		var m;
    		if (this.$config.credits !== player.credits) {
    			if (player.credits > this.$config.credits) m = expandMissionText("LIB_EVENT_EARNED", { amount: formatCredits(player.credits - this.$config.credits, 1), stn: st.displayName });
    			else m = expandMissionText("LIB_EVENT_LOST", { amount: formatCredits(this.$config.credits - player.credits, 1), stn: st.displayName });
    			if (system.isInterstellarSpace) m += ".";
    			else m += " " + system.name + ".";
    			this._add("LOGS.GENERIC.list", m);
    		}
    		if (player.ship.markedForFines) this.$config.fined++;
    	};
    	this.playerBoughtNewShip = function () {
    		this.$config.credits = player.credits;
    	};
    	// Note: Handler is fired (too?) early, so guarding (this.$init) is necessary!!!
    	this.equipmentAdded = function (eq) {
    		if (this.$init) return;
    		this.$config.credits = player.credits;
    		if (eq === "EQ_NAVAL_ENERGY_UNIT") {
    			this._add("LOGS.GENERIC.list", expandMissionText("LIB_EVENT_NEU"));
    			this._add("GALCOP.NAVY.awards", expandMissionText("LIB_EVENT_AWARD_NEU"));
    		}
    		if (eq === "EQ_CLOAKING_DEVICE") {
    			this.$delay.push(["LOGS.GENERIC.list", expandMissionText("LIB_EVENT_CLOAK")]);
    			this.$delay.push(["PERSONS.GENERIC.info", expandMissionText("LIB_EVENT_AWARD_CLOAK")]);
    		}
    	};
    	this.shipLaunchedFromStation = function () {
    		this.$config.bounty = player.bounty;
    		this.$config.credits = player.credits;
    		this.$config.rank = player.rank;
    		this.$config.status = player.legalStatus;
    	};
    	this.shipBountyChanged = function (delta) {
    		var m;
    		if (this.$config.bounty === player.bounty + delta) return;
    		if (!player.bounty) m = expandMissionText("LIB_EVENT_BOUNTY_CLEARED");
    		else {
    			if (delta > 0) m = expandMissionText("LIB_EVENT_BOUNTY_RCVD");
    			if (delta < 0) m = expandMissionText("LIB_EVENT_BOUNTY_REDUCED");
    			if (this.$config.status !== player.legalStatus) m += expandMissionText("LIB_EVENT_BOUNTY_STATUS", { status: player.legalStatus });
    		}
    		if (m) this.$delay.push(["LOGS.GENERIC.list", m]);
    		this.$config.bounty = player.bounty;
    		this.$config.status = player.legalStatus;
    	};
    	this.shipKilledOther = function (whom) {
    		if (player.rank !== this.$config.rank) {
    			this.$delay.push(["LOGS.GENERIC.list", expandMissionText("LIB_EVENT_RANK", { rank: player.rank })]);
    			this.$config.rank = player.rank;
    		}
    		if (this.$config.updTP && whom && whom.isThargoid) this.$delay.push(["GALCOP.NAVY.kills", "++", 1]);
    	};
    	this.shipWillExitWitchspace = function () {
    		if (system.isInterstellarSpace) this.$delay.push(["LOGS.GENERIC.list", expandMissionText("LIB_EVENT_MISJUMP")]);
    	};
    	this._add = function (p, w, s) {
    		worldScripts.Lib_PAD._setPageEntry(p, w, s);
    	};
    	this._addDelayed = function (p, w, s) {
    		var l = this.$delay.length;
    		if (l) {
    			for (var i = 0; i < l; i++) worldScripts.Lib_PAD._setPageEntry(this.$delay[i][0], this.$delay[i][1], this.$delay[i][2]);
    		}
    		this.$delay.length = 0;
    	};
    	this._addPage = function (p, w, t, s) {
    		worldScripts.Lib_PAD._addPageInCategory(p, w, t, s);
    	};
    	this._addCurruthers = function (s) {
    		this._addPage("PERSONS.CAPTAIN CURRUTHERS",
    			{ name: expandMissionText("LIB_CHAR_NAME_CURRUTHERS"), origin: expandMissionText("LIB_CHAR_RESTRICTED"), species: expandMissionText("LIB_PAD_SPECIES_HUMAN_COLONIAL"), gender: expandMissionText("LIB_PAD_GENDER_MALE"), age: 45, ship: expandMissionText("LIB_CHAR_SHIP_VIPER"), rank: expandMissionText("LIB_CHAR_RANK_CAPTAIN"), t0: 21, t1: "lib_ovc_curruthers.png", t2: 8 }, ["GALCOP.NAVY"], s);
    	};
    	this._addFortesque = function (s) {
    		this._addPage("PERSONS.CAPTAIN FORTESQUE", { name: expandMissionText("LIB_CHAR_NAME_FORTESQUE"), origin: expandMissionText("LIB_CHAR_RESTRICTED"), species: expandMissionText("LIB_PAD_SPECIES_HUMAN_COLONIAL"), gender: expandMissionText("LIB_PAD_GENDER_MALE"), age: 41, ship: expandMissionText("LIB_CHAR_SHIP_VIPER"), rank: expandMissionText("LIB_CHAR_RANK_CAPTAIN"), t0: 21, t1: "lib_ovc_fortesque.png", t2: 8 }, ["GALCOP.NAVY"], s);
    	};
    	this._addBlake = function (s) {
    		this._addPage("PERSONS.AGENT BLAKE", { name: expandMissionText("LIB_CHAR_NAME_BLAKE"), origin: expandMissionText("LIB_CHAR_RESTRICTED"), species: expandMissionText("LIB_CHAR_RESTRICTED"), gender: expandMissionText("LIB_PAD_GENDER_MALE"), age: 34, ship: expandMissionText("LIB_CHAR_RESTRICTED"), rank: expandMissionText("LIB_CHAR_RANK_AGENT"), t0: 21, t1: "lib_ovc_blake.png" }, ["GALCOP.NAVY"], s);
    	};
    }).call(this);
    
    Scripts/Lib_Starmap.js
    /* jshint bitwise:false, forin:false */
    /* global mission,player,system,worldScripts,Vector3D */
    /* (C) Svengali 2016-2018, License CC-by-nc-sa-4.0 */
    (function(){
    "use strict";
    this.name = "Lib_Starmap";
    
    this.startUpComplete = function(){
    	worldScripts.Lib_GUI.$IDRules.Lib_Starmap = {mus:1};
    	this.$defs = {
    		off: Vector3D([0,0,0]),
    		rel: Vector3D([0,0,0])
    	};
    	this._update();
    	this.$VSE = null;
    	this.$OP = worldScripts.Lib_Main._lib.ooShaders();
    	this.$POI = null;
    };
    /** function _start( obj ) - System map
    * obj - Object
    *  ini - Array. Max 12 entries.
    *    String or Object { map:TEXTURE,ent:ENTITY [,col:Number] }
    *  message - Optional. String. Initial message text.
    *  title - Optional. String. Screen title.
    *  ani - Optional. Animation for Lib_Animator. Requires capture flag!
    *  POI - Optional. Vector. Point of interest.
    *  MUL - Optional. Number. Multiplier. Default 0.00015.
    * $return -
    */
    this._start = function(obj,force){
    	if(!obj || (this.$VSE && this.$VSE.isValid) || !this.$OP) return false;
    	if(force) this._update();
    	var absZ,col,fin,i,map,mat,md,mul,pos,tmp,rl, trck = [],
    		maxZ = 0, maxY = 0, minZ = 0, minY = 0, mag = 0, sd = 0,
    		dck = player.ship.docked, pls = 0, plm = 0, st = 0, as = 0;
    	this.$POI = null;
    	if(obj.POI){
    		this.$POI = obj.POI;
    		rl = obj.POI;
    	} else rl = this.$defs.rel;
    	if(obj.MUL) mul = obj.MUL;
    	else mul = 0.00015;
    	if(dck){
    		var head = {
    			choices:{ZZZ:expandMissionText("LIB_STARMAP_CONTINUE")},
    			message:obj.message?obj.message:"",
    			background:{name:"lib_starmap_bg.png",height:512},
    			screenID:"Lib_Starmap",
    			model:"lib_ms_helper12",
    			spinModel:false,
    			title:obj.title?obj.title:expandMissionText("LIB_STARMAP_SYSTEMMAP")
    		};
    		this.$HUD = player.ship.hudHidden;
    		player.ship.hudHidden = true;
    		mission.runScreen(head,this._choices);
    		md = mission.displayModel;
    		md.orientation = [1,0,1,0];
    		md.position = [0,0,500];
    	} else {
    		if(!this.$wp || (this.$wp && !this.$wp.isValid)) this.$wp = system.addVisualEffect("lib_starmap_wp",[0,0,0]);
    		md = system.addVisualEffect("lib_starmap12",player.ship.position);
    		md.script.$defs = rl;
    		md.script.$mul = mul;
    	}
    	md.lightsActive = false;
    	if(obj){
    		for(i=0;i<12;i++){
    			mat = md.subEntities[i].getMaterials();
    			col = 0;
    			if(i>obj.ini.length-1){
    				mat["lib_null.png"].textures[0] = "lib_blend0.png";
    			} else {
    				if(typeof(obj.ini[i])==="string"){
    					pos = this.$defs.off;
    					map = "lib_blend0.png";
    					switch(obj.ini[i]){
    						case "PS": map = "lib_starmap_5.png"; pos = player.ship.position; trck.push({ent:player.ship,sub:i}); break;
    						case "S": if(system.sun){map = "lib_starmap_0.png"; pos = system.sun.position; trck.push({ent:system.sun,sub:i});} break;
    						case "P": tmp = this._getPlanet(pls); pls++; if(tmp){map = tmp[0]; pos = tmp[1].position; trck.push({ent:tmp[1],sub:i});} break;
    						case "M": tmp = this._getMoon(plm); plm++; if(tmp){map = tmp[0]; pos = tmp[1].position; trck.push({ent:tmp[1],sub:i});} break;
    						case "ST": if(this.$st.sta){map = "lib_starmap_3.png"; pos = this.$st.sta.position; trck.push({ent:this.$st.sta,sub:i});} break;
    						case "STG": tmp = this._getGalStation(st); st++; if(tmp){map = tmp[0]; pos = tmp[1].position; trck.push({ent:tmp[1],sub:i});
    							if(tmp[1].isPolice) col = 4; else col = 3;} break;
    						case "WP": map = "lib_starmap_2.png"; trck.push({ent:this.$wp,sub:i}); break;
    						case "H": if(this.$st.sth.length){map = "lib_starmap_4.png"; pos = this.$st.sth[0].position; trck.push({ent:this.$st.sth[0],sub:i});} break;
    						case "AS": tmp = this._getAsteroidField(as); as += 20; if(tmp){map = tmp[0]; pos = tmp[1].position; trck.push({ent:tmp[1],sub:i});} break;
    					}
    				} else {
    					if(obj.ini[i].ent){
    						pos = obj.ini[i].ent.position;
    						trck.push({ent:obj.ini[i].ent,sub:i});
    					} else {
    						if(dck) pos = obj.ini[i].pos;
    						else continue;
    					}
    					map = obj.ini[i].map;
    					if(obj.ini[i].col) col = obj.ini[i].col;
    				}
    				if(col) mat["lib_null.png"].uniforms.MC.value = this._getColor(col);
    				mat["lib_null.png"].textures[0] = map;
    				pos = this._compress(pos);
    				// Scale down for display
    				fin = pos.subtract(rl).multiply(mul);
    				if(dck){
    					if(fin.z>maxZ) maxZ = fin.z;
    					if(fin.y>maxY) maxY = fin.y;
    					if(fin.z<minZ) minZ = fin.z;
    					if(fin.y<minY) minY = fin.y;
    				} else {
    					mag = fin.magnitude();
    					if(mag>110) sd = (mag-100)*0.1;
    				}
    				md.subEntities[i].position = fin;
    			}
    			md.subEntities[i].setMaterials(mat);
    		}
    		if(dck){
    			// Auto zoom
    			absZ = Vector3D([(-minZ)+maxZ,(-minY)+maxY,0]).magnitude();
    			absZ = Math.max(absZ,180);
    			md.position = [0,0,absZ*2];
    			// Animation
    			if(obj.ani) worldScripts.Lib_Animator._start(obj.ani);
    		} else {
    			if(sd) md.script.$mul = mul/sd;
    			// Track
    			if(trck.length) md.script.$upd = trck;
    			if(player.ship.compassTarget) md.orientation = player.ship.compassTarget.position.rotationTo(rl);
    			else md.orientation = player.ship.position.rotationTo(rl);
    			this.$VSE = md;
    			this.$EnableTimer = new Timer(this,this._doEnableTimer,1);
    		}
    	}
    	return true;
    };
    this._getPlanet = function(n){
    	if(!n){
    		if(!this.$pl.pla) return null;
    		return(["lib_starmap_1.png",this.$pl.pla]);
    	}
    	if(this.$pl.plp.length<=n-1) return null;
    	return(["lib_starmap_P1.png",this.$pl.plp[n-1]]);
    };
    this._getMoon = function(n){
    	if(this.$pl.plm.length<=n) return null;
    	return(["lib_starmap_M1.png",this.$pl.plm[n]]);
    };
    this._getGalStation = function(n){
    	if(this.$st.stgal.length<=n) return null;
    	if(this.$st.stgal[n].maxSpeed>5) return(["lib_starmap_i4.png",this.$st.stgal[n]]);
    	return(["lib_starmap_i5.png",this.$st.stgal[n]]);
    };
    this._getAsteroidField = function(n){
    	if(this.$as.length<=n) return null;
    	return(["lib_starmap_7.png",this.$as[n]]);
    };
    this._getColor = function(n){
    	var c;
    	switch(n){
    		case 1: c = [0.8,0,0,1]; break; // red
    		case 2: c = [0,0.8,0,1]; break; // green
    		case 3: c = [0,0,0.8,1]; break; // blue
    		case 4: c = [0.6,0,0.8,1]; break; // purple
    		case 5: c = [0.4,0.4,0.4,1]; break; // gray
    		case 6: c = [0,0.7,0.7,1]; break; // aqua
    		case 7: c = [0.8,0.6,0,1]; break; // amber
    		case 8: c = [0.8,0.6,0.3,1]; break; // gold
    		case 9: c = [0.3,0,0.5,1]; break; // UV
    		default: c = [1,1,1,1]; break; // white
    	}
    	return c;
    };
    this._addInFreeSlot = function(ent,map,col){
    	if(!this.$VSE || !this.$VSE.isValid) return;
    	var x = this.$VSE.script.$upd,m=x.length,slot,mat;
    	for(var i=0;i<12;i++){
    		if(i>m-1 || !x[i].ent){slot = i; break;}
    	}
    	if(typeof(slot)!=="number" || slot>11) return false;
    	mat = this.$VSE.subEntities[slot].getMaterials();
    	mat["lib_null.png"].textures[0] = map;
    	mat["lib_null.png"].uniforms.MC.value = this._getColor(col);
    	if(slot<=m) x[slot] = {ent:ent,sub:slot};
    	else x.push({ent:ent,sub:slot});
    	this.$VSE.script.$upd = x;
    	this.$VSE.subEntities[slot].setMaterials(mat);
    	this.$VSE.subEntities[slot].shaderVector1 = [0,0,0];
    	return true;
    };
    // Inflight options
    this._doEnableTimer = function _doEnableTimer(){
    	if(player.alertCondition<3 && !player.ship.weaponsOnline){
    		this._switchOn();
    		if(!this.$VSE || !this.$VSE.isValid) return;
    		this.$VSE.shaderFloat1 = 1;
    	} else this._switchOff();
    	this.$EnableTimer = null;
    };
    this.alertConditionChanged = function(s){
    	if(s>2) this._switchOff();
    	else if(s>0 && !player.ship.weaponsOnline) this._switchOn();
    };
    this.compassTargetChanged = function(){
    	if(!this.$VSE || !this.$VSE.isValid) return;
    	if(this.$POI) this.$VSE.orientation = player.ship.compassTarget.position.rotationTo(this.$POI);
    	else this.$VSE.orientation = player.ship.compassTarget.position.rotationTo(this.$defs.rel);
    };
    this.weaponsSystemsToggled = function(state){
    	if(state) this._switchOff();
    	else if(player.alertCondition<3) this._switchOn();
    };
    this._switchOn = function(){
    	if(!this.$VSE || !this.$VSE.isValid) return;
    	this.$VSE.script.$hide = true;
    	if(!this.$EnableTimer) this.$EnableTimer = new Timer(this,this._doEnableTimer,1);
    };
    this._switchOff = function(){
    	if(!this.$VSE || !this.$VSE.isValid) return;
    	this.$VSE.shaderFloat1 = 0;
    	this.$VSE.script.$hide = false;
    };
    this._toggleMP = function(){
    	if(!this.$VSE || !this.$VSE.isValid) return;
    	this.$VSE.script.$PSC = !this.$VSE.script.$PSC;
    };
    // Update after populator
    this.shipExitedWitchspace = this.shipWillDockWithStation = this.shipWillLaunchFromStation = function(){
    	this._update();
    };
    this._update = function(){
    	if(this.$VSE && this.$VSE.isValid) this.$VSE.remove();
    	this.$VSE = null;
    	this._updPlanets();
    	this._updStations();
    	this._updAsteroids();
    	this._getMidpoint();
    };
    this._updPlanets = function(){
    	var pl = system.planets;
    	this.$pl = {pla:null,plp:[],plm:[]};
    	for(var i=0;i<pl.length;i++){
    		if(pl[i].isMainPlanet) this.$pl.pla = pl[i];
    		else {
    			if(pl[i].hasAtmosphere) this.$pl.plp.push(pl[i]);
    			else this.$pl.plm.push(pl[i]);
    		}
    	}
    };
    this._updStations = function(){
    	var st = system.stations;
    	this.$st = {sta:null,sth:[],stgal:[],stoth:[]};
    	for(var i=0;i<st.length;i++){
    		if(st[i].isMainStation) this.$st.sta = st[i];
    		else if(st[i].isRock && st[i].name==="Rock Hermit") this.$st.sth.push(st[i]);
    		else {
    			switch(st[i].allegiance){
    				case "galcop":
    				case "hunter":
    				case "neutral": this.$st.stgal.push(st[i]); break;
    				default: if(st[i].beaconCode) this.$st.stoth.push(st[i]);
    			}
    		}
    	}
    };
    this._updAsteroids = function(){
    	this.$as = system.filteredEntities(this,function(entity){return entity.isShip && entity.isRock && entity.name==="Asteroid";},player.ship);
    };
    this._getMidpoint = function(){
    	if(system.isInterstellarSpace){
    		this.$defs.rel = this.$defs.off;
    		return;
    	} else {
    		// no c as WP is [0,0,0]
    		var a = this._compress(system.sun.position), b = this._compress(system.mainPlanet.position);
    		this.$defs.rel = Vector3D([(a.x+b.x)/3,(a.y+b.y)/3,(a.z+b.z)/3]);
    	}
    };
    // Compress far out
    this._compress = function(pos){
    	var pmag = pos.magnitude();
    	if(pmag>1199998.8) pos = Vector3D.interpolate(this.$defs.off,pos,1199998.8/pmag);
    	return pos;
    };
    this._choices = function(choice){worldScripts.Lib_Starmap._choiceEval(choice); return;};
    this._choiceEval = function(choice){
    	player.ship.hudHidden = this.$HUD;
    };
    }).call(this);
    Scripts/Lib_Starmap.txt
    // Display traders
    var a = system.filteredEntities(this,function(entity){return entity.isShip && !entity.isStation && !entity.owner && entity.isTrader;},player.ship),b=["PS","WP","P","ST","S"];
    for(var i=0;i<7;i++){if(a.length>i){b.push({map:"lib_starmap_i2.png",ent:a[i],col:5});}}worldScripts.Lib_Starmap._start({ini:b});
    
    // Display pirates
    var a = system.filteredEntities(this,function(entity){return entity.isShip && !entity.isStation && !entity.owner && entity.isPirate;},player.ship),b=["PS","WP","P","ST","S"];
    for(var i=0;i<7;i++){if(a.length>i){b.push({map:"lib_starmap_i3.png",ent:a[i],col:1});}}worldScripts.Lib_Starmap._start({ini:b});
    
    // Display police
    var a = system.filteredEntities(this,function(entity){return entity.isShip && !entity.isStation && !entity.owner && entity.isPolice;},player.ship),b=["PS","WP","P","ST","S"];
    for(var i=0;i<7;i++){if(a.length>i){b.push({map:"lib_starmap_i1.png",ent:a[i],col:4});}}worldScripts.Lib_Starmap._start({ini:b});
    
    // Default - Player, Witchpoint, Planet, Sun, MainStation, Hermit, Asteroid fields and secondary stations
    worldScripts.Lib_Starmap._start( { ini:["PS","WP","P","S","ST","H","AS","AS","AS","STG","STG","STG"] } );
    
    // Display fun
    var a = system.filteredEntities(this,function(entity){return entity.isShip && !entity.isStation && !entity.owner && entity.isTrader},player.ship),
    	c = system.filteredEntities(this,function(entity){return entity.isShip && !entity.isStation && !entity.owner && entity.isPirate && entity.group && entity.group.leader && entity.entityPersonality===entity.group.leader.entityPersonality;},player.ship),
    	b=["PS","WP","P","ST","S"];
    for(var i=0;i<15;i++){
    	if(b.length===12) break;
    	if((i&1) && a.length>i){b.push({map:"lib_starmap_i2.png",ent:a[i],col:5});}
    	else if(c.length>i){b.push({map:"lib_starmap_i2.png",ent:c[i],col:1});}
    }
    worldScripts.Lib_Starmap._start({ini:b});
    
    Scripts/lib_conditions.js
    /* (C) Svengali 2016-2018, License CC-by-nc-sa-4.0 */
    "use strict";
    this.name = "lib_conditions";
    
    this.allowSpawnShip = function(shipKey){
    	return true;
    };
    
    Scripts/lib_fx.js
    /* global addFrameCallback,player,removeFrameCallback */
    /* (C) Svengali 2016-2018, License CC-by-nc-sa-4.0 */
    (function(){
    "use strict";
    this.name = "lib_fx";
    
    this.$time = 0;
    // Set on spawning
    this.$repos = 250; 
    this.$reposTil = 0;
    this.$ridTime = 4;
    this.$look = 0;
    this.$view = 0;
    this.$offset = 0;
    this.$glare = player.ship.sunGlareFilter;
    player.ship.sunGlareFilter = 1;
    
    this.effectSpawned = function(){
    	this.$fcb = addFrameCallback(this._repos.bind(this));
    };
    this.effectRemoved = function(){
    	removeFrameCallback(this.$fcb);
    };
    this._repos = function(delta){
    	var ps = player.ship,vF=ps.vectorForward,lv,v,pos,npos,sgf;
    	if(!ps.isValid || this.$time>this.$ridTime){
    		this.visualEffect.remove();
    		return;
    	}
    	this.$time += delta;
    	if(!delta) return;
    	if(this.$glare<1 && this.$time>this.$reposTil-1){
    		if(ps.sunGlareFilter-delta>this.$glare) ps.sunGlareFilter -= delta;
    		else ps.sunGlareFilter = this.$glare;
    	}
    	if(this.$reposTil && this.$time>this.$reposTil) this.$repos = 0;
    	if(this.$repos){
    		if(this.$view){
    			switch(ps.viewDirection){
    				case "VIEW_AFT": pos = ps.position.add(ps.viewPositionAft); break;
    				case "VIEW_PORT": pos = ps.position.add(ps.viewPositionPort); break;
    				case "VIEW_STARBOARD": pos = ps.position.add(ps.viewPositionStarboard); break;
    				case "VIEW_FORWARD": pos = ps.position.add(ps.viewPositionForward); break;
    				default: pos = ps.position;
    			}
    			pos = pos.add(vF.multiply(this.$repos));
    		} else pos = ps.position.add(vF.multiply(this.$repos)).add(ps.vectorUp.multiply(this.$offset));
    		this.visualEffect.position = pos;
    		this.visualEffect.orientation = ps.orientation;
    		if(this.$look){
    			switch(ps.viewDirection){
    				case "VIEW_AFT": lv = [0,0,-1.07]; break;
    				case "VIEW_PORT": lv = [-3,0,1.4]; break;
    				case "VIEW_STARBOARD": lv = [3,0,1.4]; break;
    				case "VIEW_FORWARD": lv = [0,0,1.4]; break;
    				default: lv = [0,0,1.4];
    			}
    			this.visualEffect.shaderVector2 = lv;
    		}
    	}
    };
    }).call(this);
    
    Scripts/lib_shield.js
    "use strict";
    this.name = "lib_shield";
    
    this.$fcb;
    this.$parent;
    
    this.effectSpawned = function(){
    	this.$fcb = addFrameCallback(this._updatePosition.bind(this));
    };
    this.effectRemoved = function(){
    	removeFrameCallback(this.$fcb);
    };
    this._updatePosition = function(){
    	if(!this.$parent) return;
    	if(player.ship.position){
    		this.visualEffect.position = this.$parent.position.subtract(this.$parent.vectorForward.multiply(-120));
    		this.visualEffect.position = this.visualEffect.position.add(this.$parent.position.subtract(player.ship.position).direction().multiply(3));
    	} else removeFrameCallback(this.$fcb);
    };
    
    Scripts/lib_starmap12.js
    /* global addFrameCallback,player,removeFrameCallback,Vector3D */
    /* (C) Svengali 2016-2018, License CC-by-nc-sa-4.0 */
    (function(){
    "use strict";
    this.name = "lib_starmap12";
    
    this.$upd = [];
    this.$c = 0;
    this.$pos = null;
    this.$pmag = 0;
    this.$ps = player.ship;
    this.$defs = null;
    this.$mul = 0.00015;
    this.$hide = true;
    this.$PSC = 0;
    this.effectSpawned = function(){
    	this.$fcb = addFrameCallback(this._repos.bind(this));
    };
    this.effectRemoved = function(){
    	removeFrameCallback(this.$fcb);
    };
    this._repos = function(){
    	if(!this.$ps.isValid || !this.$ps.isInSpace){
    		this.visualEffect.remove();
    		return;
    	}
    	if(!this.$defs || (!this.visualEffect.shaderFloat1 && !this.$hide)) return;
    	else {
    		var ul = this.$upd.length;
    		this.visualEffect.position = this.$ps.position.add(this.$ps.vectorForward.multiply(400)).add(this.$ps.vectorUp.multiply(80)).add(this.$ps.vectorRight.multiply(80));
    		switch(this.$c){
    			case 0: if(ul) this._updatePos(this.$upd[0]); break;
    			case 1: if(ul>1) this._updatePos(this.$upd[1]); break;
    			case 2: if(ul>2) this._updatePos(this.$upd[2]); break;
    			case 3: if(ul>3) this._updatePos(this.$upd[3]); break;
    			case 4: if(ul>4) this._updatePos(this.$upd[4]); break;
    			case 5: if(ul>5) this._updatePos(this.$upd[5]); break;
    			case 6: if(ul>6) this._updatePos(this.$upd[6]); break;
    			case 7: if(ul>7) this._updatePos(this.$upd[7]); break;
    			case 8: if(ul>8) this._updatePos(this.$upd[8]); break;
    			case 9: if(ul>9) this._updatePos(this.$upd[9]); break;
    			case 10: if(ul>10) this._updatePos(this.$upd[10]); break;
    			case 11: if(ul>11) this._updatePos(this.$upd[11]); break;
    		}
    		this.$c++;
    		this.$c %= 12;
    	}
    };
    this._updatePos = function(obj){
    	if(!obj.ent) return;
    	if(!obj.ent.isValid || !obj.ent.isInSpace){
    		this.visualEffect.subEntities[obj.sub].shaderVector1 = [0,0,1];
    		obj.ent = null;
    	} else {
    		this.$pos = obj.ent.position;
    		this.$pmag = this.$pos.magnitude();
    		if(this.$PSC){
    			if(this.$pmag>1199998.8) this.$pos = Vector3D.interpolate(this.$ps.position,this.$pos,1199998.8/this.$pmag);
    			this.visualEffect.subEntities[obj.sub].position = this.$pos.subtract(this.$ps.position).multiply(this.$mul);
    		} else {
    			if(this.$pmag>1199998.8) this.$pos = Vector3D.interpolate(this.$defs,this.$pos,1199998.8/this.$pmag);
    			this.visualEffect.subEntities[obj.sub].position = this.$pos.subtract(this.$defs).multiply(this.$mul);
    		}
    	}
    };
    }).call(this);
    
    Scripts/lib_test.js
    /* global system,worldScripts,Timer */
    /* (C) Svengali 2016-2018, License CC-by-nc-sa-4.0 - Detect changes in custom role entity */
    (function(){
    "use strict";
    this.name = "lib_test";
    
    // There are a few AddOns which are changing / removing custom role entities. Uncalled!
    
    this.shipSpawned = function(){
    	delete this.shipSpawned;
    	this.$checkTimer = new Timer(this,this._doCheck,0.5);
    };
    this._stopTimers = function(warn){
    	if(this.$checkTimer) this.$checkTimer.stop();
    	this.$checkTimer = null;
    	return;
    };
    this._doCheck = function _doCheck(){
    	var eq = {
    		accuracy: -2,
    		autoAI: false,
    		autoWeapons: false,
    		bounty: 0,
    		cloakAutomatic: false,
    		energyRechargeRate: 3,
    		fuel: 6,
    		heatInsulation: 1,
    		isBeacon: false,
    		isCloaked: false,
    		isDerelict: false,
    		isJamming: false,
    		isPirate: false,
    		isPirateVictim: false,
    		isPolice: false,
    		isTrader: false,
    		maxEnergy: 450,
    		maxEscorts: 2,
    		missileCapacity: 4,
    		missileLoadTime: 4,
    		name: "Boa",
    		shipClassName: "Boa"
    	},
    	eqs = {
    		aftWeapon: ["EQ_WEAPON_PULSE_LASER"],
    		equipment: ["EQ_FUEL_SCOOPS","EQ_ESCAPE_POD"],
    		forwardWeapon: ["EQ_WEAPON_PULSE_LASER"],
    		missiles: ["EQ_MISSILE","EQ_MISSILE","EQ_MISSILE"],
    		portWeapon: ["EQ_WEAPON_NONE"],
    		starboardWeapon: ["EQ_WEAPON_NONE"]
    	},
    	wps = ["weaponPositionForward","weaponPositionAft","weaponPositionPort","weaponPositionStarboard"],
    	listA = Object.keys(eq), lA = listA.length,
    	listB = Object.keys(eqs), lB = listB.length,
    	i,j,k,cur, warn = worldScripts.Lib_Main._lib.$entCstChanged;
    	for(i=0;i<lA;i++) if(this.ship[listA[i]] !== eq[listA[i]] && warn.indexOf(listA[i])===-1) warn.push(listA[i]);
    	for(i=0;i<lB;i++){
    		cur = this.ship[listB[i]];
    		if(cur.length){
    			for(j=0;j<cur.length;j++) if(cur[j].equipmentKey !== eqs[listB[i]][j] && warn.indexOf(listB[i])===-1) warn.push(cur[j].equipmentKey);
    		} else if(cur.equipmentKey !== eqs[listB[i]][0] && warn.indexOf(listB[i])===-1) warn.push(listB[i]);
    	}
    	for(k=0;k<4;k++) if(this.ship[wps[k]].length>1 && warn.indexOf(wps[k])===-1) warn.push(wps[k]);
    	if(145!==this.shipDied.toSource().length+this.shipRemoved.toSource().length) worldScripts.Lib_Main._lib.$entCstPatched = true;
    	this._stopTimers();
    };
    this.shipDied = function(){this._stopTimers();};
    this.shipRemoved = function(){this._stopTimers(); worldScripts.Lib_Main._lib.$entCstRemoved = true;};
    this.entityDestroyed = function(){this._stopTimers();};
    this.shipLaunchedEscapePod = function(){this._stopTimers();};
    this.shipWillEnterWormhole = function(){this._stopTimers();};
    this.playerWillEnterWitchspace = function(){this._stopTimers();};
    }).call(this);