Network resolution by total connection cost is working now.
Network resolution by total connection cost is working now.

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 eol=crlf
+*.sln     text eol=crlf
+
+# Images should be treated as binary
+# (binary is a macro for -text -diff)
+*.png     binary
+

--- a/ARConfiguration.cs
+++ b/ARConfiguration.cs
@@ -4,22 +4,146 @@
 // copy of this license, visit http://creativecommons.org/licenses/by-nc-sa/3.0/
 
 using KSP;
+using KSP.UI.Screens;
 using System;
-using ToadicusTools;
+using ToadicusTools.Extensions;
+using ToadicusTools.Text;
+using ToadicusTools.GUIUtils;
+using ToadicusTools.Wrappers;
 using UnityEngine;
 
 namespace AntennaRange
 {
+	/// <summary>
+	/// A <see cref="UnityEngine.MonoBehaviour"/> responsible for managing configuration items for AntennaRange.
+	/// </summary>
 	[KSPAddon(KSPAddon.Startup.SpaceCentre, false)]
 	public class ARConfiguration : MonoBehaviour
 	{
+		private const string WINDOW_POS_KEY = "configWindowPos";
+		private const string REQUIRE_LOS_KEY = "requireLineOfSight";
+		private const string GRACE_RATIO_KEY = "graceRatio";
+		private const string REQUIRE_PROBE_CONNECTION_KEY = "requireConnectionForControl";
+		private const string FIXED_POWER_KEY = "fixedPowerCost";
+		private const string PRETTY_LINES_KEY = "drawPrettyLines";
+		private const string UPDATE_DELAY_KEY = "updateDelay";
+		private const string USE_ADDITIVE_KEY = "useAdditiveRanges";
+
+		private const string TRACKING_STATION_RANGES_KEY = "TRACKING_STATION_RANGES";
+		private const string RANGE_KEY = "range";
+
+		private const string USE_TOOLBAR_KEY = "useToolbarIfAvailable";
+
+		/// <summary>
+		/// Indicates whether connections require line of sight.
+		/// </summary>
+		public static bool RequireLineOfSight
+		{
+			get;
+			private set;
+		}
+
+		/// <summary>
+		/// A "fudge factor" ratio that pretends planets and moons are slightly smaller than reality to make
+		/// building communication constellations easier.
+		/// </summary>
+		public static double RadiusRatio
+		{
+			get;
+			private set;
+		}
+
+		/// <summary>
+		/// Indicates whether unmanned vessels require a connection for control.
+		/// </summary>
+		public static bool RequireConnectionForControl
+		{
+			get;
+			private set;
+		}
+
+		/// <summary>
+		/// If true, relays will fix their power cost when above nominal range, decreasing data rate instead.
+		/// </summary>
+		public static bool FixedPowerCost
+		{
+			get;
+			private set;
+		}
+
+		/// <summary>
+		/// Indicates whether this AntennaRange will draw pretty lines in map view.
+		/// </summary>
+		public static bool PrettyLines
+		{
+			get;
+			set;
+		}
+
+		/// <summary>
+		/// Gets the update delay.
+		/// </summary>
+		public static long UpdateDelay
+		{
+			get;
+			private set;
+		}
+
+		/// <summary>
+		/// Gets a value indicating whether AntennaRange will use additive ranges.
+		/// </summary>
+		public static bool UseAdditiveRanges
+		{
+			get;
+			private set;
+		}
+
+		/// <summary>
+		/// Gets Kerbin's relay range based on the current tracking station level.
+		/// </summary>
+		public static double KerbinRelayRange
+		{
+			get;
+			private set;
+		}
+
+		/// <summary>
+		/// Gets Kerbin's nominal relay range based on the current tracking station level.
+		/// </summary>
+		public static double KerbinNominalRange
+		{
+			get
+			{
+				return KerbinRelayRange / 2.8284271247461901d;
+			}
+		}
+
+		/// <summary>
+		/// Gets a value indicating whether we should use Toolbar if available.
+		/// </summary>
+		/// <value><c>true</c> if we should use Toolbar if available; otherwise, <c>false</c>.</value>
+		public static bool UseToolbarIfAvailable
+		{
+			get;
+			private set;
+		}
+
+#pragma warning disable 1591
+
 		private bool showConfigWindow;
 		private Rect configWindowPos;
 
+		private string updateDelayStr;
+		private long updateDelay;
+
 		private IButton toolbarButton;
 		private ApplicationLauncherButton appLauncherButton;
 
+		private double[] trackingStationRanges;
+
 		private System.Version runningVersion;
+
+		private bool runOnce;
 
 		private KSP.IO.PluginConfiguration _config;
 		private KSP.IO.PluginConfiguration config
@@ -37,60 +161,135 @@
 
 		public void Awake()
 		{
-			Tools.PostDebugMessage(this, "Waking up.");
+			this.LogDebug("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);
-
-			AntennaRelay.requireLineOfSight = this.LoadConfigValue("requireLineOfSight", false);
-
-			AntennaRelay.radiusRatio = (1 - this.LoadConfigValue("graceRatio", .05d));
-			AntennaRelay.radiusRatio *= AntennaRelay.radiusRatio;
-
-			ARFlightController.requireConnectionForControl =
-				this.LoadConfigValue("requireConnectionForControl", false);
-
-			ModuleLimitedDataTransmitter.fixedPowerCost = this.LoadConfigValue("fixedPowerCost", false);
+			this.configWindowPos = this.LoadConfigValue(WINDOW_POS_KEY, this.configWindowPos);
+
+			ARConfiguration.RequireLineOfSight = this.LoadConfigValue(REQUIRE_LOS_KEY, false);
+
+			ARConfiguration.RadiusRatio = (1 - this.LoadConfigValue(GRACE_RATIO_KEY, .05d));
+			ARConfiguration.RadiusRatio *= ARConfiguration.RadiusRatio;
+
+			ARConfiguration.RequireConnectionForControl =
+				this.LoadConfigValue(REQUIRE_PROBE_CONNECTION_KEY, false);
+
+			ARConfiguration.FixedPowerCost = this.LoadConfigValue(FIXED_POWER_KEY, false);
+
+			ARConfiguration.PrettyLines = this.LoadConfigValue(PRETTY_LINES_KEY, true);
+
+			ARConfiguration.UpdateDelay = this.LoadConfigValue(UPDATE_DELAY_KEY, 16L);
+
+			ARConfiguration.UseAdditiveRanges = this.LoadConfigValue(USE_ADDITIVE_KEY, true);
+
+			ARConfiguration.PrettyLines = this.LoadConfigValue(PRETTY_LINES_KEY, true);
+
+			ARConfiguration.UpdateDelay = this.LoadConfigValue(UPDATE_DELAY_KEY, 16L);
+			this.updateDelayStr = ARConfiguration.UpdateDelay.ToString();
+
+			ARConfiguration.UseToolbarIfAvailable = this.LoadConfigValue(USE_TOOLBAR_KEY, true);
 
 			GameEvents.onGameSceneLoadRequested.Add(this.onSceneChangeRequested);
+			GameEvents.OnKSCFacilityUpgraded.Add(this.onFacilityUpgraded);
 
 			Debug.Log(string.Format("{0} v{1} - ARConfiguration loaded!", this.GetType().Name, this.runningVersion));
 
-			Tools.PostDebugMessage(this, "Awake.");
+			ConfigNode[] tsRangeNodes = GameDatabase.Instance.GetConfigNodes(TRACKING_STATION_RANGES_KEY);
+
+			if (tsRangeNodes.Length > 0)
+			{
+				string[] rangeValues = tsRangeNodes[0].GetValues(RANGE_KEY);
+
+				this.trackingStationRanges = new double[rangeValues.Length];
+
+				for (int idx = 0; idx < rangeValues.Length; idx++)
+				{
+					if (!double.TryParse(rangeValues[idx], out this.trackingStationRanges[idx]))
+					{
+						this.LogError("Could not parse value '{0}' to double; Tracking Station ranges may not work!");
+						this.trackingStationRanges[idx] = 0d;
+					}
+				}
+
+				this.Log("Loaded Tracking Station ranges from config: [{0}]", this.trackingStationRanges.SPrint());
+			}
+			else
+			{
+				this.trackingStationRanges = new double[]
+				{
+					51696576d,
+					37152180000d,
+					224770770000d
+				};
+
+				this.LogWarning("Failed to load Tracking Station ranges from config, using hard-coded values: [{0}]",
+					this.trackingStationRanges.SPrint());
+			}
+
+			this.runOnce = true;
+
+			this.LogDebug("Awake.");
+		}
+
+		public void Update()
+		{
+			if (
+				this.runOnce &&
+				(ScenarioUpgradeableFacilities.Instance != null || HighLogic.CurrentGame.Mode != Game.Modes.CAREER)
+			)
+			{
+				this.runOnce = false;
+
+				this.SetKerbinRelayRange();
+			}
 		}
 
 		public void OnGUI()
 		{
 			// Only runs once, if the Toolbar is available.
-			if (this.toolbarButton == null && ToolbarManager.ToolbarAvailable)
-			{
-				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();
-				};
+			if (ToolbarManager.ToolbarAvailable && ARConfiguration.UseToolbarIfAvailable)
+			{
+				if (this.toolbarButton == null)
+				{
+					this.LogDebug("Toolbar available; initializing toolbar button.");
+
+					if (this.appLauncherButton != null)
+					{
+						ApplicationLauncher.Instance.RemoveModApplication(this.appLauncherButton);
+						this.appLauncherButton = null;
+					}
+
+					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.");
+				if (this.toolbarButton != null)
+				{
+					this.toolbarButton.Destroy();
+					this.toolbarButton = null;
+				}
+
+				this.LogDebug("Toolbar available; initializing AppLauncher button.");
 
 				this.appLauncherButton = ApplicationLauncher.Instance.AddModApplication(
 					this.toggleConfigWindow,
 					this.toggleConfigWindow,
 					ApplicationLauncher.AppScenes.SPACECENTER,
 					GameDatabase.Instance.GetTexture(
-						"AntennaRange/Textures/toolbarIcon",
+						"AntennaRange/Textures/appLauncherIcon",
 						false
 					)
 				);
@@ -106,12 +305,12 @@
 					GUILayout.ExpandWidth(true)
 				);
 
-				configPos = Tools.ClampRectToScreen(configPos, 20);
+				configPos = WindowTools.ClampRectToScreen(configPos, 20);
 
 				if (configPos != this.configWindowPos)
 				{
 					this.configWindowPos = configPos;
-					this.SaveConfigValue("configWindowPos", this.configWindowPos);
+					this.SaveConfigValue(WINDOW_POS_KEY, this.configWindowPos);
 				}
 			}
 		}
@@ -122,11 +321,11 @@
 
 			GUILayout.BeginHorizontal(GUILayout.ExpandWidth(true));
 
-			bool requireLineOfSight = GUILayout.Toggle(AntennaRelay.requireLineOfSight, "Require Line of Sight");
-			if (requireLineOfSight != AntennaRelay.requireLineOfSight)
-			{
-				AntennaRelay.requireLineOfSight = requireLineOfSight;
-				this.SaveConfigValue("requireLineOfSight", requireLineOfSight);
+			bool requireLineOfSight = Layout.Toggle(ARConfiguration.RequireLineOfSight, "Require Line of Sight");
+			if (requireLineOfSight != ARConfiguration.RequireLineOfSight)
+			{
+				ARConfiguration.RequireLineOfSight = requireLineOfSight;
+				this.SaveConfigValue(REQUIRE_LOS_KEY, requireLineOfSight);
 			}
 
 			GUILayout.EndHorizontal();
@@ -134,34 +333,83 @@
 			GUILayout.BeginHorizontal(GUILayout.ExpandWidth(true));
 
 			bool requireConnectionForControl =
-				GUILayout.Toggle(
-					ARFlightController.requireConnectionForControl,
+				Layout.Toggle(
+					ARConfiguration.RequireConnectionForControl,
 					"Require Connection for Probe Control"
 				);
-			if (requireConnectionForControl != ARFlightController.requireConnectionForControl)
-			{
-				ARFlightController.requireConnectionForControl = requireConnectionForControl;
-				this.SaveConfigValue("requireConnectionForControl", requireConnectionForControl);
+			if (requireConnectionForControl != ARConfiguration.RequireConnectionForControl)
+			{
+				ARConfiguration.RequireConnectionForControl = requireConnectionForControl;
+				this.SaveConfigValue(REQUIRE_PROBE_CONNECTION_KEY, requireConnectionForControl);
 			}
 
 			GUILayout.EndHorizontal();
 
 			GUILayout.BeginHorizontal();
 
-			bool fixedPowerCost = GUILayout.Toggle(ModuleLimitedDataTransmitter.fixedPowerCost, "Use Fixed Power Cost");
-			if (fixedPowerCost != ModuleLimitedDataTransmitter.fixedPowerCost)
-			{
-				ModuleLimitedDataTransmitter.fixedPowerCost = fixedPowerCost;
-				this.SaveConfigValue("fixedPowerCost", fixedPowerCost);
-			}
-
-			GUILayout.EndHorizontal();
+			bool fixedPowerCost = Layout.Toggle(ARConfiguration.FixedPowerCost, "Use Fixed Power Cost");
+			if (fixedPowerCost != ARConfiguration.FixedPowerCost)
+			{
+				ARConfiguration.FixedPowerCost = fixedPowerCost;
+				this.SaveConfigValue(FIXED_POWER_KEY, fixedPowerCost);
+			}
+
+			GUILayout.EndHorizontal();
+
+			GUILayout.BeginHorizontal();
+
+			bool useAdditive = Layout.Toggle(ARConfiguration.UseAdditiveRanges, "Use Additive Ranges");
+			if (useAdditive != ARConfiguration.UseAdditiveRanges)
+			{
+				ARConfiguration.UseAdditiveRanges = useAdditive;
+				this.SaveConfigValue(USE_ADDITIVE_KEY, useAdditive);
+			}
+
+			GUILayout.EndHorizontal();
+
+			GUILayout.BeginHorizontal();
+
+			bool prettyLines = Layout.Toggle(ARConfiguration.PrettyLines, "Draw Pretty Lines");
+			if (prettyLines != ARConfiguration.PrettyLines)
+			{
+				ARConfiguration.PrettyLines = prettyLines;
+				this.SaveConfigValue(PRETTY_LINES_KEY, prettyLines);
+			}
+
+			GUILayout.EndHorizontal();
+
+			GUILayout.BeginHorizontal();
+
+			bool useToolbar = Layout.Toggle(ARConfiguration.UseToolbarIfAvailable, "Use Blizzy's Toolbar, if Available");
+			if (useToolbar != ARConfiguration.UseToolbarIfAvailable)
+			{
+				ARConfiguration.UseToolbarIfAvailable = useToolbar;
+				this.SaveConfigValue(USE_TOOLBAR_KEY, useToolbar);
+			}
+
+			GUILayout.EndHorizontal();
+
+			GUILayout.BeginHorizontal();
+
+			GUILayout.Label("Update Delay", GUILayout.ExpandWidth(false));
+
+			this.updateDelayStr = GUILayout.TextField(this.updateDelayStr, 4, GUILayout.Width(40f));
+
+			GUILayout.Label("ms", GUILayout.ExpandWidth(false));
+
+			GUILayout.EndHorizontal();
+
+			if (this.updateDelayStr.Length > 1 && long.TryParse(this.updateDelayStr, out this.updateDelay))
+			{
+				ARConfiguration.UpdateDelay = Math.Min(Math.Max(this.updateDelay, 16), 2500);
+				this.updateDelayStr = ARConfiguration.UpdateDelay.ToString();
+			}
 
 			if (requireLineOfSight)
 			{
 				GUILayout.BeginHorizontal();
 
-				double graceRatio = 1d - Math.Sqrt(AntennaRelay.radiusRatio);
+				double graceRatio = 1d - Math.Sqrt(ARConfiguration.RadiusRatio);
 				double newRatio;
 
 				GUILayout.Label(string.Format("Line of Sight 'Fudge Factor': {0:P0}", graceRatio));
@@ -175,8 +423,8 @@
 
 				if (newRatio != graceRatio)
 				{
-					AntennaRelay.radiusRatio = (1d - newRatio) * (1d - newRatio);
-					this.SaveConfigValue("graceRatio", newRatio);
+					ARConfiguration.RadiusRatio = (1d - newRatio) * (1d - newRatio);
+					this.SaveConfigValue(GRACE_RATIO_KEY, newRatio);
 				}
 
 				GUILayout.EndHorizontal();
@@ -190,19 +438,22 @@
 		public void OnDestroy()
 		{
 			GameEvents.onGameSceneLoadRequested.Remove(this.onSceneChangeRequested);
+			GameEvents.OnKSCFacilityUpgraded.Remove(this.onFacilityUpgraded);
 
 			if (this.toolbarButton != null)
 			{
 				this.toolbarButton.Destroy();
+				this.toolbarButton = null;
 			}
 
 			if (this.appLauncherButton != null)
 			{
 				ApplicationLauncher.Instance.RemoveModApplication(this.appLauncherButton);
-			}
-		}
-
-		protected void onSceneChangeRequested(GameScenes scene)
+				this.appLauncherButton = null;
+			}
+		}
+
+		private void onSceneChangeRequested(GameScenes scene)
 		{
 			if (scene != GameScenes.SPACECENTER)
 			{
@@ -211,9 +462,44 @@
 			}
 		}
 
+		private void onFacilityUpgraded(Upgradeables.UpgradeableFacility fac, int lvl)
+		{
+			if (fac.id == "SpaceCenter/TrackingStation")
+			{
+				this.Log("Caught onFacilityUpgraded for {0} at level {1}", fac.id, lvl);
+				this.SetKerbinRelayRange();
+			}
+		}
+
+		private void SetKerbinRelayRange()
+		{
+			int tsLevel;
+
+			if (HighLogic.CurrentGame.Mode == Game.Modes.CAREER)
+			{
+				tsLevel = ScenarioUpgradeableFacilities.protoUpgradeables["SpaceCenter/TrackingStation"]
+					.facilityRefs[0].FacilityLevel;
+			}
+			else
+			{
+				tsLevel = this.trackingStationRanges.Length - 1;
+			}
+
+			if (tsLevel < this.trackingStationRanges.Length && tsLevel >= 0)
+			{
+				KerbinRelayRange = this.trackingStationRanges[tsLevel];
+				this.Log("Setting Kerbin's range to {0}", KerbinRelayRange);
+			}
+			else
+			{
+				this.LogError("Could not set Kerbin's range with invalid Tracking Station level {0}", tsLevel);
+			}
+		}
+
 		private void toggleConfigWindow()
 		{
 			this.showConfigWindow = !this.showConfigWindow;
+			this.updateDelayStr = ARConfiguration.UpdateDelay.ToString();
 		}
 
 		private T LoadConfigValue<T>(string key, T defaultValue)

--- a/ARFlightController.cs
+++ b/ARFlightController.cs
@@ -2,7 +2,7 @@
 //
 // ARFlightController.cs
 //
-// Copyright © 2014, toadicus
+// Copyright © 2014-2015, toadicus
 // All rights reserved.
 //
 // Redistribution and use in source and binary forms, with or without modification,
@@ -26,10 +26,16 @@
 // 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.
 
+#pragma warning disable 1591
+
 using KSP;
+using KSP.UI.Screens;
 using System;
 using System.Collections.Generic;
-using ToadicusTools;
+using ToadicusTools.Extensions;
+using ToadicusTools.Text;
+using ToadicusTools.DebugTools;
+using ToadicusTools.Wrappers;
 using UnityEngine;
 
 namespace AntennaRange
@@ -37,30 +43,45 @@
 	[KSPAddon(KSPAddon.Startup.Flight, false)]
 	public class ARFlightController : MonoBehaviour
 	{
-		#region Static Members
-		public static bool requireConnectionForControl;
+		#region Static
+		private static List<IAntennaRelay> usefulRelays;
+		public static IList<IAntennaRelay> UsefulRelays;
 		#endregion
 
 		#region Fields
-		protected Dictionary<ConnectionStatus, string> connectionTextures;
-
-		protected IButton toolbarButton;
-
-		protected ApplicationLauncherButton appLauncherButton;
+		private Dictionary<ConnectionStatus, string> toolbarTextures;
+		private Dictionary<ConnectionStatus, Texture> appLauncherTextures;
+
+		private ARMapRenderer mapRenderer;
+
+		private IButton toolbarButton;
+
+		private ApplicationLauncherButton appLauncherButton;
+		private PooledDebugLogger log;
+
+		private System.Diagnostics.Stopwatch updateTimer;
 		#endregion
 
 		#region Properties
 		public ConnectionStatus currentConnectionStatus
 		{
 			get;
-			protected set;
-		}
-
-		protected string currentConnectionTexture
+			private set;
+		}
+
+		private string currentConnectionTexture
 		{
 			get
 			{
-				return this.connectionTextures[this.currentConnectionStatus];
+				return this.toolbarTextures[this.currentConnectionStatus];
+			}
+		}
+
+		private Texture currentAppLauncherTexture
+		{
+			get
+			{
+				return this.appLauncherTextures[this.currentConnectionStatus];
 			}
 		}
 
@@ -80,7 +101,7 @@
 		public string lockID
 		{
 			get;
-			protected set;
+			private set;
 		}
 
 		public ControlTypes lockSet
@@ -106,51 +127,82 @@
 		#endregion
 
 		#region MonoBehaviour LifeCycle
-		protected void Awake()
+		private 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";
-
-			if (ToolbarManager.ToolbarAvailable)
+			this.log = PooledDebugLogger.New(this);
+
+			this.updateTimer = new System.Diagnostics.Stopwatch();
+
+			this.toolbarTextures = new Dictionary<ConnectionStatus, string>();
+
+			this.toolbarTextures[ConnectionStatus.None] = "AntennaRange/Textures/toolbarIconNoConnection";
+			this.toolbarTextures[ConnectionStatus.Suboptimal] = "AntennaRange/Textures/toolbarIconSubOptimal";
+			this.toolbarTextures[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 && ARConfiguration.UseToolbarIfAvailable)
 			{
 				this.toolbarButton = ToolbarManager.Instance.add("AntennaRange", "ARConnectionStatus");
 
-				this.toolbarButton.TexturePath = this.connectionTextures[ConnectionStatus.None];
+				this.toolbarButton.TexturePath = this.toolbarTextures[ConnectionStatus.None];
 				this.toolbarButton.Text = "AntennaRange";
 				this.toolbarButton.Visibility = new GameScenesVisibility(GameScenes.FLIGHT);
-				this.toolbarButton.Enabled = false;
+				this.toolbarButton.OnClick += (e) => (this.buttonToggle());
 			}
 
 			GameEvents.onGameSceneLoadRequested.Add(this.onSceneChangeRequested);
 			GameEvents.onVesselChange.Add(this.onVesselChange);
-		}
-
-		protected void FixedUpdate()
-		{
-			if (this.appLauncherButton == null && !ToolbarManager.ToolbarAvailable && ApplicationLauncher.Ready)
-			{
-				this.appLauncherButton = ApplicationLauncher.Instance.AddModApplication(
-					ApplicationLauncher.AppScenes.FLIGHT,
-					GameDatabase.Instance.GetTexture("AntennaRange/Textures/toolbarIconNoConnection", false)
-				);
-			}
-
-			Tools.DebugLogger log = Tools.DebugLogger.New(this);
+
+			usefulRelays = new List<IAntennaRelay>();
+			UsefulRelays = usefulRelays.AsReadOnly();
+		}
+
+		private void FixedUpdate()
+		{
+			this.log.Clear();
+
+			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 &&
-				requireConnectionForControl &&
+				ARConfiguration.RequireConnectionForControl &&
 				this.vessel != null &&
 				this.vessel.vesselType != VesselType.EVA &&
-				!this.vessel.hasCrewCommand() &&
-				!this.vessel.HasConnectedRelay())
+				!(
+				    (availableCommand & VesselCommand.Crew) == VesselCommand.Crew ||
+				    (availableCommand & VesselCommand.Probe) == VesselCommand.Probe && vessel.HasConnectedRelay()
+				))
 			{
 				// ...and if the controls are not currently locked...
 				if (currentControlLock == ControlTypes.None)
@@ -166,92 +218,167 @@
 				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.appLauncherButton != null)
-				{
-					this.appLauncherButton.SetTexture(
-						GameDatabase.Instance.GetTexture(this.currentConnectionTexture, false));
-				}
-			}
-
 			log.Print();
 		}
 
-		protected void OnDestroy()
+		private void Update()
+		{
+			if (MapView.MapIsEnabled && this.mapRenderer == null)
+			{
+				this.mapRenderer = MapView.MapCamera.gameObject.AddComponent<ARMapRenderer>();
+			}
+
+			if (this.toolbarButton != null)
+			{
+				this.toolbarButton.Enabled = MapView.MapIsEnabled;
+			}
+
+			if (this.appLauncherButton == null && !ToolbarManager.ToolbarAvailable && ApplicationLauncher.Ready)
+			{
+				this.appLauncherButton = ApplicationLauncher.Instance.AddModApplication(
+					this.buttonToggle, this.buttonToggle,
+					ApplicationLauncher.AppScenes.FLIGHT | ApplicationLauncher.AppScenes.MAPVIEW,
+					this.appLauncherTextures[ConnectionStatus.None]
+				);
+			}
+
+			if (!this.updateTimer.IsRunning || this.updateTimer.ElapsedMilliseconds > ARConfiguration.UpdateDelay)
+			{
+				this.updateTimer.Restart();
+			}
+			else
+			{
+				return;
+			}
+
+			this.log.Clear();
+
+			this.log.Append("[ARFlightController]: Update");
+
+			if (HighLogic.LoadedSceneIsFlight && FlightGlobals.ready && FlightGlobals.ActiveVessel != null)
+			{
+				Vessel vessel;
+				IAntennaRelay relay;
+				IAntennaRelay bestActiveRelay = null;
+				IList<IAntennaRelay> activeVesselRelays;
+
+				usefulRelays.Clear();
+
+				for (int vIdx = 0; vIdx < FlightGlobals.Vessels.Count; vIdx++)
+				{
+					vessel = FlightGlobals.Vessels[vIdx];
+
+					if (vessel == null || vessel == FlightGlobals.ActiveVessel)
+					{
+						continue;
+					}
+
+					switch (vessel.vesselType)
+					{
+						case VesselType.Debris:
+						case VesselType.Flag:
+						case VesselType.Unknown:
+							continue;
+					}
+
+					log.AppendFormat("\nFetching best relay for vessel {0}", vessel);
+
+					relay = vessel.GetBestRelay();
+
+					if (relay != null)
+					{
+						log.AppendFormat("\n\tAdding useful relay {0}", relay);
+
+						usefulRelays.Add(relay);
+					}
+				}
+
+				activeVesselRelays = RelayDatabase.Instance[FlightGlobals.ActiveVessel];
+
+				if (activeVesselRelays.Count > 0)
+				{
+					bestActiveRelay = RelayDatabase.Instance.GetBestVesselRelay(FlightGlobals.ActiveVessel);
+
+					log.AppendFormat("\n\tAdding best active vessel relay {0} to usefulRelays", bestActiveRelay);
+
+					usefulRelays.Add(bestActiveRelay);
+				}
+
+				log.AppendFormat("\n\tDoing target searches for {0} useful relays", usefulRelays.Count);
+
+				for (int uIdx = 0; uIdx < usefulRelays.Count; uIdx++)
+				{
+					relay = usefulRelays[uIdx];
+
+					if (relay == null)
+					{
+						continue;
+					}
+
+					log.AppendFormat("\n\tDoing target search for useful relay {0}", relay);
+
+					relay.FindNearestRelay();
+					relay.RecalculateTransmissionRates();
+				}
+
+				// Very last, find routes for the non-best relays on the active vessel.
+				for (int rIdx = 0; rIdx < activeVesselRelays.Count; rIdx++)
+				{
+					relay = activeVesselRelays[rIdx];
+
+					// The best active relay will get checked with the other useful relays later.
+					if (relay == null || relay == bestActiveRelay)
+					{
+						continue;
+					}
+
+					log.AppendFormat("\nFinding nearest relay for active vessel relay {0}", relay);
+
+					relay.RecalculateTransmissionRates();
+					relay.FindNearestRelay();
+				}
+
+				if (this.toolbarButton != null || this.appLauncherButton != null)
+				{
+					log.Append("\nChecking active vessel relay status.");
+
+					this.currentConnectionStatus = FlightGlobals.ActiveVessel.GetConnectionStatus();
+
+					log.AppendFormat("\n\tcurrentConnectionStatus: {0}, setting texture to {1}",
+						this.currentConnectionStatus, this.currentConnectionTexture);
+
+					if (this.toolbarButton != null)
+					{
+						this.toolbarButton.TexturePath = this.currentConnectionTexture;
+
+						if (this.currentConnectionStatus == ConnectionStatus.None)
+						{
+							if (!this.toolbarButton.Important) this.toolbarButton.Important = true;
+						}
+						else
+						{
+							if (this.toolbarButton.Important) this.toolbarButton.Important = false;
+						}
+					}
+					if (this.appLauncherButton != null)
+					{
+						this.appLauncherButton.SetTexture(this.currentAppLauncherTexture);
+					}
+				}
+			}
+
+			log.Print(false);
+		}
+
+		private void OnDestroy()
 		{
 			InputLockManager.RemoveControlLock(this.lockID);
 
+			if (this.mapRenderer != null)
+			{
+				GameObject.Destroy(this.mapRenderer);
+			}
+
 			if (this.toolbarButton != null)
 			{
 				this.toolbarButton.Destroy();
@@ -259,7 +386,8 @@
 
 			if (this.appLauncherButton != null)
 			{
-				ApplicationLauncher.Instance.RemoveApplication(this.appLauncherButton);
+				ApplicationLauncher.Instance.RemoveModApplication(this.appLauncherButton);
+				this.appLauncherButton = null;
 			}
 
 			GameEvents.onGameSceneLoadRequested.Remove(this.onSceneChangeRequested);
@@ -269,28 +397,26 @@
 		}
 		#endregion
 
+		private void buttonToggle()
+		{
+			if (MapView.MapIsEnabled)
+			{
+				ARConfiguration.PrettyLines = !ARConfiguration.PrettyLines;
+			}
+		}
+
 		#region Event Handlers
-		protected void onSceneChangeRequested(GameScenes scene)
-		{
-			if (scene != GameScenes.FLIGHT)
-			{
-				print("ARFlightController: Requesting Destruction.");
-				MonoBehaviour.Destroy(this);
-			}
-		}
-
-		protected void onVesselChange(Vessel vessel)
+		private void onSceneChangeRequested(GameScenes scene)
+		{
+			print("ARFlightController: Requesting Destruction.");
+			MonoBehaviour.Destroy(this);
+		}
+
+		private 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,348 @@
-
+// AntennaRange
+//
+// ARMapRenderer.cs
+//
+// Copyright © 2014-2015, 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.
+
+#pragma warning disable 1591
+
+using KSP;
+using System;
+using System.Collections.Generic;
+using ToadicusTools.Extensions;
+using ToadicusTools.DebugTools;
+using UnityEngine;
+
+namespace AntennaRange
+{
+	public class ARMapRenderer : MonoBehaviour
+	{
+		#if BENCH
+		private static ulong updateCount = 0u;
+		private static ulong updateTimer = 0u;
+		private readonly static RollingAverage averager = new RollingAverage();
+		private static long twiceAverageTime = long.MaxValue;
+		#endif
+
+		#region Fields
+		private Dictionary<Guid, LineRenderer> vesselLineRenderers;
+
+		// Debug Stuff
+		#pragma warning disable 649
+		private System.Diagnostics.Stopwatch timer;
+		private PooledDebugLogger log;
+		private long relayStart;
+		private long start;
+		#pragma warning restore 649
+
+		#pragma warning disable 414
+		private Color thisColor;
+		#pragma warning restore 414
+		#endregion
+
+		#region Properties
+		public LineRenderer this[Guid idx]
+		{
+			get
+			{
+				LineRenderer lr;
+
+				if (!this.vesselLineRenderers.TryGetValue(idx, out lr))
+				{
+					GameObject obj = new GameObject();
+					obj.layer = 31;
+
+					lr = obj.AddComponent<LineRenderer>();
+
+					lr.material = MapView.OrbitLinesMaterial;
+
+					this.vesselLineRenderers[idx] = lr;
+
+					return lr;
+				}
+
+				return lr;
+			}
+		}
+		#endregion
+
+		#region MonoBehaviour Lifecycle
+		private void Awake()
+		{
+			if (ARConfiguration.PrettyLines)
+			{
+				this.vesselLineRenderers = new Dictionary<Guid, LineRenderer>();
+			}
+
+			#if DEBUG || BENCH
+			this.timer = new System.Diagnostics.Stopwatch();
+			#endif
+			#if DEBUG
+			this.log = PooledDebugLogger.Get();
+			#endif
+		}
+
+		private void OnPreCull()
+		{
+			if (!HighLogic.LoadedSceneIsFlight || !MapView.MapIsEnabled || !ARConfiguration.PrettyLines)
+			{
+				this.Cleanup(!HighLogic.LoadedSceneIsFlight);
+
+				return;
+			}
+
+			#if DEBUG || BENCH
+			timer.Restart();
+			#endif
+
+			try
+			{
+				log.Clear();
+
+				log.AppendFormat("OnPreCull.\n");
+/* @ TODO: Fix
+				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("FlightGlobals ready and Vessels list not null.");
+
+				IAntennaRelay relay;
+
+				for (int i = 0; i < ARFlightController.UsefulRelays.Count; i++)
+				{
+					relay = ARFlightController.UsefulRelays[i];
+
+					if (relay == null)
+					{
+						log.AppendFormat("\n\tGot null relay, skipping");
+						continue;
+					}
+
+					log.AppendFormat("\n\tDrawing pretty lines for useful relay {0}", relay);
+					
+					#if DEBUG
+					start = timer.ElapsedMilliseconds;
+					#endif
+
+					this.SetRelayVertices(relay);
+
+					log.AppendFormat("\n\tSet relay vertices for {0} in {1}ms",
+						relay, timer.ElapsedMilliseconds - start);
+				}
+			}
+			catch (Exception ex)
+			{
+				this.LogError("Caught {0}: {1}\n{2}\n", ex.GetType().Name, ex.ToString(), ex.StackTrace.ToString());
+				this.Cleanup(false);
+			}
+			#if DEBUG
+			finally
+			{
+				log.AppendFormat("\n\tOnPreCull finished in {0}ms\n", timer.ElapsedMilliseconds);
+
+				log.Print();
+			}
+			#endif
+
+			#if BENCH
+			ARMapRenderer.updateCount++;
+			ARMapRenderer.updateTimer += (ulong)this.timer.ElapsedTicks;
+
+			if (ARMapRenderer.updateCount >= (ulong)(8d / Time.smoothDeltaTime))
+			{
+				ARMapRenderer.averager.AddItem((double)ARMapRenderer.updateTimer / (double)ARMapRenderer.updateCount);
+				ARMapRenderer.updateTimer = 0u;
+				ARMapRenderer.updateCount = 0u;
+				ARMapRenderer.twiceAverageTime = (long)(ARMapRenderer.averager.Average * 2d);
+			}
+
+			if (this.timer.ElapsedTicks > ARMapRenderer.twiceAverageTime)
+			{
+				this.Log("PreCull took significant longer than usual ({0:S3}s vs {1:S3}s)",
+					(double)this.timer.ElapsedTicks / (double)System.Diagnostics.Stopwatch.Frequency,
+					ARMapRenderer.averager.Average / (double)System.Diagnostics.Stopwatch.Frequency
+				);
+			}
+			#endif
+		}
+
+		private void OnDestroy()
+		{
+			this.Cleanup(true);
+
+			this.Log("Destroyed");
+		}
+		#endregion
+
+		#region Utility
+		private void SetRelayVertices(IAntennaRelay relay)
+		{
+			log.AppendFormat("\n\t\tDrawing line for relay chain starting at {0}.", relay);
+
+			if (relay.vessel == null)
+			{
+				log.Append("\n\t\tvessel is null, bailing out");
+				return;
+			}
+
+			LineRenderer renderer = this[relay.vessel.id];
+			Vector3 start = ScaledSpace.LocalToScaledSpace(relay.vessel.GetWorldPos3D());
+
+			float lineWidth;
+			float d = Screen.height / 2f + 0.01f;
+
+			if (MapView.Draw3DLines)
+			{
+				lineWidth = 0.00833333333f * MapView.MapCamera.Distance;
+			}
+			else
+			{
+				lineWidth = 3f;
+
+				// TODO: No idea if this substitution is right.
+				// start = MapView.MapCamera.camera.WorldToScreenPoint(start);
+				start = PlanetariumCamera.Camera.WorldToScreenPoint(start);
+
+				start.z = start.z >= 0f ? d : -d;
+			}
+
+			renderer.SetWidth(lineWidth, lineWidth);
+
+			renderer.SetPosition(0, start);
+
+			int idx = 0;
+
+			#if DEBUG
+			relayStart = timer.ElapsedMilliseconds;
+			#endif
+
+			Vector3 nextPoint;
+
+			renderer.enabled = true;
+
+			if (!relay.CanTransmit())
+			{
+				thisColor = Color.red;
+			}
+			else
+			{
+				if (relay.LinkStatus == ConnectionStatus.Optimal)
+				{
+					thisColor = Color.green;
+				}
+				else
+				{
+					thisColor = Color.yellow;
+				}
+			}
+
+			if (relay.KerbinDirect)
+			{
+				nextPoint = ScaledSpace.LocalToScaledSpace(AntennaRelay.Kerbin.position);
+			}
+			else
+			{
+				if (relay.targetRelay == null || relay.targetRelay.vessel == null)
+				{
+					this.LogError(
+						"SetRelayVertices: relay {0} has null target relay or vessel when not KerbinDirect, bailing out!",
+						relay
+					);
+
+					renderer.enabled = false;
+					return;
+				}
+
+				switch (relay.targetRelay.vessel.vesselType)
+				{
+					case VesselType.Debris:
+					case VesselType.Flag:
+					case VesselType.Unknown:
+						renderer.enabled = false;
+						return;
+					default:
+						break;
+				}
+
+				nextPoint = ScaledSpace.LocalToScaledSpace(relay.targetRelay.vessel.GetWorldPos3D());
+			}
+
+			renderer.SetColors(thisColor, thisColor);
+
+			if (!MapView.Draw3DLines)
+			{
+				// TODO: No idea if this substitution is right.
+				// nextPoint = MapView.MapCamera.camera.WorldToScreenPoint(nextPoint);
+				nextPoint = PlanetariumCamera.Camera.WorldToScreenPoint(nextPoint);
+				nextPoint.z = nextPoint.z >= 0f ? d : -d;
+			}
+
+			idx++;
+
+			renderer.SetVertexCount(idx + 1);
+			renderer.SetPosition(idx, nextPoint);
+
+			log.AppendFormat("\n\t\t\t...finished segment in {0} ms", timer.ElapsedMilliseconds - relayStart);
+		}
+
+		private void Cleanup(bool freeObjects)
+		{
+			if (this.vesselLineRenderers != null && this.vesselLineRenderers.Count > 0)
+			{
+				IEnumerator<LineRenderer> enumerator = this.vesselLineRenderers.Values.GetEnumerator();
+				LineRenderer lineRenderer;
+
+				while (enumerator.MoveNext())
+				{
+					lineRenderer = enumerator.Current;
+
+					if (lineRenderer == null)
+					{
+						continue;
+					}
+
+					lineRenderer.enabled = false;
+
+					if (freeObjects)
+					{
+						GameObject.Destroy(lineRenderer.gameObject);
+					}
+				}
+
+				if (freeObjects)
+				{
+					this.vesselLineRenderers.Clear();
+				}
+			}
+		}
+		#endregion
+	}
+}
+

file:a/AntennaRange.cfg (deleted)
--- a/AntennaRange.cfg
+++ /dev/null
@@ -1,72 +1,1 @@
-// 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
-	}
-}
-
-@PART[mediumDishAntenna]:FOR[AntennaRange]:NEEDS[!RemoteTech2]
-{
-	@MODULE[ModuleDataTransmitter]
-	{
-		@name = ModuleLimitedDataTransmitter
-		nominalRange = 30000000
-		maxPowerFactor = 8
-		maxDataFactor = 4
-	}
-}
-
-@PART[commDish]:FOR[AntennaRange]:NEEDS[!RemoteTech2]
-{
-	@MODULE[ModuleDataTransmitter]
-	{
-		@name = ModuleLimitedDataTransmitter
-		nominalRange = 80000000000
-		maxPowerFactor = 8
-		maxDataFactor = 4
-	}
-}
-

--- a/AntennaRange.csproj
+++ b/AntennaRange.csproj
@@ -25,8 +25,7 @@
     <ConsolePause>false</ConsolePause>
     <CustomCommands>
       <CustomCommands>
-        <Command type="AfterBuild" command="xcopy /y ${ProjectDir}\AntennaRange.cfg C:\Users\andy\Games\KSP_win\GameData\AntennaRange\" />
-        <Command type="AfterBuild" command="xcopy /y ${TargetFile} C:\Users\andy\Games\KSP_win\GameData\AntennaRange\" />
+        <Command type="AfterBuild" command="xcopy /y ${TargetFile} ${ProjectDir}\GameData\AntennaRange\" />
       </CustomCommands>
     </CustomCommands>
   </PropertyGroup>
@@ -38,10 +37,10 @@
     <ConsolePause>false</ConsolePause>
     <CustomCommands>
       <CustomCommands>
-        <Command type="AfterBuild" command="xcopy /y ${ProjectDir}\AntennaRange.cfg C:\Users\andy\Games\KSP_win\GameData\AntennaRange\" />
-        <Command type="AfterBuild" command="xcopy /y ${TargetFile} C:\Users\andy\Games\KSP_win\GameData\AntennaRange\" />
+        <Command type="AfterBuild" command="xcopy /y ${TargetFile} ${ProjectDir}\GameData\AntennaRange\" />
       </CustomCommands>
     </CustomCommands>
+    <DocumentationFile>bin\Release\AntennaRange.xml</DocumentationFile>
   </PropertyGroup>
   <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug_linux|AnyCPU' ">
     <DebugSymbols>true</DebugSymbols>
@@ -54,7 +53,7 @@
     <ConsolePause>false</ConsolePause>
     <CustomCommands>
       <CustomCommands>
-        <Command type="AfterBuild" command="cp -afv ${TargetFile} ${ProjectDir}/${ProjectName}.cfg /opt/games/KSP_linux/GameData/${ProjectName}/" />
+        <Command type="AfterBuild" command="cp -afv ${TargetFile} ${ProjectDir}/GameData/${ProjectName}/" />
       </CustomCommands>
     </CustomCommands>
   </PropertyGroup>
@@ -65,7 +64,7 @@
     <WarningLevel>4</WarningLevel>
     <CustomCommands>
       <CustomCommands>
-        <Command type="AfterBuild" command="cp -afv ${TargetFile} ${ProjectDir}/${ProjectName}.cfg /opt/games/KSP_linux/GameData/${ProjectName}/" />
+        <Command type="AfterBuild" command="cp -afv ${TargetFile} ${ProjectDir}/GameData/${ProjectName}/" />
       </CustomCommands>
     </CustomCommands>
     <ConsolePause>false</ConsolePause>
@@ -80,13 +79,10 @@
     <Compile Include="RelayExtensions.cs" />
     <Compile Include="ARConfiguration.cs" />
     <Compile Include="ARFlightController.cs" />
+    <Compile Include="ARMapRenderer.cs" />
+    <Compile Include="RelayDataCost.cs" />
   </ItemGroup>
   <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
-  <ItemGroup>
-    <None Include="AntennaRange.cfg">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-  </ItemGroup>
   <ItemGroup>
     <Reference Include="Assembly-CSharp">
       <HintPath>..\_KSPAssemblies\Assembly-CSharp.dll</HintPath>
@@ -98,7 +94,12 @@
     </Reference>
     <Reference Include="UnityEngine">
       <HintPath>..\_KSPAssemblies\UnityEngine.dll</HintPath>
-      <Private>False</Private>
+    </Reference>
+    <Reference Include="KSPUtil">
+      <HintPath>..\_KSPAssemblies\KSPUtil.dll</HintPath>
+    </Reference>
+    <Reference Include="UnityEngine.UI">
+      <HintPath>..\_KSPAssemblies\UnityEngine.UI.dll</HintPath>
     </Reference>
   </ItemGroup>
   <ItemGroup>
@@ -107,4 +108,8 @@
       <Name>ToadicusTools</Name>
     </ProjectReference>
   </ItemGroup>
+  <ItemGroup>
+    <None Include="GameData\AntennaRange\AntennaRange.cfg" />
+    <None Include="GameData\AntennaRange\ATM_AntennaRange.cfg" />
+  </ItemGroup>
 </Project>

--- a/AntennaRelay.cs
+++ b/AntennaRelay.cs
@@ -2,7 +2,7 @@
 //
 // AntennaRelay.cs
 //
-// Copyright © 2014, toadicus
+// Copyright © 2014-2015, toadicus
 // All rights reserved.
 //
 // Redistribution and use in source and binary forms, with or without modification,
@@ -28,26 +28,57 @@
 
 using System;
 using System.Collections.Generic;
-using System.Linq;
-using ToadicusTools;
+using ToadicusTools.DebugTools;
+using ToadicusTools.Extensions;
+using UnityEngine;
 
 namespace AntennaRange
 {
+	/// <summary>
+	/// Relay code at the heart of AntennaRange
+	/// </summary>
 	public class AntennaRelay
 	{
-		public static bool requireLineOfSight;
-		public static double radiusRatio;
-
 		// We don't have a Bard, so we'll hide Kerbin here.
-		protected CelestialBody Kerbin;
-
-		protected CelestialBody _firstOccludingBody;
-
-		protected IAntennaRelay _nearestRelayCache;
+		private static CelestialBody _Kerbin;
+
+		/// <summary>
+		/// Fetches, caches, and returns a <see cref="CelestialBody"/> reference to Kerbin
+		/// </summary>
+		public static CelestialBody Kerbin
+		{
+			get
+			{
+				if (_Kerbin == null && FlightGlobals.ready)
+				{
+					_Kerbin = FlightGlobals.GetHomeBody();
+				}
+
+				return _Kerbin;
+			}
+		}
+
+		#if BENCH
+		private static ushort relayCount = 0;
+		private static ulong searchCount = 0u;
+		private static ulong searchTimer = 0u;
+		private readonly static RollingAverage averager = new RollingAverage(16);
+		private static long doubleAverageTime = long.MaxValue;
+
+
+		private System.Diagnostics.Stopwatch performanceTimer = new System.Diagnostics.Stopwatch();
+		#endif
+
+		private bool canTransmit;
+
+		private IAntennaRelay nearestRelay;
+		private IAntennaRelay bestOccludedRelay;
+
+		/// <summary>
+		/// The <see cref="AntennaRange.ModuleLimitedDataTransmitter"/> reference underlying this AntennaRelay, as an
+		/// <see cref="AntennaRange.IAntennaRelay"/>
+		/// </summary>
 		protected IAntennaRelay moduleRef;
-
-		protected System.Diagnostics.Stopwatch searchTimer;
-		protected long millisecondsBetweenSearches;
 
 		/// <summary>
 		/// Gets the parent Vessel.
@@ -62,83 +93,210 @@
 		}
 
 		/// <summary>
-		/// Gets or sets the nearest relay.
-		/// </summary>
-		/// <value>The nearest relay</value>
-		public IAntennaRelay nearestRelay
+		/// Gets the target <see cref="AntennaRange.IAntennaRelay"/>relay.
+		/// </summary>
+		public IAntennaRelay targetRelay
+		{
+			get;
+			protected set;
+		}
+
+		/// <summary>
+		/// Gets a value indicating whether this <see cref="AntennaRange.IAntennaRelay"/> Relay is communicating
+		/// directly with Kerbin.
+		/// </summary>
+		public virtual bool KerbinDirect
+		{
+			get;
+			protected set;
+		}
+
+		/// <summary>
+		/// Gets or sets the nominal link distance, in meters.
+		/// </summary>
+		public virtual double NominalLinkSqrDistance
+		{
+			get;
+			protected set;
+		}
+
+		/// <summary>
+		/// Gets or sets the maximum link distance, in meters.
+		/// </summary>
+		public virtual double MaximumLinkSqrDistance
+		{
+			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 CurrentLinkSqrDistance
 		{
 			get
 			{
-				if (this.searchTimer.IsRunning &&
-					this.searchTimer.ElapsedMilliseconds > this.millisecondsBetweenSearches)
-				{
-					this._nearestRelayCache = this.FindNearestRelay();
-					this.searchTimer.Restart();
-				}
-
-				return this._nearestRelayCache;
-			}
-			protected set
-			{
-				this._nearestRelayCache = value;
-			}
-		}
-
-		/// <summary>
-		/// Gets the first occluding body.
-		/// </summary>
-		/// <value>The first occluding body.</value>
-		public CelestialBody firstOccludingBody
+				if (this.KerbinDirect || this.targetRelay == null)
+				{
+					return this.SqrDistanceTo(Kerbin);
+				}
+				else
+				{
+					return this.SqrDistanceTo(this.targetRelay);
+				}
+			}
+		}
+
+		/// <summary>
+		/// Gets the current link resource rate in EC/MiT.
+		/// </summary>
+		/// <value>The current link resource rate in EC/MiT.</value>
+		public virtual RelayDataCost CurrentLinkCost
 		{
 			get
 			{
-				return this._firstOccludingBody;
-			}
-		}
-
-		/// <summary>
-		/// Gets the transmit distance.
-		/// </summary>
-		/// <value>The transmit distance.</value>
-		public double transmitDistance
+				return new RelayDataCost(this.moduleRef.PacketResourceCost, this.moduleRef.PacketSize);
+			}
+		}
+
+		/// <summary>
+		/// Gets the current network link cost back to Kerbin, in EC/MiT.
+		/// </summary>
+		/// <value>The current network link cost back to Kerbin, in EC/MiT.</value>
+		public virtual RelayDataCost CurrentNetworkLinkCost
 		{
 			get
 			{
-				this.nearestRelay = this.FindNearestRelay();
-
-				// If there is no available relay nearby...
-				if (this.nearestRelay == null)
-				{
-					// .. return the distance to Kerbin
-					return this.DistanceTo(this.Kerbin);
-				}
-				else
-				{
-					/// ...otherwise, return the distance to the nearest available relay.
-					return this.DistanceTo(nearestRelay);
-				}
-			}
-		}
-
-		/// <summary>
-		/// The maximum distance at which this relay can operate.
-		/// </summary>
-		/// <value>The max transmit distance.</value>
-		public virtual float maxTransmitDistance
+				RelayDataCost cost = new RelayDataCost();
+
+				IAntennaRelay relay = this.moduleRef;
+
+				ushort iters = 0;
+				while (relay != null)
+				{
+					cost += new RelayDataCost(relay.PacketResourceCost, relay.PacketSize);
+
+					if (relay.KerbinDirect)
+					{
+						break;
+					}
+
+					iters++;
+
+					if (iters > 255)
+					{
+						this.LogError("Bailing out of AntennaRelay.CurrentNetworkLinkCost because it looks like " +
+							"we're stuck in an infinite loop.  This is probably a bug.");
+						
+						break;
+					}
+
+					relay = relay.targetRelay;
+				}
+
+				return cost;
+			}
+		}
+
+		/// <summary>
+		/// Gets or sets the link status.
+		/// </summary>
+		public virtual ConnectionStatus LinkStatus
+		{
+			get;
+			protected set;
+		}
+
+		/// <summary>
+		/// Gets the nominal transmit distance at which the Antenna behaves just as prescribed by Squad's config.
+		/// </summary>
+		public virtual double nominalTransmitDistance
 		{
 			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
+		/// The maximum distance at which this relay can operate.
+		/// </summary>
+		/// <value>The max transmit distance.</value>
+		public virtual double maxTransmitDistance
 		{
 			get;
-			protected set;
+			set;
+		}
+		/*
+		 * 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.
+		 * */
+		/// <summary>
+		/// Override ModuleDataTransmitter.DataRate to just return packetSize, because we want antennas to be scored in
+		/// terms of joules/byte
+		/// </summary>
+		public virtual float DataRate
+		{
+			get
+			{
+				if (this.CanTransmit())
+				{
+					return this.moduleRef.PacketSize;
+				}
+				else
+				{
+					return float.Epsilon;
+				}
+			}
+		}
+
+		/// <summary>
+		/// Override ModuleDataTransmitter.DataResourceCost to just return packetResourceCost, because we want antennas
+		/// to be scored in terms of joules/byte
+		/// </summary>
+		public virtual double DataResourceCost
+		{
+			get
+			{
+				if (this.CanTransmit())
+				{
+					return this.moduleRef.PacketResourceCost;
+				}
+				else
+				{
+					return float.PositiveInfinity;
+				}
+			}
 		}
 
 		/// <summary>
@@ -147,175 +305,803 @@
 		/// <returns><c>true</c> if this instance can transmit; otherwise, <c>false</c>.</returns>
 		public virtual bool CanTransmit()
 		{
-			if (
-				this.transmitDistance > this.maxTransmitDistance ||
-				(
-					requireLineOfSight &&
-					this.nearestRelay == null &&
-					!this.vessel.hasLineOfSightTo(this.Kerbin, out this._firstOccludingBody, radiusRatio)
-				)
-			)
-			{
-				return false;
+			return this.canTransmit;
+		}
+
+		/// <summary>
+		/// Recalculates the transmission rates.
+		/// </summary>
+		public void RecalculateTransmissionRates()
+		{
+			if (!this.canTransmit) {
+				this.moduleRef.PacketSize = 0f;
+				this.moduleRef.PacketResourceCost = float.PositiveInfinity;
+				return;
+			}
+
+			RelayDataCost cost = this.GetPotentialLinkCost(this.CurrentLinkSqrDistance, this.NominalLinkSqrDistance);
+
+			this.moduleRef.PacketSize = cost.PacketSize;
+			this.moduleRef.PacketResourceCost = cost.PacketResourceCost;
+		}
+
+		/// <summary>
+		/// Gets the potential link cost, in EC/MiT.
+		/// </summary>
+		/// <returns>The potential link cost, in EC/MiT.</returns>
+		/// <param name="currentSqrDistance">Square of the current distance to the target</param>
+		/// <param name="nominalSqrDistance">Square of the nominal range to the target.</param>
+		public RelayDataCost GetPotentialLinkCost(double currentSqrDistance, double nominalSqrDistance)
+		{
+			RelayDataCost linkCost = new RelayDataCost();
+
+			float rangeFactor = (float)(nominalSqrDistance / currentSqrDistance);
+
+			linkCost.PacketSize = this.moduleRef.PacketSize;
+
+			if (ARConfiguration.FixedPowerCost)
+			{
+				linkCost.PacketResourceCost = this.moduleRef.BasePacketResourceCost;
+
+				linkCost.PacketSize = Mathf.Min(
+					this.moduleRef.BasePacketSize * rangeFactor,
+					this.moduleRef.BasePacketSize * this.moduleRef.MaxDataFactor
+				);
 			}
 			else
 			{
-				return true;
-			}
+				if (currentSqrDistance > nominalSqrDistance)
+				{
+					linkCost.PacketSize = this.moduleRef.BasePacketSize;
+					linkCost.PacketResourceCost = this.moduleRef.BasePacketResourceCost / rangeFactor;
+				}
+				else
+				{
+					linkCost.PacketSize = Mathf.Min(
+						this.moduleRef.BasePacketSize * rangeFactor,
+						this.moduleRef.BasePacketSize * this.moduleRef.MaxDataFactor
+					);
+					linkCost.PacketResourceCost = this.moduleRef.BasePacketResourceCost;
+				}
+			}
+
+			linkCost.PacketResourceCost *= this.moduleRef.PacketThrottle / 100f;
+			linkCost.PacketSize *= this.moduleRef.PacketThrottle / 100f;
+
+			return linkCost;
+		}
+
+		/// <summary>
+		/// Gets the potential link cost, in EC/MiT.
+		/// </summary>
+		/// <returns>The potential link cost, in EC/MiT.</returns>
+		/// <param name="potentialTarget">Potential target relay.</param>
+		public RelayDataCost GetPotentialLinkCost(IAntennaRelay potentialTarget)
+		{
+			if (potentialTarget == null)
+			{
+				return RelayDataCost.Infinity;
+			}
+
+			double currentSqrDistance = this.SqrDistanceTo(potentialTarget);
+
+			if (currentSqrDistance > this.MaxLinkSqrDistanceTo(potentialTarget))
+			{
+				return RelayDataCost.Infinity;
+			}
+
+			double nominalSqrDistance;
+			if (ARConfiguration.UseAdditiveRanges)
+			{
+				nominalSqrDistance = this.nominalTransmitDistance * potentialTarget.nominalTransmitDistance;
+			}
+			else
+			{
+				nominalSqrDistance = this.nominalTransmitDistance * this.nominalTransmitDistance;
+			}
+
+			return GetPotentialLinkCost(currentSqrDistance, nominalSqrDistance);
+		}
+
+		/// <summary>
+		/// Gets the potential link cost, in EC/MiT.
+		/// </summary>
+		/// <returns>The potential link cost, in EC/MiT.</returns>
+		/// <param name="body">Potential target Body</param>
+		public RelayDataCost GetPotentialLinkCost(CelestialBody body)
+		{
+			if (body == null || body != Kerbin)
+			{
+				return RelayDataCost.Infinity;
+			}
+
+			double currentSqrDistance = this.SqrDistanceTo(body);
+
+			if (currentSqrDistance > this.MaxLinkSqrDistanceTo(body))
+			{
+				return RelayDataCost.Infinity;
+			}
+
+			double nominalSqrDistance;
+			if (ARConfiguration.UseAdditiveRanges)
+			{
+				nominalSqrDistance = this.nominalTransmitDistance * ARConfiguration.KerbinNominalRange;
+			}
+			else
+			{
+				nominalSqrDistance = this.nominalTransmitDistance * this.nominalTransmitDistance;
+			}
+
+			return GetPotentialLinkCost(currentSqrDistance, nominalSqrDistance);
 		}
 
 		/// <summary>
 		/// Finds the nearest relay.
 		/// </summary>
 		/// <returns>The nearest relay or null, if no relays in range.</returns>
-		public IAntennaRelay FindNearestRelay()
-		{
-			if (this.searchTimer.IsRunning && this.searchTimer.ElapsedMilliseconds < this.millisecondsBetweenSearches)
-			{
-				return this.nearestRelay;
-			}
-
-			if (this.searchTimer.IsRunning)
-			{
-				this.searchTimer.Stop();
-				this.searchTimer.Reset();
-			}
-
-			this.searchTimer.Start();
-
-			Tools.PostDebugMessage(string.Format(
-				"{0}: finding nearest relay for {1} ({2})",
-				this.GetType().Name,
-				this,
-				this.vessel.id
-			));
-
-			this._firstOccludingBody = null;
-
-			// Set this vessel as checked, so that we don't check it again.
-			RelayDatabase.Instance.CheckedVesselsTable[vessel.id] = true;
-
-			double nearestSqrDistance = double.PositiveInfinity;
-			IAntennaRelay _nearestRelay = null;
+		public void FindNearestRelay()
+		{
+			if (!FlightGlobals.ready)
+			{
+				return;
+			}
+
+			PooledDebugLogger log;
+			#if DEBUG
+			log = PooledDebugLogger.New(this);
+			#endif
+
+			#if BENCH
+			this.performanceTimer.Restart();
+
+			long startVesselLoopTicks;
+			long totalVesselLoopTicks;
+
+			string slowestLOSVesselName = string.Empty;
+			long slowestLOSVesselTicks = long.MinValue;
+			long startLOSVesselTicks;
+			long totalLOSVesselTicks;
+
+			string slowestCircularVesselName = string.Empty;
+			long slowestCircularVesselTicks = long.MinValue;
+			long startCircularVesselTicks;
+			long totalCircularVesselTicks;
+
+			long startKerbinLOSTicks;
+			long totalKerbinLOSTicks;
+			long statusResolutionTicks;
+
+			ushort usefulVesselCount = 0;
+			#endif
+
+			log.AppendFormat("{0}: Target search started).", this.ToString());
+
+			#if DEBUG
+			try {
+			#endif
+
+			// Declare a bunch of variables we'll be using.
+			CelestialBody bodyOccludingBestOccludedRelay = null;
+			IAntennaRelay needle;
+
+			RelayDataCost cheapestRelayRate = RelayDataCost.Infinity;
+			RelayDataCost cheapestOccludedRelayRate = RelayDataCost.Infinity;
+
+			RelayDataCost potentialRelayRate;
+
+			RelayDataCost kerbinRelayRate = this.GetPotentialLinkCost(Kerbin);
+
+			bool isCircular;
+			int iterCount;
+
+			// Blank everything we're trying to find before the search.
+			this.firstOccludingBody = null;
+			this.bestOccludedRelay = null;
+			this.targetRelay = null;
+			this.nearestRelay = null;
+
+			// Default to KerbinDirect = true in case something in here doesn't work right.
+			this.KerbinDirect = true;
 
 			/*
-			 * 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.
+			 * Loop through the useful relays as determined by ARFlightController and check each for line of sight and
+			 * distance, searching for the relay with the best distance/maxRange ratio that is in sight, in range, and
+			 * can transmit, also stashing the "best" relay outside of line of sight for failure report.
 			 * */
-			foreach (Vessel potentialVessel in FlightGlobals.Vessels)
-			{
-				// Skip vessels that have already been checked for a nearest relay this pass.
-				if (RelayDatabase.Instance.CheckedVesselsTable.ContainsKey(potentialVessel.id))
-				{
+			IAntennaRelay potentialBestRelay;
+			CelestialBody fob;
+
+			#if BENCH
+			startVesselLoopTicks = performanceTimer.ElapsedTicks;
+			#endif
+			
+			for (int rIdx = 0; rIdx < ARFlightController.UsefulRelays.Count; rIdx++)
+			{
+				potentialBestRelay = ARFlightController.UsefulRelays[rIdx];
+				log.AppendFormat("\n\tgot useful relay {0}",
+					potentialBestRelay == null ? "null" : potentialBestRelay.ToString());
+
+				if (potentialBestRelay == null)
+				{
+					log.Append("\n\t...skipping null relay");
 					continue;
 				}
 
-				// 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;
-				}
-
-				// Skip vessels to which we do not have line of sight.
-				if (requireLineOfSight &&
-					!this.vessel.hasLineOfSightTo(potentialVessel, out this._firstOccludingBody, radiusRatio))
-				{
-					Tools.PostDebugMessage(
-						this,
-						"Vessel {0} discarded because we do not have line of sight.",
-						potentialVessel.vesselName
+				if (potentialBestRelay == this || potentialBestRelay.vessel == this.vessel)
+				{
+					log.AppendFormat(
+						"\n\t...skipping relay {0} because it or its vessel ({1}) is the same as ours" +
+						"\n\t\t(our vessel is {2})",
+						potentialBestRelay,
+						potentialBestRelay.vessel == null ? "null" : potentialBestRelay.vessel.vesselName,
+						this.vessel == null ? "null" : this.vessel.vesselName
 					);
 					continue;
 				}
 
+				#if BENCH
+				usefulVesselCount++;
+				#endif
+
 				// Find the distance from here to the vessel...
-				double potentialSqrDistance = (potentialVessel.GetWorldPos3D() - vessel.GetWorldPos3D()).sqrMagnitude;
+				log.Append("\n\tgetting cost to potential vessel");
+				potentialRelayRate = potentialBestRelay.CurrentNetworkLinkCost + this.GetPotentialLinkCost(potentialBestRelay);
+
+				log.AppendFormat("\n\tpotentialRelayRate = {0} ({1} + {2})", potentialRelayRate, potentialBestRelay.CurrentNetworkLinkCost, this.GetPotentialLinkCost(potentialBestRelay));
+
+				#if BENCH
+				startLOSVesselTicks = performanceTimer.ElapsedTicks;
+				#endif
+
+				log.Append("\n\t\tdoing LOS check");
+				// Skip vessels to which we do not have line of sight.
+				if (
+					ARConfiguration.RequireLineOfSight &&
+					!this.vessel.hasLineOfSightTo(potentialBestRelay.vessel, out fob, ARConfiguration.RadiusRatio)
+				)
+				{
+					#if BENCH
+					totalLOSVesselTicks = performanceTimer.ElapsedTicks - startLOSVesselTicks;
+
+					if (totalLOSVesselTicks > slowestLOSVesselTicks)
+					{
+						slowestLOSVesselTicks = totalLOSVesselTicks;
+						slowestLOSVesselName = vessel.vesselName;
+					}
+					#endif
+
+					log.Append("\n\t\t...failed LOS check");
+
+					log.AppendFormat("\n\t\t\t{0}: Relay {1} not in line of sight.",
+						this.ToString(), potentialBestRelay);
+					
+					log.AppendFormat("\n\t\t\tpotentialRelayRate: {0}", potentialRelayRate);
+					log.AppendFormat("\n\t\t\tcheapestOccludedRelayRate: {0}", cheapestOccludedRelayRate);
+
+					if (
+						(potentialRelayRate < cheapestRelayRate) &&
+						this.IsInRangeOf(potentialBestRelay) &&
+						potentialBestRelay.CanTransmit()
+					)
+					{
+						log.Append("\n\t\t...vessel is cheapest and close enough and potentialBestRelay can transmit");
+						log.AppendFormat("\n\t\t...{0} found new best occluded relay {1}", this, potentialBestRelay);
+
+						this.bestOccludedRelay = potentialBestRelay;
+						bodyOccludingBestOccludedRelay = fob;
+						cheapestOccludedRelayRate = potentialRelayRate;
+					}
+					else
+					{
+						log.Append("\n\t\t...vessel is not close enough to check for occluded relays, carrying on");
+					}
+					
+					continue;
+				}
+				#if BENCH
+				else
+				{
+					totalLOSVesselTicks = performanceTimer.ElapsedTicks - startLOSVesselTicks;
+				}
+
+				if (totalLOSVesselTicks > slowestLOSVesselTicks)
+				{
+					slowestLOSVesselTicks = totalLOSVesselTicks;
+					slowestLOSVesselName = vessel.vesselName;
+				}
+				#endif
+
+				log.Append("\n\t\t...passed LOS check");
 
 				/*
-				 * ...so that we can skip the vessel if it is further away than Kerbin, our transmit distance, or a
-				 * vessel we've already checked.
+				 * ...so that we can skip the vessel if it is further away than a vessel we've already checked.
 				 * */
-				if (
-					potentialSqrDistance > Tools.Min(
-						this.maxTransmitDistance * this.maxTransmitDistance,
-						nearestSqrDistance,
-						this.vessel.sqrDistanceTo(Kerbin)
-					)
-				)
-				{
-					Tools.PostDebugMessage(
-						this,
-						"Vessel {0} discarded because it is out of range, or farther than another relay.",
-						potentialVessel.vesselName
+				if (potentialRelayRate > cheapestRelayRate)
+				{
+					
+					log.AppendFormat(
+							"\n\t{0}: Relay {1} discarded because it is more expensive than the cheapest relay." +
+							"\n\t\t({2}, {3} > {4})",
+						this.ToString(),
+						potentialBestRelay,
+						this.nearestRelay == null ? "NULL" : this.nearestRelay.ToString(),
+						potentialRelayRate, cheapestRelayRate
 					);
 					continue;
 				}
 
-				nearestSqrDistance = potentialSqrDistance;
-
-				foreach (IAntennaRelay potentialRelay in potentialVessel.GetAntennaRelays())
-				{
-					if (potentialRelay.CanTransmit())
-					{
-						_nearestRelay = potentialRelay;
-						Tools.PostDebugMessage(string.Format("{0}: found new best relay {1} ({2})",
-							this.GetType().Name,
-							_nearestRelay.ToString(),
-							_nearestRelay.vessel.id
-						));
-						break;
-					}
-				}
-			}
-
-			// 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);
-
-			// Return the nearest available relay, or null if there are no available relays nearby.
-			return _nearestRelay;
-		}
-
-		/// <summary>
-		/// Initializes a new instance of the <see cref="AntennaRange.ProtoDataTransmitter"/> class.
-		/// </summary>
-		/// <param name="ms"><see cref="ProtoPartModuleSnapshot"/></param>
+				log.Append("\n\t\t...passed distance check");
+
+				if (potentialBestRelay.CanTransmit())
+				{
+					#if BENCH
+					startCircularVesselTicks = performanceTimer.ElapsedTicks;
+					#endif
+
+					needle = potentialBestRelay;
+					isCircular = false;
+
+					iterCount = 0;
+					while (needle != null)
+					{
+						iterCount++;
+
+						if (needle.KerbinDirect)
+						{
+							break;
+						}
+
+						if (needle.targetRelay == null)
+						{
+							break;
+						}
+
+						if (needle.targetRelay.vessel == this.vessel || needle == this.moduleRef)
+						{
+							isCircular = true;
+							break;
+						}
+
+						// Avoid infinite loops when we're not catching things right.
+						if (iterCount > FlightGlobals.Vessels.Count)
+						{
+							this.LogError(
+								"iterCount exceeded while checking for circular network; assuming it is circular" +
+								"\n\tneedle={0}" +
+								"\n\tthis.moduleRef={1}",
+								needle == null ? "null" : string.Format(
+									"{0}, needle.KerbinDirect={1}, needle.targetRelay={2}",
+									needle, needle.KerbinDirect, needle.targetRelay == null ? "null" : string.Format(
+										"{0}\n\tneedle.targetRelay.vessel={1}",
+										needle.targetRelay,
+										needle.targetRelay.vessel == null ?
+											"null" : needle.targetRelay.vessel.vesselName
+									)
+								),
+								this.moduleRef == null ? "null" : this.moduleRef.ToString()
+							);
+							isCircular = true;
+							break;
+						}
+
+						needle = needle.targetRelay;
+					}
+
+					if (!isCircular)
+					{
+						cheapestRelayRate = potentialRelayRate;
+						this.nearestRelay = potentialBestRelay;
+
+						log.AppendFormat("\n\t{0}: found new cheapest relay {1} ({2} EC/MiT)",
+							this.ToString(),
+							this.nearestRelay.ToString(),
+							cheapestRelayRate
+						);
+					}
+					else
+					{
+						log.AppendFormat("\n\t\t...connection to {0} would result in a circular network, skipping",
+							potentialBestRelay
+						);
+					}
+
+					#if BENCH
+					totalCircularVesselTicks = performanceTimer.ElapsedTicks - startCircularVesselTicks;
+
+					if (totalCircularVesselTicks > slowestCircularVesselTicks)
+					{
+						slowestCircularVesselName = vessel.vesselName;
+						slowestCircularVesselTicks = totalCircularVesselTicks;
+					}
+
+					#endif
+				}
+			}
+
+			#if BENCH
+			totalVesselLoopTicks = performanceTimer.ElapsedTicks - startVesselLoopTicks;
+			#endif
+
+			CelestialBody bodyOccludingKerbin = null;
+
+			log.AppendFormat("\n{0} ({1}): Search done, figuring status.", this.ToString(), this.GetType().Name);
+			log.AppendFormat(
+					"\n{0}: nearestRelay={1} ({2})), bestOccludedRelay={3} ({4}), kerbinRelayRate={5} EC/MiT)",
+				this,
+				this.nearestRelay == null ? "null" : this.nearestRelay.ToString(),
+				cheapestRelayRate,
+				this.bestOccludedRelay == null ? "null" : this.bestOccludedRelay.ToString(),
+				cheapestOccludedRelayRate,
+				kerbinRelayRate
+			);
+			
+			#if BENCH
+			startKerbinLOSTicks = this.performanceTimer.ElapsedTicks;
+			#endif
+
+			// If we don't have LOS to Kerbin, focus on relays
+			if (
+				ARConfiguration.RequireLineOfSight &&
+				!this.vessel.hasLineOfSightTo(Kerbin, out bodyOccludingKerbin, ARConfiguration.RadiusRatio)
+			)
+			{
+				#if BENCH
+				totalKerbinLOSTicks = this.performanceTimer.ElapsedTicks - startKerbinLOSTicks;
+				#endif
+				log.AppendFormat("\n\tKerbin LOS is blocked by {0}.", bodyOccludingKerbin.bodyName);
+
+				// If we're in range of the "nearest" (actually cheapest) relay, use it.
+				if (this.IsInRangeOf(this.nearestRelay))
+				{
+					log.AppendFormat("\n\t\tCan transmit to nearby relay {0}).",
+						this.nearestRelay == null ? "null" : this.nearestRelay.ToString()
+					);
+
+					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\t\tCan't transmit to nearby relay {0}.",
+						this.nearestRelay == null ? "null" : this.nearestRelay.ToString()
+					);
+
+					this.canTransmit = false;
+
+					// If the best occluded relay is cheaper than Kerbin, check it against the nearest relay.
+					// Since cheapestOccludedRelayRate is infinity if there are no occluded relays, this is safe
+					if (cheapestOccludedRelayRate < kerbinRelayRate)
+					{
+						log.AppendFormat("\n\t\t\tBest occluded relay is cheaper than Kerbin ({0} < {1})",
+							cheapestOccludedRelayRate, kerbinRelayRate);
+						
+						this.KerbinDirect = false;
+
+						// If the nearest relay is cheaper than the best occluded relay, pick it.
+						// Since cheapestRelayRate is infinity if there are no nearby relays, this is safe.
+						if (cheapestRelayRate < cheapestOccludedRelayRate)
+						{
+							log.AppendFormat("\n\t\t\t\t...but the nearest relay is cheaper ({0} < {1}), so picking it.",
+								cheapestRelayRate, cheapestOccludedRelayRate);
+							
+							this.targetRelay = this.nearestRelay;
+							this.firstOccludingBody = null;
+						}
+						// Otherwise, target the best occluded relay.
+						else
+						{
+							log.AppendFormat("\n\t\t\t\t...and cheaper than the nearest relay ({0} >= {1}), so picking it.",
+								cheapestRelayRate, cheapestOccludedRelayRate);
+							
+							this.targetRelay = bestOccludedRelay;
+							this.firstOccludingBody = bodyOccludingBestOccludedRelay;
+						}
+					}
+					// Otherwise, check Kerbin against the "nearest" (cheapest) relay.
+					else
+					{
+						log.AppendFormat("\n\t\t\tKerbin is cheaper than the best occluded relay ({0} >= {1})",
+							cheapestOccludedRelayRate, kerbinRelayRate);
+						
+						// If the "nearest" (cheapest) relay is cheaper than Kerbin, pick it.
+						// Since cheapestRelayRate is infinity if there are no nearby relays, this is safe.
+						if (cheapestRelayRate < kerbinRelayRate)
+						{
+							log.AppendFormat("\n\t\t\t\t...but the nearest relay is cheaper ({0} < {1}), so picking it.",
+								cheapestRelayRate, kerbinRelayRate);
+
+							// Since we have LOS, blank the first occluding body.
+							this.firstOccludingBody = null;
+
+							this.KerbinDirect = false;
+							this.targetRelay = this.nearestRelay;
+						}
+						// Otherwise, pick Kerbin.
+						else
+						{
+							log.AppendFormat("\n\t\t\t\t...and cheaper than the nearest relay ({0} >= {1}), so picking it.",
+								cheapestRelayRate, kerbinRelayRate);
+							
+							this.KerbinDirect = true;
+							this.firstOccludingBody = bodyOccludingKerbin;
+							this.targetRelay = null;
+						}
+					}
+				}
+			}
+			// If we do have LOS to Kerbin, try to prefer the closest of nearestRelay and Kerbin
+			else
+			{
+				#if BENCH
+				totalKerbinLOSTicks = this.performanceTimer.ElapsedTicks - startKerbinLOSTicks;
+				#endif
+
+				log.AppendFormat("\n\tKerbin is in LOS.");
+
+				// If the nearest relay is in range, we can transmit.
+				if (this.IsInRangeOf(this.nearestRelay))
+				{
+					log.AppendFormat("\n\t\tCan transmit to nearby relay {0} (in range).",
+						this.nearestRelay == null ? "null" : this.nearestRelay.ToString()
+					);
+
+					this.canTransmit = true;
+
+					// If the nearestRelay is closer than Kerbin, use it.
+					if (cheapestRelayRate < kerbinRelayRate)
+					{
+						log.AppendFormat("\n\t\t\tPicking relay {0} over Kerbin ({1} < {2}).",
+							this.nearestRelay == null ? "null" : this.nearestRelay.ToString(),
+							cheapestRelayRate, kerbinRelayRate);
+
+						this.KerbinDirect = false;
+						this.targetRelay = this.nearestRelay;
+					}
+					// Otherwise, Kerbin is closer, so use it.
+					else
+					{
+						log.AppendFormat("\n\t\t\tBut picking Kerbin over nearby relay {0} ({1} >= {2}).",
+							this.nearestRelay == null ? "null" : this.nearestRelay.ToString(),
+							cheapestRelayRate, kerbinRelayRate);
+
+						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\t\tCheapest relay {0} is out of range.",
+						this.nearestRelay == null ? "null" : this.nearestRelay.ToString()
+					);
+
+					// If Kerbin is in range, use it.
+					if (this.IsInRangeOf(Kerbin))
+					{
+						log.AppendFormat("\n\t\t\tCan transmit to Kerbin (in range).");
+
+						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\t\t\tCan't transmit to Kerbin (out of range).");
+
+						this.canTransmit = false;
+
+						// If the best occluded relay is cheaper than Kerbin, check it against the nearest relay.
+						// Since bestOccludedSqrDistance is infinity if there are no occluded relays, this is safe
+						if (cheapestOccludedRelayRate < kerbinRelayRate)
+						{
+							log.AppendFormat("\n\t\t\tBest occluded relay is closer than Kerbin ({0} < {1})",
+								cheapestOccludedRelayRate, kerbinRelayRate);
+							
+							this.KerbinDirect = false;
+
+							// If the nearest relay is closer than the best occluded relay, pick it.
+							// Since cheapestRelayRate is infinity if there are no nearby relays, this is safe.
+							if (cheapestRelayRate < cheapestOccludedRelayRate)
+							{
+								log.AppendFormat("\n\t\t\t\t...but the nearest relay is cheaper ({0} < {1}), so picking it.",
+									cheapestRelayRate, cheapestOccludedRelayRate);
+								
+								this.targetRelay = this.nearestRelay;
+								this.firstOccludingBody = null;
+							}
+							// Otherwise, target the best occluded relay.
+							else
+							{
+								log.AppendFormat("\n\t\t\t\t...and cheaper than the nearest relay ({0} >= {1}), so picking it.",
+									cheapestRelayRate, cheapestOccludedRelayRate);
+								
+								this.targetRelay = bestOccludedRelay;
+								this.firstOccludingBody = bodyOccludingBestOccludedRelay;
+							}
+						}
+						// Otherwise, check Kerbin against the nearest relay.
+						// Since we have LOS, blank the first occluding body.
+						else
+						{
+							log.AppendFormat("\n\t\t\tKerbin is cheaper than the best occluded relay ({0} >= {1})",
+								cheapestOccludedRelayRate, kerbinRelayRate);
+							
+							this.firstOccludingBody = null;
+
+							// If the nearest relay is cheaper than Kerbin, pick it.
+							// Since cheapestRelayRate is infinity if there are no nearby relays, this is safe.
+							if (cheapestRelayRate < kerbinRelayRate)
+							{
+								log.AppendFormat("\n\t\t\t\t...but the nearest relay is cheaper ({0} < {1}), so picking it.",
+									cheapestRelayRate, kerbinRelayRate);
+								
+								this.KerbinDirect = false;
+								this.targetRelay = this.nearestRelay;
+							}
+							// Otherwise, pick Kerbin.
+							else
+							{
+								log.AppendFormat("\n\t\t\t\t...and cheaper than the nearest relay ({0} >= {1}), so picking it.",
+									cheapestRelayRate, kerbinRelayRate);
+								
+								this.KerbinDirect = true;
+								this.targetRelay = null;
+							}
+						}
+					}
+				}
+			}
+
+			if (ARConfiguration.UseAdditiveRanges)
+			{
+				if (this.KerbinDirect)
+				{
+					this.NominalLinkSqrDistance = this.nominalTransmitDistance * ARConfiguration.KerbinNominalRange;
+					this.MaximumLinkSqrDistance = this.maxTransmitDistance * ARConfiguration.KerbinRelayRange;
+				}
+				else
+				{
+					this.NominalLinkSqrDistance = this.nominalTransmitDistance * this.targetRelay.nominalTransmitDistance;
+					this.MaximumLinkSqrDistance = this.maxTransmitDistance * this.targetRelay.maxTransmitDistance;
+				}
+			}
+			else
+			{
+				this.NominalLinkSqrDistance = this.nominalTransmitDistance * this.nominalTransmitDistance;
+				this.MaximumLinkSqrDistance = this.maxTransmitDistance * this.maxTransmitDistance;
+			}
+
+			if (this.canTransmit)
+			{
+				if (this.CurrentLinkSqrDistance < this.NominalLinkSqrDistance)
+				{
+					this.LinkStatus = ConnectionStatus.Optimal;
+				}
+				else
+				{
+					this.LinkStatus = ConnectionStatus.Suboptimal;
+				}
+			}
+			else
+			{
+				this.LinkStatus = ConnectionStatus.None;
+			}
+
+			#if BENCH
+			statusResolutionTicks = performanceTimer.ElapsedTicks - startKerbinLOSTicks - totalKerbinLOSTicks;
+			#endif
+
+			log.AppendFormat("\n{0}: Target search and status determination complete.", this.ToString());
+			
+			#if DEBUG
+			} catch (Exception ex) {
+				log.AppendFormat("\nCaught {0}: {1}\n{2}", ex.GetType().FullName, ex.ToString(), ex.StackTrace);
+			#if QUIT_ON_EXCEPTION
+				UnityEngine.Application.Quit();
+			#endif
+			} finally {
+			#endif
+			log.Print(false);
+			#if DEBUG
+			}
+			#endif
+
+			#if BENCH
+			AntennaRelay.searchTimer += (ulong)this.performanceTimer.ElapsedTicks;
+			AntennaRelay.searchCount++;
+			this.performanceTimer.Stop();
+
+			double averageSearchTime = (double)AntennaRelay.searchTimer / (double)AntennaRelay.searchCount;
+
+			if (AntennaRelay.searchCount >= 8000u / (ulong)ARConfiguration.UpdateDelay)
+			{
+				AntennaRelay.searchCount = 0u;
+				AntennaRelay.searchTimer = 0u;
+
+				AntennaRelay.averager.AddItem(averageSearchTime);
+				AntennaRelay.doubleAverageTime = (long)(AntennaRelay.averager.Average * 2d);
+			}
+
+			if (this.performanceTimer.ElapsedTicks > AntennaRelay.doubleAverageTime)
+			{
+				System.Text.StringBuilder sb = Tools.GetStringBuilder();
+
+				sb.AppendFormat(Tools.SIFormatter, "[AntennaRelay] FindNearestRelay search for {0}" +
+					" took significantly longer than average ({1:S3}s vs {2:S3}s)",
+					this.ToString(),
+					(double)this.performanceTimer.ElapsedTicks / (double)System.Diagnostics.Stopwatch.Frequency,
+					(double)AntennaRelay.averager.Average / (double)System.Diagnostics.Stopwatch.Frequency
+				);
+
+				sb.AppendFormat(Tools.SIFormatter, "\n\tVessel loop time: {0:S3}s",
+					(double)totalVesselLoopTicks / (double)System.Diagnostics.Stopwatch.Frequency
+				);
+
+				sb.AppendFormat(Tools.SIFormatter, "\n\t\tAverage vessel time for each of {1} vessels: {0:S3}s",
+					(double)totalVesselLoopTicks / (double)System.Diagnostics.Stopwatch.Frequency /
+					(double)usefulVesselCount,
+					usefulVesselCount
+				);
+
+				sb.AppendFormat(Tools.SIFormatter, "\n\t\tSlowest vessel LOS check: {0:S3}s to {1}",
+					(double)slowestLOSVesselTicks / (double)System.Diagnostics.Stopwatch.Frequency,
+					slowestLOSVesselName
+				);
+
+				sb.AppendFormat(Tools.SIFormatter, "\n\t\tSlowest circular relay check: {0:S3}s for {1}",
+					(double)slowestCircularVesselTicks / (double)System.Diagnostics.Stopwatch.Frequency,
+					slowestCircularVesselName
+				);
+
+				sb.AppendFormat(Tools.SIFormatter, "\n\tKerbin LOS check: {0:S3}s",
+					(double)totalKerbinLOSTicks / (double)System.Diagnostics.Stopwatch.Frequency
+				);
+
+				sb.AppendFormat(Tools.SIFormatter, "\n\tStatus resolution check: {0:S3}s",
+					(double)statusResolutionTicks / (double)System.Diagnostics.Stopwatch.Frequency
+				);
+
+				// sb.AppendFormat(Tools.SIFormatter, "", start)
+
+				this.LogWarning(sb.ToString());
+
+				Tools.PutStringBuilder(sb);
+			}
+			#endif
+		}
+
+		/// <summary>
+		/// Returns a <see cref="System.String"/> that represents the current <see cref="AntennaRange.AntennaRelay"/>.
+		/// </summary>
+		/// <returns>A <see cref="System.String"/> that represents the current <see cref="AntennaRange.AntennaRelay"/>.</returns>
+		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.AntennaRelay"/> class.
+		/// </summary>
+		/// <param name="module">The module reference underlying this AntennaRelay,
+		/// as an <see cref="AntennaRange.IAntennaRelay"/></param>
 		public AntennaRelay(IAntennaRelay module)
 		{
+			this.KerbinDirect = true;
 			this.moduleRef = module;
 
-			this.searchTimer = new System.Diagnostics.Stopwatch();
-			this.millisecondsBetweenSearches = 5000;
-
-			// HACK: This might not be safe in all circumstances, but since AntennaRelays are not built until Start,
-			// we hope it is safe enough.
-			this.Kerbin = FlightGlobals.Bodies.FirstOrDefault(b => b.name == "Kerbin");
-		}
-
-		static AntennaRelay()
-		{
-			var config = KSP.IO.PluginConfiguration.CreateForType<AntennaRelay>();
-
-			config.load();
-
-			AntennaRelay.requireLineOfSight = config.GetValue<bool>("requireLineOfSight", false);
-
-			config.save();
+			#if BENCH
+			AntennaRelay.relayCount++;
+			#endif
+
+			this.LogDebug("{0}: constructed {1}", this.GetType().Name, this.ToString());
 		}
 	}
 }

--- /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,168 @@
+// AntennaRange
+//
+// AntennaRange.cfg
+//
+// Copyright © 2014-2015, 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[!RemoteTech]
+{
+	@TechRequired = start
+
+	@MODULE[ModuleDataTransmitter]
+	{
+		@name = ModuleLimitedDataTransmitter
+		nominalRange = 6364
+		simpleRange = 20500000
+		maxPowerFactor = 8
+		maxDataFactor = 4
+	}
+
+	MODULE
+	{
+		name = ModuleScienceContainer
+
+		dataIsCollectable = true
+		dataIsStorable = false
+
+		storageRange = 2
+	}
+}
+
+@PART[mediumDishAntenna]:FOR[AntennaRange]:NEEDS[!RemoteTech]
+{
+	@MODULE[ModuleDataTransmitter]
+	{
+		@name = ModuleLimitedDataTransmitter
+		nominalRange = 3150000000
+		simpleRange = 18000000000
+		maxPowerFactor = 4
+		maxDataFactor = 8
+	}
+
+	MODULE
+	{
+		name = ModuleScienceContainer
+
+		dataIsCollectable = true
+		dataIsStorable = false
+
+		storageRange = 2
+	}
+}
+
+@PART[commDish]:FOR[AntennaRange]:NEEDS[!RemoteTech]
+{
+	@MODULE[ModuleDataTransmitter]
+	{
+		@name = ModuleLimitedDataTransmitter
+		@packetResourceCost /= 1.414213
+		nominalRange = 9250000000
+		simpleRange = 56250000000
+		maxPowerFactor = 16
+		maxDataFactor = 2
+	}
+
+	MODULE
+	{
+		name = ModuleScienceContainer
+
+		dataIsCollectable = true
+		dataIsStorable = false
+
+		storageRange = 2
+	}
+}
+
+@PART[HighGainAntenna]:FOR[AntennaRange]:NEEDS[!RemoteTech]
+{
+	@TechRequired = electronics
+	@description = Repurposed for medium range probes, the HG-55 provdes high speed directional data transmission.
+
+	@MODULE[ModuleDataTransmitter]
+	{
+		@name = ModuleLimitedDataTransmitter
+		nominalRange = 7774867578
+		simpleRange = 25030376544
+		maxPowerFactor = 2.6180339887498948
+		maxDataFactor = 9
+	}
+
+	MODULE
+	{
+		name = ModuleScienceContainer
+
+		dataIsCollectable = true
+		dataIsStorable = false
+
+		storageRange = 2
+	}
+}
+
+TRACKING_STATION_RANGES
+{
+	range = 800000
+	range = 200000000000
+	range = 2000000000000
+}
+
+EVA_MODULE
+{
+	name = ModuleLimitedDataTransmitter
+
+	nominalRange = 1389
+	simpleRange = 5000
+	maxPowerFactor = 1
+	maxDataFactor = 1
+
+	packetInterval = 0.2
+	packetSize = 1
+	packetResourceCost = 6.25
+
+	requiredResource = ElectricCharge
+}
+
+EVA_RESOURCE
+{
+	name = ElectricCharge
+	amount = 0
+	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
--- a/IAntennaRelay.cs
+++ b/IAntennaRelay.cs
@@ -2,7 +2,7 @@
 //
 // IAntennaRelay.cs
 //
-// Copyright © 2014, toadicus
+// Copyright © 2014-2015, toadicus
 // All rights reserved.
 //
 // Redistribution and use in source and binary forms, with or without modification,
@@ -31,41 +31,140 @@
 
 namespace AntennaRange
 {
-	/*
-	 * Interface defining the basic functionality of AntennaRelay modules for AntennaRange.
-	 * */
+	/// <summary>
+	/// Interface defining the basic functionality of AntennaRelay modules for AntennaRange.
+	/// </summary>
 	public interface IAntennaRelay
 	{
 		/// <summary>
 		/// Gets the parent Vessel.
 		/// </summary>
-		/// <value>The parent Vessel.</value>
 		Vessel vessel { get; }
+
+		/// <summary>
+		/// Gets the target <see cref="AntennaRange.IAntennaRelay"/>relay.
+		/// </summary>
+		IAntennaRelay targetRelay { get; }
+
+		/// <summary>
+		/// Gets or sets the data capacity of a packet, in MiT/packet
+		/// </summary>
+		/// <value>The data capacity of a packet, in MiT/packet</value>
+		float PacketSize { get; set; }
+
+		/// <summary>
+		/// Gets the base data capacity of a packet, in MiT/packet
+		/// </summary>
+		/// <value>The base data capacity of a packet, in MiT/packet</value>
+		float BasePacketSize { get; }
+
+		/// <summary>
+		/// Gets or sets the resource cost of a packet, in EC/packet
+		/// </summary>
+		/// <value>The resource cost of a packet, in EC/packet</value>
+		float PacketResourceCost { get; set; }
+
+		/// <summary>
+		/// Gets the base resource cost of a packet, in EC/packet
+		/// </summary>
+		/// <value>The base resource cost of a packet, in EC/packet</value>
+		float BasePacketResourceCost { get; }
+
+		/// <summary>
+		/// Gets the packet throttle.
+		/// </summary>
+		/// <value>The packet throttle in range [0..100].</value>
+		float PacketThrottle { get; }
+
+		/// <summary>
+		/// Gets the max data factor.
+		/// </summary>
+		/// <value>The max data factor.</value>
+		float MaxDataFactor { get; }
+
+		/// <summary>
+		/// Gets the data resource cost in EC/MiT.
+		/// </summary>
+		/// <value>The data resource cost in EC/MiT.</value>
+		double DataResourceCost { get; }
+
+		/// <summary>
+		/// Gets the current network resource rate in EC/MiT.
+		/// </summary>
+		/// <value>The current network resource rate in EC/MiT.</value>
+		RelayDataCost CurrentNetworkLinkCost { get; }
+
+		/// <summary>
+		/// Gets a value indicating whether this <see cref="AntennaRange.IAntennaRelay"/> Relay is communicating
+		/// directly with Kerbin.
+		/// </summary>
+		bool KerbinDirect { get; }
+
+		/// <summary>
+		/// The link distance, in meters, at which this relay behaves nominally.
+		/// </summary>
+		double NominalLinkSqrDistance { get; }
+
+		/// <summary>
+		/// The link distance, in meters, beyond which this relay cannot operate.
+		/// </summary>
+		double MaximumLinkSqrDistance { 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 CurrentLinkSqrDistance { get; }
+
+		/// <summary>
+		/// Gets the link status.
+		/// </summary>
+		ConnectionStatus LinkStatus { get; }
+
+		/// <summary>
+		/// Gets the nominal transmit distance at which the Antenna behaves just as prescribed by Squad's config.
+		/// </summary>
+		double nominalTransmitDistance { get; }
 
 		/// <summary>
 		/// The maximum distance at which this relay can operate.
 		/// </summary>
-		/// <value>The max transmit distance.</value>
-		float maxTransmitDistance { get; }
+		double maxTransmitDistance { get; }
 
 		/// <summary>
-		/// Gets a value indicating whether this <see cref="AntennaRange.ProtoDataTransmitter"/> has been checked during
-		/// the current relay attempt.
+		/// The first CelestialBody blocking line of sight to a 
 		/// </summary>
-		/// <value><c>true</c> if relay checked; otherwise, <c>false</c>.</value>
-		bool relayChecked { get; }
+		CelestialBody firstOccludingBody { get; }
+
+		/// <summary>
+		/// Gets the Part title.
+		/// </summary>
+		string Title { get; }
 
 		/// <summary>
 		/// Determines whether this instance can transmit.
+		/// <c>true</c> if this instance can transmit; otherwise, <c>false</c>.
 		/// </summary>
-		/// <returns><c>true</c> if this instance can transmit; otherwise, <c>false</c>.</returns>
 		bool CanTransmit();
+
+		/// <summary>
+		/// Recalculates the transmission rates.
+		/// </summary>
+		void RecalculateTransmissionRates();
+
+		/// <summary>
+		/// Finds the nearest relay.
+		/// </summary>
+		void FindNearestRelay();
+
+		/// <summary>
+		/// Recalculates the max range; useful for making sure we're using additive ranges when enabled.
+		/// </summary>
+		void RecalculateMaxRange();
+
+		/// <summary>
+		/// Returns a <see cref="System.String"/> that represents the current <see cref="AntennaRange.IAntennaRelay"/>.
+		/// </summary>
+		string ToString();
 	}
 }
 

--- a/ModuleLimitedDataTransmitter.cs
+++ b/ModuleLimitedDataTransmitter.cs
@@ -2,7 +2,7 @@
 //
 // ModuleLimitedDataTransmitter.cs
 //
-// Copyright © 2014, toadicus
+// Copyright © 2014-2015, toadicus
 // All rights reserved.
 //
 // Redistribution and use in source and binary forms, with or without modification,
@@ -29,79 +29,114 @@
 using KSP;
 using System;
 using System.Collections.Generic;
-using System.Linq;
 using System.Text;
-using ToadicusTools;
+using ToadicusTools.DebugTools;
+using ToadicusTools.Extensions;
+using ToadicusTools.Text;
 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
+	/// <summary>
+	/// <para>ModuleLimitedDataTransmitter is designed as a drop-in replacement for ModuleDataTransmitter, and handles
+	/// rangefinding, power scaling, and data scaling for antennas during science transmission.  Its functionality
+	/// varies with three tunables: nominalRange, maxPowerFactor, and maxDataFactor, set in .cfg files.</para>
+	/// 
+	/// <para>In general, the scaling functions assume the following relation:</para>
+	/// 
+	///	<para>	D² α P/R,</para>
+	/// 
+	/// <para>where D is the total transmission distance, P is the transmission power, and R is the data rate.</para>
+	/// </summary>
+	public class ModuleLimitedDataTransmitter
+		: ModuleDataTransmitter, IScienceDataTransmitter, IAntennaRelay, IModuleInfo
 	{
-		// If true, use a fixed power cost at the configured value and degrade data rates instead of increasing power
-		// requirements.
-		public static bool fixedPowerCost;
-
-		// Stores the packetResourceCost as defined in the .cfg file.
-		protected float _basepacketResourceCost;
-
-		// Stores the packetSize as defined in the .cfg file.
-		protected float _basepacketSize;
+		private const string tooltipSkinName = "PartTooltipSkin";
+		private static GUISkin partTooltipSkin;
+		private static GUIStyle partTooltipBodyStyle;
+		private static GUIStyle partTooltipHeaderStyle;
 
 		// Every antenna is a relay.
-		protected AntennaRelay relay;
-
-		// Keep track of vessels with transmitters for relay purposes.
-		protected List<Vessel> _relayVessels;
+		private AntennaRelay relay;
 
 		// 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.
+		private ScreenMessage ErrorMsg;
+
+		// Used in module info panes for part tooltips in the editor and R&D
+		private GUIContent moduleInfoContent;
+
+		/// <summary>
+		/// When additive ranges are enabled, the distance from Kerbin at which the antenna will perform exactly as
+		/// prescribed by packetResourceCost and packetSize.
+		/// </summary>
 		[KSPField(isPersistant = false)]
-		public float nominalRange;
-
+		public double nominalRange;
+
+		/// <summary>
+		/// When additive ranges are disabled, the distance from Kerbin at which the antenna will perform exactly as
+		/// prescribed by packetResourceCost and packetSize.
+		/// </summary>
+		[KSPField(isPersistant = false)]
+		public double simpleRange;
+
+		/// <summary>
+		/// Relay status string for use in action menus.
+		/// </summary>
+		[KSPField(isPersistant = false, guiActive = true, guiName = "Status")]
+		public string UIrelayStatus;
+
+		/// <summary>
+		/// Relay target string for use in action menus.
+		/// </summary>
 		[KSPField(isPersistant = false, guiActive = true, guiName = "Relay")]
-		public string UIrelayStatus;
-
+		public string UIrelayTarget;
+
+		/// <summary>
+		/// Transmit distance string for use in action menus.
+		/// </summary>
 		[KSPField(isPersistant = false, guiActive = true, guiName = "Transmission Distance")]
 		public string UItransmitDistance;
 
-		[KSPField(isPersistant = false, guiActive = true, guiName = "Maximum Distance")]
+		/// <summary>
+		/// The nominal range string for use in action menus.
+		/// </summary>
+		[KSPField(isPersistant = false, guiActive = true, guiName = "Nominal Range")]
+		public string UInominalLinkDistance;
+
+		/// <summary>
+		/// Maximum distance string for use in action menus.
+		/// </summary>
+		[KSPField(isPersistant = false, guiActive = true, guiName = "Maximum Range")]
 		public string UImaxTransmitDistance;
 
+		/// <summary>
+		/// Packet size string for use in action menus.
+		/// </summary>
 		[KSPField(isPersistant = false, guiActive = true, guiName = "Packet Size")]
 		public string UIpacketSize;
 
+		/// <summary>
+		/// Packet cost string for use in action menus.
+		/// </summary>
 		[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.
+		/// <summary>
+		/// The multiplier on packetResourceCost that defines the maximum power output of the antenna.  When the power
+		/// cost exceeds packetResourceCost * maxPowerFactor, transmission will fail.
+		/// </summary>
 		[KSPField(isPersistant = false)]
 		public float maxPowerFactor;
 
-		// The multipler on packetSize that defines the maximum data bandwidth of the antenna.
+		/// <summary>
+		/// The multipler on packetSize that defines the maximum data bandwidth of the antenna.
+		/// </summary>
 		[KSPField(isPersistant = false)]
 		public float maxDataFactor;
 
+		/// <summary>
+		/// The packet throttle.
+		/// </summary>
 		[KSPField(
 			isPersistant = true,
 			guiName = "Packet Throttle",
@@ -112,35 +147,253 @@
 		[UI_FloatRange(maxValue = 100f, minValue = 2.5f, stepIncrement = 2.5f)]
 		public float packetThrottle;
 
-		protected bool actionUIUpdate;
+		private bool actionUIUpdate;
 
 		/*
 		 * Properties
 		 * */
-		// Returns the parent vessel housing this antenna.
+		/// <summary>
+		/// Gets the parent Vessel.
+		/// </summary>
 		public new Vessel vessel
 		{
 			get
 			{
-				return base.vessel;
-			}
-		}
-
-		// Returns the distance to the nearest relay or Kerbin, whichever is closer.
-		public double transmitDistance
-		{
-			get
-			{
-				return this.relay.transmitDistance;
-			}
-		}
-
-		// Returns the maximum distance this module can transmit
-		public float maxTransmitDistance
-		{
-			get
-			{
-				return Mathf.Sqrt (this.maxPowerFactor) * this.nominalRange;
+				if (base.vessel != null)
+				{
+					return base.vessel;
+				}
+				else if (this.part != null && this.part.vessel != null)
+				{
+					return this.part.vessel;
+				}
+				else if (
+					this.part.protoPartSnapshot != null &&
+					this.part.protoPartSnapshot.pVesselRef != null &&
+					this.part.protoPartSnapshot.pVesselRef.vesselRef != null
+				)
+				{
+					return this.part.protoPartSnapshot.pVesselRef.vesselRef;
+				}
+				else
+				{
+					this.LogError("Vessel and/or part reference are null, returning null vessel.");
+					#if DEBUG && VERBOSE
+					this.LogError(new System.Diagnostics.StackTrace().ToString());
+					#endif
+					return null;
+				}
+			}
+		}
+
+		/// <summary>
+		/// Gets or sets the data capacity of a packet, in MiT/packet
+		/// </summary>
+		/// <value>The data capacity of a packet, in MiT/packet</value>
+		public float PacketSize
+		{
+			get
+			{
+				return this.packetSize;
+			}
+			set
+			{
+				this.packetSize = value;
+			}
+		}
+
+		/// <summary>
+		/// Gets the base data capacity of a packet, in MiT/packet
+		/// </summary>
+		/// <value>The base data capacity of a packet, in MiT/packet</value>
+		public float BasePacketSize
+		{
+			get;
+			private set;
+		}
+
+		/// <summary>
+		/// Gets or sets the resource cost of a packet, in EC/packet
+		/// </summary>
+		/// <value>The resource cost of a packet, in EC/packet</value>
+		public float PacketResourceCost
+		{
+			get
+			{
+				return this.packetResourceCost;
+			}
+			set
+			{
+				this.packetResourceCost = value;
+			}
+		}
+
+		/// <summary>
+		/// Gets the base resource cost of a packet, in EC/packet
+		/// </summary>
+		/// <value>The base resource cost of a packet, in EC/packet</value>
+		public float BasePacketResourceCost
+		{
+			get;
+			private set;
+		}
+
+		/// <summary>
+		/// Gets the packet throttle.
+		/// </summary>
+		/// <value>The packet throttle in range [0..100].</value>
+		public float PacketThrottle
+		{
+			get
+			{
+				return this.packetThrottle;
+			}
+		}
+
+		/// <summary>
+		/// Gets the max data factor.
+		/// </summary>
+		/// <value>The max data factor.</value>
+		public float MaxDataFactor
+		{
+			get
+			{
+				return this.maxDataFactor;
+			}
+		}
+
+		/// <summary>
+		/// Gets the target <see cref="AntennaRange.IAntennaRelay"/>relay.
+		/// </summary>
+		public IAntennaRelay targetRelay
+		{
+			get
+			{
+				if (this.relay == null)
+				{
+					return null;
+				}
+
+				return this.relay.targetRelay;
+			}
+		}
+
+		/// <summary>
+		/// Gets a value indicating whether this <see cref="AntennaRange.IAntennaRelay"/> Relay is communicating
+		/// directly with Kerbin.
+		/// </summary>
+		public bool KerbinDirect
+		{
+			get
+			{
+				if (this.relay != null)
+				{
+					return this.relay.KerbinDirect;
+				}
+
+				return false;
+			}
+		}
+
+		/// <summary>
+		/// Gets or sets the nominal link distance, in meters.
+		/// </summary>
+		public double NominalLinkSqrDistance
+		{
+			get
+			{
+				if (this.relay != null)
+				{
+					return this.relay.NominalLinkSqrDistance;
+				}
+
+				return 0d;
+			}
+		}
+
+		/// <summary>
+		/// Gets or sets the maximum link distance, in meters.
+		/// </summary>
+		public double MaximumLinkSqrDistance
+		{
+			get
+			{
+				if (this.relay != null)
+				{
+					return this.relay.MaximumLinkSqrDistance;
+				}
+
+				return 0d;
+			}
+		}
+
+		/// <summary>
+		/// Gets the distance to the nearest relay or Kerbin, whichever is closer.
+		/// </summary>
+		public double CurrentLinkSqrDistance
+		{
+			get
+			{
+				if (this.relay == null)
+				{
+					return double.PositiveInfinity;
+				}
+
+				return this.relay.CurrentLinkSqrDistance;
+			}
+		}
+
+		/// <summary>
+		/// Gets the link status.
+		/// </summary>
+		public ConnectionStatus LinkStatus
+		{
+			get
+			{
+				if (this.relay == null)
+				{
+					return ConnectionStatus.None;
+				}
+
+				return this.relay.LinkStatus;
+			}
+		}
+
+		/// <summary>
+		/// Gets the nominal transmit distance at which the Antenna behaves just as prescribed by Squad's config.
+		/// </summary>
+		public double nominalTransmitDistance
+		{
+			get
+			{
+				if (ARConfiguration.UseAdditiveRanges)
+				{
+					return this.nominalRange;
+				}
+				else
+				{
+					return this.simpleRange;
+				}
+			}
+		}
+
+		/// <summary>
+		/// The maximum distance at which this relay can operate.
+		/// </summary>
+		public double maxTransmitDistance
+		{
+			get;
+			protected set;
+		}
+
+		/// <summary>
+		/// The first CelestialBody blocking line of sight to a 
+		/// </summary>
+		public CelestialBody firstOccludingBody
+		{
+			get
+			{
+				return this.relay.firstOccludingBody;
 			}
 		}
 
@@ -170,50 +423,70 @@
 		 * 
 		 * 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
+		/// <summary>
+		/// Override ModuleDataTransmitter.DataRate to just return packetSize, because we want antennas to be scored in
+		/// terms of joules/byte
+		/// </summary>
 		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
+				if (this.relay == null)
 				{
 					return float.PositiveInfinity;
 				}
-			}
-		}
-
-		// Reports whether this antenna has been checked as a viable relay already in the current FindNearestRelay.
-		public bool relayChecked
-		{
-			get
-			{
-				return this.relay.relayChecked;
+
+				return this.relay.DataRate;
+			}
+		}
+
+		/// <summary>
+		/// Override ModuleDataTransmitter.DataResourceCost to just return packetResourceCost, because we want antennas
+		/// to be scored in terms of joules/byte
+		/// </summary>
+		public new double DataResourceCost
+		{
+			get
+			{
+				if (this.relay == null)
+				{
+					return double.PositiveInfinity;
+				}
+
+				return this.relay.DataResourceCost;
+			}
+		}
+
+		/// <summary>
+		/// Gets the current network resource rate in EC/MiT.
+		/// </summary>
+		/// <value>The current network resource rate in EC/MiT.</value>
+		public RelayDataCost CurrentNetworkLinkCost
+		{
+			get
+			{
+				if (this.relay == null)
+				{
+					return RelayDataCost.Infinity;
+				}
+
+				return this.relay.CurrentNetworkLinkCost;
+			}
+		}
+
+		/// <summary>
+		/// Gets the Part title.
+		/// </summary>
+		public string Title
+		{
+			get
+			{
+				if (this.part != null && this.part.partInfo != null)
+				{
+					return this.part.partInfo.title;
+				}
+
+				return string.Empty;
 			}
 		}
 
@@ -223,30 +496,65 @@
 		// Build ALL the objects.
 		public ModuleLimitedDataTransmitter () : base()
 		{
-			this.ErrorMsg = new ScreenMessage("", 4f, false, ScreenMessageStyle.UPPER_LEFT);
+			this.ErrorMsg = new ScreenMessage("", 4f, ScreenMessageStyle.UPPER_LEFT);
 			this.packetThrottle = 100f;
 		}
 
-		// At least once, when the module starts with a state on the launch pad or later, go find Kerbin.
+		/// <summary>
+		/// PartModule OnAwake override; runs at Unity Awake.
+		/// </summary>
+		public override void OnAwake()
+		{
+			base.OnAwake();
+
+			this.BasePacketSize = base.packetSize;
+			this.BasePacketResourceCost = base.packetResourceCost;
+			this.moduleInfoContent = new GUIContent();
+
+			this.LogDebug("{0} loaded:\n" +
+				"packetSize: {1}\n" +
+				"packetResourceCost: {2}\n" +
+				"nominalTransmitDistance: {3}\n" +
+				"maxPowerFactor: {4}\n" +
+				"maxDataFactor: {5}\n",
+				this,
+				base.packetSize,
+				this.BasePacketResourceCost,
+				this.nominalTransmitDistance,
+				this.maxPowerFactor,
+				this.maxDataFactor
+			);
+		}
+
+		/// <summary>
+		/// PartModule OnStart override; runs at Unity Start.
+		/// </summary>
+		/// <param name="state">State.</param>
 		public override void OnStart (StartState state)
 		{
 			base.OnStart (state);
 
+			this.RecalculateMaxRange();
+
 			if (state >= StartState.PreLaunch)
 			{
 				this.relay = new AntennaRelay(this);
+				this.relay.nominalTransmitDistance = this.nominalTransmitDistance;
 				this.relay.maxTransmitDistance = this.maxTransmitDistance;
 
-				this.UImaxTransmitDistance = Tools.MuMech_ToSI(this.maxTransmitDistance) + "m";
+				this.UImaxTransmitDistance = TextTools.Format("{0:S3}m", this.maxTransmitDistance);
 
 				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.
+		/// <summary>
+		/// 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.
+		/// </summary>
+		/// <param name="node"><see cref="ConfigNode"/> with data for this module.</param>
 		public override void OnLoad(ConfigNode node)
 		{
 			this.Fields.Load(node);
@@ -254,28 +562,434 @@
 
 			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
-			));
+			this.RecalculateMaxRange();
+		}
+
+		/// <summary>
+		/// Gets the human-friendly module title.
+		/// </summary>
+		public string GetModuleTitle()
+		{
+			return "Comms Transceiver";
+		}
+
+		/// <summary>
+		/// Returns drawTooltipWidget as a callback for part tooltips.
+		/// </summary>
+		public Callback<Rect> GetDrawModulePanelCallback()
+		{
+			return this.drawTooltipWidget;
+		}
+
+		// Called by Squad's part tooltip system when drawing tooltips.
+		// HACK: Currently hacks around Squad's extraneous layout box, see KSPModders issue #5118
+		private void drawTooltipWidget(Rect rect)
+		{
+			this.moduleInfoContent.text = this.GetInfo();
+
+			if (partTooltipSkin == null)
+			{
+				UnityEngine.Object[] skins = Resources.FindObjectsOfTypeAll(typeof(GUISkin));
+				GUISkin skin;
+				for (int sIdx = 0; sIdx < skins.Length; sIdx++)
+				{
+					skin = (GUISkin)skins[sIdx];
+
+					if (skin.name == tooltipSkinName)
+					{
+						partTooltipSkin = skin;
+						partTooltipBodyStyle = partTooltipSkin.customStyles[0];
+						partTooltipHeaderStyle = partTooltipSkin.customStyles[1];
+					}
+				}
+
+				if (partTooltipSkin == null)
+				{
+					this.LogError("Could not find GUISkin {0}?  Please report this!", tooltipSkinName);
+					return;
+				}
+				else
+				{
+					this.Log("Loaded GUISkin {0}", tooltipSkinName);
+				}
+			}
+
+			float width = rect.width;
+			float orgHeight = rect.height;
+			float height = partTooltipBodyStyle.CalcHeight(this.moduleInfoContent, width);
+
+			rect.height = height;
+
+			GUI.Box(rect, this.moduleInfoContent, partTooltipBodyStyle);
+			GUI.Label(rect, this.GetModuleTitle(), partTooltipHeaderStyle);
+
+			GUILayout.Space(height - orgHeight
+				- partTooltipBodyStyle.padding.bottom - partTooltipBodyStyle.padding.top
+				- 2f * (partTooltipBodyStyle.margin.bottom + partTooltipBodyStyle.margin.top)
+			);
+		}
+
+		/// <summary>
+		/// Returns an empty string, because we don't really have a "primary field" like some modules do.
+		/// </summary>
+		public string GetPrimaryField()
+		{
+			return string.Empty;
+		}
+
+		/// <summary>
+		/// Override ModuleDataTransmitter.GetInfo to add nominal and maximum range to the VAB description.
+		/// </summary>
+		public override string GetInfo()
+		{
+			using (PooledStringBuilder sb = PooledStringBuilder.Get())
+			{
+				string text;
+
+				sb.Append(base.GetInfo());
+
+				if (ARConfiguration.UseAdditiveRanges)
+				{
+					sb.AppendFormat("Nominal Range to Kerbin: {0:S3}m\n",
+						Math.Sqrt(this.nominalTransmitDistance * ARConfiguration.KerbinNominalRange)
+					);
+					sb.AppendFormat("Maximum Range to Kerbin: {0:S3}m",
+						Math.Sqrt(
+							this.nominalTransmitDistance * Math.Sqrt(this.maxPowerFactor) *
+							ARConfiguration.KerbinRelayRange
+						)
+					);
+				}
+				else
+				{
+					sb.AppendFormat("Nominal Range: {0:S3}m\n", this.nominalTransmitDistance);
+					sb.AppendFormat("Maximum Range: {0:S3}m", this.maxTransmitDistance);
+				}
+
+				text = sb.ToString();
+
+				return text;
+			}
+		}
+
+		/// <summary>
+		/// Determines whether this instance can transmit.
+		/// <c>true</c> if this instance can transmit; otherwise, <c>false</c>.
+		/// </summary>
+		public new bool CanTransmit()
+		{
+			if (this.part == null || this.relay == null)
+			{
+				return false;
+			}
+
+			switch (this.part.State)
+			{
+				case PartStates.DEAD:
+				case PartStates.DEACTIVATED:
+					this.LogDebug(
+						"{0}: {1} on {2} cannot transmit: {3}",
+						this.GetType().Name,
+						this.part.partInfo.title,
+						this.vessel.vesselName,
+						Enum.GetName(typeof(PartStates), this.part.State)
+					);
+					return false;
+				default:
+					break;
+			}
+
+			return this.relay.CanTransmit();
+		}
+
+		/// <summary>
+		/// Recalculates the transmission rates.
+		/// </summary>
+		public void RecalculateTransmissionRates()
+		{
+			if (this.relay != null)
+			{
+				this.relay.RecalculateTransmissionRates();
+			}
+		}
+
+		/// <summary>
+		/// Finds the nearest relay.
+		/// </summary>
+		public void FindNearestRelay()
+		{
+			if (this.relay != null)
+			{
+				this.relay.FindNearestRelay();
+			}
+		}
+
+		/// <summary>
+		/// Override ModuleDataTransmitter.TransmitData to check against CanTransmit and fail out when CanTransmit
+		/// returns false.
+		/// </summary>
+		/// <param name="dataQueue">List of <see cref="ScienceData"/> to transmit.</param>
+		public new void TransmitData(List<ScienceData> dataQueue)
+		{
+			this.LogDebug(
+				"TransmitData(List<ScienceData> dataQueue, Callback callback) called.  dataQueue.Count={0}",
+				dataQueue.Count
+			);
+
+			if (this.CanTransmit())
+			{
+				ScreenMessages.PostScreenMessage(this.buildTransmitMessage(), 4f, ScreenMessageStyle.UPPER_LEFT);
+
+				this.LogDebug(
+					"CanTransmit in TransmitData, calling base.TransmitData with dataQueue=[{0}] and callback={1}",
+					dataQueue.SPrint()
+				);
+
+				base.TransmitData(dataQueue);
+			}
+			else
+			{
+				this.LogDebug("{0} unable to transmit during TransmitData.", this.part.partInfo.title);
+
+				var logger = PooledDebugLogger.New(this);
+
+				IList<ModuleScienceContainer> vesselContainers = this.vessel.getModulesOfType<ModuleScienceContainer>();
+				ModuleScienceContainer scienceContainer;
+				for (int cIdx = 0; cIdx < vesselContainers.Count; cIdx++)
+				{
+					scienceContainer = vesselContainers[cIdx];
+
+					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>();
+
+					ScienceData data;
+					for (int dIdx = 0; dIdx < dataQueue.Count; dIdx++)
+					{
+						data = dataQueue[dIdx];
+						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)
+				{
+					using (PooledStringBuilder sb = PooledStringBuilder.Get())
+					{
+						sb.Append('[');
+						sb.Append(this.part.partInfo.title);
+						sb.AppendFormat("]: {0} data items could not be saved: no space available in data containers.\n");
+						sb.Append("Data to be discarded:\n");
+
+						ScienceData data;
+						for (int dIdx = 0; dIdx < dataQueue.Count; dIdx++)
+						{
+							data = dataQueue[dIdx];
+							sb.AppendFormat("\t{0}\n", data.title);
+						}
+
+						ScreenMessages.PostScreenMessage(sb.ToString(), 4f, ScreenMessageStyle.UPPER_LEFT);
+
+						this.LogDebug(sb.ToString());
+					}
+				}
+
+				this.PostCannotTransmitError();
+			}
+
+			this.LogDebug(
+				"distance: " + this.CurrentLinkSqrDistance
+				+ " packetSize: " + this.packetSize
+				+ " packetResourceCost: " + this.packetResourceCost
+			);
+		}
+
+		/// <summary>
+		/// Override ModuleDataTransmitter.StartTransmission to check against CanTransmit and fail out when CanTransmit
+		/// returns false.
+		/// </summary>
+		public new void StartTransmission()
+		{
+			this.LogDebug(
+				"distance: " + this.CurrentLinkSqrDistance
+				+ " packetSize: " + this.packetSize
+				+ " packetResourceCost: " + this.packetResourceCost
+				);
+
+			if (this.CanTransmit())
+			{
+				ScreenMessages.PostScreenMessage(this.buildTransmitMessage(), 4f, ScreenMessageStyle.UPPER_LEFT);
+
+				base.StartTransmission();
+			}
+			else
+			{
+				this.PostCannotTransmitError ();
+			}
+		}
+
+		/// <summary>
+		/// MonoBehaviour Update
+		/// </summary>
+		public void Update()
+		{
+			if (this.actionUIUpdate)
+			{
+				this.UImaxTransmitDistance = TextTools.Format("{0:S3}m",
+					Math.Sqrt(this.MaximumLinkSqrDistance));
+				this.UInominalLinkDistance = TextTools.Format("{0:S3}m",
+					Math.Sqrt(this.NominalLinkSqrDistance));
+				
+				if (this.CanTransmit())
+				{
+					this.UIrelayStatus = this.LinkStatus.ToString();
+					this.UItransmitDistance = TextTools.Format("{0:S3}m",
+						Math.Sqrt(this.CurrentLinkSqrDistance));
+					this.UIpacketSize = TextTools.Format("{0:S3}MiT", this.DataRate);
+					this.UIpacketCost = TextTools.Format("{0:S3}EC", this.DataResourceCost);
+				}
+				else
+				{
+					if (this.relay.firstOccludingBody == null)
+					{
+						this.UItransmitDistance = TextTools.Format("{0:S3}m",
+							Math.Sqrt(this.CurrentLinkSqrDistance));
+						this.UIrelayStatus = "Out of range";
+					}
+					else
+					{
+						this.UItransmitDistance = "N/A";
+						this.UIrelayStatus = TextTools.Format("Blocked by {0}", this.relay.firstOccludingBody.bodyName);
+					}
+					this.UIpacketSize = "N/A";
+					this.UIpacketCost = "N/A";
+				}
+
+				if (this.KerbinDirect)
+				{
+					this.UIrelayTarget = AntennaRelay.Kerbin.bodyName;
+				}
+				else
+				{
+					if (this.targetRelay != null)
+					{
+						this.UIrelayTarget = this.targetRelay.ToString();
+					}
+					else
+					{
+						this.UIrelayTarget = "A mysterious null entity";
+					}
+				}
+			}
+		}
+
+		/// <summary>
+		/// Recalculates the max range; useful for making sure we're using additive ranges when enabled.
+		/// </summary>
+		public void RecalculateMaxRange()
+		{
+			this.maxTransmitDistance = Math.Sqrt(this.maxPowerFactor) * this.nominalTransmitDistance;
+
+			#if DEBUG
+			this.Log("Recalculated max range: sqrt({0}) * {1} = {2}",
+				this.maxPowerFactor, this.nominalTransmitDistance, this.maxTransmitDistance);
+			#endif
+		}
+
+		/// <summary>
+		/// Returns a <see cref="System.String"/> that represents the current <see cref="AntennaRange.ModuleLimitedDataTransmitter"/>.
+		/// </summary>
+		/// <returns>A <see cref="System.String"/> that represents the current <see cref="AntennaRange.ModuleLimitedDataTransmitter"/>.</returns>
+		public override string ToString()
+		{
+			using (PooledStringBuilder sb = PooledStringBuilder.Get())
+			{
+				string msg;
+
+				if (this.part != null && this.part.partInfo != null)
+				{
+					sb.Append(this.part.partInfo.title);
+				}
+				else
+				{
+					sb.Append(this.GetType().Name);
+				}
+
+				if (vessel != null)
+				{
+					sb.Append(" on ");
+					sb.Append(vessel.vesselName);
+				}
+				else if (
+					this.part != null &&
+					this.part.protoPartSnapshot != null &&
+					this.part.protoPartSnapshot != null &&
+					this.part.protoPartSnapshot.pVesselRef != null)
+				{
+					sb.Append(" on ");
+					sb.Append(this.part.protoPartSnapshot.pVesselRef.vesselName);
+				}
+
+				msg = sb.ToString();
+
+				return msg;
+			}
+		}
+
+		// When we catch an onPartActionUICreate event for our part, go ahead and update every frame to look pretty.
+		private void onPartActionUICreate(Part eventPart)
+		{
+			if (eventPart == base.part)
+			{
+				this.actionUIUpdate = true;
+			}
+		}
+
+		// When we catch an onPartActionUIDismiss event for our part, stop updating every frame to look pretty.
+		private void onPartActionUIDismiss(Part eventPart)
+		{
+			if (eventPart == base.part)
+			{
+				this.actionUIUpdate = false;
+			}
 		}
 
 		// Post an error in the communication messages describing the reason transmission has failed.  Currently there
 		// is only one reason for this.
-		protected void PostCannotTransmitError()
+		private void PostCannotTransmitError()
 		{
 			string ErrorText = string.Intern("Unable to transmit: no visible receivers in range!");
 
@@ -288,280 +1002,73 @@
 				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 (fixedPowerCost || this.transmitDistance <= this.nominalRange)
-			{
-				base.packetResourceCost = this._basepacketResourceCost;
-			}
-			else
-			{
-				base.packetResourceCost = this._basepacketResourceCost
-					* (float)Math.Pow (this.transmitDistance / this.nominalRange, 2);
-			}
-
-			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 (!fixedPowerCost && 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);
-			}
-
-			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()
-		{
-			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 ");
-
-				if (this.relay.nearestRelay == null)
-				{
-					message.Append("directly to Kerbin.");
+			this.LogDebug(this.ErrorMsg.message);
+
+			ScreenMessages.PostScreenMessage(this.ErrorMsg);
+		}
+
+		private string buildTransmitMessage()
+		{
+			using (PooledStringBuilder sb = PooledStringBuilder.Get())
+			{
+				string msg;
+
+				sb.Append("[");
+				sb.Append(base.part.partInfo.title);
+				sb.Append("]: ");
+
+				sb.Append("Beginning transmission ");
+
+				if (this.KerbinDirect)
+				{
+					sb.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
-			{
-				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 ");
-
-				if (this.relay.nearestRelay == null)
-				{
-					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 = string.Intern("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 = string.Intern("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";
-				}
-			}
-		}
-
-		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);
-			}
-
-			return msg.ToString();
-		}
-
+					sb.Append("via ");
+					sb.Append(this.relay.targetRelay);
+				}
+
+				msg = sb.ToString();
+
+				return msg;
+			}
+		}
+
+		#if DEBUG
 		// 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}\n" +
-				"NearestRelay: {12}\n" +
-				"Vessel ID: {13}",
-				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.FindNearestRelay(),
-				this.vessel.id
-				);
-			Tools.PostDebugMessage(msg);
+			if (this.relay != null)
+				this.relay.RecalculateTransmissionRates();
+
+			DebugPartModule.DumpClassObject(this);
 		}
 
 		[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)]
+			using (PooledStringBuilder sb = PooledStringBuilder.Get())
+			{
+				sb.Append("Dumping FlightGlobals.Vessels:");
+
+				Vessel vessel;
+				for (int i = 0; i < FlightGlobals.Vessels.Count; i++)
+				{
+					vessel = FlightGlobals.Vessels[i];
+					sb.AppendFormat("\n'{0} ({1})'", vessel.vesselName, vessel.id);
+				}
+		    
+				ToadicusTools.Logging.PostDebugMessage(sb.ToString());
+			}
+		}
+		 
+		/*[KSPEvent (guiName = "Dump RelayDB", active = true, guiActive = true)]
 		public void DumpRelayDB()
 		{
 			RelayDatabase.Instance.Dump();
-		}
+		}*/
 		#endif
 	}
 }

--- a/Properties/AssemblyInfo.cs
+++ b/Properties/AssemblyInfo.cs
@@ -2,7 +2,7 @@
 //
 // AssemblyInfo.cs
 //
-// Copyright © 2014, toadicus
+// Copyright © 2014-2015, toadicus
 // All rights reserved.
 //
 // Redistribution and use in source and binary forms, with or without modification,
@@ -39,7 +39,7 @@
 // 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.4.*")]
+[assembly: AssemblyVersion("1.10.3.*")]
 // 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)]

--- a/ProtoAntennaRelay.cs
+++ b/ProtoAntennaRelay.cs
@@ -2,7 +2,7 @@
 //
 // ProtoAntennaRelay.cs
 //
-// Copyright © 2014, toadicus
+// Copyright © 2014-2015, toadicus
 // All rights reserved.
 //
 // Redistribution and use in source and binary forms, with or without modification,
@@ -28,33 +28,185 @@
 
 using KSP;
 using System;
-using System.Linq;
 using ToadicusTools;
+using ToadicusTools.Text;
 
 namespace AntennaRange
 {
-	/*
-	 * Wrapper class for ProtoPartModuleSnapshot extending AntennaRelay and implementing IAntennaRelay.
-	 * This is used for finding relays in unloaded Vessels.
-	 * */
+	/// <summary>
+	/// Wrapper class for ProtoPartModuleSnapshot extending AntennaRelay and implementing IAntennaRelay.
+	/// This is used for finding relays in unloaded Vessels.
+	/// </summary>
 	public class ProtoAntennaRelay : AntennaRelay, IAntennaRelay
 	{
 		// Stores the prototype part so we can make sure we haven't exploded or so.
-		protected ProtoPartSnapshot protoPart;
-
+		private ProtoPartSnapshot protoPart;
+
+		/// <summary>
+		/// Gets the parent Vessel.
+		/// </summary>
 		public override Vessel vessel
 		{
 			get
 			{
-				return this.protoPart.pVesselRef.vesselRef;
-			}
-		}
-
-		/// <summary>
-		/// The maximum distance at which this transmitter can operate.
-		/// </summary>
-		/// <value>The max transmit distance.</value>
-		public override float maxTransmitDistance
+				if (
+					this.protoPart != null &&
+					this.protoPart.pVesselRef != null &&
+					this.protoPart.pVesselRef.vesselRef != null
+				)
+				{
+					return this.protoPart.pVesselRef.vesselRef;
+				}
+				else
+				{
+					this.LogError("Could not fetch vessel!  {0}{1}{2}",
+						this.protoPart == null ? "\n\tprotoPart=null" : string.Empty,
+						this.protoPart != null && this.protoPart.pVesselRef == null ?
+							"\n\tthis.protoPart.pVesselRef=null" : string.Empty,
+						this.protoPart != null && this.protoPart.pVesselRef != null &&
+							this.protoPart.pVesselRef.vesselRef == null ?
+							"\n\tthis.protoPart.pVesselRef.vesselRef=null" : string.Empty
+					);
+					return null;
+				}
+			}
+		}
+
+		/// <summary>
+		/// Gets or sets the data capacity of a packet, in MiT/packet
+		/// </summary>
+		/// <value>The data capacity of a packet, in MiT/packet</value>
+		public float PacketSize
+		{
+			get
+			{
+				if (this.moduleRef == null)
+				{
+					return float.NaN;
+				}
+
+				return this.moduleRef.PacketSize;
+			}
+			set
+			{
+				if (this.moduleRef == null)
+				{
+					return;
+				}
+
+				this.moduleRef.PacketSize = value;
+			}
+		}
+
+		/// <summary>
+		/// Gets the base data capacity of a packet, in MiT/packet
+		/// </summary>
+		/// <value>The base data capacity of a packet, in MiT/packet</value>
+		public float BasePacketSize
+		{
+			get
+			{
+				if (this.moduleRef == null)
+				{
+					return float.NaN;
+				}
+
+				return this.moduleRef.BasePacketSize;
+			}
+		}
+
+		/// <summary>
+		/// Gets or sets the resource cost of a packet, in EC/packet
+		/// </summary>
+		/// <value>The resource cost of a packet, in EC/packet</value>
+		public float PacketResourceCost
+		{
+			get
+			{
+				if (this.moduleRef == null)
+				{
+					return float.NaN;
+				}
+
+				return this.moduleRef.PacketResourceCost;
+			}
+			set
+			{
+				if (this.moduleRef == null)
+				{
+					return;
+				}
+
+				this.moduleRef.PacketResourceCost = value;
+			}
+		}
+
+		/// <summary>
+		/// Gets the base resource cost of a packet, in EC/packet
+		/// </summary>
+		/// <value>The base resource cost of a packet, in EC/packet</value>
+		public float BasePacketResourceCost
+		{
+			get
+			{
+				if (this.moduleRef == null)
+				{
+					return float.NaN;
+				}
+
+				return this.moduleRef.BasePacketResourceCost;
+			}
+		}
+
+		/// <summary>
+		/// Gets the packet throttle.
+		/// </summary>
+		/// <value>The packet throttle in range [0..100].</value>
+		public float PacketThrottle
+		{
+			get
+			{
+				if (this.moduleRef == null)
+				{
+					return float.NaN;
+				}
+
+				return this.moduleRef.PacketThrottle;
+			}
+		}
+
+		/// <summary>
+		/// Gets the max data factor.
+		/// </summary>
+		/// <value>The max data factor.</value>
+		public float MaxDataFactor
+		{
+			get
+			{
+				if (this.moduleRef == null)
+				{
+					return float.NaN;
+				}
+
+				return this.moduleRef.MaxDataFactor;
+			}
+		}
+
+		/// <summary>
+		/// Gets the nominal transmit distance at which the Antenna behaves just as prescribed by Squad's config.
+		/// </summary>
+		public override double nominalTransmitDistance
+		{
+			get
+			{
+				return this.moduleRef.nominalTransmitDistance;
+			}
+		}
+
+		/// <summary>
+		/// The maximum distance at which this relay can operate.
+		/// </summary>
+		public override double maxTransmitDistance
 		{
 			get
 			{
@@ -63,37 +215,35 @@
 		}
 
 		/// <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
-			{
-				return this.protoPart.partInfo.title;
-			}
-		}
-
+		public string Title
+		{
+			get
+			{
+				if (this.protoPart != null && this.protoPart.partInfo != null)
+				{
+					return this.protoPart.partInfo.title;
+				}
+
+				return string.Empty;
+			}
+		}
+
+		/// <summary>
+		/// Determines whether this instance can transmit.
+		/// <c>true</c> if this instance can transmit; otherwise, <c>false</c>.
+		/// </summary>
 		public override bool CanTransmit()
 		{
 			PartStates partState = (PartStates)this.protoPart.state;
 			if (partState == PartStates.DEAD || partState == PartStates.DEACTIVATED)
 			{
-				Tools.PostDebugMessage(string.Format(
+				Logging.PostDebugMessage(string.Format(
 					"{0}: {1} on {2} cannot transmit: {3}",
 					this.GetType().Name,
-					this.title,
+					this.Title,
 					this.vessel.vesselName,
 					Enum.GetName(typeof(PartStates), partState)
 				));
@@ -102,31 +252,49 @@
 			return base.CanTransmit();
 		}
 
+		/// <summary>
+		/// Recalculates the max range; useful for making sure we're using additive ranges when enabled.
+		/// </summary>
+		public void RecalculateMaxRange()
+		{
+			if (this.moduleRef != null)
+			{
+				this.moduleRef.RecalculateMaxRange();
+			}
+		}
+
+		/// <summary>
+		/// Returns a <see cref="System.String"/> that represents the current <see cref="AntennaRange.ProtoAntennaRelay"/>.
+		/// </summary>
+		/// <returns>A <see cref="System.String"/> that represents the current <see cref="AntennaRange.ProtoAntennaRelay"/>.</returns>
 		public override string ToString()
 		{
-			return string.Format(
-				"{0} on {1} (proto)",
-				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>
+			using (PooledStringBuilder sb = PooledStringBuilder.Get())
+			{
+				sb.Append(this.Title);
+
+				if (this.protoPart != null && this.protoPart.pVesselRef != null)
+				{
+					sb.AppendFormat(" on {0}", this.protoPart.pVesselRef.vesselName);
+				}
+
+				return sb.ToString();
+			}
+		}
+
+		/// <summary>
+		/// Initializes a new instance of the <see cref="AntennaRange.AntennaRelay"/> class.
+		/// </summary>
+		/// <param name="prefabRelay">The module reference underlying this AntennaRelay,
+		/// as an <see cref="AntennaRange.IAntennaRelay"/></param>
+		/// <param name="pps">The prototype partreference on which the module resides.</param>
 		public ProtoAntennaRelay(IAntennaRelay prefabRelay, ProtoPartSnapshot pps) : base(prefabRelay)
 		{
 			this.protoPart = pps;
-		}
-
-		~ProtoAntennaRelay()
-		{
-			Tools.PostDebugMessage(string.Format(
-				"{0}: destroyed",
-				this.ToString()
-			));
+
+			this.Log("constructed ({0})", this.GetType().Name);
+
+			this.RecalculateMaxRange();
 		}
 	}
 }

file:b/README.md (new)
--- /dev/null
+++ b/README.md
@@ -1,1 +1,212 @@
-
+# AntennaRange
+A KSP mod that enforces and encourages the use of the bigger antennas.
+
+# For Part Developers
+## The Fields
+AntennaRange extends and augments the functionality of the stock ModuleDataTransmitter through the new `ModuleLimitedDataTransmitter` class. This class uses four additional configurable fields to define the part's behavior.
+
+`nominalRange` is the range, in meters, at which the part should function identically to the stock module, i.e. without any modification to the power cost or packet size. This is used along with maxPowerFactor to calculate the maximum range of the part.  
+`simpleRange` is the same as nominalRange, but is used when the mod is in "simple" mode. In general it will probably need to be a much larger number than nominalRange.  
+`maxPowerFactor` effectively sets the maximum range of the antenna by essentially capping how much the power may be "turned up" to get better range. I originally used 8 for this number, because it felt right. I've since used 4 (for my DTS) and 16 (for my Comm. 88-88). You don't want this number to be too high, or small probes will go uncontrollable a lot when transmitting.  
+`maxDataFactor` defines the maximum "speed up" bonus that comes from using antennas at less their nominal range. I originally used 4 for this number for all parts; the DTS has a higher bonus now.
+
+Note that all of the fields needed for Squad's `ModuleDataTransmitter` still need to be filled out. Depending on how you're defining your parts, they might need to go in your AntennaRange patch, or they might already be defined on the base part.
+
+## The Mechanic
+In general, the scaling functions assume the relation `D² α P/R,` where D is the total transmission distance, P is the transmission power, and R is the data rate.  Data rate increases as range decreases below nominalRange: `R α nominalRange² / D²`.  By default, power use increases as range increases above `nominalRange`: `P α D² / nominalRange²`.  Optionally, power use may remain fixed, and data rate instead decreases as range increases above `nominalRange`: `R α nominalRange² / D²`.
+
+## Patch Conventions
+To maximize cross-compatibility, please consider the following conventions for ModuleManager patches regarding AntennaRange:
+
+When providing new definitions for your own parts, always specify a `:FOR[YourModHere]` pass name.
+Whenever changing default AntennaRange definitions (e.g. if you were going to rebalance my antennas to suit your mod), please do so in the `:AFTER[AntennaRange]` pass.
+I recommend providing all optional functionality (e.g. enabling RemoteTech vs. AntennaRange modules) in separate patches using `:NEEDS[]` blocks.
+
+A sample AntennaRange configuration for an all-new mod part might look like this:
+```
+@PART[modPartName]:FOR[YourModName]:NEEDS[AntennaRange,!RemoteTech]
+{
+	MODULE
+	{
+		// ### Module Definition ###
+		name = ModuleLimitedDataTransmitter
+		
+		// ### Squad Definitions ###
+		// Delay between transmission packets, in seconds
+		packetInterval = 0.10
+		
+		// Data capacity of nominal transmission packets, in MiT
+		packetSize = 2
+		
+		// Resource cost of nominal transmission packets, in units
+		packetResourceCost = 20.0
+		
+		// Resource name to be consumed by transmission
+		requiredResource = ElectricCharge
+		
+		// Animation module index, 0-based, of the antenna extend/retract animation
+		DeployFxModules = 0
+		
+		// ### AntennaRange Defintions ###
+		// Range, in meters, at which the antenna behaves per the "nominal" characteristics above
+		// Used with "additive" ranges.
+		nominalRange = 10000000000
+		
+		// Range, in meters, at which the antenna behaves per the "nominal" characteristics above
+		// Used with "simple" ranges.
+		simpleRange = 56250000000
+		
+		// The maxmimum multiplier on packetResourceCost, essentially defining the maximum power output of the
+		// transmitter.  Maximum range is defined as: maxTransmitDistance = nominalRange * sqrt(maxPowerFactor)
+		maxPowerFactor = 16
+		
+		// The maximum multiplier on packetSize, essentially defining the maximum data throughput of the
+		// transmitter.
+		maxDataFactor = 2
+	}
+	
+	// We add this ModuleScienceContainer so that when transmission fails the antennas can try to stash the data instead of dumping it to the void.
+	MODULE
+	{
+		name = ModuleScienceContainer
+
+		dataIsCollectable = true
+		dataIsStorable = false
+
+		storageRange = 2
+	}
+}
+```
+
+This example assumes that the base part definition does not include a `ModuleDataTransmitter` module, or any RT modules. If the base part definition includes a `ModuleDataTransmitter` module, a sample AntennaRange patch could look like this:
+```
+@PART[modPartName]:FOR[YourModName]:NEEDS[AntennaRange,!RemoteTech]
+{
+	@MODULE[ModuleDataTransmitter]
+	{
+		@name = ModuleLimitedDataTransmitter
+		nominalRange = 10000000000
+		simpleRange = 56250000000
+		maxPowerFactor = 16
+		maxDataFactor = 2
+	}
+	
+	// We add this ModuleScienceContainer so that when transmission fails the antennas can try to stash the data instead of dumping it to the void.
+	MODULE
+	{
+		name = ModuleScienceContainer
+
+		dataIsCollectable = true
+		dataIsStorable = false
+
+		storageRange = 2
+	}
+}
+```
+
+IIRC, RemoteTech parts should not have `ModuleDataTransmitter` definitions. In that case, to facilitate RT, AR, and Stock compatibility, a suite of patches like this might be appropriate:
+
+```
+// If we don't have RemoteTech, add a stock ModuleDataTransmitter first.
+@PART[modPartName]:NEEDS[!RemoteTech]:BEFORE[YourModName]
+{
+	MODULE
+	{
+		// ### Module Definition ###
+		name = ModuleDataTransmitter
+		
+		// ### Squad Definitions ###
+		// Delay between transmission packets, in seconds
+		packetInterval = 0.10
+		
+		// Data capacity of nominal transmission packets, in MiT
+		packetSize = 2
+		
+		// Resource cost of nominal transmission packets, in units
+		packetResourceCost = 20.0
+		
+		// Resource name to be consumed by transmission
+		requiredResource = ElectricCharge
+		
+		// Animation module index, 0-based, of the antenna extend/retract animation
+		DeployFxModules = 0
+	}
+}
+
+// If AntennaRange is installed, convert that to a ModuleLimitedDataTransmitter
+@PART[modPartName]:NEEDS[AntennaRange,!RemoteTech]:FOR[YourModName]
+{
+	@MODULE[ModuleDataTransmitter]
+	{
+		// ### Module Redefinition ###
+		@name = ModuleLimitedDataTransmitter
+		
+		// ### AntennaRange Defintions ###
+		// Range, in meters, at which the antenna behaves per the "nominal" characteristics above
+		// Used with "additive" ranges.
+		nominalRange = 10000000000
+		
+		// Range, in meters, at which the antenna behaves per the "nominal" characteristics above
+		// Used with "simple" ranges.
+		simpleRange = 56250000000
+		
+		// The maxmimum multiplier on packetResourceCost, essentially defining the maximum power output of the
+		// transmitter.  Maximum range is defined as: maxTransmitDistance = nominalRange * sqrt(maxPowerFactor)
+		maxPowerFactor = 16
+		
+		// The maximum multiplier on packetSize, essentially defining the maximum data throughput of the
+		// transmitter.
+		maxDataFactor = 2
+	}
+
+	// We add this ModuleScienceContainer so that when transmission fails the antennas can try to stash the data instead of dumping it to the void.
+	MODULE
+	{
+		name = ModuleScienceContainer
+
+		dataIsCollectable = true
+		dataIsStorable = false
+
+		storageRange = 2
+	}
+}
+
+// If RemoteTech is installed, do their module(s) instead
+@PART[modPartName]:NEEDS[RemoteTech]:FOR[YourModName]
+{
+	// RemoteTech module(s) here
+}
+```
+
+## Useful Formulas
+
+### Per Antenna
+`nominalRange` is a given, and is never calculated
+`maxPowerFactor` is a given, and is never calculated
+`maxTransmitDistance = nominalRange * sqrt(maxPowerFactor)`
+
+### Per Link
+A "link" is any connected pair of antennas.  
+`NominalLinkDistance = sqrt(nominalRange1 * nominalRange2)`  
+`MaxLinkDistance = sqrt(maxTransmitDistance1 * maxTransmitDistance2)`
+
+Therefore, to find the `MaxLinkDistance` from two sets of `nominalRange` and `maxPowerFactor`:  
+`MaxLinkDistance = sqrt(nominalRange1 * sqrt(maxPowerFactor1) * nominalRange2 * sqrt(maxPowerFactor2))`
+
+To find a single antenna's `nominalRange` from a desired `maxTransmitDistance` given its `maxPowerFactor`:  
+`nominalRange = maxTransmitDistance / sqrt(maxPowerFactor)`
+
+To find a single antenna's desired maximum range given the desired maximum link distance and another set `maxTransmitDistance`:  
+`maxTransmitDistance1 = MaxLinkDistance * MaxLinkDistance / maxTransmitDistance2`
+
+Remember that `maxPowerFactor` may differ between antennas (and does, in my lastest configs: longAntenna is 8, mediumDish is 4, commDish is 16).
+
+Currently Kerbin's `maxPowerFactor` is hard-coded as 8.
+
+Feel free to use this spreadsheet for balancing antennas if it's useful to you: https://goo.gl/ChsbfL
+
+## On Balance
+In my configs I've balanced the three stock antennas to cover all of the stock solar system. Since you're introducing five more antennas and working with OPM, you will probably want to change the behavior of the stock parts and diversify the range to gradually cover the whole OPM system. Since you have some parts specifically designed for use in planetary subsystems, their balance when transmitting to other parts is probably more important than their balance when transmitting to Kerbin. For longer range parts designed to make the whole interplanetary leap, the inverse is probably true.
+
+Feel free to ask questions! If anything's unclear or you just want to bounce balance ideas off of me, don't be shy. I'm always happy to help.
+

file:b/RelayDataCost.cs (new)
--- /dev/null
+++ b/RelayDataCost.cs
@@ -1,1 +1,259 @@
-
+// AntennaRange
+//
+// RelayLinkCost.cs
+//
+// Copyright © 2016, 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 UnityEngine;
+
+namespace AntennaRange
+{
+	/// <summary>
+	/// A struct representing the cost of sending data through a relay.
+	/// </summary>
+	public struct RelayDataCost : IComparable, IComparable<RelayDataCost>
+	{
+		/// <summary>
+		/// A RelayDataCost object representing infinitely high cost.
+		/// </summary>
+		public static readonly RelayDataCost Infinity = new RelayDataCost(float.PositiveInfinity, 0f);
+
+		/// <param name="one">Left</param>
+		/// <param name="two">Right</param>
+		public static RelayDataCost operator+ (RelayDataCost one, RelayDataCost two)
+		{
+			RelayDataCost gcd, lcd;
+
+			if (one.PacketSize > two.PacketSize) {
+				gcd = one;
+				lcd = two;
+			}
+			else
+			{
+				gcd = two;
+				lcd = one;
+			}
+
+			if (lcd.PacketSize != 0f)
+			{
+				float mul = gcd.PacketSize / lcd.PacketSize;
+
+				lcd.PacketSize *= mul;
+				lcd.PacketResourceCost *= mul;
+			}
+
+			return new RelayDataCost(gcd.PacketResourceCost + lcd.PacketResourceCost, gcd.PacketSize + lcd.PacketSize);
+		}
+
+		/// <param name="only">RelayDataCost to be negated</param>
+		public static RelayDataCost operator- (RelayDataCost only)
+		{
+			return new RelayDataCost(-only.PacketResourceCost, only.PacketSize);
+		}
+
+		/// <param name="left">Left.</param>
+		/// <param name="right">Right.</param>
+		public static RelayDataCost operator- (RelayDataCost left, RelayDataCost right)
+		{
+			return left + -right;
+		}
+
+		/// <param name="left">Left.</param>
+		/// <param name="right">Right.</param>
+		public static bool operator> (RelayDataCost left, RelayDataCost right)
+		{
+			return (left.CompareTo(right) > 0);
+		}
+
+		/// <param name="left">Left.</param>
+		/// <param name="right">Right.</param>
+		public static bool operator>= (RelayDataCost left, RelayDataCost right)
+		{
+			return (left.CompareTo(right) >= 0);
+		}
+
+		/// <param name="left">Left.</param>
+		/// <param name="right">Right.</param>
+		public static bool operator== (RelayDataCost left, RelayDataCost right)
+		{
+			return (left.CompareTo(right) == 0);
+		}
+
+		/// <param name="left">Left.</param>
+		/// <param name="right">Right.</param>
+		public static bool operator<= (RelayDataCost left, RelayDataCost right)
+		{
+			return (left.CompareTo(right) <= 0);
+		}
+
+		/// <param name="left">Left.</param>
+		/// <param name="right">Right.</param>
+		public static bool operator< (RelayDataCost left, RelayDataCost right)
+		{
+			return (left.CompareTo(right) < 0);
+		}
+
+		/// <param name="left">Left.</param>
+		/// <param name="right">Right.</param>
+		public static bool operator!= (RelayDataCost left, RelayDataCost right)
+		{
+			return (left.CompareTo(right) != 0);
+		}
+
+		/// <summary>
+		/// The resource cost of a packet, in EC/packet
+		/// </summary>
+		public float PacketResourceCost;
+
+		/// <summary>
+		/// The data capacity of a packet, MiT/packet
+		/// </summary>
+		public float PacketSize;
+
+		/// <summary>
+		/// Gets the resource cost per unit data, in EC/MiT
+		/// </summary>
+		/// <value>The resource cost per unit data, in EC/MiT</value>
+		public double ResourceCostPerData
+		{
+			get
+			{
+				if (this.PacketSize == 0f || float.IsInfinity(this.PacketResourceCost))
+				{
+					return double.PositiveInfinity;
+				}
+
+				return (double)this.PacketResourceCost / (double)this.PacketSize;
+			}
+		}
+
+		/// <summary>
+		/// Initializes a new instance of the <see cref="AntennaRange.RelayDataCost"/> struct.
+		/// </summary>
+		/// <param name="cost">resource cost of a packet, in EC/packet</param>
+		/// <param name="size">data capacity of a packet, MiT/packet</param>
+		public RelayDataCost(float cost, float size)
+		{
+			this.PacketResourceCost = cost;
+			this.PacketSize = size;
+		}
+
+		/// <summary>
+		/// Returns a <see cref="System.String"/> that represents the current <see cref="AntennaRange.RelayDataCost"/>.
+		/// </summary>
+		/// <returns>A <see cref="System.String"/> that represents the current <see cref="AntennaRange.RelayDataCost"/>.</returns>
+		public override string ToString()
+		{
+			return string.Format("{0} EC/MiT", this.ResourceCostPerData);
+		}
+
+		/// <summary>
+		/// Determines whether the specified <see cref="System.Object"/> is equal to the current <see cref="AntennaRange.RelayDataCost"/>.
+		/// </summary>
+		/// <param name="obj">The <see cref="System.Object"/> to compare with the current <see cref="AntennaRange.RelayDataCost"/>.</param>
+		/// <returns><c>true</c> if the specified <see cref="System.Object"/> is equal to the current
+		/// <see cref="AntennaRange.RelayDataCost"/>; otherwise, <c>false</c>.</returns>
+		public override bool Equals(object obj)
+		{
+			if (obj is RelayDataCost)
+			{
+				return ((RelayDataCost)obj == this);
+			}
+			else
+			{
+				return false;
+			}
+		}
+
+		/// <summary>
+		/// Serves as a hash function for a <see cref="AntennaRange.RelayDataCost"/> object.
+		/// </summary>
+		/// <returns>A hash code for this instance that is suitable for use in hashing algorithms and data structures such as a hash table.</returns>
+		public override int GetHashCode()
+		{
+			int hash = 137;
+
+			hash = (hash * 61) + this.PacketResourceCost.GetHashCode();
+			hash = (hash * 61) + this.PacketSize.GetHashCode();
+
+			return hash;
+		}
+
+		/// <summary>
+		/// Compares this RelayDataCost to another object.  Throws NotImplementedException for objects
+		/// that are not RelayDataCost objects
+		/// </summary>
+		/// <returns>-1 if this is less than o, 0 if this equals o, 1 if this is greater than o</returns>
+		/// <param name="o">Another object</param>
+		public int CompareTo(object o)
+		{
+			if (o is RelayDataCost)
+			{
+				return this.CompareTo((RelayDataCost)o);
+			}
+
+			throw new NotImplementedException(
+				string.Format(
+					"Cannot compare {0} to foreign type {1}",
+					this.GetType().Name,
+					o.GetType().Name
+				)
+			);
+		}
+
+		/// <summary>
+		/// Compares this RelayDataCost to another object.  Throws NotImplementedException for objects
+		/// that are not RelayDataCost objects
+		/// </summary>
+		/// <returns>-1 if this is less than o, 0 if this equals o, 1 if this is greater than o</returns>
+		/// <param name="o">Another RelayDataCost</param>
+		public int CompareTo(RelayDataCost o)
+		{
+			int val;
+
+			if (this.ResourceCostPerData > o.ResourceCostPerData)
+			{
+				val = 1;
+			}
+			else if (this.ResourceCostPerData < o.ResourceCostPerData)
+			{
+				val = -1;
+			}
+			else
+			{
+				val = 0;
+			}
+
+			#if DEBUG
+			Debug.LogErrorFormat("RelayLinkCost comparing {0} to {1}, returning {2}", this, o, val);
+			#endif
+
+			return val;
+		}
+	}
+}
+
+

--- a/RelayDatabase.cs
+++ b/RelayDatabase.cs
@@ -2,7 +2,7 @@
 //
 // RelayDatabase.cs
 //
-// Copyright © 2014, toadicus
+// Copyright © 2014-2015, toadicus
 // All rights reserved.
 //
 // Redistribution and use in source and binary forms, with or without modification,
@@ -26,6 +26,8 @@
 // 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.
 
+#pragma warning disable 1591
+
 using KSP;
 using System;
 using System.Collections.Generic;
@@ -35,27 +37,8 @@
 
 namespace AntennaRange
 {
-	public class RelayDatabase
+	public class RelayDatabase : Singleton<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
 		 * */
@@ -64,22 +47,20 @@
 		 * Fields
 		 * */
 		// Vessel.id-keyed hash table of Part.GetHashCode()-keyed tables of relay objects.
-		protected Dictionary<Guid, Dictionary<int, IAntennaRelay>> relayDatabase;
+		private Dictionary<Guid, List<IAntennaRelay>> relayDatabase;
+		private Dictionary<Guid, IAntennaRelay> bestRelayTable;
 
 		// 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;
+		private Dictionary<Guid, int> vesselPartCountTable;
+
+		private int cacheHits;
+		private int cacheMisses;
 
 		/*
 		 * Properties
 		 * */
 		// Gets the Part-hashed table of relays in a given vessel
-		public Dictionary<int, IAntennaRelay> this [Vessel vessel]
+		public IList<IAntennaRelay> this [Vessel vessel]
 		{
 			get
 			{
@@ -104,16 +85,66 @@
 				}
 
 				// Return the Part-hashed table of relays for this vessel
-				return relayDatabase[vessel.id];
+				return relayDatabase[vessel.id].AsReadOnly();
 			}
 		}
 
 		/* 
 		 * Methods
 		 * */
+		// Remove a vessel from the cache, if it exists.
+		public void DirtyVessel(Vessel vessel)
+		{
+			#if DEBUG
+			Logging.PostDebugMessage("RelayDatabase: Dirtying cache for vessel {0} in frame {1}",
+				vessel, new System.Diagnostics.StackTrace().ToString());
+			#else
+			Logging.PostLogMessage("RelayDatabase: Dirtying cache for vessel {0}", vessel.vesselName);
+			#endif
+
+			this.relayDatabase.Remove(vessel.id);
+			this.vesselPartCountTable.Remove(vessel.id);
+			this.bestRelayTable.Remove(vessel.id);
+		}
+
+		public void ClearCache()
+		{
+			Logging.PostLogMessage("RelayDatabase: onSceneChange clearing entire cache.");
+
+			this.relayDatabase.Clear();
+			this.bestRelayTable.Clear();
+			this.vesselPartCountTable.Clear();
+		}
+
+		// 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);
+		}
+
+		public IAntennaRelay GetBestVesselRelay(Vessel vessel)
+		{
+			IAntennaRelay relay;
+			if (this.bestRelayTable.TryGetValue(vessel.id, out relay))
+			{
+				return relay;
+			}
+			else
+			{
+				var dump = this[vessel];
+				return null;
+			}
+		}
+
 		// 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)
+		private bool AddVessel(Vessel vessel)
 		{
 			// If this vessel is already here...
 			if (this.ContainsKey(vessel))
@@ -133,7 +164,7 @@
 			else
 			{
 				// Build an empty table...
-				this.relayDatabase[vessel.id] = new Dictionary<int, IAntennaRelay>();
+				this.relayDatabase[vessel.id] = new List<IAntennaRelay>();
 
 				// Update the empty index
 				this.UpdateVessel(vessel);
@@ -144,7 +175,7 @@
 		}
 
 		// Update the vessel's entry in the table
-		public void UpdateVessel(Vessel vessel)
+		private void UpdateVessel(Vessel vessel)
 		{
 			// Squak if the database doesn't have the vessel
 			if (!this.ContainsKey(vessel))
@@ -157,7 +188,7 @@
 				));
 			}
 
-			Dictionary<int, IAntennaRelay> vesselTable = this.relayDatabase[vessel.id];
+			List<IAntennaRelay> vesselTable = this.relayDatabase[vessel.id];
 
 			// Actually build and assign the table
 			this.getVesselRelays(vessel, ref vesselTable);
@@ -165,31 +196,6 @@
 			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)
 		{
@@ -200,7 +206,7 @@
 				// or if we've just docked or undocked)...
 				if (this.vesselPartCountTable[vessel.id] != vessel.Parts.Count || vessel.loaded)
 				{
-					Tools.PostDebugMessage(string.Format(
+					Logging.PostDebugMessage(string.Format(
 						"{0}: dirtying cache for vessel '{1}' ({2}).",
 						this.GetType().Name,
 						vessel.vesselName,
@@ -214,18 +220,41 @@
 		}
 
 		// 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);
-			}
+		private void onSceneChange(GameScenes scene)
+		{
+			Logging.PostDebugMessage(
+				"RelayDatabase: caught onSceneChangeRequested in scene {0} to scene {1}.  ActiveVessel is {2}",
+				HighLogic.LoadedScene,
+				scene,
+				FlightGlobals.ActiveVessel == null ? "null" : FlightGlobals.ActiveVessel.vesselName
+			);
+
+			if (scene == GameScenes.FLIGHT)
+			{
+				if (scene == HighLogic.LoadedScene)
+				{
+					if (FlightGlobals.ActiveVessel != null)
+					{
+						Logging.PostDebugMessage("RelayDatabase: onSceneChange clearing {0} from cache.",
+							FlightGlobals.ActiveVessel.vesselName);
+
+						this.onVesselEvent(FlightGlobals.ActiveVessel);
+					}
+				}
+				else
+				{
+					this.ClearCache();
+				}
+			}
+		}
+
+		private void onGameLoaded(object data)
+		{
+			this.ClearCache();
 		}
 
 		// Runs when parts are undocked
-		public void onPartEvent(Part part)
+		private void onPartEvent(Part part)
 		{
 			if (part != null && part.vessel != null)
 			{
@@ -234,43 +263,61 @@
 		}
 
 		// Runs when parts are coupled, as in docking
-		public void onFromPartToPartEvent(GameEvents.FromToAction<Part, Part> data)
+		private 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)
+		private void getVesselRelays(Vessel vessel, ref List<IAntennaRelay> relays)
 		{
 			// We're going to completely regen this table, so dump the current contents.
 			relays.Clear();
 
-			Tools.PostDebugMessage(string.Format(
+			Logging.PostDebugMessage(string.Format(
 				"{0}: Getting antenna relays from vessel {1}.",
 				"IAntennaRelay",
 				vessel.vesselName
 			));
 
+			double bestRelayRange = double.NegativeInfinity;
+			IAntennaRelay bestRelay = null;
+			IAntennaRelay relay;
+
 			// If the vessel is loaded, we can fetch modules implementing IAntennaRelay directly.
 			if (vessel.loaded) {
-				Tools.PostDebugMessage(string.Format(
+				Logging.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)
-				{
+				Part part;
+				for (int partIdx = 0; partIdx < vessel.Parts.Count; partIdx++)
+				{
+					part = vessel.Parts[partIdx];
+
 					// ...loop through the PartModules in the Part...
-					foreach (PartModule module in part.Modules)
+					PartModule module;
+					for (int modIdx = 0; modIdx < part.Modules.Count; modIdx++)
 					{
+						module = part.Modules[modIdx];
+
 						// ...if the module is a relay...
 						if (module is IAntennaRelay)
 						{
+							relay = (module as IAntennaRelay);
+
+							if (relay.maxTransmitDistance > bestRelayRange)
+							{
+								bestRelayRange = relay.maxTransmitDistance;
+								bestRelay = relay;
+							}
+
 							// ...add the module to the table
-							relays.Add(part.GetHashCode(), module as IAntennaRelay);
+							relays.Add(relay);
 							// ...neglect relay objects after the first in each part.
 							break;
 						}
@@ -280,16 +327,19 @@
 			// If the vessel is not loaded, we need to build ProtoAntennaRelays when we find relay ProtoPartSnapshots.
 			else
 			{
-				Tools.PostDebugMessage(string.Format(
+				Logging.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(
+				ProtoPartSnapshot pps;
+				for (int ppsIdx = 0; ppsIdx < vessel.protoVessel.protoPartSnapshots.Count; ppsIdx++)
+				{
+					pps = vessel.protoVessel.protoPartSnapshots[ppsIdx];
+
+					Logging.PostDebugMessage(string.Format(
 						"{0}: Searching in protopartsnapshot {1}",
 						this.GetType().Name,
 						pps
@@ -298,7 +348,7 @@
 					// ...Fetch the prefab, because it's more useful for what we're doing.
 					Part partPrefab = PartLoader.getPartInfoByName(pps.partName).partPrefab;
 
-					Tools.PostDebugMessage(string.Format(
+					Logging.PostDebugMessage(string.Format(
 						"{0}: Got partPrefab {1} in protopartsnapshot {2}",
 						this.GetType().Name,
 						partPrefab,
@@ -306,9 +356,12 @@
 					));
 
 					// ...loop through the PartModules in the prefab...
-					foreach (PartModule module in partPrefab.Modules)
+					PartModule module;
+					for (int modIdx = 0; modIdx < partPrefab.Modules.Count; modIdx++)
 					{
-						Tools.PostDebugMessage(string.Format(
+						module = partPrefab.Modules[modIdx];
+
+						Logging.PostDebugMessage(string.Format(
 							"{0}: Searching in partmodule {1}",
 							this.GetType().Name,
 							module
@@ -317,14 +370,22 @@
 						// ...if the module is a relay...
 						if (module is IAntennaRelay)
 						{
-							Tools.PostDebugMessage(string.Format(
+							Logging.PostDebugMessage(string.Format(
 								"{0}: partmodule {1} is antennarelay",
 								this.GetType().Name,
 								module
 							));
 
+							relay = new ProtoAntennaRelay(module as IAntennaRelay, pps);
+
+							if (relay.maxTransmitDistance > bestRelayRange)
+							{
+								bestRelayRange = relay.maxTransmitDistance;
+								bestRelay = relay;
+							}
+
 							// ...build a new ProtoAntennaRelay and add it to the table
-							relays.Add(pps.GetHashCode(), new ProtoAntennaRelay(module as IAntennaRelay, pps));
+							relays.Add(relay);
 							// ...neglect relay objects after the first in each part.
 							break;
 						}
@@ -332,7 +393,9 @@
 				}
 			}
 
-			Tools.PostDebugMessage(string.Format(
+			this.bestRelayTable[vessel.id] = bestRelay;
+
+			Logging.PostDebugMessage(string.Format(
 				"{0}: vessel '{1}' ({2}) has {3} transmitters.",
 				"IAntennaRelay",
 				vessel.vesselName,
@@ -342,12 +405,12 @@
 		}
 
 		// Construct the singleton
-		protected RelayDatabase()
+		private RelayDatabase()
 		{
 			// Initialize the databases
-			this.relayDatabase = new Dictionary<Guid, Dictionary<int, IAntennaRelay>>();
+			this.relayDatabase = new Dictionary<Guid, List<IAntennaRelay>>();
+			this.bestRelayTable = new Dictionary<Guid, IAntennaRelay>();
 			this.vesselPartCountTable = new Dictionary<Guid, int>();
-			this.CheckedVesselsTable = new Dictionary<Guid, bool>();
 
 			this.cacheHits = 0;
 			this.cacheMisses = 0;
@@ -359,6 +422,7 @@
 			GameEvents.onGameSceneLoadRequested.Add(this.onSceneChange);
 			GameEvents.onPartCouple.Add(this.onFromPartToPartEvent);
 			GameEvents.onPartUndock.Add(this.onPartEvent);
+			GameEvents.onGameStateLoad.Add(this.onGameLoaded);
 		}
 
 		~RelayDatabase()
@@ -370,8 +434,9 @@
 			GameEvents.onGameSceneLoadRequested.Remove(this.onSceneChange);
 			GameEvents.onPartCouple.Remove(this.onFromPartToPartEvent);
 			GameEvents.onPartUndock.Remove(this.onPartEvent);
-
-			Tools.PostDebugMessage(this.GetType().Name + " destroyed.");
+			GameEvents.onGameStateLoad.Remove(this.onGameLoaded);
+
+			Logging.PostDebugMessage(this.GetType().Name + " destroyed.");
 
 			KSPLog.print(string.Format(
 				"{0} destructed.  Cache hits: {1}, misses: {2}, hit rate: {3:P1}",
@@ -385,21 +450,27 @@
 		#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());
+			using (ToadicusTools.Text.PooledStringBuilder sb = ToadicusTools.Text.PooledStringBuilder.Get())
+			{
+				sb.Append("Dumping RelayDatabase:");
+
+				var dbEnum = this.relayDatabase.GetEnumerator();
+				IList<IAntennaRelay> vesselRelays;
+				while (dbEnum.MoveNext())
+				{
+					sb.AppendFormat("\nVessel {0}:", dbEnum.Current.Key);
+
+					vesselRelays = dbEnum.Current.Value;
+					IAntennaRelay relay;
+					for (int rIdx = 0; rIdx < vesselRelays.Count; rIdx++)
+					{
+						relay = vesselRelays[rIdx];
+						sb.AppendFormat("\n\t{0}", relay.ToString());
+					}
+				}
+
+				Logging.PostDebugMessage(sb.ToString());
+			}
 		}
 		#endif
 	}

--- a/RelayExtensions.cs
+++ b/RelayExtensions.cs
@@ -2,7 +2,7 @@
 //
 // Extensions.cs
 //
-// Copyright © 2014, toadicus
+// Copyright © 2014-2015, toadicus
 // All rights reserved.
 //
 // Redistribution and use in source and binary forms, with or without modification,
@@ -28,16 +28,59 @@
 
 using System;
 using System.Collections.Generic;
-using System.Linq;
-using ToadicusTools;
+using ToadicusTools.Extensions;
 
 namespace AntennaRange
 {
-	/*
-	 * A class of utility extensions for Vessels and Relays to help find a relay path back to Kerbin.
-	 * */
+	/// <summary>
+	/// A class of utility extensions for Vessels and Relays to help find a relay path back to Kerbin.
+	/// </summary>
 	public static class RelayExtensions
 	{
+		/// <summary>
+		/// Returns the distance between two IAntennaRelays.
+		/// </summary>
+		/// <param name="relayOne">Relay one.</param>
+		/// <param name="relayTwo">Relay two.</param>
+		public static double DistanceTo(this IAntennaRelay relayOne, IAntennaRelay relayTwo)
+		{
+			return relayOne.vessel.DistanceTo(relayTwo.vessel);
+		}
+
+		/// <summary>
+		/// Returns the distance from this IAntennaRelay to the given CelestialBody
+		/// </summary>
+		/// <param name="relay">Relay.</param>
+		/// <param name="body">Body.</param>
+		public static double SqrDistanceTo(this IAntennaRelay relay, CelestialBody body)
+		{
+			double range = relay.vessel.DistanceTo(body) - body.Radius;
+
+			return range * range;
+		}
+
+		/// <summary>
+		/// Returns the distance between two IAntennaRelays.
+		/// </summary>
+		/// <param name="relayOne">Relay one.</param>
+		/// <param name="relayTwo">Relay two.</param>
+		public static double SqrDistanceTo(this IAntennaRelay relayOne, IAntennaRelay relayTwo)
+		{
+			return relayOne.vessel.sqrDistanceTo(relayTwo.vessel);
+		}
+
+		/// <summary>
+		/// Returns the distance from this IAntennaRelay to the given CelestialBody
+		/// </summary>
+		/// <param name="relay">Relay.</param>
+		/// <param name="body">Body.</param>
+		public static double DistanceTo(this IAntennaRelay relay, CelestialBody body)
+		{
+			double range = relay.vessel.DistanceTo(body) - body.Radius;
+
+			return range;
+		}
+
 		/// <summary>
 		/// Returns the distance between this IAntennaRelay and a Vessel
 		/// </summary>
@@ -55,7 +98,7 @@
 		/// <param name="body">A <see cref="CelestialBody"/></param>
 		public static double DistanceTo(this AntennaRelay relay, CelestialBody body)
 		{
-			return relay.vessel.DistanceTo(body);
+			return relay.vessel.DistanceTo(body) - body.Radius;
 		}
 
 		/// <summary>
@@ -69,12 +112,117 @@
 		}
 
 		/// <summary>
+		/// Returns the square of 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 SqrDistanceTo(this AntennaRelay relay, Vessel vessel)
+		{
+			return relay.vessel.sqrDistanceTo(vessel);
+		}
+
+		/// <summary>
+		/// Returns the square of 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 SqrDistanceTo(this AntennaRelay relay, CelestialBody body)
+		{
+			double dist = (relay.vessel.GetWorldPos3D() - body.position).magnitude - body.Radius;
+
+			return dist * dist;
+		}
+
+		/// <summary>
+		/// Returns the square of 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 SqrDistanceTo(this AntennaRelay relayOne, IAntennaRelay relayTwo)
+		{
+			return relayOne.vessel.sqrDistanceTo(relayTwo.vessel);
+		}
+
+		/// <summary>
+		/// Returns the square of the maximum link range between two relays.
+		/// </summary>
+		/// <returns>The maximum link range between two relays.</returns>
+		/// <param name="relayOne">Relay one.</param>
+		/// <param name="relayTwo">Relay two.</param>
+		public static double MaxLinkSqrDistanceTo(this AntennaRelay relayOne, IAntennaRelay relayTwo)
+		{
+			if (ARConfiguration.UseAdditiveRanges)
+			{
+				return relayOne.maxTransmitDistance * relayTwo.maxTransmitDistance;
+			}
+			else
+			{
+				return relayOne.maxTransmitDistance * relayOne.maxTransmitDistance;
+			}
+		}
+
+		/// <summary>
+		/// Returns the square of the maximum link range between a relay and Kerbin.
+		/// </summary>
+		/// <returns>The maximum link range between a relay and Kerbin.</returns>
+		/// <param name="relayOne">Relay one.</param>
+		/// <param name="body">A CelestialBody (must be Kerbin).</param>
+		public static double MaxLinkSqrDistanceTo(this AntennaRelay relayOne, CelestialBody body)
+		{
+			if (body != AntennaRelay.Kerbin)
+			{
+				return 0d;
+			}
+
+			if (ARConfiguration.UseAdditiveRanges)
+			{
+				return relayOne.maxTransmitDistance * ARConfiguration.KerbinRelayRange;
+			}
+			else
+			{
+				return relayOne.maxTransmitDistance * relayOne.maxTransmitDistance;
+			}
+		}
+
+		/// <summary>
+		/// Determines if relayOne is in range of the specified relayTwo.
+		/// </summary>
+		/// <returns><c>true</c> if relayOne is in range of the specifie relayTwo; otherwise, <c>false</c>.</returns>
+		/// <param name="relayOne">Relay one.</param>
+		/// <param name="relayTwo">Relay two.</param>
+		public static bool IsInRangeOf(this AntennaRelay relayOne, IAntennaRelay relayTwo)
+		{
+			if (relayOne == null || relayTwo == null)
+			{
+				return false;
+			}
+
+			return relayOne.SqrDistanceTo(relayTwo) <= relayOne.MaxLinkSqrDistanceTo(relayTwo);
+		}
+
+		/// <summary>
+		/// Determines if relayOne is in range of the specified body.
+		/// </summary>
+		/// <returns><c>true</c> if relayOne is in range of the specified body; otherwise, <c>false</c>.</returns>
+		/// <param name="relayOne">Relay one.</param>
+		/// <param name="body">Body.</param>
+		public static bool IsInRangeOf(this AntennaRelay relayOne, CelestialBody body)
+		{
+			if (relayOne == null || body == null)
+			{
+				return false;
+			}
+
+			return relayOne.SqrDistanceTo(body) <= relayOne.MaxLinkSqrDistanceTo(body);
+		}
+
+		/// <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();
+		public static IList<IAntennaRelay> GetAntennaRelays (this Vessel vessel)
+		{
+			return RelayDatabase.Instance[vessel];
 		}
 
 		/// <summary>
@@ -84,8 +232,11 @@
 		/// <param name="vessel"></param>
 		public static bool HasConnectedRelay(this Vessel vessel)
 		{
-			foreach (IAntennaRelay relay in RelayDatabase.Instance[vessel].Values)
-			{
+			IList<IAntennaRelay> vesselRelays = RelayDatabase.Instance[vessel];
+			IAntennaRelay relay;
+			for (int rIdx = 0; rIdx < vesselRelays.Count; rIdx++)
+			{
+				relay = vesselRelays[rIdx];
 				if (relay.CanTransmit())
 				{
 					return true;
@@ -94,6 +245,126 @@
 
 			return false;
 		}
+
+		/// <summary>
+		/// Gets the <see cref="AntennaRange.ConnectionStatus"/> for this <see cref="Vessel"/>
+		/// </summary>
+		/// <param name="vessel">This <see cref="Vessel"/></param>
+		public static ConnectionStatus GetConnectionStatus(this Vessel vessel)
+		{
+			bool canTransmit = false;
+
+			IList<IAntennaRelay> vesselRelays = RelayDatabase.Instance[vessel];
+			IAntennaRelay relay;
+			for (int rIdx = 0; rIdx < vesselRelays.Count; rIdx++)
+			{
+				relay = vesselRelays[rIdx];
+				if (relay.LinkStatus > ConnectionStatus.None)
+				{
+					canTransmit = true;
+
+					if (relay.LinkStatus == ConnectionStatus.Optimal)
+					{
+						return ConnectionStatus.Optimal;
+					}
+				}
+			}
+
+			if (canTransmit)
+			{
+				return ConnectionStatus.Suboptimal;
+			}
+			else
+			{
+				return ConnectionStatus.None;
+			}
+		}
+
+		/// <summary>
+		/// Gets the best relay on this Vessel.  The best relay may not be able to transmit.
+		/// </summary>
+		/// <param name="vessel">This <see cref="Vessel"/></param>
+		public static IAntennaRelay GetBestRelay(this Vessel vessel)
+		{
+			return RelayDatabase.Instance.GetBestVesselRelay(vessel);
+		}
+
+		/// <summary>
+		/// Logs a message on behalf of this relay
+		/// </summary>
+		public static void Log(this AntennaRelay relay, string format, params object[] args)
+		{
+			ToadicusTools.Logging.PostLogMessage(string.Format("[{0}] {1}", relay.ToString(), format), args);
+		}
+
+		/// <summary>
+		/// Logs a message on behalf of this relay
+		/// </summary>
+		public static void Log(this AntennaRelay relay, string msg)
+		{
+			ToadicusTools.Logging.PostLogMessage("[{0}] {1}", relay.ToString(), msg);
+		}
+
+		/// <summary>
+		/// Logs a warning message on behalf of this relay
+		/// </summary>
+		public static void LogWarning(this AntennaRelay relay, string format, params object[] args)
+		{
+			ToadicusTools.Logging.PostWarningMessage(string.Format("[{0}] {1}", relay.ToString(), format), args);
+		}
+
+		/// <summary>
+		/// Logs a warning message on behalf of this relay
+		/// </summary>
+		public static void LogWarning(this AntennaRelay relay, string msg)
+		{
+			ToadicusTools.Logging.PostWarningMessage("[{0}] {1}", relay.ToString(), msg);
+		}
+
+		/// <summary>
+		/// Logs an error message on behalf of this relay
+		/// </summary>
+		public static void LogError(this AntennaRelay relay, string format, params object[] args)
+		{
+			ToadicusTools.Logging.PostErrorMessage(string.Format("[{0}] {1}", relay.ToString(), format), args);
+		}
+
+		/// <summary>
+		/// Logs an error message on behalf of this relay
+		/// </summary>
+		public static void LogError(this AntennaRelay relay, string msg)
+		{
+			ToadicusTools.Logging.PostErrorMessage("[{0}] {1}", relay.ToString(), msg);
+		}
+
+		/// <summary>
+		/// Logs a debug-only message on behalf of this relay
+		/// </summary>
+		[System.Diagnostics.Conditional("DEBUG")]
+		public static void LogDebug(this AntennaRelay relay, string format, params object[] args)
+		{
+			ToadicusTools.Logging.PostDebugMessage(string.Format("[{0}] {1}", relay.ToString(), format), args);
+		}
+
+		/// <summary>
+		/// Logs a debug-only message on behalf of this relay
+		/// </summary>
+		[System.Diagnostics.Conditional("DEBUG")]
+		public static void LogDebug(this AntennaRelay relay, string msg)
+		{
+			ToadicusTools.Logging.PostDebugMessage("[{0}] {1}", relay.ToString(), msg);
+		}
+	}
+
+	#pragma warning disable 1591
+	/// <summary>
+	/// An Enum describing the connection status of a vessel or relay.
+	/// </summary>
+	public enum ConnectionStatus
+	{
+		None,
+		Suboptimal,
+		Optimal
 	}
 }
 

 Binary files a/toolbarIcon.xcf 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