diff --git a/Botanist/BioPlanetDetail.cs b/Botanist/BioPlanetDetail.cs new file mode 100644 index 0000000..65b2c3e --- /dev/null +++ b/Botanist/BioPlanetDetail.cs @@ -0,0 +1,8 @@ +namespace Botanist; + +class BioPlanetDetail +{ + public string BodyName { get; set; } + public int BioTotal { get; set; } + public Dictionary SpeciesFound { get; set; } +} \ No newline at end of file diff --git a/Botanist/BioSampleDetail.cs b/Botanist/BioSampleDetail.cs new file mode 100644 index 0000000..89b56d5 --- /dev/null +++ b/Botanist/BioSampleDetail.cs @@ -0,0 +1,7 @@ +namespace Botanist; + +class BioSampleDetail +{ + public string Genus { get; set; } + public bool Analysed { get; set; } +} \ No newline at end of file diff --git a/Botanist/BodyAddress.cs b/Botanist/BodyAddress.cs new file mode 100644 index 0000000..7ca92df --- /dev/null +++ b/Botanist/BodyAddress.cs @@ -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); + } +} \ No newline at end of file diff --git a/Botanist/Botanist.cs b/Botanist/Botanist.cs index bdac10d..c70de78 100644 --- a/Botanist/Botanist.cs +++ b/Botanist/Botanist.cs @@ -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 EnglishGenusByIdentifier = new() + public static readonly IReadOnlyDictionary EnglishGenusByIdentifier = new Dictionary { { "$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 ColonyDistancesByGenus = new() + public static readonly IReadOnlyDictionary ColonyDistancesByGenus = new Dictionary() { { "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 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 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; } } \ No newline at end of file diff --git a/Botanist/BotanistGrid.cs b/Botanist/BotanistGrid.cs new file mode 100644 index 0000000..652eb8e --- /dev/null +++ b/Botanist/BotanistGrid.cs @@ -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; } +} \ No newline at end of file diff --git a/Explorer/CriteriaLoadException.cs b/Explorer/CriteriaLoadException.cs deleted file mode 100644 index 9b286c2..0000000 --- a/Explorer/CriteriaLoadException.cs +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/Explorer/CustomCriteriaManager.cs b/Explorer/CustomCriteriaManager.cs deleted file mode 100644 index df54763..0000000 --- a/Explorer/CustomCriteriaManager.cs +++ /dev/null @@ -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 CriteriaFunctions; - private Dictionary CriteriaWithErrors = new(); - Action ErrorLogger; - private uint ScanCount; - - public CustomCriteriaManager(Action 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(); - 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> scanHistory, Dictionary> 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 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; - } - -} \ No newline at end of file diff --git a/Explorer/DefaultCriteria.cs b/Explorer/DefaultCriteria.cs deleted file mode 100644 index 3a6789d..0000000 --- a/Explorer/DefaultCriteria.cs +++ /dev/null @@ -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> scanHistory, Dictionary> 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 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 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 boostMaterials = new() - { - "Carbon", - "Germanium", - "Arsenic", - "Niobium", - "Yttrium", - "Polonium" - }; - - List 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 { "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; - } - - /// - /// Removes materials from the list if found on the specified body. - /// - /// - /// - /// Count of materials remaining in list. - private static int RemoveMatchedMaterials(this List 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)); - } -} \ No newline at end of file diff --git a/Explorer/Explorer.cs b/Explorer/Explorer.cs deleted file mode 100644 index 2dc7f7c..0000000 --- a/Explorer/Explorer.cs +++ /dev/null @@ -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> SystemBodyHistory; - private Dictionary> BodySignalHistory; - private Dictionary> 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()); - } - - 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()); - } - - 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 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 = - // "" + bodyAffix[..ringIndex] - // + "" + 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 = $"{bodyLabel} {spokenAffix}", - // Detail = notificationDetail.ToString(), - // Sender = ExplorerWorker.ShortName, - // ExtendedDetails = notificationExtendedDetail.ToString(), - // CoalescingId = scanEvent.BodyID, - // }; - // - // ObservatoryCore.SendNotification(args); - } - } -} \ No newline at end of file diff --git a/Explorer/Explorer.csproj b/Explorer/Explorer.csproj deleted file mode 100644 index 94401ae..0000000 --- a/Explorer/Explorer.csproj +++ /dev/null @@ -1,17 +0,0 @@ - - - - net8.0 - enable - Explorer - - - - - - - - - - - diff --git a/Explorer/Explorer.sln b/Explorer/Explorer.sln deleted file mode 100644 index 7481c63..0000000 --- a/Explorer/Explorer.sln +++ /dev/null @@ -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 diff --git a/Explorer/ExplorerSettings.cs b/Explorer/ExplorerSettings.cs deleted file mode 100644 index 2663be1..0000000 --- a/Explorer/ExplorerSettings.cs +++ /dev/null @@ -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; } -} \ No newline at end of file diff --git a/Explorer/ExplorerUIResults.cs b/Explorer/ExplorerUIResults.cs deleted file mode 100644 index 8e13775..0000000 --- a/Explorer/ExplorerUIResults.cs +++ /dev/null @@ -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; } -} \ No newline at end of file diff --git a/Explorer/Worker.cs b/Explorer/Worker.cs deleted file mode 100644 index bc6f1c3..0000000 --- a/Explorer/Worker.cs +++ /dev/null @@ -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 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 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; -} \ No newline at end of file diff --git a/ObservatoryFramework/EventArgs.cs b/ObservatoryFramework/EventArgs.cs index 14f69fb..5c5d055 100644 --- a/ObservatoryFramework/EventArgs.cs +++ b/ObservatoryFramework/EventArgs.cs @@ -1,20 +1,22 @@ -namespace Observatory.Framework; +using System.Text.Json.Nodes; + +namespace Observatory.Framework; /// /// Provides data for Elite Dangerous journal events. /// -public class JournalEventArgs : EventArgs +public class JournalEventArgs(string journalEventType, JsonObject journalEvent) : EventArgs { /// /// Type of journal entry that triggered event. - /// 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. /// - public Type journalType; + public string JournalEventType = journalEventType; + /// /// Elite Dangerous journal event, deserialized into a .NET object of the type specified by JournalEventArgs.journalType. /// Unhandled json values within a journal entry type will be contained in member property:
Dictionary<string, object> AdditionalProperties.
///
- public object journalEvent; + public JsonObject JournalEvent = journalEvent; } /// @@ -26,48 +28,59 @@ public class NotificationArgs /// Text typically displayed as header content. /// public string Title; + /// /// SSML representation of Title text.
/// This value is optional, if omitted the value of NotificationArgs.Title will be used for voice synthesis without markup. ///
public string TitleSsml; + /// /// Text typically displayed as body content. /// public string Detail; + /// /// SSML representation of Detail text.
/// This value is optional, if omitted the value of NotificationArgs.Detail will be used for voice synthesis without markup. ///
public string DetailSsml; + /// /// Specify window timeout in ms (overrides Core setting). Specify 0 timeout to persist until removed via IObservatoryCore.CancelNotification. Default -1 (use Core setting). /// public int Timeout = -1; + /// /// Specify window X position as a percentage from upper left corner (overrides Core setting). Default -1.0 (use Core setting). /// public double XPos = -1.0; + /// /// Specify window Y position as a percentage from upper left corner (overrides Core setting). Default -1.0 (use Core setting). /// public double YPos = -1.0; + /// /// Specifies the desired renderings of the notification. Defaults to . /// public NotificationRendering Rendering = NotificationRendering.All; + /// /// Specifies if some part of the notification should be suppressed. Not supported by all notifiers. Defaults to . /// public NotificationSuppression Suppression = NotificationSuppression.None; + /// /// The plugin sending this notification. /// public string Sender = ""; + /// /// Additional notification detailed (generally not rendered by voice or popup; potentially used by aggregating/logging plugins). /// public string ExtendedDetails; + /// /// A value which allows grouping of notifications together. For example, values >= 0 <= 1000 could be system body IDs, -1 is the system, anything else is arbitrary. /// @@ -84,10 +97,12 @@ public enum NotificationSuppression /// No suppression. /// None = 0, + /// /// Suppress title. /// Title = 1, + /// /// Suppress detail. /// @@ -104,14 +119,17 @@ public enum NotificationRendering /// Send notification to native visual popup notificaiton handler. /// NativeVisual = 1, + /// /// Send notification to native speech notification handler. /// NativeVocal = 2, + /// /// Send notification to all installed notifier plugins. /// PluginNotifier = 4, + /// /// Send notification to all available handlers. /// @@ -128,18 +146,21 @@ public enum LogMonitorState : uint /// Monitoring is stopped. /// Idle = 0, + /// /// Real-time monitoring is active. /// Realtime = 1, + /// /// Batch read of historical journals is in progress. /// - Batch = 2, + BatchProcessing = 2, + /// - /// 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 . /// - PreRead = 4 + Init = 4 } /// @@ -164,6 +185,6 @@ public class LogMonitorStateChangedEventArgs : EventArgs /// A boolean; True iff the state provided represents a batch-mode read. public static bool IsBatchRead(LogMonitorState state) { - return state.HasFlag(LogMonitorState.Batch) || state.HasFlag(LogMonitorState.PreRead); + return state.HasFlag(LogMonitorState.BatchProcessing) || state.HasFlag(LogMonitorState.Init); } } \ No newline at end of file diff --git a/ObservatoryFramework/Files/Journal/FleetCarrier/CarrierDecommission.cs b/ObservatoryFramework/Files/Journal/FleetCarrier/CarrierDecommission.cs index f20b7cd..c78d1b1 100644 --- a/ObservatoryFramework/Files/Journal/FleetCarrier/CarrierDecommission.cs +++ b/ObservatoryFramework/Files/Journal/FleetCarrier/CarrierDecommission.cs @@ -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; } \ No newline at end of file diff --git a/ObservatoryFramework/Files/Journal/FleetCarrier/CarrierJumpRequest.cs b/ObservatoryFramework/Files/Journal/FleetCarrier/CarrierJumpRequest.cs index e46030d..c107492 100644 --- a/ObservatoryFramework/Files/Journal/FleetCarrier/CarrierJumpRequest.cs +++ b/ObservatoryFramework/Files/Journal/FleetCarrier/CarrierJumpRequest.cs @@ -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; } } \ No newline at end of file diff --git a/ObservatoryFramework/Files/Journal/JournalBase.cs b/ObservatoryFramework/Files/Journal/JournalBase.cs index 861d87b..ba975c0 100644 --- a/ObservatoryFramework/Files/Journal/JournalBase.cs +++ b/ObservatoryFramework/Files/Journal/JournalBase.cs @@ -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 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(); - } } \ No newline at end of file diff --git a/ObservatoryFramework/Files/Journal/JournalUtilities.cs b/ObservatoryFramework/Files/Journal/JournalUtilities.cs index 018a3dd..32a31da 100644 --- a/ObservatoryFramework/Files/Journal/JournalUtilities.cs +++ b/ObservatoryFramework/Files/Journal/JournalUtilities.cs @@ -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."; } \ No newline at end of file diff --git a/ObservatoryFramework/Files/Journal/Travel/Docked.cs b/ObservatoryFramework/Files/Journal/Travel/Docked.cs index 88234fc..2a93f85 100644 --- a/ObservatoryFramework/Files/Journal/Travel/Docked.cs +++ b/ObservatoryFramework/Files/Journal/Travel/Docked.cs @@ -22,23 +22,14 @@ public class Docked : JournalBase [Obsolete(JournalUtilities.ObsoleteMessage), JsonConverter(typeof(LegacyFactionConverter))] 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 StationEconomies { get; init; } diff --git a/ObservatoryFramework/Files/Journal/Travel/FSDJump.cs b/ObservatoryFramework/Files/Journal/Travel/FSDJump.cs index 8d198ad..a67bda2 100644 --- a/ObservatoryFramework/Files/Journal/Travel/FSDJump.cs +++ b/ObservatoryFramework/Files/Journal/Travel/FSDJump.cs @@ -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 diff --git a/ObservatoryFramework/Files/Journal/Travel/Location.cs b/ObservatoryFramework/Files/Journal/Travel/Location.cs index 0533890..b63a20b 100644 --- a/ObservatoryFramework/Files/Journal/Travel/Location.cs +++ b/ObservatoryFramework/Files/Journal/Travel/Location.cs @@ -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 diff --git a/ObservatoryFramework/Files/ParameterTypes/CurrentGoal.cs b/ObservatoryFramework/Files/ParameterTypes/CurrentGoal.cs index e898e4c..b3431b5 100644 --- a/ObservatoryFramework/Files/ParameterTypes/CurrentGoal.cs +++ b/ObservatoryFramework/Files/ParameterTypes/CurrentGoal.cs @@ -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; } diff --git a/ObservatoryFramework/Files/ParameterTypes/MaterialTrader.cs b/ObservatoryFramework/Files/ParameterTypes/MaterialTrader.cs index 3f6eb5a..d63d1df 100644 --- a/ObservatoryFramework/Files/ParameterTypes/MaterialTrader.cs +++ b/ObservatoryFramework/Files/ParameterTypes/MaterialTrader.cs @@ -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; } diff --git a/ObservatoryFramework/IObservatoryCore.cs b/ObservatoryFramework/IObservatoryCore.cs new file mode 100644 index 0000000..b68db45 --- /dev/null +++ b/ObservatoryFramework/IObservatoryCore.cs @@ -0,0 +1,81 @@ +using Observatory.Framework.Files; + +namespace Observatory.Framework; + +/// +/// Interface passed by Observatory Core to plugins. Primarily used for sending notifications and UI updates back to Core. +/// +public interface IObservatoryCore +{ + /// + /// Send a notification out to all native notifiers and any plugins implementing IObservatoryNotifier. + /// + /// Title text for notification. + /// Detail/body text for notificaiton. + /// Guid associated with the notification during its lifetime. Used as an argument with CancelNotification and UpdateNotification. + public Guid SendNotification(string title, string detail); + + /// + /// Send a notification with arguments out to all native notifiers and any plugins implementing IObservatoryNotifier. + /// + /// NotificationArgs object specifying notification content and behaviour. + /// Guid associated with the notification during its lifetime. Used as an argument with CancelNotification and UpdateNotification. + public Guid SendNotification(NotificationArgs notificationEventArgs); + + /// + /// Cancel or close an active notification. + /// + /// Guid of notification to be cancelled. + public void CancelNotification(Guid notificationId); + + /// + /// Update an active notification with a new set of NotificationsArgs. Timeout values are reset and begin counting again from zero if specified. + /// + /// Guid of notification to be updated. + /// NotificationArgs object specifying updated notification content and behaviour. + public void UpdateNotification(Guid notificationId, NotificationArgs notificationEventArgs); + + /// + /// Requests current Elite Dangerous status.json content. + /// + /// Status object reflecting current Elite Dangerous player status. + public Status GetStatus(); + + /// + /// Version string of Observatory Core. + /// + public string Version { get; } + + /// + /// Returns a delegate for logging an error for the calling plugin. A plugin can wrap this method + /// or pass it along to its collaborators. + /// + /// The calling plugin + public Action GetPluginErrorLogger(IObservatoryPlugin plugin); + + /// + /// Shared application HttpClient object. Provided so that plugins can adhere to .NET recommended behaviour of a single HttpClient object per application. + /// + public HttpClient HttpClient { get; } + + /// + /// Returns the current LogMonitor state. + /// + public LogMonitorState CurrentLogMonitorState { get; } + + /// + /// Returns true if the current LogMonitor state represents a batch-read mode. + /// + public bool IsLogMonitorBatchReading { get; } + + /// + /// Retrieves and ensures creation of a location which can be used by the plugin to store persistent data. + /// + public string PluginStorageFolder { get; } + + /// + /// 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. + /// + /// Utf8 data to be sent. Must be serializable to JSON. + public void SendPluginMessage(IObservatoryPlugin plugin, ReadOnlySpan message); +} \ No newline at end of file diff --git a/ObservatoryFramework/Interfaces.cs b/ObservatoryFramework/Interfaces.cs index 6e8fb1a..9f4c11d 100644 --- a/ObservatoryFramework/Interfaces.cs +++ b/ObservatoryFramework/Interfaces.cs @@ -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.
/// Can be omitted, in which case the full Name will be used. ///
- public string ShortName { get => Name; } + public string ShortName => Name; /// /// Version string displayed in the Core settings tab's plugin list.
@@ -39,23 +38,6 @@ public interface IObservatoryPlugin ///
public PluginUI PluginUI { get; } - /// - /// Accessors for plugin settings object. Should be initialized with a default state during the plugin constructor. - /// 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. - /// 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.
- /// The [SettingDisplayName(string name)] attribute can be used to specify a display name, otherwise the name of the property will be used.
- /// Private or internal properties and methods are ignored and can be used for backing values or any other purpose.
- /// If a public property is necessary but not required to be user accessible the [SettingIgnore] property will suppress display.
- ///
- public object Settings { get; set; } - - /// - /// Plugin-specific object implementing the IComparer interface which is used to sort columns in the basic UI datagrid. - /// If omitted a natural sort order is used. - /// - public IObservatoryComparer ColumnSorter - { get => null; } - /// /// Receives data sent by other plugins. /// @@ -93,24 +75,6 @@ public interface IObservatoryWorker : IObservatoryPlugin /// public void LogMonitorStateChanged(LogMonitorStateChangedEventArgs eventArgs) { } - - /// - /// Method called when the user begins "Read All" journal processing, before any journal events are sent.
- /// Used to track if a "Read All" operation is in progress or not to avoid unnecessary processing or notifications.
- /// Can be omitted for plugins which do not require the distinction. - ///
- [Obsolete("Deprecated in favour of LogMonitorStateChanged")] - public void ReadAllStarted() - { } - - /// - /// Method called when "Read All" journal processing completes.
- /// Used to track if a "Read All" operation is in progress or not to avoid unnecessary processing or notifications.
- /// Can be omitted for plugins which do not require the distinction. - ///
- [Obsolete("Deprecated in favour of LogMonitorStateChanged")] - public void ReadAllFinished() - { } } /// @@ -127,116 +91,3 @@ public interface IObservatoryNotifier : IObservatoryPlugin public void OnNotificationEvent(NotificationArgs notificationEventArgs); } -/// -/// Interface passed by Observatory Core to plugins. Primarily used for sending notifications and UI updates back to Core. -/// -public interface IObservatoryCore -{ - /// - /// Send a notification out to all native notifiers and any plugins implementing IObservatoryNotifier. - /// - /// Title text for notification. - /// Detail/body text for notificaiton. - /// Guid associated with the notification during its lifetime. Used as an argument with CancelNotification and UpdateNotification. - public Guid SendNotification(string title, string detail); - - /// - /// Send a notification with arguments out to all native notifiers and any plugins implementing IObservatoryNotifier. - /// - /// NotificationArgs object specifying notification content and behaviour. - /// Guid associated with the notification during its lifetime. Used as an argument with CancelNotification and UpdateNotification. - public Guid SendNotification(NotificationArgs notificationEventArgs); - - /// - /// Cancel or close an active notification. - /// - /// Guid of notification to be cancelled. - public void CancelNotification(Guid notificationId); - - /// - /// Update an active notification with a new set of NotificationsArgs. Timeout values are reset and begin counting again from zero if specified. - /// - /// Guid of notification to be updated. - /// NotificationArgs object specifying updated notification content and behaviour. - public void UpdateNotification(Guid notificationId, NotificationArgs notificationEventArgs); - - /// - /// Add an item to the bottom of the basic UI grid. - /// - /// Reference to the calling plugin's worker interface. - /// Grid item to be added. Object type should match original template item used to create the grid. - public void AddGridItem(IObservatoryWorker worker, object item); - - /// - /// Add multiple items to the bottom of the basic UI grid. - /// - /// Reference to the calling plugin's worker interface. - /// Grid items to be added. Object types should match original template item used to create the grid. - public void AddGridItems(IObservatoryWorker worker, IEnumerable> items); - - /// - /// Replace the contents of the grid with the provided items. - /// - /// Reference to the calling plugin's worker interface. - /// Grid items to be added. Object types should match original template item used to create the grid. - public void SetGridItems(IObservatoryWorker worker, IEnumerable> items); - - /// - /// Requests current Elite Dangerous status.json content. - /// - /// Status object reflecting current Elite Dangerous player status. - public Status GetStatus(); - - /// - /// Version string of Observatory Core. - /// - public string Version { get; } - - /// - /// Returns a delegate for logging an error for the calling plugin. A plugin can wrap this method - /// or pass it along to its collaborators. - /// - /// The calling plugin - public Action GetPluginErrorLogger(IObservatoryPlugin plugin); - - /// - /// Shared application HttpClient object. Provided so that plugins can adhere to .NET recommended behaviour of a single HttpClient object per application. - /// - public HttpClient HttpClient { get; } - - /// - /// Returns the current LogMonitor state. - /// - public LogMonitorState CurrentLogMonitorState { get; } - - /// - /// Returns true if the current LogMonitor state represents a batch-read mode. - /// - public bool IsLogMonitorBatchReading { get; } - - /// - /// Retrieves and ensures creation of a location which can be used by the plugin to store persistent data. - /// - public string PluginStorageFolder { get; } - - /// - /// 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. - /// - public void SendPluginMessage(IObservatoryPlugin plugin, object message); -} - -/// -/// Extends the base IComparer interface with exposed values for the column ID and sort order to use. -/// -public interface IObservatoryComparer : IComparer -{ - /// - /// Column ID to be currently sorted by. - /// - public int SortColumn { get; set; } - - /// - /// Current order of sorting. Ascending = 1, Descending = -1, No sorting = 0. - /// - public int Order { get; set; } -} \ No newline at end of file diff --git a/ObservatoryFramework/ObservatoryFramework.csproj b/ObservatoryFramework/ObservatoryFramework.csproj index 4c97cc8..26178c4 100644 --- a/ObservatoryFramework/ObservatoryFramework.csproj +++ b/ObservatoryFramework/ObservatoryFramework.csproj @@ -5,6 +5,7 @@ enable Observatory.Framework Debug;Release;Portable + enable diff --git a/Pulsar.sln b/Pulsar.sln index e7bb23f..6be4d9f 100644 --- a/Pulsar.sln +++ b/Pulsar.sln @@ -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 diff --git a/Pulsar/Assets/EOCIcon-Presized.ico b/Pulsar/Assets/EOCIcon-Presized.ico deleted file mode 100644 index 6103052..0000000 Binary files a/Pulsar/Assets/EOCIcon-Presized.ico and /dev/null differ diff --git a/Pulsar/Global.Usings.cs b/Pulsar/Global.Usings.cs new file mode 100644 index 0000000..e289899 --- /dev/null +++ b/Pulsar/Global.Usings.cs @@ -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; \ No newline at end of file diff --git a/Pulsar/LoggingUtils.cs b/Pulsar/LoggingUtils.cs index 719cb66..6f74ebb 100644 --- a/Pulsar/LoggingUtils.cs +++ b/Pulsar/LoggingUtils.cs @@ -1,5 +1,3 @@ -using System.Text; - namespace Pulsar; public static class LoggingUtils diff --git a/Pulsar/PluginManagement/PlaceholderPlugin.cs b/Pulsar/PluginManagement/PlaceholderPlugin.cs deleted file mode 100644 index 0145e2a..0000000 --- a/Pulsar/PluginManagement/PlaceholderPlugin.cs +++ /dev/null @@ -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) - { } -} \ No newline at end of file diff --git a/Pulsar/PluginManagement/PluginCore.cs b/Pulsar/PluginManagement/PluginCore.cs deleted file mode 100644 index 3d85b2c..0000000 --- a/Pulsar/PluginManagement/PluginCore.cs +++ /dev/null @@ -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 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(); - } - - /// - /// Adds an item to the datagrid on UI thread to ensure visual update. - /// - /// - /// - public void AddGridItem(IObservatoryWorker worker, object item) - { - worker.PluginUI.DataGrid.Add(item); - } - - public void AddGridItems(IObservatoryWorker worker, IEnumerable> items) - { - - } - - public void SetGridItems(IObservatoryWorker worker, IEnumerable> 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 Notification; - - internal event EventHandler 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)); - } -} \ No newline at end of file diff --git a/Pulsar/PluginManagement/PluginEventHandler.cs b/Pulsar/PluginManagement/PluginEventHandler.cs deleted file mode 100644 index 7984d06..0000000 --- a/Pulsar/PluginManagement/PluginEventHandler.cs +++ /dev/null @@ -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 observatoryWorkers; - private IEnumerable observatoryNotifiers; - private HashSet disabledPlugins; - private List<(string error, string detail)> errorList; - private Timer timer; - - public PluginEventHandler(IEnumerable observatoryWorkers, IEnumerable 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().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; - } -} \ No newline at end of file diff --git a/Pulsar/PluginManagement/PluginManager.cs b/Pulsar/PluginManagement/PluginManager.cs deleted file mode 100644 index 08fb22e..0000000 --- a/Pulsar/PluginManagement/PluginManager.cs +++ /dev/null @@ -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 _instance = new Lazy(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 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 GetSettingDisplayNames(object settings) - { - var settingNames = new Dictionary(); - - if (settings != null) - { - var properties = settings.GetType().GetProperties(); - foreach (var property in properties) - { - var attrib = property.GetCustomAttribute(); - 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().ToArray(); - } - catch - { - types = Array.Empty(); - } - - IEnumerable workerTypes = types.Where(t => t.IsAssignableTo(typeof(IObservatoryWorker))); - foreach (var worker in workerTypes) - { - var constructor = worker.GetConstructor(Array.Empty()); - if (constructor != null) - { - var instance = constructor.Invoke(Array.Empty()); - 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()); - if (constructor != null) - { - var instance = constructor.Invoke(Array.Empty()); - 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)); - } - - /// - /// Possible plugin load results and signature statuses. - /// - public enum PluginStatus - { - /// - /// Plugin valid and signed with matching certificate. - /// - Signed, - /// - /// Plugin valid but not signed with any certificate. - /// - Unsigned, - /// - /// Plugin valid but not signed with valid certificate. - /// - InvalidSignature, - /// - /// Plugin invalid and cannot be loaded. Possible version mismatch. - /// - InvalidPlugin, - /// - /// Plugin not a CLR library. - /// - InvalidLibrary, - /// - /// Plugin valid but executing assembly has no certificate to match against. - /// - NoCert, - /// - /// Plugin signature checks disabled. - /// - SigCheckDisabled - } -} \ No newline at end of file diff --git a/Pulsar/Program.cs b/Pulsar/Program.cs index 1e98164..4ff2411 100644 --- a/Pulsar/Program.cs +++ b/Pulsar/Program.cs @@ -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, ""); } \ No newline at end of file diff --git a/Pulsar/Pulsar.csproj b/Pulsar/Pulsar.csproj index 6835446..d21ab34 100644 --- a/Pulsar/Pulsar.csproj +++ b/Pulsar/Pulsar.csproj @@ -6,6 +6,7 @@ enable enable Pulsar + latest diff --git a/Pulsar/Utils/CollectionExtesions.cs b/Pulsar/Utils/CollectionExtesions.cs new file mode 100644 index 0000000..fc27ed6 --- /dev/null +++ b/Pulsar/Utils/CollectionExtesions.cs @@ -0,0 +1,14 @@ +namespace Pulsar.Utils; + +public static class CollectionExtensions +{ + public static void Add(this ICollection<(T1,T2)> collection, T1 t1, T2 t2) + { + collection.Add((t1, t2)); + } + + public static void Add(this ICollection<(T1,T2,T3)> collection, T1 t1, T2 t2, T3 t3) + { + collection.Add((t1, t2, t3)); + } +} \ No newline at end of file diff --git a/Pulsar/Utils/ErrorReporter.cs b/Pulsar/Utils/ErrorReporter.cs index 6053475..49b93d6 100644 --- a/Pulsar/Utils/ErrorReporter.cs +++ b/Pulsar/Utils/ErrorReporter.cs @@ -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(); } diff --git a/Pulsar/Utils/HttpClient.cs b/Pulsar/Utils/HttpClient.cs index 36351d5..fb75fb5 100644 --- a/Pulsar/Utils/HttpClient.cs +++ b/Pulsar/Utils/HttpClient.cs @@ -7,13 +7,7 @@ public sealed class HttpClient private static readonly Lazy lazy = new Lazy(() => 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) { diff --git a/Pulsar/Utils/JournalReader.cs b/Pulsar/Utils/JournalReader.cs index 7efa47a..588f3df 100644 --- a/Pulsar/Utils/JournalReader.cs +++ b/Pulsar/Utils/JournalReader.cs @@ -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(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(json.Replace("\"RotationPeriod\":inf,", "")); + invalidJson = new InvalidJson + { + Event = "InvalidJson", + Timestamp = DateTimeOffset.UnixEpoch, + OriginalEvent = "Invalid" + }; } - else - { - deserialized = JsonSerializer.Deserialize(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(json.Replace("\"RotationPeriod\":inf,", "")); + } + else + { + deserialized = JsonSerializer.Deserialize(json); } - - public static Dictionary PopulateEventClasses() - { - var eventClasses = new Dictionary(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; + } } \ No newline at end of file diff --git a/Pulsar/Utils/LogMonitor.cs b/Pulsar/Utils/LogMonitor.cs index fad9718..cef80ba 100644 --- a/Pulsar/Utils/LogMonitor.cs +++ b/Pulsar/Utils/LogMonitor.cs @@ -5,184 +5,184 @@ 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 _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; } - + #endregion #region Public Methods 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> 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 ReadAllJournals() + IEnumerable ReadAllJournals() + { + var readErrors = new List(); + 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 lastSystemLines = new(); - List lastFileLines = new(); - List 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 lastSystemLines = new(); + List lastFileLines = new(); + List 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() ?? false) || + (line["OnFoot"]?.GetValue() ?? 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 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 LogMonitorStateChanged; + public event EventHandler? LogMonitorStateChanged; - public event EventHandler JournalEntry; + public event EventHandler? JournalEntry; - public event EventHandler StatusUpdate; + public event EventHandler? StatusUpdate; #endregion @@ -194,17 +194,19 @@ class LogMonitor private readonly Dictionary 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 ProcessJournal(IEnumerable 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 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 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 ReadAllLines(string path) - { - var lines = new List(); + 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, "") }); + ReportErrors([(ex, eventArgs.Name ?? string.Empty, line)]); } - return lines; } + currentLine[eventArgs.FullPath] = fileContent.Count(); + } + + private static IEnumerable 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(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; + handler(this, new JournalEventArgs("Status", statusLines)); } + } /// /// Touches most recent journal file once every 250ms while LogMonitor is monitoring. @@ -416,36 +405,38 @@ class LogMonitor /// 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 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 } \ No newline at end of file