diff --git a/.gitignore b/.gitignore
index 68d765860..f80f3b36d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -167,3 +167,6 @@ GameData/RemoteTech/Plugins/RemoteTech.xml
GameData/RemoteTech/Versioning/RemoteTech.version
GameData/build.txt
src/RemoteTech/RemoteTech.sln.ide/
+
+# In-game test plugin build output (the source lives under tests/)
+GameData/RemoteTech.Tests/
diff --git a/RemoteTech.sln b/RemoteTech.sln
new file mode 100644
index 000000000..119211d13
--- /dev/null
+++ b/RemoteTech.sln
@@ -0,0 +1,40 @@
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.31903.59
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RemoteTech", "src\RemoteTech\RemoteTech.csproj", "{67470554-F754-4664-AC7C-D002943ACD72}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RemoteTech.Tests", "tests\RemoteTech.Tests\RemoteTech.Tests.csproj", "{A7585803-6434-436E-BFC1-B09A520425D7}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RemoteTech.InGameTests", "tests\RemoteTech.InGameTests\RemoteTech.InGameTests.csproj", "{C5C1CBBB-83DD-4850-A6F8-6823AB14B54E}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{065AC022-06A2-4079-9DEC-051B87D76A08}"
+ ProjectSection(SolutionItems) = preProject
+ CHANGES.md = CHANGES.md
+ CONTRIBUTING.md = CONTRIBUTING.md
+ README.md = README.md
+ EndProjectSection
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {67470554-F754-4664-AC7C-D002943ACD72}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {67470554-F754-4664-AC7C-D002943ACD72}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {67470554-F754-4664-AC7C-D002943ACD72}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {67470554-F754-4664-AC7C-D002943ACD72}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A7585803-6434-436E-BFC1-B09A520425D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A7585803-6434-436E-BFC1-B09A520425D7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A7585803-6434-436E-BFC1-B09A520425D7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A7585803-6434-436E-BFC1-B09A520425D7}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C5C1CBBB-83DD-4850-A6F8-6823AB14B54E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C5C1CBBB-83DD-4850-A6F8-6823AB14B54E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C5C1CBBB-83DD-4850-A6F8-6823AB14B54E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C5C1CBBB-83DD-4850-A6F8-6823AB14B54E}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+EndGlobal
diff --git a/RemoteTech.slnx b/RemoteTech.slnx
deleted file mode 100644
index 54c1e38a7..000000000
--- a/RemoteTech.slnx
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/RemoteTech/API/API.cs b/src/RemoteTech/API/API.cs
index 909a7122e..f74021b8f 100644
--- a/src/RemoteTech/API/API.cs
+++ b/src/RemoteTech/API/API.cs
@@ -1,5 +1,5 @@
using RemoteTech.RangeModel;
-using RemoteTech.Modules;
+using RemoteTech.Modules;
using RemoteTech.SimpleTypes;
using System;
using System.Collections.Generic;
@@ -10,7 +10,9 @@ namespace RemoteTech.API
{
public static class API
{
- /// If true then RTCore will be available in the Space Center
+ ///
+ /// If true then RTCore will be available in the Space Center
+ ///
internal static bool enabledInSPC = false;
public static bool IsRemoteTechEnabled()
@@ -43,7 +45,7 @@ public static bool HasLocalControl(Guid id)
public static bool HasFlightComputer(Guid id)
{
if (RTCore.Instance == null) return false;
- var satellite = RTCore.Instance.Satellites.Where(sat => sat.Guid.Equals(id)).FirstOrDefault();
+ var satellite = RTCore.Instance.Satellites[id];
if (satellite == null) return false;
var hasFlightComputer = satellite.FlightComputer != null;
@@ -55,7 +57,7 @@ public static bool HasFlightComputer(Guid id)
public static void AddSanctionedPilot(Guid id, Action autopilot)
{
if (RTCore.Instance == null) return;
- var satellite = RTCore.Instance.Satellites.Where(sat => sat.Guid.Equals(id)).FirstOrDefault();
+ var satellite = RTCore.Instance.Satellites[id];
if (satellite == null || satellite.SignalProcessor == null) return;
foreach (var spu in satellite.SignalProcessors)
@@ -70,7 +72,7 @@ public static void AddSanctionedPilot(Guid id, Action autopilot
public static void RemoveSanctionedPilot(Guid id, Action autopilot)
{
if (RTCore.Instance == null) return;
- var satellite = RTCore.Instance.Satellites.Where(sat => sat.Guid.Equals(id)).FirstOrDefault();
+ var satellite = RTCore.Instance.Satellites[id];
if (satellite == null || satellite.SignalProcessor == null) return;
foreach (var spu in satellite.SignalProcessors)
@@ -84,10 +86,10 @@ public static void RemoveSanctionedPilot(Guid id, Action autopi
public static bool HasAnyConnection(Guid id)
{
if (RTCore.Instance == null) return false;
- var satellite = RTCore.Instance.Satellites.Where(sat => sat.Guid.Equals(id)).FirstOrDefault();
+ var satellite = RTCore.Instance.Satellites[id];
if (satellite == null) return false;
- var hasConnection = RTCore.Instance.Network[satellite].Any();
+ var hasConnection = RTCore.Instance.Network.IsConnected(satellite);
RTLog.Verbose("Flight: {0} Has Connection: {1}", RTLogLevel.API, id, hasConnection);
return hasConnection;
}
@@ -95,54 +97,77 @@ public static bool HasAnyConnection(Guid id)
public static bool HasConnectionToKSC(Guid id)
{
if (RTCore.Instance == null) return false;
- var satellite = RTCore.Instance.Satellites.Where(sat => sat.Guid.Equals(id)).FirstOrDefault();
+ var satellite = RTCore.Instance.Satellites[id];
if (satellite == null) return false;
- var connectedToKerbin = RTCore.Instance.Network[satellite].Any(r => RTCore.Instance.Network.GroundStations.ContainsKey(r.Goal.Guid));
+ var connectedToKerbin = RTCore.Instance.Network.IsConnected(satellite, groundOnly: true);
RTLog.Verbose("Flight: {0} Has Connection to Kerbin: {1}", RTLogLevel.API, id, connectedToKerbin);
return connectedToKerbin;
}
- /// Determines if a satellite directly targets a ground station.
+ ///
+ /// Determines if a satellite directly targets a ground station.
+ ///
/// The satellite id.
/// true if the satellite has an antenna with a ground station as its first link, false otherwise.
public static bool HasDirectGroundStation(Guid id)
{
if (RTCore.Instance == null) return false;
- var satellite = RTCore.Instance.Satellites.Where(sat => sat.Guid.Equals(id)).FirstOrDefault();
+ var satellite = RTCore.Instance.Satellites[id];
if (satellite == null) return false;
- var targetsGroundStation = RTCore.Instance.Network[satellite].Any(r => RTCore.Instance.Network.GroundStations.ContainsKey(r.Links.FirstOrDefault().Target.Guid));
+ var targetsGroundStation = RTCore.Instance.Network.GetLinks(satellite)
+ .Any(link => RTCore.Instance.Network.GroundStations.ContainsKey(link.Target.Guid));
RTLog.Verbose("Flight: {0} Directly targets a ground station: {1}", RTLogLevel.API, id, targetsGroundStation);
return targetsGroundStation;
}
- /// Gets the name of the ground station directly targeted with the shortest link to the satellite.
+ ///
+ /// Gets the name of the ground station directly targeted with the shortest link to the satellite.
+ ///
/// The satellite id.
/// name of the ground station if one is found, null otherwise.
public static string GetClosestDirectGroundStation(Guid id)
{
if (RTCore.Instance == null) return null;
- var satellite = RTCore.Instance.Satellites.Where(sat => sat.Guid.Equals(id)).FirstOrDefault();
+ var satellite = RTCore.Instance.Satellites[id];
if (satellite == null) return null;
- var namedGroundStation = RTCore.Instance.Network[satellite].Where
- (r => RTCore.Instance.Network.GroundStations.ContainsKey(r.Links.FirstOrDefault().Target.Guid)).Min().Goal.Name;
+ ISatellite closest = null;
+ var bestDistance = double.PositiveInfinity;
+ foreach (var link in RTCore.Instance.Network.GetLinks(satellite))
+ {
+ if (!RTCore.Instance.Network.GroundStations.ContainsKey(link.Target.Guid)) continue;
+
+ var distance = RangeModelExtensions.DistanceTo(satellite, link.Target);
+ if (distance < bestDistance)
+ {
+ bestDistance = distance;
+ closest = link.Target;
+ }
+ }
+ if (closest == null) return null;
+
+ var namedGroundStation = closest.Name;
RTLog.Verbose("Flight: {0} Directly targets the closest ground station: {1}", RTLogLevel.API, id, namedGroundStation);
return namedGroundStation;
}
- /// Gets the name of the first hop satellite with the shortest link to KSC by the specified satellite.
+ ///
+ /// Gets the name of the first hop satellite with the shortest link to KSC by the specified satellite.
+ ///
/// The satellite id.
/// name of the satellite if one is found, null otherwise.
public static string GetFirstHopToKSC(Guid id)
{
if (RTCore.Instance == null) return null;
- var satellite = RTCore.Instance.Satellites.Where(sat => sat.Guid.Equals(id)).FirstOrDefault();
+ var satellite = RTCore.Instance.Satellites[id];
if (satellite == null) return null;
- var namedSatellite = RTCore.Instance.Network[satellite].Where
- (r => RTCore.Instance.Network.GroundStations.ContainsKey(r.Goal.Guid)).Min().Links.FirstOrDefault().Target.Name;
+ var route = RTCore.Instance.Network.GetRoute(satellite, groundOnly: true);
+ if (route == null || route.Count == 0) return null;
+
+ var namedSatellite = route[0].Target.Name;
RTLog.Verbose("Flight: {0} Has first hop satellite with shortest link to KSC: {1}", RTLogLevel.API, id, namedSatellite);
return namedSatellite;
}
@@ -166,25 +191,27 @@ public static Guid GetAntennaTarget(Part part) {
return module.Target;
}
- /// Gets Guids of all satellites in the control route
+ ///
+ /// Gets Guids of all satellites in the control route
+ ///
/// The satellite id
/// Guid array of all satellite in ground station router
public static Guid[] GetControlPath(Guid id)
{
if (RTCore.Instance == null) return new Guid[]{ };
- var satellite = RTCore.Instance.Satellites.Where(sat => sat.Guid.Equals(id)).FirstOrDefault();
+ var satellite = RTCore.Instance.Satellites[id];
if (satellite == null) return new Guid[] { };
- if (!RTCore.Instance.Network[satellite].Any(r => RTCore.Instance.Network.GroundStations.ContainsKey(r.Goal.Guid))) return new Guid[] { };
+ var route = RTCore.Instance.Network.GetRoute(satellite, groundOnly: true);
+ if (route == null) return new Guid[] { };
- List> bestRouter = RTCore.Instance.Network[satellite].Where(r => RTCore.Instance.Network.GroundStations.ContainsKey(r.Goal.Guid)).Min().Links;
- Guid[] guids = new Guid[bestRouter.Count];
+ Guid[] guids = new Guid[route.Count];
// Get all satellites till the ground station
- for (int i = 0; i < bestRouter.Count; i++)
+ for (int i = 0; i < route.Count; i++)
{
- guids[i] = bestRouter[i].Target.Guid;
+ guids[i] = route[i].Target.Guid;
}
return guids;
@@ -218,13 +245,15 @@ public static Guid GetGroundStationGuid(String name)
return groundStation.mGuid;
}
- /// Gets the name of a satellite.
+ ///
+ /// Gets the name of a satellite.
+ ///
/// The satellite id.
/// name of the satellite with matching id if found, otherwise null
public static string GetName(Guid id)
{
if (RTCore.Instance == null) return null;
- var satellite = RTCore.Instance.Network.Where(sat => sat.Guid.Equals(id)).FirstOrDefault();
+ var satellite = RTCore.Instance.Network[id];
if (satellite == null) return null;
string satellitename = satellite.Name;
@@ -238,22 +267,21 @@ public static Guid GetCelestialBodyGuid(CelestialBody celestialBody)
}
public static Guid GetNoTargetGuid() {
- return new Guid(RTSettings.Instance.NoTargetGuid);
+ return RTSettings.Instance.NoTargetGuidParsed;
}
public static Guid GetActiveVesselGuid() {
- return new Guid(RTSettings.Instance.ActiveVesselGuid);
+ return RTSettings.Instance.ActiveVesselGuidParsed;
}
public static double GetShortestSignalDelay(Guid id)
{
if (RTCore.Instance == null) return double.PositiveInfinity;
- var satellite = RTCore.Instance.Satellites.Where(sat => sat.Guid.Equals(id)).FirstOrDefault();
+ var satellite = RTCore.Instance.Satellites[id];
if (satellite == null) return double.PositiveInfinity;
- if (!RTCore.Instance.Network[satellite].Any()) return double.PositiveInfinity;
- var shortestDelay = RTCore.Instance.Network[satellite].Min().Delay;
+ var shortestDelay = RTCore.Instance.Network.ShortestDelay(satellite);
RTLog.Verbose("Flight: Shortest signal delay from {0} to {1}", RTLogLevel.API, id, shortestDelay);
return shortestDelay;
}
@@ -261,12 +289,11 @@ public static double GetShortestSignalDelay(Guid id)
public static double GetSignalDelayToKSC(Guid id)
{
if (RTCore.Instance == null) return double.PositiveInfinity;
- var satellite = RTCore.Instance.Satellites.Where(sat => sat.Guid.Equals(id)).FirstOrDefault();
+ var satellite = RTCore.Instance.Satellites[id];
if (satellite == null) return double.PositiveInfinity;
- if (!RTCore.Instance.Network[satellite].Any(r => RTCore.Instance.Network.GroundStations.ContainsKey(r.Goal.Guid))) return double.PositiveInfinity;
- var signalDelaytoKerbin = RTCore.Instance.Network[satellite].Where(r => RTCore.Instance.Network.GroundStations.ContainsKey(r.Goal.Guid)).Min().Delay;
+ var signalDelaytoKerbin = RTCore.Instance.Network.ShortestDelay(satellite, groundOnly: true);
RTLog.Verbose("Connection from {0} to Kerbin Delay: {1}", RTLogLevel.API, id, signalDelaytoKerbin);
return signalDelaytoKerbin;
}
@@ -274,17 +301,12 @@ public static double GetSignalDelayToKSC(Guid id)
public static double GetSignalDelayToSatellite(Guid a, Guid b)
{
if (RTCore.Instance == null) return double.PositiveInfinity;
- var satelliteA = RTCore.Instance.Satellites.Where(sat => sat.Guid.Equals(a)).FirstOrDefault();
- var satelliteB = RTCore.Instance.Satellites.Where(sat => sat.Guid.Equals(b)).FirstOrDefault();
+ var satelliteA = RTCore.Instance.Satellites[a];
+ var satelliteB = RTCore.Instance.Satellites[b];
if (satelliteA == null || satelliteB == null) return double.PositiveInfinity;
- Func>> neighbors = RTCore.Instance.Network.FindNeighbors;
- Func, double> cost = RangeModelExtensions.DistanceTo;
- Func heuristic = RangeModelExtensions.DistanceTo;
-
- var path = NetworkPathfinder.Solve(satelliteA, satelliteB, neighbors, cost, heuristic);
- var delayBetween = path.Delay;
+ var delayBetween = RTCore.Instance.Network.ShortestDelayBetween(satelliteA, satelliteB);
RTLog.Verbose("Connection from {0} to {1} Delay: {2}", RTLogLevel.API, a, b, delayBetween);
return delayBetween;
}
@@ -410,7 +432,7 @@ public static bool ChangeGroundStationRange(Guid stationId, float newRange)
public static bool SetRadioBlackoutGuid(Guid id, bool flag, string reason = "")
{
if (RTCore.Instance == null) return false;
- var satellite = RTCore.Instance.Satellites.Where(sat => sat.Guid.Equals(id)).FirstOrDefault();
+ var satellite = RTCore.Instance.Satellites[id];
if (satellite == null) return false;
satellite.IsInRadioBlackout = flag;
@@ -425,7 +447,7 @@ public static bool SetRadioBlackoutGuid(Guid id, bool flag, string reason = "")
public static bool GetRadioBlackoutGuid(Guid id)
{
if (RTCore.Instance == null) return false;
- var satellite = RTCore.Instance.Satellites.Where(sat => sat.Guid.Equals(id)).FirstOrDefault();
+ var satellite = RTCore.Instance.Satellites[id];
if (satellite == null) return false;
var blackoutFlag = satellite.IsInRadioBlackout;
@@ -440,7 +462,7 @@ public static bool GetRadioBlackoutGuid(Guid id)
public static bool SetPowerDownGuid(Guid id, bool flag, string reason = "")
{
if (RTCore.Instance == null) return false;
- var satellite = RTCore.Instance.Satellites.Where(sat => sat.Guid.Equals(id)).FirstOrDefault();
+ var satellite = RTCore.Instance.Satellites[id];
if (satellite == null) return false;
satellite.PowerShutdownFlag = flag;
@@ -455,7 +477,7 @@ public static bool SetPowerDownGuid(Guid id, bool flag, string reason = "")
public static bool GetPowerDownGuid(Guid id)
{
if (RTCore.Instance == null) return false;
- var satellite = RTCore.Instance.Satellites.Where(sat => sat.Guid.Equals(id)).FirstOrDefault();
+ var satellite = RTCore.Instance.Satellites[id];
if (satellite == null) return false;
var flag = satellite.PowerShutdownFlag;
@@ -481,30 +503,33 @@ public static double GetMaxRangeDistance(Guid sat_a, Guid sat_b)
if (satelliteA == null || satelliteB == null) return 0.0;
//get link object
- NetworkLink link = null;
+ NetworkLink? maybeLink = null;
+ List interfaces = null;
switch (RTSettings.Instance.RangeModelType)
{
case RangeModel.RangeModel.Additive:
- link = RangeModelRoot.GetLink(satelliteA, satelliteB);
+ maybeLink = RangeModelRoot.GetLink(satelliteA, satelliteB);
+ interfaces = RangeModelRoot.GetLinkInterfaces(satelliteA, satelliteB);
break;
default:
- link = RangeModelStandard.GetLink(satelliteA, satelliteB);
+ maybeLink = RangeModelStandard.GetLink(satelliteA, satelliteB);
+ interfaces = RangeModelStandard.GetLinkInterfaces(satelliteA, satelliteB);
break;
}
- if (link == null) return 0.0; //no connection possible
+ if (maybeLink == null) return 0.0; //no connection possible
//get max distance out of multiple antenna connections
var distance = 0.0;
var maxDistance = 0.0;
- for(int i=0; i < link.Interfaces.Count; i++)
+ for(int i=0; i < interfaces.Count; i++)
{
switch (RTSettings.Instance.RangeModelType)
{
case RangeModel.RangeModel.Additive:
- distance = RangeModelRoot.GetRangeInContext(link.Interfaces[i], satelliteB, satelliteA);
+ distance = RangeModelRoot.GetRangeInContext(interfaces[i], satelliteB, satelliteA);
break;
default:
- distance = RangeModelStandard.GetRangeInContext(link.Interfaces[i], satelliteB, satelliteA);
+ distance = RangeModelStandard.GetRangeInContext(interfaces[i], satelliteB, satelliteA);
break;
}
maxDistance = Math.Max(maxDistance, Double.IsNaN(distance) ? 0.0 : distance);
@@ -524,8 +549,8 @@ public static double GetRangeDistance(Guid sat_a, Guid sat_b)
if (RTCore.Instance == null) return 0.0;
//sanity check
- var satelliteA = RTCore.Instance.Network.Where(sat => sat.Guid.Equals(sat_a)).FirstOrDefault();
- var satelliteB = RTCore.Instance.Network.Where(sat => sat.Guid.Equals(sat_b)).FirstOrDefault();
+ var satelliteA = RTCore.Instance.Network[sat_a];
+ var satelliteB = RTCore.Instance.Network[sat_b];
if (satelliteA == null || satelliteB == null) return 0.0;
diff --git a/src/RemoteTech/AddOns/Kerbalism.cs b/src/RemoteTech/AddOns/Kerbalism.cs
index 4d442702c..11546deff 100644
--- a/src/RemoteTech/AddOns/Kerbalism.cs
+++ b/src/RemoteTech/AddOns/Kerbalism.cs
@@ -3,7 +3,9 @@
namespace RemoteTech.AddOns
{
- /// Simple class to detect if Kerbalism is loaded
+ ///
+ /// Simple class to detect if Kerbalism is loaded
+ ///
public static class Kerbalism
{
private static readonly Type API;
@@ -21,7 +23,9 @@ static Kerbalism()
}
}
- /// Returns true if Kerbalism is detected for the current game
+ ///
+ /// Returns true if Kerbalism is detected for the current game
+ ///
public static bool Exists
{
get { return API != null; }
diff --git a/src/RemoteTech/AntennaManager.cs b/src/RemoteTech/AntennaManager.cs
index 4d4d296eb..2213a9beb 100644
--- a/src/RemoteTech/AntennaManager.cs
+++ b/src/RemoteTech/AntennaManager.cs
@@ -15,14 +15,14 @@ public class AntennaManager : IDisposable, IEnumerable
public event Action OnRegister = delegate { };
public event Action OnUnregister = delegate { };
- public IEnumerable this[ISatellite s] { get { return For(s.Guid); } }
- public IEnumerable this[Vessel v] { get { return For(v.id); } }
- public IEnumerable this[Guid g] { get { return For(g); } }
+ public IReadOnlyList this[ISatellite s] { get { return For(s.Guid); } }
+ public IReadOnlyList this[Vessel v] { get { return For(v.id); } }
+ public IReadOnlyList this[Guid g] { get { return For(g); } }
private readonly Dictionary> mLoadedAntennaCache =
new Dictionary>();
- private readonly Dictionary> mProtoAntennaCache =
- new Dictionary>();
+ private readonly Dictionary> mProtoAntennaCache =
+ new Dictionary>();
public AntennaManager()
{
@@ -35,6 +35,15 @@ public AntennaManager()
public void Dispose()
{
GameEvents.onVesselGoOnRails.Remove(OnVesselGoOnRails);
+
+ foreach (List antennas in mProtoAntennaCache.Values)
+ {
+ foreach (ProtoAntenna antenna in antennas)
+ {
+ antenna.Dispose();
+ }
+ }
+ mProtoAntennaCache.Clear();
}
public void Register(Guid key, IAntenna antenna)
@@ -98,7 +107,7 @@ public void RegisterProtos(Vessel v)
{
if (!mProtoAntennaCache.ContainsKey(key))
{
- mProtoAntennaCache[key] = new List();
+ mProtoAntennaCache[key] = new List();
}
ProtoAntenna proto = new ProtoAntenna(v, pps, ppms);
mProtoAntennaCache[key].Add(proto);
@@ -113,25 +122,22 @@ public void UnregisterProtos(Guid key)
if (!mProtoAntennaCache.ContainsKey(key)) return;
- foreach (IAntenna a in mProtoAntennaCache[key])
+ foreach (ProtoAntenna a in mProtoAntennaCache[key])
{
OnUnregister.Invoke(a);
+ a.Dispose();
}
mProtoAntennaCache.Remove(key);
}
- private IEnumerable For(Guid key)
+ private IReadOnlyList For(Guid key)
{
if (mLoadedAntennaCache.ContainsKey(key))
- {
return mLoadedAntennaCache[key];
- }
if (mProtoAntennaCache.ContainsKey(key))
- {
return mProtoAntennaCache[key];
- }
- return Enumerable.Empty();
+ return [];
}
private void OnVesselGoOnRails(Vessel v)
@@ -163,11 +169,6 @@ public static bool IsAntenna(this ProtoPartModuleSnapshot ppms)
ppms.GetBool("IsRTActive");
}
- public static bool IsAntenna(this PartModule pm)
- {
- return pm.Fields.GetValue("IsRTAntenna") &&
- pm.Fields.GetValue("IsRTPowered") &&
- pm.Fields.GetValue("IsRTActive");
- }
+ public static bool IsAntenna(this PartModule pm) => pm is IAntenna;
}
}
\ No newline at end of file
diff --git a/src/RemoteTech/Collections/ArrayMap.cs b/src/RemoteTech/Collections/ArrayMap.cs
new file mode 100644
index 000000000..dd8c02a21
--- /dev/null
+++ b/src/RemoteTech/Collections/ArrayMap.cs
@@ -0,0 +1,146 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+
+namespace RemoteTech.Collections;
+
+public class ArrayMap : IEnumerable>
+{
+ private readonly Dictionary indices;
+ private readonly List keys;
+ private readonly List values;
+
+ public int Count => values.Count;
+ public V this[K key]
+ {
+ get => values[indices[key]];
+ set
+ {
+ if (indices.TryGetValue(key, out var index))
+ {
+ keys[index] = key;
+ values[index] = value;
+ }
+ else
+ {
+ index = values.Count;
+ indices.Add(key, index);
+ keys.Add(key);
+ values.Add(value);
+ }
+ }
+ }
+
+ public ReadOnlySpan Keys => keys.AsReadOnlySpan();
+ public Span Values => values.AsSpan();
+
+ public ArrayMap()
+ {
+ indices = [];
+ keys = [];
+ values = [];
+ }
+
+ public ArrayMap(int capacity)
+ {
+ indices = new(capacity);
+ keys = new(capacity);
+ values = new(capacity);
+ }
+
+ public bool TryGetValue(K key, out V value)
+ {
+ value = default;
+ if (!indices.TryGetValue(key, out var index))
+ return false;
+
+ value = values[index];
+ return true;
+ }
+
+ public bool ContainsKey(K key) => indices.ContainsKey(key);
+
+ public int IndexOf(K key) => indices.TryGetValue(key, out var index) ? index : -1;
+
+ public void Add(K key, V value)
+ {
+ var count = Count;
+ indices.Add(key, count);
+ keys.Add(key);
+ values.Add(value);
+ }
+
+ public bool TryAdd(K key, V value)
+ {
+ if (indices.ContainsKey(key))
+ return false;
+
+ var index = Count;
+ indices.Add(key, index);
+ keys.Add(key);
+ values.Add(value);
+ return true;
+ }
+
+ public bool Remove(K key)
+ {
+ if (!indices.TryGetValue(key, out var index))
+ return false;
+
+ if (index == Count - 1)
+ {
+ indices.Remove(key);
+ keys.RemoveAt(index);
+ values.RemoveAt(index);
+ }
+ else
+ {
+ var count = Count;
+ var other = keys[index] = keys[count - 1];
+ values[index] = values[count - 1];
+
+ indices[other] = index;
+ indices.Remove(key);
+ keys.RemoveAt(count - 1);
+ values.RemoveAt(count - 1);
+ }
+
+ return true;
+ }
+
+ public void Clear()
+ {
+ indices.Clear();
+ keys.Clear();
+ values.Clear();
+ }
+
+ public Enumerator GetEnumerator() => new(this);
+ IEnumerator> IEnumerable>.GetEnumerator() => GetEnumerator();
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+
+ public struct Enumerator(ArrayMap map) : IEnumerator>
+ {
+ List.Enumerator keys = map.keys.GetEnumerator();
+ List.Enumerator vals = map.values.GetEnumerator();
+
+ public KeyValuePair Current => new(keys.Current, vals.Current);
+ object IEnumerator.Current => Current;
+
+ public bool MoveNext() => keys.MoveNext() & vals.MoveNext();
+
+ void IEnumerator.Reset()
+ {
+ DoReset(ref keys);
+ DoReset(ref vals);
+ }
+
+ public void Dispose() { }
+
+ private static void DoReset(ref T enumerator)
+ where T : IEnumerator
+ {
+ enumerator.Reset();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/RemoteTech/Collections/ArrayMinHeap.cs b/src/RemoteTech/Collections/ArrayMinHeap.cs
new file mode 100644
index 000000000..0ac9148e2
--- /dev/null
+++ b/src/RemoteTech/Collections/ArrayMinHeap.cs
@@ -0,0 +1,106 @@
+using System;
+using Unity.Collections;
+
+namespace RemoteTech.Collections;
+
+///
+/// Binary min-heap over an internally-allocated .
+/// Grows on demand, so callers don't need to size it to a total-pushes upper
+/// bound themselves.
+///
+internal struct ArrayMinHeap(int capacity, Allocator allocator)
+ where T : unmanaged, IComparable
+{
+ private NativeList _items = new(capacity, allocator);
+
+ public int Count => _items.Length;
+
+ public void Push(T item)
+ {
+ int index = Count;
+ _items.Add(item);
+ MoveUp(index);
+ }
+
+ ///
+ /// Adds a batch of items in O(n) via bottom-up heapify, rather than O(n log n)
+ /// from pushing them one at a time.
+ ///
+ public void AddRange(NativeArray items)
+ {
+ _items.AddRange(items);
+
+ for (int i = (Count >> 1) - 1; i >= 0; i--)
+ MoveDown(i);
+ }
+
+ public bool TryPop(out T item)
+ {
+ if (Count == 0)
+ {
+ item = default;
+ return false;
+ }
+
+ item = _items[0];
+ _items.RemoveAtSwapBack(0);
+
+ if (Count > 0)
+ MoveDown(0);
+
+ return true;
+ }
+
+ ///
+ /// Sifts the item at up toward the root until its
+ /// parent is no greater.
+ ///
+ private void MoveUp(int index)
+ {
+ T item = _items[index];
+ while (index > 0)
+ {
+ int parent = (index - 1) >> 1;
+ T parentItem = _items[parent];
+ if (item.CompareTo(parentItem) >= 0)
+ break;
+
+ _items[index] = parentItem;
+ index = parent;
+ }
+ _items[index] = item;
+ }
+
+ ///
+ /// Sifts the item at down toward the leaves until
+ /// both children are no smaller.
+ ///
+ private void MoveDown(int index)
+ {
+ T item = _items[index];
+ int count = Count;
+ while (true)
+ {
+ int left = (index << 1) + 1;
+ int right = left + 1;
+ if (left >= count) break;
+
+ int min = left;
+ T minItem = _items[left];
+ if (right < count)
+ {
+ T r = _items[right];
+ if (r.CompareTo(minItem) < 0)
+ {
+ min = right;
+ minItem = r;
+ }
+ }
+
+ if (item.CompareTo(minItem) <= 0) break;
+ _items[index] = minItem;
+ index = min;
+ }
+ _items[index] = item;
+ }
+}
diff --git a/src/RemoteTech/Collections/Extensions.cs b/src/RemoteTech/Collections/Extensions.cs
new file mode 100644
index 000000000..9b1e6ed43
--- /dev/null
+++ b/src/RemoteTech/Collections/Extensions.cs
@@ -0,0 +1,159 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using Unity.Collections;
+using Unity.Collections.LowLevel.Unsafe;
+using Unity.Mathematics;
+
+namespace RemoteTech.Collections;
+
+internal static class RTExtensions
+{
+ #region NativeArray
+ public static unsafe ref T GetElement(this NativeArray array, int index)
+ where T : unmanaged
+ => ref ((T*)NativeArrayUnsafeUtility.GetUnsafePtr(array))[index];
+
+ public static void Fill(this NativeArray array, T value)
+ where T : unmanaged
+ {
+ for (int i = 0; i < array.Length; ++i)
+ array[i] = value;
+ }
+
+ public static unsafe void Clear(this NativeArray array)
+ where T : unmanaged
+ {
+ UnsafeUtility.MemClear(
+ NativeArrayUnsafeUtility.GetUnsafePtr(array),
+ (long)array.Length * UnsafeUtility.SizeOf());
+ }
+ #endregion
+
+ #region NativeList
+ public static unsafe ref T GetElement(this NativeList list, int index)
+ where T : unmanaged
+ {
+ return ref ((T*)NativeListUnsafeUtility.GetUnsafePtr(list))[index];
+ }
+
+ public static void Resize(this NativeList list, int length, T value)
+ where T : unmanaged
+ {
+ var start = list.Length;
+ list.ResizeUninitialized(length);
+
+ for (int i = start; i < length; ++i)
+ list[i] = value;
+ }
+ #endregion
+
+ #region NativeSlice
+ public static unsafe NativeArray AsNativeArray(this NativeSlice slice)
+ where T : unmanaged
+ {
+ return NativeArrayUnsafeUtility.ConvertExistingDataToNativeArray(
+ NativeSliceUnsafeUtility.GetUnsafePtr(slice),
+ slice.Length,
+ Allocator.Invalid);
+ }
+ #endregion
+
+ #region Vector3d
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static double3 ToDouble3(this Vector3d v) => new(v.x, v.y, v.z);
+ #endregion
+
+ #region List
+ public static Span AsSpan(this List list) =>
+ ((Span)list._items).Slice(0, list.Count);
+
+ public static ReadOnlySpan AsReadOnly(this Span span) => span;
+ public static ReadOnlySpan AsReadOnlySpan(this List list) => list.AsSpan();
+ #endregion
+
+ #region Span
+ public static SpanEnumerator GetEnumerator(this Span span) => new(span);
+
+ public static ReadOnlySpanEnumerator GetEnumerator(this ReadOnlySpan span) => new(span);
+
+ public static T[] Concat(this ReadOnlySpan a, ReadOnlySpan b)
+ {
+ T[] array = new T[a.Length + b.Length];
+ a.CopyTo(array.AsSpan().Slice(0, a.Length));
+ b.CopyTo(array.AsSpan().Slice(a.Length));
+ return array;
+ }
+ #endregion
+
+ #region NativeKeyValueArrays
+ public static NativeKeyValueArraysEnumerator GetEnumerator(this NativeKeyValueArrays arrays)
+ where K : unmanaged
+ where V : unmanaged
+ {
+ return new(arrays);
+ }
+ #endregion
+}
+
+public ref struct SpanEnumerator(Span span) : IEnumerator
+{
+ readonly Span span = span;
+ int index = -1;
+
+ public readonly ref T Current => ref span[index];
+ readonly T IEnumerator.Current => Current;
+ readonly object IEnumerator.Current => Current;
+
+ public bool MoveNext()
+ {
+ index += 1;
+ return index < span.Length;
+ }
+
+ public void Dispose() { }
+
+ public void Reset() => index = -1;
+}
+
+public ref struct ReadOnlySpanEnumerator(ReadOnlySpan span) : IEnumerator
+{
+ readonly ReadOnlySpan span = span;
+ int index = -1;
+
+ public readonly T Current => span[index];
+ readonly object IEnumerator.Current => Current;
+
+ public bool MoveNext()
+ {
+ index += 1;
+ return index < span.Length;
+ }
+
+ public void Dispose() { }
+
+ public void Reset() => index = -1;
+}
+
+internal struct NativeKeyValueArraysEnumerator(NativeKeyValueArrays arrays) : IEnumerator>
+ where K : unmanaged
+ where V : unmanaged
+{
+ int index = -1;
+ readonly NativeKeyValueArrays arrays = arrays;
+
+ public KeyValuePair Current => new (arrays.Keys[index], arrays.Values[index]);
+
+ object IEnumerator.Current => Current;
+
+ public bool MoveNext()
+ {
+ index += 1;
+ return index < arrays.Keys.Length;
+ }
+
+ public void Reset() => index = -1;
+
+ public void Dispose() { }
+}
diff --git a/src/RemoteTech/Collections/IJobParallelForBatchDefer.cs b/src/RemoteTech/Collections/IJobParallelForBatchDefer.cs
new file mode 100644
index 000000000..fb6241798
--- /dev/null
+++ b/src/RemoteTech/Collections/IJobParallelForBatchDefer.cs
@@ -0,0 +1,73 @@
+using System;
+using System.Runtime.InteropServices;
+using Unity.Collections;
+using Unity.Collections.LowLevel.Unsafe;
+using Unity.Jobs;
+using Unity.Jobs.LowLevel.Unsafe;
+
+namespace RemoteTech.Collections;
+
+///
+/// A batched equivalent of .
+///
+[JobProducerType(typeof(IJobParallelForBatchDeferExtensions.JobData<>))]
+internal interface IJobParallelForBatchDefer
+{
+ void Execute(int start, int count);
+}
+
+internal static class IJobParallelForBatchDeferExtensions
+{
+ [StructLayout(LayoutKind.Sequential, Size = 1)]
+ internal struct JobData
+ where T : struct, IJobParallelForBatchDefer
+ {
+ public delegate void ExecuteJobFunction(
+ ref T jobData,
+ IntPtr additionalPtr,
+ IntPtr bufferRangePatchData,
+ ref JobRanges ranges,
+ int jobIndex);
+
+ public readonly static IntPtr JobReflectionData = JobsUtility.CreateJobReflectionData(
+ typeof(T),
+ typeof(T),
+ JobType.ParallelFor,
+ new ExecuteJobFunction(Execute));
+
+ public static void Execute(
+ ref T jobData,
+ IntPtr additionalPtr,
+ IntPtr bufferRangePatchData,
+ ref JobRanges ranges,
+ int jobIndex)
+ {
+ while (JobsUtility.GetWorkStealingRange(ref ranges, jobIndex, out var start, out var end))
+ {
+ jobData.Execute(start, end - start);
+ }
+ }
+ }
+
+ public unsafe static JobHandle Schedule(
+ this T job,
+ NativeList list,
+ int innerloopBatchCount,
+ JobHandle dependsOn = default
+ )
+ where T : struct, IJobParallelForBatchDefer
+ where U : unmanaged
+ {
+ var parameters = new JobsUtility.JobScheduleParameters(
+ UnsafeUtility.AddressOf(ref job),
+ JobData.JobReflectionData,
+ dependsOn,
+ ScheduleMode.Batched);
+
+ return JobsUtility.ScheduleParallelForDeferArraySize(
+ ref parameters,
+ innerloopBatchCount,
+ NativeListUnsafeUtility.GetInternalListDataPtrUnchecked(ref list),
+ null);
+ }
+}
\ No newline at end of file
diff --git a/src/RemoteTech/Collections/IntRange.cs b/src/RemoteTech/Collections/IntRange.cs
new file mode 100644
index 000000000..7eef094ed
--- /dev/null
+++ b/src/RemoteTech/Collections/IntRange.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+
+namespace RemoteTech.Collections;
+
+internal readonly struct IntRange(int start, int length)
+{
+ public int Start { get; }= start;
+ public int Length { get; } = length;
+ public int End => Start + Length;
+
+ public bool IsEmpty => Length == 0;
+
+ public RangeEnumerator GetEnumerator() => new(this);
+}
+
+internal struct RangeEnumerator(IntRange range) : IEnumerator
+{
+ int index = range.Start - 1;
+ readonly int end = range.End;
+
+ public readonly int Current => index;
+ readonly object IEnumerator.Current => Current;
+
+ public readonly void Dispose() { }
+
+ public bool MoveNext()
+ {
+ index += 1;
+ return index < end;
+ }
+
+ public void Reset() => throw new NotImplementedException();
+
+ public readonly RangeEnumerator GetEnumerator() => this;
+}
\ No newline at end of file
diff --git a/src/RemoteTech/Collections/ObjectExt.cs b/src/RemoteTech/Collections/ObjectExt.cs
new file mode 100644
index 000000000..fe5129ff4
--- /dev/null
+++ b/src/RemoteTech/Collections/ObjectExt.cs
@@ -0,0 +1,27 @@
+using System;
+using System.Runtime.CompilerServices;
+
+namespace RemoteTech.Collections;
+
+internal static class ObjectExt
+{
+ ///
+ /// True if this reference is null or if the instance is destroyed
+ /// Equivalent as testing == null but 4-5 times faster.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool IsNullOrDestroyed(this UnityEngine.Object unityObject)
+ {
+ return unityObject is null || unityObject.m_CachedPtr == IntPtr.Zero;
+ }
+
+ ///
+ /// True if this reference is not null and the instance is not destroyed
+ /// Equivalent as testing != null but 4-5 times faster.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool IsNotNullOrDestroyed(this UnityEngine.Object unityObject)
+ {
+ return unityObject is not null && unityObject.m_CachedPtr != IntPtr.Zero;
+ }
+}
\ No newline at end of file
diff --git a/src/RemoteTech/Collections/ObjectHandle.cs b/src/RemoteTech/Collections/ObjectHandle.cs
new file mode 100644
index 000000000..e01d59bc4
--- /dev/null
+++ b/src/RemoteTech/Collections/ObjectHandle.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using Unity.Jobs;
+
+namespace RemoteTech.Collections;
+
+///
+/// A helper struct that allows you to pass managed objects to jobs.
+///
+///
+internal struct ObjectHandle(T value, GCHandleType type) : IDisposable
+ where T : class
+{
+ GCHandle handle = GCHandle.Alloc(value, type);
+
+ public T Target
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => (T)handle.Target;
+ }
+
+ public bool IsAllocated => handle.IsAllocated;
+
+ public ObjectHandle(T value)
+ : this(value, GCHandleType.Normal) { }
+
+ public IntPtr AddrOfPinnedObject() => handle.AddrOfPinnedObject();
+
+ public void Dispose() => handle.Free();
+
+ public JobHandle Dispose(JobHandle job) =>
+ new DisposeJob { handle = this }.Schedule(job);
+
+ struct DisposeJob : IJob
+ {
+ public ObjectHandle handle;
+
+ public void Execute() => handle.Dispose();
+ }
+}
diff --git a/src/RemoteTech/Collections/SpookyHash.cs b/src/RemoteTech/Collections/SpookyHash.cs
new file mode 100644
index 000000000..56dbd30cb
--- /dev/null
+++ b/src/RemoteTech/Collections/SpookyHash.cs
@@ -0,0 +1,187 @@
+using Unity.Collections.LowLevel.Unsafe;
+using UnityEngine;
+
+namespace RemoteTech.Collections;
+
+///
+/// Burst-compatible port of SpookyHash V2.
+///
+///
+///
+/// Unity actually has this available internally, but it is not burst-compatible
+/// due to a call made in its static constructor.
+///
+internal static unsafe class SpookyHash
+{
+ const ulong SeedConst = 0xdeadbeefdeadbeefUL;
+
+ static readonly byte[] MixRot = [11, 32, 43, 31, 17, 28, 39, 57, 55, 54, 22, 46];
+ static readonly byte[] EndRot = [44, 15, 34, 21, 38, 33, 10, 13, 38, 53, 42, 54];
+ static readonly byte[] ShortMixRot = [50, 52, 30, 41, 54, 48, 38, 37, 62, 34, 5, 36];
+ static readonly byte[] ShortEndRot = [15, 52, 26, 51, 28, 9, 47, 54, 32, 25, 63];
+
+ static ulong Rot64(ulong x, int k) => (x << k) | (x >> (64 - k));
+
+ ///
+ /// Updates the 128-bit state in place, seeded from 's current value.
+ ///
+ public static void Hash128(void* data, int length, ref Hash128 hash)
+ {
+ fixed (Hash128* hp = &hash)
+ {
+ ulong* seed = (ulong*)hp;
+ ulong hash1 = seed[0];
+ ulong hash2 = seed[1];
+
+ if (length < 192)
+ {
+ Short(data, length, ref hash1, ref hash2);
+ }
+ else
+ {
+ var s = stackalloc ulong[12];
+ for (int i = 0; i < 12; i++)
+ s[i] = (i % 3) switch { 0 => hash1, 1 => hash2, _ => SeedConst };
+
+ byte* p = (byte*)data;
+ byte* end = p + length / 96 * 96;
+ while (p < end)
+ {
+ Mix((ulong*)p, s);
+ p += 96;
+ }
+
+ var buf = stackalloc ulong[12];
+ int remainder = length - (int)(end - (byte*)data);
+ UnsafeUtility.MemCpy(buf, p, remainder);
+ UnsafeUtility.MemClear((byte*)buf + remainder, 96 - remainder);
+ ((byte*)buf)[95] = (byte)remainder;
+
+ End(buf, s);
+
+ hash1 = s[0];
+ hash2 = s[1];
+ }
+
+ seed[0] = hash1;
+ seed[1] = hash2;
+ }
+ }
+
+ static void Mix(ulong* data, ulong* s)
+ {
+ for (int i = 0; i < 12; i++)
+ {
+ int a = i;
+ int b = (i + 2) % 12;
+ int c = (i + 10) % 12;
+ int d = (i + 11) % 12;
+ int e = (i + 1) % 12;
+ s[a] += data[a];
+ s[b] ^= s[c];
+ s[d] ^= s[a];
+ s[a] = Rot64(s[a], MixRot[a]);
+ s[d] += s[e];
+ }
+ }
+
+ static void End(ulong* data, ulong* s)
+ {
+ for (int i = 0; i < 12; i++) s[i] += data[i];
+
+ for (int round = 0; round < 3; round++)
+ {
+ for (int i = 0; i < 12; i++)
+ {
+ int a = (i + 11) % 12;
+ int b = (i + 1) % 12;
+ int c = (i + 2) % 12;
+ s[a] += s[b];
+ s[c] ^= s[a];
+ s[b] = Rot64(s[b], EndRot[i]);
+ }
+ }
+ }
+
+ static void ShortMix(ulong* h)
+ {
+ for (int k = 0; k < 12; k++)
+ {
+ int cur = (2 + k) % 4;
+ int nxt = (cur + 1) % 4;
+ int prev2 = (cur + 2) % 4;
+ h[cur] = Rot64(h[cur], ShortMixRot[k]);
+ h[cur] += h[nxt];
+ h[prev2] ^= h[cur];
+ }
+ }
+
+ static readonly int[] ShortEndX = { 3, 0, 1, 2 };
+ static readonly int[] ShortEndY = { 2, 3, 0, 1 };
+
+ static void ShortEnd(ulong* h)
+ {
+ for (int k = 0; k < 11; k++)
+ {
+ int x = ShortEndX[k % 4];
+ int y = ShortEndY[k % 4];
+ h[x] ^= h[y];
+ h[y] = Rot64(h[y], ShortEndRot[k]);
+ h[x] += h[y];
+ }
+ }
+
+ static void Short(void* message, int length, ref ulong hash1, ref ulong hash2)
+ {
+ var h = stackalloc ulong[4];
+ h[0] = hash1;
+ h[1] = hash2;
+ h[2] = SeedConst;
+ h[3] = SeedConst;
+
+ byte* p = (byte*)message;
+ int remaining = length;
+
+ while (remaining >= 32)
+ {
+ h[2] += *(ulong*)p;
+ h[3] += *(ulong*)(p + 8);
+ ShortMix(h);
+ h[0] += *(ulong*)(p + 16);
+ h[1] += *(ulong*)(p + 24);
+ p += 32;
+ remaining -= 32;
+ }
+
+ if (remaining >= 16)
+ {
+ h[2] += *(ulong*)p;
+ h[3] += *(ulong*)(p + 8);
+ ShortMix(h);
+ p += 16;
+ remaining -= 16;
+ }
+
+ h[3] += (ulong)(uint)length << 56;
+
+ if (remaining > 0)
+ {
+ // Zero-padded scratch instead of Unity's in-place tail reads, which overrun the buffer for a 7-byte remainder.
+ var tail = stackalloc byte[16];
+ UnsafeUtility.MemClear(tail, 16);
+ UnsafeUtility.MemCpy(tail, p, remaining);
+ h[2] += *(ulong*)tail;
+ h[3] += *(ulong*)(tail + 8);
+ }
+ else
+ {
+ h[2] += SeedConst;
+ h[3] += SeedConst;
+ }
+
+ ShortEnd(h);
+
+ hash1 = h[0];
+ hash2 = h[1];
+ }
+}
diff --git a/src/RemoteTech/FlightComputer/Commands/ExternalAPICommand.cs b/src/RemoteTech/FlightComputer/Commands/ExternalAPICommand.cs
index 3ce05d512..19859e5bb 100644
--- a/src/RemoteTech/FlightComputer/Commands/ExternalAPICommand.cs
+++ b/src/RemoteTech/FlightComputer/Commands/ExternalAPICommand.cs
@@ -5,27 +5,49 @@ namespace RemoteTech.FlightComputer.Commands
{
public class ExternalAPICommand : AbstractCommand
{
- /// original ConfigNode object passed from the api
+ ///
+ /// original ConfigNode object passed from the api
+ ///
private ConfigNode externalData;
- /// Name of the mod who passed this command
+ ///
+ /// Name of the mod who passed this command
+ ///
private string Executor;
- /// Label for this command on the queue
+ ///
+ /// Label for this command on the queue
+ ///
private string QueueLabel;
- /// Label for this command if its active
+ ///
+ /// Label for this command if its active
+ ///
private string ActiveLabel;
- /// Label for alert on no power
+ ///
+ /// Label for alert on no power
+ ///
private string ShortLabel;
- /// The ReflectionType for the methods to invoke
+ ///
+ /// The ReflectionType for the methods to invoke
+ ///
private string ReflectionType;
- /// Name of the Pop-method on the ReflectionType
+ ///
+ /// Name of the Pop-method on the ReflectionType
+ ///
private string ReflectionPopMethod = "";
- /// Name of the Execution-method on the ReflectionType
+ ///
+ /// Name of the Execution-method on the ReflectionType
+ ///
private string ReflectionExecuteMethod = "";
- /// Name of the Abort-method on the ReflectionType
+ ///
+ /// Name of the Abort-method on the ReflectionType
+ ///
private string ReflectionAbortMethod = "";
- /// GUID of the vessel
+ ///
+ /// GUID of the vessel
+ ///
private string GUIDString;
- /// true - when this command will be aborted
+ ///
+ /// true - when this command will be aborted
+ ///
private bool AbortCommand = false;
public override int Priority { get { return 0; } }
diff --git a/src/RemoteTech/FlightComputer/Commands/ManeuverCommand.cs b/src/RemoteTech/FlightComputer/Commands/ManeuverCommand.cs
index 8482e9004..db78a0e48 100644
--- a/src/RemoteTech/FlightComputer/Commands/ManeuverCommand.cs
+++ b/src/RemoteTech/FlightComputer/Commands/ManeuverCommand.cs
@@ -6,7 +6,9 @@ namespace RemoteTech.FlightComputer.Commands
{
public class ManeuverCommand : AbstractCommand
{
- /// Index id of this maneuver node from patchedConicSolver.maneuverNodes list
+ ///
+ /// Index id of this maneuver node from patchedConicSolver.maneuverNodes list
+ ///
[Persistent] public int NodeIndex;
///
[Persistent] public string KaCItemId = String.Empty;
diff --git a/src/RemoteTech/FlightComputer/FlightComputer.cs b/src/RemoteTech/FlightComputer/FlightComputer.cs
index 385d34b2f..723899a1d 100644
--- a/src/RemoteTech/FlightComputer/FlightComputer.cs
+++ b/src/RemoteTech/FlightComputer/FlightComputer.cs
@@ -16,49 +16,74 @@ namespace RemoteTech.FlightComputer
///
public class FlightComputer : IDisposable
{
- /// Flight computer loaded configuration from persistent save.
+ ///
+ /// Flight computer loaded configuration from persistent save.
+ ///
private ConfigNode _fcLoadedConfigs;
- /// List of active commands in the flight computer.
+ ///
+ /// List of active commands in the flight computer.
+ ///
private readonly SortedDictionary _activeCommands = new SortedDictionary();
- /// List of commands queued in the flight computer.
+ ///
+ /// List of commands queued in the flight computer.
+ ///
private readonly List _commandQueue = new List();
- /// Flight control queue: this is a priority queue used to delay .
+ ///
+ /// Flight control queue: this is a priority queue used to delay .
+ ///
private readonly PriorityQueue _flightCtrlQueue = new PriorityQueue();
- /// The window of the flight computer.
+ ///
+ /// The window of the flight computer.
+ ///
private FlightComputerWindow _flightComputerWindow;
- /// Current state of the flight computer.
+ ///
+ /// Current state of the flight computer.
+ ///
[Flags]
public enum State
{
- /// Normal state.
+ ///
+ /// Normal state.
+ ///
Normal = 0,
- /// The flight computer (and its vessel) are packed: vessels are only packed when they come within about 300m of the active vessel.
+ ///
+ /// The flight computer (and its vessel) are packed: vessels are only packed when they come within about 300m of the active vessel.
+ ///
Packed = 2,
- /// The flight computer (and its vessel) are out of power.
+ ///
+ /// The flight computer (and its vessel) are out of power.
+ ///
OutOfPower = 4,
- /// The flight computer (and its vessel) have no connection.
+ ///
+ /// The flight computer (and its vessel) have no connection.
+ ///
NoConnection = 8,
- /// The flight computer signal processor is not the vessel main signal processor (see ).
+ ///
+ /// The flight computer signal processor is not the vessel main signal processor (see ).
+ ///
NotMaster = 16,
}
- /// Gets whether or not it is possible to give input to the flight computer (and consequently, to the vessel).
+ ///
+ /// Gets whether or not it is possible to give input to the flight computer (and consequently, to the vessel).
+ ///
public bool InputAllowed
{
get
{
var satellite = RTCore.Instance.Network[SignalProcessor.VesselId];
- var connection = RTCore.Instance.Network[satellite];
- return (satellite != null && satellite.HasLocalControl) || (SignalProcessor.Powered && connection.Any());
+ return (satellite != null && satellite.HasLocalControl) || (SignalProcessor.Powered && RTCore.Instance.Network.IsConnected(satellite));
}
}
- /// Gets the delay applied to a flight computer (and hence, its vessel).
+ ///
+ /// Gets the delay applied to a flight computer (and hence, its vessel).
+ ///
public double Delay
{
get
@@ -68,70 +93,106 @@ public double Delay
if (satellite != null && satellite.HasLocalControl)
return 0.0;
- var connection = RTCore.Instance.Network[satellite];
- return !connection.Any() ? double.PositiveInfinity : connection.Min().Delay;
+ return RTCore.Instance.Network.ShortestDelay(satellite);
}
}
- /// Gets the current status of the flight computer.
+ ///
+ /// Gets the current status of the flight computer.
+ ///
public State Status
{
get
{
var satellite = RTCore.Instance.Network[SignalProcessor.VesselId];
- var connection = RTCore.Instance.Network[satellite];
var status = State.Normal;
if (!SignalProcessor.Powered) status |= State.OutOfPower;
if (!SignalProcessor.IsMaster) status |= State.NotMaster;
- if (!connection.Any()) status |= State.NoConnection;
+ if (!RTCore.Instance.Network.IsConnected(satellite)) status |= State.NoConnection;
if (Vessel.packed) status |= State.Packed;
return status;
}
}
- /// Returns true to keep the throttle on the current position without a connection, otherwise false.
+ ///
+ /// Returns true to keep the throttle on the current position without a connection, otherwise false.
+ ///
public bool KeepThrottleNoConnect => !RTSettings.Instance.ThrottleZeroOnNoConnection;
- /// Returns true to lock the throttle on the current position without a connection, otherwise false.
+ ///
+ /// Returns true to lock the throttle on the current position without a connection, otherwise false.
+ ///
public bool LockedThrottleNoConnect = false;
- /// Returns the last known position of throttle prior to connection loss.
+ ///
+ /// Returns the last known position of throttle prior to connection loss.
+ ///
public float LockedThrottlePositionNoConnect = 0f;
- /// Returns true to set the time wrap factor to 1 upon a connection reestablished, otherwise false
+ ///
+ /// Returns true to set the time wrap factor to 1 upon a connection reestablished, otherwise false
+ ///
public bool StopTimeWrapOnReconnect => RTSettings.Instance.StopTimeWrapOnReConnection;
- /// Returns true to indicate whether a connection loss occurs
+ ///
+ /// Returns true to indicate whether a connection loss occurs
+ ///
private bool TimeWrapConnectionLoss = false;
- /// Gets or sets the total delay which is the usual light speed delay + any manual delay.
+ ///
+ /// Gets or sets the total delay which is the usual light speed delay + any manual delay.
+ ///
public double TotalDelay { get; set; }
- /// The target () of a .
+ ///
+ /// The target () of a .
+ ///
public ITargetable DelayedTarget { get; set; }
- /// The last used by the Flight Computer.
+ ///
+ /// The last used by the Flight Computer.
+ ///
public TargetCommand LastTarget;
- /// The vessel owning this flight computer.
+ ///
+ /// The vessel owning this flight computer.
+ ///
public Vessel Vessel { get; private set; }
- /// The signal processor (; ) used by this flight computer.
+ ///
+ /// The signal processor (; ) used by this flight computer.
+ ///
public ISignalProcessor SignalProcessor { get; }
- /// List of autopilots for this flight computer. Used by external mods to add their own autopilots ( class).
+ ///
+ /// List of autopilots for this flight computer. Used by external mods to add their own autopilots ( class).
+ ///
public List> SanctionedPilots { get; }
- /// List of commands that are currently active (not queued).
+ ///
+ /// List of commands that are currently active (not queued).
+ ///
public IEnumerable ActiveCommands => _activeCommands.Values;
- /// List of queued commands in the flight computer.
+ ///
+ /// List of queued commands in the flight computer.
+ ///
public IEnumerable QueuedCommands => _commandQueue;
- /// Action triggered if the active command is aborted.
+ ///
+ /// Action triggered if the active command is aborted.
+ ///
public Action OnActiveCommandAbort;
- /// Action triggered if a new command popped to an active command.
+ ///
+ /// Action triggered if a new command popped to an active command.
+ ///
public Action OnNewCommandPop;
- /// Get the active Flight mode as an ().
+ ///
+ /// Get the active Flight mode as an ().
+ ///
public AttitudeCommand CurrentFlightMode => _activeCommands[0] as AttitudeCommand;
- /// Proportional Integral Derivative vessel controller.
+ ///
+ /// Proportional Integral Derivative vessel controller.
+ ///
public PIDController PIDController;
public static double PIDKp = 2.0, PIDKi = 0.8, PIDKd = 1.0;
public static readonly double RoverPIDKp = 1.0, RoverPIDKi = 0.0, RoverPIDKd = 0.0;
- /// The window of the flight computer.
+ ///
+ /// The window of the flight computer.
+ ///
public FlightComputerWindow Window
{
get
@@ -141,10 +202,14 @@ public FlightComputerWindow Window
}
}
- /// Computer able to pilot a rover (part of the flight computer).
+ ///
+ /// Computer able to pilot a rover (part of the flight computer).
+ ///
public RoverComputer RoverComputer { get; }
- /// Flight Computer constructor.
+ ///
+ /// Flight Computer constructor.
+ ///
/// A signal processor (most probably a instance.)
public FlightComputer(ISignalProcessor s)
{
@@ -180,7 +245,9 @@ public FlightComputer(ISignalProcessor s)
}
}
- /// Called when a game switch is requested: close the current computer.
+ ///
+ /// Called when a game switch is requested: close the current computer.
+ ///
/// data with from and to scenes.
private void OnSceneSwitchRequested(GameEvents.FromToAction data)
{
@@ -188,7 +255,9 @@ private void OnSceneSwitchRequested(GameEvents.FromToActionCalled when there's a vessel switch, switching from `fromVessel` to `toVessel`.
+ ///
+ /// Called when there's a vessel switch, switching from `fromVessel` to `toVessel`.
+ ///
/// The vessel we switch from.
/// The vessel we're switching to.
private void OnVesselSwitching(Vessel fromVessel, Vessel toVessel)
@@ -205,7 +274,9 @@ private void OnVesselSwitching(Vessel fromVessel, Vessel toVessel)
_flightComputerWindow?.Hide();
}
- /// After switching the vessel hide the current flight computer UI.
+ ///
+ /// After switching the vessel hide the current flight computer UI.
+ ///
/// The **new** vessel we are changing to.
public void OnVesselChange(Vessel vessel)
{
@@ -214,7 +285,9 @@ public void OnVesselChange(Vessel vessel)
_flightComputerWindow?.Hide();
}
- /// Called when the flight computer is disposed. This happens when the is destroyed.
+ ///
+ /// Called when the flight computer is disposed. This happens when the is destroyed.
+ ///
public void Dispose()
{
RTLog.Notify("FlightComputer: Dispose");
@@ -244,7 +317,9 @@ public void Dispose()
}
}
- /// Abort all active commands.
+ ///
+ /// Abort all active commands.
+ ///
public void Reset()
{
foreach (var cmd in _activeCommands.Values)
@@ -255,7 +330,9 @@ public void Reset()
OnActiveCommandAbort.Invoke();
}
- /// Enqueue a command in the flight computer command queue.
+ ///
+ /// Enqueue a command in the flight computer command queue.
+ ///
/// The command to be enqueued.
/// If true the command is not enqueued.
/// If true, the command is executed immediately, otherwise the light speed delay is applied.
@@ -280,7 +357,9 @@ public void Enqueue(ICommand cmd, bool ignoreControl = false, bool ignoreDelay =
}
}
- /// Remove a command from the flight computer command queue.
+ ///
+ /// Remove a command from the flight computer command queue.
+ ///
/// The command to be removed from the command queue.
public void Remove(ICommand cmd)
{
@@ -288,7 +367,9 @@ public void Remove(ICommand cmd)
if (_activeCommands.ContainsValue(cmd)) _activeCommands.Remove(cmd.Priority);
}
- /// Called by the method during the Update() "Game Logic" engine phase.
+ ///
+ /// Called by the method during the Update() "Game Logic" engine phase.
+ ///
/// This checks if there are any commands that can be removed from the FC queue if their delay has elapsed.
public void OnUpdate()
{
@@ -298,7 +379,9 @@ public void OnUpdate()
ExecuteConnectionStatusActions();
}
- /// Called by the method during the "Physics" engine phase.
+ ///
+ /// Called by the method during the "Physics" engine phase.
+ ///
public void OnFixedUpdate()
{
if (RTCore.Instance == null) return;
@@ -345,7 +428,9 @@ public void OnFixedUpdate()
}
}
- /// Updates the last target command used by the flight computer.
+ ///
+ /// Updates the last target command used by the flight computer.
+ ///
private void UpdateLastTarget()
{
int lastTargetIndex = _commandQueue.FindLastIndex(c => (c is TargetCommand));
@@ -364,7 +449,9 @@ private void UpdateLastTarget()
}
}
- /// Enqueue a to the flight control queue.
+ ///
+ /// Enqueue a to the flight control queue.
+ ///
/// The to be queued.
private void Enqueue(FlightCtrlState fs)
{
@@ -392,7 +479,9 @@ private void Enqueue(FlightCtrlState fs)
_flightCtrlQueue.Enqueue(dfs);
}
- /// Remove a from the flight control queue.
+ ///
+ /// Remove a from the flight control queue.
+ ///
/// The to be removed from the queue.
/// The satellite from which the should be removed.
private void PopFlightCtrl(FlightCtrlState fcs, ISatellite sat)
@@ -422,7 +511,9 @@ private void PopFlightCtrl(FlightCtrlState fcs, ISatellite sat)
fcs.CopyFrom(delayed);
}
- /// Check whether there are commands that can be removed from the flight computer command queue (when their delay time has elapsed).
+ ///
+ /// Check whether there are commands that can be removed from the flight computer command queue (when their delay time has elapsed).
+ ///
/// This is done during the Update() phase of the game engine. method.
private void PopCommand()
{
@@ -489,7 +580,9 @@ private void PopCommand()
}
}
- /// Control the flight. Called before the method.
+ ///
+ /// Control the flight. Called before the method.
+ ///
/// The input flight control state.
private void OnFlyByWirePre(FlightCtrlState fcs)
{
@@ -520,7 +613,9 @@ private void OnFlyByWirePre(FlightCtrlState fcs)
}
}
- /// Control the flight. Called after the method.
+ ///
+ /// Control the flight. Called after the method.
+ ///
/// The input flight control state.
private void OnFlyByWirePost(FlightCtrlState fcs)
{
@@ -545,7 +640,9 @@ private void OnFlyByWirePost(FlightCtrlState fcs)
}
}
- /// Orders the command queue to be chronological.
+ ///
+ /// Orders the command queue to be chronological.
+ ///
public void OrderCommandList()
{
if (_commandQueue.Count <= 0) return;
@@ -563,7 +660,9 @@ public void OrderCommandList()
}
}
- /// Restores the flight computer from the persistent save.
+ ///
+ /// Restores the flight computer from the persistent save.
+ ///
/// Node with the informations for the flight computer
public void Load(ConfigNode configNode)
{
@@ -663,7 +762,9 @@ public void Load(ConfigNode configNode)
UpdateLastTarget();
}
- /// Saves all values for the flight computer to the persistent.
+ ///
+ /// Saves all values for the flight computer to the persistent.
+ ///
/// Node to save in
public void Save(ConfigNode n)
{
@@ -699,7 +800,9 @@ public void Save(ConfigNode n)
n.AddNode(flightNode);
}
- /// Returns true if there's a least one on the queue.
+ ///
+ /// Returns true if there's a least one on the queue.
+ ///
public bool HasManeuverCommands()
{
if (_commandQueue.Count <= 0)
@@ -710,7 +813,9 @@ public bool HasManeuverCommands()
return maneuverFound != null;
}
- /// Looks for the passed on the command queue and returns true if the node is already on the list.
+ ///
+ /// Looks for the passed on the command queue and returns true if the node is already on the list.
+ ///
/// Node to search in the queued commands
public bool HasManeuverCommandByNode(ManeuverNode node)
{
@@ -722,7 +827,9 @@ public bool HasManeuverCommandByNode(ManeuverNode node)
return maneuverFound != null;
}
- /// Triggers a for the given
+ ///
+ /// Triggers a for the given
+ ///
/// Node to cancel from the queue
public void RemoveManeuverCommandByNode(ManeuverNode node)
{
@@ -749,7 +856,7 @@ private void ExecuteConnectionStatusActions()
//stop time wrap if re-connection occurred
if (StopTimeWrapOnReconnect && TimeWarp.CurrentRate > 1.0f)
{
- if (!RTCore.Instance.Satellites[SignalProcessor.VesselId].Connections.Any()) // no connection
+ if (!RTCore.Instance.Network.IsConnected(RTCore.Instance.Satellites[SignalProcessor.VesselId])) // no connection
{
TimeWrapConnectionLoss = true;
}
diff --git a/src/RemoteTech/FlightComputer/UIPartActionMenuPatcher.cs b/src/RemoteTech/FlightComputer/UIPartActionMenuPatcher.cs
index 283502442..24cc8f6e7 100644
--- a/src/RemoteTech/FlightComputer/UIPartActionMenuPatcher.cs
+++ b/src/RemoteTech/FlightComputer/UIPartActionMenuPatcher.cs
@@ -126,7 +126,9 @@ public WrappedField(BaseField baseField, KSPField field) : base(field, baseField
{
}
- /// Gets or sets the future field value.
+ ///
+ /// Gets or sets the future field value.
+ ///
public object NewValue { get; set; }
public Type NewValueType => FieldInfo.FieldType;
diff --git a/src/RemoteTech/ISatellite.cs b/src/RemoteTech/ISatellite.cs
index c492aa544..fe5956533 100644
--- a/src/RemoteTech/ISatellite.cs
+++ b/src/RemoteTech/ISatellite.cs
@@ -5,42 +5,91 @@
namespace RemoteTech
{
+ public struct SatelliteState
+ {
+ public Guid Guid;
+ public Guid Body;
+ public Vector3d Position;
+ public bool Powered;
+ public bool IsCommandStation;
+ public bool CanRelaySignal;
+ public bool IsInRadioBlackout;
+ }
+
public interface ISatellite
{
- /// Gets whether or not a satellite if visible in the Tracking station or the Flight Map view.
+ ///
+ /// Gets whether or not a satellite if visible in the Tracking station or the Flight Map view.
+ ///
bool Visible { get; }
- /// Gets or sets the name of the satellite.
+ ///
+ /// Gets or sets the name of the satellite.
+ ///
string Name { get; set; }
- /// Gets the satellite id.
+ ///
+ /// Gets the satellite id.
+ ///
Guid Guid { get; }
- /// Gets a double precision vector for the satellite's world space position.
+ ///
+ /// Gets a double precision vector for the satellite's world space position.
+ ///
Vector3d Position { get; }
- /// Gets the celestial body around which the satellite is orbiting.
+ ///
+ /// Gets the celestial body around which the satellite is orbiting.
+ ///
CelestialBody Body { get; }
- /// Gets the color of the ground station mark in Tracking station or Flight map view.
+ ///
+ /// Gets the color of the ground station mark in Tracking station or Flight map view.
+ ///
Color MarkColor { get; }
- /// Gets if the satellite is actually powered or not.
+ ///
+ /// Gets if the satellite is actually powered or not.
+ ///
bool Powered { get; }
- /// Gets if the satellite is a RemoteTech command station.
+ ///
+ /// Gets if the satellite is a RemoteTech command station.
+ ///
bool IsCommandStation { get; }
- /// Gets whether the satellite has local control or not (that is, if it is locally controlled or not).
+ ///
+ /// Gets whether the satellite has local control or not (that is, if it is locally controlled or not).
+ ///
bool HasLocalControl { get; }
- /// Indicates whether the ISatellite corresponds to a vessel
+ ///
+ /// Indicates whether the ISatellite corresponds to a vessel
+ ///
/// true if satellite is vessel or asteroid; otherwise (e.g. a ground station), false.
bool isVessel { get; }
- /// The vessel hosting the ISatellite, if one exists.
+ ///
+ /// The vessel hosting the ISatellite, if one exists.
+ ///
/// The vessel corresponding to this ISatellite. Returns null if !isVessel.
Vessel parentVessel { get; }
- /// Gets a list of antennas for this satellite.
- IEnumerable Antennas { get; }
- /// Called on connection refresh to update the connections.
+ ///
+ /// Gets a list of antennas for this satellite.
+ ///
+ IReadOnlyList Antennas { get; }
+ ///
+ /// Called on connection refresh to update the connections.
+ ///
/// List of network routes.
void OnConnectionRefresh(List> routes);
- /// Gets if the satellite is capable to forward other signals.
+ ///
+ /// Gets if the satellite is capable to forward other signals.
+ ///
bool CanRelaySignal { get; }
- /// Indicates whether the satellite is in radio blackout.
+ ///
+ /// Indicates whether the satellite is in radio blackout.
+ ///
bool IsInRadioBlackout { get; set; }
- /// Indicates whether the manual power override is engaged.
+ ///
+ /// Indicates whether the manual power override is engaged.
+ ///
bool PowerShutdownFlag { get; set; }
+
+ ///
+ /// Get all the state needed for a background update at once.
+ ///
+ ///
+ SatelliteState GetState();
}
}
diff --git a/src/RemoteTech/Modules/MissionControlAntenna.cs b/src/RemoteTech/Modules/MissionControlAntenna.cs
index e636be542..3cdb83e77 100644
--- a/src/RemoteTech/Modules/MissionControlAntenna.cs
+++ b/src/RemoteTech/Modules/MissionControlAntenna.cs
@@ -1,41 +1,72 @@
using System;
using System.Linq;
+using RemoteTech.SimpleTypes;
namespace RemoteTech.Modules
{
- public sealed class MissionControlAntenna : IAntenna
+ public sealed class MissionControlAntenna : IAntenna, IConfigNode
{
- [Persistent] public float Omni = 75000000;
- [Persistent] public float Dish = 0.0f;
- [Persistent] public double CosAngle = 1.0;
+ public float Omni = 75000000;
+ public float Dish = 0.0f;
+ public double CosAngle = 1.0;
///
/// Semicolon seperated list with omni ranges for each tech lvl of the tracking station
///
- [Persistent] public string UpgradeableOmni = String.Empty;
+ public string UpgradeableOmni = String.Empty;
///
/// Semicolon seperated list with dish ranges for each tech lvl of the tracking station
///
- [Persistent] public string UpgradeableDish = String.Empty;
+ public string UpgradeableDish = String.Empty;
///
/// Semicolon seperated list with CosAngle ranges for each tech lvl of the tracking station
///
- [Persistent] public string UpgradeableCosAngle = String.Empty;
+ public string UpgradeableCosAngle = String.Empty;
+
+ public void Load(ConfigNode node)
+ {
+ node.TryGetValue("Omni", ref Omni);
+ node.TryGetValue("Dish", ref Dish);
+ node.TryGetValue("CosAngle", ref CosAngle);
+ node.TryGetValue("UpgradeableOmni", ref UpgradeableOmni);
+ node.TryGetValue("UpgradeableDish", ref UpgradeableDish);
+ node.TryGetValue("UpgradeableCosAngle", ref UpgradeableCosAngle);
+ }
+
+ public void Save(ConfigNode node)
+ {
+ node.AddValue("Omni", Omni);
+ node.AddValue("Dish", Dish);
+ node.AddValue("CosAngle", CosAngle);
+ node.AddValue("UpgradeableOmni", UpgradeableOmni);
+ node.AddValue("UpgradeableDish", UpgradeableDish);
+ node.AddValue("UpgradeableCosAngle", UpgradeableCosAngle);
+ }
public ISatellite Parent { get; set; }
- float IAntenna.Omni { get { return Omni * MissionControlRangeMultiplier; } }
- Guid IAntenna.Guid { get { return Parent.Guid; } }
- String IAntenna.Name { get { return "Dummy Antenna"; } }
- bool IAntenna.Powered { get { return true; } }
- public bool Connected { get { return RTCore.Instance.Network.Graph [((IAntenna)this).Guid].Any (l => l.Interfaces.Contains (this)); } }
- bool IAntenna.Activated { get { return true; } set { return; } }
- float IAntenna.Consumption { get { return 0.0f; } }
- bool IAntenna.CanTarget { get { return false; } }
- Guid IAntenna.Target { get { return new Guid(RTSettings.Instance.ActiveVesselGuid); } set { return; } }
- float IAntenna.Dish { get { return Dish * MissionControlRangeMultiplier; } }
- double IAntenna.CosAngle { get { return CosAngle; } }
- private float MissionControlRangeMultiplier { get { return RTSettings.Instance.MissionControlRangeMultiplier; } }
+ private readonly AntennaState mState = new();
+
+ float IAntenna.Omni => Omni * MissionControlRangeMultiplier;
+ Guid IAntenna.Guid => Parent.Guid;
+ string IAntenna.Name => "Dummy Antenna";
+ bool IAntenna.Powered => true;
+ public bool Connected => RTCore.Instance.Network.IsAntennaConnected(this);
+ bool IAntenna.Activated
+ {
+ get => true;
+ set { }
+ }
+ float IAntenna.Consumption => 0.0f;
+ bool IAntenna.CanTarget => false;
+ Guid IAntenna.Target
+ {
+ get => RTSettings.Instance.ActiveVesselGuidParsed;
+ set { }
+ }
+ float IAntenna.Dish => Dish * MissionControlRangeMultiplier;
+ double IAntenna.CosAngle => CosAngle;
+ private float MissionControlRangeMultiplier => RTSettings.Instance.MissionControlRangeMultiplier;
public void reloadUpgradeableAntennas(int techlvl = 0)
{
@@ -93,6 +124,31 @@ public void reloadUpgradeableAntennas(int techlvl = 0)
}
}
+ internal void RegisterState()
+ {
+ UpdateState();
+ mState.Register();
+ }
+
+ internal void UnregisterState() => mState.Dispose();
+
+ internal void UpdateState()
+ {
+ IAntenna antenna = this;
+ mState.Guid = antenna.Guid;
+ mState.Target = antenna.Target;
+ mState.Activated = antenna.Activated;
+ mState.Powered = antenna.Powered;
+ mState.Connected = RTCore.Instance != null
+ && RTCore.Instance.Network != null
+ && RTCore.Instance.Network.IsAntennaConnected(this);
+ mState.CanTarget = antenna.CanTarget;
+ mState.Dish = antenna.Dish;
+ mState.CosAngle = antenna.CosAngle;
+ mState.Omni = antenna.Omni;
+ mState.Consumption = antenna.Consumption;
+ }
+
public void OnConnectionRefresh() { }
public int CompareTo(IAntenna antenna)
diff --git a/src/RemoteTech/Modules/ModuleRTAntenna.cs b/src/RemoteTech/Modules/ModuleRTAntenna.cs
index 41b5a2fa4..e7500ea3e 100644
--- a/src/RemoteTech/Modules/ModuleRTAntenna.cs
+++ b/src/RemoteTech/Modules/ModuleRTAntenna.cs
@@ -4,6 +4,7 @@
using System.Linq;
using System.Reflection;
using System.Text;
+using RemoteTech.SimpleTypes;
using RemoteTech.UI;
using UnityEngine;
using KSP.Localization;
@@ -11,15 +12,17 @@
namespace RemoteTech.Modules
{
- /// This module represents a part that can receive control transmissions from another vessel or a ground station.
+ ///
+ /// This module represents a part that can receive control transmissions from another vessel or a ground station.
+ ///
/// You must remove any modules from the antenna if using .
[KSPModule("#RT_Editor_Antenna")]//Antenna
public class ModuleRTAntenna : PartModule, IAntenna, IContractObjectiveModule, IResourceConsumer
{
public String Name { get { return part.partInfo.title; } }
- public Guid Guid { get { return mRegisteredId; } }
+ public Guid Guid { get { return mState.Guid; } }
public bool Powered { get { return IsRTPowered; } }
- public bool Connected { get { return (RTCore.Instance != null && RTCore.Instance.Network.Graph [Guid].Any (l => l.Interfaces.Contains (this))); } }
+ public bool Connected { get { return RTCore.Instance != null && RTCore.Instance.Network.IsAntennaConnected(this); } }
public bool Activated { get { return IsRTActive; } set { SetState(value); } }
public bool CanAnimate { get { return mDeployFxModules.Count > 0; } }
public bool AnimClosed { get { return mDeployFxModules.Any(fx => fx.GetScalar <= 0.1f ); } }
@@ -30,12 +33,12 @@ public class ModuleRTAntenna : PartModule, IAntenna, IContractObjectiveModule, I
public Guid Target
{
- get { return RTAntennaTarget; }
+ get => RTAntennaTarget;
set
{
RTAntennaTarget = value;
Events["EventTarget"].guiName = RTUtil.TargetName(Target);
- foreach (UIPartActionWindow w in GameObject.FindObjectsOfType(typeof(UIPartActionWindow)).Where(w => ((UIPartActionWindow) w).part == part))
+ foreach (UIPartActionWindow w in GameObject.FindObjectsOfType(typeof(UIPartActionWindow)).Where(w => ((UIPartActionWindow)w).part == part))
{
w.displayDirty = true;
}
@@ -94,21 +97,19 @@ public float
[KSPField(isPersistant = true)]
public bool
IsRTAntenna = true,
- IsRTActive = false,
- IsRTPowered = false,
IsRTBroken = false,
IsNonRetractable = false;
- [KSPField(isPersistant = true)]
- public double RTDishCosAngle = 1.0f;
+ public bool IsRTActive { get => mState.Activated; set => mState.Activated = value; }
+ public bool IsRTPowered { get => mState.Powered; set => mState.Powered = value; }
+ public double RTDishCosAngle { get => mState.CosAngle; set => mState.CosAngle = value; }
[KSPField(isPersistant = true)]
public float
RTOmniRange = 0.0f,
RTDishRange = 0.0f;
- [KSPField] // Persistence handled by Save()
- public Guid RTAntennaTarget = Guid.Empty;
+ public Guid RTAntennaTarget { get => mState.Target; set => mState.Target = value; }
[KSPField(guiName = "#RT_ModuleUI_Autothreshold")]//Auto threshold
public String GUI_DeReactivation_Status = Localizer.Format("#RT_ModuleUI_Autothreshold_Off");//"Off"
@@ -143,6 +144,7 @@ public float
private bool isPartActionUIOpened;
private UI_FloatRange deactivatePowerThresholdFloatRange;
private UI_FloatRange activatePowerThresholdFloatRange;
+ private readonly AntennaState mState = new();
private enum State
{
@@ -153,7 +155,7 @@ private enum State
Malfunction,
}
- private Guid mRegisteredId;
+ private Guid mRegisteredId { get => mState.Guid; set => mState.Guid = value; }
public override string GetInfo()
{
@@ -251,7 +253,8 @@ public virtual void SetState(bool state)
if (RTCore.Instance != null)
{
var satellite = RTCore.Instance.Network[Guid];
- bool route_home = RTCore.Instance.Network[satellite].Any(r => r.Links[0].Interfaces.Contains(this) && RTCore.Instance.Network.GroundStations.ContainsKey(r.Goal.Guid));
+ bool route_home = (Omni > 0 || Dish > 0)
+ && RTCore.Instance.Network[satellite].Any(r => RTCore.Instance.Network.GroundStations.ContainsKey(r.Goal.Guid));
if (mTransmitter == null && route_home)
{
AddTransmitter();
@@ -374,6 +377,19 @@ public void OnConnectionRefresh()
public override void OnLoad(ConfigNode node)
{
base.OnLoad(node);
+
+ bool isRTActive = IsRTActive;
+ node.TryGetValue("IsRTActive", ref isRTActive);
+ IsRTActive = isRTActive;
+
+ bool isRTPowered = IsRTPowered;
+ node.TryGetValue("IsRTPowered", ref isRTPowered);
+ IsRTPowered = isRTPowered;
+
+ double rtDishCosAngle = RTDishCosAngle;
+ node.TryGetValue("RTDishCosAngle", ref rtDishCosAngle);
+ RTDishCosAngle = rtDishCosAngle;
+
if (node.HasValue("RTAntennaTarget"))
{
try
@@ -444,6 +460,10 @@ public override void OnSave(ConfigNode node)
{
node.AddValue("RTAntennaTarget", RTAntennaTarget.ToString());
}
+
+ node.SetValue("IsRTActive", IsRTActive, createIfNotFound: true);
+ node.SetValue("IsRTPowered", IsRTPowered, createIfNotFound: true);
+ node.SetValue("RTDishCosAngle", RTDishCosAngle, createIfNotFound: true);
}
public override void OnAwake()
@@ -465,6 +485,10 @@ public override void OnAwake()
public override void OnStart(StartState state)
{
+ // Registered here rather than OnAwake: OnAwake also fires on part
+ // prefabs during compilation, whose state is invalid to register.
+ mState.Register();
+
Actions["ActionOpen"].guiName = ActionMode1Name;
Actions["ActionOpen"].active = !IsRTBroken;
Actions["ActionClose"].guiName = ActionMode0Name;
@@ -618,6 +642,12 @@ private void FixedUpdate()
HandleDynamicPressure();
UpdateContext();
ValidateAntennaThresholds();
+
+ mState.Dish = Dish;
+ mState.Omni = Omni;
+ mState.Consumption = Consumption;
+ mState.CanTarget = CanTarget;
+ mState.Connected = Connected;
}
private void UpdateContext()
@@ -765,6 +795,7 @@ private IEnumerator SetFXModules_Coroutine(List modules, float tg
private void OnDestroy()
{
RTLog.Notify("ModuleRTAntenna: OnDestroy");
+ mState.Dispose();
GameEvents.onVesselWasModified.Remove(OnVesselModified);
GameEvents.onPartUndock.Remove(OnPartUndock);
if (RTCore.Instance != null && mRegisteredId != Guid.Empty)
diff --git a/src/RemoteTech/Modules/ModuleRTAntennaPassive.cs b/src/RemoteTech/Modules/ModuleRTAntennaPassive.cs
index 7e240d301..d754dc88d 100644
--- a/src/RemoteTech/Modules/ModuleRTAntennaPassive.cs
+++ b/src/RemoteTech/Modules/ModuleRTAntennaPassive.cs
@@ -3,6 +3,7 @@
using System.Linq;
using System.Text;
using KSP.Localization;
+using RemoteTech.SimpleTypes;
namespace RemoteTech.Modules
{
@@ -15,7 +16,7 @@ public class ModuleRTAntennaPassive : PartModule, IAntenna
public String Name { get { return part.partInfo.title; } }
public Guid Guid { get { return vessel.id; } }
public bool Powered { get { return Activated; } }
- public bool Connected { get { return (RTCore.Instance != null && RTCore.Instance.Network.Graph [Guid].Any (l => l.Interfaces.Contains (this))); } }
+ public bool Connected { get { return RTCore.Instance != null && RTCore.Instance.Network.IsAntennaConnected(this); } }
public bool Activated { get { return Unlocked; } set { return; } }
public bool Animating { get { return false; } }
@@ -50,12 +51,11 @@ public float
[KSPField(isPersistant = true)]
public bool
IsRTAntenna = true,
- IsRTActive = true,
- IsRTPowered = false,
IsRTBroken = false;
- [KSPField(isPersistant = true)]
- public double RTDishCosAngle = 1.0f;
+ public bool IsRTActive { get => mState.Activated; set => mState.Activated = value; }
+ public bool IsRTPowered { get => mState.Powered; set => mState.Powered = value; }
+ public double RTDishCosAngle { get => mState.CosAngle; set => mState.CosAngle = value; }
[KSPField(isPersistant = true)]
public float
@@ -75,8 +75,9 @@ public float
public int[] mDeployFxModuleIndices, mProgressFxModuleIndices;
public ConfigNode mTransmitterConfig;
private IScienceDataTransmitter mTransmitter;
+ private readonly AntennaState mState = new() { Activated = true };
- private Guid mRegisteredId;
+ private Guid mRegisteredId { get => mState.Guid; set => mState.Guid = value; }
public override string GetInfo()
{
@@ -95,7 +96,8 @@ public virtual void SetState(bool state)
if(RTCore.Instance != null)
{
var satellite = RTCore.Instance.Network[Guid];
- bool route_home = RTCore.Instance.Network[satellite].Any(r => r.Links[0].Interfaces.Contains(this) && RTCore.Instance.Network.GroundStations.ContainsKey(r.Goal.Guid));
+ bool route_home = (Omni > 0 || Dish > 0)
+ && RTCore.Instance.Network[satellite].Any(r => RTCore.Instance.Network.GroundStations.ContainsKey(r.Goal.Guid));
if (mTransmitter == null && route_home)
{
AddTransmitter();
@@ -115,6 +117,19 @@ public void OnConnectionRefresh()
public override void OnLoad(ConfigNode node)
{
base.OnLoad(node);
+
+ bool isRTActive = IsRTActive;
+ node.TryGetValue("IsRTActive", ref isRTActive);
+ IsRTActive = isRTActive;
+
+ bool isRTPowered = IsRTPowered;
+ node.TryGetValue("IsRTPowered", ref isRTPowered);
+ IsRTPowered = isRTPowered;
+
+ double rtDishCosAngle = RTDishCosAngle;
+ node.TryGetValue("RTDishCosAngle", ref rtDishCosAngle);
+ RTDishCosAngle = rtDishCosAngle;
+
if (node.HasNode("TRANSMITTER"))
{
RTLog.Notify("ModuleRTAntennaPassive: Found TRANSMITTER block.");
@@ -133,8 +148,21 @@ public override void OnLoad(ConfigNode node)
}
}
+ public override void OnSave(ConfigNode node)
+ {
+ base.OnSave(node);
+
+ node.SetValue("IsRTActive", IsRTActive, createIfNotFound: true);
+ node.SetValue("IsRTPowered", IsRTPowered, createIfNotFound: true);
+ node.SetValue("RTDishCosAngle", RTDishCosAngle, createIfNotFound: true);
+ }
+
public override void OnStart(StartState state)
{
+ // Registered here rather than OnAwake: OnAwake also fires on part
+ // prefabs during compilation, whose state is invalid to register.
+ mState.Register();
+
// workarround for ksp 1.0
if (mTransmitterConfig == null)
{
@@ -162,6 +190,12 @@ private void FixedUpdate()
RTDishRange = Dish;
IsRTPowered = Powered;
Fields["GUI_OmniRange"].guiActive = Activated && ShowGUI_OmniRange;
+
+ mState.Dish = Dish;
+ mState.Omni = Omni;
+ mState.Consumption = Consumption;
+ mState.CanTarget = CanTarget;
+ mState.Connected = Connected;
}
private void AddTransmitter()
@@ -218,6 +252,7 @@ private List FindFxModules(int[] indices, bool showUI)
private void OnDestroy()
{
RTLog.Notify("ModuleRTAntennaPassive: OnDestroy");
+ mState.Dispose();
GameEvents.onVesselWasModified.Remove(OnVesselModified);
GameEvents.onPartUndock.Remove(OnPartUndock);
if (RTCore.Instance != null && mRegisteredId != Guid.Empty)
diff --git a/src/RemoteTech/Modules/ModuleRTDataTransmitter.cs b/src/RemoteTech/Modules/ModuleRTDataTransmitter.cs
index 819a42e06..44c3a5fa0 100644
--- a/src/RemoteTech/Modules/ModuleRTDataTransmitter.cs
+++ b/src/RemoteTech/Modules/ModuleRTDataTransmitter.cs
@@ -7,7 +7,9 @@
namespace RemoteTech.Modules
{
- /// Used to transmit science from a vessel with an antenna.
+ ///
+ /// Used to transmit science from a vessel with an antenna.
+ ///
public sealed class ModuleRTDataTransmitter : PartModule, IScienceDataTransmitter
{
//Default parameters unless loaded from antenna configuration
diff --git a/src/RemoteTech/Modules/ModuleSPU.cs b/src/RemoteTech/Modules/ModuleSPU.cs
index 9b51e9a02..89524344c 100644
--- a/src/RemoteTech/Modules/ModuleSPU.cs
+++ b/src/RemoteTech/Modules/ModuleSPU.cs
@@ -129,7 +129,7 @@ private State UpdateControlState()
}
}
- if (Satellite == null || !RTCore.Instance.Network[Satellite].Any())
+ if (Satellite == null || !RTCore.Instance.Network.IsConnected(Satellite))
{
return State.NoConnection;
}
diff --git a/src/RemoteTech/Modules/ProtoAntenna.cs b/src/RemoteTech/Modules/ProtoAntenna.cs
index 033cc7dea..c55087d5f 100644
--- a/src/RemoteTech/Modules/ProtoAntenna.cs
+++ b/src/RemoteTech/Modules/ProtoAntenna.cs
@@ -1,25 +1,26 @@
-using System;
+using System;
using System.Linq;
+using RemoteTech.SimpleTypes;
namespace RemoteTech.Modules
{
- internal class ProtoAntenna : IAntenna
+ internal class ProtoAntenna : IAntenna, IDisposable
{
public String Name { get; private set; }
- public Guid Guid { get; private set; }
- public bool Powered { get; private set; }
- public bool Activated { get; set; }
- public bool Connected { get { return RTCore.Instance.Network.Graph [Guid].Any (l => l.Interfaces.Contains (this)); } }
- public float Consumption { get; private set; }
+ public Guid Guid { get => mState.Guid; private set => mState.Guid = value; }
+ public bool Powered { get => mState.Powered; private set => mState.Powered = value; }
+ public bool Activated { get => mState.Activated; set => mState.Activated = value; }
+ public bool Connected { get { return RTCore.Instance.Network.IsAntennaConnected(this); } }
+ public float Consumption { get => mState.Consumption; private set => mState.Consumption = value; }
public bool CanTarget { get { return Dish != -1; } }
public Guid Target
{
- get { return mDishTarget; }
+ get { return mState.Target; }
set
{
- mDishTarget = value;
+ mState.Target = value;
if (mProtoModule != null)
{
mProtoModule.moduleValues.SetValue("RTAntennaTarget", value.ToString());
@@ -33,14 +34,13 @@ public Guid Target
}
}
- public float Dish { get; private set; }
- public double CosAngle { get; private set; }
- public float Omni { get; private set; }
+ public float Dish { get => mState.Dish; private set => mState.Dish = value; }
+ public double CosAngle { get => mState.CosAngle; private set => mState.CosAngle = value; }
+ public float Omni { get => mState.Omni; private set => mState.Omni = value; }
private readonly ProtoPartSnapshot mProtoPart;
private readonly ProtoPartModuleSnapshot mProtoModule;
-
- private Guid mDishTarget;
+ private readonly AntennaState mState = new();
public ProtoAntenna(Vessel v, ProtoPartSnapshot p, ProtoPartModuleSnapshot ppms)
{
@@ -49,22 +49,38 @@ public ProtoAntenna(Vessel v, ProtoPartSnapshot p, ProtoPartModuleSnapshot ppms)
Guid = v.id;
mProtoPart = p;
mProtoModule = ppms;
+
try
{
- mDishTarget = new Guid(ppms.moduleValues.GetValue("RTAntennaTarget"));
+ mState.Target = new Guid(ppms.moduleValues.GetValue("RTAntennaTarget"));
}
catch (Exception ex) when (ex is ArgumentNullException || ex is FormatException || ex is OverflowException)
{
- mDishTarget = Guid.Empty;
+ mState.Target = Guid.Empty;
}
- double temp_double;
- float temp_float;
- bool temp_bool;
- Dish = Single.TryParse(ppms.moduleValues.GetValue("RTDishRange"), out temp_float) ? temp_float : 0.0f;
- CosAngle = Double.TryParse(ppms.moduleValues.GetValue("RTDishCosAngle"), out temp_double) ? temp_double : 0.0;
- Omni = Single.TryParse(ppms.moduleValues.GetValue("RTOmniRange"), out temp_float) ? temp_float : 0.0f;
- Powered = Boolean.TryParse(ppms.moduleValues.GetValue("IsRTPowered"), out temp_bool) ? temp_bool : false;
- Activated = Boolean.TryParse(ppms.moduleValues.GetValue("IsRTActive"), out temp_bool) ? temp_bool : false;
+
+ float dish = 0f;
+ if (ppms.moduleValues.TryGetValue("RTDishRange", ref dish))
+ Dish = dish;
+
+ double cosAngle = 0f;
+ if (ppms.moduleValues.TryGetValue("RTDishCosAngle", ref cosAngle))
+ CosAngle = cosAngle;
+
+ float omni = 0f;
+ if (ppms.moduleValues.TryGetValue("RTOmniRange", ref omni))
+ Omni = omni;
+
+ bool powered = false;
+ if (ppms.moduleValues.TryGetValue("IsRTPowered", ref powered))
+ Powered = powered;
+
+ bool activated = false;
+ if (ppms.moduleValues.TryGetValue("IsRTActive", ref activated))
+ Activated = activated;
+
+ mState.CanTarget = Dish != -1;
+ mState.Register();
RTLog.Notify(ToString());
}
@@ -79,11 +95,18 @@ public ProtoAntenna(String name, Guid guid, float omni)
CosAngle = 1.0f;
Activated = true;
Powered = true;
+ mState.CanTarget = CanTarget;
+ mState.Register();
+ }
+
+ public void Dispose()
+ {
+ mState.Dispose();
}
public void OnConnectionRefresh()
{
- ;
+ mState.Connected = Connected;
}
public int CompareTo(IAntenna antenna)
@@ -96,4 +119,4 @@ public override string ToString()
return String.Format("ProtoAntenna(Name: {0}, Guid: {1}, Dish: {2}, Omni: {3}, Target: {4}, CosAngle: {5})", Name, Guid, Dish, Omni, Target, CosAngle);
}
}
-}
\ No newline at end of file
+}
diff --git a/src/RemoteTech/Network/CelestialBodyInfo.cs b/src/RemoteTech/Network/CelestialBodyInfo.cs
new file mode 100644
index 000000000..5a903ee7f
--- /dev/null
+++ b/src/RemoteTech/Network/CelestialBodyInfo.cs
@@ -0,0 +1,13 @@
+using RemoteTech.Collections;
+using Unity.Mathematics;
+
+namespace RemoteTech.Network;
+
+internal struct JobBody
+{
+ public double3 position;
+ public double radius;
+
+ public int parent;
+ public IntRange subtree;
+}
diff --git a/src/RemoteTech/Network/ConeMeshJob.cs b/src/RemoteTech/Network/ConeMeshJob.cs
new file mode 100644
index 000000000..c916152bd
--- /dev/null
+++ b/src/RemoteTech/Network/ConeMeshJob.cs
@@ -0,0 +1,135 @@
+using Unity.Burst;
+using Unity.Collections;
+using Unity.Jobs;
+using Unity.Mathematics;
+using UnityEngine;
+
+namespace RemoteTech.Network;
+
+///
+/// A cone-eligible antenna, gathered once per tick by
+/// ; target uses the same
+/// encoding as .
+///
+internal struct ConeCandidate
+{
+ public int nodeIndex;
+ public int target;
+ public double cosAngle;
+ public double dishRange;
+}
+
+///
+/// Camera/ScaledSpace snapshot for ; refUp is the camera target body's up axis used for the cone's side-spread.
+///
+internal struct ConeViewParams
+{
+ public double invScale;
+ public double3 totalOffset;
+ public float3 refUp;
+ public float4x4 view, proj, invView, invProj;
+ public float pixelWidth, pixelHeight, halfWidth;
+ public int mode;
+ public Color32 color;
+}
+
+///
+/// Builds a pair of billboard quads (the cone's two side lines) per candidate into the mesh buffers.
+///
+[BurstCompile]
+internal struct ConeMeshJob : IJobParallelFor
+{
+ [ReadOnly] public NativeArray candidates;
+ [ReadOnly] public NativeArray nodes;
+ [ReadOnly] public NativeArray bodies;
+ public ConeViewParams p;
+
+ [WriteOnly, NativeDisableParallelForRestriction] public NativeArray verts;
+ [WriteOnly, NativeDisableParallelForRestriction] public NativeArray indices;
+
+ public void Execute(int i)
+ {
+ ConeCandidate c = candidates[i];
+ double3 antennaLocal = nodes[c.nodeIndex].position;
+ double3 targetLocal = c.target > 0 ? nodes[c.target - 1].position : bodies[-c.target - 1].position;
+
+ float3 antenna = LineMeshCore.ToScaled(antennaLocal, p.invScale, p.totalOffset);
+ float3 target = LineMeshCore.ToScaled(targetLocal, p.invScale, p.totalOffset);
+
+ float dist = math.distance(antenna, target);
+ float spread = dist * (float)math.tan(math.acos(c.cosAngle));
+ float3 space = math.normalizesafe(math.cross(target - antenna, p.refUp)) * spread;
+
+ float lim = math.min((float)(c.dishRange * p.invScale), dist);
+ float3 end1 = antenna + math.normalizesafe(target + space - antenna) * lim;
+ float3 end2 = antenna + math.normalizesafe(target - space - antenna) * lim;
+
+ float3 a = LineMeshCore.WorldToScreen(antenna, p.view, p.proj, p.pixelWidth, p.pixelHeight);
+ float3 e1 = LineMeshCore.WorldToScreen(end1, p.view, p.proj, p.pixelWidth, p.pixelHeight);
+ float3 e2 = LineMeshCore.WorldToScreen(end2, p.view, p.proj, p.pixelWidth, p.pixelHeight);
+
+ if (p.mode != 0) // 2D flat overlay: clamp to a near plane, flip behind-camera endpoints
+ {
+ if (a.z < 0f)
+ {
+ float3 coneCenter = LineMeshCore.WorldToScreen(target, p.view, p.proj, p.pixelWidth, p.pixelHeight);
+ a = LineMeshCore.FlipDirection(a, coneCenter);
+ }
+ else if (e1.z < 0f || e2.z < 0f)
+ {
+ e1 = LineMeshCore.FlipDirection(e1, a);
+ e2 = LineMeshCore.FlipDirection(e2, a);
+ }
+
+ float d = p.pixelHeight * 0.5f + 0.01f;
+ a.z = a.z >= 0f ? d : -d;
+ e1.z = e1.z >= 0f ? d : -d;
+ e2.z = e2.z >= 0f ? d : -d;
+ }
+
+ float2 dir1 = new(e1.y - a.y, a.x - e1.x);
+ float2 dir2 = new(e2.y - a.y, a.x - e2.x);
+ float3 seg1 = new(math.normalizesafe(dir1) * p.halfWidth, 0f);
+ float3 seg2 = new(math.normalizesafe(dir2) * p.halfWidth, 0f);
+
+ int vb = 8 * i;
+ verts[vb + 0] = MakeVertex(a - seg1, p, new float2(0f, 1f));
+ verts[vb + 1] = MakeVertex(a + seg1, p, new float2(0f, 0f));
+ verts[vb + 2] = MakeVertex(e1 - seg1, p, new float2(1f, 1f));
+ verts[vb + 3] = MakeVertex(e1 + seg1, p, new float2(1f, 0f));
+ verts[vb + 4] = MakeVertex(a - seg2, p, new float2(0f, 1f));
+ verts[vb + 5] = MakeVertex(a + seg2, p, new float2(0f, 0f));
+ verts[vb + 6] = MakeVertex(e2 - seg2, p, new float2(1f, 1f));
+ verts[vb + 7] = MakeVertex(e2 + seg2, p, new float2(1f, 0f));
+
+ int ib = 12 * i;
+ uint v = (uint)vb;
+ indices[ib + 0] = v + 0; indices[ib + 1] = v + 2; indices[ib + 2] = v + 1;
+ indices[ib + 3] = v + 2; indices[ib + 4] = v + 3; indices[ib + 5] = v + 1;
+ indices[ib + 6] = v + 4; indices[ib + 7] = v + 6; indices[ib + 8] = v + 5;
+ indices[ib + 9] = v + 6; indices[ib + 10] = v + 7; indices[ib + 11] = v + 5;
+ }
+
+ private static LineVertex MakeVertex(float3 screen, in ConeViewParams p, float2 uv) => new LineVertex
+ {
+ position = LineMeshCore.ScreenToWorld(screen, p.proj, p.invProj, p.invView, p.pixelWidth, p.pixelHeight),
+ color = p.color,
+ uv = uv,
+ };
+}
+
+internal struct ConeMeshData
+{
+ public NativeArray verts;
+ public NativeArray indices;
+ public NativeArray bounds;
+ public JobHandle handle;
+ public double3 builtOffset;
+
+ public void Dispose()
+ {
+ verts.Dispose();
+ indices.Dispose();
+ bounds.Dispose();
+ }
+}
diff --git a/src/RemoteTech/Network/DrawableMesh.cs b/src/RemoteTech/Network/DrawableMesh.cs
new file mode 100644
index 000000000..9ed96da09
--- /dev/null
+++ b/src/RemoteTech/Network/DrawableMesh.cs
@@ -0,0 +1,203 @@
+using Unity.Collections;
+using Unity.Mathematics;
+using UnityEngine;
+using UnityEngine.Rendering;
+
+namespace RemoteTech.Network;
+
+///
+/// A persistent dynamic mesh plus the buffer sizes it was last declared with, so
+/// vertex/index buffer params are only re-set when the counts actually change.
+///
+internal struct DrawableMesh
+{
+ private const MeshUpdateFlags MeshFlags = MeshUpdateFlags.DontValidateIndices | MeshUpdateFlags.DontRecalculateBounds;
+
+ // Orbit lines draw on layer 10, we do the same here in order to match appropriately.
+ private const int ScaledSceneryLayer = 10;
+
+ public Mesh mesh;
+ private int lastVc, lastIc;
+
+ public void Ensure(string name)
+ {
+ if (mesh != null) return;
+ mesh = new Mesh { name = name };
+ mesh.MarkDynamic();
+ }
+
+ public void Upload(
+ NativeArray verts,
+ NativeArray indices,
+ in Bounds3 b,
+ VertexAttributeDescriptor[] layout)
+ {
+ int vc = verts.Length, ic = indices.Length;
+
+ if (vc != lastVc) { mesh.SetVertexBufferParams(vc, layout); lastVc = vc; }
+ mesh.SetVertexBufferData(verts, 0, 0, vc, 0, MeshFlags);
+ if (ic != lastIc) { mesh.SetIndexBufferParams(ic, IndexFormat.UInt32); lastIc = ic; }
+ mesh.SetIndexBufferData(indices, 0, 0, ic, MeshFlags);
+ mesh.subMeshCount = 1;
+ mesh.SetSubMesh(0, new SubMeshDescriptor(0, ic, MeshTopology.Triangles), MeshUpdateFlags.DontRecalculateBounds);
+
+ float3 center = (b.min + b.max) * 0.5f, size = b.max - b.min;
+ mesh.bounds = new Bounds(new Vector3(center.x, center.y, center.z), new Vector3(size.x, size.y, size.z));
+ }
+
+ public void Draw(Material material, Camera camera, Matrix4x4 model)
+ => Graphics.DrawMesh(mesh, model, material, ScaledSceneryLayer, camera, 0, null, false, false, false);
+
+ public void Destroy()
+ {
+ if (mesh == null) return;
+ Object.Destroy(mesh);
+ mesh = null;
+ }
+}
+
+///
+/// The map-view connection-line mesh: its persistent GPU buffers together with
+/// this tick's pending build, scheduled in LateUpdate and drawn in OnPreCull.
+///
+internal struct LineMesh
+{
+ public DrawableMesh drawable;
+ public LineMeshData frame;
+ public bool hasFrame;
+
+ public void Ensure() => drawable.Ensure("RTNetworkLines");
+
+ public void SetFrame(in LineMeshData f)
+ {
+ frame = f;
+ hasFrame = true;
+ }
+
+ ///
+ /// Waits on the build, then uploads and draws it translated by
+ /// onto the current ScaledSpace origin (the build was billboarded against a stale one).
+ ///
+ public void CompleteAndDraw(VertexAttributeDescriptor[] layout, Material material, Camera camera, float3 delta)
+ {
+ frame.handle.Complete();
+ if (frame.verts.Length != 0 && frame.indices.Length != 0)
+ {
+ drawable.Upload(frame.verts.AsArray(), frame.indices.AsArray(), frame.bounds[0], layout);
+ drawable.Draw(material, camera, Matrix4x4.Translate(delta));
+ }
+ DisposeFrame();
+ }
+
+ public void Drop()
+ {
+ if (!hasFrame) return;
+ frame.handle.Complete();
+ DisposeFrame();
+ }
+
+ private void DisposeFrame()
+ {
+ frame.Dispose();
+ hasFrame = false;
+ }
+
+ public void Destroy()
+ {
+ Drop();
+ drawable.Destroy();
+ }
+}
+
+///
+/// The map-view dish-cone mesh, mirroring : persistent GPU
+/// buffers plus this tick's pending build.
+///
+internal struct ConeMesh
+{
+ public DrawableMesh drawable;
+ public ConeMeshData frame;
+ public bool hasFrame;
+
+ public void Ensure() => drawable.Ensure("RTNetworkCones");
+
+ public void SetFrame(in ConeMeshData f)
+ {
+ frame = f;
+ hasFrame = true;
+ }
+
+ public void CompleteAndDraw(VertexAttributeDescriptor[] layout, Material material, Camera camera, float3 delta)
+ {
+ frame.handle.Complete();
+ if (frame.verts.Length != 0 && frame.indices.Length != 0)
+ {
+ drawable.Upload(frame.verts, frame.indices, frame.bounds[0], layout);
+ drawable.Draw(material, camera, Matrix4x4.Translate(delta));
+ }
+ DisposeFrame();
+ }
+
+ public void Drop()
+ {
+ if (!hasFrame)
+ return;
+ frame.handle.Complete();
+ DisposeFrame();
+ }
+
+ private void DisposeFrame()
+ {
+ frame.Dispose();
+ hasFrame = false;
+ }
+
+ public void Destroy()
+ {
+ Drop();
+ drawable.Destroy();
+ }
+}
+
+///
+/// The map-view satellite marks, mirroring : this tick's
+/// pending filter/projection build plus the state that produced it (needed to
+/// resolve a mark back to its satellite for the mouse-over panel). Unlike the
+/// meshes these are drawn as GUI textures in OnGUI, so the frame is harvested
+/// there rather than in OnPreCull.
+///
+internal struct SatelliteMarks
+{
+ public SatelliteMarkData frame;
+ public NetworkState state;
+ public bool hasFrame;
+
+ public void SetFrame(in SatelliteMarkData f, NetworkState s)
+ {
+ frame = f;
+ state = s;
+ hasFrame = true;
+ }
+
+ ///
+ /// Waits on the build and returns the projected marks; the frame is kept until
+ /// the next so repeated OnGUI repaints can redraw it.
+ ///
+ public NativeArray Complete()
+ {
+ frame.handle.Complete();
+ return frame.marks;
+ }
+
+ public void Drop()
+ {
+ if (!hasFrame)
+ return;
+
+ frame.Dispose();
+ state = null;
+ hasFrame = false;
+ }
+
+ public void Destroy() => Drop();
+}
diff --git a/src/RemoteTech/Network/Edges.cs b/src/RemoteTech/Network/Edges.cs
new file mode 100644
index 000000000..fb03f7aef
--- /dev/null
+++ b/src/RemoteTech/Network/Edges.cs
@@ -0,0 +1,185 @@
+using System.Runtime.InteropServices;
+using RemoteTech.Collections;
+using RemoteTech.SimpleTypes;
+using Unity.Burst;
+using Unity.Collections;
+using Unity.Jobs;
+using Unity.Mathematics;
+
+using RModel = RemoteTech.RangeModel.RangeModel;
+
+namespace RemoteTech.Network;
+
+
+internal struct NetworkEdge
+{
+ public double distance;
+ /// Max joint range the two nodes' antennas could achieve, independent
+ /// of . Only nonzero when .
+ public double maxRange;
+ public int aIdx;
+ public int bIdx;
+ public LinkType linkType;
+
+ public readonly bool valid => linkType != LinkType.None;
+}
+
+[BurstCompile]
+internal struct BuildEdgesJob : IJobParallelFor
+{
+ public JobConfig config;
+ public RModel model;
+
+ [ReadOnly] public NativeArray nodes;
+ [ReadOnly] public NativeArray antennas;
+ [ReadOnly] public NativeArray bodies;
+ [ReadOnly] public NativeArray vesselMaxRange;
+ [ReadOnly] public NativeArray antennaDirections;
+ [WriteOnly] public NativeArray edges;
+
+ public void Execute(int index)
+ {
+ if (model == RModel.Additive)
+ Execute(index, new AdditiveRangeModel());
+ else
+ Execute(index, new StandardRangeModel());
+ }
+
+ void Execute(int index, TRange model)
+ where TRange : unmanaged, IRangeModel
+ {
+ NetworkUpdateMath.DecodePairIndex(index, out int i, out int j);
+
+ JobNode a = nodes[i];
+ JobNode b = nodes[j];
+
+ ref var record = ref edges.GetElement(index);
+ record = new NetworkEdge()
+ {
+ aIdx = i,
+ bIdx = j,
+ linkType = LinkType.None,
+ distance = math.distance(a.position, b.position)
+ };
+
+ double distance = record.distance;
+
+ // if either node is in blackout then we have nothing else to do
+ if (a.InBlackout || b.InBlackout)
+ return;
+
+ // otherwise, pre-filter by range
+ if (!NetworkUpdateMath.CouldPossiblyLink(vesselMaxRange[i], vesselMaxRange[j], distance))
+ return;
+
+ // attempt line of sight
+ if (!config.ignoreLineOfSight)
+ {
+ int nca = GetNearestCommonAncestor(a.bodyIndex, b.bodyIndex);
+ JobBody ncaBody = bodies[nca];
+ if (!NetworkUpdateMath.HasLineOfSight(a.position, b.position, bodies, ncaBody.subtree))
+ return;
+ }
+
+ // now do the full range model
+ double oc = config.omniClamp;
+ double dc = config.dishClamp;
+ double mult = config.multipleAntennaMultiplier;
+
+ double maxOmniA = MaxOmni(a.antennas);
+ double maxOmniB = MaxOmni(b.antennas);
+ double bonusA = NetworkUpdateMath.GetMultipleAntennaBonus(antennas, a.antennas, maxOmniA, mult);
+ double bonusB = NetworkUpdateMath.GetMultipleAntennaBonus(antennas, b.antennas, maxOmniB, mult);
+
+ double3 dirAB = b.position - a.position;
+ double3 dirBA = -dirAB;
+ double invDist = record.distance > 0.0 ? 1.0 / record.distance : 0.0;
+ double3 dirABNorm = dirAB * invDist;
+ double3 dirBANorm = dirBA * invDist;
+
+ double maxDishA = MaxConnectedDish(a.antennas, dirABNorm);
+ double maxDishB = MaxConnectedDish(b.antennas, dirBANorm);
+
+ // CheckRange is symmetric under swapping (r1,clamp1)<->(r2,clamp2), so these
+ // four also cover the "from B" combinations instead of recomputing them.
+ double rangeOO = Range(model, maxOmniA + bonusA, oc, maxOmniB + bonusB, oc);
+ double rangeOD = Range(model, maxOmniA + bonusA, oc, maxDishB, dc);
+ double rangeDO = Range(model, maxDishA, dc, maxOmniB + bonusB, oc);
+ double rangeDD = Range(model, maxDishA, dc, maxDishB, dc);
+
+ bool reachAOmni = rangeOO >= distance || rangeOD >= distance;
+ bool reachADish = rangeDO >= distance || rangeDD >= distance;
+ bool reachBOmni = rangeOO >= distance || rangeDO >= distance;
+ bool reachBDish = rangeOD >= distance || rangeDD >= distance;
+
+ bool aSideReaches = reachAOmni || reachADish;
+ bool bSideReaches = reachBOmni || reachBDish;
+ if (!aSideReaches || !bSideReaches)
+ return;
+
+ record.maxRange = math.max(math.max(rangeOO, rangeOD), math.max(rangeDO, rangeDD));
+ record.linkType = (reachADish || reachBDish) ? LinkType.Dish : LinkType.Omni;
+ }
+
+ int GetNearestCommonAncestor(int bodyA, int bodyB)
+ {
+ while (bodyA != bodyB)
+ {
+ var min = math.min(bodyA, bodyB);
+ var max = math.max(bodyA, bodyB);
+
+ if (min < 0)
+ return 0;
+
+ bodyA = min;
+ bodyB = bodies[max].parent;
+ }
+
+ return bodyA;
+ }
+
+ double MaxOmni(IntRange range)
+ {
+ double max = 0.0;
+ foreach (int k in range)
+ max = math.max(max, antennas[k].omni);
+
+ return max;
+ }
+
+ ///
+ /// The range of the largest dish that is actually pointed towards the target
+ /// direction.
+ ///
+ private double MaxConnectedDish(IntRange range, double3 dir)
+ {
+ double max = 0.0;
+ foreach (int k in range)
+ {
+ JobAntenna ant = antennas[k];
+ if (ant.dish <= 0.0)
+ continue;
+
+ if (ant.target == 0)
+ continue;
+ double dot = math.dot(antennaDirections[k], dir);
+ if (dot < ant.cosAngle)
+ continue;
+
+ max = math.max(max, ant.dish);
+ }
+
+ return max;
+ }
+
+ private static double Range(
+ TRange model,
+ double r1,
+ double clamp1,
+ double r2,
+ double clamp2)
+ where TRange : unmanaged, IRangeModel
+ {
+ return NetworkUpdateMath.CheckRange(model, r1, clamp1, r2, clamp2);
+ }
+}
diff --git a/src/RemoteTech/Network/Jobs.cs b/src/RemoteTech/Network/Jobs.cs
new file mode 100644
index 000000000..3913841e0
--- /dev/null
+++ b/src/RemoteTech/Network/Jobs.cs
@@ -0,0 +1,196 @@
+using System;
+using System.Runtime.InteropServices;
+using RemoteTech.Collections;
+using Unity.Burst;
+using Unity.Collections;
+using Unity.Collections.LowLevel.Unsafe;
+using Unity.Jobs;
+
+namespace RemoteTech.Network;
+
+internal struct JobConfig
+{
+ public double omniClamp;
+ public double dishClamp;
+ public double multipleAntennaMultiplier;
+ [MarshalAs(UnmanagedType.U1)]
+ public bool ignoreLineOfSight;
+ [MarshalAs(UnmanagedType.U1)]
+ public bool signalRelayEnabled;
+}
+
+///
+/// Given a set of origins this job finds the distance of each node from the
+/// nearest origin.
+///
+[BurstCompile]
+internal struct MultiSourceDijkstraJob : IJob
+{
+ [ReadOnly] public NativeArray ranges;
+ [ReadOnly] public NativeArray adjacency;
+ [ReadOnly] public NativeArray distances;
+ [ReadOnly] public NativeArray roots;
+
+ // Per-node "may forward a signal onward". Roots and destinations don't need it;
+ // only intermediate hops do (see NetworkUpdate.CanTransit).
+ [ReadOnly] public NativeBitArray canTransit;
+
+ public NativeArray scores;
+ public NativeArray parents;
+ public NativeArray origins;
+
+ public void Execute()
+ {
+ for (int i = 0; i < ranges.Length; ++i)
+ {
+ scores[i] = double.PositiveInfinity;
+ parents[i] = -1;
+ origins[i] = -1;
+ }
+
+ var heap = new ArrayMinHeap(ranges.Length, Allocator.Temp);
+ for (int i = 0; i < roots.Length; ++i)
+ {
+ var index = roots[i];
+ if (index < 0 || index >= ranges.Length)
+ continue;
+
+ heap.Push(new()
+ {
+ cost = 0.0,
+ node = index,
+ parent = -1
+ });
+ }
+
+ while (heap.TryPop(out var current))
+ {
+ if (current.cost >= scores[current.node])
+ continue;
+
+ scores[current.node] = current.cost;
+ parents[current.node] = current.parent;
+ if (current.parent == -1)
+ origins[current.node] = current.node;
+ else
+ origins[current.node] = origins[current.parent];
+
+ // A root (parent == -1) is a route endpoint and always forwards; any
+ // other node may only be relayed through if it can transit.
+ if (current.parent != -1 && !canTransit.IsSet(current.node))
+ continue;
+
+ foreach (int edge in ranges[current.node])
+ {
+ int node = adjacency[edge];
+ var cost = current.cost + distances[edge];
+ if (cost >= scores[node])
+ continue;
+
+ heap.Push(new()
+ {
+ cost = cost,
+ node = node,
+ parent = current.node
+ });
+ }
+ }
+ }
+
+ struct HeapNode : IComparable
+ {
+ public double cost;
+ public int node;
+ public int parent;
+
+ public int CompareTo(HeapNode other) => cost.CompareTo(other.cost);
+ }
+}
+
+///
+/// Single-pair shortest path over the persisted edge table: the minimum total
+/// link distance from to . Links exist
+/// between powered nodes; the relay constraint gates transit only, so the two
+/// endpoints need not be relay-capable (see ).
+/// Writes +inf through if the target is unreachable.
+///
+[BurstCompile]
+internal unsafe struct NetworkPathfindJob : IJob
+{
+ public JobConfig config;
+ public int source;
+ public int target;
+
+ [ReadOnly] public NativeArray nodes;
+ [ReadOnly] public NativeArray edges;
+
+ [NativeDisableUnsafePtrRestriction] public double* length;
+
+ public void Execute()
+ {
+ *length = double.PositiveInfinity;
+
+ int n = nodes.Length;
+ if ((uint)source >= (uint)n || (uint)target >= (uint)n)
+ return;
+ if (source == target)
+ {
+ *length = 0.0;
+ return;
+ }
+
+ var dist = new NativeArray(n, Allocator.Temp, NativeArrayOptions.UninitializedMemory);
+ for (int i = 0; i < n; ++i)
+ dist[i] = double.PositiveInfinity;
+
+ var heap = new ArrayMinHeap(n, Allocator.Temp);
+ dist[source] = 0.0;
+ heap.Push(new HeapNode { cost = 0.0, node = source });
+
+ while (heap.TryPop(out var current))
+ {
+ if (current.cost > dist[current.node])
+ continue;
+ if (current.node == target)
+ {
+ *length = current.cost;
+ return;
+ }
+
+ int u = current.node;
+ if (!nodes[u].Powered)
+ continue;
+
+ // The source is a route endpoint; every other node may only be relayed
+ // through if it can transit. The target is exempt too — it returns above
+ // before ever reaching this expansion.
+ if (u != source && !NetworkUpdate.CanTransit(in config, nodes[u]))
+ continue;
+
+ for (int v = 0; v < n; ++v)
+ {
+ if (v == u)
+ continue;
+
+ var edge = edges[NetworkUpdateMath.EncodePairIndex(u, v)];
+ if (!edge.valid || !nodes[v].Powered)
+ continue;
+
+ double next = current.cost + edge.distance;
+ if (next < dist[v])
+ {
+ dist[v] = next;
+ heap.Push(new HeapNode { cost = next, node = v });
+ }
+ }
+ }
+ }
+
+ struct HeapNode : IComparable
+ {
+ public double cost;
+ public int node;
+
+ public int CompareTo(HeapNode other) => cost.CompareTo(other.cost);
+ }
+}
diff --git a/src/RemoteTech/Network/LineMeshJob.cs b/src/RemoteTech/Network/LineMeshJob.cs
new file mode 100644
index 000000000..6fa9df46d
--- /dev/null
+++ b/src/RemoteTech/Network/LineMeshJob.cs
@@ -0,0 +1,295 @@
+using System.Runtime.InteropServices;
+using RemoteTech.SimpleTypes;
+using Unity.Burst;
+using Unity.Collections;
+using Unity.Collections.LowLevel.Unsafe;
+using Unity.Jobs;
+using Unity.Mathematics;
+using UnityEngine;
+
+namespace RemoteTech.Network;
+
+///
+/// A connection line to draw: two node indices and a resolved colour.
+///
+internal struct DrawEdge
+{
+ public int a;
+ public int b;
+ public Color32 color;
+}
+
+///
+/// Axis-aligned bounds in scaled space, computed by .
+///
+internal struct Bounds3
+{
+ public float3 min;
+ public float3 max;
+}
+
+///
+/// One mesh vertex; field order matches the renderer's vertex layout (pos, Color32, uv).
+///
+[StructLayout(LayoutKind.Sequential)]
+internal struct LineVertex
+{
+ public float3 position;
+ public Color32 color;
+ public float2 uv;
+}
+
+///
+/// Per-frame inputs for , captured on the main thread.
+///
+internal struct DrawEdgeParams
+{
+ public int targetNode; // camera-target vessel's node, or -1
+ public int nodeCount;
+ public int pairCount;
+ public byte showOmni, showDish, showPath, showMultiPath, signalRelay;
+ public Color32 active, direct, omni, dish, grey;
+}
+
+///
+/// Selects the visible edges and resolves each colour (the old CheckVisibility/CheckColor, by node index),
+/// then sizes the vertex/index buffers to the selected-edge count (4 verts / 6 indices each).
+///
+[BurstCompile]
+internal struct BuildDrawEdgesJob : IJob
+{
+ [ReadOnly] public NativeArray edges; // triangular pair index
+ [ReadOnly] public NativeArray parent; // any-root predecessor forest (command-station roots)
+ [ReadOnly] public NativeArray nodes; // flags: Visible | CanRelay, set at tick time by NetworkUpdateJob
+ public DrawEdgeParams p;
+ public NativeList outEdges;
+ public NativeList verts;
+ public NativeList indices;
+
+ public void Execute()
+ {
+ var onPath = new NativeBitArray(math.max(p.nodeCount, 1), Allocator.Temp, NativeArrayOptions.ClearMemory);
+ if (p.showPath != 0 && p.targetNode >= 0)
+ {
+ for (int cur = p.targetNode; cur >= 0; cur = parent[cur])
+ onPath.Set(cur, true);
+ }
+ // A route back to the command station is only worth drawing if a visible
+ // vessel actually sits at its far end; a path rooted at a filtered-out
+ // vessel stays hidden even though its edges still exist in the forest.
+ if (p.showMultiPath != 0)
+ {
+ for (int i = 0; i < p.nodeCount; i++)
+ {
+ if (nodes[i].kind != NodeKind.Vessel || (nodes[i].flags & NodeFlags.Visible) == 0)
+ continue;
+ for (int cur = i; cur >= 0; cur = parent[cur])
+ onPath.Set(cur, true);
+ }
+ }
+
+ outEdges.Capacity = math.max(p.nodeCount, 16);
+ outEdges.Clear();
+ for (int pair = 0; pair < p.pairCount; pair++)
+ {
+ NetworkEdge r = edges[pair];
+ if (!r.valid)
+ continue;
+
+ bool tree = parent[r.aIdx] == r.bIdx || parent[r.bIdx] == r.aIdx;
+ bool onP = tree && onPath.IsSet(r.aIdx) && onPath.IsSet(r.bIdx);
+
+ bool visA = (nodes[r.aIdx].flags & NodeFlags.Visible) != 0;
+ bool visB = (nodes[r.bIdx].flags & NodeFlags.Visible) != 0;
+ bool show = onP
+ || (r.linkType == LinkType.Omni && p.showOmni != 0 && visA && visB)
+ || (r.linkType == LinkType.Dish && p.showDish != 0 && visA && visB);
+ if (!show)
+ continue;
+
+ Color32 col;
+ if (onP)
+ col = p.active;
+ else if (p.signalRelay != 0 && (!nodes[r.aIdx].CanRelay || !nodes[r.bIdx].CanRelay))
+ col = p.direct;
+ else if (r.linkType == LinkType.Omni)
+ col = p.omni;
+ else if (r.linkType == LinkType.Dish)
+ col = p.dish;
+ else
+ col = p.grey;
+
+ outEdges.Add(new DrawEdge { a = r.aIdx, b = r.bIdx, color = col });
+ }
+
+ onPath.Dispose();
+
+ int l = outEdges.Length;
+ verts.ResizeUninitialized(4 * l);
+ indices.ResizeUninitialized(6 * l);
+ }
+}
+
+///
+/// Camera/projection helpers shared by the mesh job (replicate Unity's screen transforms).
+///
+internal static class LineMeshCore
+{
+ ///
+ /// Local (physics) space -> scaled space: the affine transform ScaledSpace applies.
+ ///
+ public static float3 ToScaled(double3 local, double invScale, double3 totalOffset)
+ => (float3)(local * invScale - totalOffset);
+
+ ///
+ /// Replicates Camera.WorldToScreenPoint (z = world distance in front of the camera).
+ ///
+ public static float3 WorldToScreen(float3 world, float4x4 view, float4x4 proj, float pw, float ph)
+ {
+ float4 v = math.mul(view, new float4(world, 1f));
+ float4 c = math.mul(proj, v);
+ float3 ndc = c.xyz / c.w;
+ return new float3((ndc.x * 0.5f + 0.5f) * pw, (ndc.y * 0.5f + 0.5f) * ph, -v.z);
+ }
+
+ ///
+ /// Replicates Camera.ScreenToWorldPoint (s.z = world distance in front).
+ ///
+ public static float3 ScreenToWorld(float3 s, float4x4 proj, float4x4 invProj, float4x4 invView, float pw, float ph)
+ {
+ float2 ndc = new(s.x / pw * 2f - 1f, s.y / ph * 2f - 1f);
+ float eyeZ = -s.z;
+ float4 atDepth = math.mul(proj, new float4(0f, 0f, eyeZ, 1f));
+ float clipW = atDepth.w;
+ float ndcZ = atDepth.z / atDepth.w;
+ float4 clip = new float4(ndc.x, ndc.y, ndcZ, 1f) * clipW;
+ float4 eye = math.mul(invProj, clip);
+ float3 eyePt = eye.xyz / eye.w;
+ return math.mul(invView, new float4(eyePt, 1f)).xyz;
+ }
+
+ ///
+ /// Mirror a screen point through a pivot (behind-camera handling for the 2D path).
+ ///
+ public static float3 FlipDirection(float3 point, float3 pivot) => 2f * pivot - point;
+}
+
+///
+/// Camera/ScaledSpace snapshot for .
+///
+internal struct LineMeshViewParams
+{
+ public double invScale;
+ public double3 totalOffset;
+ public float4x4 view, proj, invView, invProj;
+ public float pixelWidth, pixelHeight, halfWidth;
+ public int mode;
+}
+
+///
+/// Builds a billboard quad per edge into the mesh buffers.
+///
+[BurstCompile(FloatMode = FloatMode.Fast)]
+internal struct LineMeshJob : IJobParallelForDefer
+{
+ [ReadOnly] public NativeArray edges; // drawEdges.AsDeferredJobArray()
+ [ReadOnly] public NativeArray nodes; // NetworkState.Nodes (position is local space)
+ public double invScale;
+ public double3 totalOffset;
+ public float4x4 view, proj, invView, invProj;
+ public float pixelWidth, pixelHeight, halfWidth;
+ public int mode; // 0 = 3D billboard, 1 = 2D faked-depth overlay
+
+ [WriteOnly, NativeDisableParallelForRestriction] public NativeArray verts;
+ [WriteOnly, NativeDisableParallelForRestriction] public NativeArray indices;
+
+ public void Execute(int i)
+ {
+ DrawEdge e = edges[i];
+ float3 a = LineMeshCore.WorldToScreen(LineMeshCore.ToScaled(nodes[e.a].position, invScale, totalOffset), view, proj, pixelWidth, pixelHeight);
+ float3 b = LineMeshCore.WorldToScreen(LineMeshCore.ToScaled(nodes[e.b].position, invScale, totalOffset), view, proj, pixelWidth, pixelHeight);
+
+ if (mode != 0) // 2D flat overlay: clamp to a near plane, flip behind-camera endpoints
+ {
+ if (a.z < 0f) a = LineMeshCore.FlipDirection(a, b);
+ else if (b.z < 0f) b = LineMeshCore.FlipDirection(b, a);
+ float d = pixelHeight * 0.5f + 0.01f;
+ a.z = a.z >= 0.15f ? d : -d;
+ b.z = b.z >= 0.15f ? d : -d;
+ }
+
+ float2 dir = new(b.y - a.y, a.x - b.x);
+ float len = math.length(dir);
+ float2 ndir = len > 1e-6f ? dir / len : new float2(1f, 0f);
+ float3 seg = new(ndir * halfWidth, 0f);
+
+ float3 p0 = LineMeshCore.ScreenToWorld(a - seg, proj, invProj, invView, pixelWidth, pixelHeight);
+ float3 p1 = LineMeshCore.ScreenToWorld(a + seg, proj, invProj, invView, pixelWidth, pixelHeight);
+ float3 p2 = LineMeshCore.ScreenToWorld(b - seg, proj, invProj, invView, pixelWidth, pixelHeight);
+ float3 p3 = LineMeshCore.ScreenToWorld(b + seg, proj, invProj, invView, pixelWidth, pixelHeight);
+
+ Color32 col = e.color;
+ int vb = 4 * i;
+ verts[vb + 0] = new LineVertex { position = p0, color = col, uv = new float2(0f, 1f) };
+ verts[vb + 1] = new LineVertex { position = p1, color = col, uv = new float2(0f, 0f) };
+ verts[vb + 2] = new LineVertex { position = p2, color = col, uv = new float2(1f, 1f) };
+ verts[vb + 3] = new LineVertex { position = p3, color = col, uv = new float2(1f, 0f) };
+
+ int ib = 6 * i;
+ uint v = (uint)vb;
+ indices[ib + 0] = v + 0; indices[ib + 1] = v + 2; indices[ib + 2] = v + 1;
+ indices[ib + 3] = v + 2; indices[ib + 4] = v + 3; indices[ib + 5] = v + 1;
+ }
+}
+
+///
+/// Reduces the written verts to an AABB for mesh.bounds.
+///
+[BurstCompile]
+internal struct LineBoundsJob : IJob
+{
+ [ReadOnly] public NativeArray verts; // _verts.AsDeferredJobArray()
+ [WriteOnly] public NativeArray boundsOut;
+
+ public void Execute()
+ {
+ int n = verts.Length;
+ if (n == 0)
+ {
+ boundsOut[0] = new Bounds3 { min = float3.zero, max = float3.zero };
+ return;
+ }
+ float3 mn = verts[0].position;
+ float3 mx = mn;
+ for (int i = 1; i < n; i++)
+ {
+ float3 pos = verts[i].position;
+ mn = math.min(mn, pos);
+ mx = math.max(mx, pos);
+ }
+ boundsOut[0] = new Bounds3 { min = mn, max = mx };
+ }
+}
+
+///
+/// One frame's mesh buffers from ; verts are
+/// billboarded against , so the draw must translate them onto the
+/// current ScaledSpace origin.
+///
+internal struct LineMeshData
+{
+ public NativeList drawEdges;
+ public NativeList verts;
+ public NativeList indices;
+ public NativeArray bounds;
+ public JobHandle handle;
+ public double3 builtOffset;
+
+ public void Dispose()
+ {
+ if (drawEdges.IsCreated) drawEdges.Dispose();
+ if (verts.IsCreated) verts.Dispose();
+ if (indices.IsCreated) indices.Dispose();
+ if (bounds.IsCreated) bounds.Dispose();
+ }
+}
diff --git a/src/RemoteTech/Network/NetworkState.cs b/src/RemoteTech/Network/NetworkState.cs
new file mode 100644
index 000000000..0f18d6fc0
--- /dev/null
+++ b/src/RemoteTech/Network/NetworkState.cs
@@ -0,0 +1,871 @@
+using System;
+using System.Collections.Generic;
+using RemoteTech.Collections;
+using RemoteTech.SimpleTypes;
+using Unity.Burst;
+using Unity.Collections;
+using Unity.Jobs;
+using Unity.Mathematics;
+using Unity.Profiling;
+using UnityEngine;
+
+namespace RemoteTech.Network;
+
+internal class NetworkState : IDisposable
+{
+ JobHandle handle;
+
+ ///
+ /// A handle for consumers of this state. It won't block
+ /// but it will be completed before the arrays are disposed of.
+ ///
+ JobHandle dependentHandle;
+
+ ISatellite[] satellites;
+
+ // The settings snapshot this state was built from; reused by point-to-point
+ // queries so they gate hops the same way the tick's routing did.
+ JobConfig config;
+
+ // This maps guid to satellite index
+ NativeHashMap satmap;
+ NativeArray nodes;
+ NativeArray bodies;
+ NativeArray edges;
+
+ // Whether individual antennas are connected.
+ NativeBitArray connected;
+
+ // Network topology relative to the ground stations
+ NativeArray scoreGs;
+ NativeArray parentGs;
+ NativeArray originGs;
+
+ // Network topology relative to all command stations
+ NativeArray scoreCs;
+ NativeArray parentCs;
+ NativeArray originCs;
+
+ // This tick's per-node route fingerprint (used to diff against the previous
+ // tick's state without a per-satellite dictionary walk).
+ NativeList fingerprints;
+
+ // Antenna candidates for rendering the cone mesh
+ NativeList coneCandidates;
+
+ // Ground stations and command-station vessels eligible for a map-view mark
+ NativeList markCandidates;
+
+ // Node indices (this tick's indexing) whose fingerprint differs from the
+ // previous tick's — see ScheduleFingerprintDiff / FireConnectionRefresh.
+ NativeList changedNodes;
+
+ readonly Dictionary[]> routeCache = [];
+
+ private NetworkState() { }
+
+ static readonly ProfilerMarker CompleteMarker = new("NetworkState.Complete");
+ public void Complete()
+ {
+ using var scope = CompleteMarker.Auto();
+ handle.Complete();
+ }
+
+ public void Dispose()
+ {
+ Dispose(JobHandle.CombineDependencies(this.handle, dependentHandle));
+ }
+
+ public JobHandle Dispose(JobHandle deps)
+ {
+ var handle = new NetworkStateDisposeJob { state = new ObjectHandle(this) }
+ .Schedule(JobHandle.CombineDependencies(deps, this.handle, dependentHandle));
+ return handle;
+ }
+
+ void DisposeInternal()
+ {
+ satmap.Dispose();
+ nodes.Dispose();
+ edges.Dispose();
+ connected.Dispose();
+
+ scoreGs.Dispose();
+ parentGs.Dispose();
+ originGs.Dispose();
+
+ scoreCs.Dispose();
+ parentCs.Dispose();
+ originCs.Dispose();
+
+ fingerprints.Dispose();
+ changedNodes.Dispose();
+
+ bodies.Dispose();
+ coneCandidates.Dispose();
+ markCandidates.Dispose();
+ }
+
+ void AddDependentHandle(JobHandle handle)
+ {
+ dependentHandle = JobHandle.CombineDependencies(dependentHandle, handle);
+ }
+
+ static readonly ProfilerMarker ScheduleNetworkUpdateMarker = new("NetworkState.ScheduleNetworkUpdate");
+
+ ///
+ /// Kick off all jobs needed to compute the current network state.
+ ///
+ ///
+ /// The state from the previous tick, or null. Used to determine which
+ /// satellite connections have been changed.
+ ///
+ public static NetworkState ScheduleNetworkUpdate(NetworkManager manager, NetworkState previous)
+ {
+ using var scope = ScheduleNetworkUpdateMarker.Auto();
+ var cbs = FlightGlobals.Bodies;
+
+ var handles = new NativeList(16, Allocator.Temp);
+
+ var vesselCount = RTCore.Instance.Satellites.Count;
+ var groundStationCount = manager.GroundStations.Count;
+ var nodeCount = vesselCount + groundStationCount;
+ int edgeCount = NetworkUpdateMath.PairCount(nodeCount);
+
+ var rawVessels = new NativeArray(
+ vesselCount, Allocator.TempJob, NativeArrayOptions.UninitializedMemory);
+ var satHandle = new ObjectHandle>(
+ RTCore.Instance.Satellites.SatelliteCache);
+ var recordVessels = new NetworkUpdate.RecordVesselInfoJob
+ {
+ satellites = satHandle,
+ infos = rawVessels
+ }.ScheduleBatch(vesselCount, 24);
+
+ var rawStations = new NativeArray(
+ groundStationCount, Allocator.TempJob, NativeArrayOptions.UninitializedMemory);
+ var stationHandle = new ObjectHandle>(
+ manager.GroundStations);
+ var recordStations = new NetworkUpdate.RecordGroundStationsJob
+ {
+ stations = stationHandle,
+ states = rawStations
+ }.Schedule();
+
+ var rawCbs = new NativeArray(
+ cbs.Count, Allocator.TempJob, NativeArrayOptions.UninitializedMemory);
+ var recordCbs = new NetworkUpdate.RecordCelestialBodiesJob
+ {
+ bodies = new(cbs),
+ infos = rawCbs
+ }.Schedule();
+
+ var syncHandle = JobHandle.CombineDependencies(recordVessels, recordStations);
+ JobHandle.ScheduleBatchedJobs();
+
+ handles.Add(satHandle.Dispose(recordVessels));
+ handles.Add(stationHandle.Dispose(recordStations));
+
+ JobConfig config = new()
+ {
+ omniClamp = RTSettings.Instance.OmniRangeClampFactor,
+ dishClamp = RTSettings.Instance.DishRangeClampFactor,
+ multipleAntennaMultiplier = RTSettings.Instance.MultipleAntennaMultiplier,
+ ignoreLineOfSight = RTSettings.Instance.IgnoreLineOfSight,
+ signalRelayEnabled = RTSettings.Instance.SignalRelayEnabled,
+ };
+ NetworkUpdate.VisibilityState visibility = new()
+ {
+ ActiveVessel = FlightGlobals.ActiveVessel?.id ?? default,
+ TargetVessel = FlightGlobals.fetch?.VesselTarget?.GetVessel().id ?? default,
+ filter = MapViewFiltering.Instance.IsNotNullOrDestroyed()
+ ? MapViewFiltering.vesselTypeFilter
+ : null
+ };
+
+ var nodes = new NativeArray(
+ nodeCount, Allocator.Persistent, NativeArrayOptions.UninitializedMemory);
+ var guids = new NativeArray(
+ nodeCount, Allocator.TempJob, NativeArrayOptions.UninitializedMemory);
+ var bodies = new NativeArray(
+ cbs.Count, Allocator.Persistent, NativeArrayOptions.UninitializedMemory);
+ var ranges = new NativeArray(
+ nodeCount, Allocator.TempJob, NativeArrayOptions.UninitializedMemory);
+ var antennas = new NativeList(Allocator.TempJob);
+ var directions = new NativeList(Allocator.TempJob);
+ var coneCandidates = new NativeList(0, Allocator.Persistent);
+ var markCandidates = new NativeList(0, Allocator.Persistent);
+ var satmap = new NativeHashMap(vesselCount, Allocator.Persistent);
+ var update1 = new NetworkUpdateJob
+ {
+ config = config,
+ visibility = visibility,
+ vessels = rawVessels,
+ cbs = rawCbs,
+ stations = rawStations,
+ antennaData = StateManager.Antennas.GetStateArray(Allocator.TempJob),
+
+ nodes = nodes,
+ guids = guids,
+ bodies = bodies,
+ antennas = antennas,
+ directions = directions,
+ ranges = ranges,
+ satmap = satmap,
+ coneCandidates = coneCandidates,
+ markCandidates = markCandidates,
+ }.Schedule(JobHandle.CombineDependencies(syncHandle, recordCbs));
+ StateManager.Antennas.AddUseHandle(update1);
+
+ var edges = new NativeArray(
+ edgeCount, Allocator.Persistent, NativeArrayOptions.UninitializedMemory);
+ var buildEdges = new BuildEdgesJob
+ {
+ config = config,
+ model = RTSettings.Instance.RangeModelType,
+ nodes = nodes,
+ bodies = bodies,
+ antennas = antennas.AsDeferredJobArray(),
+ vesselMaxRange = ranges,
+ antennaDirections = directions.AsDeferredJobArray(),
+ edges = edges
+ }.Schedule(edgeCount, 256, update1);
+
+ var adjranges = new NativeList(Allocator.TempJob);
+ var adjacency = new NativeList(Allocator.TempJob);
+ var distances = new NativeList(Allocator.TempJob);
+ var connected = new NativeBitArray(nodeCount, Allocator.Persistent);
+
+ var commandStations = new NativeList(Allocator.TempJob);
+ var groundStations = new NativeList(Allocator.TempJob);
+ var canTransit = new NativeBitArray(nodeCount, Allocator.TempJob);
+
+ var update2 = new NetworkAdjacencyJob
+ {
+ config = config,
+ nodes = nodes,
+ edges = edges,
+
+ ranges = adjranges,
+ adjacency = adjacency,
+ distances = distances,
+ connected = connected,
+ canTransit = canTransit,
+
+ commandStations = commandStations,
+ groundStations = groundStations,
+ }.Schedule(buildEdges);
+
+ var scoreGs = new NativeArray(nodeCount, Allocator.Persistent, NativeArrayOptions.UninitializedMemory);
+ var scoreCs = new NativeArray(nodeCount, Allocator.Persistent, NativeArrayOptions.UninitializedMemory);
+ var parentGs = new NativeArray(nodeCount, Allocator.Persistent, NativeArrayOptions.UninitializedMemory);
+ var parentCs = new NativeArray(nodeCount, Allocator.Persistent, NativeArrayOptions.UninitializedMemory);
+ var originGs = new NativeArray(nodeCount, Allocator.Persistent, NativeArrayOptions.UninitializedMemory);
+ var originCs = new NativeArray(nodeCount, Allocator.Persistent, NativeArrayOptions.UninitializedMemory);
+
+ var dijkstraDeps = update2;
+
+ var dijkstraCs = new MultiSourceDijkstraJob
+ {
+ ranges = adjranges.AsDeferredJobArray(),
+ adjacency = adjacency.AsDeferredJobArray(),
+ distances = distances.AsDeferredJobArray(),
+ roots = commandStations.AsDeferredJobArray(),
+ canTransit = canTransit,
+
+ scores = scoreCs,
+ parents = parentCs,
+ origins = originCs,
+ }.Schedule(dijkstraDeps);
+ var dijkstraGs = new MultiSourceDijkstraJob
+ {
+ ranges = adjranges.AsDeferredJobArray(),
+ adjacency = adjacency.AsDeferredJobArray(),
+ distances = distances.AsDeferredJobArray(),
+ roots = groundStations.AsDeferredJobArray(),
+ canTransit = canTransit,
+
+ scores = scoreGs,
+ parents = parentGs,
+ origins = originGs,
+ }.Schedule(dijkstraDeps);
+ var dijkstraAll = JobHandle.CombineDependencies(dijkstraCs, dijkstraGs);
+
+ handles.Add(new DisposeTempContainersJob
+ {
+ antennas = antennas,
+ directions = directions,
+ ranges = ranges,
+ adjranges = adjranges,
+ adjacency = adjacency,
+ distances = distances,
+ commandStations = commandStations,
+ groundStations = groundStations,
+ canTransit = canTransit,
+ }.Schedule(dijkstraAll));
+
+ var fingerprints = new NativeList(0, Allocator.Persistent);
+ var changedNodes = new NativeList(0, Allocator.Persistent);
+
+ NativeHashMap prevSatmap;
+ NativeArray prevFingerprints;
+ var fingerprintDeps = dijkstraAll;
+ if (previous is not null)
+ {
+ prevSatmap = previous.satmap;
+ prevFingerprints = previous.fingerprints.AsDeferredJobArray();
+ fingerprintDeps = JobHandle.CombineDependencies(fingerprintDeps, previous.handle);
+ }
+ else
+ {
+ prevSatmap = new NativeHashMap(1, Allocator.Temp);
+ prevFingerprints = default;
+ }
+
+ var fingerprintHandle = new NetworkFingerprintJob
+ {
+ guids = guids,
+ originCs = originCs,
+ parentCs = parentCs,
+ originGs = originGs,
+ parentGs = parentGs,
+ prevSatmap = prevSatmap,
+ prevFingerprints = prevFingerprints,
+ fingerprints = fingerprints,
+ changed = changedNodes,
+ }.Schedule(fingerprintDeps);
+
+ if (previous is not null)
+ previous.AddDependentHandle(fingerprintHandle);
+ else
+ fingerprintHandle = prevSatmap.Dispose(fingerprintHandle);
+
+ handles.Add(fingerprintHandle);
+
+ JobHandle.ScheduleBatchedJobs();
+ var satellites = GetSatellites(manager);
+ syncHandle.Complete();
+
+ return new()
+ {
+ handle = JobHandle.CombineDependencies(handles.AsArray()),
+ satellites = satellites,
+ config = config,
+
+ satmap = satmap,
+ nodes = nodes,
+ edges = edges,
+ connected = connected,
+
+ scoreGs = scoreGs,
+ parentGs = parentGs,
+ originGs = originGs,
+
+ scoreCs = scoreCs,
+ parentCs = parentCs,
+ originCs = originCs,
+
+ fingerprints = fingerprints,
+ changedNodes = changedNodes,
+
+ bodies = bodies,
+ coneCandidates = coneCandidates,
+ markCandidates = markCandidates,
+ };
+ }
+
+ /// Fires ISatellite.OnConnectionRefresh for every node whose route
+ /// fingerprint changed since the previous tick. Call after .
+ internal void FireConnectionRefresh(NetworkManager manager)
+ {
+ for (int i = 0; i < changedNodes.Length; i++)
+ {
+ var sat = satellites[changedNodes[i]];
+ if (sat != null)
+ sat.OnConnectionRefresh(manager[sat]);
+ }
+ }
+
+ private static ISatellite[] GetSatellites(NetworkManager manager)
+ {
+ var vessels = RTCore.Instance.Satellites.SatelliteCache.Values;
+ var ground = manager.GroundStations.Values;
+
+ var satellites = new ISatellite[vessels.Length + ground.Length];
+
+ for (int i = 0; i < vessels.Length; ++i)
+ satellites[i] = vessels[i];
+ ground.CopyTo(satellites.AsSpan().Slice(vessels.Length));
+
+ return satellites;
+ }
+
+ ///
+ /// Indicates whether the vessel containing this antenna is connected.
+ ///
+ ///
+ ///
+ public bool IsAntennaConnected(IAntenna antenna)
+ {
+ if (antenna is null)
+ return false;
+ if (!satmap.TryGetValue(antenna.Guid, out var index))
+ return false;
+ if (!connected.IsSet(index))
+ return false;
+ if (!antenna.Activated)
+ return false;
+ return antenna.Omni > 0 || antenna.Dish > 0;
+ }
+
+ internal bool TryGetNode(ISatellite sat, out int node)
+ {
+ if (sat != null) return satmap.TryGetValue(sat.Guid, out node);
+ node = -1;
+ return false;
+ }
+
+ internal ISatellite SatAt(int node) => satellites[node];
+
+ internal int NodeCount => nodes.Length;
+ internal int PairCount => edges.Length;
+
+ internal NativeArray Nodes => nodes;
+
+ ///
+ /// Builds this tick's map-view connection-line mesh (edge selection/colouring
+ /// through billboard-quad geometry); only /
+ /// vary per frame.
+ ///
+ internal LineMeshData ScheduleLineMesh(in DrawEdgeParams p, in LineMeshViewParams view)
+ {
+ var drawEdges = new NativeList(0, Allocator.TempJob);
+ var verts = new NativeList(0, Allocator.TempJob);
+ var indices = new NativeList(0, Allocator.TempJob);
+ var bounds = new NativeArray(1, Allocator.TempJob);
+
+ JobHandle h1 = new BuildDrawEdgesJob
+ {
+ edges = edges,
+ parent = parentCs,
+ nodes = nodes,
+ p = p,
+ outEdges = drawEdges,
+ verts = verts,
+ indices = indices,
+ }.Schedule(handle);
+
+ JobHandle h2 = new LineMeshJob
+ {
+ edges = drawEdges.AsDeferredJobArray(),
+ nodes = nodes,
+ verts = verts.AsDeferredJobArray(),
+ indices = indices.AsDeferredJobArray(),
+ invScale = view.invScale,
+ totalOffset = view.totalOffset,
+ view = view.view,
+ proj = view.proj,
+ invView = view.invView,
+ invProj = view.invProj,
+ pixelWidth = view.pixelWidth,
+ pixelHeight = view.pixelHeight,
+ halfWidth = view.halfWidth,
+ mode = view.mode,
+ }.Schedule(drawEdges, 1024, h1);
+
+ JobHandle h3 = new LineBoundsJob
+ {
+ verts = verts.AsDeferredJobArray(),
+ boundsOut = bounds,
+ }.Schedule(h2);
+
+ AddDependentHandle(h3);
+
+ return new LineMeshData
+ {
+ drawEdges = drawEdges,
+ verts = verts,
+ indices = indices,
+ bounds = bounds,
+ handle = h3,
+ builtOffset = view.totalOffset,
+ };
+ }
+
+ internal int ConeCandidateCount => coneCandidates.Length;
+
+ ///
+ /// Builds this tick's map-view dish-cone mesh from the candidates
+ /// gathered during the tick;
+ /// like it billboards against
+ /// , so the draw must translate onto the
+ /// current ScaledSpace origin.
+ ///
+ internal ConeMeshData ScheduleConeMesh(in ConeViewParams p)
+ {
+ int count = coneCandidates.Length;
+ var verts = new NativeArray(8 * count, Allocator.TempJob, NativeArrayOptions.UninitializedMemory);
+ var indices = new NativeArray(12 * count, Allocator.TempJob, NativeArrayOptions.UninitializedMemory);
+ var bounds = new NativeArray(1, Allocator.TempJob);
+
+ JobHandle h1 = new ConeMeshJob
+ {
+ candidates = coneCandidates.AsArray(),
+ nodes = nodes,
+ bodies = bodies,
+ p = p,
+ verts = verts,
+ indices = indices,
+ }.Schedule(count, 128, handle);
+
+ JobHandle h2 = new LineBoundsJob
+ {
+ verts = verts,
+ boundsOut = bounds,
+ }.Schedule(h1);
+
+ AddDependentHandle(h2);
+
+ return new ConeMeshData { verts = verts, indices = indices, bounds = bounds, handle = h2, builtOffset = p.totalOffset };
+ }
+
+ internal int MarkCandidateCount => markCandidates.Length;
+
+ ///
+ /// Filters this tick's mark candidates to the ones visible in the map view and
+ /// projects each to its GUI-space screen position; the renderer draws the
+ /// survivors in OnGUI.
+ ///
+ internal SatelliteMarkData ScheduleSatelliteMarks(in SatelliteMarkViewParams p)
+ {
+ var marks = new NativeList(markCandidates.Length, Allocator.TempJob);
+
+ JobHandle h = new SatelliteMarkJob
+ {
+ candidates = markCandidates.AsArray(),
+ nodes = nodes,
+ bodies = bodies,
+ p = p,
+ marks = marks,
+ }.Schedule(handle);
+
+ AddDependentHandle(h);
+
+ return new SatelliteMarkData { marks = marks, handle = h };
+ }
+
+ internal double RouteLength(int node, bool groundOnly)
+ {
+ var origin = GetOrigin(node, groundOnly);
+ if (origin < 0)
+ return double.PositiveInfinity;
+
+ return groundOnly ? scoreGs[node] : scoreCs[node];
+ }
+
+ ///
+ /// True even for a root's own zero-hop self-route; see for the >=1-hop version.
+ ///
+ ///
+ /// Only consider ground stations, not vessels.
+ public bool GetRouteExists(ISatellite sat, bool groundOnly)
+ {
+ if (!satmap.TryGetValue(sat.Guid, out int index))
+ return false;
+
+ return groundOnly
+ ? originGs[index] >= 0
+ : originCs[index] >= 0;
+ }
+
+ ///
+ /// Unlike , false for a root's own zero-hop self-route.
+ ///
+ internal bool RouteExists(int node, bool groundOnly)
+ {
+ var origin = GetOrigin(node, groundOnly);
+ return origin >= 0 && origin != node;
+ }
+
+ internal ISatellite RouteGoalSat(int node, bool groundOnly)
+ {
+ var origin = GetOrigin(node, groundOnly);
+ return origin >= 0 ? satellites[origin] : null;
+ }
+
+ private static long HopKey(int node, bool groundOnly) =>
+ ((long)node << 1) | (groundOnly ? 1L : 0L);
+
+ private static readonly NetworkLink[] NoHops = [];
+
+ ///
+ /// Returns the network route from this satellite to its control station,
+ /// or null if there is no route.
+ ///
+ ///
+ /// Only consider ground stations, not vessels.
+ ///
+ public IReadOnlyList> GetRoute(ISatellite sat, bool groundOnly)
+ {
+ if (!satmap.TryGetValue(sat.Guid, out int index))
+ return null;
+ if (GetOrigin(index, groundOnly) < 0)
+ return null;
+
+ return BuildHops(index, groundOnly);
+ }
+
+ ///
+ /// Never null, unlike (empty when unreachable or a root's self-route).
+ ///
+ internal IReadOnlyList> RouteHops(int node, bool groundOnly) =>
+ BuildHops(node, groundOnly);
+
+ private IReadOnlyList> BuildHops(int index, bool groundOnly)
+ {
+ var cacheKey = HopKey(index, groundOnly);
+ if (routeCache.TryGetValue(cacheKey, out var cached))
+ return cached;
+
+ var origin = GetOrigin(index, groundOnly);
+ if (origin < 0 || origin == index)
+ return routeCache[cacheKey] = NoHops;
+
+ int start = index;
+ int count = 0;
+
+ while (true)
+ {
+ var parent = GetParent(index, groundOnly);
+ if (parent < 0)
+ break;
+
+ count += 1;
+ index = parent;
+ }
+
+ var links = new NetworkLink[count];
+ index = start;
+ for (int i = 0; i < count; ++i)
+ {
+ var parent = GetParent(index, groundOnly);
+ links[i] = BuildLink(index, parent);
+ index = parent;
+ }
+
+ return routeCache[cacheKey] = links;
+ }
+
+ private int GetOrigin(int index, bool groundOnly) =>
+ groundOnly ? originGs[index] : originCs[index];
+
+ private int GetParent(int index, bool groundOnly) =>
+ groundOnly ? parentGs[index] : parentCs[index];
+
+ private NetworkLink BuildLink(int fromIdx, int toIdx)
+ {
+ var port = LinkType.None;
+ if (TryGetEdge(fromIdx, toIdx, out var edge) && edge.valid)
+ port = edge.linkType;
+
+ return new NetworkLink(satellites[toIdx], port);
+ }
+
+ ///
+ /// Direct links out of (its adjacency row).
+ ///
+ public List> GetLinks(ISatellite sat)
+ {
+ if (!TryGetNode(sat, out int node))
+ return [];
+
+ return GetLinksByNode(node);
+ }
+
+ private List> GetLinksByNode(int node)
+ {
+ var result = new List>();
+ if (!connected.IsSet(node))
+ return result;
+
+ for (int j = 0; j < satellites.Length; j++)
+ {
+ if (j == node)
+ continue;
+ if (TryGetEdge(node, j, out var edge) && edge.valid)
+ result.Add(new NetworkLink(satellites[j], edge.linkType));
+ }
+ return result;
+ }
+
+ ///
+ /// Enumerates the whole adjacency graph (inspection/debug seam).
+ ///
+ internal IEnumerable>>> EnumerateLinks()
+ {
+ for (int i = 0; i < satellites.Length; i++)
+ {
+ var sat = satellites[i];
+ if (sat == null)
+ continue;
+ yield return new KeyValuePair>>(sat.Guid, GetLinksByNode(i));
+ }
+ }
+
+ ///
+ /// O(1) shortest signal delay for