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

Expansion Trails

Content

Manifest

from Expansion Manager's OXP list from Expansion Manifest
Description Ships leaves condensation trails, rocks make gray vapour when move. Works without shaders but not recommended for old computers where FPS could be low when display many trails. No trails in Red Alert below 50 FPS if weapons are online. Ships leaves condensation trails, rocks make gray vapour when move. Works without shaders but not recommended for old computers where FPS could be low when display many trails. No trails in Red Alert below 50 FPS if weapons are online.
Identifier oolite.oxp.Norby.Trails oolite.oxp.Norby.Trails
Title Trails Trails
Category Ambience Ambience
Author Norby Norby
Version 1.11 1.11
Tags
Required Oolite Version
Maximum Oolite Version
Required Expansions
Optional Expansions
Conflict Expansions
Dependent Expansions
  • oolite.oxp.Norby.Ambience_Collection:1.3
  • oolite.oxp.Norby.Trail_Detector:1.0
  • Information URL http://wiki.alioth.net/index.php/Trails n/a
    Download URL https://wiki.alioth.net/img_auth.php/c/ca/Trails_1.11.oxz n/a
    License CC BY-NC-SA 4 CC BY-NC-SA 4
    File Size n/a
    Upload date 1610873471

    Relationships Diagram

    Documentation

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

    Trails_readme.txt

    Trails
    
    NPC ships leaves condensation trails. Rocks make gray vapour when move.
    
    Player ship will not make trail to avoid disturbing the aft view.
    
    Based on Zireael's Engine Trails but rewritten from scratch to avoid the "Universe is full" error.
    
    The default lifetime of a trail element is 9 seconds, the maximal length is 300m and will be reduced automatically when the total number of entities step over 1500.
    
    The lifetime and the number of used light sources will be reduced below 25 FPS which cause thicker trails.
    
    No trails in Red Alert below 50 FPS if weapons are online for smooth combat.
    
    Visual elements based on the fuel leak effect in CommonSenseOTB's CustomShields OXP.
    
    
    Instructions:
    
    Do not unzip the .oxz file, just move into the AddOns folder of your Oolite installation.
    
    License:
    
    This work is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike License version 4.0.
    If you are re-using any piece of this OXP, please let me know by sending an e-mail to norbylite@gmail.com.
    
    
    Changelog:
     2016.05.04. v1.11 No trails after mini ships below 10t mass like Tie Fighters.
     2016.02.16. v1.9  No trails in Red Alert below 50 FPS if weapons are online for smooth combat.
                       Fixed trail on Gnat, thanks to phkb.
     2016.02.15. v1.8  Render trail elements in sight only, others moved far away.
                       Lifetime of elements start at 9 seconds and step down to 3 seconds in low-FPS.
     2016.02.14. v1.7  New low-FPS mode added.
     2016.02.13. v1.6  Time-based trail dissipation so trail length is depending on speed.
                       Less visible trails at long range.
                       Thinner trail on small ships.
                       Missiles are excluded.
     2016.02.03. v1.5  Sparks in Griff Alloys and Wreckage OXP are are excluded for explosions.
     2016.02.03. v1.4  Wreckages, alloys and cargopods are excluded for speed up explosions.
                       Increase quality back to normal over 50 FPS and not the double of MinFPS.
     2016.02.03. v1.3  Rocks make gray vapour when move.
                       Speedup for explosions: wreckages make rare trail only.
                       General speedup due to less flashers (half than before).
                       Another speedup on trails over 12km.
                       Default of MinFPS is increased to 25 FPS.
                       Fixed an oddity with Telescope.
     2015.05.01. v1.2  Escort ships on EscortDeck does not exhaust fumes anymore.
                       Fixed a gap after fast ships.
                       No more trails after ships without exhaust like Thargoids, Nyoka.
                       Steam trails after moving rocks.
                       Always use less flashers in trails over 10km.
                       Trail starts near the engine.
     2015.05.01. v1.1  Use less flashers in trails for better performance.
                       Visibility of trails increased up to 2x scanner range.
     2015.04.30. v1.0  First release.
    

    Equipment

    This expansion declares no equipment.

    Ships

    This expansion declares no ships.

    Models

    This expansion declares no models.

    Scripts

    Path
    Scripts/trails.js
    "use strict";
    this.name	= "trails";
    this.author	= "Norby";
    this.copyright	= "2015 Norby";
    this.description= "Ships leaves condensation trails";
    this.licence	= "CC BY-NC-SA 4.0";
    
    this.$GoodFPS = 50; //if FPS is over this and quality is reduced then increase back
    this.$LowFPS = 25; //if fall below then reduce quality using smaller number in dataKey
    this.$Zoom = false; //increase brightness over 12km to detect far ships, disabled in ambience pack
    
    //internal properties, should not touch
    this.$Delta = 0; //delta counter for FCB
    this.$Delta2 = 0; //store first delta in the last second for FCB
    this.$Distance = 60; //distance between trail elements based on effectada.plist
    this.$ED = null; //store a pointer to escortdeck worldscript
    this.$FC = 0; //frame counter for calculating FPS
    this.$FP = null; //FCB pointer
    this.$FPS = 60; //lastly measured FPS
    this.$Length = this.$MaxLength; //actual length to avoid "Universe is full"
    this.$MaxAngle = 0.7; //max. angle of sight in radian (0.7 for FOV80), move far away if out of sight
    this.$MaxKey = 3; //max. used dataKey from effectdata.plist ("trails3")
    this.$Key = this.$MaxKey; //start at the max. available dataKey
    this.$MaxLength = 50; //how many elements are in a trail (60m each) based on effectdata.plist
    this.$MaxSec = 3 + 2*this.$MaxKey; //max timeout of trail elements in seconds (9)
    this.$Old = false; //old version before Oolite v1.82
    this.$Red = false; //No trails in red alert below 50 FPS if weapons are online
    this.$S = [];	//tracked ships and visualEffects
    		//$S[i][0]: the ship object,
    		//$S[i][1]: last position of the ship where a visualEffect is created - unused
    		//$S[i][2]: lastly used "k" indexes
                    //$S[i][k] where k >= 3: visualEffects
    this.$Sec = this.$MaxSec; //timeout of trail elements in seconds (3-6)
    this.$Shift = 10000000000; //x coord. shift if the trail element is not in sight
    this.$T = [];	//timestamps of the creation of the trail elements
    //this.$V = [];	//storage of all visualEffects
    
    //worldscript events
    this.startUp = function() {
    	this.$ED = worldScripts.escortdeck;
    	this.$Clear(this);
    	this.$FC = 0; //frame counter
    	this.$Delta = 0; //delta counter
    	if (0 < oolite.compareVersion("1.82")) {
    		this.$Old = true;
    /*		
    		//pre-creating visualEffects
    		var key = "trails";
    		var pos = Vector3D(100000000,0,0);
    		for( var i = 0; i < 100; i++ ) {
    			this.$V.push(system.addVisualEffect( key, pos ));
    		}
    */		
    	} else { //from Oolite v1.82 flashers in subentities are usable, like in "trails+"
    		this.$Old = false;
    	}
    	if( !isValidFrameCallback( this.$FP ) )
                    this.$FP = addFrameCallback( this.$FCB.bind(this) );
    }
    
    this.shipWillEnterWitchspace = function() {
    	this.$Clear(this);
    }
    
    //local methods
    this.$Clear = function(w) {  //start a new array, free up memory
    	w.$Length = w.$MaxLength;
            w.$RemoveAllTrails(w);
    	delete w.$S;
    	w.$S = [];
    	w.$S[0] = [];
    	w.$S[0][0] = player.ship; //the first ship
            delete w.$T;
            w.$T = [];
            w.$T[0] = [];
            w.$Red = false;
    }
    
    this.$FCB = function( delta ) {
    	var w = this; //worldscript
    	var p = player.ship;
    	var pp = false;
    	if( p && p.isValid ) pp = p.position;
    	w.$FC++; //frame counter for calculating FPS
    	w.$Delta += delta;
    	if( w.$Delta > w.$Delta2 + 1 && pp ) { //like an 1s timer, search for new ships
    		w.$FPS = w.$FC; //save measured fps for the trail length reducer code below
    		w.$FC = 0; //reset frame counter
    		w.$Delta2 = w.$Delta; //save delta
    		
    		//No trails in red alert below 50 FPS if weapons are online
                    if( player.alertCondition > 2 && p.weaponsOnline ) {
                            if( !w.$Red && w.$FPS < w.$GoodFPS ) {
                                    //first time after red alert started
                                    w.$RemoveAllTrails(w);
                                    w.$Red = true;
                            } //restore trails in red alert over 100FPS only
                            if( w.$Red && w.$FPS < 2 * w.$GoodFPS ) return;
                    }
                    w.$Red = false;
    		
    		var ships = [];
    		if( !p.docked ) ships = p.checkScanner(false);//faster than filteredEntities
    				//must use checkScanner with false to add the "green" escorts
    				//and a good side effect boulders get steam tails
    				//moreover true return the full list in Oolite 1.80 (fixed in 1.81)
    		for( var i = 1; i < w.$S.length; i++ ) { //remove trails of far ships
    			var o = w.$S[i]; //data of an old ship
    			var x = -2;
    			if( o && o[0] && o[0].isValid ) {
    				x = ships.indexOf( o[0] );
    				if( w.$ED ) { //disable escort trails on deck, enable if launched
    					if( !o[0].script ) //for sure
    						o[0].setScript("oolite-default-ship-script.js");
    					var pad = w.$ED.$EscortDeckShip.indexOf( o[0] );
    					if( pad != -1 ) { //change on ships used by escortdeck only
                                                    if( w.$ED.$EscortDeckShipPos[pad] )
                                                            o[0].script.$TrailsDisabled = true;
                                                    else o[0].script.$TrailsDisabled = false;
    					}
    				}
    			}
    			//if invalid then docked, jumped or destroyed, leave to run remove below
    			if( x == -1 ) { //not in the scanner anymore?
    				//must check distance due to many ships near exhibitions
    				//can leave out important ones from the limited checkScanner list
    				if( pp.distanceTo( o[0].position ) > 2 * p.scannerRange ) {
    					for( var k = 3; k <= w.$Length; k++ ) {
    						var v = o[k]; //remove visualEffects
    						if( v && v.isValid ) v.remove();
    					}
    					w.$S[i] = false; //remove this ship from the tracked array
    				}
    			} else if( x >= 0 ) ships[x] = false; //don't add into $S if already in
    
    		}
    		
    		if( p.docked ) {
    			if( w.$S[1] ) w.$Clear(w); //start a new array, free up memory
    		} else for( var i = 0; i < ships.length; i++ ) { //add new ships into $S
    			var ship = ships[i];
    			if( ship && ship.isValid && ship.maxSpeed > 0 //do not add buoy, etc.
    				&& ( ship.exhausts && ship.exhausts[0] //need valid exhaust
    					|| ship.scanClass == "CLASS_ROCK" )
                                    && !ship.isMissile
                                    && ship.primaryRole != "wreckage" //exclude for explosions
                                    && ship.primaryRole != "oolite-wreckage-chunk"
                                    && ship.primaryRole != "alloy"
                                    && ship.primaryRole != "cargopod"
                                    && ship.primaryRole != "griffspark" //explosion in griff's wreckages
    				&& ship.dataKey != "telescopemarker" //exclude for Telescope
                                    && ship.mass > 10000 //exclude TIE fighters and too small ships
                                    && ship.scanClass != "CLASS_NO_DRAW" ) { //exclude for sure
    				//do not add escorts landed on EscortDeck
    				var add = true;
    				if( w.$ED ) {
    					var pad = w.$ED.$EscortDeckShip.indexOf( ship );
    					if( pad != -1 && w.$ED.$EscortDeckShipPos[pad] ) {
    						add = false;
    					}
    				}
    				if( add ) {
    					var l = w.$S.length; //add after the last ship
    					w.$S[l] = [];
    					w.$S[l][0] = ship;
                                            w.$T[l] = [];
    				}
    			}
    		}
    	}
    	if( w.$Red ) return; //No trails in red alert below 50 FPS if weapons are online
    
    	//reduce the number of flashers if FPS is low
            var newkey = false;
            if( w.$FPS < w.$LowFPS ) {
    		if( w.$Key > 1 ) {
    			w.$Key--;
                            newkey = true;
    //                      player.consoleMessage("FPS:"+w.$FPS+" key:"+w.$Key); //debug
    		}
                    w.$FPS = w.$LowFPS; //do not reduce again until the next measue
    	} else if( w.$FPS > w.$GoodFPS ) { //extend up to maxkey
                    if( w.$Key < w.$MaxKey ) {
                            w.$Key++;
                            newkey = true;
    //                      player.consoleMessage("FPS:"+w.$FPS+" key:"+w.$Key); //debug
    		}
                    w.$FPS = w.$GoodFPS; //do not extend again until the next measue
    	}
    	var key = "trails"+w.$Key;
            if( newkey ) w.$Sec = w.$MaxSec - 2 * w.$MaxKey + 2 * w.$Key;
    
    	//reduce length over 1500 entity to prevent "Universe is full" error at 2047
    	var a = system.allShips.length + system.allVisualEffects.length;
            var r = w.$Length;
    	if( a > 1500 && w.$Length > 10 ) { //too much entity
    		r = Math.ceil( w.$Length/2 ); //reduced length
                    w.$RemoveAllTrails(w); //restart trails
    		w.$Length = r;
    	}
    	
    	var zoom = w.$Zoom;
    	
    	//create new trail elements
    	var dist = w.$Distance;
            var shift = w.$Shift;
            var ma = w.$MaxAngle;
    	for( var i = 1; i < w.$S.length; i++ ) { //start from 0 and player ship will make trail also
    		var t = w.$S[i];
    		if( t ) {
    			var ship = t[0];
    			if( ship && ship.isValid && !ship.isDerelict
    				&& !ship.script.$TrailsDisabled ) {
    				var sp = ship.position;
    				var sc = 1;
    				
                                    if( ship.scanClass == "CLASS_ROCK" ) key = "trails-rock";
                                    else {
                                            key = "trails"+w.$Key;//need if the previous ship was over 12km
    				        if( pp ) { //create less flashers over 12km
    					        var pd = sp.distanceTo( pp ) / 1000;
    					        if( pd > 12 ) {
    						        key = "trails0";
    						        if( zoom ) //increase size in 12-20km for brightness
                                                                sc = 1.5+Math.max(0,Math.min(2,pd/5-2));
    					        } //below the same formula is used again, keep these equal
    				        }
                                    }
    
    				var c = 0; //safety counter to surely exit from the next cycle soon
    				//check distance of the last position where a trail is created
    				//need to do in cycle if travelled more in the last frame than
    				//the length of a visualEffect element in effectdata.plist (20m)
    				//which is happen often with more than 1x TAF
    				var k = t[2];
                                    var bp = sp;//.add(ship.heading.multiply(w.$Distance));//start earlier
                                    var ez = 1; //default for ships without engine like rocks
                                    var e = ship.exhausts; //use engine exhaust z distance from ship center
                                    if( e && e[0] && e[0].position && e[0].position.z )
                                            ez = 1-Math.min(0, e[0].position.z); //ez must >=1 for Gnat
                                    var kp = bp;
                                    if(t[k] && t[k].isValid) {
                                            kp = t[k].position;
                                            if( kp.x > 0.9*shift ) {//moved far away
                                                    kp = Vector3D(kp.x-shift, kp.y, kp.z);
    //                                              if(p.target == ship) log(w.name, ship.name+" far i:"+i); //debug
                                            }
                                    }
                                    var kv = t[k];
    				while( c < 50 && ( !k || !kv || !kv.isValid
    					|| bp.distanceTo(kp) > dist/2+ez )) {
    					c++;
    					if( !w.$S[i][2] ) w.$S[i][2] = 3; //new trail
    					k = w.$S[i][2];
    					if( !k ) k = 3;
    					var a = w.$S[i][k]; //actual (lastly created) visualEffect
    					var pos = bp;
    					var q = ship.orientation; //for the first element
    					if( a && a.isValid ) { //from the second element
                                                    var ap = a.position;
                                                    if( ap.x > 0.9*shift ) //moved far away
                                                            ap = Vector3D(ap.x-shift, ap.y, ap.z);
    						var u = bp.subtract(ap).direction();
    						pos = ap.add(u.multiply(dist));
    						if( a && a.isVisualEffect ) {
    							//facing from the previous trail element
    							q = a.orientation;
    							var z=pos.subtract(ap).direction();
    							var ah = a.heading;
    							var angle = ah.angleTo(z);
    							//set the plane where we should rotate in
    							var cr = ah.cross(z).direction();
    							q = q.rotate( cr, -angle );
    						}
    					}
    //					w.$S[i][1] = pos; //save the new position - unused
    
    					k++; //index of the next item
    					if( k > w.$Length ) k = 3; //restart over $Length
    					w.$S[i][2] = k; //save the new index
    					
    /*                                      //make the end of the trail darker but prevent blue end on gray dust
    					if( ship.scanClass != "CLASS_ROCK" ) {
    					        var pk = k+1; //previous k points to the oldest visualEffect
    					        if( pk > w.$Length ) pk = 3;
    					        var pv = w.$S[i][pk]; //the oldest visualEffect
    					        if( pv && pv.isVisualEffect ) {
    						        var nv = system.addVisualEffect(
    							        "trails-end", pv.position );
    						        if( nv ) {
    							        nv.orientation = pv.orientation;
                                                                    nv.scaleX = nv.scaleY = 0.5; //shrink
    							        pv.remove();
    							        w.$S[i][pk] = nv; //save the new item
                                                            }
    						}
    					}
    */
    					//move or replace old visual item
    					var ov = w.$S[i][k];
                                            if( ov && ov.isVisualEffect && ov.dataKey == key ) {
                                                    ov.position = pos;
                                                    ov.orientation = q;
                                            } else {
                                                    if( ov && ov.isVisualEffect ) {
                                                            ov.remove();
                                                    }
                                                    var v = system.addVisualEffect( key, pos );
                                                    if( v ) {
                                                            v.orientation = q;
                                                            w.$S[i][k] = v; //save the new element
                                                    }
                                            }
    
                                            w.$T[i][k] = w.$Delta; //Timestamp of creation
                                            kv = w.$S[i][k];
                                            if( kv && kv.isValid ) {
                                                    kp = kv.position;
                                                    if( kp.x > 0.9*shift ) //moved far away
                                                            kp = Vector3D(kp.x-shift, kp.y, kp.z);
                                            } else kp = bp;
    				}
    //				if(c > 40) log(w.name, ship.name+" new i:"+i+" c:"+c); //debug
    				//increase flasher size with the distance
    				if( sc > 1 ) for( var j = 3; j <= w.$Length; j++ ) {
    					var v = w.$S[i][j];
    					if( v && v.isValid ) v.scaleX = v.scaleY = sc;
    				}				//but not scaleZ!
    			} else { //ship docked, jumped or destroyed
    				var k = t[2]; //actual item index - old code without timestamp
    				if( k ) {
    					k++;// += 0.5; //slower count for slower remove
    					if( k > w.$Length ) k = 3; //restart over $Length
    					w.$S[i][2] = k; //save the new index
    					if( k == Math.floor(k) ) {
    						if( w.$S[i][k] == -1 ) { //all visualEffect removed
    							//so remove this ship from the tracked array
    							for( var k = 3; k <= w.$Length; k++ ) {
    								var v = t[k]; //remove again for sure
    								if( v && v.isValid ) v.remove();
    							}
    							w.$S[i] = false; 
    						} else {
    							var ov = t[k]; //remove old visual item
    							if( ov && ov.isVisualEffect ) ov.remove();
    							w.$S[i][k] = -1; //flag for remove ship
    						}
    					}
    				}
    			}
    			
                            if( pp && ship && ship.isValid && ship.scanClass != "CLASS_ROCK" ) {
                                    var max = 1;
                                    var sd = ship.position.distanceTo( pp );
                                    if( !zoom ) {
                                            if( ship.mass < 20000 ) max = 0.2;
                                            else if( ship.mass < 30000 ) max = 0.4;
                                            else if( ship.mass < 130000 ) max = 0.8;
                                    } else {
                                            if( sd > 12000 ) //zoom in 12-20km, same formula as above
                                                    max = 1.5+Math.max(0,Math.min(2,sd/5000-2));
                                            else if( ship.mass < 20000 ) max = 0.4;
                                            else if( ship.mass < 30000 ) max = 0.8;
                                    }
    //                              var c = 0;
                                    for( var j = 3; j <= w.$Length; j++ ) {
                                            var v = w.$S[i][j];
                                            if( v && v.isValid && w.$T[i][j] ) {
    //                                              c++;
                                                    if( w.$Delta > w.$T[i][j] + w.$Sec ) { //timeout
                                                            v.remove();
                                                            w.$S[i][j] = null;
                                                            w.$T[i][j] = null;
                                                    } else {
                                                            var vp = v.position;
                                                            var ph = p.heading;
                                                            switch( p.viewDirection ) {
                                                                case "VIEW_AFT":
                                                                        ph = ph.multiply(-1);
                                                                        break;
                                                                case "VIEW_PORT":
                                                                        ph = p.orientation.vectorRight()
                                                                                .multiply(-1);
                                                                        break;
                                                                case "VIEW_STARBOARD":
                                                                        ph = p.orientation.vectorRight();
                                                                        break;
                                                            }
    
    /*                                                      //reduce flashers if far or near parallel - cause jams
                                                            var key2 = key;
                                                            if( w.$Key == 1 ) {
                                                                    var an = vp.subtract(pp).angleTo(v.heading);
                                                                    if( vp.distanceTo( pp )>12000 || an<0.3 ) {
                                                                            key2 = "trails0";
                                                                    //improve if not enough paralell
                                                                    } else key2 = "trails1";
                                                                    if(p.target==ship) log(w.name, //debug
                                                                        an+" i:"+i+" j:"+j+" x:"+vp.x+" "+key2);
                                                            }*/
                                                            if( w.$Key < 2 && v.dataKey != key ) {
                                                                    //immediately reduce quality in lowest mode
                                                                    z = system.addVisualEffect(key, v.position);
                                                                    if( z ) {
                                                                            z.orientation = v.orientation;
                                                                            v.remove();
                                                                            w.$S[i][j] = z;
                                                                            v = z;
                                                                    }
                                                            }
    
                                                            //time-based trail dissipation
                                                            var s = 4;
                                                            if( !zoom || sd < 12000 )
                                                                    s = 2.01-((w.$Delta-w.$T[i][j])/w.$Sec)*2;
                                                            //s must be always greater than 0 for scale!
                                                            v.scaleX = v.scaleY = Math.min(max, s);//shrink
    
                                                            //performance boost:
                                                            //move far away if not in sight or over 15km
                                                            if( vp.x < 0.9*shift ) {
                                                                    var ang = ph.angleTo(vp.subtract(pp));
                                                                    if( ang >= ma || !zoom && sd > 15000 ) {
                                                                            v.position = Vector3D(
                                                                            vp.x + shift, vp.y, vp.z );
                                                                    }
                                                            } else { //move back if in sight again
                                                                    vp = Vector3D(vp.x-shift, vp.y, vp.z);
                                                                    if( ph.angleTo(vp.subtract(pp)) < ma
                                                                            && ( zoom || sd <= 15000 ) ) {
                                                                            v.position = vp;
                                                                    }
                                                            }
                                                    }
                                            }
                                    }
                                    //if(c > 45) log(w.name, ship.name+" end i:"+i+" c:"+c); //debug
    			}
    		}
    	}
    }
    
    this.$RemoveAllTrails = function(w) {  
            var i, k, t, v;
            for( i = 0; i < w.$S.length; i++ ) {
                    t = w.$S[i];
                    if( t ) {
                            //remove the whole long trail
                            for( k = 3; k <= w.$Length; k++ ) {
                                    v = t[k];
                                    if( v && v.isValid ) v.remove();
                            }
                            w.$S[i][2] = 3;//reset visualEffect index
                    }
            }
    }