2
0
mirror of https://github.com/9ParsonsB/Pulsar.git synced 2025-04-05 17:39:39 -04:00

Rework Journal File Reading

Remove Explorer
Remove Plugin Architecture
This commit is contained in:
Ben Parsons 2024-04-14 21:51:56 +10:00
parent c0c69dcdf7
commit 256ebb179e
42 changed files with 855 additions and 2807 deletions

View File

@ -0,0 +1,8 @@
namespace Botanist;
class BioPlanetDetail
{
public string BodyName { get; set; }
public int BioTotal { get; set; }
public Dictionary<string, BioSampleDetail> SpeciesFound { get; set; }
}

View File

@ -0,0 +1,7 @@
namespace Botanist;
class BioSampleDetail
{
public string Genus { get; set; }
public bool Analysed { get; set; }
}

33
Botanist/BodyAddress.cs Normal file
View File

@ -0,0 +1,33 @@
namespace Botanist;
class BodyAddress
{
public ulong SystemAddress { get; set; }
public int BodyID { get; set; }
public override bool Equals(object obj)
{
// We want value equality here.
//
// See the full list of guidelines at
// http://go.microsoft.com/fwlink/?LinkID=85237
// and also the guidance for operator== at
// http://go.microsoft.com/fwlink/?LinkId=85238
//
if (obj == null || GetType() != obj.GetType())
{
return false;
}
var other = (BodyAddress)obj;
return other.SystemAddress == SystemAddress
&& other.BodyID == BodyID;
}
public override int GetHashCode()
{
return HashCode.Combine(SystemAddress, BodyID);
}
}

View File

@ -1,5 +1,4 @@
using System.Collections.ObjectModel;
using System.Reflection;
using Observatory.Framework;
using Observatory.Framework.Files.Journal;
using Observatory.Framework.Files.Journal.Exploration;
@ -19,7 +18,7 @@ public class Botanist : IObservatoryWorker
// To make this journal locale agnostic, use the genus identifier and map to English names used in notifications.
// Note: Values here are also used in the lookup for colony distance, so we also use this to resolve misspellings and Frontier bugs.
private readonly Dictionary<string, string> EnglishGenusByIdentifier = new()
public static readonly IReadOnlyDictionary<string, string> EnglishGenusByIdentifier = new Dictionary<string, string>
{
{ "$Codex_Ent_Aleoids_Genus_Name;", "Aleoida" },
{ "$Codex_Ent_Bacterial_Genus_Name;", "Bacterium" },
@ -51,7 +50,7 @@ public class Botanist : IObservatoryWorker
};
// Note: Some Horizons bios may be missing, but they'll get localized genus name and default colony distance
private readonly Dictionary<string, int> ColonyDistancesByGenus = new()
public static readonly IReadOnlyDictionary<string, int> ColonyDistancesByGenus = new Dictionary<string, int>()
{
{ "Aleoida", 150 },
{ "Bacterium", 500 },
@ -99,7 +98,7 @@ public class Botanist : IObservatoryWorker
public object Settings
{
get => botanistSettings;
set { botanistSettings = (BotanistSettings)value; }
set => botanistSettings = (BotanistSettings)value;
}
public void JournalEvent<TJournal>(TJournal journal) where TJournal : JournalBase
@ -216,8 +215,6 @@ public class Botanist : IObservatoryWorker
break;
}
}
UpdateUIGrid();
}
break;
case LeaveBody:
@ -259,121 +256,4 @@ public class Botanist : IObservatoryWorker
Core = observatoryCore;
}
public void LogMonitorStateChanged(LogMonitorStateChangedEventArgs args)
{
if (LogMonitorStateChangedEventArgs.IsBatchRead(args.NewState))
{
// Beginning a batch read. Clear grid.
Core.SetGridItems(this,
[
typeof(BotanistGrid).GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Select(p => p.Name)
.ToDictionary(p => p, p => string.Empty)
]);
}
else if (LogMonitorStateChangedEventArgs.IsBatchRead(args.PreviousState))
{
// Batch read is complete. Show data.
UpdateUIGrid();
}
}
private void UpdateUIGrid()
{
// Suppress repainting the entire contents of the grid on every ScanOrganic record we read.
if (Core.IsLogMonitorBatchReading) return;
Core.SetGridItems(this, [
typeof(BotanistGrid).GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Select(p => p.Name)
.ToDictionary(p => p, p => string.Empty)
]);
foreach (var bioPlanet in BioPlanets.Values)
{
if (bioPlanet.SpeciesFound.Count == 0)
{
var planetRow = new BotanistGrid
{
Body = bioPlanet.BodyName,
BioTotal = bioPlanet.BioTotal.ToString(),
Species = "(NO SAMPLES TAKEN)",
Analysed = string.Empty,
ColonyDistance = string.Empty,
};
Core.AddGridItem(this, planetRow);
}
var firstRow = true;
foreach (var entry in bioPlanet.SpeciesFound)
{
var colonyDistance =
ColonyDistancesByGenus.GetValueOrDefault(entry.Value.Genus ?? "", DEFAULT_COLONY_DISTANCE);
var speciesRow = new BotanistGrid
{
Body = firstRow ? bioPlanet.BodyName : string.Empty,
BioTotal = firstRow ? bioPlanet.BioTotal.ToString() : string.Empty,
Species = entry.Key,
Analysed = entry.Value.Analysed ? "✓" : "",
ColonyDistance = $"{colonyDistance}m",
};
Core.AddGridItem(this, speciesRow);
firstRow = false;
}
}
}
}
class BodyAddress
{
public ulong SystemAddress { get; set; }
public int BodyID { get; set; }
public override bool Equals(object obj)
{
// We want value equality here.
//
// See the full list of guidelines at
// http://go.microsoft.com/fwlink/?LinkID=85237
// and also the guidance for operator== at
// http://go.microsoft.com/fwlink/?LinkId=85238
//
if (obj == null || GetType() != obj.GetType())
{
return false;
}
var other = (BodyAddress)obj;
return other.SystemAddress == SystemAddress
&& other.BodyID == BodyID;
}
public override int GetHashCode()
{
return HashCode.Combine(SystemAddress, BodyID);
}
}
class BioPlanetDetail
{
public string BodyName { get; set; }
public int BioTotal { get; set; }
public Dictionary<string, BioSampleDetail> SpeciesFound { get; set; }
}
class BioSampleDetail
{
public string Genus { get; set; }
public bool Analysed { get; set; }
}
public class BotanistGrid
{
public string Body { get; set; }
public string BioTotal { get; set; }
public string Species { get; set; }
public string Analysed { get; set; }
public string ColonyDistance { get; set; }
}

10
Botanist/BotanistGrid.cs Normal file
View File

@ -0,0 +1,10 @@
namespace Botanist;
public class BotanistGrid
{
public string Body { get; set; }
public string BioTotal { get; set; }
public string Species { get; set; }
public string Analysed { get; set; }
public string ColonyDistance { get; set; }
}

View File

@ -1,13 +0,0 @@
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

@ -1,382 +0,0 @@
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;
}
}

View File

@ -1,389 +0,0 @@
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));
}
}

View File

@ -1,304 +0,0 @@
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);
}
}
}

View File

@ -1,17 +0,0 @@
<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>

View File

@ -1,25 +0,0 @@

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

@ -1,78 +0,0 @@
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

@ -1,12 +0,0 @@
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; }
}

View File

@ -1,121 +0,0 @@
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;
}

View File

@ -1,20 +1,22 @@
namespace Observatory.Framework;
using System.Text.Json.Nodes;
namespace Observatory.Framework;
/// <summary>
/// Provides data for Elite Dangerous journal events.
/// </summary>
public class JournalEventArgs : EventArgs
public class JournalEventArgs(string journalEventType, JsonObject journalEvent) : EventArgs
{
/// <summary>
/// <para>Type of journal entry that triggered event.</para>
/// <para>Will be a class from the Observatory.Framework.Files.Journal namespace derived from JournalBase, or JournalBase itself in the case of an unhandled journal event type.</para>
/// </summary>
public Type journalType;
public string JournalEventType = journalEventType;
/// <summary>
/// <para>Elite Dangerous journal event, deserialized into a .NET object of the type specified by JournalEventArgs.journalType.</para>
/// <para>Unhandled json values within a journal entry type will be contained in member property:<br/>Dictionary&lt;string, object&gt; AdditionalProperties.</para>
/// </summary>
public object journalEvent;
public JsonObject JournalEvent = journalEvent;
}
/// <summary>
@ -26,48 +28,59 @@ public class NotificationArgs
/// Text typically displayed as header content.
/// </summary>
public string Title;
/// <summary>
/// SSML representation of Title text.<br/>
/// This value is optional, if omitted the value of <c>NotificationArgs.Title</c> will be used for voice synthesis without markup.
/// </summary>
public string TitleSsml;
/// <summary>
/// Text typically displayed as body content.
/// </summary>
public string Detail;
/// <summary>
/// SSML representation of Detail text.<br/>
/// This value is optional, if omitted the value of <c>NotificationArgs.Detail</c> will be used for voice synthesis without markup.
/// </summary>
public string DetailSsml;
/// <summary>
/// Specify window timeout in ms (overrides Core setting). Specify 0 timeout to persist until removed via IObservatoryCore.CancelNotification. Default -1 (use Core setting).
/// </summary>
public int Timeout = -1;
/// <summary>
/// Specify window X position as a percentage from upper left corner (overrides Core setting). Default -1.0 (use Core setting).
/// </summary>
public double XPos = -1.0;
/// <summary>
/// Specify window Y position as a percentage from upper left corner (overrides Core setting). Default -1.0 (use Core setting).
/// </summary>
public double YPos = -1.0;
/// <summary>
/// Specifies the desired renderings of the notification. Defaults to <see cref="NotificationRendering.All"/>.
/// </summary>
public NotificationRendering Rendering = NotificationRendering.All;
/// <summary>
/// Specifies if some part of the notification should be suppressed. Not supported by all notifiers. Defaults to <see cref="NotificationSuppression.None"/>.
/// </summary>
public NotificationSuppression Suppression = NotificationSuppression.None;
/// <summary>
/// The plugin sending this notification.
/// </summary>
public string Sender = "";
/// <summary>
/// Additional notification detailed (generally not rendered by voice or popup; potentially used by aggregating/logging plugins).
/// </summary>
public string ExtendedDetails;
/// <summary>
/// A value which allows grouping of notifications together. For example, values &gt;= 0 &lt;= 1000 could be system body IDs, -1 is the system, anything else is arbitrary.
/// </summary>
@ -84,10 +97,12 @@ public enum NotificationSuppression
/// No suppression.
/// </summary>
None = 0,
/// <summary>
/// Suppress title.
/// </summary>
Title = 1,
/// <summary>
/// Suppress detail.
/// </summary>
@ -104,14 +119,17 @@ public enum NotificationRendering
/// Send notification to native visual popup notificaiton handler.
/// </summary>
NativeVisual = 1,
/// <summary>
/// Send notification to native speech notification handler.
/// </summary>
NativeVocal = 2,
/// <summary>
/// Send notification to all installed notifier plugins.
/// </summary>
PluginNotifier = 4,
/// <summary>
/// Send notification to all available handlers.
/// </summary>
@ -128,18 +146,21 @@ public enum LogMonitorState : uint
/// Monitoring is stopped.
/// </summary>
Idle = 0,
/// <summary>
/// Real-time monitoring is active.
/// </summary>
Realtime = 1,
/// <summary>
/// Batch read of historical journals is in progress.
/// </summary>
Batch = 2,
BatchProcessing = 2,
/// <summary>
/// Batch read of recent journals is in progress to establish current player state.
/// Initial read of recent journals to establish current player state is in progress .
/// </summary>
PreRead = 4
Init = 4
}
/// <summary>
@ -164,6 +185,6 @@ public class LogMonitorStateChangedEventArgs : EventArgs
/// <returns>A boolean; True iff the state provided represents a batch-mode read.</returns>
public static bool IsBatchRead(LogMonitorState state)
{
return state.HasFlag(LogMonitorState.Batch) || state.HasFlag(LogMonitorState.PreRead);
return state.HasFlag(LogMonitorState.BatchProcessing) || state.HasFlag(LogMonitorState.Init);
}
}

View File

@ -5,11 +5,5 @@ public class CarrierDecommission : JournalBase
public ulong CarrierID { get; init; }
public long ScrapRefund { get; init; }
public long ScrapTime { get; init; }
public DateTime ScrapTimeUTC
{
get
{
return DateTimeOffset.FromUnixTimeSeconds(ScrapTime).UtcDateTime;
}
}
public DateTime ScrapTimeUTC => DateTimeOffset.FromUnixTimeSeconds(ScrapTime).UtcDateTime;
}

View File

@ -1,6 +1,4 @@
using System.Text.Json.Serialization;
namespace Observatory.Framework.Files.Journal.FleetCarrier;
namespace Observatory.Framework.Files.Journal.FleetCarrier;
public class CarrierJumpRequest : JournalBase
{
@ -10,10 +8,5 @@ public class CarrierJumpRequest : JournalBase
public ulong CarrierID { get; init; }
public string SystemName { get; init; }
public ulong SystemID { get; init; }
public string DepartureTime { get; init; }
[JsonIgnore]
public DateTimeOffset DepartureTimeDateTime {
get => ParseDateTime(DepartureTime);
}
public DateTimeOffset DepartureTime { get; init; }
}

View File

@ -1,52 +1,289 @@
using System.Globalization;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization;
using Observatory.Framework.Files.Journal.Combat;
using Observatory.Framework.Files.Journal.Exploration;
using Observatory.Framework.Files.Journal.FleetCarrier;
using Observatory.Framework.Files.Journal.Odyssey;
using Observatory.Framework.Files.Journal.Other;
using Observatory.Framework.Files.Journal.Powerplay;
using Observatory.Framework.Files.Journal.Squadron;
using Observatory.Framework.Files.Journal.Startup;
using Observatory.Framework.Files.Journal.StationServices;
using Observatory.Framework.Files.Journal.Trade;
using Observatory.Framework.Files.Journal.Travel;
namespace Observatory.Framework.Files.Journal;
[JsonDerivedType(typeof(BackpackFile))]
[JsonDerivedType(typeof(CargoFile))]
[JsonDerivedType(typeof(FCMaterialsFile))]
[JsonDerivedType(typeof(Bounty))]
[JsonDerivedType(typeof(CapShipBound))]
[JsonDerivedType(typeof(Died))]
[JsonDerivedType(typeof(EscapeInterdiction))]
[JsonDerivedType(typeof(FactionKillBond))]
[JsonDerivedType(typeof(FighterDestroyed))]
[JsonDerivedType(typeof(HeatDamage))]
[JsonDerivedType(typeof(HeatWarning))]
[JsonDerivedType(typeof(HullDamage))]
[JsonDerivedType(typeof(Interdicted))]
[JsonDerivedType(typeof(Interdiction))]
[JsonDerivedType(typeof(PVPKill))]
[JsonDerivedType(typeof(SRVDestroyed))]
[JsonDerivedType(typeof(ShieldState))]
[JsonDerivedType(typeof(ShipTargeted))]
[JsonDerivedType(typeof(UnderAttack))]
[JsonDerivedType(typeof(BuyExplorationData))]
[JsonDerivedType(typeof(CodexEntry))]
[JsonDerivedType(typeof(DiscoveryScan))]
[JsonDerivedType(typeof(FSSAllBodiesFound))]
[JsonDerivedType(typeof(FSSBodySignals))]
[JsonDerivedType(typeof(FSSDiscoveryScan))]
[JsonDerivedType(typeof(FSSSignalDiscovered))]
[JsonDerivedType(typeof(MaterialCollected))]
[JsonDerivedType(typeof(MaterialDiscarded))]
[JsonDerivedType(typeof(MaterialDiscovered))]
[JsonDerivedType(typeof(MultiSellExplorationData))]
[JsonDerivedType(typeof(NavBeaconScan))]
[JsonDerivedType(typeof(SAAScanComplete))]
[JsonDerivedType(typeof(SAASignalsFound))]
[JsonDerivedType(typeof(Scan))]
[JsonDerivedType(typeof(ScanBaryCentre))]
[JsonDerivedType(typeof(Screenshot))]
[JsonDerivedType(typeof(SellExplorationData))]
[JsonDerivedType(typeof(CarrierBankTransfer))]
[JsonDerivedType(typeof(CarrierBuy))]
[JsonDerivedType(typeof(CarrierCancelDecommission))]
[JsonDerivedType(typeof(CarrierCrewServices))]
[JsonDerivedType(typeof(CarrierDecommission))]
[JsonDerivedType(typeof(CarrierDepositFuel))]
[JsonDerivedType(typeof(CarrierDockingPermission))]
[JsonDerivedType(typeof(CarrierFinance))]
[JsonDerivedType(typeof(CarrierJump))]
[JsonDerivedType(typeof(CarrierJumpCancelled))]
[JsonDerivedType(typeof(CarrierJumpRequest))]
[JsonDerivedType(typeof(CarrierModulePack))]
[JsonDerivedType(typeof(CarrierShipPack))]
[JsonDerivedType(typeof(CarrierStats))]
[JsonDerivedType(typeof(CarrierTradeOrder))]
[JsonDerivedType(typeof(FCMaterlas))]
[JsonDerivedType(typeof(InvalidJson))]
[JsonDerivedType(typeof(BackPack))]
[JsonDerivedType(typeof(BackpackChange))]
[JsonDerivedType(typeof(BackpackMaterials))]
[JsonDerivedType(typeof(BookDropship))]
[JsonDerivedType(typeof(BookTaxi))]
[JsonDerivedType(typeof(BuyMicroResources))]
[JsonDerivedType(typeof(BuySuit))]
[JsonDerivedType(typeof(BuyWeapon))]
[JsonDerivedType(typeof(CancelDropship))]
[JsonDerivedType(typeof(CancelTaxi))]
[JsonDerivedType(typeof(CollectItems))]
[JsonDerivedType(typeof(CreateSuitLoadout))]
[JsonDerivedType(typeof(DeleteSuitLoadout))]
[JsonDerivedType(typeof(Disembark))]
[JsonDerivedType(typeof(DropItems))]
[JsonDerivedType(typeof(DropShipDeploy))]
[JsonDerivedType(typeof(Embark))]
[JsonDerivedType(typeof(FCMaterials))]
[JsonDerivedType(typeof(LoadoutEquipModule))]
[JsonDerivedType(typeof(LoadoutRemoveModule))]
[JsonDerivedType(typeof(RenameSuitLoadout))]
[JsonDerivedType(typeof(ScanOrganic))]
[JsonDerivedType(typeof(SellMicroResources))]
[JsonDerivedType(typeof(SellOrganicData))]
[JsonDerivedType(typeof(SellSuit))]
[JsonDerivedType(typeof(SellWeapon))]
[JsonDerivedType(typeof(ShipLockerMaterials))]
[JsonDerivedType(typeof(SuitLoadout))]
[JsonDerivedType(typeof(SwitchSuitLoadout))]
[JsonDerivedType(typeof(TradeMicroResources))]
[JsonDerivedType(typeof(TransferMicroResources))]
[JsonDerivedType(typeof(UpgradeSuit))]
[JsonDerivedType(typeof(UpgradeWeapon))]
[JsonDerivedType(typeof(UseConsumable))]
[JsonDerivedType(typeof(AfmuRepairs))]
[JsonDerivedType(typeof(ApproachSettlement))]
[JsonDerivedType(typeof(CargoTransfer))]
[JsonDerivedType(typeof(ChangeCrewRole))]
[JsonDerivedType(typeof(CockpitBreached))]
[JsonDerivedType(typeof(CommitCrime))]
[JsonDerivedType(typeof(Continued))]
[JsonDerivedType(typeof(CrewLaunchFighter))]
[JsonDerivedType(typeof(CrewMemberJoins))]
[JsonDerivedType(typeof(CrewMemberQuits))]
[JsonDerivedType(typeof(CrewMemberRoleChange))]
[JsonDerivedType(typeof(CrimeVictim))]
[JsonDerivedType(typeof(DataScanned))]
[JsonDerivedType(typeof(DatalinkScan))]
[JsonDerivedType(typeof(DatalinkVoucher))]
[JsonDerivedType(typeof(DockFighter))]
[JsonDerivedType(typeof(DockSRV))]
[JsonDerivedType(typeof(EndCrewSession))]
[JsonDerivedType(typeof(FighterRebuilt))]
[JsonDerivedType(typeof(Friends))]
[JsonDerivedType(typeof(FuelScoop))]
[JsonDerivedType(typeof(JetConeBoost))]
[JsonDerivedType(typeof(JetConeDamage))]
[JsonDerivedType(typeof(JoinACrew))]
[JsonDerivedType(typeof(KickCrewMember))]
[JsonDerivedType(typeof(LaunchDrone))]
[JsonDerivedType(typeof(LaunchFighter))]
[JsonDerivedType(typeof(LaunchSRV))]
[JsonDerivedType(typeof(ModuleInfo))]
[JsonDerivedType(typeof(Music))]
[JsonDerivedType(typeof(NpcCrewPaidWage))]
[JsonDerivedType(typeof(NpcCrewRank))]
[JsonDerivedType(typeof(Promotion))]
[JsonDerivedType(typeof(ProspectedAsteroid))]
[JsonDerivedType(typeof(QuitACrew))]
[JsonDerivedType(typeof(RebootRepair))]
[JsonDerivedType(typeof(ReceiveText))]
[JsonDerivedType(typeof(RepairDrone))]
[JsonDerivedType(typeof(ReservoirReplenished))]
[JsonDerivedType(typeof(Resurrect))]
[JsonDerivedType(typeof(Scanned))]
[JsonDerivedType(typeof(SelfDestruct))]
[JsonDerivedType(typeof(SendText))]
[JsonDerivedType(typeof(Shutdown))]
[JsonDerivedType(typeof(Synthesis))]
[JsonDerivedType(typeof(SystemsShutdown))]
[JsonDerivedType(typeof(USSDrop))]
[JsonDerivedType(typeof(VehicleSwitch))]
[JsonDerivedType(typeof(WingAdd))]
[JsonDerivedType(typeof(WingInvite))]
[JsonDerivedType(typeof(WingJoin))]
[JsonDerivedType(typeof(WingLeave))]
[JsonDerivedType(typeof(PowerplayCollect))]
[JsonDerivedType(typeof(PowerplayDefect))]
[JsonDerivedType(typeof(PowerplayDeliver))]
[JsonDerivedType(typeof(PowerplayFastTrack))]
[JsonDerivedType(typeof(PowerplayJoin))]
[JsonDerivedType(typeof(PowerplayLeave))]
[JsonDerivedType(typeof(PowerplaySalary))]
[JsonDerivedType(typeof(PowerplayVote))]
[JsonDerivedType(typeof(PowerplayVoucher))]
[JsonDerivedType(typeof(AppliedToSquadron))]
[JsonDerivedType(typeof(DisbandedSquadron))]
[JsonDerivedType(typeof(InvitedToSquadron))]
[JsonDerivedType(typeof(JoinedSquadron))]
[JsonDerivedType(typeof(KickedFromSquadron))]
[JsonDerivedType(typeof(LeftSquadron))]
[JsonDerivedType(typeof(SharedBookmarkToSquadron))]
[JsonDerivedType(typeof(SquadronCreated))]
[JsonDerivedType(typeof(SquadronDemotion))]
[JsonDerivedType(typeof(SquadronPromotion))]
[JsonDerivedType(typeof(SquadronStartup))]
[JsonDerivedType(typeof(WonATrophyForSquadron))]
[JsonDerivedType(typeof(Cargo))]
[JsonDerivedType(typeof(ClearSavedGame))]
[JsonDerivedType(typeof(Commander))]
[JsonDerivedType(typeof(FileHeader))]
[JsonDerivedType(typeof(LoadGame))]
[JsonDerivedType(typeof(Loadout))]
[JsonDerivedType(typeof(Materials))]
[JsonDerivedType(typeof(Missions))]
[JsonDerivedType(typeof(NewCommander))]
[JsonDerivedType(typeof(Passengers))]
[JsonDerivedType(typeof(Startup.Powerplay))]
[JsonDerivedType(typeof(Progress))]
[JsonDerivedType(typeof(Rank))]
[JsonDerivedType(typeof(Reputation))]
[JsonDerivedType(typeof(Statistics))]
[JsonDerivedType(typeof(BuyAmmo))]
[JsonDerivedType(typeof(BuyDrones))]
[JsonDerivedType(typeof(CargoDepot))]
[JsonDerivedType(typeof(ClearImpound))]
[JsonDerivedType(typeof(CommunityGoal))]
[JsonDerivedType(typeof(CommunityGoalDiscard))]
[JsonDerivedType(typeof(CommunityGoalJoin))]
[JsonDerivedType(typeof(CommunityGoalReward))]
[JsonDerivedType(typeof(CrewAssign))]
[JsonDerivedType(typeof(CrewFire))]
[JsonDerivedType(typeof(CrewHire))]
[JsonDerivedType(typeof(EngineerApply))]
[JsonDerivedType(typeof(EngineerContribution))]
[JsonDerivedType(typeof(EngineerCraft))]
[JsonDerivedType(typeof(EngineerLegacyConvert))]
[JsonDerivedType(typeof(EngineerProgress))]
[JsonDerivedType(typeof(FetchRemoteModule))]
[JsonDerivedType(typeof(Market))]
[JsonDerivedType(typeof(MassModuleStore))]
[JsonDerivedType(typeof(MaterialTrade))]
[JsonDerivedType(typeof(MissionAbandoned))]
[JsonDerivedType(typeof(MissionAccepted))]
[JsonDerivedType(typeof(MissionCompleted))]
[JsonDerivedType(typeof(MissionFailed))]
[JsonDerivedType(typeof(MissionRedirected))]
[JsonDerivedType(typeof(ModuleBuy))]
[JsonDerivedType(typeof(ModuleRetrieve))]
[JsonDerivedType(typeof(ModuleSell))]
[JsonDerivedType(typeof(ModuleSellRemote))]
[JsonDerivedType(typeof(ModuleStore))]
[JsonDerivedType(typeof(ModuleSwap))]
[JsonDerivedType(typeof(Outfitting))]
[JsonDerivedType(typeof(PayBounties))]
[JsonDerivedType(typeof(PayFines))]
[JsonDerivedType(typeof(PayLegacyFines))]
[JsonDerivedType(typeof(RedeemVoucher))]
[JsonDerivedType(typeof(RefuelAll))]
[JsonDerivedType(typeof(RefuelPartial))]
[JsonDerivedType(typeof(Repair))]
[JsonDerivedType(typeof(RepairAll))]
[JsonDerivedType(typeof(RestockVehicle))]
[JsonDerivedType(typeof(ScientificResearch))]
[JsonDerivedType(typeof(SearchAndRescue))]
[JsonDerivedType(typeof(SellDrones))]
[JsonDerivedType(typeof(SellShipOnRebuy))]
[JsonDerivedType(typeof(SetUserShipName))]
[JsonDerivedType(typeof(Shipyard))]
[JsonDerivedType(typeof(ShipyardBuy))]
[JsonDerivedType(typeof(ShipyardNew))]
[JsonDerivedType(typeof(ShipyardSell))]
[JsonDerivedType(typeof(ShipyardSwap))]
[JsonDerivedType(typeof(ShipyardTransfer))]
[JsonDerivedType(typeof(StoredModules))]
[JsonDerivedType(typeof(StoredShips))]
[JsonDerivedType(typeof(TechnologyBroker))]
[JsonDerivedType(typeof(AsteroidCracked))]
[JsonDerivedType(typeof(BuyTradeData))]
[JsonDerivedType(typeof(CollectCargo))]
[JsonDerivedType(typeof(EjectCargo))]
[JsonDerivedType(typeof(MarketBuy))]
[JsonDerivedType(typeof(MarketSell))]
[JsonDerivedType(typeof(MiningRefined))]
[JsonDerivedType(typeof(ApproachBody))]
[JsonDerivedType(typeof(Docked))]
[JsonDerivedType(typeof(DockingCancelled))]
[JsonDerivedType(typeof(DockingDenied))]
[JsonDerivedType(typeof(DockingGranted))]
[JsonDerivedType(typeof(DockingRequested))]
[JsonDerivedType(typeof(DockingTimeout))]
[JsonDerivedType(typeof(FSDJump))]
[JsonDerivedType(typeof(FSDTarget))]
[JsonDerivedType(typeof(LeaveBody))]
[JsonDerivedType(typeof(Liftoff))]
[JsonDerivedType(typeof(Location))]
[JsonDerivedType(typeof(NavRoute))]
[JsonDerivedType(typeof(NavRouteClear))]
[JsonDerivedType(typeof(StartJump))]
[JsonDerivedType(typeof(SupercruiseDestinationDrop))]
[JsonDerivedType(typeof(SupercruiseEntry))]
[JsonDerivedType(typeof(SupercruiseExit))]
[JsonDerivedType(typeof(Touchdown))]
[JsonDerivedType(typeof(Undocked))]
[JsonDerivedType(typeof(MarketFile))]
[JsonDerivedType(typeof(ModuleInfoFile))]
[JsonDerivedType(typeof(NavRouteFile))]
[JsonDerivedType(typeof(OutfittingFile))]
[JsonDerivedType(typeof(ShipyardFile))]
[JsonDerivedType(typeof(Status))]
public class JournalBase
{
[JsonPropertyName("timestamp")]
public string Timestamp { get; init; }
[JsonIgnore]
public DateTimeOffset TimestampDateTime
{
get => ParseDateTime(Timestamp);
}
public DateTimeOffset Timestamp { get; init; }
[JsonPropertyName("event")]
public string Event { get; init; }
[JsonExtensionData]
public Dictionary<string, object> AdditionalProperties { get; init; }
[JsonIgnore]
public string Json
{
get => json;
set
{
if (json == null || string.IsNullOrWhiteSpace(json))
{
json = value;
}
else
{
throw new Exception("Journal property \"Json\" can only be set while empty.");
}
}
}
private string json;
// For use by Journal object classes for .*DateTime properties, like TimestampeDateTime, above.
internal static DateTimeOffset ParseDateTime(string value)
{
if (DateTime.TryParseExact(value, "yyyy-MM-ddTHH:mm:ssZ", null, DateTimeStyles.AssumeUniversal, out var dateTimeValue))
{
return dateTimeValue;
}
return new DateTime();
}
}

View File

@ -1,34 +1,12 @@
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
namespace Observatory.Framework.Files.Journal;
public static class JournalUtilities
{
public static string GetEventType(string line)
public static string? GetEventType(JsonObject? line)
{
var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(line));
var result = string.Empty;
try
{
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.PropertyName && reader.GetString() == "event")
{
reader.Read();
result = reader.GetString();
}
}
}
catch
{
result = "InvalidJson";
}
return result;
return line.ContainsKey("event") ? line["event"]?.ToString() : null;
}
public static string CleanScanEvent(string line)
@ -38,6 +16,6 @@ public static class JournalUtilities
public const string ObsoleteMessage = "Unused in Elite Dangerous 3.7+, may appear in legacy journal data.";
public const string UnusedMessage = "Documented by Frontier, but no occurances of this value ever found in real journal data.";
public const string UnusedMessage =
"Documented by Frontier, but no occurances of this value ever found in real journal data.";
}

View File

@ -22,23 +22,14 @@ public class Docked : JournalBase
[Obsolete(JournalUtilities.ObsoleteMessage), JsonConverter(typeof(LegacyFactionConverter<Faction>))]
public Faction Faction
{
private get
{
return StationFaction;
}
init
{
StationFaction = value;
}
private get => StationFaction;
init => StationFaction = value;
}
[Obsolete(JournalUtilities.ObsoleteMessage)]
public string FactionState
{
private get
{
return StationFaction.FactionState;
}
private get => StationFaction.FactionState;
init
{
@ -51,24 +42,24 @@ public class Docked : JournalBase
[Obsolete(JournalUtilities.ObsoleteMessage)]
public string Government
{
private get { return StationGovernment; }
init { StationGovernment = value; }
private get => StationGovernment;
init => StationGovernment = value;
}
public string StationGovernment_Localised { get; init; }
[Obsolete(JournalUtilities.ObsoleteMessage)]
public string Government_Localised
{
private get { return StationGovernment_Localised; }
init { StationGovernment_Localised = value; }
private get => StationGovernment_Localised;
init => StationGovernment_Localised = value;
}
public string StationAllegiance { get; init; }
[Obsolete(JournalUtilities.ObsoleteMessage)]
public string Allegiance
{
private get { return StationAllegiance; }
init { StationAllegiance = value; }
private get => StationAllegiance;
init => StationAllegiance = value;
}
[JsonConverter(typeof(StationServiceConverter))]
@ -78,16 +69,16 @@ public class Docked : JournalBase
[Obsolete(JournalUtilities.ObsoleteMessage)]
public string Economy
{
private get { return StationEconomy; }
init { StationEconomy = value; }
private get => StationEconomy;
init => StationEconomy = value;
}
public string StationEconomy_Localised { get; init; }
[Obsolete(JournalUtilities.ObsoleteMessage)]
public string Economy_Localised
{
private get { return StationEconomy_Localised; }
init { StationEconomy_Localised = value; }
private get => StationEconomy_Localised;
init => StationEconomy_Localised = value;
}
public ImmutableList<StationEconomy> StationEconomies { get; init; }

View File

@ -23,10 +23,7 @@ public class FSDJump : JournalBase
[Obsolete(JournalUtilities.ObsoleteMessage)]
public string FactionState
{
get
{
return SystemFaction.FactionState;
}
get => SystemFaction.FactionState;
init
{
//Stale Data, discard

View File

@ -14,10 +14,7 @@ public class Location : JournalBase
[Obsolete(JournalUtilities.ObsoleteMessage)]
public string FactionState
{
get
{
return SystemFaction.FactionState;
}
get => SystemFaction.FactionState;
init
{
//Stale Data, discard

View File

@ -1,6 +1,4 @@
using Observatory.Framework.Files.Journal;
namespace Observatory.Framework.Files.ParameterTypes;
namespace Observatory.Framework.Files.ParameterTypes;
public class CurrentGoal
{
@ -8,11 +6,7 @@ public class CurrentGoal
public string Title { get; init; }
public string SystemName { get; init; }
public string MarketName { get; init; }
public string Expiry { get; init; }
public DateTimeOffset ExpiryDateTime
{
get => JournalBase.ParseDateTime(Expiry);
}
public DateTimeOffset Expiry { get; init; }
public bool IsComplete { get; init; }
public long CurrentTotal { get; init; }
public long PlayerContribution { get; init; }

View File

@ -22,13 +22,7 @@ public class MaterialTrader
[JsonPropertyName("Raw_Materials_Traded")]
public int RawMaterialsTraded { get; init; }
public int DataMaterialsTraded
{
get
{
return MaterialsTraded - EncodedMaterialsTraded - RawMaterialsTraded;
}
}
public int DataMaterialsTraded => MaterialsTraded - EncodedMaterialsTraded - RawMaterialsTraded;
[JsonPropertyName("Grade_1_Materials_Traded")]
public int Grade1MaterialsTraded { get; init; }

View File

@ -0,0 +1,81 @@
using Observatory.Framework.Files;
namespace Observatory.Framework;
/// <summary>
/// Interface passed by Observatory Core to plugins. Primarily used for sending notifications and UI updates back to Core.
/// </summary>
public interface IObservatoryCore
{
/// <summary>
/// Send a notification out to all native notifiers and any plugins implementing IObservatoryNotifier.
/// </summary>
/// <param name="title">Title text for notification.</param>
/// <param name="detail">Detail/body text for notificaiton.</param>
/// <returns>Guid associated with the notification during its lifetime. Used as an argument with CancelNotification and UpdateNotification.</returns>
public Guid SendNotification(string title, string detail);
/// <summary>
/// Send a notification with arguments out to all native notifiers and any plugins implementing IObservatoryNotifier.
/// </summary>
/// <param name="notificationEventArgs">NotificationArgs object specifying notification content and behaviour.</param>
/// <returns>Guid associated with the notification during its lifetime. Used as an argument with CancelNotification and UpdateNotification.</returns>
public Guid SendNotification(NotificationArgs notificationEventArgs);
/// <summary>
/// Cancel or close an active notification.
/// </summary>
/// <param name="notificationId">Guid of notification to be cancelled.</param>
public void CancelNotification(Guid notificationId);
/// <summary>
/// Update an active notification with a new set of NotificationsArgs. Timeout values are reset and begin counting again from zero if specified.
/// </summary>
/// <param name="notificationId">Guid of notification to be updated.</param>
/// <param name="notificationEventArgs">NotificationArgs object specifying updated notification content and behaviour.</param>
public void UpdateNotification(Guid notificationId, NotificationArgs notificationEventArgs);
/// <summary>
/// Requests current Elite Dangerous status.json content.
/// </summary>
/// <returns>Status object reflecting current Elite Dangerous player status.</returns>
public Status GetStatus();
/// <summary>
/// Version string of Observatory Core.
/// </summary>
public string Version { get; }
/// <summary>
/// Returns a delegate for logging an error for the calling plugin. A plugin can wrap this method
/// or pass it along to its collaborators.
/// </summary>
/// <param name="plugin">The calling plugin</param>
public Action<Exception, string> GetPluginErrorLogger(IObservatoryPlugin plugin);
/// <summary>
/// Shared application HttpClient object. Provided so that plugins can adhere to .NET recommended behaviour of a single HttpClient object per application.
/// </summary>
public HttpClient HttpClient { get; }
/// <summary>
/// Returns the current LogMonitor state.
/// </summary>
public LogMonitorState CurrentLogMonitorState { get; }
/// <summary>
/// Returns true if the current LogMonitor state represents a batch-read mode.
/// </summary>
public bool IsLogMonitorBatchReading { get; }
/// <summary>
/// Retrieves and ensures creation of a location which can be used by the plugin to store persistent data.
/// </summary>
public string PluginStorageFolder { get; }
/// <summary>
/// Sends arbitrary data to all other plugins. The full name and version of the sending plugin will be used to identify the sender to any recipients.
/// </summary>
/// <param name="message">Utf8 data to be sent. Must be serializable to JSON.</param>
public void SendPluginMessage(IObservatoryPlugin plugin, ReadOnlySpan<byte> message);
}

View File

@ -1,5 +1,4 @@
using System.Collections;
using Observatory.Framework.Files;
using Observatory.Framework.Files;
using Observatory.Framework.Files.Journal;
namespace Observatory.Framework;
@ -26,7 +25,7 @@ public interface IObservatoryPlugin
/// Short name of the plugin. Used as the tab title for the plugin UI.<br/>
/// Can be omitted, in which case the full Name will be used.
/// </summary>
public string ShortName { get => Name; }
public string ShortName => Name;
/// <summary>
/// Version string displayed in the Core settings tab's plugin list.<br/>
@ -39,23 +38,6 @@ public interface IObservatoryPlugin
/// </summary>
public PluginUI PluginUI { get; }
/// <summary>
/// <para>Accessors for plugin settings object. Should be initialized with a default state during the plugin constructor.</para>
/// <para>Saving and loading of settings is handled by Observatory Core, and any previously saved settings will be set after plugin instantiation, but before Load() is called.</para>
/// <para>A plugin's settings class is expected to consist of properties with public getters and setters. The settings UI will be automatically generated based on each property type.<br/>
/// The [SettingDisplayName(string name)] attribute can be used to specify a display name, otherwise the name of the property will be used.<br/>
/// Private or internal properties and methods are ignored and can be used for backing values or any other purpose.<br/>
/// If a public property is necessary but not required to be user accessible the [SettingIgnore] property will suppress display.</para>
/// </summary>
public object Settings { get; set; }
/// <summary>
/// <para>Plugin-specific object implementing the IComparer interface which is used to sort columns in the basic UI datagrid.</para>
/// <para>If omitted a natural sort order is used.</para>
/// </summary>
public IObservatoryComparer ColumnSorter
{ get => null; }
/// <summary>
/// Receives data sent by other plugins.
/// </summary>
@ -93,24 +75,6 @@ public interface IObservatoryWorker : IObservatoryPlugin
/// </summary>
public void LogMonitorStateChanged(LogMonitorStateChangedEventArgs eventArgs)
{ }
/// <summary>
/// Method called when the user begins "Read All" journal processing, before any journal events are sent.<br/>
/// Used to track if a "Read All" operation is in progress or not to avoid unnecessary processing or notifications.<br/>
/// Can be omitted for plugins which do not require the distinction.
/// </summary>
[Obsolete("Deprecated in favour of LogMonitorStateChanged")]
public void ReadAllStarted()
{ }
/// <summary>
/// Method called when "Read All" journal processing completes.<br/>
/// Used to track if a "Read All" operation is in progress or not to avoid unnecessary processing or notifications.<br/>
/// Can be omitted for plugins which do not require the distinction.
/// </summary>
[Obsolete("Deprecated in favour of LogMonitorStateChanged")]
public void ReadAllFinished()
{ }
}
/// <summary>
@ -127,116 +91,3 @@ public interface IObservatoryNotifier : IObservatoryPlugin
public void OnNotificationEvent(NotificationArgs notificationEventArgs);
}
/// <summary>
/// Interface passed by Observatory Core to plugins. Primarily used for sending notifications and UI updates back to Core.
/// </summary>
public interface IObservatoryCore
{
/// <summary>
/// Send a notification out to all native notifiers and any plugins implementing IObservatoryNotifier.
/// </summary>
/// <param name="title">Title text for notification.</param>
/// <param name="detail">Detail/body text for notificaiton.</param>
/// <returns>Guid associated with the notification during its lifetime. Used as an argument with CancelNotification and UpdateNotification.</returns>
public Guid SendNotification(string title, string detail);
/// <summary>
/// Send a notification with arguments out to all native notifiers and any plugins implementing IObservatoryNotifier.
/// </summary>
/// <param name="notificationEventArgs">NotificationArgs object specifying notification content and behaviour.</param>
/// <returns>Guid associated with the notification during its lifetime. Used as an argument with CancelNotification and UpdateNotification.</returns>
public Guid SendNotification(NotificationArgs notificationEventArgs);
/// <summary>
/// Cancel or close an active notification.
/// </summary>
/// <param name="notificationId">Guid of notification to be cancelled.</param>
public void CancelNotification(Guid notificationId);
/// <summary>
/// Update an active notification with a new set of NotificationsArgs. Timeout values are reset and begin counting again from zero if specified.
/// </summary>
/// <param name="notificationId">Guid of notification to be updated.</param>
/// <param name="notificationEventArgs">NotificationArgs object specifying updated notification content and behaviour.</param>
public void UpdateNotification(Guid notificationId, NotificationArgs notificationEventArgs);
/// <summary>
/// Add an item to the bottom of the basic UI grid.
/// </summary>
/// <param name="worker">Reference to the calling plugin's worker interface.</param>
/// <param name="item">Grid item to be added. Object type should match original template item used to create the grid.</param>
public void AddGridItem(IObservatoryWorker worker, object item);
/// <summary>
/// Add multiple items to the bottom of the basic UI grid.
/// </summary>
/// <param name="worker">Reference to the calling plugin's worker interface.</param>
/// <param name="items">Grid items to be added. Object types should match original template item used to create the grid.</param>
public void AddGridItems(IObservatoryWorker worker, IEnumerable<Dictionary<string,string>> items);
/// <summary>
/// Replace the contents of the grid with the provided items.
/// </summary>
/// <param name="worker">Reference to the calling plugin's worker interface.</param>
/// <param name="items">Grid items to be added. Object types should match original template item used to create the grid.</param>
public void SetGridItems(IObservatoryWorker worker, IEnumerable<Dictionary<string,string>> items);
/// <summary>
/// Requests current Elite Dangerous status.json content.
/// </summary>
/// <returns>Status object reflecting current Elite Dangerous player status.</returns>
public Status GetStatus();
/// <summary>
/// Version string of Observatory Core.
/// </summary>
public string Version { get; }
/// <summary>
/// Returns a delegate for logging an error for the calling plugin. A plugin can wrap this method
/// or pass it along to its collaborators.
/// </summary>
/// <param name="plugin">The calling plugin</param>
public Action<Exception, string> GetPluginErrorLogger(IObservatoryPlugin plugin);
/// <summary>
/// Shared application HttpClient object. Provided so that plugins can adhere to .NET recommended behaviour of a single HttpClient object per application.
/// </summary>
public HttpClient HttpClient { get; }
/// <summary>
/// Returns the current LogMonitor state.
/// </summary>
public LogMonitorState CurrentLogMonitorState { get; }
/// <summary>
/// Returns true if the current LogMonitor state represents a batch-read mode.
/// </summary>
public bool IsLogMonitorBatchReading { get; }
/// <summary>
/// Retrieves and ensures creation of a location which can be used by the plugin to store persistent data.
/// </summary>
public string PluginStorageFolder { get; }
/// <summary>
/// Sends arbitrary data to all other plugins. The full name and version of the sending plugin will be used to identify the sender to any recipients.
/// </summary>
public void SendPluginMessage(IObservatoryPlugin plugin, object message);
}
/// <summary>
/// Extends the base IComparer interface with exposed values for the column ID and sort order to use.
/// </summary>
public interface IObservatoryComparer : IComparer
{
/// <summary>
/// Column ID to be currently sorted by.
/// </summary>
public int SortColumn { get; set; }
/// <summary>
/// Current order of sorting. Ascending = 1, Descending = -1, No sorting = 0.
/// </summary>
public int Order { get; set; }
}

View File

@ -5,6 +5,7 @@
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>Observatory.Framework</RootNamespace>
<Configurations>Debug;Release;Portable</Configurations>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup>

View File

@ -5,8 +5,6 @@ VisualStudioVersion = 17.3.32922.545
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pulsar", "Pulsar\Pulsar.csproj", "{F389FFD8-B189-4A9F-A077-8D355BA23328}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Explorer", "Explorer\Explorer.csproj", "{6CDB4320-1DC9-405A-97E8-272C9B4CD27C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Botanist", "Botanist\Botanist.csproj", "{25F01564-0E35-471B-A9AC-C61C83AF3275}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ObservatoryFramework", "ObservatoryFramework\ObservatoryFramework.csproj", "{27525BDD-2940-4CB2-8B76-395677997AEF}"
@ -21,10 +19,6 @@ Global
{F389FFD8-B189-4A9F-A077-8D355BA23328}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F389FFD8-B189-4A9F-A077-8D355BA23328}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F389FFD8-B189-4A9F-A077-8D355BA23328}.Release|Any CPU.Build.0 = Release|Any CPU
{6CDB4320-1DC9-405A-97E8-272C9B4CD27C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6CDB4320-1DC9-405A-97E8-272C9B4CD27C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6CDB4320-1DC9-405A-97E8-272C9B4CD27C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6CDB4320-1DC9-405A-97E8-272C9B4CD27C}.Release|Any CPU.Build.0 = Release|Any CPU
{25F01564-0E35-471B-A9AC-C61C83AF3275}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{25F01564-0E35-471B-A9AC-C61C83AF3275}.Debug|Any CPU.Build.0 = Debug|Any CPU
{25F01564-0E35-471B-A9AC-C61C83AF3275}.Release|Any CPU.ActiveCfg = Release|Any CPU

Binary file not shown.

Before

Width:  |  Height:  |  Size: 210 KiB

6
Pulsar/Global.Usings.cs Normal file
View File

@ -0,0 +1,6 @@
global using Pulsar;
global using Pulsar.Utils;
global using System.Text;
global using System.Text.Json;
global using System.Text.Json.Nodes;
global using System.Text.Json.Serialization;

View File

@ -1,5 +1,3 @@
using System.Text;
namespace Pulsar;
public static class LoggingUtils

View File

@ -1,29 +0,0 @@
using Observatory.Framework;
namespace Pulsar.PluginManagement;
public class PlaceholderPlugin : IObservatoryNotifier
{
public PlaceholderPlugin(string name)
{
this.name = name;
}
public string Name => name;
private string name;
public string ShortName => name;
public string Version => string.Empty;
public PluginUI PluginUI => new PluginUI(PluginUI.UIType.None, null);
public object Settings { get => null; set { } }
public void Load(IObservatoryCore observatoryCore)
{ }
public void OnNotificationEvent(NotificationArgs notificationArgs)
{ }
}

View File

@ -1,95 +0,0 @@
using System.Reflection;
using Observatory.Framework;
using Observatory.Framework.Files;
using Pulsar.Utils;
using HttpClient = System.Net.Http.HttpClient;
namespace Pulsar.PluginManagement;
public class PluginCore : IObservatoryCore
{
public string Version => Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "0";
public Action<Exception, string> GetPluginErrorLogger(IObservatoryPlugin plugin)
{
return (ex, context) =>
{
LoggingUtils.LogError(ex, $"from plugin {plugin.ShortName} {context}");
};
}
public Status GetStatus() => LogMonitor.GetInstance.Status;
public Guid SendNotification(string title, string text)
{
return SendNotification(new NotificationArgs { Title = title, Detail = text });
}
public Guid SendNotification(NotificationArgs notificationArgs)
{
throw new NotImplementedException();
}
public void CancelNotification(Guid notificationId)
{
throw new NotImplementedException();
}
public void UpdateNotification(Guid id, NotificationArgs notificationArgs)
{
throw new NotImplementedException();
}
/// <summary>
/// Adds an item to the datagrid on UI thread to ensure visual update.
/// </summary>
/// <param name="worker"></param>
/// <param name="item"></param>
public void AddGridItem(IObservatoryWorker worker, object item)
{
worker.PluginUI.DataGrid.Add(item);
}
public void AddGridItems(IObservatoryWorker worker, IEnumerable<Dictionary<string,string>> items)
{
}
public void SetGridItems(IObservatoryWorker worker, IEnumerable<Dictionary<string,string>> items)
{
}
public HttpClient HttpClient
{
get => Utils.HttpClient.Client;
}
public LogMonitorState CurrentLogMonitorState
{
get => LogMonitor.GetInstance.CurrentState;
}
public bool IsLogMonitorBatchReading
{
get => LogMonitorStateChangedEventArgs.IsBatchRead(LogMonitor.GetInstance.CurrentState);
}
public event EventHandler<NotificationArgs> Notification;
internal event EventHandler<PluginMessageArgs> PluginMessage;
public string PluginStorageFolder
{
get
{
throw new NotImplementedException();
}
}
public void SendPluginMessage(IObservatoryPlugin plugin, object message)
{
PluginMessage?.Invoke(this, new PluginMessageArgs(plugin.Name, plugin.Version, message));
}
}

View File

@ -1,192 +0,0 @@
using System.Timers;
using Observatory.Framework;
using Observatory.Framework.Files;
using Observatory.Framework.Files.Journal;
using Pulsar.Utils;
using Timer = System.Timers.Timer;
namespace Pulsar.PluginManagement;
class PluginEventHandler
{
private IEnumerable<IObservatoryWorker> observatoryWorkers;
private IEnumerable<IObservatoryNotifier> observatoryNotifiers;
private HashSet<IObservatoryPlugin> disabledPlugins;
private List<(string error, string detail)> errorList;
private Timer timer;
public PluginEventHandler(IEnumerable<IObservatoryWorker> observatoryWorkers, IEnumerable<IObservatoryNotifier> observatoryNotifiers)
{
this.observatoryWorkers = observatoryWorkers;
this.observatoryNotifiers = observatoryNotifiers;
disabledPlugins = new();
errorList = new();
InitializeTimer();
}
private void InitializeTimer()
{
// Use a timer to delay error reporting until incoming errors are "quiet" for one full second.
// Should resolve issue where repeated plugin errors open hundreds of error windows.
timer = new();
timer.Interval = 1000;
timer.Elapsed += ReportErrorsIfAny;
}
public void OnJournalEvent(object source, JournalEventArgs journalEventArgs)
{
foreach (var worker in observatoryWorkers)
{
if (disabledPlugins.Contains(worker)) continue;
try
{
worker.JournalEvent((JournalBase)journalEventArgs.journalEvent);
}
catch (PluginException ex)
{
RecordError(ex);
}
catch (Exception ex)
{
RecordError(ex, worker.Name, journalEventArgs.journalType.Name, ((JournalBase)journalEventArgs.journalEvent).Json);
}
ResetTimer();
}
}
public void OnStatusUpdate(object sourece, JournalEventArgs journalEventArgs)
{
foreach (var worker in observatoryWorkers)
{
if (disabledPlugins.Contains(worker)) continue;
try
{
worker.StatusChange((Status)journalEventArgs.journalEvent);
}
catch (PluginException ex)
{
RecordError(ex);
}
catch (Exception ex)
{
RecordError(ex, worker.Name, journalEventArgs.journalType.Name, ((JournalBase)journalEventArgs.journalEvent).Json);
}
ResetTimer();
}
}
internal void OnLogMonitorStateChanged(object sender, LogMonitorStateChangedEventArgs e)
{
foreach (var worker in observatoryWorkers)
{
if (disabledPlugins.Contains(worker)) continue;
try
{
worker.LogMonitorStateChanged(e);
}
catch (Exception ex)
{
RecordError(ex, worker.Name, "LogMonitorStateChanged event", ex.StackTrace ?? "");
}
}
}
public void OnNotificationEvent(object source, NotificationArgs notificationArgs)
{
foreach (var notifier in observatoryNotifiers)
{
if (disabledPlugins.Contains(notifier)) continue;
try
{
notifier.OnNotificationEvent(notificationArgs);
}
catch (PluginException ex)
{
RecordError(ex);
}
catch (Exception ex)
{
RecordError(ex, notifier.Name, notificationArgs.Title, notificationArgs.Detail);
}
ResetTimer();
}
}
public void OnPluginMessageEvent(object _, PluginMessageArgs messageArgs)
{
foreach (var plugin in observatoryNotifiers.Cast<IObservatoryPlugin>().Concat(observatoryWorkers))
{
if (disabledPlugins.Contains(plugin)) continue;
try
{
plugin.HandlePluginMessage(messageArgs.SourceName, messageArgs.SourceVersion, messageArgs.Message);
}
catch (PluginException ex)
{
RecordError(ex);
}
catch(Exception ex)
{
RecordError(ex, plugin.Name, "OnPluginMessageEvent event", "");
}
}
}
public void SetPluginEnabled(IObservatoryPlugin plugin, bool enabled)
{
if (enabled) disabledPlugins.Remove(plugin);
else disabledPlugins.Add(plugin);
}
private void ResetTimer()
{
timer.Stop();
try
{
timer.Start();
}
catch
{
// Not sure why this happens, but I've reproduced it twice in a row after hitting
// read-all while also playing (ie. generating journals).
InitializeTimer();
timer.Start();
}
}
private void RecordError(PluginException ex)
{
errorList.Add(($"Error in {ex.PluginName}: {ex.Message}", ex.StackTrace ?? ""));
}
private void RecordError(Exception ex, string plugin, string eventType, string eventDetail)
{
errorList.Add(($"Error in {plugin} while handling {eventType}: {ex.Message}", eventDetail));
}
private void ReportErrorsIfAny(object sender, ElapsedEventArgs e)
{
if (errorList.Any())
{
ErrorReporter.ShowErrorPopup($"Plugin Error{(errorList.Count > 1 ? "s" : "")}", errorList);
timer.Stop();
}
}
}
internal class PluginMessageArgs
{
internal string SourceName;
internal string SourceVersion;
internal object Message;
internal PluginMessageArgs(string sourceName, string sourceVersion, object message)
{
SourceName = sourceName;
SourceVersion = sourceVersion;
Message = message;
}
}

View File

@ -1,329 +0,0 @@
using System.IO.Compression;
using System.Reflection;
using System.Runtime.Loader;
using Observatory.Framework;
using Pulsar.Utils;
namespace Pulsar.PluginManagement;
public class PluginManager
{
public static PluginManager GetInstance
{
get
{
return _instance.Value;
}
}
private static readonly Lazy<PluginManager> _instance = new Lazy<PluginManager>(NewPluginManager);
private static PluginManager NewPluginManager()
{
return new PluginManager();
}
public readonly List<(string error, string? detail)> errorList;
public readonly List<(IObservatoryWorker plugin, PluginStatus signed)> workerPlugins;
public readonly List<(IObservatoryNotifier plugin, PluginStatus signed)> notifyPlugins;
private readonly PluginCore core;
private readonly PluginEventHandler pluginHandler;
private PluginManager()
{
errorList = LoadPlugins(out workerPlugins, out notifyPlugins);
pluginHandler = new PluginEventHandler(workerPlugins.Select(p => p.plugin), notifyPlugins.Select(p => p.plugin));
var logMonitor = LogMonitor.GetInstance;
logMonitor.JournalEntry += pluginHandler.OnJournalEvent;
logMonitor.StatusUpdate += pluginHandler.OnStatusUpdate;
logMonitor.LogMonitorStateChanged += pluginHandler.OnLogMonitorStateChanged;
core = new PluginCore();
List<IObservatoryPlugin> errorPlugins = new();
foreach (var plugin in workerPlugins.Select(p => p.plugin))
{
try
{
LoadSettings(plugin);
plugin.Load(core);
}
catch (PluginException ex)
{
errorList.Add((FormatErrorMessage(ex), ex.StackTrace));
errorPlugins.Add(plugin);
}
}
workerPlugins.RemoveAll(w => errorPlugins.Contains(w.plugin));
errorPlugins.Clear();
foreach (var plugin in notifyPlugins.Select(p => p.plugin))
{
// Notifiers which are also workers need not be loaded again (they are the same instance).
if (!plugin.GetType().IsAssignableTo(typeof(IObservatoryWorker)))
{
try
{
LoadSettings(plugin);
plugin.Load(core);
}
catch (PluginException ex)
{
errorList.Add((FormatErrorMessage(ex), ex.StackTrace));
errorPlugins.Add(plugin);
}
catch (Exception ex)
{
errorList.Add(($"{plugin.ShortName}: {ex.Message}", ex.StackTrace));
errorPlugins.Add(plugin);
}
}
}
notifyPlugins.RemoveAll(n => errorPlugins.Contains(n.plugin));
core.Notification += pluginHandler.OnNotificationEvent;
core.PluginMessage += pluginHandler.OnPluginMessageEvent;
if (errorList.Any())
ErrorReporter.ShowErrorPopup("Plugin Load Error" + (errorList.Count > 1 ? "s" : string.Empty), errorList);
}
private static string FormatErrorMessage(PluginException ex)
{
return $"{ex.PluginName}: {ex.UserMessage}";
}
private void LoadSettings(IObservatoryPlugin plugin)
{
throw new NotImplementedException();
}
public static Dictionary<PropertyInfo, string> GetSettingDisplayNames(object settings)
{
var settingNames = new Dictionary<PropertyInfo, string>();
if (settings != null)
{
var properties = settings.GetType().GetProperties();
foreach (var property in properties)
{
var attrib = property.GetCustomAttribute<SettingDisplayName>();
if (attrib == null)
{
settingNames.Add(property, property.Name);
}
else
{
settingNames.Add(property, attrib.DisplayName);
}
}
}
return settingNames;
}
public void SaveSettings(IObservatoryPlugin plugin, object settings)
{
throw new NotImplementedException();
}
public void SetPluginEnabled(IObservatoryPlugin plugin, bool enabled)
{
pluginHandler.SetPluginEnabled(plugin, enabled);
}
private static List<(string, string?)> LoadPlugins(out List<(IObservatoryWorker plugin, PluginStatus signed)> observatoryWorkers, out List<(IObservatoryNotifier plugin, PluginStatus signed)> observatoryNotifiers)
{
observatoryWorkers = new();
observatoryNotifiers = new();
var errorList = new List<(string, string?)>();
var pluginPath = $"{AppDomain.CurrentDomain.BaseDirectory}{Path.DirectorySeparatorChar}plugins";
if (Directory.Exists(pluginPath))
{
ExtractPlugins(pluginPath);
var pluginLibraries = Directory.GetFiles($"{AppDomain.CurrentDomain.BaseDirectory}{Path.DirectorySeparatorChar}plugins", "*.dll");
foreach (var dll in pluginLibraries)
{
try
{
var pluginStatus = PluginStatus.SigCheckDisabled;
var loadOkay = true;
if (loadOkay)
{
var error = LoadPluginAssembly(dll, observatoryWorkers, observatoryNotifiers, pluginStatus);
if (!string.IsNullOrWhiteSpace(error))
{
errorList.Add((error, string.Empty));
}
}
}
catch (Exception ex)
{
errorList.Add(($"ERROR: {new FileInfo(dll).Name}, {ex.Message}", ex.StackTrace ?? string.Empty));
LoadPlaceholderPlugin(dll, PluginStatus.InvalidLibrary, observatoryNotifiers);
}
}
}
return errorList;
}
private static void ExtractPlugins(string pluginFolder)
{
var files = Directory.GetFiles(pluginFolder, "*.zip")
.Concat(Directory.GetFiles(pluginFolder, "*.eop")); // Elite Observatory Plugin
foreach (var file in files)
{
try
{
ZipFile.ExtractToDirectory(file, pluginFolder, true);
File.Delete(file);
}
catch
{
// Just ignore files that don't extract successfully.
}
}
}
private static string LoadPluginAssembly(string dllPath, List<(IObservatoryWorker plugin, PluginStatus signed)> workers, List<(IObservatoryNotifier plugin, PluginStatus signed)> notifiers, PluginStatus pluginStatus)
{
var recursionGuard = string.Empty;
AssemblyLoadContext.Default.Resolving += (context, name) => {
if ((name?.Name?.EndsWith("resources")).GetValueOrDefault(false))
{
return null;
}
// Importing Observatory.Framework in the Explorer Lua scripts causes an attempt to reload
// the assembly, just hand it back the one we already have.
if ((name?.Name?.StartsWith("Observatory.Framework")).GetValueOrDefault(false) || name?.Name == "ObservatoryFramework")
{
return context.Assemblies.Where(a => (a.FullName?.Contains("ObservatoryFramework")).GetValueOrDefault(false)).First();
}
var foundDlls = Directory.GetFileSystemEntries(new FileInfo($"{AppDomain.CurrentDomain.BaseDirectory}{Path.DirectorySeparatorChar}plugins{Path.DirectorySeparatorChar}deps").FullName, name.Name + ".dll", SearchOption.TopDirectoryOnly);
if (foundDlls.Any())
{
return context.LoadFromAssemblyPath(foundDlls[0]);
}
if (name.Name != recursionGuard && name.Name != null)
{
recursionGuard = name.Name;
return context.LoadFromAssemblyName(name);
}
throw new Exception("Unable to load assembly " + name.Name);
};
var pluginAssembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(new FileInfo(dllPath).FullName);
Type[] types;
var err = string.Empty;
var pluginCount = 0;
try
{
types = pluginAssembly.GetTypes();
}
catch (ReflectionTypeLoadException ex)
{
types = ex.Types.OfType<Type>().ToArray();
}
catch
{
types = Array.Empty<Type>();
}
IEnumerable<Type> workerTypes = types.Where(t => t.IsAssignableTo(typeof(IObservatoryWorker)));
foreach (var worker in workerTypes)
{
var constructor = worker.GetConstructor(Array.Empty<Type>());
if (constructor != null)
{
var instance = constructor.Invoke(Array.Empty<object>());
workers.Add(((instance as IObservatoryWorker)!, pluginStatus));
if (instance is IObservatoryNotifier)
{
// This is also a notifier; add to the notifier list as well, so the work and notifier are
// the same instance and can share state.
notifiers.Add(((instance as IObservatoryNotifier)!, pluginStatus));
}
pluginCount++;
}
}
// Filter out items which are also workers as we've already created them above.
var notifyTypes = types.Where(t =>
t.IsAssignableTo(typeof(IObservatoryNotifier)) && !t.IsAssignableTo(typeof(IObservatoryWorker)));
foreach (var notifier in notifyTypes)
{
var constructor = notifier.GetConstructor(Array.Empty<Type>());
if (constructor != null)
{
var instance = constructor.Invoke(Array.Empty<object>());
notifiers.Add(((instance as IObservatoryNotifier)!, pluginStatus));
pluginCount++;
}
}
if (pluginCount == 0)
{
err += $"ERROR: Library '{dllPath}' contains no suitable interfaces.";
LoadPlaceholderPlugin(dllPath, PluginStatus.InvalidPlugin, notifiers);
}
return err;
}
private static void LoadPlaceholderPlugin(string dllPath, PluginStatus pluginStatus, List<(IObservatoryNotifier plugin, PluginStatus signed)> notifiers)
{
PlaceholderPlugin placeholder = new(new FileInfo(dllPath).Name);
notifiers.Add((placeholder, pluginStatus));
}
/// <summary>
/// Possible plugin load results and signature statuses.
/// </summary>
public enum PluginStatus
{
/// <summary>
/// Plugin valid and signed with matching certificate.
/// </summary>
Signed,
/// <summary>
/// Plugin valid but not signed with any certificate.
/// </summary>
Unsigned,
/// <summary>
/// Plugin valid but not signed with valid certificate.
/// </summary>
InvalidSignature,
/// <summary>
/// Plugin invalid and cannot be loaded. Possible version mismatch.
/// </summary>
InvalidPlugin,
/// <summary>
/// Plugin not a CLR library.
/// </summary>
InvalidLibrary,
/// <summary>
/// Plugin valid but executing assembly has no certificate to match against.
/// </summary>
NoCert,
/// <summary>
/// Plugin signature checks disabled.
/// </summary>
SigCheckDisabled
}
}

View File

@ -1,26 +1,21 @@
using System.Reflection;
using Observatory;
using Pulsar;
using Pulsar.Utils;
SettingsManager.Load();
if (args.Length > 0 && File.Exists(args[0]))
{
var fileInfo = new FileInfo(args[0]);
if (fileInfo.Extension == ".eop" || fileInfo.Extension == ".zip")
File.Copy(
fileInfo.FullName,
$"{AppDomain.CurrentDomain.BaseDirectory}{Path.DirectorySeparatorChar}plugins{Path.DirectorySeparatorChar}{fileInfo.Name}");
if (fileInfo.Extension is ".eop" or ".zip")
File.Copy(fileInfo.FullName, Path.Join(AppDomain.CurrentDomain.BaseDirectory, "plugins", fileInfo.Name));
}
var version = Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "0";
try
{
//TODO: Start Application
WebApplicationBuilder builder = WebApplication.CreateSlimBuilder(args);
var app = builder.Build();
SettingsManager.Load();
await app.RunAsync();
}
catch (Exception ex)
{
LoggingUtils.LogError(ex, version);
LoggingUtils.LogError(ex, "");
}

View File

@ -6,6 +6,7 @@
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>Pulsar</RootNamespace>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>

View File

@ -0,0 +1,14 @@
namespace Pulsar.Utils;
public static class CollectionExtensions
{
public static void Add<T1, T2>(this ICollection<(T1,T2)> collection, T1 t1, T2 t2)
{
collection.Add((t1, t2));
}
public static void Add<T1, T2, T3>(this ICollection<(T1,T2,T3)> collection, T1 t1, T2 t2, T3 t3)
{
collection.Add((t1, t2, t3));
}
}

View File

@ -1,10 +1,8 @@
using System.Text;
namespace Pulsar.Utils;
namespace Pulsar.Utils;
public static class ErrorReporter
{
public static void ShowErrorPopup(string title, List<(string error, string detail)> errorList)
public static void ShowErrorPopup(string title, List<(string error, JsonObject detail)> errorList)
{
// Limit number of errors displayed.
StringBuilder displayMessage = new();
@ -22,7 +20,7 @@ public static class ErrorReporter
foreach (var error in errorList)
{
errorLog.AppendLine($"[{timestamp}]:");
errorLog.AppendLine($"{error.error} - {error.detail}");
errorLog.AppendLine($"{error.error} - {error.detail.ToJsonString()}");
errorLog.AppendLine();
}

View File

@ -7,13 +7,7 @@ public sealed class HttpClient
private static readonly Lazy<System.Net.Http.HttpClient> lazy = new Lazy<System.Net.Http.HttpClient>(() => new System.Net.Http.HttpClient());
public static System.Net.Http.HttpClient Client
{
get
{
return lazy.Value;
}
}
public static System.Net.Http.HttpClient Client => lazy.Value;
public static string GetString(string url)
{

View File

@ -1,7 +1,4 @@
using System.Reflection;
using System.Text;
using System.Text.Json;
using Observatory.Framework.Files.Journal;
using Observatory.Framework.Files.Journal;
using Observatory.Framework.Files.Journal.Exploration;
namespace Pulsar.Utils;
@ -10,85 +7,64 @@ public class JournalReader
{
public static TJournal ObservatoryDeserializer<TJournal>(string json) where TJournal : JournalBase
{
TJournal deserialized;
TJournal deserialized;
if (typeof(TJournal) == typeof(InvalidJson))
if (typeof(TJournal) == typeof(InvalidJson))
{
InvalidJson invalidJson;
try
{
InvalidJson invalidJson;
try
{
var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(json));
var eventType = string.Empty;
var timestamp = string.Empty;
var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(json));
var eventType = string.Empty;
var timestamp = string.Empty;
while ((eventType == string.Empty || timestamp == string.Empty) && reader.Read())
while ((eventType == string.Empty || timestamp == string.Empty) && reader.Read())
{
if (reader.TokenType == JsonTokenType.PropertyName)
{
if (reader.TokenType == JsonTokenType.PropertyName)
if (reader.GetString() == "event")
{
if (reader.GetString() == "event")
{
reader.Read();
eventType = reader.GetString();
}
else if (reader.GetString() == "timestamp")
{
reader.Read();
timestamp = reader.GetString();
}
reader.Read();
eventType = reader.GetString();
}
else if (reader.GetString() == "timestamp")
{
reader.Read();
timestamp = reader.GetString();
}
}
invalidJson = new InvalidJson
{
Event = "InvalidJson",
Timestamp = timestamp,
OriginalEvent = eventType
};
}
catch
invalidJson = new InvalidJson
{
invalidJson = new InvalidJson
{
Event = "InvalidJson",
Timestamp = string.Empty,
OriginalEvent = "Invalid"
};
}
deserialized = (TJournal)Convert.ChangeType(invalidJson, typeof(TJournal));
Event = "InvalidJson",
Timestamp = DateTimeOffset.UnixEpoch,
OriginalEvent = eventType
};
}
//Journal potentially had invalid JSON for a brief period in 2017, check for it and remove.
//TODO: Check if this gets handled by InvalidJson now.
else if (typeof(TJournal) == typeof(Scan) && json.Contains("\"RotationPeriod\":inf"))
catch
{
deserialized = JsonSerializer.Deserialize<TJournal>(json.Replace("\"RotationPeriod\":inf,", ""));
invalidJson = new InvalidJson
{
Event = "InvalidJson",
Timestamp = DateTimeOffset.UnixEpoch,
OriginalEvent = "Invalid"
};
}
else
{
deserialized = JsonSerializer.Deserialize<TJournal>(json);
}
deserialized.Json = json;
return deserialized;
deserialized = (TJournal)Convert.ChangeType(invalidJson, typeof(TJournal));
}
//Journal potentially had invalid JSON for a brief period in 2017, check for it and remove.
//TODO: Check if this gets handled by InvalidJson now.
else if (typeof(TJournal) == typeof(Scan) && json.Contains("\"RotationPeriod\":inf"))
{
deserialized = JsonSerializer.Deserialize<TJournal>(json.Replace("\"RotationPeriod\":inf,", ""));
}
else
{
deserialized = JsonSerializer.Deserialize<TJournal>(json);
}
public static Dictionary<string, Type> PopulateEventClasses()
{
var eventClasses = new Dictionary<string, Type>(StringComparer.InvariantCultureIgnoreCase);
var allTypes = Assembly.GetAssembly(typeof(JournalBase)).GetTypes();
var journalTypes = allTypes.Where(a => a.IsSubclassOf(typeof(JournalBase)));
foreach (var journalType in journalTypes)
{
eventClasses.Add(journalType.Name, journalType);
}
eventClasses.Add("JournalBase", typeof(JournalBase));
return eventClasses;
}
return deserialized;
}
}

View File

@ -5,40 +5,33 @@ using Observatory.Framework.Files.Journal;
namespace Pulsar.Utils;
using JournalEvent = (Exception ex, string file, JsonObject line);
class LogMonitor
{
#region Singleton Instantiation
public static LogMonitor GetInstance
{
get
{
return _instance.Value;
}
}
public static LogMonitor GetInstance => _instance.Value;
private static readonly Lazy<LogMonitor> _instance = new(NewLogMonitor);
private static LogMonitor NewLogMonitor()
{
return new LogMonitor();
}
return new LogMonitor();
}
private LogMonitor()
{
currentLine = new();
journalTypes = JournalReader.PopulateEventClasses();
InitializeWatchers(string.Empty);
SetLogMonitorState(LogMonitorState.Idle);
}
currentLine = new();
InitializeWatchers(string.Empty);
SetLogMonitorState(LogMonitorState.Idle);
}
#endregion
#region Public properties
public LogMonitorState CurrentState
{
get => currentState;
}
public LogMonitorState CurrentState => currentState;
public Status Status { get; private set; }
@ -48,141 +41,148 @@ class LogMonitor
public void Start()
{
if (firstStartMonitor)
{
// Only pre-read on first start monitor. Beyond that it's simply pause/resume.
firstStartMonitor = false;
PrereadJournals();
}
journalWatcher.EnableRaisingEvents = true;
statusWatcher.EnableRaisingEvents = true;
SetLogMonitorState(LogMonitorState.Realtime);
JournalPoke();
if (firstStartMonitor)
{
// Only pre-read on first start monitor. Beyond that it's simply pause/resume.
firstStartMonitor = false;
PrereadJournals();
}
journalWatcher.EnableRaisingEvents = true;
statusWatcher.EnableRaisingEvents = true;
SetLogMonitorState(LogMonitorState.Realtime);
JournalPoke();
}
public void Stop()
{
journalWatcher.EnableRaisingEvents = false;
statusWatcher.EnableRaisingEvents = false;
SetLogMonitorState(LogMonitorState.Idle);
}
journalWatcher.EnableRaisingEvents = false;
statusWatcher.EnableRaisingEvents = false;
SetLogMonitorState(LogMonitorState.Idle);
}
public void ChangeWatchedDirectory(string path)
{
journalWatcher.Dispose();
statusWatcher.Dispose();
InitializeWatchers(path);
}
journalWatcher.Dispose();
statusWatcher.Dispose();
InitializeWatchers(path);
}
public bool IsMonitoring()
{
return currentState.HasFlag(LogMonitorState.Realtime);
}
return currentState.HasFlag(LogMonitorState.Realtime);
}
// TODO(fredjk_gh): Remove?
public bool ReadAllInProgress()
{
return LogMonitorStateChangedEventArgs.IsBatchRead(currentState);
}
return LogMonitorStateChangedEventArgs.IsBatchRead(currentState);
}
public Func<IEnumerable<string>> ReadAllGenerator(out int fileCount)
{
// Prevent pre-reading when starting monitoring after reading all.
firstStartMonitor = false;
SetLogMonitorState(currentState | LogMonitorState.Batch);
// Prevent pre-reading when starting monitoring after reading all.
firstStartMonitor = false;
SetLogMonitorState(currentState | LogMonitorState.BatchProcessing);
var logDirectory = GetJournalFolder();
var files = GetJournalFilesOrdered(logDirectory);
fileCount = files.Count();
var logDirectory = GetJournalFolder();
var files = GetJournalFilesOrdered(logDirectory);
fileCount = files.Count();
IEnumerable<string> ReadAllJournals()
IEnumerable<string> ReadAllJournals()
{
var readErrors = new List<JournalEvent>();
foreach (var file in files)
{
var readErrors = new List<(Exception ex, string file, string line)>();
foreach (var file in files)
{
yield return file.Name;
readErrors.AddRange(
ProcessLines(ReadAllLines(file.FullName), file.Name));
}
yield return file.Name;
readErrors.AddRange(ProcessJournal(ReadByLines(file.FullName), file.Name));
}
ReportErrors(readErrors);
SetLogMonitorState(currentState & ~LogMonitorState.Batch);
};
return ReadAllJournals;
ReportErrors(readErrors);
SetLogMonitorState(currentState & ~LogMonitorState.BatchProcessing);
}
;
return ReadAllJournals;
}
public void PrereadJournals()
{
SetLogMonitorState(currentState | LogMonitorState.PreRead);
SetLogMonitorState(currentState | LogMonitorState.Init);
var logDirectory = GetJournalFolder();
var files = GetJournalFilesOrdered(logDirectory).ToList();
var logDirectory = GetJournalFolder();
var files = GetJournalFilesOrdered(logDirectory).ToList();
// Read at most the last two files (in case we were launched after the game and the latest
// journal is mostly empty) but keeping only the lines since the last FSDJump.
List<string> lastSystemLines = new();
List<string> lastFileLines = new();
List<string> fileHeaderLines = new();
var sawFSDJump = false;
foreach (var file in files.Skip(Math.Max(files.Count - 2, 0)))
// Read at most the last two files (in case we were launched after the game and the latest
// journal is mostly empty) but keeping only the lines since the last FSDJump.
//TODO: strongly type these
List<JsonObject?> lastSystemLines = new();
List<JsonObject?> lastFileLines = new();
List<JsonObject?> fileHeaderLines = new();
var sawFSDJump = false;
foreach (var file in files.Skip(Math.Max(files.Count - 2, 0)))
{
var lines = ReadByLines(file.FullName);
foreach (var line in lines)
{
var lines = ReadAllLines(file.FullName);
foreach (var line in lines)
var eventType = JournalUtilities.GetEventType(line);
if (eventType == "FSDJump" || eventType == "CarrierJump" &&
((line["Docked"]?.GetValue<bool>() ?? false) ||
(line["OnFoot"]?.GetValue<bool>() ?? false)))
{
var eventType = JournalUtilities.GetEventType(line);
if (eventType.Equals("FSDJump") || (eventType.Equals("CarrierJump") && (line.Contains("\"Docked\":true") || line.Contains("\"OnFoot\":true"))))
{
// Reset, start collecting again.
lastSystemLines.Clear();
sawFSDJump = true;
}
else if (eventType.Equals("Fileheader"))
{
lastFileLines.Clear();
fileHeaderLines.Clear();
fileHeaderLines.Add(line);
}
else if (eventType.Equals("LoadGame") || eventType.Equals("Statistics"))
{
// A few header lines to collect.
fileHeaderLines.Add(line);
}
lastSystemLines.Add(line);
lastFileLines.Add(line);
// Reset, start collecting again.
lastSystemLines.Clear();
sawFSDJump = true;
}
}
// If we didn't see a jump in the recent logs (Cmdr is stationary in a system for a while
// ie. deep-space mining from a carrier), at very least, read from the beginning of the
// current journal file which includes the important stuff like the last "LoadGame", etc. This
// also helps out in cases where one forgets to hit "Start Monitor" until part-way into the
// session (if auto-start is not enabled).
List<string> linesToRead = lastFileLines;
if (sawFSDJump)
{
// If we saw any relevant header lines, insert them as well. This ensures odyssey biologicials are properly
// counted/presented, current Commander name is present, etc.
if (fileHeaderLines.Count > 0)
else if (eventType == "Fileheader")
{
lastSystemLines.InsertRange(0, fileHeaderLines);
lastFileLines.Clear();
fileHeaderLines.Clear();
fileHeaderLines.Add(line);
}
else if (eventType == "LoadGame" || eventType == "Statistics")
{
// A few header lines to collect.
fileHeaderLines.Add(line);
}
linesToRead = lastSystemLines;
}
ReportErrors(ProcessLines(linesToRead, "Pre-read"));
SetLogMonitorState(currentState & ~LogMonitorState.PreRead);
lastSystemLines.Add(line);
lastFileLines.Add(line);
}
}
// If we didn't see a jump in the recent logs (Cmdr is stationary in a system for a while
// ie. deep-space mining from a carrier), at very least, read from the beginning of the
// current journal file which includes the important stuff like the last "LoadGame", etc. This
// also helps out in cases where one forgets to hit "Start Monitor" until part-way into the
// session (if auto-start is not enabled).
var linesToRead = lastFileLines;
if (sawFSDJump)
{
// If we saw any relevant header lines, insert them as well. This ensures odyssey biologicials are properly
// counted/presented, current Commander name is present, etc.
if (fileHeaderLines.Count > 0)
{
lastSystemLines.InsertRange(0, fileHeaderLines);
}
linesToRead = lastSystemLines;
}
ReportErrors(ProcessJournal(linesToRead, "Pre-read"));
SetLogMonitorState(currentState & ~LogMonitorState.Init);
}
#endregion
#region Public Events
public event EventHandler<LogMonitorStateChangedEventArgs> LogMonitorStateChanged;
public event EventHandler<LogMonitorStateChangedEventArgs>? LogMonitorStateChanged;
public event EventHandler<JournalEventArgs> JournalEntry;
public event EventHandler<JournalEventArgs>? JournalEntry;
public event EventHandler<JournalEventArgs> StatusUpdate;
public event EventHandler<JournalEventArgs>? StatusUpdate;
#endregion
@ -194,17 +194,19 @@ class LogMonitor
private readonly Dictionary<string, int> currentLine;
private LogMonitorState currentState = LogMonitorState.Idle; // Change via #SetLogMonitorState
private bool firstStartMonitor = true;
private readonly string[] EventsWithAncillaryFile = {
"Cargo",
"NavRoute",
"Market",
"Outfitting",
"Shipyard",
"Backpack",
"FCMaterials",
"ModuleInfo",
"ShipLocker"
};
private readonly string[] EventsWithAncillaryFile =
{
"Cargo",
"NavRoute",
"Market",
"Outfitting",
"Shipyard",
"Backpack",
"FCMaterials",
"ModuleInfo",
"ShipLocker"
};
#endregion
@ -212,203 +214,190 @@ class LogMonitor
private void SetLogMonitorState(LogMonitorState newState)
{
var oldState = currentState;
currentState = newState;
LogMonitorStateChanged?.Invoke(this, new LogMonitorStateChangedEventArgs
{
PreviousState = oldState,
NewState = newState
}); ;
var oldState = currentState;
currentState = newState;
LogMonitorStateChanged?.Invoke(this, new LogMonitorStateChangedEventArgs
{
PreviousState = oldState,
NewState = newState
});
;
Debug.WriteLine("LogMonitor State change: {0} -> {1}", oldState, newState);
}
Debug.WriteLine("LogMonitor State change: {0} -> {1}", oldState, newState);
}
private void InitializeWatchers(string path)
{
var logDirectory = GetJournalFolder();
var logDirectory = GetJournalFolder();
journalWatcher = new FileSystemWatcher(logDirectory, "Journal.*.??.log")
{
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size |
NotifyFilters.FileName | NotifyFilters.CreationTime
};
journalWatcher.Changed += LogChangedEvent;
journalWatcher.Created += LogCreatedEvent;
journalWatcher = new FileSystemWatcher(logDirectory, "Journal.*.??.log")
{
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size |
NotifyFilters.FileName | NotifyFilters.CreationTime
};
journalWatcher.Changed += LogChangedEvent;
journalWatcher.Created += LogCreatedEvent;
statusWatcher = new FileSystemWatcher(logDirectory, "Status.json")
{
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size
};
statusWatcher.Changed += StatusUpdateEvent;
}
statusWatcher = new FileSystemWatcher(logDirectory, "Status.json")
{
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size
};
statusWatcher.Changed += StatusUpdateEvent;
}
private static string GetJournalFolder()
{
throw new NotImplementedException();
throw new NotImplementedException();
}
private List<JournalEvent> ProcessJournal(IEnumerable<JsonObject?> lines, string file)
{
var readErrors = new List<(Exception ex, string file, JsonObject line)>();
foreach (var line in lines)
{
try
{
DeserializeAndInvoke(line);
}
catch (Exception ex)
{
readErrors.Add(ex, file, line);
}
}
private List<(Exception ex, string file, string line)> ProcessLines(List<string> lines, string file)
return readErrors;
}
private void DeserializeAndInvoke(JsonObject? line)
{
var readErrors = new List<(Exception ex, string file, string line)>();
foreach (var line in lines)
var eventType = JournalUtilities.GetEventType(line);
var journalEvent = new JournalEventArgs(eventType, line);
JournalEntry?.Invoke(this, journalEvent);
// Files are only valid if realtime, otherwise they will be stale or empty.
if (!currentState.HasFlag(LogMonitorState.BatchProcessing) && EventsWithAncillaryFile.Contains(eventType))
{
HandleModuleInfoFile(eventType);
}
}
private async Task HandleModuleInfoFile(string eventType)
{
var filename = eventType == "ModuleInfo"
? "ModulesInfo.json" // Just FDev things
: eventType + ".json";
// I have no idea what order Elite writes these files or if they're already written
// by the time the journal updates.
// Brief sleep to ensure the content is updated before we read it.
// Some files are still locked by another process after 50ms.
// Retry every 50ms for 0.5 seconds before giving up.
JsonObject? fileContent = null;
var retryCount = 0;
while (fileContent == null && retryCount < 10)
{
await Task.Delay(TimeSpan.FromSeconds(0.5));
try
{
try
fileContent = ReadFile(Path.Join(journalWatcher.Path, filename));
var fileObject = new JournalEventArgs(eventType, fileContent);
JournalEntry?.Invoke(this, fileObject);
}
catch
{
retryCount++;
}
}
}
private static void ReportErrors(List<JournalEvent> readErrors)
{
if (readErrors.Any())
{
var errorList = readErrors.Select(error =>
{
string message;
if (error.ex.InnerException == null)
{
DeserializeAndInvoke(line);
message = error.ex.Message;
}
catch (Exception ex)
else
{
readErrors.Add((ex, file, line));
message = error.ex.InnerException.Message;
}
}
return readErrors;
}
private JournalEventArgs DeserializeToEventArgs(string eventType, string line)
{
var eventClass = journalTypes[eventType];
var journalRead = typeof(JournalReader).GetMethod(nameof(JournalReader.ObservatoryDeserializer));
var journalGeneric = journalRead.MakeGenericMethod(eventClass);
var entry = journalGeneric.Invoke(null, new object[] { line });
return new JournalEventArgs { journalType = eventClass, journalEvent = entry };
}
private void DeserializeAndInvoke(string line)
{
var eventType = JournalUtilities.GetEventType(line);
if (!journalTypes.ContainsKey(eventType))
{
eventType = "JournalBase";
}
var journalEvent = DeserializeToEventArgs(eventType, line);
JournalEntry?.Invoke(this, journalEvent);
// Files are only valid if realtime, otherwise they will be stale or empty.
if (!currentState.HasFlag(LogMonitorState.Batch) && EventsWithAncillaryFile.Contains(eventType))
{
HandleAncillaryFile(eventType);
}
}
private void HandleAncillaryFile(string eventType)
{
var filename = eventType == "ModuleInfo"
? "ModulesInfo.json" // Just FDev things
: eventType + ".json";
// I have no idea what order Elite writes these files or if they're already written
// by the time the journal updates.
// Brief sleep to ensure the content is updated before we read it.
// Some files are still locked by another process after 50ms.
// Retry every 50ms for 0.5 seconds before giving up.
string fileContent = null;
var retryCount = 0;
while (fileContent == null && retryCount < 10)
{
Thread.Sleep(50);
try
{
using var fileStream = File.Open(journalWatcher.Path + Path.DirectorySeparatorChar + filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using var reader = new StreamReader(fileStream);
fileContent = reader.ReadToEnd();
var fileObject = DeserializeToEventArgs(eventType + "File", fileContent);
JournalEntry?.Invoke(this, fileObject);
}
catch
{
retryCount++;
}
}
}
private static void ReportErrors(List<(Exception ex, string file, string line)> readErrors)
{
if (readErrors.Any())
{
var errorList = readErrors.Select(error =>
{
string message;
if (error.ex.InnerException == null)
{
message = error.ex.Message;
}
else
{
message = error.ex.InnerException.Message;
}
return ($"Error reading file {error.file}: {message}", error.line);
});
ErrorReporter.ShowErrorPopup($"Journal Read Error{(readErrors.Count > 1 ? "s" : "")}", errorList.ToList());
}
return ($"Error reading file {error.file}: {message}", error.line);
});
ErrorReporter.ShowErrorPopup($"Journal Read Error{(readErrors.Count > 1 ? "s" : "")}", errorList.ToList());
}
}
private void LogChangedEvent(object source, FileSystemEventArgs eventArgs)
{
var fileContent = ReadAllLines(eventArgs.FullPath);
var fileContent = ReadByLines(eventArgs.FullPath);
if (!currentLine.ContainsKey(eventArgs.FullPath))
{
currentLine.Add(eventArgs.FullPath, fileContent.Count - 1);
}
foreach (var line in fileContent.Skip(currentLine[eventArgs.FullPath]))
{
try
{
DeserializeAndInvoke(line);
}
catch (Exception ex)
{
ReportErrors(new List<(Exception ex, string file, string line)> { (ex, eventArgs.Name ?? string.Empty, line) });
}
}
currentLine[eventArgs.FullPath] = fileContent.Count;
if (!currentLine.ContainsKey(eventArgs.FullPath))
{
currentLine.Add(eventArgs.FullPath, fileContent.Count() - 1);
}
private static List<string> ReadAllLines(string path)
{
var lines = new List<string>();
foreach (var line in fileContent.Skip(currentLine[eventArgs.FullPath]))
{
try
{
using StreamReader file = new(File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite));
while (!file.EndOfStream)
{
lines.Add(file.ReadLine() ?? string.Empty);
}
DeserializeAndInvoke(line);
}
catch (IOException ioEx)
catch (Exception ex)
{
ReportErrors(new List<(Exception, string, string)> { (ioEx, path, "<reading all lines>") });
ReportErrors([(ex, eventArgs.Name ?? string.Empty, line)]);
}
return lines;
}
currentLine[eventArgs.FullPath] = fileContent.Count();
}
private static IEnumerable<JsonObject> ReadByLines(string path)
{
const int bufferSize = 512;
using var file = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using var reader = new StreamReader(file, Encoding.UTF8, true, bufferSize);
while (reader.ReadLine() is { } line
&& JsonNode.Parse(line) is { } parsed)
{
yield return parsed.AsObject();
}
}
private static JsonObject? ReadFile(string path)
{
using var file = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
return JsonNode.Parse(file)?.AsObject();
}
private void LogCreatedEvent(object source, FileSystemEventArgs eventArgs)
{
currentLine.Add(eventArgs.FullPath, 0);
LogChangedEvent(source, eventArgs);
}
currentLine.Add(eventArgs.FullPath, 0);
LogChangedEvent(source, eventArgs);
}
private void StatusUpdateEvent(object source, FileSystemEventArgs eventArgs)
{
var handler = StatusUpdate;
var statusLines = ReadAllLines(eventArgs.FullPath);
if (statusLines.Count > 0)
{
var status = JournalReader.ObservatoryDeserializer<Status>(statusLines[0]);
Status = status;
handler?.Invoke(this, new JournalEventArgs { journalType = typeof(Status), journalEvent = status });
}
var handler = StatusUpdate;
var statusLines = ReadFile(eventArgs.FullPath);
if (statusLines != null)
{
var status = statusLines.Deserialize<Status>();
Status = status;
handler(this, new JournalEventArgs("Status", statusLines));
}
}
/// <summary>
/// Touches most recent journal file once every 250ms while LogMonitor is monitoring.
@ -416,36 +405,38 @@ class LogMonitor
/// </summary>
private async void JournalPoke()
{
var journalFolder = GetJournalFolder();
var journalFolder = GetJournalFolder();
await Task.Run(() =>
await Task.Run(() =>
{
while (IsMonitoring())
{
while (IsMonitoring())
{
var journals = GetJournalFilesOrdered(journalFolder);
var journals = GetJournalFilesOrdered(journalFolder);
if (journals.Any())
{
var fileToPoke = GetJournalFilesOrdered(journalFolder).Last();
using var stream = fileToPoke.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
stream.Close();
}
Thread.Sleep(250);
if (journals.Any())
{
var fileToPoke = GetJournalFilesOrdered(journalFolder).Last();
using var stream = fileToPoke.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
stream.Close();
}
});
}
Thread.Sleep(250);
}
});
}
private static string GetSavedGamesPath()
{
throw new NotImplementedException();
}
throw new NotImplementedException();
}
private static IEnumerable<FileInfo> GetJournalFilesOrdered(string path)
{
var journalFolder = new DirectoryInfo(path);
return from file in journalFolder.GetFiles("Journal.*.??.log")
orderby file.LastWriteTime
select file;
}
var journalFolder = new DirectoryInfo(path);
return from file in journalFolder.GetFiles("Journal.*.??.log")
orderby file.LastWriteTime
select file;
}
#endregion
}