Mostly refactoring, and removing the onVesselDestroy event subscription, which wasn't being very useful.
Mostly refactoring, and removing the onVesselDestroy event subscription, which wasn't being very useful.

--- a/AntennaRange.cfg
+++ b/AntennaRange.cfg
@@ -1,50 +1,50 @@
-//

-// AntennaRange © 2013 toadicus

-// 

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

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

-//

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

-// 3.0 Uported License.

-//

-// Specifications:

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

-// 					and packetSize.

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

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

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

-// 

-

-@PART[longAntenna]

-{

-	@MODULE[ModuleDataTransmitter]

-	{

-		@name = ModuleLimitedDataTransmitter

-		nominalRange = 1500000

-		maxPowerFactor = 8

-		maxDataFactor = 4

-	}

-}

-

-@PART[mediumDishAntenna]

-{

-	@MODULE[ModuleDataTransmitter]

-	{

-		@name = ModuleLimitedDataTransmitter

-		nominalRange = 30000000

-		maxPowerFactor = 8

-		maxDataFactor = 4

-	}

-}

-

-@PART[commDish]

-{

-	@MODULE[ModuleDataTransmitter]

-	{

-		@name = ModuleLimitedDataTransmitter

-		nominalRange = 80000000000

-		maxPowerFactor = 8

-		maxDataFactor = 4

-	}

-}

+//
+// AntennaRange © 2013 toadicus
+// 
+// This work is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License. To view a
+// copy of this license, visit http://creativecommons.org/licenses/by-nc-sa/3.0/
+//
+// This software uses the ModuleManager library © 2013 ialdabaoth, used under a Creative Commons Attribution-ShareAlike
+// 3.0 Uported License.
+//
+// Specifications:
+// nominalRange:	The distance from Kerbin at which the antenna will perform exactly as prescribed by packetResourceCost
+// 					and packetSize.
+// maxPowerFactor:	The multiplier on packetResourceCost that defines the maximum power output of the antenna.  When the
+//					power cost exceeds packetResourceCost * maxPowerFactor, transmission will fail.
+// maxDataFactor:	The multipler on packetSize that defines the maximum data bandwidth of the antenna.
+// 
 
+@PART[longAntenna]
+{
+	@MODULE[ModuleDataTransmitter]
+	{
+		@name = ModuleLimitedDataTransmitter
+		nominalRange = 1500000
+		maxPowerFactor = 8
+		maxDataFactor = 4
+	}
+}
+
+@PART[mediumDishAntenna]
+{
+	@MODULE[ModuleDataTransmitter]
+	{
+		@name = ModuleLimitedDataTransmitter
+		nominalRange = 30000000
+		maxPowerFactor = 8
+		maxDataFactor = 4
+	}
+}
+
+@PART[commDish]
+{
+	@MODULE[ModuleDataTransmitter]
+	{
+		@name = ModuleLimitedDataTransmitter
+		nominalRange = 80000000000
+		maxPowerFactor = 8
+		maxDataFactor = 4
+	}
+}
+

--- a/AntennaRelay.cs
+++ b/AntennaRelay.cs
@@ -21,10 +21,13 @@
 
 namespace AntennaRange
 {
-	public class AntennaRelay : IAntennaRelay
+	public class AntennaRelay
 	{
 		// We don't have a Bard, so we'll hide Kerbin here.
 		protected CelestialBody Kerbin;
+
+		protected System.Diagnostics.Stopwatch searchTimer;
+		protected long millisecondsBetweenSearches;
 
 		/// <summary>
 		/// Gets the parent Vessel.
@@ -95,7 +98,7 @@
 		/// Determines whether this instance can transmit.
 		/// </summary>
 		/// <returns><c>true</c> if this instance can transmit; otherwise, <c>false</c>.</returns>
-		public bool CanTransmit()
+		public virtual bool CanTransmit()
 		{
 			if (this.transmitDistance > this.maxTransmitDistance)
 			{
@@ -113,6 +116,19 @@
 		/// <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();
+
 			// Set this relay as checked, so that we don't check it again.
 			this.relayChecked = true;
 
@@ -191,15 +207,6 @@
 			return _nearestRelay;
 		}
 
-		public override string ToString()
-		{
-			return string.Format(
-				"Antenna relay on vessel {0} (range to relay: {1}m)",
-				vessel,
-				Tools.MuMech_ToSI(transmitDistance)
-			);
-		}
-
 		/// <summary>
 		/// Initializes a new instance of the <see cref="AntennaRange.ProtoDataTransmitter"/> class.
 		/// </summary>
@@ -207,6 +214,9 @@
 		public AntennaRelay(Vessel v)
 		{
 			this.vessel = v;
+
+			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.

--- a/Extensions.cs
+++ b/Extensions.cs
@@ -82,76 +82,10 @@
 		/// <param name="vessel">This <see cref="Vessel"/></param>
 		public static IEnumerable<IAntennaRelay> GetAntennaRelays (this Vessel vessel)
 		{
-			Tools.PostDebugMessage(string.Format(
-				"{0}: Getting antenna relays from vessel {1}.",
-				"IAntennaRelay",
-				vessel.name
-			));
-
-			List<IAntennaRelay> Transmitters;
-
-			// If the vessel is loaded, we can fetch modules implementing IAntennaRelay directly.
-			if (vessel.loaded) {
-				Tools.PostDebugMessage(string.Format(
-					"{0}: vessel {1} is loaded.",
-					"IAntennaRelay",
-					vessel.name
-					));
-
-				// Gets a list of PartModules implementing IAntennaRelay
-				Transmitters = vessel.Parts
-					.SelectMany (p => p.Modules.OfType<IAntennaRelay> ())
-					.ToList();
-			}
-			// If the vessel is not loaded, we need to find ProtoPartModuleSnapshots with a true IsAntenna field.
-			else
-			{
-				Tools.PostDebugMessage(string.Format(
-					"{0}: vessel {1} is not loaded.",
-					"IAntennaRelay",
-					vessel.name
-					));
-
-				Transmitters = new List<IAntennaRelay>();
-
-				// Loop through the ProtoPartModuleSnapshots in this Vessel
-				foreach (ProtoPartSnapshot pps in vessel.protoVessel.protoPartSnapshots)
-				{
-					ProtoPartModuleSnapshot ppms = pps.modules.FirstOrDefault(p => p.IsAntenna());
-					// If they are antennas...
-					if (ppms != null)
-					{
-						// ...add a new ProtoAntennaRelay wrapper to the list.
-						Transmitters.Add(new ProtoAntennaRelay(ppms, pps, vessel));
-					}
-				}
-			}
-
-			Tools.PostDebugMessage(string.Format(
-				"{0}: vessel {1} has {2} transmitters.",
-				"IAntennaRelay",
-				vessel.name,
-				Transmitters.Count
-				));
-
-			// Return the list of IAntennaRelays
-			return Transmitters;
+			return RelayDatabase.Instance[vessel].Values.ToList();
 		}
 
-		// Returns true if this PartModule contains a True IsAntenna field, false otherwise.
-		public static bool IsAntenna (this PartModule module)
-		{
-			return module.Fields.GetValue<bool> ("IsAntenna");
-		}
 
-		// Returns true if this ProtoPartModuleSnapshot contains a persistent True IsAntenna field, false otherwise
-		public static bool IsAntenna(this ProtoPartModuleSnapshot protomodule)
-		{
-			bool result;
-
-			return Boolean.TryParse (protomodule.moduleValues.GetValue ("IsAntenna") ?? "False", out result)
-				? result : false;
-		}
 	}
 }
 

--- a/ModuleLimitedDataTransmitter.cs
+++ b/ModuleLimitedDataTransmitter.cs
@@ -18,6 +18,7 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
+using System.Text;
 using KSP;
 using UnityEngine;
 
@@ -41,10 +42,6 @@
 	 * */
 	public class ModuleLimitedDataTransmitter : ModuleDataTransmitter, IScienceDataTransmitter, IAntennaRelay
 	{
-		// Call this an antenna so that you don't have to.
-		[KSPField(isPersistant = true)]
-		protected bool IsAntenna;
-
 		// Stores the packetResourceCost as defined in the .cfg file.
 		protected float _basepacketResourceCost;
 
@@ -59,15 +56,24 @@
 
 		// Sometimes we will need to communicate errors; this is how we do it.
 		protected ScreenMessage ErrorMsg;
-
-		// Let's make the error text pretty!
-		protected UnityEngine.GUIStyle ErrorStyle;
 
 		// The distance from Kerbin at which the antenna will perform exactly as prescribed by packetResourceCost
 		// and packetSize.
 		[KSPField(isPersistant = false)]
 		public float nominalRange;
 
+		[KSPField(isPersistant = false, guiActive = true, guiName = "Transmission Distance")]
+		public string UItransmitDistance;
+
+		[KSPField(isPersistant = false, guiActive = true, guiName = "Maximum Distance")]
+		public string UImaxTransmitDistance;
+
+		[KSPField(isPersistant = false, guiActive = true, guiName = "Packet Size")]
+		public string UIpacketSize;
+
+		[KSPField(isPersistant = false, guiActive = true, guiName = "Packet Cost")]
+		public string UIpacketCost;
+
 		// The multiplier on packetResourceCost that defines the maximum power output of the antenna.  When the power
 		// cost exceeds packetResourceCost * maxPowerFactor, transmission will fail.
 		[KSPField(isPersistant = false)]
@@ -76,6 +82,8 @@
 		// The multipler on packetSize that defines the maximum data bandwidth of the antenna.
 		[KSPField(isPersistant = false)]
 		public float maxDataFactor;
+
+		protected bool actionUIUpdate;
 
 		/*
 		 * Properties
@@ -140,7 +148,15 @@
 			get
 			{
 				this.PreTransmit_SetPacketSize();
-				return this.packetSize;
+
+				if (this.CanTransmit())
+				{
+					return this.packetSize;
+				}
+				else
+				{
+					return float.Epsilon;
+				}
 			}
 		}
 
@@ -178,15 +194,7 @@
 		// Build ALL the objects.
 		public ModuleLimitedDataTransmitter () : base()
 		{
-			// Make the error posting prettier.
-			this.ErrorStyle = new UnityEngine.GUIStyle();
-			this.ErrorStyle.normal.textColor = (UnityEngine.Color)XKCDColors.OrangeRed;
-			this.ErrorStyle.active.textColor = (UnityEngine.Color)XKCDColors.OrangeRed;
-			this.ErrorStyle.hover.textColor = (UnityEngine.Color)XKCDColors.OrangeRed;
-			this.ErrorStyle.fontStyle = UnityEngine.FontStyle.Normal;
-			this.ErrorStyle.padding.top = 32;
-
-			this.ErrorMsg = new ScreenMessage("", 4f, false, ScreenMessageStyle.UPPER_LEFT, this.ErrorStyle);
+			this.ErrorMsg = new ScreenMessage("", 4f, false, ScreenMessageStyle.UPPER_LEFT);
 		}
 
 		// At least once, when the module starts with a state on the launch pad or later, go find Kerbin.
@@ -198,6 +206,11 @@
 			{
 				this.relay = new AntennaRelay(vessel);
 				this.relay.maxTransmitDistance = this.maxTransmitDistance;
+
+				this.UImaxTransmitDistance = Tools.MuMech_ToSI(this.maxTransmitDistance) + "m";
+
+				GameEvents.onPartActionUICreate.Add(this.onPartActionUICreate);
+				GameEvents.onPartActionUIDismiss.Add(this.onPartActionUIDismiss);
 			}
 		}
 
@@ -208,8 +221,6 @@
 		{
 			this.Fields.Load(node);
 			base.Fields.Load(node);
-
-			this.IsAntenna = true;
 
 			base.OnLoad (node);
 
@@ -242,9 +253,18 @@
 				Tools.MuMech_ToSI((double)this.transmitDistance, 2)
 				);
 
-			this.ErrorMsg.message = ErrorText;
-
-			ScreenMessages.PostScreenMessage(this.ErrorMsg, true);
+			this.ErrorMsg.message = string.Format(
+				"<color='#{0}{1}{2}{3}'><b>{4}</b></color>",
+				((int)(XKCDColors.OrangeRed.r * 255f)).ToString("x2"),
+				((int)(XKCDColors.OrangeRed.g * 255f)).ToString("x2"),
+				((int)(XKCDColors.OrangeRed.b * 255f)).ToString("x2"),
+				((int)(XKCDColors.OrangeRed.a * 255f)).ToString("x2"),
+				ErrorText
+			);
+
+			Tools.PostDebugMessage(this.GetType().Name + ": " + this.ErrorMsg.message);
+
+			ScreenMessages.PostScreenMessage(this.ErrorMsg, false);
 		}
 
 		// Before transmission, set packetResourceCost.  Per above, packet cost increases with the square of
@@ -291,6 +311,18 @@
 		// 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.name,
+					Enum.GetName(typeof(PartStates), partState)
+				));
+				return false;
+			}
 			return this.relay.CanTransmit();
 		}
 
@@ -329,18 +361,25 @@
 
 			if (this.CanTransmit())
 			{
-				this.ErrorMsg.message = "Beginning transmission ";
+				StringBuilder message = new StringBuilder();
+
+				message.Append("[");
+				message.Append(base.part.partInfo.title);
+				message.Append("] ");
+
+				message.Append("Beginning transmission ");
 
 				if (this.relay.nearestRelay == null)
 				{
-					this.ErrorMsg.message += "directly to Kerbin.";
+					message.Append("directly to Kerbin.");
 				}
 				else
 				{
-					this.ErrorMsg.message += "via relay " + this.relay.nearestRelay;
-				}
-
-				ScreenMessages.PostScreenMessage(this.ErrorMsg);
+					message.Append("via ");
+					message.Append(this.relay.nearestRelay);
+				}
+
+				ScreenMessages.PostScreenMessage(message.ToString(), 4f, ScreenMessageStyle.UPPER_LEFT);
 
 				base.StartTransmission();
 			}
@@ -348,6 +387,41 @@
 			{
 				this.PostCannotTransmitError ();
 			}
+		}
+
+		public void Update()
+		{
+			if (this.actionUIUpdate)
+			{
+				this.UItransmitDistance = Tools.MuMech_ToSI(this.transmitDistance) + "m";
+				this.UIpacketSize = this.CanTransmit() ? Tools.MuMech_ToSI(this.DataRate) + "MiT" : "N/A";
+				this.UIpacketCost = this.CanTransmit() ? Tools.MuMech_ToSI(this.DataResourceCost) + "E" : "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()
+		{
+			return string.Format(
+				"{0} on {1}.",
+				this.part.partInfo.title,
+				vessel.name
+			);
 		}
 
 		// When debugging, it's nice to have a button that just tells you everything.

--- a/ProtoAntennaRelay.cs
+++ b/ProtoAntennaRelay.cs
@@ -26,11 +26,11 @@
 	 * */
 	public class ProtoAntennaRelay : AntennaRelay, IAntennaRelay
 	{
-		// Stores the proto module.
-		protected ProtoPartModuleSnapshot protoModule;
+		// Stores the relay prefab
+		protected IAntennaRelay relayPrefab;
 
-		// Stores the proto part, which seems silly because all we need is the name.
-		protected Part partPrefab;
+		// Stores the prototype part so we can make sure we haven't exploded or so.
+		protected ProtoPartSnapshot protoPart;
 
 		/// <summary>
 		/// The maximum distance at which this transmitter can operate.
@@ -40,7 +40,7 @@
 		{
 			get
 			{
-				return this.partPrefab.Modules.OfType<ModuleLimitedDataTransmitter>().First().maxTransmitDistance;
+				return relayPrefab.maxTransmitDistance;
 			}
 		}
 
@@ -51,23 +51,46 @@
 		/// <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
 			{
-				bool result;
-				Boolean.TryParse(this.protoModule.moduleValues.GetValue("relayChecked"), out result);
-				return result;
+				return this.protoPart.partInfo.title;
 			}
-			protected set
+		}
+
+		public override bool CanTransmit()
+		{
+			PartStates partState = (PartStates)this.protoPart.state;
+			if (partState == PartStates.DEAD || partState == PartStates.DEACTIVATED)
 			{
-				if (this.protoModule.moduleValues.HasValue("relayChecked"))
-				{
-					this.protoModule.moduleValues.SetValue("relayChecked", value.ToString ());
-				}
-				else
-				{
-					this.protoModule.moduleValues.AddValue("relayChecked", value);
-				}
+				Tools.PostDebugMessage(string.Format(
+					"{0}: {1} on {2} cannot transmit: {3}",
+					this.GetType().Name,
+					this.title,
+					this.vessel.name,
+					Enum.GetName(typeof(PartStates), partState)
+				));
+				return false;
 			}
+			return base.CanTransmit();
+		}
+
+		public override string ToString()
+		{
+			return string.Format(
+				"{0} on {1}.",
+				this.title,
+				this.protoPart.pVesselRef.vesselName
+			);
 		}
 
 		/// <summary>
@@ -75,10 +98,11 @@
 		/// </summary>
 		/// <param name="ms">The ProtoPartModuleSnapshot to wrap</param>
 		/// <param name="vessel">The parent Vessel</param>
-		public ProtoAntennaRelay(ProtoPartModuleSnapshot ppms, ProtoPartSnapshot pps, Vessel vessel) : base(vessel)
+		public ProtoAntennaRelay(IAntennaRelay prefabRelay, ProtoPartSnapshot pps) : base(pps.pVesselRef.vesselRef)
 		{
-			this.protoModule = ppms;
-			this.partPrefab = PartLoader.getPartInfoByName(pps.partName).partPrefab;
+			this.relayPrefab = prefabRelay;
+			this.protoPart = pps;
+			this.vessel = pps.pVesselRef.vesselRef;
 		}
 	}
 }

file:b/RelayDatabase.cs (new)
--- /dev/null
+++ b/RelayDatabase.cs
@@ -1,1 +1,277 @@
-
+// AntennaRange © 2014 toadicus
+//
+// This work is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License. To view a
+// copy of this license, visit http://creativecommons.org/licenses/by-nc-sa/3.0/
+
+using KSP;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using UnityEngine;
+
+namespace AntennaRange
+{
+	public class RelayDatabase
+	{
+		/*
+		 * Static members
+		 * */
+		// Singleton storage
+		protected static RelayDatabase _instance;
+		// Gets the singleton
+		public static RelayDatabase Instance
+		{
+			get
+			{	
+				if (_instance == null)
+				{
+					_instance = new RelayDatabase();
+				}
+
+				return _instance;
+			}
+		}
+
+		/*
+		 * Instance members
+		 * */
+
+		/*
+		 * Fields
+		 * */
+		// Vessel.id-keyed hash table of Part.GetHashCode()-keyed tables of relay objects.
+		protected Dictionary<Guid, Dictionary<int, IAntennaRelay>> relayDatabase;
+
+		// Vessel.id-keyed hash table of part counts, used for caching
+		protected Dictionary<Guid, int> vesselPartCountTable;
+
+		/*
+		 * Properties
+		 * */
+		// Gets the Part-hashed table of relays in a given vessel
+		public Dictionary<int, IAntennaRelay> this [Vessel vessel]
+		{
+			get
+			{
+				// If we don't have an entry for this vessel...
+				if (!this.ContainsKey(vessel.id))
+				{
+					// ...Generate an entry for this vessel.
+					this.AddVessel(vessel);
+				}
+				// If our part count disagrees with the vessel's part count...
+				if (this.vesselPartCountTable[vessel.id] != vessel.Parts.Count)
+				{
+					// ...Update the our vessel in the cache
+					this.UpdateVessel(vessel);
+				}
+
+				// Return the Part-hashed table of relays for this vessel
+				return relayDatabase[vessel.id];
+			}
+		}
+
+		/* 
+		 * Methods
+		 * */
+		// Adds a vessel to the database
+		// The return for this function isn't used yet, but seems useful for potential future API-uses
+		public bool AddVessel(Vessel vessel)
+		{
+			// If this vessel is already here...
+			if (relayDatabase.ContainsKey(vessel.id))
+			{
+				// ...post an error
+				Debug.LogWarning(string.Format(
+					"{0}: Cannot add vessel '{1}' (id: {2}): Already in database.",
+					this.GetType().Name,
+					vessel.name,
+					vessel.id
+				));
+
+				// ...and refuse to add
+				return false;
+			}
+			// otherwise, add the vessel to our tables...
+			else
+			{
+				// Build an empty table...
+				this.relayDatabase[vessel.id] = new Dictionary<int, IAntennaRelay>();
+
+				// Update the empty index
+				this.UpdateVessel(vessel);
+
+				// Return success
+				return true;
+			}
+		}
+
+		// Update the vessel's entry in the table
+		public void UpdateVessel(Vessel vessel)
+		{
+			// Squak if the database doesn't have the vessel
+			if (!relayDatabase.ContainsKey(vessel.id))
+			{
+				throw new InvalidOperationException(string.Format(
+					"{0}: Update called vessel '{1}' (id: {2}) not in database: vessel will be added.",
+					this.GetType().Name,
+					vessel.name,
+					vessel.id
+				));
+			}
+
+			Dictionary<int, IAntennaRelay> vesselTable = this.relayDatabase[vessel.id];
+
+			// Actually build and assign the table
+			this.getVesselRelays(vessel, ref vesselTable);
+			// Set the part count
+			this.vesselPartCountTable[vessel.id] = vessel.Parts.Count;
+		}
+
+		// Returns true if both the relayDatabase and the vesselPartCountDB contain the vessel id.
+		public bool ContainsKey(Guid key)
+		{
+			return (this.relayDatabase.ContainsKey(key) & this.vesselPartCountTable.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 onVesselWasModified(Vessel vessel)
+		{
+			// If we have this vessel in our cache...
+			if (this.ContainsKey(vessel))
+			{
+				// If our part counts disagree (such as if a part has been added or broken off,
+				// or if we've just docked or undocked)...
+				if (this.vesselPartCountTable[vessel.id] != vessel.Parts.Count)
+				{
+					Tools.PostDebugMessage(string.Format(
+						"{0}: dirtying cache for vessel '{1}' (id: {2}).",
+						this.GetType().Name,
+						vessel.name,
+						vessel.id
+					));
+
+					// Dirty the cache (real vessels will never have negative part counts)
+					this.vesselPartCountTable[vessel.id] = -1;
+				}
+			}
+		}
+
+		// 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.onVesselWasModified(FlightGlobals.ActiveVessel);
+			}
+		}
+
+		// Produce a Part-hashed table of relays for the given vessel
+		protected void getVesselRelays(Vessel vessel, ref Dictionary<int, IAntennaRelay> relays)
+		{
+			// We're going to completely regen this table, so dump the current contents.
+			relays.Clear();
+
+			Tools.PostDebugMessage(string.Format(
+				"{0}: Getting antenna relays from vessel {1}.",
+				"IAntennaRelay",
+				vessel.name
+			));
+
+			// If the vessel is loaded, we can fetch modules implementing IAntennaRelay directly.
+			if (vessel.loaded) {
+				Tools.PostDebugMessage(string.Format(
+					"{0}: vessel {1} is loaded, searching for modules in loaded parts.",
+					"IAntennaRelay",
+					vessel.name
+				));
+
+				// Loop through the Parts in the Vessel...
+				foreach (Part part in vessel.Parts)
+				{
+					// ...loop through the PartModules in the Part...
+					foreach (PartModule module in part.Modules)
+					{
+						// ...if the module is a relay...
+						if (module is IAntennaRelay)
+						{
+							// ...add the module to the table
+							relays.Add(part.GetHashCode(), module as IAntennaRelay);
+							// ...neglect relay objects after the first in each part.
+							break;
+						}
+					}
+				}
+			}
+			// If the vessel is not loaded, we need to build ProtoAntennaRelays when we find relay ProtoPartSnapshots.
+			else
+			{
+				Tools.PostDebugMessage(string.Format(
+					"{0}: vessel {1} is not loaded, searching for modules in prototype parts.",
+					"IAntennaRelay",
+					vessel.name
+				));
+
+				// Loop through the ProtoPartModuleSnapshots in the Vessel...
+				foreach (ProtoPartSnapshot pps in vessel.protoVessel.protoPartSnapshots)
+				{
+					// ...Fetch the prefab, because it's more useful for what we're doing.
+					Part partPrefab = PartLoader.getPartInfoByName(pps.partName).partPrefab;
+
+					// ...loop through the PartModules in the prefab...
+					foreach (PartModule module in partPrefab.Modules)
+					{
+						// ...if the module is a relay...
+						if (module is IAntennaRelay)
+						{
+							// ...build a new ProtoAntennaRelay and add it to the table
+							relays.Add(pps.GetHashCode(), new ProtoAntennaRelay(module as IAntennaRelay, pps));
+							// ...neglect relay objects after the first in each part.
+							break;
+						}
+					}
+				}
+			}
+
+			Tools.PostDebugMessage(string.Format(
+				"{0}: vessel '{1}' has {2} transmitters.",
+				"IAntennaRelay",
+				vessel.name,
+				relays.Count
+			));
+		}
+
+		// Construct the singleton
+		protected RelayDatabase()
+		{
+			// Initialize the databases
+			relayDatabase =	new Dictionary<Guid, Dictionary<int, IAntennaRelay>>();
+			vesselPartCountTable = new Dictionary<Guid, int>();
+
+			// Subscribe to some events
+			GameEvents.onVesselWasModified.Add(this.onVesselWasModified);
+			GameEvents.onVesselChange.Add(this.onVesselWasModified);
+			GameEvents.onGameSceneLoadRequested.Add(this.onSceneChange);
+		}
+
+		~RelayDatabase()
+		{
+			// Unsubscribe from the events
+			GameEvents.onVesselWasModified.Remove(this.onVesselWasModified);
+			GameEvents.onVesselChange.Remove(this.onVesselWasModified);
+			GameEvents.onGameSceneLoadRequested.Remove(this.onSceneChange);
+
+			Tools.PostDebugMessage(this.GetType().Name + " destroyed.");
+		}
+	}
+}
+
+