From: toadicus Date: Tue, 09 Jun 2015 21:46:52 +0000 Subject: AntennaRelay: Move all variable declarations outside the loop, just in case. X-Git-Url: http://git.toad.homelinux.net/projects/AntennaRange.git/commitdiff/094538b --- AntennaRelay: Move all variable declarations outside the loop, just in case. --- --- /dev/null +++ b/.gitattributes @@ -1,1 +1,13 @@ +* text=auto +* eol=lf +# These files are text and should be normalized (convert crlf => lf) +*.cs text diff=csharp +*.cfg text +*.csproj text +*.sln text + +# Images should be treated as binary +# (binary is a macro for -text -diff) +*.png binary + --- a/ARConfiguration.cs +++ b/ARConfiguration.cs @@ -10,15 +10,124 @@ namespace AntennaRange { + /// + /// A responsible for managing configuration items for AntennaRange. + /// [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"; + + /// + /// Indicates whether connections require line of sight. + /// + public static bool RequireLineOfSight + { + get; + private set; + } + + /// + /// A "fudge factor" ratio that pretends planets and moons are slightly smaller than reality to make + /// building communication constellations easier. + /// + public static double RadiusRatio + { + get; + private set; + } + + /// + /// Indicates whether unmanned vessels require a connection for control. + /// + public static bool RequireConnectionForControl + { + get; + private set; + } + + /// + /// If true, relays will fix their power cost when above nominal range, decreasing data rate instead. + /// + public static bool FixedPowerCost + { + get; + private set; + } + + /// + /// Indicates whether this AntennaRange will draw pretty lines in map view. + /// + public static bool PrettyLines + { + get; + set; + } + + /// + /// Gets the update delay. + /// + public static long UpdateDelay + { + get; + private set; + } + + /// + /// Gets a value indicating whether AntennaRange will use additive ranges. + /// + public static bool UseAdditiveRanges + { + get; + private set; + } + + /// + /// Gets Kerbin's relay range based on the current tracking station level. + /// + public static double KerbinRelayRange + { + get; + private set; + } + + /// + /// Gets Kerbin's nominal relay range based on the current tracking station level. + /// + public static double KerbinNominalRange + { + get + { + return KerbinRelayRange / 2.8284271247461901d; + } + } + +#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 @@ -38,38 +147,120 @@ { Tools.PostDebugMessage(this, "Waking up."); + this.runningVersion = this.GetType().Assembly.GetName().Version; + this.showConfigWindow = false; this.configWindowPos = new Rect(Screen.width / 4, Screen.height / 2, 180, 15); + + this.configWindowPos = this.LoadConfigValue(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); + + this.updateDelayStr = ARConfiguration.UpdateDelay.ToString(); + + GameEvents.onGameSceneLoadRequested.Add(this.onSceneChangeRequested); + GameEvents.OnKSCFacilityUpgraded.Add(this.onFacilityUpgraded); + + Debug.Log(string.Format("{0} v{1} - ARConfiguration loaded!", this.GetType().Name, this.runningVersion)); + + 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; + Tools.PostDebugMessage(this, "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) - { - this.runningVersion = this.GetType().Assembly.GetName().Version; - - Tools.PostDebugMessage(this, "Toolbar available; initializing 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.showConfigWindow = !this.showConfigWindow; - }; - - this.configWindowPos = this.LoadConfigValue("configWindowPos", this.configWindowPos); - AntennaRelay.requireLineOfSight = this.LoadConfigValue("requireLineOfSight", false); - ARFlightController.requireConnectionForControl = - this.LoadConfigValue("requireConnectionForControl", false); - ModuleLimitedDataTransmitter.fixedPowerCost = this.LoadConfigValue("fixedPowerCost", false); - - Debug.Log(string.Format("{0} v{1} - ARonfiguration loaded!", this.GetType().Name, this.runningVersion)); + if (ToolbarManager.ToolbarAvailable) + { + if (this.toolbarButton == null) + { + Tools.PostDebugMessage(this, "Toolbar available; initializing toolbar button."); + + this.toolbarButton = ToolbarManager.Instance.add("AntennaRange", "ARConfiguration"); + this.toolbarButton.Visibility = new GameScenesVisibility(GameScenes.SPACECENTER); + this.toolbarButton.Text = "AR"; + this.toolbarButton.TexturePath = "AntennaRange/Textures/toolbarIcon"; + this.toolbarButton.TextColor = (Color)XKCDColors.Amethyst; + this.toolbarButton.OnClick += delegate(ClickEvent e) + { + this.toggleConfigWindow(); + }; + } + } + else if (this.appLauncherButton == null && ApplicationLauncher.Ready) + { + Tools.PostDebugMessage(this, "Toolbar available; initializing AppLauncher button."); + + this.appLauncherButton = ApplicationLauncher.Instance.AddModApplication( + this.toggleConfigWindow, + this.toggleConfigWindow, + ApplicationLauncher.AppScenes.SPACECENTER, + GameDatabase.Instance.GetTexture( + "AntennaRange/Textures/appLauncherIcon", + false + ) + ); } if (this.showConfigWindow) @@ -87,7 +278,7 @@ if (configPos != this.configWindowPos) { this.configWindowPos = configPos; - this.SaveConfigValue("configWindowPos", this.configWindowPos); + this.SaveConfigValue(WINDOW_POS_KEY, this.configWindowPos); } } } @@ -98,11 +289,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 = GUITools.Toggle(ARConfiguration.RequireLineOfSight, "Require Line of Sight"); + if (requireLineOfSight != ARConfiguration.RequireLineOfSight) + { + ARConfiguration.RequireLineOfSight = requireLineOfSight; + this.SaveConfigValue(REQUIRE_LOS_KEY, requireLineOfSight); } GUILayout.EndHorizontal(); @@ -110,40 +301,164 @@ GUILayout.BeginHorizontal(GUILayout.ExpandWidth(true)); bool requireConnectionForControl = - GUILayout.Toggle( - ARFlightController.requireConnectionForControl, + GUITools.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 = GUITools.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 = GUITools.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 = GUITools.Toggle(ARConfiguration.PrettyLines, "Draw Pretty Lines"); + if (prettyLines != ARConfiguration.PrettyLines) + { + ARConfiguration.PrettyLines = prettyLines; + this.SaveConfigValue(PRETTY_LINES_KEY, prettyLines); + } + + 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(ARConfiguration.RadiusRatio); + double newRatio; + + GUILayout.Label(string.Format("Line of Sight 'Fudge Factor': {0:P0}", graceRatio)); + + GUILayout.EndHorizontal(); + + GUILayout.BeginHorizontal(); + + newRatio = GUILayout.HorizontalSlider((float)graceRatio, 0f, 1f, GUILayout.ExpandWidth(true)); + newRatio = Math.Round(newRatio, 2); + + if (newRatio != graceRatio) + { + ARConfiguration.RadiusRatio = (1d - newRatio) * (1d - newRatio); + this.SaveConfigValue(GRACE_RATIO_KEY, newRatio); + } + + GUILayout.EndHorizontal(); + } GUILayout.EndVertical(); GUI.DragWindow(); } - public void Destroy() - { + 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); + this.appLauncherButton = null; + } + } + + private void onSceneChangeRequested(GameScenes scene) + { + if (scene != GameScenes.SPACECENTER) + { + print("ARConfiguration: Requesting Destruction."); + MonoBehaviour.Destroy(this); + } + } + + 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(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,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; @@ -37,28 +39,40 @@ [KSPAddon(KSPAddon.Startup.Flight, false)] public class ARFlightController : MonoBehaviour { - #region Static Members - public static bool requireConnectionForControl; - #endregion - #region Fields - protected Dictionary connectionTextures; - - protected IButton toolbarButton; + private Dictionary toolbarTextures; + private Dictionary appLauncherTextures; + + private ARMapRenderer mapRenderer; + + private IButton toolbarButton; + + private ApplicationLauncherButton appLauncherButton; + private Tools.DebugLogger log; + + private System.Diagnostics.Stopwatch updateTimer; #endregion #region Properties public ConnectionStatus currentConnectionStatus { get; - protected set; - } - - protected string currentConnectionTexture - { - get - { - return this.connectionTextures[this.currentConnectionStatus]; + private set; + } + + private string currentConnectionTexture + { + get + { + return this.toolbarTextures[this.currentConnectionStatus]; + } + } + + private Texture currentAppLauncherTexture + { + get + { + return this.appLauncherTextures[this.currentConnectionStatus]; } } @@ -78,7 +92,7 @@ public string lockID { get; - protected set; + private set; } public ControlTypes lockSet @@ -104,43 +118,79 @@ #endregion #region MonoBehaviour LifeCycle - protected void Awake() + private void Awake() { this.lockID = "ARConnectionRequired"; + this.log = Tools.DebugLogger.New(this); + + this.updateTimer = new System.Diagnostics.Stopwatch(); + + this.toolbarTextures = new Dictionary(); + + 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(); + + this.appLauncherTextures[ConnectionStatus.None] = + GameDatabase.Instance.GetTexture("AntennaRange/Textures/appLauncherIconNoConnection", false); + this.appLauncherTextures[ConnectionStatus.Suboptimal] = + GameDatabase.Instance.GetTexture("AntennaRange/Textures/appLauncherIconSubOptimal", false); + this.appLauncherTextures[ConnectionStatus.Optimal] = + GameDatabase.Instance.GetTexture("AntennaRange/Textures/appLauncherIcon", false); + if (ToolbarManager.ToolbarAvailable) { - this.connectionTextures = new Dictionary(); - - this.connectionTextures[ConnectionStatus.None] = "AntennaRange/Textures/toolbarIconNoConnection"; - this.connectionTextures[ConnectionStatus.Suboptimal] = "AntennaRange/Textures/toolbarIconSubOptimal"; - this.connectionTextures[ConnectionStatus.Optimal] = "AntennaRange/Textures/toolbarIcon"; - 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() - { - Tools.DebugLogger log = Tools.DebugLogger.New(this); + 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) @@ -156,83 +206,126 @@ InputLockManager.RemoveControlLock(this.lockID); } - if (this.toolbarButton != null && HighLogic.LoadedSceneIsFlight && FlightGlobals.ActiveVessel != null) - { - log.Append("Checking vessel relay status.\n"); - - List relays = - FlightGlobals.ActiveVessel.getModulesOfType(); - - 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) + log.Print(); + } + + private void Update() + { + if (MapView.MapIsEnabled && this.mapRenderer == null) + { + this.mapRenderer = MapView.MapCamera.gameObject.AddComponent(); + } + + 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(); + + if (HighLogic.LoadedSceneIsFlight && FlightGlobals.ActiveVessel != null) + { + Vessel vessel; + IAntennaRelay relay; + IList activeVesselRelays; + + for (int vIdx = 0; vIdx < FlightGlobals.Vessels.Count; vIdx++) + { + vessel = FlightGlobals.Vessels[vIdx]; + + if (vessel == null || vessel == FlightGlobals.ActiveVessel) { - vesselCanTransmit = true; + continue; } - if (!vesselHasOptimalRelay && - relayCanTransmit && - relay.transmitDistance <= (double)relay.nominalRange) + log.AppendFormat("Fetching best relay for vessel {0}", vessel); + + relay = vessel.GetBestRelay(); + + if (relay != null) { - vesselHasOptimalRelay = true; + log.AppendFormat("Finding nearest relay for best relay {0}", relay); + + relay.FindNearestRelay(); } - - if (vesselCanTransmit && vesselHasOptimalRelay) + } + + activeVesselRelays = RelayDatabase.Instance[FlightGlobals.ActiveVessel]; + for (int rIdx = 0; rIdx < activeVesselRelays.Count; rIdx++) + { + relay = activeVesselRelays[rIdx]; + + relay.FindNearestRelay(); + } + + if (this.toolbarButton != null || this.appLauncherButton != null) + { + log.Append("Checking vessel relay status.\n"); + + this.currentConnectionStatus = FlightGlobals.ActiveVessel.GetConnectionStatus(); + + log.AppendFormat("currentConnectionStatus: {0}, setting texture to {1}", + this.currentConnectionStatus, this.currentConnectionTexture); + + if (this.toolbarButton != null) { - break; + 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; + } } - } - - 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); - - this.toolbarButton.TexturePath = this.currentConnectionTexture; + if (this.appLauncherButton != null) + { + this.appLauncherButton.SetTexture(this.currentAppLauncherTexture); + } + } } log.Print(); } - protected void OnDestroy() + private void OnDestroy() { InputLockManager.RemoveControlLock(this.lockID); + if (this.mapRenderer != null) + { + GameObject.Destroy(this.mapRenderer); + } + if (this.toolbarButton != null) { this.toolbarButton.Destroy(); + } + + if (this.appLauncherButton != null) + { + ApplicationLauncher.Instance.RemoveModApplication(this.appLauncherButton); + this.appLauncherButton = null; } GameEvents.onGameSceneLoadRequested.Remove(this.onSceneChangeRequested); @@ -242,29 +335,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 - } } } - --- /dev/null +++ b/ARMapRenderer.cs @@ -1,1 +1,360 @@ - +// 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; +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 vesselLineRenderers; + + // Debug Stuff + #pragma warning disable 649 + private System.Diagnostics.Stopwatch timer; + private Tools.DebugLogger 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(); + + // lr.SetColors(Color.green, Color.green); + lr.material = MapView.OrbitLinesMaterial; + // lr.SetVertexCount(2); + + this.vesselLineRenderers[idx] = lr; + + return lr; + } + + return lr; + } + } + #endregion + + #region MonoBehaviour Lifecycle + private void Awake() + { + if (ARConfiguration.PrettyLines) + { + this.vesselLineRenderers = new Dictionary(); + } + + #if DEBUG || BENCH + this.timer = new System.Diagnostics.Stopwatch(); + #endif + #if DEBUG + this.log = Tools.DebugLogger.New(this); + #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"); + + 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 + ); + + if (FlightGlobals.ready && FlightGlobals.Vessels != null) + { + log.AppendLine("FlightGlobals ready and Vessels list not null."); + + for (int i = 0; i < FlightGlobals.Vessels.Count; i++) + { + Vessel vessel = FlightGlobals.Vessels[i]; + + log.AppendFormat("\nStarting check for vessel {0} at {1}ms", vessel, timer.ElapsedMilliseconds); + + if (vessel == null) + { + log.AppendFormat("\n\tSkipping vessel {0} altogether because it is null.", vessel); + continue; + } + + switch (vessel.vesselType) + { + case VesselType.Debris: + case VesselType.EVA: + case VesselType.Unknown: + case VesselType.SpaceObject: + log.AppendFormat("\n\tDiscarded because vessel is of invalid type {0}", + vessel.vesselType); + continue; + } + + log.AppendFormat("\n\tChecking vessel {0}.", vessel.vesselName); + + #if DEBUG + start = timer.ElapsedMilliseconds; + #endif + + IAntennaRelay vesselRelay = vessel.GetBestRelay(); + + if (vesselRelay == null) + { + log.AppendFormat("\n\tGot null relay for vessel {0}", vessel.vesselName); + continue; + } + + log.AppendFormat("\n\tGot best relay {0} ({3}) for vessel {1} in {2} ms", + vesselRelay, vessel, timer.ElapsedMilliseconds - start, vesselRelay.GetType().Name); + + if (vesselRelay != null) + { + #if DEBUG + start = timer.ElapsedMilliseconds; + #endif + + this.SetRelayVertices(vesselRelay); + + log.AppendFormat("\n\tSet relay vertices for {0} in {1}ms", + vessel, 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]; + Vector3d start = ScaledSpace.LocalToScaledSpace(relay.vessel.GetWorldPos3D()); + + float lineWidth; + float d = Screen.height / 2f + 0.01f; + + if (MapView.Draw3DLines) + { + lineWidth = 0.005859375f * MapView.MapCamera.Distance; + } + else + { + lineWidth = 2f; + + start = MapView.MapCamera.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 + + Vector3d 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; + } + + nextPoint = ScaledSpace.LocalToScaledSpace(relay.targetRelay.vessel.GetWorldPos3D()); + } + + renderer.SetColors(thisColor, thisColor); + + if (!MapView.Draw3DLines) + { + nextPoint = MapView.MapCamera.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 enumerator = this.vesselLineRenderers.Values.GetEnumerator(); + LineRenderer lineRenderer; + + while (enumerator.MoveNext()) + { + lineRenderer = enumerator.Current; + lineRenderer.enabled = false; + + if (freeObjects) + { + GameObject.Destroy(lineRenderer.gameObject); + } + } + + if (freeObjects) + { + this.vesselLineRenderers.Clear(); + } + } + } + #endregion + } +} + --- 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 @@ false - - + @@ -38,10 +37,10 @@ false - - + + bin\Release\AntennaRange.xml true @@ -54,7 +53,7 @@ false - + @@ -65,7 +64,7 @@ 4 - + false @@ -80,13 +79,9 @@ + - - - PreserveNewest - - ..\_KSPAssemblies\Assembly-CSharp.dll @@ -107,4 +102,8 @@ ToadicusTools + + + + --- 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,25 +28,55 @@ using System; using System.Collections.Generic; -using System.Linq; using ToadicusTools; namespace AntennaRange { + /// + /// Relay code at the heart of AntennaRange + /// public class AntennaRelay { - public static bool requireLineOfSight; - // 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; + + /// + /// Fetches, caches, and returns a reference to Kerbin + /// + 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; + + /// + /// The reference underlying this AntennaRelay, as an + /// + /// protected IAntennaRelay moduleRef; - - protected System.Diagnostics.Stopwatch searchTimer; - protected long millisecondsBetweenSearches; /// /// Gets the parent Vessel. @@ -61,38 +91,49 @@ } /// - /// Gets or sets the nearest relay. - /// - /// The nearest relay - public IAntennaRelay nearestRelay - { - 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; - } - } - - /// - /// Gets the first occluding body. - /// - /// The first occluding body. - public CelestialBody firstOccludingBody - { - get - { - return this._firstOccludingBody; - } + /// Gets the target relay. + /// + public IAntennaRelay targetRelay + { + get; + protected set; + } + + /// + /// Gets a value indicating whether this Relay is communicating + /// directly with Kerbin. + /// + public virtual bool KerbinDirect + { + get; + protected set; + } + + /// + /// Gets or sets the nominal link distance, in meters. + /// + public virtual double NominalLinkDistance + { + get; + protected set; + } + + /// + /// Gets or sets the maximum link distance, in meters. + /// + public virtual double MaximumLinkDistance + { + get; + protected set; + } + + /// + /// Gets the first found to be blocking line of sight. + /// + public virtual CelestialBody firstOccludingBody + { + get; + protected set; } /// @@ -103,41 +144,43 @@ { 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); + if (this.KerbinDirect || this.targetRelay == null) + { + return this.DistanceTo(Kerbin); } else { - /// ...otherwise, return the distance to the nearest available relay. - return this.DistanceTo(nearestRelay); - } - } - } - - /// - /// The maximum distance at which this relay can operate. - /// - /// The max transmit distance. - public virtual float maxTransmitDistance + return this.DistanceTo(this.targetRelay); + } + } + } + + /// + /// Gets or sets the link status. + /// + public virtual ConnectionStatus LinkStatus + { + get; + protected set; + } + + /// + /// Gets the nominal transmit distance at which the Antenna behaves just as prescribed by Squad's config. + /// + public virtual double nominalTransmitDistance { get; set; } /// - /// Gets a value indicating whether this has been checked during - /// the current relay attempt. - /// - /// true if relay checked; otherwise, false. - public virtual bool relayChecked + /// The maximum distance at which this relay can operate. + /// + /// The max transmit distance. + public virtual double maxTransmitDistance { get; - protected set; + set; } /// @@ -146,72 +189,115 @@ /// true if this instance can transmit; otherwise, false. public virtual bool CanTransmit() { - if ( - this.transmitDistance > this.maxTransmitDistance || - ( - requireLineOfSight && - this.nearestRelay == null && - !this.vessel.hasLineOfSightTo(this.Kerbin, out this._firstOccludingBody) - ) - ) - { - return false; - } - else - { - return true; - } + return this.canTransmit; } /// /// Finds the nearest relay. /// /// The nearest relay or null, if no relays in range. - 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; + } + + Tools.DebugLogger log; + #if DEBUG + log = Tools.DebugLogger.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; + + double nearestRelaySqrQuotient = double.PositiveInfinity; + double bestOccludedSqrQuotient = double.PositiveInfinity; + + double potentialSqrDistance; + double maxLinkSqrDistance; + double potentialSqrQuotient; + + double kerbinSqrDistance; + double kerbinSqrQuotient; + + 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 + * and that can transmit. Once we find a suitable candidate, assign it to nearestRelay for comparison * against future finds. * */ - foreach (Vessel potentialVessel in FlightGlobals.Vessels) - { - // Skip vessels that have already been checked for a nearest relay this pass. - if (RelayDatabase.Instance.CheckedVesselsTable.ContainsKey(potentialVessel.id)) - { - continue; - } + Vessel potentialVessel; + IAntennaRelay potentialBestRelay; + CelestialBody fob; + + #if BENCH + startVesselLoopTicks = performanceTimer.ElapsedTicks; + #endif + + for (int vIdx = 0; vIdx < FlightGlobals.Vessels.Count; vIdx++) + { + log.AppendFormat("\nFetching vessel at index {0}", vIdx); + potentialVessel = FlightGlobals.Vessels[vIdx]; + + if (potentialVessel == null) + { + Tools.PostErrorMessage("{0}: Skipping vessel at index {1} because it is null.", this, vIdx); + log.AppendFormat("\n\tSkipping vessel at index {0} because it is null.", vIdx); + log.Print(); + return; + } + #if DEBUG + else + { + log.AppendFormat("\n\tGot vessel {0}", potentialVessel); + } + #endif // Skip vessels of the wrong type. + log.Append("\n\tchecking vessel type"); switch (potentialVessel.vesselType) { case VesselType.Debris: @@ -219,101 +305,620 @@ case VesselType.EVA: case VesselType.SpaceObject: case VesselType.Unknown: + log.Append("\n\tSkipping because vessel is the wrong type."); continue; default: break; } - + + log.Append("\n\tchecking if vessel is this vessel"); // Skip vessels with the wrong ID if (potentialVessel.id == vessel.id) { + log.Append("\n\tSkipping because vessel is this vessel."); continue; } + potentialBestRelay = potentialVessel.GetBestRelay(); + log.AppendFormat("\n\t\tgot best vessel relay {0}", + potentialBestRelay == null ? "null" : potentialBestRelay.ToString()); + + if (potentialBestRelay == null) + { + log.Append("\n\t\t...skipping null relay"); + continue; + } + + #if BENCH + usefulVesselCount++; + #endif + + // Find the distance from here to the vessel... + log.Append("\n\tgetting distance to potential vessel"); + potentialSqrDistance = this.sqrDistanceTo(potentialVessel); + log.Append("\n\tgetting best vessel relay"); + + log.Append("\n\tgetting max link distance to potential relay"); + + if (ARConfiguration.UseAdditiveRanges) + { + maxLinkSqrDistance = this.maxTransmitDistance * potentialBestRelay.maxTransmitDistance; + } + else + { + maxLinkSqrDistance = this.maxTransmitDistance * this.maxTransmitDistance; + } + + log.AppendFormat("\n\tmax link distance: {0}", maxLinkSqrDistance); + + potentialSqrQuotient = potentialSqrDistance / maxLinkSqrDistance; + + #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 (requireLineOfSight && !this.vessel.hasLineOfSightTo(potentialVessel, out this._firstOccludingBody)) - { - Tools.PostDebugMessage( - this, - "Vessel {0} discarded because we do not have line of sight.", + if ( + ARConfiguration.RequireLineOfSight && + !this.vessel.hasLineOfSightTo(potentialVessel, 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}: Vessel {1} not in line of sight.", + this.ToString(), potentialVessel.vesselName); + + log.AppendFormat("\n\t\t\tpotentialSqrDistance: {0}", potentialSqrDistance); + log.AppendFormat("\n\t\t\tbestOccludedSqrQuotient: {0}", bestOccludedSqrQuotient); + log.AppendFormat("\n\t\t\tmaxTransmitSqrDistance: {0}", maxLinkSqrDistance); + + if ( + (potentialSqrQuotient < bestOccludedSqrQuotient) && + (potentialSqrQuotient <= 1d) && + potentialBestRelay.CanTransmit() + ) + { + log.Append("\n\t\t...vessel is close enough to and potentialBestRelay can transmit"); + log.AppendFormat("\n\t\t...{0} found new best occluded relay {1}", this, potentialBestRelay); + + this.bestOccludedRelay = potentialBestRelay; + bodyOccludingBestOccludedRelay = fob; + bestOccludedSqrQuotient = potentialSqrQuotient; + } + 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 a vessel we've already checked. + * */ + if (potentialSqrQuotient > nearestRelaySqrQuotient) + { + + log.AppendFormat("\n\t{0}: Vessel {1} discarded because it is farther than another the nearest relay.", + this.ToString(), potentialVessel.vesselName ); continue; } - // Find the distance from here to the vessel... - double potentialSqrDistance = (potentialVessel.GetWorldPos3D() - vessel.GetWorldPos3D()).sqrMagnitude; - - /* - * ...so that we can skip the vessel if it is further away than Kerbin, our transmit distance, or 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 - ); - 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; - } - - /// - /// Initializes a new instance of the class. - /// - /// + 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) + { + Tools.PostErrorMessage( + "[{0}] iterCount exceeded while checking for circular network; assuming it is circular" + + "\n\tneedle={1}" + + "\n\tthis.moduleRef={2}", + this, + 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) + { + nearestRelaySqrQuotient = potentialSqrQuotient; + this.nearestRelay = potentialBestRelay; + + log.AppendFormat("\n\t{0}: found new nearest relay {1} ({2}m²)", + this.ToString(), + this.nearestRelay.ToString(), + Math.Sqrt(nearestRelaySqrQuotient) + ); + } + 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; + + kerbinSqrDistance = this.vessel.DistanceTo(Kerbin) - Kerbin.Radius; + kerbinSqrDistance *= kerbinSqrDistance; + + if (ARConfiguration.UseAdditiveRanges) + { + kerbinSqrQuotient = kerbinSqrDistance / + (this.maxTransmitDistance * ARConfiguration.KerbinRelayRange); + } + else + { + kerbinSqrQuotient = kerbinSqrDistance / + (this.maxTransmitDistance * this.maxTransmitDistance); + } + + log.AppendFormat("\n{0} ({1}): Search done, figuring status.", this.ToString(), this.GetType().Name); + log.AppendFormat( + "\n{0}: nearestRelay={1} ({2}m²)), bestOccludedRelay={3} ({4}m²), kerbinSqrDistance={5}m²)", + this, + this.nearestRelay == null ? "null" : this.nearestRelay.ToString(), + nearestRelaySqrQuotient, + this.bestOccludedRelay == null ? "null" : this.bestOccludedRelay.ToString(), + bestOccludedSqrQuotient, + kerbinSqrDistance + ); + + #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); + + // nearestRelaySqrDistance will be infinity if all relays are occluded or none exist. + // Therefore, this will only be true if a valid relay is in range. + if (nearestRelaySqrQuotient <= 1d) + { + log.AppendFormat("\n\t\tCan transmit to nearby relay {0} ({1} <= {2}).", + this.nearestRelay == null ? "null" : this.nearestRelay.ToString(), + nearestRelaySqrQuotient, 1d); + + 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} ({1} > {2}).", + this.nearestRelay == null ? "null" : this.nearestRelay.ToString(), + nearestRelaySqrQuotient, 1d); + + this.canTransmit = false; + + // If the best occluded relay is closer than Kerbin, check it against the nearest relay. + // Since bestOccludedSqrDistance is infinity if there are no occluded relays, this is safe + if (bestOccludedSqrQuotient < kerbinSqrQuotient) + { + log.AppendFormat("\n\t\t\tBest occluded relay is closer than Kerbin ({0} < {1})", + bestOccludedRelay, kerbinSqrDistance); + + this.KerbinDirect = false; + + // If the nearest relay is closer than the best occluded relay, pick it. + // Since nearestRelaySqrDistane is infinity if there are no nearby relays, this is safe. + if (nearestRelaySqrQuotient < bestOccludedSqrQuotient) + { + log.AppendFormat("\n\t\t\t\t...but the nearest relay is closer ({0} < {1}), so picking it.", + nearestRelaySqrQuotient, bestOccludedSqrQuotient); + + this.targetRelay = this.nearestRelay; + this.firstOccludingBody = null; + } + // Otherwise, target the best occluded relay. + else + { + log.AppendFormat("\n\t\t\t\t...and closer than the nearest relay ({0} >= {1}), so picking it.", + nearestRelaySqrQuotient, bestOccludedSqrQuotient); + + 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 closer than the best occluded relay ({0} >= {1})", + bestOccludedRelay, kerbinSqrDistance); + + // If the nearest relay is closer than Kerbin, pick it. + // Since nearestRelaySqrDistane is infinity if there are no nearby relays, this is safe. + if (nearestRelaySqrQuotient < kerbinSqrQuotient) + { + log.AppendFormat("\n\t\t\t\t...but the nearest relay is closer ({0} < {1}), so picking it.", + nearestRelaySqrQuotient, kerbinSqrQuotient); + + this.KerbinDirect = false; + this.firstOccludingBody = null; + this.targetRelay = this.nearestRelay; + } + // Otherwise, pick Kerbin. + else + { + log.AppendFormat("\n\t\t\t\t...and closer than the nearest relay ({0} >= {1}), so picking it.", + nearestRelaySqrQuotient, kerbinSqrQuotient); + + 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 closer than Kerbin and in range, transmit to it. + if (nearestRelaySqrQuotient <= 1d) + { + log.AppendFormat("\n\t\tCan transmit to nearby relay {0} ({1} <= {2}).", + this.nearestRelay == null ? "null" : this.nearestRelay.ToString(), + nearestRelaySqrQuotient, 1d); + + this.canTransmit = true; + + // If the nearestRelay is closer than Kerbin, use it. + if (nearestRelaySqrQuotient < kerbinSqrQuotient) + { + log.AppendFormat("\n\t\t\tPicking relay {0} over Kerbin ({1} < {2}).", + this.nearestRelay == null ? "null" : this.nearestRelay.ToString(), + nearestRelaySqrQuotient, kerbinSqrQuotient); + + 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(), + nearestRelaySqrQuotient, kerbinSqrQuotient); + + 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\tCan't transmit to nearby relay {0} ({1} > {2}).", + this.nearestRelay == null ? "null" : this.nearestRelay.ToString(), + nearestRelaySqrQuotient, 1d); + + // If Kerbin is in range, use it. + if (kerbinSqrQuotient <= 1d) + { + log.AppendFormat("\n\t\t\tCan transmit to Kerbin ({0} <= {1}).", + kerbinSqrQuotient, 1d); + + 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 ({0} > {1}).", + kerbinSqrQuotient, 1d); + + this.canTransmit = false; + + // If the best occluded relay is closer than Kerbin, check it against the nearest relay. + // Since bestOccludedSqrDistance is infinity if there are no occluded relays, this is safe + if (bestOccludedSqrQuotient < kerbinSqrQuotient) + { + log.AppendFormat("\n\t\t\tBest occluded relay is closer than Kerbin ({0} < {1})", + bestOccludedRelay, kerbinSqrDistance); + + this.KerbinDirect = false; + + // If the nearest relay is closer than the best occluded relay, pick it. + // Since nearestRelaySqrDistane is infinity if there are no nearby relays, this is safe. + if (nearestRelaySqrQuotient < bestOccludedSqrQuotient) + { + log.AppendFormat("\n\t\t\t\t...but the nearest relay is closer ({0} < {1}), so picking it.", + nearestRelaySqrQuotient, bestOccludedSqrQuotient); + + this.targetRelay = this.nearestRelay; + this.firstOccludingBody = null; + } + // Otherwise, target the best occluded relay. + else + { + log.AppendFormat("\n\t\t\t\t...and closer than the nearest relay ({0} >= {1}), so picking it.", + nearestRelaySqrQuotient, bestOccludedSqrQuotient); + + 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 closer than the best occluded relay ({0} >= {1})", + bestOccludedRelay, kerbinSqrDistance); + + this.firstOccludingBody = null; + + // If the nearest relay is closer than Kerbin, pick it. + // Since nearestRelaySqrDistane is infinity if there are no nearby relays, this is safe. + if (nearestRelaySqrQuotient < kerbinSqrQuotient) + { + log.AppendFormat("\n\t\t\t\t...but the nearest relay is closer ({0} < {1}), so picking it.", + nearestRelaySqrQuotient, kerbinSqrQuotient); + + this.KerbinDirect = false; + this.targetRelay = this.nearestRelay; + } + // Otherwise, pick Kerbin. + else + { + log.AppendFormat("\n\t\t\t\t...and closer than the nearest relay ({0} >= {1}), so picking it.", + nearestRelaySqrQuotient, kerbinSqrQuotient); + + this.KerbinDirect = true; + this.targetRelay = null; + } + } + } + } + } + + if (ARConfiguration.UseAdditiveRanges) + { + if (this.KerbinDirect) + { + this.NominalLinkDistance = Math.Sqrt(this.nominalTransmitDistance * ARConfiguration.KerbinNominalRange); + this.MaximumLinkDistance = Math.Sqrt(this.maxTransmitDistance * ARConfiguration.KerbinRelayRange); + } + else + { + this.NominalLinkDistance = Math.Sqrt(this.nominalTransmitDistance * this.targetRelay.nominalTransmitDistance); + this.MaximumLinkDistance = Math.Sqrt(this.maxTransmitDistance * this.targetRelay.maxTransmitDistance); + } + } + else + { + this.NominalLinkDistance = this.nominalTransmitDistance; + this.MaximumLinkDistance = this.maxTransmitDistance; + } + + if (this.canTransmit) + { + if (this.transmitDistance < this.NominalLinkDistance) + { + 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) + + Tools.PostWarningMessage(sb.ToString()); + + Tools.PutStringBuilder(sb); + } + #endif + } + + /// + /// Returns a that represents the current . + /// + /// A that represents the current . + public override string ToString() + { + if (this is ProtoAntennaRelay) + { + return (this as ProtoAntennaRelay).ToString(); + } + return this.moduleRef.ToString(); + } + + /// + /// Initializes a new instance of the class. + /// + /// The module reference underlying this AntennaRelay, + /// as an public AntennaRelay(IAntennaRelay module) { 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(); - - config.load(); - - AntennaRelay.requireLineOfSight = config.GetValue("requireLineOfSight", false); - - config.save(); + #if BENCH + AntennaRelay.relayCount++; + #endif + + Tools.PostLogMessage("{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,140 @@ +// 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[!RemoteTech2] +{ + @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[!RemoteTech2] +{ + @MODULE[ModuleDataTransmitter] + { + @name = ModuleLimitedDataTransmitter + nominalRange = 3500000000 + simpleRange = 18000000000 + maxPowerFactor = 4 + maxDataFactor = 8 + } + + MODULE + { + name = ModuleScienceContainer + + dataIsCollectable = true + dataIsStorable = false + + storageRange = 2 + } +} + +@PART[commDish]:FOR[AntennaRange]:NEEDS[!RemoteTech2] +{ + @MODULE[ModuleDataTransmitter] + { + @name = ModuleLimitedDataTransmitter + @packetResourceCost /= 1.414213 + nominalRange = 10000000000 + simpleRange = 56250000000 + maxPowerFactor = 16 + maxDataFactor = 2 + } + + MODULE + { + name = ModuleScienceContainer + + dataIsCollectable = true + dataIsStorable = false + + storageRange = 2 + } +} + +TRACKING_STATION_RANGES +{ + range = 800000 + range = 200000000000 + range = 2250000000000 +} + +EVA_MODULE +{ + name = ModuleLimitedDataTransmitter + + nominalRange = 1389 + maxPowerFactor = 1 + maxDataFactor = 1 + + packetInterval = 0.2 + packetSize = 1 + packetResourceCost = 6.25 + + requiredResource = ElectricCharge +} + +EVA_RESOURCE +{ + name = ElectricCharge + amount = 100 + maxAmount = 100 +} + +@EVA_RESOURCE[ElectricCharge]:AFTER[AntennaRange]:NEEDS[TacLifeSupport] +{ + !name = DELETE +} + --- 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,83 @@ namespace AntennaRange { - /* - * Interface defining the basic functionality of AntennaRelay modules for AntennaRange. - * */ + /// + /// Interface defining the basic functionality of AntennaRelay modules for AntennaRange. + /// public interface IAntennaRelay { /// /// Gets the parent Vessel. /// - /// The parent Vessel. Vessel vessel { get; } + + /// + /// Gets the target relay. + /// + IAntennaRelay targetRelay { get; } + + /// + /// Gets a value indicating whether this Relay is communicating + /// directly with Kerbin. + /// + bool KerbinDirect { get; } + + /// + /// The link distance, in meters, at which this relay behaves nominally. + /// + double NominalLinkDistance { get; } + + + /// + /// The link distance, in meters, beyond which this relay cannot operate. + /// + double MaximumLinkDistance { get; } /// /// Gets the distance to the nearest relay or Kerbin, whichever is closer. /// - /// The distance to the nearest relay or Kerbin, whichever is closer. double transmitDistance { get; } + + /// + /// Gets the link status. + /// + ConnectionStatus LinkStatus { get; } + + /// + /// Gets the nominal transmit distance at which the Antenna behaves just as prescribed by Squad's config. + /// + double nominalTransmitDistance { get; } /// /// The maximum distance at which this relay can operate. /// - /// The max transmit distance. - float maxTransmitDistance { get; } + double maxTransmitDistance { get; } /// - /// Gets a value indicating whether this has been checked during - /// the current relay attempt. + /// The first CelestialBody blocking line of sight to a /// - /// true if relay checked; otherwise, false. - bool relayChecked { get; } + CelestialBody firstOccludingBody { get; } + + /// + /// Gets the Part title. + /// + string Title { get; } /// /// Determines whether this instance can transmit. + /// true if this instance can transmit; otherwise, false. /// - /// true if this instance can transmit; otherwise, false. bool CanTransmit(); + + /// + /// Finds the nearest relay. + /// + void FindNearestRelay(); + + /// + /// Returns a that represents the current . + /// + 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,109 @@ using KSP; using System; using System.Collections.Generic; -using System.Linq; using System.Text; using ToadicusTools; using UnityEngine; namespace AntennaRange { - /* - * ModuleLimitedDataTransmitter is designed as a drop-in replacement for ModuleDataTransmitter, and handles range- - * finding, power scaling, and data scaling for antennas during science transmission. Its functionality varies with - * three tunables: nominalRange, maxPowerFactor, and maxDataFactor, set in .cfg files. - * - * In general, the scaling functions assume the following relation: - * - * D² α P/R, - * - * where D is the total transmission distance, P is the transmission power, and R is the data rate. - * - * */ - - /* - * Fields - * */ + /// + /// 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. + /// + /// In general, the scaling functions assume the following relation: + /// + /// D² α P/R, + /// + /// where D is the total transmission distance, P is the transmission power, and R is the data rate. + /// public class ModuleLimitedDataTransmitter : ModuleDataTransmitter, IScienceDataTransmitter, IAntennaRelay { - // 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; + private float _basepacketResourceCost; // Stores the packetSize as defined in the .cfg file. - protected float _basepacketSize; + private float _basepacketSize; // Every antenna is a relay. - protected AntennaRelay relay; - - // Keep track of vessels with transmitters for relay purposes. - protected List _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; + + /// + /// When additive ranges are enabled, the distance from Kerbin at which the antenna will perform exactly as + /// prescribed by packetResourceCost and packetSize. + /// [KSPField(isPersistant = false)] - public float nominalRange; - + public double nominalRange; + + /// + /// When additive ranges are disabled, the distance from Kerbin at which the antenna will perform exactly as + /// prescribed by packetResourceCost and packetSize. + /// + [KSPField(isPersistant = false)] + public double simpleRange; + + /// + /// Relay status string for use in action menus. + /// + [KSPField(isPersistant = false, guiActive = true, guiName = "Status")] + public string UIrelayStatus; + + /// + /// Relay target string for use in action menus. + /// [KSPField(isPersistant = false, guiActive = true, guiName = "Relay")] - public string UIrelayStatus; - + public string UIrelayTarget; + + /// + /// Transmit distance string for use in action menus. + /// [KSPField(isPersistant = false, guiActive = true, guiName = "Transmission Distance")] public string UItransmitDistance; - [KSPField(isPersistant = false, guiActive = true, guiName = "Maximum Distance")] + /// + /// The nominal range string for use in action menus. + /// + [KSPField(isPersistant = false, guiActive = true, guiName = "Nominal Range")] + public string UInominalLinkDistance; + + /// + /// Maximum distance string for use in action menus. + /// + [KSPField(isPersistant = false, guiActive = true, guiName = "Maximum Range")] public string UImaxTransmitDistance; + /// + /// Packet size string for use in action menus. + /// [KSPField(isPersistant = false, guiActive = true, guiName = "Packet Size")] public string UIpacketSize; + /// + /// Packet cost string for use in action menus. + /// [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. + /// + /// The multiplier on packetResourceCost that defines the maximum power output of the antenna. When the power + /// cost exceeds packetResourceCost * maxPowerFactor, transmission will fail. + /// [KSPField(isPersistant = false)] public float maxPowerFactor; - // The multipler on packetSize that defines the maximum data bandwidth of the antenna. + /// + /// The multipler on packetSize that defines the maximum data bandwidth of the antenna. + /// [KSPField(isPersistant = false)] public float maxDataFactor; + /// + /// The packet throttle. + /// [KSPField( isPersistant = true, guiName = "Packet Throttle", @@ -112,35 +142,174 @@ [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. + /// + /// Gets the parent Vessel. + /// public new Vessel vessel { get { - return base.vessel; - } - } - - // Returns the distance to the nearest relay or Kerbin, whichever is closer. + 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."); + return null; + } + } + } + + /// + /// Gets the target relay. + /// + public IAntennaRelay targetRelay + { + get + { + if (this.relay == null) + { + return null; + } + + return this.relay.targetRelay; + } + } + + /// + /// Gets a value indicating whether this Relay is communicating + /// directly with Kerbin. + /// + public bool KerbinDirect + { + get + { + if (this.relay != null) + { + return this.relay.KerbinDirect; + } + + return false; + } + } + + /// + /// Gets or sets the nominal link distance, in meters. + /// + public double NominalLinkDistance + { + get + { + if (this.relay != null) + { + return this.relay.NominalLinkDistance; + } + + return 0d; + } + } + + /// + /// Gets or sets the maximum link distance, in meters. + /// + public double MaximumLinkDistance + { + get + { + if (this.relay != null) + { + return this.relay.MaximumLinkDistance; + } + + return 0d; + } + } + + /// + /// Gets the distance to the nearest relay or Kerbin, whichever is closer. + /// public double transmitDistance { get { + if (this.relay == null) + { + return double.PositiveInfinity; + } + return this.relay.transmitDistance; } } - // Returns the maximum distance this module can transmit - public float maxTransmitDistance - { - get - { - return Mathf.Sqrt (this.maxPowerFactor) * this.nominalRange; + /// + /// Gets the link status. + /// + public ConnectionStatus LinkStatus + { + get + { + if (this.relay == null) + { + return ConnectionStatus.None; + } + + return this.relay.LinkStatus; + } + } + + /// + /// Gets the nominal transmit distance at which the Antenna behaves just as prescribed by Squad's config. + /// + public double nominalTransmitDistance + { + get + { + if (ARConfiguration.UseAdditiveRanges) + { + return this.nominalRange; + } + else + { + return this.simpleRange; + } + } + } + + /// + /// The maximum distance at which this relay can operate. + /// + public double maxTransmitDistance + { + get; + protected set; + } + + /// + /// The first CelestialBody blocking line of sight to a + /// + public CelestialBody firstOccludingBody + { + get + { + return this.relay.firstOccludingBody; } } @@ -170,8 +339,10 @@ * * 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 + /// + /// Override ModuleDataTransmitter.DataRate to just return packetSize, because we want antennas to be scored in + /// terms of joules/byte + /// public new float DataRate { get @@ -189,9 +360,11 @@ } } - // Override ModuleDataTransmitter.DataResourceCost to just return packetResourceCost, because we want antennas - // to be scored in terms of joules/byte - public new float DataResourceCost + /// + /// Override ModuleDataTransmitter.DataResourceCost to just return packetResourceCost, because we want antennas + /// to be scored in terms of joules/byte + /// + public new double DataResourceCost { get { @@ -208,12 +381,19 @@ } } - // Reports whether this antenna has been checked as a viable relay already in the current FindNearestRelay. - public bool relayChecked - { - get - { - return this.relay.relayChecked; + /// + /// Gets the Part title. + /// + public string Title + { + get + { + if (this.part != null && this.part.partInfo != null) + { + return this.part.partInfo.title; + } + + return string.Empty; } } @@ -227,32 +407,12 @@ this.packetThrottle = 100f; } - // At least once, when the module starts with a state on the launch pad or later, go find Kerbin. - public override void OnStart (StartState state) - { - base.OnStart (state); - - if (state >= StartState.PreLaunch) - { - this.relay = new AntennaRelay(this); - this.relay.maxTransmitDistance = this.maxTransmitDistance; - - this.UImaxTransmitDistance = Tools.MuMech_ToSI(this.maxTransmitDistance) + "m"; - - GameEvents.onPartActionUICreate.Add(this.onPartActionUICreate); - GameEvents.onPartActionUIDismiss.Add(this.onPartActionUIDismiss); - } - } - - // When the module loads, fetch the Squad KSPFields from the base. This is necessary in part because - // overloading packetSize and packetResourceCostinto a property in ModuleLimitedDataTransmitter didn't - // work. - public override void OnLoad(ConfigNode node) - { - this.Fields.Load(node); - base.Fields.Load(node); - - base.OnLoad (node); + /// + /// PartModule OnAwake override; runs at Unity Awake. + /// + public override void OnAwake() + { + base.OnAwake(); this._basepacketSize = base.packetSize; this._basepacketResourceCost = base.packetResourceCost; @@ -261,21 +421,388 @@ "{0} loaded:\n" + "packetSize: {1}\n" + "packetResourceCost: {2}\n" + - "nominalRange: {3}\n" + + "nominalTransmitDistance: {3}\n" + "maxPowerFactor: {4}\n" + "maxDataFactor: {5}\n", this.name, base.packetSize, this._basepacketResourceCost, - this.nominalRange, + this.nominalTransmitDistance, this.maxPowerFactor, this.maxDataFactor )); } + /// + /// PartModule OnStart override; runs at Unity Start. + /// + /// State. + public override void OnStart (StartState state) + { + base.OnStart (state); + + if (state >= StartState.PreLaunch) + { + this.maxTransmitDistance = Math.Sqrt(this.maxPowerFactor) * this.nominalTransmitDistance; + + this.relay = new AntennaRelay(this); + this.relay.nominalTransmitDistance = this.nominalTransmitDistance; + this.relay.maxTransmitDistance = this.maxTransmitDistance; + + this.UImaxTransmitDistance = string.Format(Tools.SIFormatter, "{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. + /// + /// with data for this module. + public override void OnLoad(ConfigNode node) + { + this.Fields.Load(node); + base.Fields.Load(node); + + base.OnLoad (node); + + this.maxTransmitDistance = Math.Sqrt(this.maxPowerFactor) * this.nominalTransmitDistance; + } + + /// + /// Override ModuleDataTransmitter.GetInfo to add nominal and maximum range to the VAB description. + /// + public override string GetInfo() + { + StringBuilder sb = Tools.GetStringBuilder(); + string text; + + sb.Append(base.GetInfo()); + sb.AppendFormat(Tools.SIFormatter, "Nominal Range: {0:S3}m\n", this.nominalTransmitDistance); + sb.AppendFormat(Tools.SIFormatter, "Maximum Range: {0:S3}m\n", this.maxTransmitDistance); + + text = sb.ToString(); + + Tools.PutStringBuilder(sb); + + return text; + } + + /// + /// Determines whether this instance can transmit. + /// true if this instance can transmit; otherwise, false. + /// + public new bool CanTransmit() + { + if (this.part == null || this.relay == null) + { + return false; + } + + switch (this.part.State) + { + case PartStates.DEAD: + case 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), this.part.State) + )); + return false; + default: + break; + } + + return this.relay.CanTransmit(); + } + + /// + /// Finds the nearest relay. + /// + public void FindNearestRelay() + { + if (this.relay != null) + { + this.relay.FindNearestRelay(); + } + } + + /// + /// Override ModuleDataTransmitter.TransmitData to check against CanTransmit and fail out when CanTransmit + /// returns false. + /// + /// List of to transmit. + /// Callback function + public new void TransmitData(List dataQueue, Callback callback) + { + this.LogDebug( + "TransmitData(List dataQueue, Callback callback) called. dataQueue.Count={0}", + dataQueue.Count + ); + + this.FindNearestRelay(); + + this.PreTransmit_SetPacketSize(); + this.PreTransmit_SetPacketResourceCost(); + + 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(), + callback == null ? "null" : callback.ToString() + ); + + if (callback == null) + { + base.TransmitData(dataQueue); + } + else + { + base.TransmitData(dataQueue, callback); + } + } + else + { + Tools.PostDebugMessage(this, "{0} unable to transmit during TransmitData.", this.part.partInfo.title); + + var logger = Tools.DebugLogger.New(this); + + IList vesselContainers = this.vessel.getModulesOfType(); + 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 dataStored = new List(); + + 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) + { + StringBuilder sb = Tools.GetStringBuilder(); + + 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); + + Tools.PostDebugMessage(sb.ToString()); + + Tools.PutStringBuilder(sb); + } + + this.PostCannotTransmitError(); + } + + Tools.PostDebugMessage ( + "distance: " + this.transmitDistance + + " packetSize: " + this.packetSize + + " packetResourceCost: " + this.packetResourceCost + ); + } + + /// + /// Override ModuleDataTransmitter.TransmitData to check against CanTransmit and fail out when CanTransmit + /// returns false. + /// + /// List of to transmit. + public new void TransmitData(List dataQueue) + { + this.LogDebug( + "TransmitData(List dataQueue) called, dataQueue.Count={0}", + dataQueue.Count + ); + + this.TransmitData(dataQueue, null); + } + + /// + /// Override ModuleDataTransmitter.StartTransmission to check against CanTransmit and fail out when CanTransmit + /// returns false. + /// + public new void StartTransmission() + { + this.FindNearestRelay(); + + PreTransmit_SetPacketSize (); + PreTransmit_SetPacketResourceCost (); + + Tools.PostDebugMessage ( + "distance: " + this.transmitDistance + + " packetSize: " + this.packetSize + + " packetResourceCost: " + this.packetResourceCost + ); + + if (this.CanTransmit()) + { + ScreenMessages.PostScreenMessage(this.buildTransmitMessage(), 4f, ScreenMessageStyle.UPPER_LEFT); + + base.StartTransmission(); + } + else + { + this.PostCannotTransmitError (); + } + } + + /// + /// MonoBehaviour Update + /// + public void Update() + { + if (this.actionUIUpdate) + { + this.UImaxTransmitDistance = string.Format(Tools.SIFormatter, "{0:S3}m", this.MaximumLinkDistance); + this.UInominalLinkDistance = string.Format(Tools.SIFormatter, "{0:S3}m", this.NominalLinkDistance); + + if (this.CanTransmit()) + { + this.UIrelayStatus = this.LinkStatus.ToString(); + this.UItransmitDistance = string.Format(Tools.SIFormatter, "{0:S3}m", this.transmitDistance); + this.UIpacketSize = string.Format(Tools.SIFormatter, "{0:S3}MiT", this.DataRate); + this.UIpacketCost = string.Format(Tools.SIFormatter, "{0:S3}EC", this.DataResourceCost); + } + else + { + if (this.relay.firstOccludingBody == null) + { + this.UItransmitDistance = string.Format(Tools.SIFormatter, "{0:S3}m", this.transmitDistance); + this.UIrelayStatus = "Out of range"; + } + else + { + this.UItransmitDistance = "N/A"; + this.UIrelayStatus = string.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 + { + this.UIrelayTarget = this.targetRelay.ToString(); + } + } + } + + /// + /// Returns a that represents the current . + /// + /// A that represents the current . + public override string ToString() + { + StringBuilder sb = Tools.GetStringBuilder(); + string msg; + + sb.Append(this.part.partInfo.title); + + 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(); + + Tools.PutStringBuilder(sb); + + 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!"); @@ -296,16 +823,19 @@ // 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) + private void PreTransmit_SetPacketResourceCost() + { + if (ARConfiguration.FixedPowerCost || this.transmitDistance <= this.NominalLinkDistance) { base.packetResourceCost = this._basepacketResourceCost; } else { + float rangeFactor = (float)(this.transmitDistance / this.NominalLinkDistance); + rangeFactor *= rangeFactor; + base.packetResourceCost = this._basepacketResourceCost - * (float)Math.Pow (this.transmitDistance / this.nominalRange, 2); + * rangeFactor; } base.packetResourceCost *= this.packetThrottle / 100f; @@ -313,250 +843,83 @@ // 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) + private void PreTransmit_SetPacketSize() + { + if (!ARConfiguration.FixedPowerCost && this.transmitDistance >= this.NominalLinkDistance) { base.packetSize = this._basepacketSize; } else { - base.packetSize = Math.Min( - this._basepacketSize * (float)Math.Pow (this.nominalRange / this.transmitDistance, 2), + float rangeFactor = (float)(this.NominalLinkDistance / this.transmitDistance); + rangeFactor *= rangeFactor; + + base.packetSize = Mathf.Min( + this._basepacketSize * rangeFactor, 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 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."); - } - else - { - message.Append("via "); - message.Append(this.relay.nearestRelay); - } - - ScreenMessages.PostScreenMessage(message.ToString(), 4f, ScreenMessageStyle.UPPER_LEFT); - - base.TransmitData(dataQueue); + private string buildTransmitMessage() + { + StringBuilder sb = Tools.GetStringBuilder(); + 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 { - 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() + sb.Append("via "); + sb.Append(this.relay.targetRelay); + } + + msg = sb.ToString(); + + Tools.PutStringBuilder(sb); + + return msg; + } + + #if DEBUG + // When debugging, it's nice to have a button that just tells you everything. + [KSPEvent (guiName = "Show Debug Info", active = true, guiActive = true)] + public void DebugInfo() { PreTransmit_SetPacketSize (); PreTransmit_SetPacketResourceCost (); - 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(); - } - - // 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); + DebugPartModule.DumpClassObject(this); } [KSPEvent (guiName = "Dump Vessels", active = true, guiActive = true)] public void PrintAllVessels() { - StringBuilder sb = new StringBuilder(); - + StringBuilder sb = Tools.GetStringBuilder(); + sb.Append("Dumping FlightGlobals.Vessels:"); - foreach (Vessel vessel in 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); } - + Tools.PostDebugMessage(sb.ToString()); - } - + + Tools.PutStringBuilder(sb); + } + [KSPEvent (guiName = "Dump RelayDB", active = true, guiActive = true)] public void DumpRelayDB() { --- 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.3.*")] +[assembly: AssemblyVersion("1.9.*")] // 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,65 @@ using KSP; using System; -using System.Linq; using ToadicusTools; namespace AntennaRange { - /* - * Wrapper class for ProtoPartModuleSnapshot extending AntennaRelay and implementing IAntennaRelay. - * This is used for finding relays in unloaded Vessels. - * */ + /// + /// Wrapper class for ProtoPartModuleSnapshot extending AntennaRelay and implementing IAntennaRelay. + /// This is used for finding relays in unloaded Vessels. + /// public class ProtoAntennaRelay : AntennaRelay, IAntennaRelay { // Stores the prototype part so we can make sure we haven't exploded or so. - protected ProtoPartSnapshot protoPart; + private ProtoPartSnapshot protoPart; + /// + /// Gets the parent Vessel. + /// public override Vessel vessel { get { - return this.protoPart.pVesselRef.vesselRef; + if ( + this.protoPart != null && + this.protoPart.pVesselRef != null && + this.protoPart.pVesselRef.vesselRef != null + ) + { + return this.protoPart.pVesselRef.vesselRef; + } + else + { + Tools.PostErrorMessage("{0}: Could not fetch vessel! {1}{2}{3}", + this.ToString(), + 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; + } } } /// - /// The maximum distance at which this transmitter can operate. + /// Gets the nominal transmit distance at which the Antenna behaves just as prescribed by Squad's config. /// - /// The max transmit distance. - public override float maxTransmitDistance + public override double nominalTransmitDistance + { + get + { + return this.moduleRef.nominalTransmitDistance; + } + } + + /// + /// The maximum distance at which this relay can operate. + /// + public override double maxTransmitDistance { get { @@ -63,28 +95,26 @@ } /// - /// Gets a value indicating whether this has been checked during - /// the current relay attempt. + /// Gets the underlying part's title. /// - /// true if relay checked; otherwise, false. - public override bool relayChecked + /// The title. + public string Title { - get; - protected set; + get + { + if (this.protoPart != null && this.protoPart.partInfo != null) + { + return this.protoPart.partInfo.title; + } + + return string.Empty; + } } /// - /// Gets the underlying part's title. + /// Determines whether this instance can transmit. + /// true if this instance can transmit; otherwise, false. /// - /// The title. - public string title - { - get - { - return this.protoPart.partInfo.title; - } - } - public override bool CanTransmit() { PartStates partState = (PartStates)this.protoPart.state; @@ -93,7 +123,7 @@ Tools.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 +132,37 @@ return base.CanTransmit(); } + /// + /// Returns a that represents the current . + /// + /// A that represents the current . public override string ToString() { - return string.Format( - "{0} on {1} (proto)", - this.title, - this.protoPart.pVesselRef.vesselName - ); + System.Text.StringBuilder sb = Tools.GetStringBuilder(); + + sb.Append(this.Title); + + if (this.protoPart != null && this.protoPart.pVesselRef != null) + { + sb.AppendFormat(" on {0}", this.protoPart.pVesselRef.vesselName); + } + + Tools.PutStringBuilder(sb); + + return sb.ToString(); } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - /// The ProtoPartModuleSnapshot to wrap - /// The parent Vessel + /// The module reference underlying this AntennaRelay, + /// as an + /// The prototype partreference on which the module resides. public ProtoAntennaRelay(IAntennaRelay prefabRelay, ProtoPartSnapshot pps) : base(prefabRelay) { this.protoPart = pps; - } - ~ProtoAntennaRelay() - { - Tools.PostDebugMessage(string.Format( - "{0}: destroyed", - this.ToString() - )); + Tools.PostLogMessage("{0}: constructed {1}", this.GetType().Name, this.ToString()); } } } --- 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 { - /* - * 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> relayDatabase; + private Dictionary> relayDatabase; + private Dictionary bestRelayTable; // Vessel.id-keyed hash table of part counts, used for caching - protected Dictionary vesselPartCountTable; - - // Vessel.id-keyed hash table of booleans to track what vessels have been checked so far this time. - public Dictionary CheckedVesselsTable; - - protected int cacheHits; - protected int cacheMisses; + private Dictionary vesselPartCountTable; + + private int cacheHits; + private int cacheMisses; /* * Properties * */ // Gets the Part-hashed table of relays in a given vessel - public Dictionary this [Vessel vessel] + public IList 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 + Tools.PostDebugMessage("RelayDatabase: Dirtying cache for vessel {0} in frame {1}", + vessel, new System.Diagnostics.StackTrace().ToString()); + #else + Tools.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() + { + Tools.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(); + this.relayDatabase[vessel.id] = new List(); // 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,37 +188,12 @@ )); } - Dictionary vesselTable = this.relayDatabase[vessel.id]; + List vesselTable = this.relayDatabase[vessel.id]; // Actually build and assign the table this.getVesselRelays(vessel, ref vesselTable); // Set the part count this.vesselPartCountTable[vessel.id] = vessel.Parts.Count; - } - - // Remove a vessel from the cache, if it exists. - public void DirtyVessel(Vessel vessel) - { - if (this.relayDatabase.ContainsKey(vessel.id)) - { - this.relayDatabase.Remove(vessel.id); - } - if (this.vesselPartCountTable.ContainsKey(vessel.id)) - { - this.vesselPartCountTable.Remove(vessel.id); - } - } - - // Returns true if both the relayDatabase and the vesselPartCountDB contain the vessel id. - public bool ContainsKey(Guid key) - { - return this.relayDatabase.ContainsKey(key); - } - - // Returns true if both the relayDatabase and the vesselPartCountDB contain the vessel. - public bool ContainsKey(Vessel vessel) - { - return this.ContainsKey(vessel.id); } // Runs when a vessel is modified (or when we switch to one, to catch docking events) @@ -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) + { + Tools.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) + { + Tools.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,14 +263,14 @@ } // Runs when parts are coupled, as in docking - public void onFromPartToPartEvent(GameEvents.FromToAction data) + private void onFromPartToPartEvent(GameEvents.FromToAction 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 relays) + private void getVesselRelays(Vessel vessel, ref List relays) { // We're going to completely regen this table, so dump the current contents. relays.Clear(); @@ -251,6 +280,10 @@ "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) { @@ -261,16 +294,30 @@ )); // 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; } @@ -287,8 +334,11 @@ )); // Loop through the ProtoPartModuleSnapshots in the Vessel... - foreach (ProtoPartSnapshot pps in vessel.protoVessel.protoPartSnapshots) - { + ProtoPartSnapshot pps; + for (int ppsIdx = 0; ppsIdx < vessel.protoVessel.protoPartSnapshots.Count; ppsIdx++) + { + pps = vessel.protoVessel.protoPartSnapshots[ppsIdx]; + Tools.PostDebugMessage(string.Format( "{0}: Searching in protopartsnapshot {1}", this.GetType().Name, @@ -306,8 +356,11 @@ )); // ...loop through the PartModules in the prefab... - foreach (PartModule module in partPrefab.Modules) + PartModule module; + for (int modIdx = 0; modIdx < partPrefab.Modules.Count; modIdx++) { + module = partPrefab.Modules[modIdx]; + Tools.PostDebugMessage(string.Format( "{0}: Searching in partmodule {1}", this.GetType().Name, @@ -323,14 +376,24 @@ 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; } } } } + + this.bestRelayTable[vessel.id] = bestRelay; Tools.PostDebugMessage(string.Format( "{0}: vessel '{1}' ({2}) has {3} transmitters.", @@ -342,12 +405,12 @@ } // Construct the singleton - protected RelayDatabase() + private RelayDatabase() { // Initialize the databases - this.relayDatabase = new Dictionary>(); + this.relayDatabase = new Dictionary>(); + this.bestRelayTable = new Dictionary(); this.vesselPartCountTable = new Dictionary(); - this.CheckedVesselsTable = new Dictionary(); 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,6 +434,7 @@ GameEvents.onGameSceneLoadRequested.Remove(this.onSceneChange); GameEvents.onPartCouple.Remove(this.onFromPartToPartEvent); GameEvents.onPartUndock.Remove(this.onPartEvent); + GameEvents.onGameStateLoad.Remove(this.onGameLoaded); Tools.PostDebugMessage(this.GetType().Name + " destroyed."); @@ -385,21 +450,28 @@ #if DEBUG public void Dump() { - StringBuilder sb = new StringBuilder(); + StringBuilder sb = Tools.GetStringBuilder(); sb.Append("Dumping RelayDatabase:"); - foreach (Guid id in this.relayDatabase.Keys) - { - sb.AppendFormat("\nVessel {0}:", id); - - foreach (IAntennaRelay relay in this.relayDatabase[id].Values) - { + var dbEnum = this.relayDatabase.GetEnumerator(); + IList 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()); } } Tools.PostDebugMessage(sb.ToString()); + + Tools.PutStringBuilder(sb); } #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,17 +28,60 @@ using System; using System.Collections.Generic; -using System.Linq; using ToadicusTools; namespace AntennaRange { - /* - * A class of utility extensions for Vessels and Relays to help find a relay path back to Kerbin. - * */ + /// + /// A class of utility extensions for Vessels and Relays to help find a relay path back to Kerbin. + /// public static class RelayExtensions { /// + /// Returns the distance between two IAntennaRelays. + /// + /// Relay one. + /// Relay two. + public static double DistanceTo(this IAntennaRelay relayOne, IAntennaRelay relayTwo) + { + return relayOne.vessel.DistanceTo(relayTwo.vessel); + } + + /// + /// Returns the distance from this IAntennaRelay to the given CelestialBody + /// + /// Relay. + /// Body. + public static double SqrDistanceTo(this IAntennaRelay relay, CelestialBody body) + { + double range = relay.vessel.DistanceTo(body) - body.Radius; + + return range * range; + } + + /// + /// Returns the distance between two IAntennaRelays. + /// + /// Relay one. + /// Relay two. + public static double SqrDistanceTo(this IAntennaRelay relayOne, IAntennaRelay relayTwo) + { + return relayOne.vessel.sqrDistanceTo(relayTwo.vessel); + } + + /// + /// Returns the distance from this IAntennaRelay to the given CelestialBody + /// + /// Relay. + /// Body. + public static double DistanceTo(this IAntennaRelay relay, CelestialBody body) + { + double range = relay.vessel.DistanceTo(body) - body.Radius; + + return range; + } + + /// /// Returns the distance between this IAntennaRelay and a Vessel /// /// This @@ -55,7 +98,7 @@ /// A public static double DistanceTo(this AntennaRelay relay, CelestialBody body) { - return relay.vessel.DistanceTo(body); + return relay.vessel.DistanceTo(body) - body.Radius; } /// @@ -69,12 +112,42 @@ } /// + /// Returns the square of the distance between this IAntennaRelay and a Vessel + /// + /// This + /// A + public static double sqrDistanceTo(this AntennaRelay relay, Vessel vessel) + { + return relay.vessel.sqrDistanceTo(vessel); + } + + /// + /// Returns the square of the distance between this IAntennaRelay and a CelestialBody + /// + /// This + /// A + public static double sqrDistanceTo(this AntennaRelay relay, CelestialBody body) + { + return relay.vessel.sqrDistanceTo(body); + } + + /// + /// Returns the square of the distance between this IAntennaRelay and another IAntennaRelay + /// + /// This + /// Another + public static double sqrDistanceTo(this AntennaRelay relayOne, IAntennaRelay relayTwo) + { + return relayOne.vessel.sqrDistanceTo(relayTwo.vessel); + } + + /// /// Returns all of the PartModules or ProtoPartModuleSnapshots implementing IAntennaRelay in this Vessel. /// /// This - public static IEnumerable GetAntennaRelays (this Vessel vessel) - { - return RelayDatabase.Instance[vessel].Values.ToList(); + public static IList GetAntennaRelays (this Vessel vessel) + { + return RelayDatabase.Instance[vessel]; } /// @@ -84,8 +157,11 @@ /// public static bool HasConnectedRelay(this Vessel vessel) { - foreach (IAntennaRelay relay in RelayDatabase.Instance[vessel].Values) - { + IList vesselRelays = RelayDatabase.Instance[vessel]; + IAntennaRelay relay; + for (int rIdx = 0; rIdx < vesselRelays.Count; rIdx++) + { + relay = vesselRelays[rIdx]; if (relay.CanTransmit()) { return true; @@ -94,6 +170,60 @@ return false; } + + /// + /// Gets the for this + /// + /// This + public static ConnectionStatus GetConnectionStatus(this Vessel vessel) + { + bool canTransmit = false; + + IList 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; + } + } + + /// + /// Gets the best relay on this Vessel. The best relay may not be able to transmit. + /// + /// This + public static IAntennaRelay GetBestRelay(this Vessel vessel) + { + return RelayDatabase.Instance.GetBestVesselRelay(vessel); + } + } + + #pragma warning disable 1591 + /// + /// An Enum describing the connection status of a vessel or relay. + /// + public enum ConnectionStatus + { + None, + Suboptimal, + Optimal } }