Made the pretty lines slightly prettier.
Made the pretty lines slightly prettier.

file:b/.gitattributes (new)
--- /dev/null
+++ b/.gitattributes
@@ -1,1 +1,13 @@
+* text=auto
+* eol=lf
 
+# These files are text and should be normalized (convert crlf => lf)
+*.cs      text diff=csharp
+*.cfg     text
+*.csproj  text
+*.sln     text
+
+# Images should be treated as binary
+# (binary is a macro for -text -diff)
+*.png     binary
+

--- /dev/null
+++ b/ARConfiguration.cs
@@ -1,1 +1,282 @@
-
+// AntennaRange © 2014 toadicus
+//
+// This work is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License. To view a
+// copy of this license, visit http://creativecommons.org/licenses/by-nc-sa/3.0/
+
+using KSP;
+using System;
+using ToadicusTools;
+using UnityEngine;
+
+namespace AntennaRange
+{
+	[KSPAddon(KSPAddon.Startup.SpaceCentre, false)]
+	public class ARConfiguration : MonoBehaviour
+	{
+		public static bool RequireLineOfSight
+		{
+			get;
+			private set;
+		}
+
+		public static double RadiusRatio
+		{
+			get;
+			private set;
+		}
+
+		public static bool RequireConnectionForControl
+		{
+			get;
+			private set;
+		}
+
+		public static bool FixedPowerCost
+		{
+			get;
+			private set;
+		}
+
+		public static bool PrettyLines
+		{
+			get;
+			private set;
+		}
+
+		private bool showConfigWindow;
+		private Rect configWindowPos;
+
+		private IButton toolbarButton;
+		private ApplicationLauncherButton appLauncherButton;
+
+		private System.Version runningVersion;
+
+		private KSP.IO.PluginConfiguration _config;
+		private KSP.IO.PluginConfiguration config
+		{
+			get
+			{
+				if (this._config == null)
+				{
+					this._config = KSP.IO.PluginConfiguration.CreateForType<AntennaRelay>();
+				}
+
+				return this._config;
+			}
+		}
+
+		public void Awake()
+		{
+			Tools.PostDebugMessage(this, "Waking up.");
+
+			this.runningVersion = this.GetType().Assembly.GetName().Version;
+
+			this.showConfigWindow = false;
+			this.configWindowPos = new Rect(Screen.width / 4, Screen.height / 2, 180, 15);
+
+
+			this.configWindowPos = this.LoadConfigValue("configWindowPos", this.configWindowPos);
+
+			ARConfiguration.RequireLineOfSight = this.LoadConfigValue("requireLineOfSight", false);
+
+			ARConfiguration.RadiusRatio = (1 - this.LoadConfigValue("graceRatio", .05d));
+			ARConfiguration.RadiusRatio *= ARConfiguration.RadiusRatio;
+
+			ARConfiguration.RequireConnectionForControl =
+				this.LoadConfigValue("requireConnectionForControl", false);
+
+			ARConfiguration.FixedPowerCost = this.LoadConfigValue("fixedPowerCost", false);
+
+			ARConfiguration.PrettyLines = this.LoadConfigValue("drawPrettyLines", true);
+
+			GameEvents.onGameSceneLoadRequested.Add(this.onSceneChangeRequested);
+
+			Debug.Log(string.Format("{0} v{1} - ARConfiguration loaded!", this.GetType().Name, this.runningVersion));
+
+			Tools.PostDebugMessage(this, "Awake.");
+		}
+
+		public void OnGUI()
+		{
+			// Only runs once, if the Toolbar is available.
+			if (ToolbarManager.ToolbarAvailable)
+			{
+				if (this.toolbarButton == null)
+				{
+					Tools.PostDebugMessage(this, "Toolbar available; initializing toolbar button.");
+
+					this.toolbarButton = ToolbarManager.Instance.add("AntennaRange", "ARConfiguration");
+					this.toolbarButton.Visibility = new GameScenesVisibility(GameScenes.SPACECENTER);
+					this.toolbarButton.Text = "AR";
+					this.toolbarButton.TexturePath = "AntennaRange/Textures/toolbarIcon";
+					this.toolbarButton.TextColor = (Color)XKCDColors.Amethyst;
+					this.toolbarButton.OnClick += delegate(ClickEvent e)
+					{
+						this.toggleConfigWindow();
+					};
+				}
+			}
+			else if (this.appLauncherButton == null && ApplicationLauncher.Ready)
+			{
+				Tools.PostDebugMessage(this, "Toolbar available; initializing AppLauncher button.");
+
+				this.appLauncherButton = ApplicationLauncher.Instance.AddModApplication(
+					this.toggleConfigWindow,
+					this.toggleConfigWindow,
+					ApplicationLauncher.AppScenes.SPACECENTER,
+					GameDatabase.Instance.GetTexture(
+						"AntennaRange/Textures/appLauncherIcon",
+						false
+					)
+				);
+			}
+
+			if (this.showConfigWindow)
+			{
+				Rect configPos = GUILayout.Window(354163056,
+					this.configWindowPos,
+					this.ConfigWindow,
+					string.Format("AntennaRange {0}.{1}", this.runningVersion.Major, this.runningVersion.Minor),
+					GUILayout.ExpandHeight(true),
+					GUILayout.ExpandWidth(true)
+				);
+
+				configPos = Tools.ClampRectToScreen(configPos, 20);
+
+				if (configPos != this.configWindowPos)
+				{
+					this.configWindowPos = configPos;
+					this.SaveConfigValue("configWindowPos", this.configWindowPos);
+				}
+			}
+		}
+
+		public void ConfigWindow(int _)
+		{
+			GUILayout.BeginVertical(GUILayout.ExpandHeight(true));
+
+			GUILayout.BeginHorizontal(GUILayout.ExpandWidth(true));
+
+			bool requireLineOfSight = GUITools.Toggle(ARConfiguration.RequireLineOfSight, "Require Line of Sight");
+			if (requireLineOfSight != ARConfiguration.RequireLineOfSight)
+			{
+				ARConfiguration.RequireLineOfSight = requireLineOfSight;
+				this.SaveConfigValue("requireLineOfSight", requireLineOfSight);
+			}
+
+			GUILayout.EndHorizontal();
+
+			GUILayout.BeginHorizontal(GUILayout.ExpandWidth(true));
+
+			bool requireConnectionForControl =
+				GUITools.Toggle(
+					ARConfiguration.RequireConnectionForControl,
+					"Require Connection for Probe Control"
+				);
+			if (requireConnectionForControl != ARConfiguration.RequireConnectionForControl)
+			{
+				ARConfiguration.RequireConnectionForControl = requireConnectionForControl;
+				this.SaveConfigValue("requireConnectionForControl", requireConnectionForControl);
+			}
+
+			GUILayout.EndHorizontal();
+
+			GUILayout.BeginHorizontal();
+
+			bool fixedPowerCost = GUITools.Toggle(ARConfiguration.FixedPowerCost, "Use Fixed Power Cost");
+			if (fixedPowerCost != ARConfiguration.FixedPowerCost)
+			{
+				ARConfiguration.FixedPowerCost = fixedPowerCost;
+				this.SaveConfigValue("fixedPowerCost", fixedPowerCost);
+			}
+
+			GUILayout.EndHorizontal();
+
+			GUILayout.BeginHorizontal();
+
+			bool prettyLines = GUITools.Toggle(ARConfiguration.PrettyLines, "Draw Pretty Lines");
+			if (prettyLines != ARConfiguration.PrettyLines)
+			{
+				ARConfiguration.PrettyLines = prettyLines;
+				this.SaveConfigValue("drawPrettyLines", prettyLines);
+			}
+
+			GUILayout.EndHorizontal();
+
+			if (requireLineOfSight)
+			{
+				GUILayout.BeginHorizontal();
+
+				double graceRatio = 1d - Math.Sqrt(ARConfiguration.RadiusRatio);
+				double newRatio;
+
+				GUILayout.Label(string.Format("Line of Sight 'Fudge Factor': {0:P0}", graceRatio));
+
+				GUILayout.EndHorizontal();
+
+				GUILayout.BeginHorizontal();
+
+				newRatio = GUILayout.HorizontalSlider((float)graceRatio, 0f, 1f, GUILayout.ExpandWidth(true));
+				newRatio = Math.Round(newRatio, 2);
+
+				if (newRatio != graceRatio)
+				{
+					ARConfiguration.RadiusRatio = (1d - newRatio) * (1d - newRatio);
+					this.SaveConfigValue("graceRatio", newRatio);
+				}
+
+				GUILayout.EndHorizontal();
+			}
+
+			GUILayout.EndVertical();
+
+			GUI.DragWindow();
+		}
+
+		public void OnDestroy()
+		{
+			GameEvents.onGameSceneLoadRequested.Remove(this.onSceneChangeRequested);
+
+			if (this.toolbarButton != null)
+			{
+				this.toolbarButton.Destroy();
+			}
+
+			if (this.appLauncherButton != null)
+			{
+				ApplicationLauncher.Instance.RemoveModApplication(this.appLauncherButton);
+			}
+		}
+
+		protected void onSceneChangeRequested(GameScenes scene)
+		{
+			if (scene != GameScenes.SPACECENTER)
+			{
+				print("ARConfiguration: Requesting Destruction.");
+				MonoBehaviour.Destroy(this);
+			}
+		}
+
+		private void toggleConfigWindow()
+		{
+			this.showConfigWindow = !this.showConfigWindow;
+		}
+
+		private T LoadConfigValue<T>(string key, T defaultValue)
+		{
+			this.config.load();
+
+			return config.GetValue(key, defaultValue);
+		}
+
+		private void SaveConfigValue<T>(string key, T value)
+		{
+			this.config.load();
+
+			this.config.SetValue(key, value);
+
+			this.config.save();
+		}
+	}
+}
+

--- /dev/null
+++ b/ARFlightController.cs
@@ -1,1 +1,351 @@
-
+// AntennaRange
+//
+// ARFlightController.cs
+//
+// Copyright © 2014, toadicus
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without modification,
+// are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice,
+//    this list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation and/or other
+//    materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its contributors may be used
+//    to endorse or promote products derived from this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+using KSP;
+using System;
+using System.Collections.Generic;
+using ToadicusTools;
+using UnityEngine;
+
+namespace AntennaRange
+{
+	[KSPAddon(KSPAddon.Startup.Flight, false)]
+	public class ARFlightController : MonoBehaviour
+	{
+		#region Fields
+		protected Dictionary<ConnectionStatus, string> connectionTextures;
+		protected Dictionary<ConnectionStatus, Texture> appLauncherTextures;
+
+		protected ARMapRenderer mapRenderer;
+
+		protected IButton toolbarButton;
+
+		protected ApplicationLauncherButton appLauncherButton;
+		#endregion
+
+		#region Properties
+		public ConnectionStatus currentConnectionStatus
+		{
+			get;
+			protected set;
+		}
+
+		protected string currentConnectionTexture
+		{
+			get
+			{
+				return this.connectionTextures[this.currentConnectionStatus];
+			}
+		}
+
+		protected Texture currentAppLauncherTexture
+		{
+			get
+			{
+				return this.appLauncherTextures[this.currentConnectionStatus];
+			}
+		}
+
+		public ControlTypes currentControlLock
+		{
+			get
+			{
+				if (this.lockID == string.Empty)
+				{
+					return ControlTypes.None;
+				}
+
+				return InputLockManager.GetControlLock(this.lockID);
+			}
+		}
+
+		public string lockID
+		{
+			get;
+			protected set;
+		}
+
+		public ControlTypes lockSet
+		{
+			get
+			{
+				return ControlTypes.ALL_SHIP_CONTROLS;
+			}
+		}
+
+		public Vessel vessel
+		{
+			get
+			{
+				if (FlightGlobals.ready && FlightGlobals.ActiveVessel != null)
+				{
+					return FlightGlobals.ActiveVessel;
+				}
+
+				return null;
+			}
+		}
+		#endregion
+
+		#region MonoBehaviour LifeCycle
+		protected void Awake()
+		{
+			this.lockID = "ARConnectionRequired";
+
+			this.connectionTextures = new Dictionary<ConnectionStatus, string>();
+
+			this.connectionTextures[ConnectionStatus.None] = "AntennaRange/Textures/toolbarIconNoConnection";
+			this.connectionTextures[ConnectionStatus.Suboptimal] = "AntennaRange/Textures/toolbarIconSubOptimal";
+			this.connectionTextures[ConnectionStatus.Optimal] = "AntennaRange/Textures/toolbarIcon";
+
+			this.appLauncherTextures = new Dictionary<ConnectionStatus, Texture>();
+
+			this.appLauncherTextures[ConnectionStatus.None] =
+				GameDatabase.Instance.GetTexture("AntennaRange/Textures/appLauncherIconNoConnection", false);
+			this.appLauncherTextures[ConnectionStatus.Suboptimal] =
+				GameDatabase.Instance.GetTexture("AntennaRange/Textures/appLauncherIconSubOptimal", false);
+			this.appLauncherTextures[ConnectionStatus.Optimal] =
+				GameDatabase.Instance.GetTexture("AntennaRange/Textures/appLauncherIcon", false);
+
+			if (ToolbarManager.ToolbarAvailable)
+			{
+				this.toolbarButton = ToolbarManager.Instance.add("AntennaRange", "ARConnectionStatus");
+
+				this.toolbarButton.TexturePath = this.connectionTextures[ConnectionStatus.None];
+				this.toolbarButton.Text = "AntennaRange";
+				this.toolbarButton.Visibility = new GameScenesVisibility(GameScenes.FLIGHT);
+				this.toolbarButton.Enabled = false;
+			}
+
+			GameEvents.onGameSceneLoadRequested.Add(this.onSceneChangeRequested);
+			GameEvents.onVesselChange.Add(this.onVesselChange);
+		}
+
+		protected void Start()
+		{
+			this.mapRenderer = MapView.MapCamera.gameObject.AddComponent<ARMapRenderer>();
+		}
+
+		protected void FixedUpdate()
+		{
+			if (this.appLauncherButton == null && !ToolbarManager.ToolbarAvailable && ApplicationLauncher.Ready)
+			{
+				this.appLauncherButton = ApplicationLauncher.Instance.AddModApplication(
+					ApplicationLauncher.AppScenes.FLIGHT,
+					this.appLauncherTextures[ConnectionStatus.None]
+				);
+			}
+
+			Tools.DebugLogger log = Tools.DebugLogger.New(this);
+
+			VesselCommand availableCommand;
+
+			if (ARConfiguration.RequireConnectionForControl)
+			{
+				availableCommand = this.vessel.CurrentCommand();
+			}
+			else
+			{
+				availableCommand = VesselCommand.Crew;
+			}
+
+			log.AppendFormat("availableCommand: {0}\n\t" +
+				"(availableCommand & VesselCommand.Crew) == VesselCommand.Crew: {1}\n\t" +
+				"(availableCommand & VesselCommand.Probe) == VesselCommand.Probe: {2}\n\t" +
+				"vessel.HasConnectedRelay(): {3}",
+				(int)availableCommand,
+				(availableCommand & VesselCommand.Crew) == VesselCommand.Crew,
+				(availableCommand & VesselCommand.Probe) == VesselCommand.Probe,
+				vessel.HasConnectedRelay()
+			);
+
+			// If we are requiring a connection for control, the vessel does not have any adequately staffed pods,
+			// and the vessel does not have any connected relays...
+			if (
+				HighLogic.LoadedSceneIsFlight &&
+				ARConfiguration.RequireConnectionForControl &&
+				this.vessel != null &&
+				this.vessel.vesselType != VesselType.EVA &&
+				!(
+				    (availableCommand & VesselCommand.Crew) == VesselCommand.Crew ||
+				    (availableCommand & VesselCommand.Probe) == VesselCommand.Probe && vessel.HasConnectedRelay()
+				))
+			{
+				// ...and if the controls are not currently locked...
+				if (currentControlLock == ControlTypes.None)
+				{
+					// ...lock the controls.
+					InputLockManager.SetControlLock(this.lockSet, this.lockID);
+				}
+			}
+			// ...otherwise, if the controls are locked...
+			else if (currentControlLock != ControlTypes.None)
+			{
+				// ...unlock the controls.
+				InputLockManager.RemoveControlLock(this.lockID);
+			}
+				
+			if (
+				(this.toolbarButton != null || this.appLauncherButton != null) &&
+				HighLogic.LoadedSceneIsFlight &&
+				FlightGlobals.ActiveVessel != null
+			)
+			{
+				log.Append("Checking vessel relay status.\n");
+
+				List<ModuleLimitedDataTransmitter> relays =
+					FlightGlobals.ActiveVessel.getModulesOfType<ModuleLimitedDataTransmitter>();
+
+				log.AppendFormat("\t...found {0} relays\n", relays.Count);
+
+				bool vesselCanTransmit = false;
+				bool vesselHasOptimalRelay = false;
+
+				foreach (ModuleLimitedDataTransmitter relay in relays)
+				{
+					log.AppendFormat("\tvesselCanTransmit: {0}, vesselHasOptimalRelay: {1}\n",
+						vesselCanTransmit, vesselHasOptimalRelay);
+
+					log.AppendFormat("\tChecking relay {0}\n" +
+						"\t\tCanTransmit: {1}, transmitDistance: {2}, nominalRange: {3}\n",
+						relay,
+						relay.CanTransmit(),
+						relay.transmitDistance,
+						relay.nominalRange
+					);
+
+					bool relayCanTransmit = relay.CanTransmit();
+
+					if (!vesselCanTransmit && relayCanTransmit)
+					{
+						vesselCanTransmit = true;
+					}
+
+					if (!vesselHasOptimalRelay &&
+						relayCanTransmit &&
+						relay.transmitDistance <= (double)relay.nominalRange)
+					{
+						vesselHasOptimalRelay = true;
+					}
+
+					if (vesselCanTransmit && vesselHasOptimalRelay)
+					{
+						break;
+					}
+				}
+
+				log.AppendFormat("Done checking.  vesselCanTransmit: {0}, vesselHasOptimalRelay: {1}\n",
+					vesselCanTransmit, vesselHasOptimalRelay);
+
+				if (vesselHasOptimalRelay)
+				{
+					this.currentConnectionStatus = ConnectionStatus.Optimal;
+				}
+				else if (vesselCanTransmit)
+				{
+					this.currentConnectionStatus = ConnectionStatus.Suboptimal;
+				}
+				else
+				{
+					this.currentConnectionStatus = ConnectionStatus.None;
+				}
+
+				log.AppendFormat("currentConnectionStatus: {0}, setting texture to {1}",
+					this.currentConnectionStatus, this.currentConnectionTexture);
+
+				if (this.toolbarButton != null)
+				{
+					this.toolbarButton.TexturePath = this.currentConnectionTexture;
+
+					if (this.currentConnectionStatus == ConnectionStatus.None)
+					{
+						this.toolbarButton.Important = true;
+					}
+					else
+					{
+						this.toolbarButton.Important = false;
+					}
+				}
+				if (this.appLauncherButton != null)
+				{
+					this.appLauncherButton.SetTexture(this.currentAppLauncherTexture);
+				}
+			}
+
+			log.Print();
+		}
+
+		protected void OnDestroy()
+		{
+			InputLockManager.RemoveControlLock(this.lockID);
+
+			if (this.mapRenderer != null)
+			{
+				GameObject.Destroy(this.mapRenderer);
+			}
+
+			if (this.toolbarButton != null)
+			{
+				this.toolbarButton.Destroy();
+			}
+
+			if (this.appLauncherButton != null)
+			{
+				ApplicationLauncher.Instance.RemoveModApplication(this.appLauncherButton);
+				this.appLauncherButton = null;
+			}
+
+			GameEvents.onGameSceneLoadRequested.Remove(this.onSceneChangeRequested);
+			GameEvents.onVesselChange.Remove(this.onVesselChange);
+
+			print("ARFlightController: Destroyed.");
+		}
+		#endregion
+
+		#region Event Handlers
+		protected void onSceneChangeRequested(GameScenes scene)
+		{
+			print("ARFlightController: Requesting Destruction.");
+			MonoBehaviour.Destroy(this);
+		}
+
+		protected void onVesselChange(Vessel vessel)
+		{
+			InputLockManager.RemoveControlLock(this.lockID);
+		}
+		#endregion
+
+		public enum ConnectionStatus
+		{
+			None,
+			Suboptimal,
+			Optimal
+		}
+	}
+}
+

file:b/ARMapRenderer.cs (new)
--- /dev/null
+++ b/ARMapRenderer.cs
@@ -1,1 +1,273 @@
-
+// AntennaRange
+//
+// ARMapRenderer.cs
+//
+// Copyright © 2014, toadicus
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without modification,
+// are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice,
+//    this list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation and/or other
+//    materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its contributors may be used
+//    to endorse or promote products derived from this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+using KSP;
+using System;
+using System.Collections.Generic;
+using ToadicusTools;
+using UnityEngine;
+
+namespace AntennaRange
+{
+	public class ARMapRenderer : MonoBehaviour
+	{
+		#region Fields
+		private Dictionary<Guid, LineRenderer> vesselLineRenderers;
+		#endregion
+
+		#region Properties
+		public LineRenderer this[Guid idx]
+		{
+			get
+			{
+				if (this.vesselLineRenderers == null)
+				{
+					this.vesselLineRenderers = new Dictionary<Guid, LineRenderer>();
+				}
+
+				if (!this.vesselLineRenderers.ContainsKey(idx))
+				{
+					GameObject obj = new GameObject();
+					obj.layer = 31;
+
+					LineRenderer lr = obj.AddComponent<LineRenderer>();
+
+					lr.SetColors(Color.green, Color.green);
+					lr.material = MapView.OrbitLinesMaterial;
+					lr.SetVertexCount(2);
+
+					this.vesselLineRenderers[idx] = lr;
+				}
+
+				return this.vesselLineRenderers[idx];
+			}
+		}
+		#endregion
+
+		#region MonoBehaviour Lifecycle
+		private void Awake()
+		{
+			if (ARConfiguration.PrettyLines)
+			{
+				this.vesselLineRenderers = new Dictionary<Guid, LineRenderer>();
+			}
+		}
+
+		private void OnPreCull()
+		{
+			if (!HighLogic.LoadedSceneIsFlight || !MapView.MapIsEnabled || !ARConfiguration.PrettyLines)
+			{
+				this.Cleanup();
+
+				return;
+			}
+
+			Tools.DebugLogger log = Tools.DebugLogger.New(this);
+
+			try
+			{
+				log.AppendFormat("OnPreCull.\n");
+
+				log.AppendFormat("\tMapView: Draw3DLines: {0}\n" +
+					"\tMapView.MapCamera.camera.fieldOfView: {1}\n" +
+					"\tMapView.MapCamera.Distance: {2}\n",
+					MapView.Draw3DLines,
+					MapView.MapCamera.camera.fieldOfView,
+					MapView.MapCamera.Distance
+				);
+
+				log.AppendLine("vesselFrameCache cleared.");
+
+				if (FlightGlobals.ready && FlightGlobals.Vessels != null)
+				{
+					log.AppendLine("FlightGlobals ready and Vessels list not null.");
+
+					foreach (Vessel vessel in FlightGlobals.Vessels)
+					{
+						if (vessel == null)
+						{
+							log.AppendFormat("Skipping vessel {0} altogether because it is null.\n");
+							continue;
+						}
+
+						log.AppendFormat("Checking vessel {0}.\n", vessel.vesselName);
+
+						switch (vessel.vesselType)
+						{
+							case VesselType.Debris:
+							case VesselType.EVA:
+							case VesselType.Unknown:
+							case VesselType.SpaceObject:
+								log.AppendFormat("\tDiscarded because vessel is of invalid type {0}\n",
+									vessel.vesselType);
+								continue;
+						}
+
+						log.Append("\tChecking connection status...\n");
+
+						/*if (vessel.HasConnectedRelay())
+						{
+							log.AppendLine("\tHas a connection, checking for the best relay to use for the line.");*/
+
+							IAntennaRelay vesselRelay = null;
+							float bestScore = float.PositiveInfinity;
+							float relayScore = float.NaN;
+
+							foreach (IAntennaRelay relay in RelayDatabase.Instance[vessel].Values)
+							{
+								relayScore = (float)relay.transmitDistance / relay.maxTransmitDistance;
+
+								if (relayScore < bestScore)
+								{
+									bestScore = relayScore;
+									vesselRelay = relay as IAntennaRelay;
+								}
+							}
+
+							if (vesselRelay != null)
+							{
+								log.AppendFormat("\t...picked relay {0} with a score of {1}", 
+									vesselRelay, relayScore
+								);
+
+								this.SetRelayVertices(vesselRelay);
+							}
+						/*}
+						else if (this.vesselLineRenderers.ContainsKey(vessel.id))
+						{
+							log.AppendLine("\tDisabling line because vessel has no connection.");
+							this[vessel.id].enabled = false;
+						}*/
+					}
+				}
+			}
+			catch (Exception)
+			{
+				this.Cleanup();
+			}
+			#if DEBUG
+			finally
+			{
+				log.Print();
+			}
+			#endif
+		}
+
+		private void OnDestroy()
+		{
+			this.Cleanup();
+
+			print("ARMapRenderer: Destroyed.");
+		}
+		#endregion
+
+		private void SetRelayVertices(IAntennaRelay relay)
+		{
+			if (relay == null)
+			{
+				return;
+			}
+
+			LineRenderer renderer = this[relay.vessel.id];
+
+			Vector3d start;
+			Vector3d end;
+
+			renderer.enabled = true;
+
+			if (!relay.CanTransmit())
+			{
+				renderer.SetColors(Color.red, Color.red);
+			}
+			else
+			{
+				if (relay.transmitDistance < relay.nominalTransmitDistance)
+				{
+					renderer.SetColors(Color.green, Color.green);
+				}
+				else
+				{
+					renderer.SetColors(Color.yellow, Color.yellow);
+				}
+			}
+
+			start = ScaledSpace.LocalToScaledSpace(relay.vessel.GetWorldPos3D());
+
+			if (relay.KerbinDirect)
+			{
+				end = ScaledSpace.LocalToScaledSpace(AntennaRelay.Kerbin.position);
+			}
+			else
+			{
+				if (relay.targetRelay == null)
+				{
+					return;
+				}
+				end = ScaledSpace.LocalToScaledSpace(relay.targetRelay.vessel.GetWorldPos3D());
+			}
+
+			float lineWidth;
+
+			if (MapView.Draw3DLines)
+			{
+				lineWidth = 0.005859375f * MapView.MapCamera.Distance;
+			}
+			else
+			{
+				lineWidth = 2f;
+
+				start = MapView.MapCamera.camera.WorldToScreenPoint(start);
+				end = MapView.MapCamera.camera.WorldToScreenPoint(end);
+
+				float d = Screen.height / 2f + 0.01f;
+				start.z = start.z >= 0f ? d : -d;
+				end.z = end.z >= 0f ? d : -d;
+			}
+
+			renderer.SetWidth(lineWidth, lineWidth);
+
+			renderer.SetPosition(0, start);
+			renderer.SetPosition(1, end);
+		}
+
+		public void Cleanup()
+		{
+			if (this.vesselLineRenderers != null && this.vesselLineRenderers.Count > 0)
+			{
+				foreach (LineRenderer lineRenderer in this.vesselLineRenderers.Values)
+				{
+					lineRenderer.enabled = false;
+					GameObject.Destroy(lineRenderer.gameObject);
+				}
+				this.vesselLineRenderers.Clear();
+			}
+		}
+	}
+}
+
+

file:a/AntennaRange.cfg (deleted)
--- a/AntennaRange.cfg
+++ /dev/null
@@ -1,50 +1,1 @@
-//

-// AntennaRange © 2013 toadicus

-// 

-// This work is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License. To view a

-// copy of this license, visit http://creativecommons.org/licenses/by-nc-sa/3.0/

-//

-// This software uses the ModuleManager library © 2013 ialdabaoth, used under a Creative Commons Attribution-ShareAlike

-// 3.0 Uported License.

-//

-// Specifications:

-// nominalRange:	The distance from Kerbin at which the antenna will perform exactly as prescribed by packetResourceCost

-// 					and packetSize.

-// maxPowerFactor:	The multiplier on packetResourceCost that defines the maximum power output of the antenna.  When the

-//					power cost exceeds packetResourceCost * maxPowerFactor, transmission will fail.

-// maxDataFactor:	The multipler on packetSize that defines the maximum data bandwidth of the antenna.

-// 

-

-@PART[longAntenna]

-{

-	@MODULE[ModuleDataTransmitter]

-	{

-		@name = ModuleLimitedDataTransmitter

-		nominalRange = 1500000

-		maxPowerFactor = 8

-		maxDataFactor = 4

-	}

-}

-

-@PART[mediumDishAntenna]

-{

-	@MODULE[ModuleDataTransmitter]

-	{

-		@name = ModuleLimitedDataTransmitter

-		nominalRange = 30000000

-		maxPowerFactor = 8

-		maxDataFactor = 4

-	}

-}

-

-@PART[commDish]

-{

-	@MODULE[ModuleDataTransmitter]

-	{

-		@name = ModuleLimitedDataTransmitter

-		nominalRange = 80000000000

-		maxPowerFactor = 8

-		maxDataFactor = 4

-	}

-}

 

file:a/AntennaRange.cs (deleted)
--- a/AntennaRange.cs
+++ /dev/null
@@ -1,347 +1,1 @@
-/*
- * AntennaRange © 2013 toadicus
- * 
- * AntennaRange provides incentive and requirements for the use of the various antenna parts.
- * Nominally, the breakdown is as follows:
- * 
- *     Communotron 16 - Suitable up to Kerbalsynchronous Orbit
- *     Comms DTS-M1 - Suitable throughout the Kerbin subsystem
- *     Communotron 88-88 - Suitable throughout the Kerbol system.
- * 
- * This work is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License. To view a
- * copy of this license, visit http://creativecommons.org/licenses/by-nc-sa/3.0/
- * 
- * This software uses the ModuleManager library © 2013 ialdabaoth, used under a Creative Commons Attribution-ShareAlike 3.0 Uported License.
- * 
- */
 
-using System;
-using System.Collections.Generic;
-using KSP;
-
-namespace AntennaRange
-{
-	/*
-	 * ModuleLimitedDataTransmitter is designed as a drop-in replacement for ModuleDataTransmitter, and handles range-
-	 * finding, power scaling, and data scaling for antennas during science transmission.  Its functionality varies with
-	 * three tunables: nominalRange, maxPowerFactor, and maxDataFactor, set in .cfg files.
-	 * 
-	 * In general, the scaling functions assume the following relation:
-	 * 
-	 *     D² α P/R,
-	 * 
-	 * where D is the total transmission distance, P is the transmission power, and R is the data rate.
-	 * 
-	 * */
-	public class ModuleLimitedDataTransmitter : ModuleDataTransmitter, IScienceDataTransmitter
-	{
-		// Stores the packetResourceCost as defined in the .cfg file.
-		protected float _basepacketResourceCost;
-
-		// Stores the packetSize as defined in the .cfg file.
-		protected float _basepacketSize;
-
-		// We don't have a Bard, so we're hiding Kerbin here.
-		protected CelestialBody _Kerbin;
-
-		// Returns the current distance to the center of Kerbin, which is totally where the Kerbals keep their radioes.
-		protected double transmitDistance
-		{
-			get
-			{
-				Vector3d KerbinPos = this._Kerbin.position;
-				Vector3d ActivePos = base.vessel.GetWorldPos3D();
-
-				return (ActivePos - KerbinPos).magnitude;
-			}
-		}
-
-		// Returns the maximum distance this module can transmit
-		public double maxTransmitDistance
-		{
-			get
-			{
-				return Math.Sqrt (this.maxPowerFactor) * this.nominalRange;
-			}
-		}
-
-		// The distance from Kerbin at which the antenna will perform exactly as prescribed by packetResourceCost
-		// and packetSize.
-		[KSPField(isPersistant = false)]
-		public float nominalRange;
-
-		// The multiplier on packetResourceCost that defines the maximum power output of the antenna.  When the power
-		// cost exceeds packetResourceCost * maxPowerFactor, transmission will fail.
-		[KSPField(isPersistant = false)]
-		public float maxPowerFactor;
-
-		// The multipler on packetSize that defines the maximum data bandwidth of the antenna.
-		[KSPField(isPersistant = false)]
-		public float maxDataFactor;
-
-
-		/*
-		 * The next two functions overwrite the behavior of the stock functions and do not perform equivalently, except
-		 * in that they both return floats.  Here's some quick justification:
-		 * 
-		 * The stock implementation of GetTransmitterScore (which I cannot override) is:
-		 * 		Score = (1 + DataResourceCost) / DataRate
-		 * 
-		 * The stock DataRate and DataResourceCost are:
-		 * 		DataRate = packetSize / packetInterval
-		 * 		DataResourceCost = packetResourceCost / packetSize
-		 * 
-		 * So, the resulting score is essentially in terms of joules per byte per baud.  Rearranging that a bit, it
-		 * could also look like joule-seconds per byte per byte, or newton-meter-seconds per byte per byte.  Either way,
-		 * that metric is not a very reasonable one.
-		 * 
-		 * Two metrics that might make more sense are joules per byte or joules per byte per second.  The latter case
-		 * would look like:
-		 * 		DataRate = packetSize / packetInterval
-		 * 		DataResourceCost = packetResourceCost
-		 * 
-		 * The former case, which I've chosen to implement below, is:
-		 * 		DataRate = packetSize
-		 * 		DataResourceCost = packetResourceCost
-		 * 
-		 * So... hopefully that doesn't screw with anything else.
-		 * */
-		// Override ModuleDataTransmitter.DataRate to just return packetSize, because we want antennas to be scored in
-		// terms of joules/byte
-		public new float DataRate
-		{
-			get
-			{
-				PreTransmit_SetPacketSize();
-				return this.packetSize;
-			}
-		}
-
-		// Override ModuleDataTransmitter.DataResourceCost to just return packetResourceCost, because we want antennas
-		// to be scored in terms of joules/byte
-		public new float DataResourceCost
-		{
-			get
-			{
-				PreTransmit_SetPacketResourceCost();
-				return this.packetResourceCost;
-			}
-		}
-
-		// Build ALL the objects.
-		public ModuleLimitedDataTransmitter () : base() { }
-
-		// At least once, when the module starts with a state on the launch pad or later, go find Kerbin.
-		public override void OnStart (StartState state)
-		{
-			base.OnStart (state);
-
-			if (state >= StartState.PreLaunch && this._Kerbin == null)
-			{
-				// Go fetch Kerbin, because it is tricksy and hides from us.
-				List<CelestialBody> bodies = FlightGlobals.Bodies;
-
-				foreach (CelestialBody body in bodies)
-				{
-					if (body.name == "Kerbin")
-					{
-						this._Kerbin = body;
-						break;
-					}
-				}
-			}
-		}
-
-		// When the module loads, fetch the Squad KSPFields from the base.  This is necessary in part because
-		// overloading packetSize and packetResourceCostinto a property in ModuleLimitedDataTransmitter didn't
-		// work.
-		public override void OnLoad(ConfigNode node)
-		{
-			this.Fields.Load(node);
-			base.Fields.Load(node);
-
-			base.OnLoad (node);
-
-			this._basepacketSize = base.packetSize;
-			this._basepacketResourceCost = base.packetResourceCost;
-
-			Tools.PostDebugMessage(string.Format(
-				"{0} loaded:\n" +
-				"packetSize: {1}\n" +
-				"packetResourceCost: {2}\n" +
-				"nominalRange: {3}\n" +
-				"maxPowerFactor: {4}\n" +
-				"maxDataFactor: {5}\n",
-				this.name,
-				base.packetSize,
-				this._basepacketResourceCost,
-				this.nominalRange,
-				this.maxPowerFactor,
-				this.maxDataFactor
-			));
-		}
-
-		// Post an error in the communication messages describing the reason transmission has failed.  Currently there
-		// is only one reason for this.
-		protected void PostCannotTransmitError()
-		{
-			string ErrorText = string.Format (
-				"Unable to transmit: out of range!  Maximum range = {0}; Current range = {1}.",
-				this.maxTransmitDistance,
-				this.transmitDistance);
-			ScreenMessages.PostScreenMessage (new ScreenMessage (ErrorText, 4f, ScreenMessageStyle.UPPER_LEFT));
-		}
-
-		// Before transmission, set packetResourceCost.  Per above, packet size increases with the square of
-		// distance.  packetResourceCost maxes out at _basepacketResourceCost * maxPowerFactor, at which point
-		// transmission fails (see CanTransmit).
-		protected void PreTransmit_SetPacketResourceCost()
-		{
-			if (this.transmitDistance <= this.nominalRange)
-			{
-				base.packetResourceCost = this._basepacketResourceCost;
-			}
-			else
-			{
-				base.packetResourceCost = this._basepacketResourceCost
-					* (float)Math.Pow (this.transmitDistance / this.nominalRange, 2);
-			}
-		}
-
-		// Before transmission, set packetSize.  Per above, packet size increases with the inverse square of
-		// distance.  packetSize maxes out at _basepacketSize * maxDataFactor.
-		protected void PreTransmit_SetPacketSize()
-		{
-			if (this.transmitDistance >= this.nominalRange)
-			{
-				base.packetSize = this._basepacketSize;
-			}
-			else
-			{
-				base.packetSize = Math.Min(
-					this._basepacketSize * (float)Math.Pow (this.nominalRange / this.transmitDistance, 2),
-					this._basepacketSize * this.maxDataFactor);
-			}
-		}
-
-		// Override ModuleDataTransmitter.GetInfo to add nominal and maximum range to the VAB description.
-		public override string GetInfo()
-		{
-			string text = base.GetInfo();
-			text += "Nominal Range: " + this.nominalRange.ToString() + "\n";
-			text += "Maximum Range: " + this.maxTransmitDistance.ToString() + "\n";
-			return text;
-		}
-
-		// Override ModuleDataTransmitter.CanTransmit to return false when transmission is not possible.
-		public new bool CanTransmit()
-		{
-			if (this.transmitDistance > this.maxTransmitDistance)
-			{
-				return false;
-			}
-			return true;
-		}
-
-		// Override ModuleDataTransmitter.TransmitData to check against CanTransmit and fail out when CanTransmit
-		// returns false.
-		public new void TransmitData(List<ScienceData> dataQueue)
-		{
-			PreTransmit_SetPacketSize ();
-			PreTransmit_SetPacketResourceCost ();
-
-			Tools.PostDebugMessage (
-				"distance: " + this.transmitDistance
-				+ " packetSize: " + this.packetSize
-				+ " packetResourceCost: " + this.packetResourceCost
-			);
-			if (this.CanTransmit())
-			{
-				base.TransmitData(dataQueue);
-			}
-			else
-			{
-				this.PostCannotTransmitError ();
-			}
-		}
-
-		// Override ModuleDataTransmitter.StartTransmission to check against CanTransmit and fail out when CanTransmit
-		// returns false.
-		public new void StartTransmission()
-		{
-			PreTransmit_SetPacketSize ();
-			PreTransmit_SetPacketResourceCost ();
-
-			Tools.PostDebugMessage (
-				"distance: " + this.transmitDistance
-				+ " packetSize: " + this.packetSize
-				+ " packetResourceCost: " + this.packetResourceCost
-				);
-			if (this.CanTransmit())
-			{
-				base.StartTransmission();
-			}
-			else
-			{
-				this.PostCannotTransmitError ();
-			}
-		}
-
-		// When debugging, it's nice to have a button that just tells you everything.
-		#if DEBUG
-		[KSPEvent (guiName = "Show Debug Info", active = true, guiActive = true)]
-		public void DebugInfo()
-		{
-			PreTransmit_SetPacketSize ();
-			PreTransmit_SetPacketResourceCost ();
-
-			string msg = string.Format(
-				"'{0}'\n" + 
-				"_basepacketSize: {1}\n" +
-				"packetSize: {2}\n" +
-				"_basepacketResourceCost: {3}\n" +
-				"packetResourceCost: {4}\n" +
-				"maxTransmitDistance: {5}\n" +
-				"transmitDistance: {6}\n" +
-				"nominalRange: {7}\n" +
-				"CanTransmit: {8}\n" +
-				"DataRate: {9}\n" +
-				"DataResourceCost: {10}\n" +
-				"TransmitterScore: {11}",
-				this.name,
-				this._basepacketSize,
-				base.packetSize,
-				this._basepacketResourceCost,
-				base.packetResourceCost,
-				this.maxTransmitDistance,
-				this.transmitDistance,
-				this.nominalRange,
-				this.CanTransmit(),
-				this.DataRate,
-				this.DataResourceCost,
-				ScienceUtil.GetTransmitterScore(this)
-				);
-			ScreenMessages.PostScreenMessage (new ScreenMessage (msg, 4f, ScreenMessageStyle.UPPER_RIGHT));
-		}
-		#endif
-	}
-
-	public static class Tools
-	{
-		// When debugging, be verbose.  The Conditional attribute prevents this from firing when not DEBUGging.
-		[System.Diagnostics.Conditional("DEBUG")]
-		public static void PostDebugMessage(string Msg)
-		{
-			if (HighLogic.LoadedScene > GameScenes.SPACECENTER)
-			{
-				ScreenMessage Message = new ScreenMessage(Msg, 4f, ScreenMessageStyle.LOWER_CENTER);
-				ScreenMessages.PostScreenMessage(Message);
-			}
-			else
-			{
-				KSPLog.print(Msg);
-			}
-		}
-	}
-}
-

--- /dev/null
+++ b/AntennaRange.csproj
@@ -1,1 +1,108 @@
-
+<?xml version="1.0" encoding="utf-8"?>
+<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <PropertyGroup>
+    <Configuration Condition=" '$(Configuration)' == '' ">Debug_win</Configuration>
+    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+    <ProductVersion>8.0.30703</ProductVersion>
+    <SchemaVersion>2.0</SchemaVersion>
+    <ProjectGuid>{B36F2C11-962E-4A75-9F41-61AD56D11493}</ProjectGuid>
+    <OutputType>Library</OutputType>
+    <RootNamespace>AntennaRange</RootNamespace>
+    <AssemblyName>AntennaRange</AssemblyName>
+    <ReleaseVersion>1.3</ReleaseVersion>
+    <SynchReleaseVersion>false</SynchReleaseVersion>
+    <TargetFrameworkVersion>v3.5</TargetFrameworkVersion>
+    <UseMSBuildEngine>False</UseMSBuildEngine>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug_win|AnyCPU' ">
+    <DebugSymbols>true</DebugSymbols>
+    <DebugType>full</DebugType>
+    <Optimize>false</Optimize>
+    <OutputPath>bin\Debug</OutputPath>
+    <DefineConstants>DEBUG;TRACE;</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+    <ConsolePause>false</ConsolePause>
+    <CustomCommands>
+      <CustomCommands>
+        <Command type="AfterBuild" command="xcopy /y ${TargetFile} ${ProjectDir}\GameData\AntennaRange\" />
+      </CustomCommands>
+    </CustomCommands>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release_win|AnyCPU' ">
+    <Optimize>true</Optimize>
+    <OutputPath>bin\Release</OutputPath>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+    <ConsolePause>false</ConsolePause>
+    <CustomCommands>
+      <CustomCommands>
+        <Command type="AfterBuild" command="xcopy /y ${TargetFile} ${ProjectDir}\GameData\AntennaRange\" />
+      </CustomCommands>
+    </CustomCommands>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug_linux|AnyCPU' ">
+    <DebugSymbols>true</DebugSymbols>
+    <DebugType>full</DebugType>
+    <Optimize>false</Optimize>
+    <OutputPath>bin\Debug</OutputPath>
+    <DefineConstants>DEBUG;TRACE;</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+    <ConsolePause>false</ConsolePause>
+    <CustomCommands>
+      <CustomCommands>
+        <Command type="AfterBuild" command="cp -afv ${TargetFile} ${ProjectDir}/GameData/${ProjectName}/" />
+      </CustomCommands>
+    </CustomCommands>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release_linux|AnyCPU' ">
+    <Optimize>true</Optimize>
+    <OutputPath>bin\Release</OutputPath>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+    <CustomCommands>
+      <CustomCommands>
+        <Command type="AfterBuild" command="cp -afv ${TargetFile} ${ProjectDir}/GameData/${ProjectName}/" />
+      </CustomCommands>
+    </CustomCommands>
+    <ConsolePause>false</ConsolePause>
+  </PropertyGroup>
+  <ItemGroup>
+    <Compile Include="Properties\AssemblyInfo.cs" />
+    <Compile Include="IAntennaRelay.cs" />
+    <Compile Include="ModuleLimitedDataTransmitter.cs" />
+    <Compile Include="AntennaRelay.cs" />
+    <Compile Include="ProtoAntennaRelay.cs" />
+    <Compile Include="RelayDatabase.cs" />
+    <Compile Include="RelayExtensions.cs" />
+    <Compile Include="ARConfiguration.cs" />
+    <Compile Include="ARFlightController.cs" />
+    <Compile Include="ARMapRenderer.cs" />
+  </ItemGroup>
+  <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
+  <ItemGroup>
+    <Reference Include="Assembly-CSharp">
+      <HintPath>..\_KSPAssemblies\Assembly-CSharp.dll</HintPath>
+      <Private>False</Private>
+    </Reference>
+    <Reference Include="System">
+      <HintPath>..\_KSPAssemblies\System.dll</HintPath>
+      <Private>False</Private>
+    </Reference>
+    <Reference Include="UnityEngine">
+      <HintPath>..\_KSPAssemblies\UnityEngine.dll</HintPath>
+      <Private>False</Private>
+    </Reference>
+  </ItemGroup>
+  <ItemGroup>
+    <ProjectReference Include="..\ToadicusTools\ToadicusTools.csproj">
+      <Project>{D48A5542-6655-4149-BC27-B27DF0466F1C}</Project>
+      <Name>ToadicusTools</Name>
+    </ProjectReference>
+  </ItemGroup>
+  <ItemGroup>
+    <None Include="GameData\AntennaRange\AntennaRange.cfg" />
+    <None Include="GameData\AntennaRange\ATM_AntennaRange.cfg" />
+  </ItemGroup>
+</Project>

file:b/AntennaRelay.cs (new)
--- /dev/null
+++ b/AntennaRelay.cs
@@ -1,1 +1,530 @@
-
+// AntennaRange
+//
+// AntennaRelay.cs
+//
+// Copyright © 2014, toadicus
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without modification,
+// are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice,
+//    this list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation and/or other
+//    materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its contributors may be used
+//    to endorse or promote products derived from this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using ToadicusTools;
+
+// @DONE TODO: Retool nearestRelay to always contain the nearest relay, even if out of range.
+// @DONE TODO: Retool CanTransmit to not rely on nearestRelay == null.
+// TODO: Track occluded vessels somehow.
+
+namespace AntennaRange
+{
+	public class AntennaRelay
+	{
+		// We don't have a Bard, so we'll hide Kerbin here.
+		private static CelestialBody _Kerbin;
+		public static CelestialBody Kerbin
+		{
+			get
+			{
+				if (_Kerbin == null && FlightGlobals.ready)
+				{
+					_Kerbin = FlightGlobals.GetHomeBody();
+				}
+
+				return _Kerbin;
+			}
+		}
+
+		protected bool canTransmit;
+
+		protected IAntennaRelay moduleRef;
+
+		protected System.Diagnostics.Stopwatch searchTimer;
+		protected long millisecondsBetweenSearches;
+
+		/// <summary>
+		/// Gets the parent Vessel.
+		/// </summary>
+		/// <value>The parent Vessel.</value>
+		public virtual Vessel vessel
+		{
+			get
+			{
+				return this.moduleRef.vessel;
+			}
+		}
+
+		/// <summary>
+		/// Gets or sets the nearest relay.
+		/// </summary>
+		/// <value>The nearest relay</value>
+		public IAntennaRelay nearestRelay
+		{
+			get;
+			protected set;
+		}
+
+		public IAntennaRelay bestOccludedRelay
+		{
+			get;
+			protected set;
+		}
+
+		public IAntennaRelay targetRelay
+		{
+			get;
+			protected set;
+		}
+
+		/// <summary>
+		/// Gets the first <see cref="CelestialBody"/> found to be blocking line of sight.
+		/// </summary>
+		public virtual CelestialBody firstOccludingBody
+		{
+			get;
+			protected set;
+		}
+
+		/// <summary>
+		/// Gets the transmit distance.
+		/// </summary>
+		/// <value>The transmit distance.</value>
+		public double transmitDistance
+		{
+			get
+			{
+				this.FindNearestRelay();
+
+				if (this.KerbinDirect || this.targetRelay == null)
+				{
+					return this.DistanceTo(Kerbin);
+				}
+				else
+				{
+					return this.DistanceTo(this.targetRelay);
+				}
+			}
+		}
+
+		public virtual double nominalTransmitDistance
+		{
+			get;
+			set;
+		}
+
+		/// <summary>
+		/// The maximum distance at which this relay can operate.
+		/// </summary>
+		/// <value>The max transmit distance.</value>
+		public virtual float maxTransmitDistance
+		{
+			get;
+			set;
+		}
+
+		/// <summary>
+		/// Gets a value indicating whether this <see cref="AntennaRange.ProtoDataTransmitter"/> has been checked during
+		/// the current relay attempt.
+		/// </summary>
+		/// <value><c>true</c> if relay checked; otherwise, <c>false</c>.</value>
+		public virtual bool relayChecked
+		{
+			get;
+			protected set;
+		}
+
+		public virtual bool KerbinDirect
+		{
+			get;
+			protected set;
+		}
+
+		/// <summary>
+		/// Determines whether this instance can transmit.
+		/// </summary>
+		/// <returns><c>true</c> if this instance can transmit; otherwise, <c>false</c>.</returns>
+		public virtual bool CanTransmit()
+		{
+			this.FindNearestRelay();
+			return this.canTransmit;
+		}
+
+		/// <summary>
+		/// Finds the nearest relay.
+		/// </summary>
+		/// <returns>The nearest relay or null, if no relays in range.</returns>
+		private void FindNearestRelay()
+		{
+			if (!this.searchTimer.IsRunning || this.searchTimer.ElapsedMilliseconds > this.millisecondsBetweenSearches)
+			{
+				this.searchTimer.Reset();
+			}
+			else
+			{
+				return;
+			}
+
+			// Skip vessels that have already been checked for a nearest relay this pass.
+			if (RelayDatabase.Instance.CheckedVesselsTable.ContainsKey(this.vessel.id))
+			{
+				return;
+			}
+
+			if (FlightGlobals.ActiveVessel != null && FlightGlobals.ActiveVessel.id == this.vessel.id)
+			{
+				Tools.PostLogMessage(string.Format(
+					"{0}: finding nearest relay for {1}",
+					this.GetType().Name,
+					this.ToString()
+				));
+			}
+
+			// Set this vessel as checked, so that we don't check it again.
+			RelayDatabase.Instance.CheckedVesselsTable[vessel.id] = true;
+
+			// Blank everything we're trying to find before the search.
+			this.firstOccludingBody = null;
+			this.bestOccludedRelay = null;
+			this.targetRelay = null;
+			this.nearestRelay = null;
+
+			CelestialBody bodyOccludingBestOccludedRelay = null;
+
+			double nearestRelaySqrDistance = double.PositiveInfinity;
+			double bestOccludedSqrDistance = double.PositiveInfinity;
+			double maxTransmitSqrDistance = this.maxTransmitDistance * this.maxTransmitDistance;
+
+			/*
+			 * Loop through all the vessels and exclude this vessel, vessels of the wrong type, and vessels that are too
+			 * far away.  When we find a candidate, get through its antennae for relays which have not been checked yet
+			 * and that can transmit.  Once we find a suitable candidate, assign it to nearestRelay for comparison
+			 * against future finds.
+			 * */
+			foreach (Vessel potentialVessel in FlightGlobals.Vessels)
+			{
+				// Skip vessels of the wrong type.
+				switch (potentialVessel.vesselType)
+				{
+					case VesselType.Debris:
+					case VesselType.Flag:
+					case VesselType.EVA:
+					case VesselType.SpaceObject:
+					case VesselType.Unknown:
+						continue;
+					default:
+						break;
+				}
+
+				// Skip vessels with the wrong ID
+				if (potentialVessel.id == vessel.id)
+				{
+					continue;
+				}
+
+				// Find the distance from here to the vessel...
+				double potentialSqrDistance = this.sqrDistanceTo(potentialVessel);
+
+				CelestialBody fob = null;
+
+				// Skip vessels to which we do not have line of sight.
+				if (
+					ARConfiguration.RequireLineOfSight &&
+					!this.vessel.hasLineOfSightTo(potentialVessel, out fob, ARConfiguration.RadiusRatio)
+				)
+				{
+					this.firstOccludingBody = fob;
+
+					if (FlightGlobals.ActiveVessel != null && FlightGlobals.ActiveVessel.id == this.vessel.id)
+					{
+						Tools.PostLogMessage("{6}: Vessel {0} discarded because we do not have line of sight." +
+							"\npotentialSqrDistance: {1}, bestOccludedSqrDistance: {2}, maxTransmitSqrDistance: {3}" +
+							"\npotentialSqrDistance < bestOccludedSqrDistance: {4}" +
+							"\npotentialSqrDistance < (this.maxTransmitDistance * this.maxTransmitDistance): {5}",
+							potentialVessel.vesselName,
+							potentialSqrDistance, bestOccludedSqrDistance, this.maxTransmitDistance * this.maxTransmitDistance,
+							potentialSqrDistance < bestOccludedSqrDistance,
+							potentialSqrDistance < (this.maxTransmitDistance * this.maxTransmitDistance),
+							this.ToString()
+						);
+					}
+
+					if (
+						(potentialSqrDistance < bestOccludedSqrDistance) &&
+						(potentialSqrDistance < maxTransmitSqrDistance)
+					)
+					{
+						if (FlightGlobals.ActiveVessel != null && FlightGlobals.ActiveVessel.id == this.vessel.id)
+						{
+							Tools.PostLogMessage("{0}: Checking {1} relays on {2}.",
+								this.ToString(),
+								potentialVessel.GetAntennaRelays().Count(),
+								potentialVessel
+							);
+						}
+
+						foreach (IAntennaRelay occludedRelay in potentialVessel.GetAntennaRelays())
+						{
+							if (FlightGlobals.ActiveVessel != null && FlightGlobals.ActiveVessel.id == this.vessel.id)
+							{
+								Tools.PostLogMessage(this.ToString() +  " Checking candidate for bestOccludedRelay: {0}" +
+									"\n\tCanTransmit: {1}", occludedRelay, occludedRelay.CanTransmit());
+							}
+
+							if (occludedRelay.CanTransmit())
+							{
+								this.bestOccludedRelay = occludedRelay;
+								bodyOccludingBestOccludedRelay = fob;
+								bestOccludedSqrDistance = potentialSqrDistance;
+
+								if (FlightGlobals.ActiveVessel != null && FlightGlobals.ActiveVessel.id == this.vessel.id)
+								{
+									Tools.PostLogMessage(this.ToString() + " Found new bestOccludedRelay: {0}" +
+										"\nfirstOccludingBody: {1}" +
+										"\nbestOccludedSqrDistance: {2}",
+										occludedRelay,
+										fob,
+										potentialSqrDistance
+									);
+								}
+								break;
+							}
+						}
+					}
+
+					continue;
+				}
+
+				/*
+				 * ...so that we can skip the vessel if it is further away than a vessel we've already checked.
+				 * */
+				if (potentialSqrDistance > nearestRelaySqrDistance)
+				{
+					if (FlightGlobals.ActiveVessel != null && FlightGlobals.ActiveVessel.id == this.vessel.id)
+					{
+						Tools.PostLogMessage("{0}: Vessel {1} discarded because it is out of range, or farther than another relay.",
+							this.ToString(),
+							potentialVessel.vesselName
+						);
+					}
+					continue;
+				}
+
+				nearestRelaySqrDistance = potentialSqrDistance;
+
+				foreach (IAntennaRelay potentialRelay in potentialVessel.GetAntennaRelays())
+				{
+					if (potentialRelay.CanTransmit() && potentialRelay.targetRelay != this)
+					{
+						this.nearestRelay = potentialRelay;
+
+						if (FlightGlobals.ActiveVessel != null && FlightGlobals.ActiveVessel.id == this.vessel.id)
+						{
+							Tools.PostLogMessage(string.Format("{0}: found new best relay {1} ({2})",
+								this.ToString(),
+								this.nearestRelay.ToString(),
+								this.nearestRelay.vessel.id
+							));
+						}
+						break;
+					}
+				}
+			}
+
+			CelestialBody bodyOccludingKerbin = null;
+
+			double kerbinSqrDistance = this.vessel.DistanceTo(Kerbin) - Kerbin.Radius;
+			kerbinSqrDistance *= kerbinSqrDistance;
+
+			System.Text.StringBuilder log = new System.Text.StringBuilder();
+
+			log.AppendFormat("{0} ({1}): Search done, figuring status.", this.ToString(), this.GetType().Name);
+
+			// If we don't have LOS to Kerbin, focus on relays
+			if (!this.vessel.hasLineOfSightTo(Kerbin, out bodyOccludingKerbin, ARConfiguration.RadiusRatio))
+			{
+				log.AppendFormat("\n\tKerbin LOS is blocked by {0}.", bodyOccludingKerbin.bodyName);
+
+				// nearestRelaySqrDistance will be infinity if all relays are occluded or none exist.
+				// Therefore, this will only be true if a valid relay is in range.
+				if (nearestRelaySqrDistance <= maxTransmitSqrDistance)
+				{
+					log.AppendFormat("\n\tCan transmit to nearby relay {0} ({1} <= {2}).",
+						this.nearestRelay == null ? "null" : this.nearestRelay.ToString(),
+							nearestRelaySqrDistance, maxTransmitSqrDistance);
+
+					this.KerbinDirect = false;
+					this.canTransmit = true;
+					this.targetRelay = this.nearestRelay;
+				}
+				// If this isn't true, we can't transmit, but pick a second best of bestOccludedRelay and Kerbin anyway
+				else
+				{
+					log.AppendFormat("\n\tCan't transmit to nearby relay {0} ({1} > {2}).",
+						this.nearestRelay == null ? "null" : this.nearestRelay.ToString(),
+							nearestRelaySqrDistance, maxTransmitSqrDistance);
+
+					this.canTransmit = false;
+
+					// If the best occluded relay is closer than Kerbin, target it.
+					if (bestOccludedSqrDistance < kerbinSqrDistance)
+					{
+						log.AppendFormat("\n\t\tPicking occluded relay {0} as target ({1} < {2}).",
+							this.bestOccludedRelay == null ? "null" : this.bestOccludedRelay.ToString(),
+							bestOccludedSqrDistance, kerbinSqrDistance);
+
+						this.KerbinDirect = false;
+						this.targetRelay = this.bestOccludedRelay;
+						this.firstOccludingBody = bodyOccludingBestOccludedRelay;
+					}
+					// Otherwise, target Kerbin and report the first body blocking it.
+					else
+					{
+						log.AppendFormat("\n\t\tPicking Kerbin as target ({0} >= {1}).",
+							bestOccludedSqrDistance, kerbinSqrDistance);
+
+						this.KerbinDirect = true;
+						this.targetRelay = null;
+						this.firstOccludingBody = bodyOccludingKerbin;
+					}
+				}
+			}
+			// If we do have LOS to Kerbin, try to prefer the closest of nearestRelay and Kerbin
+			else
+			{
+				log.AppendFormat("\n\tKerbin is in LOS.");
+
+				// If the nearest relay is closer than Kerbin and in range, transmit to it.
+				if (nearestRelaySqrDistance <= maxTransmitSqrDistance)
+				{
+					log.AppendFormat("\n\tCan transmit to nearby relay {0} ({1} <= {2}).",
+						this.nearestRelay == null ? "null" : this.nearestRelay.ToString(),
+							nearestRelaySqrDistance, maxTransmitSqrDistance);
+
+					this.canTransmit = true;
+
+					// If the nearestRelay is closer than Kerbin, use it.
+					if (nearestRelaySqrDistance < kerbinSqrDistance)
+					{
+						log.AppendFormat("\n\tPicking relay {0} over Kerbin ({1} < {2}).",
+							this.nearestRelay == null ? "null" : this.nearestRelay.ToString(),
+							nearestRelaySqrDistance, kerbinSqrDistance);
+
+						this.KerbinDirect = false;
+						this.targetRelay = this.nearestRelay;
+					}
+					// Otherwise, Kerbin is closer, so use it.
+					else
+					{
+						log.AppendFormat("\n\tBut picking Kerbin over nearby relay {0} ({1} >= {2}).",
+							this.nearestRelay == null ? "null" : this.nearestRelay.ToString(),
+								nearestRelaySqrDistance, kerbinSqrDistance);
+
+						this.KerbinDirect = true;
+						this.targetRelay = null;
+					}
+				}
+				// If the nearest relay is out of range, we still need to check on Kerbin.
+				else
+				{
+					log.AppendFormat("\n\tCan't transmit to nearby relay {0} ({1} > {2}).",
+						this.nearestRelay == null ? "null" : this.nearestRelay.ToString(),
+							nearestRelaySqrDistance, maxTransmitSqrDistance);
+
+					// If Kerbin is in range, use it.
+					if (kerbinSqrDistance <= maxTransmitSqrDistance)
+					{
+						log.AppendFormat("\n\tCan transmit to Kerbin ({0} <= {1}).",
+							kerbinSqrDistance, maxTransmitSqrDistance);
+
+						this.canTransmit = true;
+						this.KerbinDirect = true;
+						this.targetRelay = null;
+					}
+					// If Kerbin is out of range and the nearest relay is out of range, pick a second best between
+					// Kerbin and bestOccludedRelay
+					else
+					{
+						log.AppendFormat("\n\tCan't transmit to Kerbin ({0} > {1}).",
+							kerbinSqrDistance, maxTransmitSqrDistance);
+
+						this.canTransmit = false;
+
+						// If the best occluded relay is closer than Kerbin, use it.
+						// Since bestOccludedSqrDistance is infinity if there are no occluded relays,
+						// this is safe
+						if (bestOccludedSqrDistance < kerbinSqrDistance)
+						{
+							log.AppendFormat("\n\t\tPicking occluded relay {0} as target ({1} < {2}).",
+								this.bestOccludedRelay == null ? "null" : this.bestOccludedRelay.ToString(),
+									bestOccludedSqrDistance, kerbinSqrDistance);
+
+							this.KerbinDirect = false;
+							this.targetRelay = bestOccludedRelay;
+							this.firstOccludingBody = bodyOccludingBestOccludedRelay;
+						}
+						// Otherwise, target Kerbin.  Since we have LOS, blank the first occluding body.
+						else
+						{
+							log.AppendFormat("\n\t\tPicking Kerbin as target ({0} >= {1}).",
+								bestOccludedSqrDistance, kerbinSqrDistance);
+
+							this.KerbinDirect = true;
+							this.targetRelay = null;
+							this.firstOccludingBody = null;
+						}
+					}
+				}
+			}
+
+			log.AppendFormat("\n{0}: Status determination complete.", this.ToString());
+
+			Tools.PostLogMessage(log.ToString());
+
+			// Now that we're done with our recursive CanTransmit checks, flag this relay as not checked so it can be
+			// used next time.
+			RelayDatabase.Instance.CheckedVesselsTable.Remove(vessel.id);
+		}
+
+		public override string ToString()
+		{
+			if (this is ProtoAntennaRelay)
+			{
+				return (this as ProtoAntennaRelay).ToString();
+			}
+			return this.moduleRef.ToString();
+		}
+
+		/// <summary>
+		/// Initializes a new instance of the <see cref="AntennaRange.ProtoDataTransmitter"/> class.
+		/// </summary>
+		/// <param name="ms"><see cref="ProtoPartModuleSnapshot"/></param>
+		public AntennaRelay(IAntennaRelay module)
+		{
+			this.moduleRef = module;
+
+			this.searchTimer = new System.Diagnostics.Stopwatch();
+			this.millisecondsBetweenSearches = 125;
+		}
+	}
+}
+
+

file:b/ChangeLog (new)
--- /dev/null
+++ b/ChangeLog
@@ -1,1 +1,7 @@
+2014-01-14  toadicus  <>
 
+	* ModuleLimitedDataTransmitter.cs: Added a ":" to the
+	  transmission communications for consistency with stock
+	  behavior.
+
+

--- /dev/null
+++ b/GameData/AntennaRange/ATM_AntennaRange.cfg
@@ -1,1 +1,15 @@
-
+ACTIVE_TEXTURE_MANAGER_CONFIG
+{
+	folder = AntennaRange
+	enabled  = true
+	OVERRIDES
+	{
+		AntennaRange/.*
+		{
+			compress = true
+			mipmaps = false
+			scale = 1
+			max_size = 0
+		}
+	}
+}

--- /dev/null
+++ b/GameData/AntennaRange/AntennaRange.cfg
@@ -1,1 +1,129 @@
+// AntennaRange
+//
+// AntennaRange.cfg
+//
+// Copyright © 2014, toadicus
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without modification,
+// are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice,
+//    this list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation and/or other
+//    materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its contributors may be used
+//    to endorse or promote products derived from this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+//
+// This software uses the ModuleManager library © 2013 ialdabaoth, used under a Creative Commons Attribution-ShareAlike
+// 3.0 Uported License.
+//
+// Specifications:
+// nominalRange:	The distance from Kerbin at which the antenna will perform exactly as prescribed by
+//					packetResourceCost and packetSize.
+// maxPowerFactor:	The multiplier on packetResourceCost that defines the maximum power output of the antenna.  When the
+//					power cost exceeds packetResourceCost * maxPowerFactor, transmission will fail.
+// maxDataFactor:	The multipler on packetSize that defines the maximum data bandwidth of the antenna.
+// 
 
+@PART[longAntenna]:FOR[AntennaRange]:NEEDS[!RemoteTech2]
+{
+	@MODULE[ModuleDataTransmitter]
+	{
+		@name = ModuleLimitedDataTransmitter
+		nominalRange = 1500000
+		maxPowerFactor = 8
+		maxDataFactor = 4
+	}
+
+	MODULE
+	{
+		name = ModuleScienceContainer
+
+		dataIsCollectable = true
+		dataIsStorable = false
+
+		storageRange = 2
+	}
+}
+
+@PART[mediumDishAntenna]:FOR[AntennaRange]:NEEDS[!RemoteTech2]
+{
+	@MODULE[ModuleDataTransmitter]
+	{
+		@name = ModuleLimitedDataTransmitter
+		nominalRange = 30000000
+		maxPowerFactor = 8
+		maxDataFactor = 4
+	}
+
+	MODULE
+	{
+		name = ModuleScienceContainer
+
+		dataIsCollectable = true
+		dataIsStorable = false
+
+		storageRange = 2
+	}
+}
+
+@PART[commDish]:FOR[AntennaRange]:NEEDS[!RemoteTech2]
+{
+	@MODULE[ModuleDataTransmitter]
+	{
+		@name = ModuleLimitedDataTransmitter
+		nominalRange = 80000000000
+		maxPowerFactor = 8
+		maxDataFactor = 4
+	}
+
+	MODULE
+	{
+		name = ModuleScienceContainer
+
+		dataIsCollectable = true
+		dataIsStorable = false
+
+		storageRange = 2
+	}
+}
+
+EVA_MODULE
+{
+	name = ModuleLimitedDataTransmitter
+
+	nominalRange = 5000
+	maxPowerFactor = 1
+	maxDataFactor = 1
+
+	packetInterval = 0.2
+	packetSize = 1
+	packetResourceCost = 6.25
+
+	requiredResource = ElectricCharge
+}
+
+EVA_RESOURCE
+{
+	name = ElectricCharge
+	amount = 100
+	maxAmount = 100
+}
+
+@EVA_RESOURCE[ElectricCharge]:AFTER[AntennaRange]:NEEDS[TacLifeSupport]
+{
+	!name = DELETE
+}
+

 Binary files /dev/null and b/GameData/AntennaRange/Textures/appLauncherIcon.png differ
 Binary files /dev/null and b/GameData/AntennaRange/Textures/appLauncherIconNoConnection.png differ
 Binary files /dev/null and b/GameData/AntennaRange/Textures/appLauncherIconSubOptimal.png differ
 Binary files /dev/null and b/GameData/AntennaRange/Textures/toolbarIcon.png differ
 Binary files /dev/null and b/GameData/AntennaRange/Textures/toolbarIconNoConnection.png differ
 Binary files /dev/null and b/GameData/AntennaRange/Textures/toolbarIconSubOptimal.png differ
file:b/IAntennaRelay.cs (new)
--- /dev/null
+++ b/IAntennaRelay.cs
@@ -1,1 +1,96 @@
+// AntennaRange
+//
+// IAntennaRelay.cs
+//
+// Copyright © 2014, toadicus
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without modification,
+// are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice,
+//    this list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation and/or other
+//    materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its contributors may be used
+//    to endorse or promote products derived from this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
+using KSP;
+using System;
+
+namespace AntennaRange
+{
+	/*
+	 * Interface defining the basic functionality of AntennaRelay modules for AntennaRange.
+	 * */
+	public interface IAntennaRelay
+	{
+		/// <summary>
+		/// Gets the parent Vessel.
+		/// </summary>
+		/// <value>The parent Vessel.</value>
+		Vessel vessel { get; }
+
+		IAntennaRelay nearestRelay { get; }
+
+		IAntennaRelay bestOccludedRelay { get; }
+
+		IAntennaRelay targetRelay { get; }
+
+		/// <summary>
+		/// Gets the distance to the nearest relay or Kerbin, whichever is closer.
+		/// </summary>
+		/// <value>The distance to the nearest relay or Kerbin, whichever is closer.</value>
+		double transmitDistance { get; }
+
+		double nominalTransmitDistance { get; }
+
+		/// <summary>
+		/// The maximum distance at which this relay can operate.
+		/// </summary>
+		/// <value>The max transmit distance.</value>
+		float maxTransmitDistance { get; }
+
+		/// <summary>
+		/// The first CelestialBody blocking line of sight to a 
+		/// </summary>
+		/// <value>The first occluding body.</value>
+		CelestialBody firstOccludingBody { get; }
+
+		/// <summary>
+		/// Gets a value indicating whether this <see cref="AntennaRange.ProtoDataTransmitter"/> has been checked during
+		/// the current relay attempt.
+		/// </summary>
+		/// <value><c>true</c> if relay checked; otherwise, <c>false</c>.</value>
+		bool relayChecked { get; }
+
+		/// <summary>
+		/// Gets a value indicating whether this <see cref="AntennaRange.IAntennaRelay"/> Relay is communicating
+		/// directly with Kerbin.
+		/// </summary>
+		bool KerbinDirect { get; }
+
+		/// <summary>
+		/// Determines whether this instance can transmit.
+		/// </summary>
+		/// <returns><c>true</c> if this instance can transmit; otherwise, <c>false</c>.</returns>
+		bool CanTransmit();
+
+		string ToString();
+
+		string Title { get; }
+	}
+}
+
+

--- /dev/null
+++ b/ModuleLimitedDataTransmitter.cs
@@ -1,1 +1,796 @@
-
+// AntennaRange
+//
+// ModuleLimitedDataTransmitter.cs
+//
+// Copyright © 2014, toadicus
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without modification,
+// are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice,
+//    this list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation and/or other
+//    materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its contributors may be used
+//    to endorse or promote products derived from this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+using KSP;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using ToadicusTools;
+using UnityEngine;
+
+namespace AntennaRange
+{
+	/*
+	 * ModuleLimitedDataTransmitter is designed as a drop-in replacement for ModuleDataTransmitter, and handles range-
+	 * finding, power scaling, and data scaling for antennas during science transmission.  Its functionality varies with
+	 * three tunables: nominalRange, maxPowerFactor, and maxDataFactor, set in .cfg files.
+	 * 
+	 * In general, the scaling functions assume the following relation:
+	 * 
+	 *     D² α P/R,
+	 * 
+	 * where D is the total transmission distance, P is the transmission power, and R is the data rate.
+	 * 
+	 * */
+
+	/*
+	 * Fields
+	 * */
+	public class ModuleLimitedDataTransmitter : ModuleDataTransmitter, IScienceDataTransmitter, IAntennaRelay
+	{
+		// Stores the packetResourceCost as defined in the .cfg file.
+		protected float _basepacketResourceCost;
+
+		// Stores the packetSize as defined in the .cfg file.
+		protected float _basepacketSize;
+
+		// Every antenna is a relay.
+		protected AntennaRelay relay;
+
+		// Keep track of vessels with transmitters for relay purposes.
+		protected List<Vessel> _relayVessels;
+
+		// Sometimes we will need to communicate errors; this is how we do it.
+		protected ScreenMessage ErrorMsg;
+
+		// The distance from Kerbin at which the antenna will perform exactly as prescribed by packetResourceCost
+		// and packetSize.
+		[KSPField(isPersistant = false)]
+		public float nominalRange;
+
+		[KSPField(isPersistant = false, guiActive = true, guiName = "Status")]
+		public string UIrelayStatus;
+
+		[KSPField(isPersistant = false, guiActive = true, guiName = "Relay")]
+		public string UIrelayTarget;
+
+		[KSPField(isPersistant = false, guiActive = true, guiName = "Transmission Distance")]
+		public string UItransmitDistance;
+
+		[KSPField(isPersistant = false, guiActive = true, guiName = "Maximum Distance")]
+		public string UImaxTransmitDistance;
+
+		[KSPField(isPersistant = false, guiActive = true, guiName = "Packet Size")]
+		public string UIpacketSize;
+
+		[KSPField(isPersistant = false, guiActive = true, guiName = "Packet Cost")]
+		public string UIpacketCost;
+
+		// The multiplier on packetResourceCost that defines the maximum power output of the antenna.  When the power
+		// cost exceeds packetResourceCost * maxPowerFactor, transmission will fail.
+		[KSPField(isPersistant = false)]
+		public float maxPowerFactor;
+
+		// The multipler on packetSize that defines the maximum data bandwidth of the antenna.
+		[KSPField(isPersistant = false)]
+		public float maxDataFactor;
+
+		[KSPField(
+			isPersistant = true,
+			guiName = "Packet Throttle",
+			guiUnits = "%",
+			guiActive = true,
+			guiActiveEditor = false
+		)]
+		[UI_FloatRange(maxValue = 100f, minValue = 2.5f, stepIncrement = 2.5f)]
+		public float packetThrottle;
+
+		protected bool actionUIUpdate;
+
+		/*
+		 * Properties
+		 * */
+		// Returns the parent vessel housing this antenna.
+		public new Vessel vessel
+		{
+			get
+			{
+				if (base.vessel != null)
+				{
+					return base.vessel;
+				}
+				else if (this.part != null)
+				{
+					return this.part.vessel;
+				}
+
+				else
+				{
+					return null;
+				}
+			}
+		}
+
+		public IAntennaRelay nearestRelay
+		{
+			get
+			{
+				if (this.relay == null)
+				{
+					return null;
+				}
+
+				return this.relay.nearestRelay;
+			}
+		}
+
+		public IAntennaRelay bestOccludedRelay
+		{
+			get
+			{
+				if (this.relay == null)
+				{
+					return null;
+				}
+
+				return this.relay.bestOccludedRelay;
+			}
+		}
+
+		public IAntennaRelay targetRelay
+		{
+			get
+			{
+				if (this.relay == null)
+				{
+					return null;
+				}
+
+				return this.relay.targetRelay;
+			}
+		}
+
+		// Returns the distance to the nearest relay or Kerbin, whichever is closer.
+		public double transmitDistance
+		{
+			get
+			{
+				if (this.relay == null)
+				{
+					return double.PositiveInfinity;
+				}
+
+				return this.relay.transmitDistance;
+			}
+		}
+
+		public double nominalTransmitDistance
+		{
+			get
+			{
+				return this.nominalRange;
+			}
+		}
+
+		// Returns the maximum distance this module can transmit
+		public float maxTransmitDistance
+		{
+			get
+			{
+				// TODO: Cache this in a way that doesn't break everything.
+				return Mathf.Sqrt(this.maxPowerFactor) * this.nominalRange;
+			}
+		}
+
+		public CelestialBody firstOccludingBody
+		{
+			get
+			{
+				return this.relay.firstOccludingBody;
+			}
+		}
+
+		/*
+		 * The next two functions overwrite the behavior of the stock functions and do not perform equivalently, except
+		 * in that they both return floats.  Here's some quick justification:
+		 * 
+		 * The stock implementation of GetTransmitterScore (which I cannot override) is:
+		 * 		Score = (1 + DataResourceCost) / DataRate
+		 * 
+		 * The stock DataRate and DataResourceCost are:
+		 * 		DataRate = packetSize / packetInterval
+		 * 		DataResourceCost = packetResourceCost / packetSize
+		 * 
+		 * So, the resulting score is essentially in terms of joules per byte per baud.  Rearranging that a bit, it
+		 * could also look like joule-seconds per byte per byte, or newton-meter-seconds per byte per byte.  Either way,
+		 * that metric is not a very reasonable one.
+		 * 
+		 * Two metrics that might make more sense are joules per byte or joules per byte per second.  The latter case
+		 * would look like:
+		 * 		DataRate = packetSize / packetInterval
+		 * 		DataResourceCost = packetResourceCost
+		 * 
+		 * The former case, which I've chosen to implement below, is:
+		 * 		DataRate = packetSize
+		 * 		DataResourceCost = packetResourceCost
+		 * 
+		 * So... hopefully that doesn't screw with anything else.
+		 * */
+		// Override ModuleDataTransmitter.DataRate to just return packetSize, because we want antennas to be scored in
+		// terms of joules/byte
+		public new float DataRate
+		{
+			get
+			{
+				this.PreTransmit_SetPacketSize();
+
+				if (this.CanTransmit())
+				{
+					return this.packetSize;
+				}
+				else
+				{
+					return float.Epsilon;
+				}
+			}
+		}
+
+		// Override ModuleDataTransmitter.DataResourceCost to just return packetResourceCost, because we want antennas
+		// to be scored in terms of joules/byte
+		public new float DataResourceCost
+		{
+			get
+			{
+				this.PreTransmit_SetPacketResourceCost();
+
+				if (this.CanTransmit())
+				{
+					return this.packetResourceCost;
+				}
+				else
+				{
+					return float.PositiveInfinity;
+				}
+			}
+		}
+
+		// Reports whether this antenna has been checked as a viable relay already in the current FindNearestRelay.
+		public bool relayChecked
+		{
+			get
+			{
+				if (this.relay != null)
+				{
+					return this.relay.relayChecked;
+				}
+
+				// If our relay is null, always return null so we're never checked.
+				return true;
+			}
+		}
+
+		public bool KerbinDirect
+		{
+			get
+			{
+				if (this.relay != null)
+				{
+					return this.relay.KerbinDirect;
+				}
+
+				return false;
+			}
+		}
+
+		public string Title
+		{
+			get
+			{
+				if (this.part != null && this.part.partInfo != null)
+				{
+					return this.part.partInfo.title;
+				}
+
+				return string.Empty;
+			}
+		}
+
+		/*
+		 * Methods
+		 * */
+		// Build ALL the objects.
+		public ModuleLimitedDataTransmitter () : base()
+		{
+			this.ErrorMsg = new ScreenMessage("", 4f, false, ScreenMessageStyle.UPPER_LEFT);
+			this.packetThrottle = 100f;
+		}
+
+		public override void OnAwake()
+		{
+			base.OnAwake();
+
+			this._basepacketSize = base.packetSize;
+			this._basepacketResourceCost = base.packetResourceCost;
+
+			Tools.PostDebugMessage(string.Format(
+				"{0} loaded:\n" +
+				"packetSize: {1}\n" +
+				"packetResourceCost: {2}\n" +
+				"nominalRange: {3}\n" +
+				"maxPowerFactor: {4}\n" +
+				"maxDataFactor: {5}\n",
+				this.name,
+				base.packetSize,
+				this._basepacketResourceCost,
+				this.nominalRange,
+				this.maxPowerFactor,
+				this.maxDataFactor
+			));
+		}
+
+		// At least once, when the module starts with a state on the launch pad or later, go find Kerbin.
+		public override void OnStart (StartState state)
+		{
+			base.OnStart (state);
+
+			if (state >= StartState.PreLaunch)
+			{
+				this.relay = new AntennaRelay(this);
+				this.relay.maxTransmitDistance = this.maxTransmitDistance;
+				this.relay.nominalTransmitDistance = this.nominalRange;
+
+				this.UImaxTransmitDistance = Tools.MuMech_ToSI(this.maxTransmitDistance) + "m";
+
+				GameEvents.onPartActionUICreate.Add(this.onPartActionUICreate);
+				GameEvents.onPartActionUIDismiss.Add(this.onPartActionUIDismiss);
+			}
+		}
+
+		// When the module loads, fetch the Squad KSPFields from the base.  This is necessary in part because
+		// overloading packetSize and packetResourceCostinto a property in ModuleLimitedDataTransmitter didn't
+		// work.
+		public override void OnLoad(ConfigNode node)
+		{
+			this.Fields.Load(node);
+			base.Fields.Load(node);
+
+			base.OnLoad (node);
+		}
+
+		// Post an error in the communication messages describing the reason transmission has failed.  Currently there
+		// is only one reason for this.
+		protected void PostCannotTransmitError()
+		{
+			string ErrorText = string.Intern("Unable to transmit: no visible receivers in range!");
+
+			this.ErrorMsg.message = string.Format(
+				"<color='#{0}{1}{2}{3}'><b>{4}</b></color>",
+				((int)(XKCDColors.OrangeRed.r * 255f)).ToString("x2"),
+				((int)(XKCDColors.OrangeRed.g * 255f)).ToString("x2"),
+				((int)(XKCDColors.OrangeRed.b * 255f)).ToString("x2"),
+				((int)(XKCDColors.OrangeRed.a * 255f)).ToString("x2"),
+				ErrorText
+			);
+
+			Tools.PostDebugMessage(this.GetType().Name + ": " + this.ErrorMsg.message);
+
+			ScreenMessages.PostScreenMessage(this.ErrorMsg, false);
+		}
+
+		// Before transmission, set packetResourceCost.  Per above, packet cost increases with the square of
+		// distance.  packetResourceCost maxes out at _basepacketResourceCost * maxPowerFactor, at which point
+		// transmission fails (see CanTransmit).
+		protected void PreTransmit_SetPacketResourceCost()
+		{
+			if (ARConfiguration.FixedPowerCost || this.transmitDistance <= this.nominalRange)
+			{
+				base.packetResourceCost = this._basepacketResourceCost;
+			}
+			else
+			{
+				double rangeFactor = (this.transmitDistance / this.nominalRange);
+				rangeFactor *= rangeFactor;
+
+				base.packetResourceCost = this._basepacketResourceCost
+					* (float)rangeFactor;
+
+				Tools.PostDebugMessage(
+					this,
+					"Pretransmit: packet cost set to {0} before throttle (rangeFactor = {1}).",
+					base.packetResourceCost,
+					rangeFactor);
+			}
+
+			base.packetResourceCost *= this.packetThrottle / 100f;
+		}
+
+		// Before transmission, set packetSize.  Per above, packet size increases with the inverse square of
+		// distance.  packetSize maxes out at _basepacketSize * maxDataFactor.
+		protected void PreTransmit_SetPacketSize()
+		{
+			if (!ARConfiguration.FixedPowerCost && this.transmitDistance >= this.nominalRange)
+			{
+				base.packetSize = this._basepacketSize;
+			}
+			else
+			{
+				double rangeFactor = (this.nominalRange / this.transmitDistance);
+				rangeFactor *= rangeFactor;
+
+				base.packetSize = Math.Min(
+					this._basepacketSize * (float)rangeFactor,
+					this._basepacketSize * this.maxDataFactor);
+
+				Tools.PostDebugMessage(
+					this,
+					"Pretransmit: packet size set to {0} before throttle (rangeFactor = {1}).",
+					base.packetSize,
+					rangeFactor);
+			}
+
+			base.packetSize *= this.packetThrottle / 100f;
+		}
+
+		// Override ModuleDataTransmitter.GetInfo to add nominal and maximum range to the VAB description.
+		public override string GetInfo()
+		{
+			string text = base.GetInfo();
+			text += "Nominal Range: " + Tools.MuMech_ToSI((double)this.nominalRange, 2) + "m\n";
+			text += "Maximum Range: " + Tools.MuMech_ToSI((double)this.maxTransmitDistance, 2) + "m\n";
+			return text;
+		}
+
+		// Override ModuleDataTransmitter.CanTransmit to return false when transmission is not possible.
+		public new bool CanTransmit()
+		{
+			if (this.part == null || this.relay == null)
+			{
+				return false;
+			}
+
+			PartStates partState = this.part.State;
+			if (partState == PartStates.DEAD || partState == PartStates.DEACTIVATED)
+			{
+				Tools.PostDebugMessage(string.Format(
+					"{0}: {1} on {2} cannot transmit: {3}",
+					this.GetType().Name,
+					this.part.partInfo.title,
+					this.vessel.vesselName,
+					Enum.GetName(typeof(PartStates), partState)
+				));
+				return false;
+			}
+			return this.relay.CanTransmit();
+		}
+
+		// Override ModuleDataTransmitter.TransmitData to check against CanTransmit and fail out when CanTransmit
+		// returns false.
+		public new void TransmitData(List<ScienceData> dataQueue)
+		{
+			this.PreTransmit_SetPacketSize();
+			this.PreTransmit_SetPacketResourceCost();
+
+			if (this.CanTransmit())
+			{
+				StringBuilder message = new StringBuilder();
+
+				message.Append("[");
+				message.Append(base.part.partInfo.title);
+				message.Append("]: ");
+
+				message.Append("Beginning transmission ");
+
+				// @DONE TODO: Fix this to fall back to Kerbin if nearestRelay cannot be contacted.
+				// @DONE TODO: Remove nearestRelay == null
+				if (this.KerbinDirect)
+				{
+					message.Append("directly to Kerbin.");
+				}
+				else
+				{
+					message.Append("via ");
+					message.Append(this.relay.nearestRelay);
+				}
+
+				ScreenMessages.PostScreenMessage(message.ToString(), 4f, ScreenMessageStyle.UPPER_LEFT);
+
+				base.TransmitData(dataQueue);
+			}
+			else
+			{
+				Tools.PostDebugMessage(this, "{0} unable to transmit during TransmitData.", this.part.partInfo.title);
+
+				var logger = Tools.DebugLogger.New(this);
+
+				foreach (ModuleScienceContainer	scienceContainer in this.vessel.getModulesOfType<ModuleScienceContainer>())
+				{
+					logger.AppendFormat("Checking ModuleScienceContainer in {0}\n",
+						scienceContainer.part.partInfo.title);
+
+					if (
+						scienceContainer.capacity != 0 &&
+						scienceContainer.GetScienceCount() >= scienceContainer.capacity
+					)
+					{
+						logger.Append("\tInsufficient capacity, skipping.\n");
+						continue;
+					}
+
+					List<ScienceData> dataStored = new List<ScienceData>();
+
+					foreach (ScienceData data in dataQueue)
+					{
+						if (!scienceContainer.allowRepeatedSubjects && scienceContainer.HasData(data))
+						{
+							logger.Append("\tAlready contains subject and repeated subjects not allowed, skipping.\n");
+							continue;
+						}
+
+						logger.AppendFormat("\tAcceptable, adding data on subject {0}... ", data.subjectID);
+						if (scienceContainer.AddData(data))
+						{
+							logger.Append("done, removing from queue.\n");
+
+							dataStored.Add(data);
+						}
+						#if DEBUG
+						else
+						{
+							logger.Append("failed.\n");
+						}
+						#endif
+					}
+
+					dataQueue.RemoveAll(i => dataStored.Contains(i));
+
+					logger.AppendFormat("\t{0} data left in queue.", dataQueue.Count);
+				}
+
+				logger.Print();
+
+				if (dataQueue.Count > 0)
+				{
+					StringBuilder msg = new StringBuilder();
+
+					msg.Append('[');
+					msg.Append(this.part.partInfo.title);
+					msg.AppendFormat("]: {0} data items could not be saved: no space available in data containers.\n");
+					msg.Append("Data to be discarded:\n");
+
+					foreach (ScienceData data in dataQueue)
+					{
+						msg.AppendFormat("\n{0}\n", data.title);
+					}
+
+					ScreenMessages.PostScreenMessage(msg.ToString(), 4f, ScreenMessageStyle.UPPER_LEFT);
+
+					Tools.PostDebugMessage(msg.ToString());
+				}
+
+				this.PostCannotTransmitError ();
+			}
+
+			Tools.PostDebugMessage (
+				"distance: " + this.transmitDistance
+				+ " packetSize: " + this.packetSize
+				+ " packetResourceCost: " + this.packetResourceCost
+			);
+		}
+
+		// Override ModuleDataTransmitter.StartTransmission to check against CanTransmit and fail out when CanTransmit
+		// returns false.
+		public new void StartTransmission()
+		{
+			PreTransmit_SetPacketSize ();
+			PreTransmit_SetPacketResourceCost ();
+
+			Tools.PostDebugMessage (
+				"distance: " + this.transmitDistance
+				+ " packetSize: " + this.packetSize
+				+ " packetResourceCost: " + this.packetResourceCost
+				);
+
+			if (this.CanTransmit())
+			{
+				StringBuilder message = new StringBuilder();
+
+				message.Append("[");
+				message.Append(base.part.partInfo.title);
+				message.Append("]: ");
+
+				message.Append("Beginning transmission ");
+
+				// @DONE TODO: Fix this to fall back to Kerbin if nearestRelay cannot be contacted.
+				// @DONE TODO: Remove nearestRelay == null
+				if (this.KerbinDirect)
+				{
+					message.Append("directly to Kerbin.");
+				}
+				else
+				{
+					message.Append("via ");
+					message.Append(this.relay.nearestRelay);
+				}
+
+				ScreenMessages.PostScreenMessage(message.ToString(), 4f, ScreenMessageStyle.UPPER_LEFT);
+
+				base.StartTransmission();
+			}
+			else
+			{
+				this.PostCannotTransmitError ();
+			}
+		}
+
+		public void Update()
+		{
+			if (this.actionUIUpdate)
+			{
+				if (this.CanTransmit())
+				{
+					this.UIrelayStatus = "Connected";
+					this.UItransmitDistance = Tools.MuMech_ToSI(this.transmitDistance) + "m";
+					this.UIpacketSize = Tools.MuMech_ToSI(this.DataRate) + "MiT";
+					this.UIpacketCost = Tools.MuMech_ToSI(this.DataResourceCost) + "E";
+				}
+				else
+				{
+					if (this.relay.firstOccludingBody == null)
+					{
+						this.UIrelayStatus = "Out of range";
+					}
+					else
+					{
+						this.UIrelayStatus = string.Format("Blocked by {0}", this.relay.firstOccludingBody.bodyName);
+					}
+					this.UImaxTransmitDistance = "N/A";
+					this.UIpacketSize = "N/A";
+					this.UIpacketCost = "N/A";
+				}
+
+				if (this.KerbinDirect)
+				{
+					this.UIrelayTarget = AntennaRelay.Kerbin.bodyName;
+				}
+				else
+				{
+					this.UIrelayTarget = this.targetRelay.ToString();
+				}
+			}
+		}
+
+		public void onPartActionUICreate(Part eventPart)
+		{
+			if (eventPart == base.part)
+			{
+				this.actionUIUpdate = true;
+			}
+		}
+
+		public void onPartActionUIDismiss(Part eventPart)
+		{
+			if (eventPart == base.part)
+			{
+				this.actionUIUpdate = false;
+			}
+		}
+
+		public override string ToString()
+		{
+			StringBuilder msg = new StringBuilder();
+
+			msg.Append(this.part.partInfo.title);
+
+			if (vessel != null)
+			{
+				msg.Append(" on ");
+				msg.Append(vessel.vesselName);
+			}
+			else if (
+				this.part != null &&
+				this.part.protoPartSnapshot != null &&
+				this.part.protoPartSnapshot != null &&
+				this.part.protoPartSnapshot.pVesselRef != null
+			)
+			{
+				msg.Append(" on ");
+				msg.Append(this.part.protoPartSnapshot.pVesselRef.vesselName);
+			}
+
+			return msg.ToString();
+		}
+
+		// When debugging, it's nice to have a button that just tells you everything.
+
+		[KSPEvent (guiName = "Show Debug Info", active = true, guiActive = true)]
+		public void DebugInfo()
+		{
+			PreTransmit_SetPacketSize ();
+			PreTransmit_SetPacketResourceCost ();
+
+			string msg = string.Format(
+				"'{0}'\n" + 
+				"_basepacketSize: {1}\n" +
+				"packetSize: {2}\n" +
+				"_basepacketResourceCost: {3}\n" +
+				"packetResourceCost: {4}\n" +
+				"maxTransmitDistance: {5}\n" +
+				"transmitDistance: {6}\n" +
+				"nominalRange: {7}\n" +
+				"CanTransmit: {8}\n" +
+				"DataRate: {9}\n" +
+				"DataResourceCost: {10}\n" +
+				"TransmitterScore: {11}\n" +
+				"NearestRelay: {12}\n" +
+				"BestOccludedRelay: {13}\n" +
+				"KerbinDirect: {14}\n" +
+				"Vessel ID: {15}",
+				this.name,
+				this._basepacketSize,
+				base.packetSize,
+				this._basepacketResourceCost,
+				base.packetResourceCost,
+				this.maxTransmitDistance,
+				this.transmitDistance,
+				this.nominalRange,
+				this.CanTransmit(),
+				this.DataRate,
+				this.DataResourceCost,
+				ScienceUtil.GetTransmitterScore(this),
+				this.relay.nearestRelay == null ? "null" : this.relay.nearestRelay.ToString(),
+				this.relay.bestOccludedRelay == null ? "null" : this.relay.bestOccludedRelay.ToString(),
+				this.KerbinDirect,
+				this.vessel.id
+				);
+
+			Tools.PostLogMessage(msg);
+		}
+
+		[KSPEvent (guiName = "Dump Vessels", active = true, guiActive = true)]
+		public void PrintAllVessels()
+		{
+			StringBuilder sb = new StringBuilder();
+
+			sb.Append("Dumping FlightGlobals.Vessels:");
+
+			foreach (Vessel vessel in FlightGlobals.Vessels)
+			{
+				sb.AppendFormat("\n'{0} ({1})'", vessel.vesselName, vessel.id);
+			}
+
+			Tools.PostDebugMessage(sb.ToString());
+		}
+
+		/*[KSPEvent (guiName = "Dump RelayDB", active = true, guiActive = true)]
+		public void DumpRelayDB()
+		{
+			RelayDatabase.Instance.Dump();
+		}*/
+	}
+}

--- /dev/null
+++ b/Properties/AssemblyInfo.cs
@@ -1,1 +1,47 @@
+// AntennaRange
+//
+// AssemblyInfo.cs
+//
+// Copyright © 2014, toadicus
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without modification,
+// are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice,
+//    this list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation and/or other
+//    materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its contributors may be used
+//    to endorse or promote products derived from this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
+using System.Reflection;
+using System.Runtime.CompilerServices;
+
+[assembly: KSPAssemblyDependency("ToadicusTools", 0, 0)]
+
+// Information about this assembly is defined by the following attributes.
+// Change them to the values specific to your project.
+[assembly: AssemblyTitle("AntennaRange")]
+[assembly: AssemblyDescription("Enforce and Encourage Antenna Diversity")]
+[assembly: AssemblyCopyright("toadicus")]
+// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}".
+// The form "{Major}.{Minor}.*" will automatically update the build and revision,
+// and "{Major}.{Minor}.{Build}.*" will update just the revision.
+[assembly: AssemblyVersion("1.8.*")]
+// The following attributes are used to specify the signing key for the assembly,
+// if desired. See the Mono documentation for more information about signing.
+//[assembly: AssemblyDelaySign(false)]
+//[assembly: AssemblyKeyFile("")]
+

--- /dev/null
+++ b/Properties/ChangeLog
@@ -1,1 +1,5 @@
+2014-01-14  toadicus  <>
 
+	* AssemblyInfo.cs: New AssemblyInfo file for reason.
+
+

--- /dev/null
+++ b/ProtoAntennaRelay.cs
@@ -1,1 +1,147 @@
+// AntennaRange
+//
+// ProtoAntennaRelay.cs
+//
+// Copyright © 2014, toadicus
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without modification,
+// are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice,
+//    this list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation and/or other
+//    materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its contributors may be used
+//    to endorse or promote products derived from this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
+using KSP;
+using System;
+using System.Linq;
+using ToadicusTools;
+
+namespace AntennaRange
+{
+	/*
+	 * Wrapper class for ProtoPartModuleSnapshot extending AntennaRelay and implementing IAntennaRelay.
+	 * This is used for finding relays in unloaded Vessels.
+	 * */
+	public class ProtoAntennaRelay : AntennaRelay, IAntennaRelay
+	{
+		// Stores the prototype part so we can make sure we haven't exploded or so.
+		protected ProtoPartSnapshot protoPart;
+
+		public override Vessel vessel
+		{
+			get
+			{
+				return this.protoPart.pVesselRef.vesselRef;
+			}
+		}
+
+		public override double nominalTransmitDistance
+		{
+			get
+			{
+				return this.moduleRef.nominalTransmitDistance;
+			}
+		}
+
+		/// <summary>
+		/// The maximum distance at which this transmitter can operate.
+		/// </summary>
+		/// <value>The max transmit distance.</value>
+		public override float maxTransmitDistance
+		{
+			get
+			{
+				return moduleRef.maxTransmitDistance;
+			}
+		}
+
+		/// <summary>
+		/// Gets a value indicating whether this <see cref="AntennaRange.ProtoDataTransmitter"/> has been checked during
+		/// the current relay attempt.
+		/// </summary>
+		/// <value><c>true</c> if relay checked; otherwise, <c>false</c>.</value>
+		public override bool relayChecked
+		{
+			get;
+			protected set;
+		}
+
+		/// <summary>
+		/// Gets the underlying part's title.
+		/// </summary>
+		/// <value>The title.</value>
+		public string Title
+		{
+			get
+			{
+				if (this.protoPart != null && this.protoPart.partInfo != null)
+				{
+					return this.protoPart.partInfo.title;
+				}
+
+				return string.Empty;
+			}
+		}
+
+		public override bool CanTransmit()
+		{
+			PartStates partState = (PartStates)this.protoPart.state;
+			if (partState == PartStates.DEAD || partState == PartStates.DEACTIVATED)
+			{
+				Tools.PostDebugMessage(string.Format(
+					"{0}: {1} on {2} cannot transmit: {3}",
+					this.GetType().Name,
+					this.Title,
+					this.vessel.vesselName,
+					Enum.GetName(typeof(PartStates), partState)
+				));
+				return false;
+			}
+			return base.CanTransmit();
+		}
+
+		public override string ToString()
+		{
+			return string.Format(
+				"{0} on {1}",
+				this.Title,
+				this.protoPart.pVesselRef.vesselName
+			);
+		}
+
+		/// <summary>
+		/// Initializes a new instance of the <see cref="AntennaRange.ProtoAntennaRelay"/> class.
+		/// </summary>
+		/// <param name="ms">The ProtoPartModuleSnapshot to wrap</param>
+		/// <param name="vessel">The parent Vessel</param>
+		public ProtoAntennaRelay(IAntennaRelay prefabRelay, ProtoPartSnapshot pps) : base(prefabRelay)
+		{
+			this.protoPart = pps;
+		}
+
+		~ProtoAntennaRelay()
+		{
+			Tools.PostDebugMessage(string.Format(
+				"{0}: destroyed",
+				this.ToString()
+			));
+		}
+	}
+}
+
+

file:b/RelayDatabase.cs (new)
--- /dev/null
+++ b/RelayDatabase.cs
@@ -1,1 +1,408 @@
-
+// AntennaRange
+//
+// RelayDatabase.cs
+//
+// Copyright © 2014, toadicus
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without modification,
+// are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice,
+//    this list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation and/or other
+//    materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its contributors may be used
+//    to endorse or promote products derived from this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+using KSP;
+using System;
+using System.Collections.Generic;
+using System.Text;
+using ToadicusTools;
+using UnityEngine;
+
+namespace AntennaRange
+{
+	public class RelayDatabase
+	{
+		/*
+		 * Static members
+		 * */
+		// Singleton storage
+		protected static RelayDatabase _instance;
+		// Gets the singleton
+		public static RelayDatabase Instance
+		{
+			get
+			{	
+				if (_instance == null)
+				{
+					_instance = new RelayDatabase();
+				}
+
+				return _instance;
+			}
+		}
+
+		/*
+		 * Instance members
+		 * */
+
+		/*
+		 * Fields
+		 * */
+		// Vessel.id-keyed hash table of Part.GetHashCode()-keyed tables of relay objects.
+		protected Dictionary<Guid, Dictionary<int, IAntennaRelay>> relayDatabase;
+
+		// Vessel.id-keyed hash table of part counts, used for caching
+		protected Dictionary<Guid, int> vesselPartCountTable;
+
+		// Vessel.id-keyed hash table of booleans to track what vessels have been checked so far this time.
+		public Dictionary<Guid, bool> CheckedVesselsTable;
+
+		protected int cacheHits;
+		protected int cacheMisses;
+
+		/*
+		 * Properties
+		 * */
+		// Gets the Part-hashed table of relays in a given vessel
+		public Dictionary<int, IAntennaRelay> this [Vessel vessel]
+		{
+			get
+			{
+				// If we don't have an entry for this vessel...
+				if (!this.ContainsKey(vessel.id))
+				{
+					// ...Generate an entry for this vessel.
+					this.AddVessel(vessel);
+					this.cacheMisses++;
+				}
+				// If our part count disagrees with the vessel's part count...
+				else if (this.vesselPartCountTable[vessel.id] != vessel.Parts.Count)
+				{
+					// ...Update the our vessel in the cache
+					this.UpdateVessel(vessel);
+					this.cacheMisses++;
+				}
+				// Otherwise, it's a hit
+				else
+				{
+					this.cacheHits++;
+				}
+
+				// Return the Part-hashed table of relays for this vessel
+				return relayDatabase[vessel.id];
+			}
+		}
+
+		/* 
+		 * Methods
+		 * */
+		// Adds a vessel to the database
+		// The return for this function isn't used yet, but seems useful for potential future API-uses
+		public bool AddVessel(Vessel vessel)
+		{
+			// If this vessel is already here...
+			if (this.ContainsKey(vessel))
+			{
+				// ...post an error
+				Debug.LogWarning(string.Format(
+					"{0}: Cannot add vessel '{1}' (id: {2}): Already in database.",
+					this.GetType().Name,
+					vessel.vesselName,
+					vessel.id
+				));
+
+				// ...and refuse to add
+				return false;
+			}
+			// otherwise, add the vessel to our tables...
+			else
+			{
+				// Build an empty table...
+				this.relayDatabase[vessel.id] = new Dictionary<int, IAntennaRelay>();
+
+				// Update the empty index
+				this.UpdateVessel(vessel);
+
+				// Return success
+				return true;
+			}
+		}
+
+		// Update the vessel's entry in the table
+		public void UpdateVessel(Vessel vessel)
+		{
+			// Squak if the database doesn't have the vessel
+			if (!this.ContainsKey(vessel))
+			{
+				throw new InvalidOperationException(string.Format(
+					"{0}: Update called for vessel '{1}' (id: {2}) not in database: vessel will be added.",
+					this.GetType().Name,
+					vessel.vesselName,
+					vessel.id
+				));
+			}
+
+			Dictionary<int, IAntennaRelay> vesselTable = this.relayDatabase[vessel.id];
+
+			// Actually build and assign the table
+			this.getVesselRelays(vessel, ref vesselTable);
+			// Set the part count
+			this.vesselPartCountTable[vessel.id] = vessel.Parts.Count;
+		}
+
+		// Remove a vessel from the cache, if it exists.
+		public void DirtyVessel(Vessel vessel)
+		{
+			if (this.relayDatabase.ContainsKey(vessel.id))
+			{
+				this.relayDatabase.Remove(vessel.id);
+			}
+			if (this.vesselPartCountTable.ContainsKey(vessel.id))
+			{
+				this.vesselPartCountTable.Remove(vessel.id);
+			}
+		}
+
+		// Returns true if both the relayDatabase and the vesselPartCountDB contain the vessel id.
+		public bool ContainsKey(Guid key)
+		{
+			return this.relayDatabase.ContainsKey(key);
+		}
+
+		// Returns true if both the relayDatabase and the vesselPartCountDB contain the vessel.
+		public bool ContainsKey(Vessel vessel)
+		{
+			return this.ContainsKey(vessel.id);
+		}
+
+		// Runs when a vessel is modified (or when we switch to one, to catch docking events)
+		public void onVesselEvent(Vessel vessel)
+		{
+			// If we have this vessel in our cache...
+			if (this.ContainsKey(vessel))
+			{
+				// If our part counts disagree (such as if a part has been added or broken off,
+				// or if we've just docked or undocked)...
+				if (this.vesselPartCountTable[vessel.id] != vessel.Parts.Count || vessel.loaded)
+				{
+					Tools.PostDebugMessage(string.Format(
+						"{0}: dirtying cache for vessel '{1}' ({2}).",
+						this.GetType().Name,
+						vessel.vesselName,
+						vessel.id
+					));
+
+					// Dirty the cache (real vessels will never have negative part counts)
+					this.DirtyVessel(vessel);
+				}
+			}
+		}
+
+		// Runs when the player requests a scene change, such as when changing vessels or leaving flight.
+		public void onSceneChange(GameScenes scene)
+		{
+			// If the active vessel is a real thing...
+			if (FlightGlobals.ActiveVessel != null)
+			{
+				// ... dirty its cache
+				this.onVesselEvent(FlightGlobals.ActiveVessel);
+			}
+		}
+
+		// Runs when parts are undocked
+		public void onPartEvent(Part part)
+		{
+			if (part != null && part.vessel != null)
+			{
+				this.onVesselEvent(part.vessel);
+			}
+		}
+
+		// Runs when parts are coupled, as in docking
+		public void onFromPartToPartEvent(GameEvents.FromToAction<Part, Part> data)
+		{
+			this.onPartEvent(data.from);
+			this.onPartEvent(data.to);
+		}
+
+		// Produce a Part-hashed table of relays for the given vessel
+		protected void getVesselRelays(Vessel vessel, ref Dictionary<int, IAntennaRelay> relays)
+		{
+			// We're going to completely regen this table, so dump the current contents.
+			relays.Clear();
+
+			Tools.PostDebugMessage(string.Format(
+				"{0}: Getting antenna relays from vessel {1}.",
+				"IAntennaRelay",
+				vessel.vesselName
+			));
+
+			// If the vessel is loaded, we can fetch modules implementing IAntennaRelay directly.
+			if (vessel.loaded) {
+				Tools.PostDebugMessage(string.Format(
+					"{0}: vessel {1} is loaded, searching for modules in loaded parts.",
+					"IAntennaRelay",
+					vessel.vesselName
+				));
+
+				// Loop through the Parts in the Vessel...
+				foreach (Part part in vessel.Parts)
+				{
+					// ...loop through the PartModules in the Part...
+					foreach (PartModule module in part.Modules)
+					{
+						// ...if the module is a relay...
+						if (module is IAntennaRelay)
+						{
+							// ...add the module to the table
+							relays.Add(part.GetHashCode(), module as IAntennaRelay);
+							// ...neglect relay objects after the first in each part.
+							break;
+						}
+					}
+				}
+			}
+			// If the vessel is not loaded, we need to build ProtoAntennaRelays when we find relay ProtoPartSnapshots.
+			else
+			{
+				Tools.PostDebugMessage(string.Format(
+					"{0}: vessel {1} is not loaded, searching for modules in prototype parts.",
+					this.GetType().Name,
+					vessel.vesselName
+				));
+
+				// Loop through the ProtoPartModuleSnapshots in the Vessel...
+				foreach (ProtoPartSnapshot pps in vessel.protoVessel.protoPartSnapshots)
+				{
+					Tools.PostDebugMessage(string.Format(
+						"{0}: Searching in protopartsnapshot {1}",
+						this.GetType().Name,
+						pps
+					));
+
+					// ...Fetch the prefab, because it's more useful for what we're doing.
+					Part partPrefab = PartLoader.getPartInfoByName(pps.partName).partPrefab;
+
+					Tools.PostDebugMessage(string.Format(
+						"{0}: Got partPrefab {1} in protopartsnapshot {2}",
+						this.GetType().Name,
+						partPrefab,
+						pps
+					));
+
+					// ...loop through the PartModules in the prefab...
+					foreach (PartModule module in partPrefab.Modules)
+					{
+						Tools.PostDebugMessage(string.Format(
+							"{0}: Searching in partmodule {1}",
+							this.GetType().Name,
+							module
+						));
+
+						// ...if the module is a relay...
+						if (module is IAntennaRelay)
+						{
+							Tools.PostDebugMessage(string.Format(
+								"{0}: partmodule {1} is antennarelay",
+								this.GetType().Name,
+								module
+							));
+
+							// ...build a new ProtoAntennaRelay and add it to the table
+							relays.Add(pps.GetHashCode(), new ProtoAntennaRelay(module as IAntennaRelay, pps));
+							// ...neglect relay objects after the first in each part.
+							break;
+						}
+					}
+				}
+			}
+
+			Tools.PostDebugMessage(string.Format(
+				"{0}: vessel '{1}' ({2}) has {3} transmitters.",
+				"IAntennaRelay",
+				vessel.vesselName,
+				vessel.id,
+				relays.Count
+			));
+		}
+
+		// Construct the singleton
+		protected RelayDatabase()
+		{
+			// Initialize the databases
+			this.relayDatabase = new Dictionary<Guid, Dictionary<int, IAntennaRelay>>();
+			this.vesselPartCountTable = new Dictionary<Guid, int>();
+			this.CheckedVesselsTable = new Dictionary<Guid, bool>();
+
+			this.cacheHits = 0;
+			this.cacheMisses = 0;
+
+			// Subscribe to some events
+			GameEvents.onVesselWasModified.Add(this.onVesselEvent);
+			GameEvents.onVesselChange.Add(this.onVesselEvent);
+			GameEvents.onVesselDestroy.Add(this.onVesselEvent);
+			GameEvents.onGameSceneLoadRequested.Add(this.onSceneChange);
+			GameEvents.onPartCouple.Add(this.onFromPartToPartEvent);
+			GameEvents.onPartUndock.Add(this.onPartEvent);
+		}
+
+		~RelayDatabase()
+		{
+			// Unsubscribe from the events
+			GameEvents.onVesselWasModified.Remove(this.onVesselEvent);
+			GameEvents.onVesselChange.Remove(this.onVesselEvent);
+			GameEvents.onVesselDestroy.Remove(this.onVesselEvent);
+			GameEvents.onGameSceneLoadRequested.Remove(this.onSceneChange);
+			GameEvents.onPartCouple.Remove(this.onFromPartToPartEvent);
+			GameEvents.onPartUndock.Remove(this.onPartEvent);
+
+			Tools.PostDebugMessage(this.GetType().Name + " destroyed.");
+
+			KSPLog.print(string.Format(
+				"{0} destructed.  Cache hits: {1}, misses: {2}, hit rate: {3:P1}",
+				this.GetType().Name,
+				this.cacheHits,
+				this.cacheMisses,
+				(float)this.cacheHits / (float)(this.cacheMisses + this.cacheHits)
+			));
+		}
+
+		#if DEBUG
+		public void Dump()
+		{
+			StringBuilder sb = new StringBuilder();
+
+			sb.Append("Dumping RelayDatabase:");
+
+			foreach (Guid id in this.relayDatabase.Keys)
+			{
+				sb.AppendFormat("\nVessel {0}:", id);
+
+				foreach (IAntennaRelay relay in this.relayDatabase[id].Values)
+				{
+					sb.AppendFormat("\n\t{0}", relay.ToString());
+				}
+			}
+
+			Tools.PostDebugMessage(sb.ToString());
+		}
+		#endif
+	}
+}
+
+

--- /dev/null
+++ b/RelayExtensions.cs
@@ -1,1 +1,115 @@
+// AntennaRange
+//
+// Extensions.cs
+//
+// Copyright © 2014, toadicus
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without modification,
+// are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice,
+//    this list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation and/or other
+//    materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its contributors may be used
+//    to endorse or promote products derived from this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using ToadicusTools;
+
+namespace AntennaRange
+{
+	/*
+	 * A class of utility extensions for Vessels and Relays to help find a relay path back to Kerbin.
+	 * */
+	public static class RelayExtensions
+	{
+		/// <summary>
+		/// Returns the distance between this IAntennaRelay and a Vessel
+		/// </summary>
+		/// <param name="relay">This <see cref="IAntennaRelay"/></param>
+		/// <param name="Vessel">A <see cref="Vessel"/></param>
+		public static double DistanceTo(this AntennaRelay relay, Vessel Vessel)
+		{
+			return relay.vessel.DistanceTo(Vessel);
+		}
+
+		/// <summary>
+		/// Returns the distance between this IAntennaRelay and a CelestialBody
+		/// </summary>
+		/// <param name="relay">This <see cref="IAntennaRelay"/></param>
+		/// <param name="body">A <see cref="CelestialBody"/></param>
+		public static double DistanceTo(this AntennaRelay relay, CelestialBody body)
+		{
+			return relay.vessel.DistanceTo(body) - body.Radius;
+		}
+
+		/// <summary>
+		/// Returns the distance between this IAntennaRelay and another IAntennaRelay
+		/// </summary>
+		/// <param name="relayOne">This <see cref="IAntennaRelay"/></param>
+		/// <param name="relayTwo">Another <see cref="IAntennaRelay"/></param>
+		public static double DistanceTo(this AntennaRelay relayOne, IAntennaRelay relayTwo)
+		{
+			return relayOne.DistanceTo(relayTwo.vessel);
+		}
+
+		public static double sqrDistanceTo(this AntennaRelay relay, Vessel vessel)
+		{
+			return relay.vessel.sqrDistanceTo(vessel);
+		}
+
+		public static double sqrDistanceTo(this AntennaRelay relay, CelestialBody body)
+		{
+			return relay.vessel.sqrDistanceTo(body);
+		}
+
+		public static double sqrDistanceTo(this AntennaRelay relayOne, IAntennaRelay relayTwo)
+		{
+			return relayOne.vessel.sqrDistanceTo(relayTwo.vessel);
+		}
+
+		/// <summary>
+		/// Returns all of the PartModules or ProtoPartModuleSnapshots implementing IAntennaRelay in this Vessel.
+		/// </summary>
+		/// <param name="vessel">This <see cref="Vessel"/></param>
+		public static IEnumerable<IAntennaRelay> GetAntennaRelays (this Vessel vessel)
+		{
+			return RelayDatabase.Instance[vessel].Values.ToList().AsReadOnly();
+		}
+
+		/// <summary>
+		/// Determines if the specified vessel has a connected relay.
+		/// </summary>
+		/// <returns><c>true</c> if the specified vessel has a connected relay; otherwise, <c>false</c>.</returns>
+		/// <param name="vessel"></param>
+		public static bool HasConnectedRelay(this Vessel vessel)
+		{
+			foreach (IAntennaRelay relay in RelayDatabase.Instance[vessel].Values)
+			{
+				if (relay.CanTransmit())
+				{
+					return true;
+				}
+			}
+
+			return false;
+		}
+	}
+}
+
+

file:b/toolbarIcon.xcf (new)
 Binary files /dev/null and b/toolbarIcon.xcf differ
 Binary files /dev/null and b/toolbarIcon_24x24.xcf differ
 Binary files /dev/null and b/toolbarIcon_38x38.xcf differ