Back to Index Page generated: Nov 12, 2024, 11:02:04 PM

Expansion Manual Witchspace Alignment

Content

Manifest

from Expansion Manager's OXP list from Expansion Manifest
Description Requires the player manually align their ship with the witchspace destination before jump can occur. Requires the player manually align their ship with the witchspace destination before jump can occur.
Identifier oolite.oxp.phkb.ManualWitchspaceAlignment oolite.oxp.phkb.ManualWitchspaceAlignment
Title Manual Witchspace Alignment Manual Witchspace Alignment
Category Mechanics Mechanics
Author phkb, Cmdr. Cheyd, Phantor Gorth, Svengali, Submersible, spara phkb, Cmdr. Cheyd, Phantor Gorth, Svengali, Submersible, spara
Version 2.9 2.9
Tags
Required Oolite Version
Maximum Oolite Version
Required Expansions
Optional Expansions
Conflict Expansions
Information URL https://wiki.alioth.net/index.php/Manual_Witchspace_Alignment n/a
Download URL https://wiki.alioth.net/img_auth.php/d/dc/ManualWitchspaceAlignment_2.9.oxz n/a
License CC BY-NC-SA 4.0 CC BY-NC-SA 4.0
File Size n/a
Upload date 1698241981

Documentation

Also read http://wiki.alioth.net/index.php/Manual%20Witchspace%20Alignment

readme.txt

Manual Witchspace Alignment
by Nick Rogers

With thanks to Cmdr. Cheyd, Phantor Gorth, spara, Svengali and Submersible for their code.

Overview
========
This OXP seeks to add some interactivity to the witchspace jumping mechanic by requiring the player to manually align their ship with a jump target. Each possible local system destination will have a different target to align with. The jump countdown will only commence once the ship is aligned (with a margin for error). Additionally, if the target is blocked (occluded) by the sun, a planet, or a station, the countdown will be stopped until the player has a clear flight path to the target.

The jump target will appear on the Advanced Space Compass to assist with alignment. If the ASC is not installed or is damaged, a console message will be displayed every 2 seconds showing alignment instructions (eg "Align: left and up"). These instructions will also be displayed in interstellar space, because the ASC does not function there.

You can still use the normal methods to force a mis-jump.

If the system you are in has gone nova, an emergency alignment protocol will engage, allowing you to enter witchspace without alignment, although some ship damage will likely occur when this takes place.

Jump Target Color Variation
===========================
It is possible to switch the color of the jump target on the HUD, if the Library OXP is installed. You can switch between the default blue, amber/orange, green, pink, purple and white. This can also be controlled through code.

As it is possible for 3rd party OXP's (like HUD's) to adjust the color of the jump target, if the user color preference has been set, a "User Override" value can be set to notify any 3rd party OXP that no color changes should take place.

Code to adjust the color of the jump target:

	if (worldScripts.ManualWitchspaceAlignment) {
		var mwa = worldScripts.ManualWitchspaceAlignment;
		if (mwa._userOverride === false) { // only do the change if the player hasn't told us not to
			mwa._color = 0; // 0 = blue, 1 = amber/orange, 2 = green, 3 = pink, 4 = purple, 5 = white
		}
	}  

Deep Horizon Advanced Navigation Computer
==========================================
The DH ANC is compatible with this OXP. If both are installed, but the ANC hasn't been purchased by the player, then the manual alignment method will be required. If the ANC has been purchased, the ANC's automatic alignment process will take place.

Acknowledgements
================
This work is quite heavily based on Cmdr. Cheyd and Phantor Gorth's "Deep Horizon Advanced Navigation Computer", and they have generously allowed me to include some of that code here. spara also helped by providing some code for checking if a point is behind a stellar body.

License
=======
This work is licensed under the Creative Commons Attribution-Noncommercial-Share Alike 3.0 Unported License. To view a copy of this license, visit http://creativecommons.org/licenses/by-nc-sa/3.0/

Part of this OXP uses code and models from the Deep Horizon Advanced Navigation Computer:
- Navigation frame
- Star textures and shaders
- Code to spawn the navigation frame and star textures, as well as to realign them as the player ship moves.

The DH ANC has has the following licence amendments:

The files that make up "Deep Horizon - Advance Navigation Computer.oxp" shall be referred to in the rest of these terms as the "Work". The Authors are the authors of the Work. Any derivatives from this version of the Work will be referred to as a Derived Work. Historic Authors are all the authors (including the Original Authors unless this is the first version of the Work) that made ancestral versions of the code that go back to the original Work. The Original Authors are those that made the first version of this Work (i.e. Blake Deakins (Cmd. Cheyd), Phantor Gorth (Paul Cooper))

license: Creative Commons Attribution-Noncommercial-ShareAlike 3.0 Unported license (Modified)
This is the Creative Commons Attribution-Noncommercial-ShareAlike 3.0 Unported license as detailed at http://creativecommons.org/licenses/by-nc-sa/3.0/ with the following additional terms and conditions:

1. These terms superseded any conflicting terms as found in the Creative Commons Attribution-Noncommercial-ShareAlike 3.0 Unported license.
2. The Work is provided on an "as is" basis and you possess and or use the Work entirely at your own risk. The Authors or Historic Authors are not liable for any damages or other consequences that arise from possession, use, or misuse of the Work.
3. You will not request support of any of the Historic Authors for issues with the Work. Any support is at the discretion of the Authors.
4. You will not criticise Historic Authors for issues with this Work if this Work is a derived work.
5. Derived Works must use a compatible license and contain these additional license terms and term descriptions that apply to the Release Version. The term Work will then refer to the Derived Work and any derived works of that new Work will become the Derived Work.
6. You will adhere to this license's terms and conditions fully up to the maximum allowed in your legal jurisdiction. Invalidation of any terms or conditions does not invalidate any or all of the remaining terms and conditions.

Version History
===============
2.9
- Fixed some instances where cancelling the hyperspace countdown would prevent the jump marker from reappearing when you restart the jump.
- Tweaked colour of green jump beacon, to be closer to the standard green.
- Tweaked effect data, to enable selected colours to reflect diffuse images better.

2.8 
- Improvement to fix from version 2.7.

2.7
- Better handling of situation where jump countdown is stopped before our start function has run.

2.6
- Making sure jump markers do not have pilots attached.

2.5
- Better integration with sound OXP's (like Halsis).

2.4
- Alignment is now not required if a system has gone nova.

2.3
- Fixed issue when escape pod is activated while visual effects are being displayed.

2.2
- Tweaks to prevent JS code time-out conditions when cancelling the jump countdown due to misalignment.
- Improved integration with BGS, particularly when starting a jump countdown when the ship is already aligned with the destination.
- Improved integration with Countdown to Zero, to prevent timer garbage collection.
- Code cleanup.

2.1
- Added Purple and White color options, and tweaked the colors of the green and pink options (thanks to gsagostinho).
- Added protection from out of bounds errors if color property is incorrectly set.

2.0
- Added Amber/Orange, Green and Pink nav beacon variations, plus code to allow the color of the nav frame to be either selected by the user, or configured via script.
- Fixed issue where selecting another ASC target would leave nav frame and star on the screen.
- Code refactoring.

1.7
- Starting docking computers will now correctly disengage the hyperspace countdown.

1.6
- Jump is now cancelled if player starts a jump and then changes the destination system on the F6 chart before aligning with nav beacon.

1.5
- Bug fixes.

1.4
- Fixed issue where variable was not being set correctly when player ship is replaced.
- Bug fixes.

1.3
- Fixed JS error if Advanced Navigation Computer OXP is not installed.

1.2
- Corrections to manifest.plist file.

1.1
- Fixed issues in interstellar space where visual effects were not being removed if the hyperspace countdown was cancelled.
- Fixed issues in normal space where quickly turning the hyperspace countdown on and off could lead to the visual effects not being removed.

1.0
- Initial release

Equipment

This expansion declares no equipment.

Ships

Name
Jump Marker

Models

This expansion declares no models.

Scripts

Path
Scripts/mwa_conditions.js
"use strict";
this.name        = "ManualWitchspaceAlignment_Conditions";
this.author      = "phkb";
this.copyright   = "2017 phkb";
this.description = "Condition script for equipment.";
this.licence     = "CC BY-NC-SA 3.0";

//-------------------------------------------------------------------------------------------------------------
this.allowAwardEquipment = function(equipment, ship, context) {
	if (context === "scripted") return true;
    return false;
}
Scripts/mwa_main.js
"use strict";
this.name = "ManualWitchspaceAlignment";
this.author = "phkb";
this.copyright = "2017 phkb";
this.description = "Reimplements the DHI AHC but with a manual alignment procedure";
this.licence = "CC BY-NC-SA 3.0";

/*
	TODO:
		work out better occlusion calc for stations
*/

this._markers = []; // array of markers representing jump points to all local systems
this._running = false; // used to work out when the spawn routine is running
this._alignAccuracy = 0.18; // used to control how accurately the player needs to align their ship to the nav beacon
// higher number means less accuracy
this._heldTarget = null;
this._colorList = ["blue", "amber", "green", "pink", "purple", "white"];
this._color = 0;
this._userOverride = false;
this._nova = false;
this._cancelled = false;
this._fcb;

this._libSettings = {
	Name: this.name,
	Display: "UI Settings",
	Alias: "Manual Witchspace Alignment",
	Alive: "_libSettings",
	Bool: {
		B0: {
			Name: "_userOverride",
			Def: false,
			Desc: "User Override"
		},
		Info: "Setting User override to true will prevent other OXP's from changing the UI color."
	},
	SInt: {
		S0: {
			Name: "_color",
			Def: 0,
			Min: 0,
			Max: 5,
			Desc: "Nav Frame Color"
		},
		Info: "0 = Blue, 1 = Amber/Orange, 2 = Green, 3 = Pink, 4 = Purple, 5 = White"
	},
};

this._trueValues = ["yes", "1", 1, "true", true];

//-------------------------------------------------------------------------------------------------------------
this.startUp = function () {
	if (worldScripts.Lib_Config) {
		if (missionVariables.MWA_UserOverride) this._userOverride = (this._trueValues.indexOf(missionVariables.MWA_UserOverride) >= 0 ? true : false);
		if (missionVariables.MWA_Color) this._color = parseInt(missionVariables.MWA_Color);
	}
	// make sure we don't get in the way of the ANC
	if (worldScripts.Deep_Horizon_Adv_Nav_Comp) {
		// turn off ws events in ANC and control them from here
		var anc = worldScripts.Deep_Horizon_Adv_Nav_Comp;
		anc.shipWillEnterWitchspace_hold = anc.shipWillEnterWitchspace;
		delete anc.shipWillEnterWitchspace;
		anc.shipWillExitWitchspace_hold = anc.shipWillExitWitchspace;
		delete anc.shipWillExitWitchspace;
		anc.playerStartedJumpCountdown_hold = anc.playerStartedJumpCountdown;
		delete anc.playerStartedJumpCountdown;
	}
	if (worldScripts.BGS) {
		player.ship.script._BGS = true;
		// we're taking charge of BGS's playerStartedJumpCountdown routine so we can make sure
		// it's only called when we actually start the countdown (ie when aligned);
		worldScripts.BGS.$mwa_playerStartedJumpCountdown = worldScripts.BGS.playerStartedJumpCountdown;
		delete worldScripts.BGS.playerStartedJumpCountdown;
	}
}

//-------------------------------------------------------------------------------------------------------------
this.startUpComplete = function () {
	if (worldScripts.Lib_Config) worldScripts.Lib_Config._registerSet(this._libSettings);
	this.$systemSetup();

	// bug fix to prevent countdown to zero from having its timer garbage collected if the jump starts/stops frequently
	if (worldScripts.countdown_to_zero) {
		worldScripts.countdown_to_zero.playerStartedJumpCountdown = function(type, seconds) {
			if (this.$hyperTimer && this.$hyperTimer.isRunning === true) {
				this.$hyperTimer.stop();
			}
			if (type === "standard") {
				missionVariables["countdown_to_zero"] = seconds;
				this.$hyperTimer = new Timer(this,this._hyperDriveCountdown,0,1);
			}
			else if (type === "galactic") {
				missionVariables["countdown_to_zero"] = seconds - 1;
				this.$hyperTimer = new Timer(this,this._hyperDriveCountdown,1,1);
			}
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.shipWillDockWithStation = function (station) {
	this.$stopNavFrameCallback(true);
}

//-------------------------------------------------------------------------------------------------------------
this.shipWillLaunchFromStation = function () {
	this._running = false;
	var ps = player.ship.script;
	ps.$checkForAlignment = this.$checkForAlignment;
	ps.$shipIsAligned = this.$shipIsAligned;
	ps.$jumpIsOccluded = this.$jumpIsOccluded;
	ps.$entityType = this.$entityType;
	ps.$reorientVEToPlayer = this.$reorientVEToPlayer;
	ps.$vectoredPositionToTarget = this.$vectoredPositionToTarget;
	ps.$lookAtRotate = this.$lookAtRotate;
	ps.$orthoNormalise = this.$orthoNormalise;
	ps.$lookAtRotateEuler = this.$lookAtRotateEuler;
	ps.$displayBasicPointerMessage = this.$displayBasicPointerMessage;
	ps.$positionNavVisualEffects = this.$positionNavVisualEffects;
	ps.$performCancel = this.$performCancel;

	//ps._useCheckCourseFunction = player.ship.hasOwnProperty("checkCourseToPosition") === true ? true : false; //oolite.compareVersion("1.87") <= 0 ? true : false;
	ps._checkingAlignment = false;
	ps._jumpStarted = false;
	ps._BGSStarted = false;
	ps._jumpMarker = null;
	ps._alignCount = 0;
	ps._occludedCount = 0;
	ps._alignWarning = false;
	ps._occludedWarning = false;
	ps._override = false;
	ps._navFrameVE = null;
	ps._navStarVE = null;
	ps._basicCompassFrameCount = 0;
	ps._alignAccuracy = this._alignAccuracy;
	this._cancelled = false;

	var anc = worldScripts.Deep_Horizon_Adv_Nav_Comp;
	ps._usingANC = false;
	if (anc) {
		if (player.ship.equipmentStatus("EQ_ADV_NAV_COMP") === "EQUIPMENT_OK") {
			// turn on anc, turn off mwa
			anc.shipWillEnterWitchspace = anc.shipWillEnterWitchspace_hold;
			anc.shipWillExitWitchspace = anc.shipWillExitWitchspace_hold;
			anc.playerStartedJumpCountdown = anc.playerStartedJumpCountdown_hold;
			ps._usingANC = true;
		} else {
			delete anc.shipWillEnterWitchspace;
			delete anc.shipWillExitWitchspace;
			delete anc.playerStartedJumpCountdown;
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.shipDied = function(whom, why) {
	this.$alternateStopNavFrameCallback(false);
}

//-------------------------------------------------------------------------------------------------------------
this.playerWillSaveGame = function () {
	missionVariables.MWA_UserOverride = this._userOverride;
	missionVariables.MWA_Color = this._color;
}

//-------------------------------------------------------------------------------------------------------------
this.guiScreenChanged = function (to, from) {
	var p = player.ship;
	if (p.isInSpace === false) return;
	if ((to === "GUI_SCREEN_LONG_RANGE_CHART" || to === "GUI_SCREEN_SHORT_RANGE_CHART") && this._heldTarget == null) this._heldTarget = p.nextSystem;
	if ((from === "GUI_SCREEN_LONG_RANGE_CHART" || from === "GUI_SCREEN_SHORT_RANGE_CHART") &&
		(to != "GUI_SCREEN_LONG_RANGE_CHART" && to != "GUI_SCREEN_SHORT_RANGE_CHART") && this._heldTarget != p.nextSystem) {
		// player changed destination system
		if (p.script._navFrameCallbackID && isValidFrameCallback(p.script._navFrameCallbackID)) {
			player.consoleMessage("Hyperspace destination changed - jump cancelled", 5);
			p.cancelHyperspaceCountdown();
			this.$stopNavFrameCallback(false);
			this._cancelled = false;
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.shipLaunchedEscapePod = function(escapePod) {
	this.$alternateStopNavFrameCallback(false);
}

//-------------------------------------------------------------------------------------------------------------
this.shipExitedWitchspace = function () {
	this.$systemSetup();
	player.ship.script._checkingAlignment = false;
	if (this._nova === true) {
		player.ship.takeInternalDamage();
		this._nova = false;
	}
}

//-------------------------------------------------------------------------------------------------------------
this.shipWillEnterWitchspace = function (cause, destination) {
	this._heldTarget = null;
	delete this.compassTargetChanged;
	delete this.playerStartedAutoPilot;
	//delete this.playerCancelledJumpCountdown;
	this._cancelled = false;
	this.$stopNavFrameCallback(false);
}

//-------------------------------------------------------------------------------------------------------------
this.playerStartedJumpCountdown = function (type, seconds) {
	if (this._cancelled == true) {
		// jump was cancelled during a playerStartedJumpCountdown worldscript
		this._cancelled = false;
		return;
	}
	var ps = player.ship.script;
	// check for galactic jump or if the anc is in play, or the system is going nova - don't do anything for these
	if (type === "galactic" || ps._usingANC === true || (system.sun && system.sun.hasGoneNova)) {
		if (ps._BGS) worldScripts.BGS.$mwa_playerStartedJumpCountdown(type, seconds);
		if (system.sun && system.sun.hasGoneNova) {
			this._nova = true;
			player.consoleMessage("System going nova - Emergency alignment engaged");
		}
		return;
	}

	if (!ps._navFrameCallbackID) {
		ps._basicCompass = false;
		if (system.isInterstellarSpace === true || player.ship.hasEquipmentProviding("EQ_ADVANCED_COMPASS") === false) {
			ps._basicCompass = true;
		}
		this.$spawnJBNavVEs();
	} else if (ps._override === false) {
		player.ship.cancelHyperspaceCountdown();
		//this.$stopNavFrameCallback(true);
		player.consoleMessage(expandDescription("[witch-user-abort]"));
	}
}

//-------------------------------------------------------------------------------------------------------------
this.playerStartedAutoPilot_hide = function () {
	player.consoleMessage(expandDescription("[witch-user-abort]"));
	player.ship.cancelHyperspaceCountdown();
	this.$stopNavFrameCallback(true);
	this._cancelled = false;
}

//-------------------------------------------------------------------------------------------------------------
this.compassTargetChanged_hide = function (whom, mode) {
	player.consoleMessage(expandDescription("[witch-user-abort]"));
	delete player.ship.script._oldTarget;
	this.$stopNavFrameCallback(true);
	player.ship.cancelHyperspaceCountdown();
	this._cancelled = false;
}

//-------------------------------------------------------------------------------------------------------------
this.playerCancelledJumpCountdown = function () {
	var ps = player.ship.script;
	// set cancelled = true if we got here before the playerStartedJumpCountdown function was executed
	// usually because some other OXP cancelled the jump in it's own playerStartedJumpCountdown function.
	if (ps._jumpMarker == null) this._cancelled = true;
	ps._BGSStarted = false;
	if (ps._override === true) return;
	if (ps._usingANC === true) return;
	this.$stopNavFrameCallback(true);
}

//-------------------------------------------------------------------------------------------------------------
this.playerJumpFailed = function (reason) {
	this.$stopNavFrameCallback(true);
}

//-------------------------------------------------------------------------------------------------------------
this.$spawnJBNavVEs = function $spawnJBNavVEs() {
	if (this._running === true) return;
	this._running = true;
	var p = player.ship;
	var ps = p.script;
	// Select marker that matches the destination system.
	var playerTarget = p.nextSystem;
	for (var i = 0; i < this._markers.length; i++) {
		if (playerTarget === this._markers[i].systemID) {
			ps._jumpMarker = this._markers[i];
			break;
		}
	}
	if (ps._jumpMarker == null) {
		log(this.name, "!!ERROR: marker not found for " + System.systemNameForID(playerTarget) + "!");
		this._running = false;
		return;
	}
	player.consoleMessage(System.systemNameForID(playerTarget) + " system beacon acquired.");
	ps._jumpMarker.beaconCode = "witch-destination-icon";
	ps._jumpMarker.beaconLabel = "Hyperspace destination: " + ps._jumpMarker.name;

	ps._jumpStarted = true;
	// start the framecount at its recycle point so an alignment check will happen next frame
	ps._frameCount = 0.4;

	// Spawn Visual Effect and store reference to it
	if (this._color > 5 || this._color < 0) this._color = 0; // default to blue if incorrectly set
	ps._navFrameVE = system.addVisualEffect("jumpbeacon_navframe_" + this._colorList[this._color], ps.$vectoredPositionToTarget(ps._jumpMarker.position, p.collisionRadius + 5000));
	ps._navStarVE = system.addVisualEffect("jumpbeacon_navstar", ps.$vectoredPositionToTarget(ps._jumpMarker.position, p.collisionRadius + 50000));
	// Orient the VE toward the player ship
	ps._navFrameVE.scale(0.66);
	ps._navStarVE.scale(3.30 - ps._jumpMarker.distanceToSystem * 0.27);
	var tex = ps._jumpMarker.systemID % 4;
	switch (tex) {
		case 1:
			ps._navStarVE.setMaterials({
				"jumpbeacon_navstar.png": {
					"textures": ["jumpbeacon_navstar2.png"],
					"fragment_shader": "jumpbeacon_jumpstar.fragment",
					"emission_map": "jumpbeacon_navstar2.png",
					"uniforms": {
						"uColorMap": {
							"type": "texture",
							"value": "0"
						},
						"uSpecIntensity": "shaderFloat1",
						"uSpecColor": "shaderVector1"
					},
					"vertex_shader": "jumpbeacon_jumpstar.vertex"
				}
			});
			break;
		case 2:
			ps._navStarVE.setMaterials({
				"jumpbeacon_navstar.png": {
					"textures": ["jumpbeacon_navstar3.png"],
					"fragment_shader": "jumpbeacon_jumpstar.fragment",
					"emission_map": "jumpbeacon_navstar3.png",
					"uniforms": {
						"uColorMap": {
							"type": "texture",
							"value": "0"
						},
						"uSpecIntensity": "shaderFloat1",
						"uSpecColor": "shaderVector1"
					},
					"vertex_shader": "jumpbeacon_jumpstar.vertex"
				}
			});
			break;
		case 3:
			ps._navStarVE.setMaterials({
				"jumpbeacon_navstar.png": {
					"textures": ["jumpbeacon_navstar4.png"],
					"fragment_shader": "jumpbeacon_jumpstar.fragment",
					"emission_map": "jumpbeacon_navstar4.png",
					"uniforms": {
						"uColorMap": {
							"type": "texture",
							"value": "0"
						},
						"uSpecIntensity": "shaderFloat1",
						"uSpecColor": "shaderVector1"
					},
					"vertex_shader": "jumpbeacon_jumpstar.vertex"
				}
			});
			break;
	}
	ps._navFrameCallbackID = addFrameCallback(ps.$positionNavVisualEffects.bind(ps));
	worldScripts.ManualWitchspaceAlignment._fcb = ps._navFrameCallbackID;
	this._running = false;
}

//-------------------------------------------------------------------------------------------------------------
this.$stopNavFrameCallback = function $stopNavFrameCallback(cancelled) {
	var p = player.ship;
	var ps = p.script;
	if (ps._navFrameCallbackID && isValidFrameCallback(ps._navFrameCallbackID)) {
		removeFrameCallback(ps._navFrameCallbackID);
	}
	delete ps._navFrameCallbackID;
	delete this.compassTargetChanged;
	delete this.playerStartedAutoPilot;
	//delete this.playerCancelledJumpCountdown;
	if (ps._jumpMarker) {
		ps._jumpMarker.beaconCode = "";
		ps._jumpMarker.beaconLabel = "";
		ps._jumpMarker = null;
		if (ps._oldTarget && ps._oldTarget.isValid) {
			p.compassTarget = ps._oldTarget;
		}
		delete ps._oldTarget;
	}
	if (ps._navFrameVE != null) {
		ps._navFrameVE.remove();
		ps._navFrameVE = null;
	}
	if (ps._navStarVE != null) {
		ps._navStarVE.remove();
		ps._navStarVE = null;
	}
	ps._jumpStarted = false;
	ps._occludedCount = 0;
	ps._occludedWarning = false;
	ps._alignWarning = false;
	ps._alignCount = 0;
	ps._override = false;
	ps._lastHeading = null;
	if (ps._BGS && ps._usingANC === false && cancelled === true) {
		ps._BGSStarted = false;
		worldScripts.BGS.playerCancelledJumpCountdown();
		worldScripts.BGS._clrTimer(1);
	}
	this._running = false;
}

//-------------------------------------------------------------------------------------------------------------
this.$alternateStopNavFrameCallback = function $alternateStopNavFrameCallback() {
	if (this.fcb && isValidFrameCallback(this._fcb)) {
		removeFrameCallback(this._fcb);
	}
	var ve = system.allVisualEffects;
	for (var i = ve.count; i >= 0; i--) {
		if (ve[i].dataKey.indexOf("jumpbeacon") >= 0) {
			ve[i].remove();
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$systemSetup = function $systemSetup() {
	this._markers.length = 0;
	// Determine how many farpoint markers are needed
	this._localSystems = System.infoForSystem(galaxyNumber, system.ID).systemsInRange(7);
	this._spawnCount = this._localSystems.length;
	// For each in-range system spawn a farpoint marker
	this._markers = system.addShips("jump_marker", this._spawnCount, player.ship.position, 5000000);
	//Assign a matching system.ID as an additional value to each buoy
	for (var i = 0; i < this._markers.length; i++) {
		var mkr = this._markers[i];
		mkr.systemID = this._localSystems[i].systemID;
		mkr.galCoordinates = this._localSystems[i].coordinates;
		mkr.name = this._localSystems[i].name;
		mkr.distanceToSystem = System.infoForSystem(galaxyNumber, system.ID).distanceToSystem(this._localSystems[i]);
		mkr.uSpecColor = this.$selectColor(this._localSystems[i].systemID, i);
		mkr.uSpecIntensity = 0.5;
		mkr.position = this.$positionJumpBeaconMarker(mkr, 100000000);
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$positionJumpBeaconMarker = function $positionJumpBeaconMarker(marker, distance) {
	if (marker.galCoordinates == null) return null;
	var t = marker.galCoordinates;
	var s = System.infoForSystem(galaxyNumber, system.ID).coordinates;

	var tV = t.subtract(s);

	var u = system.scrambledPseudoRandomNumber(34567346);
	var v = system.scrambledPseudoRandomNumber(431976567);
	var w = system.scrambledPseudoRandomNumber(9834674);

	var theta = Math.acos(2 * u - 1);
	var phi = 2 * Math.PI * v;
	var psi = 2 * Math.PI * w;

	var v1 = new Vector3D(Math.sin(theta) * Math.sin(phi), Math.sin(theta) * Math.cos(phi), Math.cos(theta));
	var v2 = new Vector3D(0, 0, 1); //straight up
	var v3 = v2.cross(v1);
	v3 = v3.direction(); // normalize (should be normalized anyway)

	var q1 = new Quaternion(1, 0, 0, 0);
	q1 = q1.rotate(v3, theta).normalize();
	var v4 = tV.rotateBy(q1).direction(); // rotate target vector tV so that the coordinate space has pole moved from straight up to v1 (theta, phi) (spherical coords)
	var q2 = new Quaternion(1, 0, 0, 0);
	q2 = q2.rotate(v1, psi).normalize();
	v4 = v4.rotateBy(q2).direction(); // rotate about the new pole v1 by psi

	return v4.multiply(distance).add(player.ship.position);
}

//-------------------------------------------------------------------------------------------------------------
this.$selectColor = function $selectColor(sysID, i) {
	var color = System.infoForSystem(galaxyNumber, sysID).sun_color;
	if (typeof (color) === 'undefined') {
		var c = Math.floor(system.scrambledPseudoRandomNumber(785331 - i) * 7);
		switch (c) {
			case 0:
				color = "magentaColor";
				break;
			case 1:
				color = "redColor";
				break;
			case 2:
				color = "orangeColor";
				break;
			case 3:
				color = "yellowColor";
				break;
			case 4:
				color = "whiteColor";
				break;
			case 5:
				color = "cyanColor";
				break;
			default:
				color = "blueColor";
				break;
		}
	}
	switch (color) {
		case "magentaColor":
			var colorVec = [1, 0, 1];
			break;
		case "redColor":
			var colorVec = [1, 0, 0];
			break;
		case "orangeColor":
			var colorVec = [1, 0.5, 0];
			break;
		case "yellowColor":
			var colorVec = [1, 1, 0];
			break;
		case "whiteColor":
			var colorVec = [1, 1, 1];
			break;
		case "cyanColor":
			var colorVec = [0, 1, 1];
			break;
		case "blueColor":
			var colorVec = [0, 0, 1];
			break;
		default:
			var colorVec = [1, 1, 1];
	}
	return colorVec;
}

//-------------------------------------------------------------------------------------------------------------
// all the following functions are attached to player.ship.script to improve performance

//-------------------------------------------------------------------------------------------------------------
// main frame callback routine
this.$positionNavVisualEffects = function $positionNavVisualEffects(delta) {
	if (delta === 0) return;
	var p = this.ship;
	// make sure we have a valid ship to work with
	if (!p || !p.position || !p.vectorForward) {
		worldScripts.ManualWitchspaceAlignment.$alternateStopNavFrameCallback(false);
		return;
	}
	var ps = this;
	// make sure we have valid entities to work with, otherwise cancel the fcb
	if (ps._navFrameVE.isValid === false || ps._navStarVE.isValid === false) {
		if (ps._navFrameCallbackID && isValidFrameCallback(ps._navFrameCallbackID)) {
			removeFrameCallback(ps._navFrameCallbackID);
		}
		delete ps._navFrameCallbackID;
		return;
	}
	ps.$reorientVEToPlayer(ps._navFrameVE);
	ps.$reorientVEToPlayer(ps._navStarVE);
	ps._navFrameVE.position = ps.$vectoredPositionToTarget(ps._jumpMarker.position, p.collisionRadius + 5000);
	ps._navStarVE.position = ps.$vectoredPositionToTarget(ps._jumpMarker.position, p.collisionRadius + 10000);
	ps._navStarVE.shaderVector1 = ps._jumpMarker.uSpecColor;
	ps._navStarVE.shaderFloat1 = ps._jumpMarker.uSpecIntensity;
	ps._frameCount += delta;
	if (ps._frameCount > 0.5) {
		ps._frameCount = 0;
		ps.$checkForAlignment();
	}
	// for non-compass directions, we need to give a console message pointer to the beacon
	if (ps._basicCompass === true) {
		ps._basicCompassFrameCount += delta;
		if (ps._basicCompassFrameCount > 2) {
			ps._basicCompassFrameCount = 0;
			ps.$displayBasicPointerMessage();
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$checkForAlignment = function $checkForAlignment() {
	//var startDate = new Date();
	// make sure we don't run over ourselves
	var ps = this;
	if (ps._checkingAlignment === true) return;
	ps._checkingAlignment = true;
	var p = this.ship;
	if (p.maxPitch - Math.abs(p.pitch) < 0.0001) {
		ps._checkingAlignment = false;
		return;
	}
	var ws = worldScripts.ManualWitchspaceAlignment;
	// set the compass to the nav jump marker, and kick in our functions to monitor when the 
	// compass target changes, and when the player cancels the jump
	if (ps._basicCompass === false && ps._jumpMarker && p.compassTarget != ps._jumpMarker) {
		ps._oldTarget = p.compassTarget;
		p.compassTarget = ps._jumpMarker;
		ws.compassTargetChanged = ws.compassTargetChanged_hide;
		ws.playerStartedAutoPilot = ws.playerStartedAutoPilot_hide;
		//ws.playerCancelledJumpCountdown = ws.playerCancelledJumpCountdown_hide;
	}
	if (ps.$shipIsAligned() === true) {
		ps._alignWarning = false;
		ps._lastHeading = p.heading;
		if (ps._jumpStarted === false) {
			ps._jumpStarted = true;
			if (ps._cancelTimer && ps._cancelTimer.isRunning) ps._cancelTimer.stop();
			ps._override = true;
			p.beginHyperspaceCountdown(p.hyperspaceSpinTime);
			//ws.playerCancelledJumpCountdown = ws.playerCancelledJumpCountdown_hide;
			ps._override = false;
		}
		if (ps._BGS && ps._BGSStarted === false) {
			ps._BGSStarted = true;
			worldScripts.BGS.$mwa_playerStartedJumpCountdown("standard", p.hyperspaceSpinTime);
		}
	} else {
		if (ps._jumpStarted === true) {
			ps._jumpStarted = false;
			//delete ws.playerCancelledJumpCountdown;
			// do the actual cancellation through a separate timer, rather than the framecallback
			// sometimes getting timeout errors when run through the fcb
			if (!ps._cancelTimer || !ps._cancelTimer.isRunning) {
				ps._cancelTimer = new Timer(ps, ps.$performCancel, 0.25, 0);
			}
			//p.cancelHyperspaceCountdown();
			/*if (ps._BGS) {
				worldScripts.BGS.playerCancelledJumpCountdown();
				worldScripts.BGS._clrTimer(1);
			}*/
		}
		ps._alignCount += 1;
		if (ps._alignCount >= 15 && ps._occludedWarning === false) ps._alignWarning = false;
		if (ps._alignWarning === false) {
			ps._alignWarning = true;
			ps._alignCount = 0;
			player.consoleMessage(expandDescription("[witch-unaligned]"), 4);
		}
	}
	ps._checkingAlignment = false;
	//log(this.name, "checkForAlignment complete in ms: " + (new Date().getTime() - startDate.getTime()));
}

//-------------------------------------------------------------------------------------------------------------
this.$performCancel = function $performCancel() {
	var p = player.ship;
	p.script._override = true;
	p.cancelHyperspaceCountdown();
	p.script._override = false;
}

//-------------------------------------------------------------------------------------------------------------
this.$vectoredPositionToTarget = function $vectoredPositionToTarget(targetPosVector, distance) {
	if (!this.ship || !this.ship.position) return null;
	var v = targetPosVector.subtract(this.ship.position).direction();
	v = v.multiply(distance);
	v = v.add(this.ship.position);
	return v;
}

//-------------------------------------------------------------------------------------------------------------
this.$reorientVEToPlayer = function $reorientVEToPlayer(navVE) {
	if (!this.ship || !this.ship.position) return;
	var orient = this.$lookAtRotate(this.ship.position.subtract(navVE.position), this.ship.orientation.vectorUp());
	if (orient === null) orient = this.$lookAtRotate(this.ship.position.subtract(navVE.position), this.ship.orientation.vectorForward().multiply(-1));
	navVE.orientation = orient;
}

//-------------------------------------------------------------------------------------------------------------
this.$lookAtRotate = function $lookAtRotate(forward, up) {
	//returns an orientation quaternion given a Forward vector and an Up vector (the Forward and Up do not need to be
	//orthonormal but they can not be parallel or anti-parallel.)
	if (forward.direction().cross(up.direction()).magnitude() < 0.01) return null; // Return null if Forward and Up are parallel or anti-parallel or nearly so.
	var f = forward.direction();
	var u = this.$orthoNormalise(f, up);

	var v = new Vector3D(0, 1, 0); //Uses Y axis at the basis axis for theta and the Z axis is used to measure phi from for finding the Euler angles.
	var u2 = this.$orthoNormalise(f, v);

	var sign = -u2.cross(u).dot(f);
	var sign = sign && sign / Math.abs(sign);

	var psi = sign * u2.angleTo(u);

	var z = new Vector3D(0, 0, 1);
	var h = new Vector3D(f.x, 0, f.z);

	sign = -z.cross(h).y;
	sign = sign && sign / Math.abs(sign);

	var phi = sign * z.angleTo(h);

	sign = f.y;
	sign = sign && sign / Math.abs(sign);

	var theta = (Math.PI / 2) - (sign * h.angleTo(f));
	return this.$lookAtRotateEuler(theta, phi, psi);
}

//-------------------------------------------------------------------------------------------------------------
this.$orthoNormalise = function $orthoNormalise(a, b) {
	//Returns a normalised vector that is in the plane of "ab" and is at 90� to "a" in the same half plane as "b".
	var a2 = a.direction();
	var b2 = b.direction();
	var c = a.cross(b).cross(a);
	return c.direction();
}

//-------------------------------------------------------------------------------------------------------------
this.$lookAtRotateEuler = function $lookAtRotateEuler(theta, phi, psi) {
	//Returns a Quaternion that matches a rotational transformation that is described by the Euler angles.
	//Uses Y axis at the basis axis for theta and the Z axis is used to measure phi from.
	var q = new Quaternion(1, 0, 0, 0);
	var theta2 = (Math.PI / 2) - theta;
	q = q.rotateZ(psi);
	q = q.rotateX(theta2);
	q = q.rotateY(phi);
	return q;
}

//-------------------------------------------------------------------------------------------------------------
this.$shipIsAligned = function $shipIsAligned() {
	//var startDate = new Date();
	var p = this.ship;
	var target = this._jumpMarker;
	if (!target) return false;
	var deviation = p.vectorForward.angleTo(target.position.subtract(p.position));
	if (deviation < this._alignAccuracy) {
		// check for occlusion
		/*if (p.script._useCheckCourseFunction === true) {
			var ent = p.checkCourseToPosition(target.position);
			if (ent && (ent.isPlanet || ent.isSun || ent.isStation)) {
				this._occludedCount += 1;
				if (this._occludedCount >= 15 && this._occludedWarning === true) this._occludedWarning = false;
				if (this._occludedWarning === false) {
					this._occludedCount = 0;
					player.consoleMessage(expandDescription("[witch-occluded]", {
						entity: this.$entityType(ent)
					}), 4);
					this._occludedWarning = true;
				}
				return false;
			}
		} else {*/
			var entities = [].concat(system.planets).concat(system.sun).concat(system.stations);
			var occluded = false;
			if (entities.length > 0) {
				for (var i = 0; i < entities.length; i++) {
					if (entities[i] && this.$jumpIsOccluded(entities[i], target) === true) {
						this._occludedCount += 1;
						if (this._occludedCount >= 15 && this._occludedWarning === true) this._occludedWarning = false;
						if (this._occludedWarning === false) {
							this._occludedCount = 0;
							player.consoleMessage(expandDescription("[witch-occluded]", {
								entity: this.$entityType(entities[i])
							}), 4);
							this._occludedWarning = true;
						}
						//log(this.name, "align check complete in ms: " + (new Date().getTime() - startDate.getTime()));
						return false;
					}
				}
			}
		//}
		//log(this.name, "align check complete in ms: " + (new Date().getTime() - startDate.getTime()));
		return true;
	} else {
		//log(this.name, "align check complete in ms: " + (new Date().getTime() - startDate.getTime()));
		return false;
	}
}

//-------------------------------------------------------------------------------------------------------------
// with thanks to spara for the calculation
this.$jumpIsOccluded_alt = function $jumpIsOccluded_alt(bodyEntity, targetEntity) {
	//if (this._jumpStarted === true && this._lastHeading && this.ship.heading.dot(this._lastHeading) > 0.99) return false;
	var vStellarBody = bodyEntity.position;
	var rStellarBody = bodyEntity.radius;
	// i'm reusing this routine for stations, but it's not ideal for non-spherical entities
	if (bodyEntity.isStation) {
		var box = bodyEntity.boundingBox;
		rStellarBody = (box.x > box.y && box.x > box.z ? box.x :
			(box.y > box.x && box.y > box.z ? box.y : box.z)) / 2;
	}
	if (isNaN(rStellarBody) === true) return false;
	var vTarget = targetEntity.position;
	var vPlayerShip = player.ship;

	var vPlayerToStellar = vStellarBody.subtract(vPlayerShip);
	var vPlayerToTarget = vTarget.subtract(vPlayerShip);

	var dPlayerToStellar = vPlayerToStellar.magnitude();
	var dPlayerToTarget = vPlayerToTarget.magnitude();

	if (dPlayerToStellar < dPlayerToTarget) {
		var aStellar = Math.asin(rStellarBody / dPlayerToStellar);
		var aTarget = vPlayerToStellar.angleTo(vPlayerToTarget);
		if (aStellar > aTarget) {
			return true;
		} else {
			return false;
		}
	}
}

//-------------------------------------------------------------------------------------------------------------
// with thanks to spara for the calculation
this.$jumpIsOccluded = function $jumpIsOccluded(bodyEntity, targetEntity) {
	//if (this._jumpStarted === true && this._lastHeading && this.ship.heading.dot(this._lastHeading) > 0.99) return false;
	//var startDate = new Date();
	//var vStellarBody = bodyEntity.position;
	var rStellarBody = bodyEntity.radius;
	// i'm reusing this routine for stations, but it's not ideal for non-spherical entities (I think)
	if (bodyEntity.isStation) {
		var box = bodyEntity.boundingBox;
		rStellarBody = (box.x > box.y && box.x > box.z ? box.x :
			(box.y > box.x && box.y > box.z ? box.y : box.z)) / 2;
	}
	if (isNaN(rStellarBody) === true) return false;
	var vTarget = targetEntity.position;
	var vPlayerShip = this.ship.position;

	var vPlayerToTarget = vTarget.subtract(vPlayerShip);

	var dPlayerToStellar = vPlayerShip.distanceTo(bodyEntity);
	var dPlayerToTarget = vPlayerToTarget.magnitude();

	if (dPlayerToStellar < dPlayerToTarget) {
		var checkVal = Math.sqrt(Math.pow(dPlayerToStellar, 2) - Math.pow(rStellarBody, 2));
		if (isNaN(checkVal) === false) {
			var vTest = vPlayerShip.add(vPlayerToTarget.direction().multiply(checkVal));
			var dTest = vTest.distanceTo(bodyEntity);
			if (rStellarBody > dTest) {
				//log(this.name, "occlusion check complete in ms: " + (new Date().getTime() - startDate.getTime()));
				return true;
			} else {
				//log(this.name, "occlusion check complete in ms: " + (new Date().getTime() - startDate.getTime()));
				return false;
			}
		} else {
			return true;
		}
	} else {
		//log(this.name, "occlusion check complete in ms: " + (new Date().getTime() - startDate.getTime()));
		return false;
	}
}

//-------------------------------------------------------------------------------------------------------------
this.$entityType = function $entityType(entity) {
	if (entity.isPlanet) return "planet";
	if (entity.isSun) return "sun";
	return "station";
}

//-------------------------------------------------------------------------------------------------------------
this.$displayBasicPointerMessage = function $displayBasicPointerMessage() {
	var p = this.ship;
	var target = this._jumpMarker;
	var output = "";
	var f_dev = p.vectorForward.angleTo(target.position.subtract(p));
	var r_dev = p.vectorRight.angleTo(target.position.subtract(p));
	var u_dev = p.vectorUp.angleTo(target.position.subtract(p));
	var s = "";
	// > 1.56 means opposite side (3.12 exact opposite)
	// so, f_dev < alignAccuracy -- aligned
	if (f_dev < this._alignAccuracy) s = "X";
	if (f_dev >= this._alignAccuracy) {
		if (r_dev > 1.69) s += "left ";
		if (u_dev < 1.45) {
			s += (s === "" ? "" : "and ") + "up ";
		} else if (u_dev > 1.69) {
			s += (s === "" ? "" : "and ") + "down ";
		}
		if (r_dev < 1.45) s += (s === "" ? "" : "and ") + "right ";
		if (f_dev > 1.57) s += " (aft)";
	}
	if (s !== "X") {
		output += "Align: " + s;
		player.consoleMessage(output, 2);
	}
}