2
0
mirror of https://github.com/9ParsonsB/Pulsar.git synced 2025-07-01 16:33:43 -04:00

Initial Commit

This commit is contained in:
2024-04-13 15:54:59 +10:00
parent 8e178cbb7b
commit 63ed43f4af
459 changed files with 8039 additions and 20504 deletions

View File

@ -0,0 +1,13 @@
namespace Explorer;
internal class CriteriaLoadException : Exception
{
public CriteriaLoadException(string message, string script)
{
Message = message;
OriginalScript = script;
}
new public readonly string Message;
public readonly string OriginalScript;
}

View File

@ -0,0 +1,382 @@
using System.Text;
using NLua;
using NLua.Exceptions;
using Observatory.Framework.Files.Journal.Exploration;
namespace Explorer;
internal class CustomCriteriaManager
{
private Lua LuaState;
private Dictionary<string,LuaFunction> CriteriaFunctions;
private Dictionary<string, string> CriteriaWithErrors = new();
Action<Exception, string> ErrorLogger;
private uint ScanCount;
public CustomCriteriaManager(Action<Exception, string> errorLogger)
{
ErrorLogger = errorLogger;
CriteriaFunctions = new();
ScanCount = 0;
}
public void RefreshCriteria(string criteriaPath)
{
LuaState = new();
LuaState.State.Encoding = Encoding.UTF8;
LuaState.LoadCLRPackage();
#region Iterators
// Empty function for nil iterators
LuaState.DoString("function nil_iterator() end");
//Materials and AtmosphereComposition
LuaState.DoString(@"
function materials (material_list)
if material_list then
local i = 0
local count = material_list.Count
return function ()
i = i + 1
if i <= count then
return { name = material_list[i - 1].Name, percent = material_list[i - 1].Percent }
end
end
else
return nil_iterator
end
end");
//Rings - internal filterable iterator
LuaState.DoString(@"
function _ringsFiltered (ring_list, filter_by)
if ring_list then
local i = 0
local count = ring_list.Count
return function ()
i = i + 1
while i <= count do
local ring = ring_list[i - 1]
if (filter_by == nil or string.find(ring.Name, filter_by)) then
return { name = ring.Name, ringclass = ring.RingClass, massmt = ring.MassMT, innerrad = ring.InnerRad, outerrad = ring.OuterRad }
else
i = i + 1
end
end
end
else
return nil_iterator
end
end");
//Rings - internal filterable hasX check
LuaState.DoString(@"
function _hasRingsFiltered (ring_list, filter_by)
if ring_list then
local i = 0
local count = ring_list.Count
while i < count do
if string.find(ring_list[i].Name, filter_by) then
return true
end
i = i + 1
end
end
return false
end");
//Rings - iterate all - nil filter
LuaState.DoString(@"
function rings (ring_list)
return _ringsFiltered(ring_list, nil)
end");
//Rings - iterate proper rings only
LuaState.DoString(@"
function ringsOnly (ring_list)
return _ringsFiltered(ring_list, 'Ring')
end");
//Rings - iterate belts only
LuaState.DoString(@"
function beltsOnly (ring_list)
return _ringsFiltered(ring_list, 'Belt')
end");
//Bodies in system
LuaState.DoString(@"
function bodies (system_list)
if system_list then
local i = 0
local count = system_list.Count
return function ()
i = i + 1
if i <= count then
return system_list[i - 1]
end
end
else
return nil_iterator
end
end");
//Parent bodies
LuaState.DoString(@"
function allparents (parent_list)
if parent_list then
local i = 0
local count
if parent_list then count = parent_list.Count else count = 0 end
return function ()
i = i + 1
if i <= count then
return { parenttype = parent_list[i - 1].ParentType, body = parent_list[i - 1].Body, scan = parent_list[i - 1].Scan }
end
end
else
return nil_iterator
end
end");
#endregion
#region Convenience Functions
//Rings - has > 0 belts
LuaState.DoString(@"
function hasBelts (ring_list)
return _hasRingsFiltered(ring_list, 'Belt')
end");
//Rings - has > 0 proper rings
LuaState.DoString(@"
function hasRings (ring_list)
return _hasRingsFiltered(ring_list, 'Ring')
end");
LuaState.DoString(@"
function isStar (scan)
return scan.StarType and scan.StarType ~= ''
end");
LuaState.DoString(@"
function isPlanet (scan)
return scan.PlanetClass ~= nil
end");
LuaState.DoString(@"
function hasAtmosphere (scan)
return scan.AtmosphereComposition ~= nil
end");
LuaState.DoString(@"
function hasLandableAtmosphere (scan)
return scan.Landable and scan.AtmosphereComposition ~= nil
end");
#endregion
CriteriaFunctions.Clear();
CriteriaWithErrors.Clear();
var criteria = File.Exists(criteriaPath) ? File.ReadAllLines(criteriaPath) : Array.Empty<string>();
StringBuilder script = new();
try
{
for (var i = 0; i < criteria.Length; i++)
{
if (criteria[i].Trim().StartsWith("::"))
{
var scriptDescription = criteria[i].Trim().Replace("::", string.Empty);
if (scriptDescription.ToLower() == "criteria" || scriptDescription.ToLower().StartsWith("criteria="))
{
var functionName = $"Criteria{i}";
script.AppendLine($"function {functionName} (scan, parents, system, biosignals, geosignals)");
i++;
do
{
if (i >= criteria.Length)
throw new Exception("Unterminated multi-line criteria.\r\nAre you missing an ::End::?");
script.AppendLine(criteria[i]);
i++;
} while (!criteria[i].Trim().ToLower().StartsWith("::end::"));
script.AppendLine("end");
LuaState.DoString(script.ToString());
CriteriaFunctions.Add(GetUniqueDescription(functionName, scriptDescription), LuaState[functionName] as LuaFunction);
script.Clear();
}
else if (scriptDescription.ToLower() == "global")
{
i++;
do
{
script.AppendLine(criteria[i]);
i++;
} while (!criteria[i].Trim().ToLower().StartsWith("::end::"));
LuaState.DoString(script.ToString());
script.Clear();
}
else
{
i++;
var functionName = $"Criteria{i}";
script.AppendLine($"function {functionName} (scan, parents, system, biosignals, geosignals)");
script.AppendLine($" local result = {criteria[i]}");
script.AppendLine(" local detail = ''");
if (criteria.Length > i + 1 && criteria[i + 1].Trim().ToLower() == "::detail::")
{
i++; i++;
// Gate detail evaluation on result to allow safe use of criteria-checked values in detail string.
script.AppendLine(" if result then");
script.AppendLine($" detail = {criteria[i]}");
script.AppendLine(" end");
}
script.AppendLine($" return result, '{scriptDescription}', detail");
script.AppendLine("end");
LuaState.DoString(script.ToString());
CriteriaFunctions.Add(GetUniqueDescription(functionName, scriptDescription), LuaState[functionName] as LuaFunction);
script.Clear();
}
}
}
}
catch (Exception e)
{
var originalScript = script.ToString().Trim();
originalScript = originalScript.Remove(originalScript.LastIndexOf(Environment.NewLine));
originalScript = originalScript[(originalScript.IndexOf(Environment.NewLine) + Environment.NewLine.Length)..];
originalScript = originalScript.Replace('\t', ' ');
StringBuilder errorDetail = new();
errorDetail.AppendLine("Error Reading Custom Criteria File:")
.AppendLine(originalScript)
.AppendLine("To correct this problem, make changes to the Lua source file, save it and either re-run read-all or scan another body. It will be automatically reloaded."); ErrorLogger(e, errorDetail.ToString());
CriteriaFunctions.Clear(); // Don't use partial parse.
throw new CriteriaLoadException(e.Message, originalScript);
}
}
public List<(string, string, bool)> CheckInterest(Scan scan, Dictionary<ulong, Dictionary<int, Scan>> scanHistory, Dictionary<ulong, Dictionary<int, FSSBodySignals>> signalHistory, ExplorerSettings settings)
{
List<(string, string, bool)> results = new();
ScanCount++;
foreach (var criteriaFunction in CriteriaFunctions)
{
// Skip criteria which have previously thrown an error. We can't remove them from the dictionary while iterating it.
if (CriteriaWithErrors.ContainsKey(criteriaFunction.Key)) continue;
var scanList = scanHistory[scan.SystemAddress].Values.ToList();
int bioSignals;
int geoSignals;
if (signalHistory.ContainsKey(scan.SystemAddress) && signalHistory[scan.SystemAddress].ContainsKey(scan.BodyID))
{
bioSignals = signalHistory[scan.SystemAddress][scan.BodyID].Signals.Where(s => s.Type == "$SAA_SignalType_Biological;").Select(s => s.Count).FirstOrDefault();
geoSignals = signalHistory[scan.SystemAddress][scan.BodyID].Signals.Where(s => s.Type == "$SAA_SignalType_Geological;").Select(s => s.Count).FirstOrDefault();
}
else
{
bioSignals = 0;
geoSignals = 0;
}
List<Parent> parents;
if (scan.Parent != null)
{
parents = new();
foreach (var parent in scan.Parent)
{
var parentScan = scanList.Where(s => s.BodyID == parent.Body);
parents.Add(new Parent
{
ParentType = parent.ParentType.ToString(),
Body = parent.Body,
Scan = parentScan.Any() ? parentScan.First() : null
});
}
}
else
{
parents = null;
}
try
{
var result = criteriaFunction.Value.Call(scan, parents, scanList, bioSignals, geoSignals);
if (result.Length == 3 && ((bool?)result[0]).GetValueOrDefault(false))
{
results.Add((result[1].ToString(), result[2].ToString(), false));
}
else if (result.Length == 2)
{
results.Add((result[0].ToString(), result[1].ToString(), false));
}
}
catch (LuaScriptException e)
{
results.Add((e.Message, scan.Json, false));
StringBuilder errorDetail = new();
errorDetail.AppendLine($"while processing custom criteria '{criteriaFunction.Key}' on scan:")
.AppendLine(scan.Json)
.AppendLine("To correct this problem, make changes to the Lua source file, save it and either re-run read-all or scan another body. It will be automatically reloaded.");
ErrorLogger(e, errorDetail.ToString());
CriteriaWithErrors.Add(criteriaFunction.Key, e.Message + Environment.NewLine + errorDetail);
}
}
// Remove any erroring criteria. They will be repopulated next time the file is parsed.
if (CriteriaWithErrors.Count > 0)
{
foreach (var criteriaKey in CriteriaWithErrors.Keys)
{
if (CriteriaFunctions.ContainsKey(criteriaKey)) CriteriaFunctions.Remove(criteriaKey);
}
}
if (ScanCount > 99)
{
ScanCount = 0;
LuaGC();
}
return results;
}
private string GetUniqueDescription(string functionName, string scriptDescription)
{
var uniqueDescription = functionName;
if (scriptDescription.ToLower().StartsWith("criteria="))
{
uniqueDescription += scriptDescription.Replace("criteria=", "=", StringComparison.CurrentCultureIgnoreCase);
}
return uniqueDescription;
}
private void LuaGC()
{
LuaState?.DoString("collectgarbage()");
}
internal class Parent
{
public string ParentType;
public int Body;
public Scan Scan;
}
}

389
Explorer/DefaultCriteria.cs Normal file
View File

@ -0,0 +1,389 @@
using System.Globalization;
using System.Text;
using Observatory.Framework.Files.Journal.Exploration;
using Observatory.Framework.Files.ParameterTypes;
namespace Explorer;
internal static class DefaultCriteria
{
public static List<(string Description, string Detail, bool SystemWide)> CheckInterest(Scan scan, Dictionary<ulong, Dictionary<int, Scan>> scanHistory, Dictionary<ulong, Dictionary<int, FSSBodySignals>> signalHistory, ExplorerSettings settings)
{
List<(string, string, bool)> results = new();
var textInfo = new CultureInfo("en-US", false).TextInfo;
var isRing = scan.BodyName.Contains("Ring");
#if DEBUG
// results.Add("Test Scan Event", "Test Detail");
#endif
#region Landable Checks
if (scan.Landable)
{
if (settings.LandableTerraformable && scan.TerraformState?.Length > 0)
{
results.Add($"Landable and {scan.TerraformState}");
}
if (settings.LandableRing && scan.Rings?.Count > 0)
{
results.Add("Ringed Landable Body");
}
if (settings.LandableAtmosphere && scan.Atmosphere.Length > 0)
{
results.Add("Landable with Atmosphere", textInfo.ToTitleCase(scan.Atmosphere));
}
if (settings.LandableHighG && scan.SurfaceGravity > 29.4)
{
results.Add("Landable with High Gravity", $"Surface gravity: {scan.SurfaceGravity / 9.81:0.00}g");
}
if (settings.LandableLarge && scan.Radius > 18000000)
{
results.Add("Landable Large Planet", $"Radius: {scan.Radius / 1000:0}km");
}
}
#endregion
#region Value Checks
if (settings.HighValueMappable)
{
IList<string> HighValueNonTerraformablePlanetClasses = new[] {
"Earthlike body",
"Ammonia world",
"Water world",
};
if (HighValueNonTerraformablePlanetClasses.Contains(scan.PlanetClass) || scan.TerraformState?.Length > 0)
{
var info = new StringBuilder();
if (!scan.WasMapped)
{
if (!scan.WasDiscovered)
info.Append("Undiscovered ");
else
info.Append("Unmapped ");
}
if (scan.TerraformState?.Length > 0)
info.Append("Terraformable ");
results.Add("High-Value Body", $"{info}{textInfo.ToTitleCase(scan.PlanetClass)}, {scan.DistanceFromArrivalLS:N0}Ls");
}
}
#endregion
#region Parent Relative Checks
if (scan.SystemAddress != 0 && scan.SemiMajorAxis != 0 &&
scanHistory[scan.SystemAddress].ContainsKey(scan.Parent[0].Body))
{
var parent = scanHistory[scan.SystemAddress][scan.Parent[0].Body];
if (settings.CloseOrbit && !isRing && parent.Radius * 3 > scan.SemiMajorAxis)
{
results.Add("Close Orbit", $"Orbital Radius: {scan.SemiMajorAxis / 1000:N0}km, Parent Radius: {parent.Radius / 1000:N0}km");
}
if (settings.ShepherdMoon && !isRing && parent.Rings?.Any() == true && parent.Rings.Last().OuterRad > scan.SemiMajorAxis && !parent.Rings.Last().Name.Contains("Belt"))
{
results.Add("Shepherd Moon", $"Orbit: {scan.SemiMajorAxis / 1000:N0}km, Ring Radius: {parent.Rings.Last().OuterRad / 1000:N0}km");
}
if (settings.CloseRing && parent.Rings?.Count > 0)
{
foreach (var ring in parent.Rings)
{
var separation = Math.Min(Math.Abs(scan.SemiMajorAxis - ring.OuterRad), Math.Abs(ring.InnerRad - scan.SemiMajorAxis));
if (separation < scan.Radius * 10)
{
var ringTypeName = ring.Name.Contains("Belt") ? "Belt" : "Ring";
var isLandable = scan.Landable ? ", Landable" : "";
results.Add($"Close {ringTypeName} Proximity",
$"Orbit: {scan.SemiMajorAxis / 1000:N0}km, Radius: {scan.Radius / 1000:N0}km, Distance from {ringTypeName.ToLower()}: {separation / 1000:N0}km{isLandable}");
}
}
}
}
#endregion
if (settings.DiverseLife && signalHistory.ContainsKey(scan.SystemAddress) && signalHistory[scan.SystemAddress].ContainsKey(scan.BodyID))
{
var bioSignals = signalHistory[scan.SystemAddress][scan.BodyID].Signals.Where(s => s.Type == "$SAA_SignalType_Biological;");
if (bioSignals.Count() > 0 && bioSignals.First().Count > 7)
{
results.Add("Diverse Life", $"Biological Signals: {bioSignals.First().Count}");
}
}
if (settings.WideRing && scan.Rings?.Count > 0)
{
foreach (var ring in scan.Rings.Where(r => !r.Name.Contains("Belt")))
{
var ringWidth = ring.OuterRad - ring.InnerRad;
if (ringWidth > scan.Radius * 5)
{
var ringName = ring.Name.Replace(scan.BodyName, "").Trim();
results.Add("Wide Ring", $"{ringName}: Width: {ringWidth / 299792458:N2}Ls / {ringWidth / 1000:N0}km, Parent Radius: {scan.Radius / 1000:N0}km");
}
}
}
if (settings.SmallObject && scan.StarType == null && scan.PlanetClass != null && scan.PlanetClass != "Barycentre" && scan.Radius < 300000)
{
results.Add("Small Object", $"Radius: {scan.Radius / 1000:N0}km");
}
if (settings.HighEccentricity && scan.Eccentricity > 0.9)
{
results.Add("Highly Eccentric Orbit", $"Eccentricity: {Math.Round(scan.Eccentricity, 4)}");
}
if (settings.Nested && !isRing && scan.Parent?.Count > 1 && scan.Parent[0].ParentType == ParentType.Planet && scan.Parent[1].ParentType == ParentType.Planet)
{
results.Add("Nested Moon");
}
if (settings.FastRotation && scan.RotationPeriod != 0 && !scan.TidalLock && Math.Abs(scan.RotationPeriod) < 28800 && !isRing && string.IsNullOrEmpty(scan.StarType))
{
results.Add("Non-locked Body with Fast Rotation", $"Period: {scan.RotationPeriod/3600:N1} hours");
}
if (settings.FastOrbit && scan.OrbitalPeriod != 0 && Math.Abs(scan.OrbitalPeriod) < 28800 && !isRing)
{
results.Add("Fast Orbit", $"Orbital Period: {Math.Abs(scan.OrbitalPeriod / 3600):N1} hours");
}
// Close binary pair
if ((settings.CloseBinary || settings.CollidingBinary) && scan.Parent?[0].ParentType == ParentType.Null && scan.Radius / scan.SemiMajorAxis > 0.4)
{
var binaryPartner = scanHistory[scan.SystemAddress].Where(priorScan => priorScan.Value.Parent?[0].Body == scan.Parent?[0].Body && scan.BodyID != priorScan.Key);
if (binaryPartner.Count() == 1)
{
if (binaryPartner.First().Value.Radius / binaryPartner.First().Value.SemiMajorAxis > 0.4)
{
if (settings.CollidingBinary && binaryPartner.First().Value.Radius + scan.Radius >= binaryPartner.First().Value.SemiMajorAxis * (1 - binaryPartner.First().Value.Eccentricity) + scan.SemiMajorAxis * (1 - scan.Eccentricity))
{
results.Add("COLLIDING binary", $"Orbit: {Math.Truncate((double)scan.SemiMajorAxis / 1000):N0}km, Radius: {Math.Truncate((double)scan.Radius / 1000):N0}km, Partner: {binaryPartner.First().Value.BodyName}");
}
else if (settings.CloseBinary)
{
results.Add("Close binary relative to body size", $"Orbit: {Math.Truncate((double)scan.SemiMajorAxis / 1000):N0}km, Radius: {Math.Truncate((double)scan.Radius / 1000):N0}km, Partner: {binaryPartner.First().Value.BodyName}");
}
}
}
}
if (settings.GoodFSDBody && scan.Landable)
{
List<string> boostMaterials = new()
{
"Carbon",
"Germanium",
"Arsenic",
"Niobium",
"Yttrium",
"Polonium"
};
if (boostMaterials.RemoveMatchedMaterials(scan) == 1)
{
results.Add("5 of 6 Premium Boost Materials", $"Missing material: {boostMaterials[0]}");
}
}
if ((settings.GreenSystem || settings.GoldSystem) && scan.Materials != null)
{
List<string> boostMaterials = new()
{
"Carbon",
"Germanium",
"Arsenic",
"Niobium",
"Yttrium",
"Polonium"
};
List<string> allSurfaceMaterials = new()
{
"Antimony", "Arsenic", "Cadmium", "Carbon",
"Chromium", "Germanium", "Iron", "Manganese",
"Mercury", "Molybdenum", "Nickel", "Niobium",
"Phosphorus", "Polonium", "Ruthenium", "Selenium",
"Sulphur", "Technetium", "Tellurium", "Tin",
"Tungsten", "Vanadium", "Yttrium", "Zinc",
"Zirconium"
};
var systemBodies = scanHistory[scan.SystemAddress];
var notifyGreen = false;
var notifyGold = false;
foreach (var body in systemBodies.Values)
{
// If we enter either check and the count is already zero then a
// previous body in system triggered the check, suppress notification.
if (settings.GreenSystem && body.Materials != null)
{
if (boostMaterials.Count == 0)
notifyGreen = false;
else if (boostMaterials.RemoveMatchedMaterials(body) == 0)
notifyGreen = true;
}
if (settings.GoldSystem && body.Materials != null)
{
if (allSurfaceMaterials.Count == 0)
notifyGold = false;
else if (allSurfaceMaterials.RemoveMatchedMaterials(body) == 0)
notifyGold = true;
}
}
if (notifyGreen)
results.Add("All Premium Boost Materials In System", string.Empty, true);
if (notifyGold)
results.Add("All Surface Materials In System", string.Empty, true);
}
if (settings.UncommonSecondary && scan.BodyID > 0 && !string.IsNullOrWhiteSpace(scan.StarType) && scan.DistanceFromArrivalLS > 10)
{
var excludeTypes = new List<string> { "O", "B", "A", "F", "G", "K", "M", "L", "T", "Y", "TTS" };
if (!excludeTypes.Contains(scan.StarType.ToUpper()))
{
results.Add("Uncommon Secondary Star Type", $"{GetUncommonStarTypeName(scan.StarType)}, Distance: {scan.DistanceFromArrivalLS:N0}Ls");
}
}
return results;
}
private static string GetUncommonStarTypeName(string starType)
{
string name;
switch (starType.ToLower())
{
case "b_bluewhitesupergiant":
name = "B Blue-White Supergiant";
break;
case "a_bluewhitesupergiant":
name = "A Blue-White Supergiant";
break;
case "f_whitesupergiant":
name = "F White Supergiant";
break;
case "g_whitesupergiant":
name = "G White Supergiant";
break;
case "k_orangegiant":
name = "K Orange Giant";
break;
case "m_redgiant":
name = "M Red Giant";
break;
case "m_redsupergiant":
name = "M Red Supergiant";
break;
case "aebe":
name = "Herbig Ae/Be";
break;
case "w":
case "wn":
case "wnc":
case "wc":
case "wo":
name = "Wolf-Rayet";
break;
case "c":
case "cs":
case "cn":
case "cj":
case "ch":
case "chd":
name = "Carbon Star";
break;
case "s":
name = "S-Type Star";
break;
case "ms":
name = "MS-Type Star";
break;
case "d":
case "da":
case "dab":
case "dao":
case "daz":
case "dav":
case "db":
case "dbz":
case "dbv":
case "do":
case "dov":
case "dq":
case "dc":
case "dcv":
case "dx":
name = "White Dwarf";
break;
case "n":
name = "Neutron Star";
break;
case "h":
name = "Black Hole";
break;
case "supermassiveblackhole":
name = "Supermassive Black Hole";
break;
case "x":
name = "Exotic Star";
break;
case "rogueplanet":
name = "Rogue Planet";
break;
case "tts":
case "t":
name = "T Tauri Type";
break;
default:
name = starType + "-Type Star";
break;
}
return name;
}
/// <summary>
/// Removes materials from the list if found on the specified body.
/// </summary>
/// <param name="materials"></param>
/// <param name="body"></param>
/// <returns>Count of materials remaining in list.</returns>
private static int RemoveMatchedMaterials(this List<string> materials, Scan body)
{
foreach (var material in body.Materials)
{
var matchedMaterial = materials.Find(mat => mat.Equals(material.Name, StringComparison.OrdinalIgnoreCase));
if (matchedMaterial != null)
{
materials.Remove(matchedMaterial);
}
}
return materials.Count;
}
private static void Add(this List<(string, string, bool)> results, string description, string detail = "", bool systemWide = false)
{
results.Add((description, detail, systemWide));
}
}

304
Explorer/Explorer.cs Normal file
View File

@ -0,0 +1,304 @@
using System.Reflection;
using System.Security;
using System.Text;
using Observatory.Framework;
using Observatory.Framework.Files.Journal.Exploration;
namespace Explorer;
internal class Explorer
{
private IObservatoryCore ObservatoryCore;
private ExplorerWorker ExplorerWorker;
private Dictionary<ulong, Dictionary<int, Scan>> SystemBodyHistory;
private Dictionary<ulong, Dictionary<int, FSSBodySignals>> BodySignalHistory;
private Dictionary<ulong, Dictionary<int, ScanBaryCentre>> BarycentreHistory;
private CustomCriteriaManager CustomCriteriaManager;
private DateTime CriteriaLastModified;
private string currentSystem = string.Empty;
internal Explorer(ExplorerWorker explorerWorker, IObservatoryCore core)
{
SystemBodyHistory = new();
BodySignalHistory = new();
BarycentreHistory = new();
ExplorerWorker = explorerWorker;
ObservatoryCore = core;
CustomCriteriaManager = new(core.GetPluginErrorLogger(explorerWorker));
CriteriaLastModified = new DateTime(0);
}
public void Clear()
{
SystemBodyHistory.Clear();
BodySignalHistory.Clear();
BarycentreHistory.Clear();
}
public void RecordSignal(FSSBodySignals bodySignals)
{
if (!BodySignalHistory.ContainsKey(bodySignals.SystemAddress))
{
BodySignalHistory.Add(bodySignals.SystemAddress, new Dictionary<int, FSSBodySignals>());
}
if (!BodySignalHistory[bodySignals.SystemAddress].ContainsKey(bodySignals.BodyID))
{
BodySignalHistory[bodySignals.SystemAddress].Add(bodySignals.BodyID, bodySignals);
}
}
public void RecordBarycentre(ScanBaryCentre scan)
{
if (!BarycentreHistory.ContainsKey(scan.SystemAddress))
{
BarycentreHistory.Add(scan.SystemAddress, new Dictionary<int, ScanBaryCentre>());
}
if (!BarycentreHistory[scan.SystemAddress].ContainsKey(scan.BodyID))
{
BarycentreHistory[scan.SystemAddress].Add(scan.BodyID, scan);
}
}
private static string IncrementOrdinal(string ordinal)
{
var ordChar = ordinal.ToCharArray().Last();
if (new[] {'Z', '9'}.Contains(ordChar))
{
ordinal = IncrementOrdinal(ordinal.Length == 1 ? " " : string.Empty + ordinal[..^1]);
ordChar = (char)(ordChar - 10);
}
return ordinal[..^1] + (char)(ordChar + 1);
}
private static string DecrementOrdinal(string ordinal)
{
var ordChar = ordinal.ToCharArray().Last();
if (new[] { 'A', '0' }.Contains(ordChar))
{
ordinal = DecrementOrdinal(ordinal.Length == 1 ? " " : string.Empty + ordinal[..^1]);
ordChar = (char)(ordChar + 10);
}
return ordinal[..^1] + (char)(ordChar - 1);
}
public Scan ConvertBarycentre(ScanBaryCentre barycentre, Scan childScan)
{
var childAffix = childScan.BodyName
.Replace(childScan.StarSystem, string.Empty).Trim();
string baryName;
if (!string.IsNullOrEmpty(childAffix))
{
var childOrdinal = childAffix.ToCharArray().Last();
// If the ID is one higher than the barycentre than this is the "first" child body
var lowChild = childScan.BodyID - barycentre.BodyID == 1;
string baryAffix;
// Barycentre ordinal always labelled as low before high, e.g. "AB"
if (lowChild)
{
baryAffix = childAffix + "-" + IncrementOrdinal(childAffix);
}
else
{
baryAffix = DecrementOrdinal(childAffix) + "-" + childAffix;
}
baryName = barycentre.StarSystem + " " + baryAffix;
}
else
{
// Without ordinals it's complicated to determine what the ordinal *should* be.
// Just name the barycentre after the child object.
baryName = childScan.BodyName + " Barycentre";
}
Scan barycentreScan = new()
{
Timestamp = barycentre.Timestamp,
BodyName = baryName,
Parents = childScan.Parents.RemoveAt(0),
PlanetClass = "Barycentre",
StarSystem = barycentre.StarSystem,
SystemAddress = barycentre.SystemAddress,
BodyID = barycentre.BodyID,
SemiMajorAxis = barycentre.SemiMajorAxis,
Eccentricity = barycentre.Eccentricity,
OrbitalInclination = barycentre.OrbitalInclination,
Periapsis = barycentre.Periapsis,
OrbitalPeriod = barycentre.OrbitalPeriod,
AscendingNode = barycentre.AscendingNode,
MeanAnomaly = barycentre.MeanAnomaly,
Json = barycentre.Json
};
return barycentreScan;
}
public void SetSystem(string potentialNewSystem)
{
if (string.IsNullOrEmpty(currentSystem) || currentSystem != potentialNewSystem)
{
currentSystem = potentialNewSystem;
if (ExplorerWorker.settings.OnlyShowCurrentSystem && !ObservatoryCore.IsLogMonitorBatchReading)
{
ObservatoryCore.SetGridItems(ExplorerWorker, [
typeof(ExplorerUIResults).GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Select(p => p.Name)
.ToDictionary(p => p, p => string.Empty)
]);
Clear();
}
}
}
public void ProcessScan(Scan scanEvent, bool readAll)
{
if (!readAll)
{
// if (File.Exists(criteriaFilePath))
// {
// var fileModified = new FileInfo(criteriaFilePath).LastWriteTime;
//
// if (fileModified != CriteriaLastModified)
// {
// try
// {
// CustomCriteriaManager.RefreshCriteria(criteriaFilePath);
// }
// catch (CriteriaLoadException e)
// {
// var exceptionResult = new ExplorerUIResults
// {
// BodyName = "Error Reading Custom Criteria File",
// Time = DateTime.Now.ToString("G"),
// Description = e.Message,
// Details = e.OriginalScript
// };
// ObservatoryCore.AddGridItem(ExplorerWorker, exceptionResult);
// }
//
// CriteriaLastModified = fileModified;
// }
// }
}
Dictionary<int, Scan> systemBodies;
if (SystemBodyHistory.ContainsKey(scanEvent.SystemAddress))
{
systemBodies = SystemBodyHistory[scanEvent.SystemAddress];
if (systemBodies.ContainsKey(scanEvent.BodyID))
{
if (scanEvent.SystemAddress != 0)
{
//We've already checked this object.
return;
}
}
}
else
{
systemBodies = new();
SystemBodyHistory.Add(scanEvent.SystemAddress, systemBodies);
}
if (SystemBodyHistory.Count > 1000)
{
foreach (var entry in SystemBodyHistory.Where(entry => entry.Key != scanEvent.SystemAddress).ToList())
{
SystemBodyHistory.Remove(entry.Key);
}
SystemBodyHistory.TrimExcess();
}
if (scanEvent.SystemAddress != 0 && !systemBodies.ContainsKey(scanEvent.BodyID))
systemBodies.Add(scanEvent.BodyID, scanEvent);
var results = DefaultCriteria.CheckInterest(scanEvent, SystemBodyHistory, BodySignalHistory, ExplorerWorker.settings);
if (BarycentreHistory.ContainsKey(scanEvent.SystemAddress) && scanEvent.Parent != null && BarycentreHistory[scanEvent.SystemAddress].ContainsKey(scanEvent.Parent[0].Body))
{
ProcessScan(ConvertBarycentre(BarycentreHistory[scanEvent.SystemAddress][scanEvent.Parent[0].Body], scanEvent), readAll);
}
// if (ExplorerWorker.settings.EnableCustomCriteria)
// results.AddRange(CustomCriteriaManager.CheckInterest(scanEvent, SystemBodyHistory, BodySignalHistory, ExplorerWorker.settings));
if (results.Count > 0)
{
StringBuilder notificationDetail = new();
StringBuilder notificationExtendedDetail = new();
foreach (var result in results)
{
var scanResult = new ExplorerUIResults
{
BodyName = result.SystemWide ? scanEvent.StarSystem : scanEvent.BodyName,
Time = scanEvent.TimestampDateTime.ToString("G"),
Description = result.Description,
Details = result.Detail
};
ObservatoryCore.AddGridItem(ExplorerWorker, scanResult);
notificationDetail.AppendLine(result.Description);
notificationExtendedDetail.AppendLine(result.Detail);
}
string bodyAffix;
if (scanEvent.StarSystem != null && scanEvent.BodyName.StartsWith(scanEvent.StarSystem))
{
bodyAffix = scanEvent.BodyName.Replace(scanEvent.StarSystem, string.Empty);
}
else
{
bodyAffix = string.Empty;
}
var bodyLabel = SecurityElement.Escape(scanEvent.PlanetClass == "Barycentre" ? "Barycentre" : "Body");
string spokenAffix;
if (bodyAffix.Length > 0)
{
if (bodyAffix.Contains("Ring"))
{
var ringIndex = bodyAffix.Length - 6;
// spokenAffix =
// "<say-as interpret-as=\"spell-out\">" + bodyAffix[..ringIndex]
// + "</say-as><break strength=\"weak\"/>" + SplitOrdinalForSsml(bodyAffix.Substring(ringIndex, 1))
// + bodyAffix[(ringIndex + 1)..];
}
else
{
//spokenAffix = SplitOrdinalForSsml(bodyAffix);
}
}
else
{
bodyLabel = "Primary Star";
spokenAffix = string.Empty;
}
throw new NotImplementedException("Scan Complete Notification Not Implemented");
// NotificationArgs args = new()
// {
// Title = bodyLabel + bodyAffix,
// TitleSsml = $"<speak version=\"1.0\" xmlns=\"http://www.w3.org/2001/10/synthesis\" xml:lang=\"en-US\"><voice name=\"\">{bodyLabel} {spokenAffix}</voice></speak>",
// Detail = notificationDetail.ToString(),
// Sender = ExplorerWorker.ShortName,
// ExtendedDetails = notificationExtendedDetail.ToString(),
// CoalescingId = scanEvent.BodyID,
// };
//
// ObservatoryCore.SendNotification(args);
}
}
}

17
Explorer/Explorer.csproj Normal file
View File

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>Explorer</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="NLua" Version="1.7.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ObservatoryFramework\ObservatoryFramework.csproj" />
</ItemGroup>
</Project>

25
Explorer/Explorer.sln Normal file
View File

@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.31205.134
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ObservatoryExplorer", "ObservatoryExplorer.csproj", "{E0FCF2A2-BF56-4F4D-836B-92A0E8269192}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{E0FCF2A2-BF56-4F4D-836B-92A0E8269192}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E0FCF2A2-BF56-4F4D-836B-92A0E8269192}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E0FCF2A2-BF56-4F4D-836B-92A0E8269192}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E0FCF2A2-BF56-4F4D-836B-92A0E8269192}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {196B0F23-25FC-4A58-A7A9-2676C7749FFD}
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,78 @@
using Observatory.Framework;
namespace Explorer;
public class ExplorerSettings
{
[SettingDisplayName("Only Show Current System")]
public bool OnlyShowCurrentSystem { get; set; }
[SettingDisplayName("Landable & Terraformable")]
public bool LandableTerraformable { get; set; }
[SettingDisplayName("Landable w/ Atmosphere")]
public bool LandableAtmosphere { get; set; }
[SettingDisplayName("Landable High-g")]
public bool LandableHighG { get; set; }
[SettingDisplayName("Landable Large")]
public bool LandableLarge { get; set; }
[SettingDisplayName("Close Orbit")]
public bool CloseOrbit { get; set; }
[SettingDisplayName("Shepherd Moon")]
public bool ShepherdMoon { get; set; }
[SettingDisplayName("Wide Ring")]
public bool WideRing { get; set; }
[SettingDisplayName("Close Binary")]
public bool CloseBinary { get; set; }
[SettingDisplayName("Colliding Binary")]
public bool CollidingBinary { get; set; }
[SettingDisplayName("Close Ring Proximity")]
public bool CloseRing { get; set; }
[SettingDisplayName("Codex Discoveries")]
public bool Codex { get; set; }
[SettingDisplayName("Uncommon Secondary Star")]
public bool UncommonSecondary { get; set; }
[SettingDisplayName("Landable w/ Ring")]
public bool LandableRing { get; set; }
[SettingDisplayName("Nested Moon")]
public bool Nested { get; set; }
[SettingDisplayName("Small Object")]
public bool SmallObject { get; set; }
[SettingDisplayName("Fast Rotation")]
public bool FastRotation { get; set; }
[SettingDisplayName("Fast Orbit")]
public bool FastOrbit { get; set; }
[SettingDisplayName("High Eccentricity")]
public bool HighEccentricity { get; set; }
[SettingDisplayName("Diverse Life")]
public bool DiverseLife { get; set; }
[SettingDisplayName("Good FSD Injection")]
public bool GoodFSDBody { get; set; }
[SettingDisplayName("All FSD Mats In System")]
public bool GreenSystem { get; set; }
[SettingDisplayName("All Surface Mats In System")]
public bool GoldSystem { get; set; }
[SettingDisplayName("High-Value Body")]
public bool HighValueMappable { get; set; }
}

View File

@ -0,0 +1,12 @@
namespace Explorer;
public class ExplorerUIResults
{
public string Time { get; set; }
public string BodyName { get; set; }
public string Description { get; set; }
public string Details { get; set; }
}

121
Explorer/Worker.cs Normal file
View File

@ -0,0 +1,121 @@
using System.Collections.ObjectModel;
using System.Reflection;
using Observatory.Framework;
using Observatory.Framework.Files.Journal;
using Observatory.Framework.Files.Journal.Exploration;
using Observatory.Framework.Files.Journal.FleetCarrier;
using Observatory.Framework.Files.Journal.Travel;
namespace Explorer;
public class ExplorerWorker : IObservatoryWorker
{
public ExplorerWorker()
{
settings = new()
{
CloseBinary = true,
CloseOrbit = true,
CloseRing = true,
CollidingBinary = true,
FastRotation = true,
HighEccentricity = true,
LandableAtmosphere = true,
LandableHighG = true,
LandableLarge = true,
LandableRing = true,
LandableTerraformable = true,
Nested = true,
ShepherdMoon = true,
SmallObject = true,
WideRing = true
};
resultsGrid = new();
}
private Explorer explorer;
private ObservableCollection<object> resultsGrid;
private IObservatoryCore Core;
private bool recordProcessedSinceBatchStart;
public string Name => "Observatory Explorer";
public string ShortName => "Explorer";
public string Version => typeof(ExplorerWorker).Assembly.GetName().Version.ToString();
private PluginUI pluginUI;
public PluginUI PluginUI => pluginUI;
public void Load(IObservatoryCore observatoryCore)
{
explorer = new Explorer(this, observatoryCore);
resultsGrid.Add(new ExplorerUIResults());
pluginUI = new PluginUI(resultsGrid);
Core = observatoryCore;
}
public void JournalEvent<TJournal>(TJournal journal) where TJournal : JournalBase
{
switch (journal)
{
case Scan scan:
explorer.ProcessScan(scan, Core.IsLogMonitorBatchReading && recordProcessedSinceBatchStart);
// Set this *after* the first scan processes so that we get the current custom criteria file.
if (Core.IsLogMonitorBatchReading) recordProcessedSinceBatchStart = true;
break;
case FSSBodySignals signals:
explorer.RecordSignal(signals);
break;
case ScanBaryCentre barycentre:
explorer.RecordBarycentre(barycentre);
break;
case FSDJump fsdjump:
if (fsdjump is CarrierJump && !((CarrierJump)fsdjump).Docked)
break;
explorer.SetSystem(fsdjump.StarSystem);
break;
case Location location:
explorer.SetSystem(location.StarSystem);
break;
case DiscoveryScan discoveryScan:
break;
case FSSDiscoveryScan discoveryScan:
break;
case FSSSignalDiscovered signalDiscovered:
break;
case NavBeaconScan beaconScan:
break;
case SAAScanComplete scanComplete:
break;
case SAASignalsFound signalsFound:
break;
}
}
public void LogMonitorStateChanged(LogMonitorStateChangedEventArgs args)
{
if (LogMonitorStateChangedEventArgs.IsBatchRead(args.NewState))
{
// Beginning a batch read. Clear grid.
recordProcessedSinceBatchStart = false;
Core.SetGridItems(this, [
typeof(ExplorerUIResults).GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Select(p => p.Name)
.ToDictionary(p => p, p => string.Empty)
]);
explorer.Clear();
}
}
public object Settings
{
get => settings;
set => settings = (ExplorerSettings)value;
}
internal ExplorerSettings settings;
}