Initial Commit

This commit is contained in:
Ben Parsons 2024-04-13 15:54:59 +10:00
commit 63ed43f4af
459 changed files with 7968 additions and 20433 deletions

379
Botanist/Botanist.cs Normal file
View file

@ -0,0 +1,379 @@
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.Odyssey;
using Observatory.Framework.Files.Journal.Other;
using Observatory.Framework.Files.Journal.Startup;
using Observatory.Framework.Files.Journal.Travel;
using Observatory.Framework.Files.ParameterTypes;
namespace Botanist;
public class Botanist : IObservatoryWorker
{
private IObservatoryCore Core;
private bool OdysseyLoaded;
private Dictionary<BodyAddress, BioPlanetDetail> BioPlanets;
// To make this journal locale agnostic, use the genus identifier and map to English names used in notifications.
// Note: Values here are also used in the lookup for colony distance, so we also use this to resolve misspellings and Frontier bugs.
private readonly Dictionary<string, string> EnglishGenusByIdentifier = new()
{
{ "$Codex_Ent_Aleoids_Genus_Name;", "Aleoida" },
{ "$Codex_Ent_Bacterial_Genus_Name;", "Bacterium" },
{ "$Codex_Ent_Cactoid_Genus_Name;", "Cactoida" },
{
"$Codex_Ent_Clepeus_Genus_Name;;", "Clypeus"
}, // Fun misspelling of the identifier discovered in the journals
{ "$Codex_Ent_Clypeus_Genus_Name;", "Clypeus" },
{ "$Codex_Ent_Conchas_Genus_Name;", "Concha" },
{ "$Codex_Ent_Electricae_Genus_Name;", "Electricae" },
{ "$Codex_Ent_Fonticulus_Genus_Name;", "Fonticulua" },
{ "$Codex_Ent_Shrubs_Genus_Name;", "Frutexa" },
{ "$Codex_Ent_Fumerolas_Genus_Name;", "Fumerola" },
{ "$Codex_Ent_Fungoids_Genus_Name;", "Fungoida" },
{ "$Codex_Ent_Osseus_Genus_Name;", "Osseus" },
{ "$Codex_Ent_Recepta_Genus_Name;", "Recepta" },
{ "$Codex_Ent_Stratum_Genus_Name;", "Stratum" },
{ "$Codex_Ent_Tubus_Genus_Name;", "Tubus" },
{ "$Codex_Ent_Tussocks_Genus_Name;", "Tussock" },
{ "$Codex_Ent_Ground_Struct_Ice_Name;", "Crystalline Shards" },
{ "$Codex_Ent_Brancae_Name;", "Brain Trees" },
{
"$Codex_Ent_Seed_Name;", "Brain Tree"
}, // Misspelling? :shrug: 'Seed' also seems to refer to peduncle things.
{ "$Codex_Ent_Sphere_Name;", "Anemone" },
{ "$Codex_Ent_Tube_Name;", "Sinuous Tubers" },
{ "$Codex_Ent_Vents_Name;", "Amphora Plant" },
{ "$Codex_Ent_Cone_Name;", "Bark Mounds" },
};
// Note: Some Horizons bios may be missing, but they'll get localized genus name and default colony distance
private readonly Dictionary<string, int> ColonyDistancesByGenus = new()
{
{ "Aleoida", 150 },
{ "Bacterium", 500 },
{ "Cactoida", 300 },
{ "Clypeus", 150 },
{ "Concha", 150 },
{ "Electricae", 1000 },
{ "Fonticulua", 500 },
{ "Frutexa", 150 },
{ "Fumerola", 100 },
{ "Fungoida", 300 },
{ "Osseus", 800 },
{ "Recepta", 150 },
{ "Stratum", 500 },
{ "Tubus", 800 },
{ "Tussock", 200 },
{ "Crystalline Shards", DEFAULT_COLONY_DISTANCE },
{ "Brain Tree", DEFAULT_COLONY_DISTANCE },
{ "Anemone", DEFAULT_COLONY_DISTANCE },
{ "Sinuous Tubers", DEFAULT_COLONY_DISTANCE },
{ "Amphora Plant", DEFAULT_COLONY_DISTANCE },
{ "Bark Mounds", DEFAULT_COLONY_DISTANCE },
};
private const int DEFAULT_COLONY_DISTANCE = 100;
ObservableCollection<object> GridCollection;
private PluginUI pluginUI;
private Guid? samplerStatusNotification;
private BotanistSettings botanistSettings = new()
{
OverlayEnabled = true,
OverlayIsSticky = true,
};
public string Name => "Observatory Botanist";
public string ShortName => "Botanist";
public string Version => typeof(Botanist).Assembly.GetName().Version.ToString();
public PluginUI PluginUI => pluginUI;
public object Settings
{
get => botanistSettings;
set { botanistSettings = (BotanistSettings)value; }
}
public void JournalEvent<TJournal>(TJournal journal) where TJournal : JournalBase
{
switch (journal)
{
case LoadGame loadGame:
OdysseyLoaded = loadGame.Odyssey;
break;
case SAASignalsFound signalsFound:
{
BodyAddress systemBodyId = new()
{
SystemAddress = signalsFound.SystemAddress,
BodyID = signalsFound.BodyID
};
if (OdysseyLoaded && !BioPlanets.ContainsKey(systemBodyId))
{
var bioSignals = from signal in signalsFound.Signals
where signal.Type == "$SAA_SignalType_Biological;"
select signal;
if (bioSignals.Any())
{
BioPlanets.Add(
systemBodyId,
new()
{
BodyName = signalsFound.BodyName,
BioTotal = bioSignals.First().Count,
SpeciesFound = new()
}
);
}
}
}
break;
case ScanOrganic scanOrganic:
{
BodyAddress systemBodyId = new()
{
SystemAddress = scanOrganic.SystemAddress,
BodyID = scanOrganic.Body
};
if (!BioPlanets.ContainsKey(systemBodyId))
{
// Unlikely to ever end up in here, but just in case create a new planet entry.
Dictionary<string, BioSampleDetail> bioSampleDetails = new();
bioSampleDetails.Add(scanOrganic.Species_Localised, new()
{
Genus = EnglishGenusByIdentifier.GetValueOrDefault(scanOrganic.Genus,
scanOrganic.Genus_Localised),
Analysed = false
});
BioPlanets.Add(systemBodyId, new()
{
BodyName = string.Empty,
BioTotal = 0,
SpeciesFound = bioSampleDetails
});
}
else
{
var bioPlanet = BioPlanets[systemBodyId];
switch (scanOrganic.ScanType)
{
case ScanOrganicType.Log:
case ScanOrganicType.Sample:
if (!Core.IsLogMonitorBatchReading && botanistSettings.OverlayEnabled)
{
var colonyDistance = GetColonyDistance(scanOrganic);
var sampleNum = scanOrganic.ScanType == ScanOrganicType.Log ? 1 : 2;
NotificationArgs args = new()
{
Title = scanOrganic.Species_Localised,
Detail =
$"Sample {sampleNum} of 3{Environment.NewLine}Colony distance: {colonyDistance} m",
Rendering = NotificationRendering.NativeVisual,
Timeout = (botanistSettings.OverlayIsSticky ? 0 : -1),
Sender = ShortName,
};
if (samplerStatusNotification == null)
{
var notificationId = Core.SendNotification(args);
if (botanistSettings.OverlayIsSticky)
samplerStatusNotification = notificationId;
}
else
{
Core.UpdateNotification(samplerStatusNotification.Value, args);
}
}
if (!bioPlanet.SpeciesFound.ContainsKey(scanOrganic.Species_Localised))
{
bioPlanet.SpeciesFound.Add(scanOrganic.Species_Localised, new()
{
Genus = EnglishGenusByIdentifier.GetValueOrDefault(scanOrganic.Genus,
scanOrganic.Genus_Localised),
Analysed = false
});
}
break;
case ScanOrganicType.Analyse:
if (!bioPlanet.SpeciesFound[scanOrganic.Species_Localised].Analysed)
{
bioPlanet.SpeciesFound[scanOrganic.Species_Localised].Analysed = true;
}
MaybeCloseSamplerStatusNotification();
break;
}
}
UpdateUIGrid();
}
break;
case LeaveBody:
case FSDJump:
case Shutdown:
// These are all good reasons to kill any open notification. Note that SupercruiseEntry is NOT a
// suitable reason to close the notification as the player hopping out only to double check the
// DSS map for another location. Note that a game client crash will not close the status notification.
MaybeCloseSamplerStatusNotification();
break;
}
}
private object GetColonyDistance(ScanOrganic scan)
{
// Map the Genus to a Genus name then lookup colony distance.
return ColonyDistancesByGenus.GetValueOrDefault(
EnglishGenusByIdentifier.GetValueOrDefault(scan.Genus, string.Empty), DEFAULT_COLONY_DISTANCE);
}
private void MaybeCloseSamplerStatusNotification()
{
if (samplerStatusNotification != null)
{
Core.CancelNotification(samplerStatusNotification.Value);
samplerStatusNotification = null;
}
}
public void Load(IObservatoryCore observatoryCore)
{
GridCollection = new();
BotanistGrid uiObject = new();
GridCollection.Add(uiObject);
pluginUI = new PluginUI(GridCollection);
BioPlanets = new();
Core = observatoryCore;
}
public void LogMonitorStateChanged(LogMonitorStateChangedEventArgs args)
{
if (LogMonitorStateChangedEventArgs.IsBatchRead(args.NewState))
{
// Beginning a batch read. Clear grid.
Core.SetGridItems(this,
[
typeof(BotanistGrid).GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Select(p => p.Name)
.ToDictionary(p => p, p => string.Empty)
]);
}
else if (LogMonitorStateChangedEventArgs.IsBatchRead(args.PreviousState))
{
// Batch read is complete. Show data.
UpdateUIGrid();
}
}
private void UpdateUIGrid()
{
// Suppress repainting the entire contents of the grid on every ScanOrganic record we read.
if (Core.IsLogMonitorBatchReading) return;
Core.SetGridItems(this, [
typeof(BotanistGrid).GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Select(p => p.Name)
.ToDictionary(p => p, p => string.Empty)
]);
foreach (var bioPlanet in BioPlanets.Values)
{
if (bioPlanet.SpeciesFound.Count == 0)
{
var planetRow = new BotanistGrid
{
Body = bioPlanet.BodyName,
BioTotal = bioPlanet.BioTotal.ToString(),
Species = "(NO SAMPLES TAKEN)",
Analysed = string.Empty,
ColonyDistance = string.Empty,
};
Core.AddGridItem(this, planetRow);
}
var firstRow = true;
foreach (var entry in bioPlanet.SpeciesFound)
{
var colonyDistance =
ColonyDistancesByGenus.GetValueOrDefault(entry.Value.Genus ?? "", DEFAULT_COLONY_DISTANCE);
var speciesRow = new BotanistGrid
{
Body = firstRow ? bioPlanet.BodyName : string.Empty,
BioTotal = firstRow ? bioPlanet.BioTotal.ToString() : string.Empty,
Species = entry.Key,
Analysed = entry.Value.Analysed ? "✓" : "",
ColonyDistance = $"{colonyDistance}m",
};
Core.AddGridItem(this, speciesRow);
firstRow = false;
}
}
}
}
class BodyAddress
{
public ulong SystemAddress { get; set; }
public int BodyID { get; set; }
public override bool Equals(object obj)
{
// We want value equality here.
//
// See the full list of guidelines at
// http://go.microsoft.com/fwlink/?LinkID=85237
// and also the guidance for operator== at
// http://go.microsoft.com/fwlink/?LinkId=85238
//
if (obj == null || GetType() != obj.GetType())
{
return false;
}
var other = (BodyAddress)obj;
return other.SystemAddress == SystemAddress
&& other.BodyID == BodyID;
}
public override int GetHashCode()
{
return HashCode.Combine(SystemAddress, BodyID);
}
}
class BioPlanetDetail
{
public string BodyName { get; set; }
public int BioTotal { get; set; }
public Dictionary<string, BioSampleDetail> SpeciesFound { get; set; }
}
class BioSampleDetail
{
public string Genus { get; set; }
public bool Analysed { get; set; }
}
public class BotanistGrid
{
public string Body { get; set; }
public string BioTotal { get; set; }
public string Species { get; set; }
public string Analysed { get; set; }
public string ColonyDistance { get; set; }
}

15
Botanist/Botanist.csproj Normal file
View file

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<SignAssembly>false</SignAssembly>
<Configurations>Debug;Release;Portable</Configurations>
<RootNamespace>Botanist</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\ObservatoryFramework\ObservatoryFramework.csproj" />
</ItemGroup>
</Project>

25
Botanist/Botanist.sln Normal file
View file

@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.31205.134
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ObservatoryBotanist", "ObservatoryBotanist.csproj", "{498F7360-D443-4D64-895C-9EAB5570D019}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{498F7360-D443-4D64-895C-9EAB5570D019}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{498F7360-D443-4D64-895C-9EAB5570D019}.Debug|Any CPU.Build.0 = Debug|Any CPU
{498F7360-D443-4D64-895C-9EAB5570D019}.Release|Any CPU.ActiveCfg = Release|Any CPU
{498F7360-D443-4D64-895C-9EAB5570D019}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {0CC48015-0A6F-420C-9939-A71F3D33FF60}
EndGlobalSection
EndGlobal

View file

@ -0,0 +1,12 @@
using Observatory.Framework;
namespace Botanist;
class BotanistSettings
{
[SettingDisplayName("Enable Sampler Status Overlay")]
public bool OverlayEnabled { get; set; }
[SettingDisplayName("Status Overlay is sticky until sampling is complete")]
public bool OverlayIsSticky { get; set; }
}

View file

@ -1 +0,0 @@
* @xjph

View file

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

View file

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

389
Explorer/DefaultCriteria.cs Normal file
View file

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

304
Explorer/Explorer.cs Normal file
View file

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

17
Explorer/Explorer.csproj Normal file
View file

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

25
Explorer/Explorer.sln Normal file
View file

@ -0,0 +1,25 @@

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

View file

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

View file

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

121
Explorer/Worker.cs Normal file
View file

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

View file

@ -1,353 +0,0 @@
using Observatory.Framework;
using Observatory.Framework.Files;
using Observatory.Framework.Files.Journal;
using Observatory.Framework.Interfaces;
using Observatory.Framework.Files.ParameterTypes;
using System.Collections.Generic;
using System.Linq;
using System;
using System.Collections.ObjectModel;
namespace Observatory.Botanist
{
public class Botanist : IObservatoryWorker
{
private IObservatoryCore Core;
private bool OdysseyLoaded = false;
private Dictionary<BodyAddress, BioPlanetDetail> BioPlanets;
// To make this journal locale agnostic, use the genus identifier and map to English names used in notifications.
// Note: Values here are also used in the lookup for colony distance, so we also use this to resolve misspellings and Frontier bugs.
private readonly Dictionary<String, String> EnglishGenusByIdentifier = new() {
{ "$Codex_Ent_Aleoids_Genus_Name;", "Aleoida" },
{ "$Codex_Ent_Bacterial_Genus_Name;", "Bacterium" },
{ "$Codex_Ent_Cactoid_Genus_Name;", "Cactoida" },
{ "$Codex_Ent_Clepeus_Genus_Name;;", "Clypeus" }, // Fun misspelling of the identifier discovered in the journals
{ "$Codex_Ent_Clypeus_Genus_Name;", "Clypeus" },
{ "$Codex_Ent_Conchas_Genus_Name;", "Concha" },
{ "$Codex_Ent_Electricae_Genus_Name;", "Electricae" },
{ "$Codex_Ent_Fonticulus_Genus_Name;", "Fonticulua" },
{ "$Codex_Ent_Shrubs_Genus_Name;", "Frutexa" },
{ "$Codex_Ent_Fumerolas_Genus_Name;", "Fumerola" },
{ "$Codex_Ent_Fungoids_Genus_Name;", "Fungoida" },
{ "$Codex_Ent_Osseus_Genus_Name;", "Osseus" },
{ "$Codex_Ent_Recepta_Genus_Name;", "Recepta" },
{ "$Codex_Ent_Stratum_Genus_Name;", "Stratum" },
{ "$Codex_Ent_Tubus_Genus_Name;", "Tubus" },
{ "$Codex_Ent_Tussocks_Genus_Name;", "Tussock" },
{ "$Codex_Ent_Ground_Struct_Ice_Name;", "Crystalline Shards" },
{ "$Codex_Ent_Brancae_Name;", "Brain Trees" },
{ "$Codex_Ent_Seed_Name;", "Brain Tree" }, // Misspelling? :shrug: 'Seed' also seems to refer to peduncle things.
{ "$Codex_Ent_Sphere_Name;", "Anemone" },
{ "$Codex_Ent_Tube_Name;", "Sinuous Tubers" },
{ "$Codex_Ent_Vents_Name;", "Amphora Plant" },
{ "$Codex_Ent_Cone_Name;", "Bark Mounds" },
};
// Note: Some Horizons bios may be missing, but they'll get localized genus name and default colony distance
private readonly Dictionary<String, int> ColonyDistancesByGenus = new() {
{ "Aleoida", 150 },
{ "Bacterium", 500 },
{ "Cactoida", 300 },
{ "Clypeus", 150 },
{ "Concha", 150 },
{ "Electricae", 1000 },
{ "Fonticulua", 500 },
{ "Frutexa", 150 },
{ "Fumerola", 100 },
{ "Fungoida", 300 },
{ "Osseus", 800 },
{ "Recepta", 150 },
{ "Stratum", 500 },
{ "Tubus", 800 },
{ "Tussock", 200 },
{ "Crystalline Shards", DEFAULT_COLONY_DISTANCE },
{ "Brain Tree", DEFAULT_COLONY_DISTANCE },
{ "Anemone", DEFAULT_COLONY_DISTANCE },
{ "Sinuous Tubers", DEFAULT_COLONY_DISTANCE },
{ "Amphora Plant", DEFAULT_COLONY_DISTANCE },
{ "Bark Mounds", DEFAULT_COLONY_DISTANCE },
};
private const int DEFAULT_COLONY_DISTANCE = 100;
ObservableCollection<object> GridCollection;
private PluginUI pluginUI;
private Guid? samplerStatusNotification = null;
private BotanistSettings botanistSettings = new()
{
OverlayEnabled = true,
OverlayIsSticky = true,
};
public string Name => "Observatory Botanist";
public string ShortName => "Botanist";
public string Version => typeof(Botanist).Assembly.GetName().Version.ToString();
public PluginUI PluginUI => pluginUI;
public object Settings { get => botanistSettings; set { botanistSettings = (BotanistSettings)value; } }
public void JournalEvent<TJournal>(TJournal journal) where TJournal : JournalBase
{
switch (journal)
{
case LoadGame loadGame:
OdysseyLoaded = loadGame.Odyssey;
break;
case SAASignalsFound signalsFound:
{
BodyAddress systemBodyId = new()
{
SystemAddress = signalsFound.SystemAddress,
BodyID = signalsFound.BodyID
};
if (OdysseyLoaded && !BioPlanets.ContainsKey(systemBodyId))
{
var bioSignals = from signal in signalsFound.Signals
where signal.Type == "$SAA_SignalType_Biological;"
select signal;
if (bioSignals.Any())
{
BioPlanets.Add(
systemBodyId,
new() {
BodyName = signalsFound.BodyName,
BioTotal = bioSignals.First().Count,
SpeciesFound = new()
}
);
}
}
}
break;
case ScanOrganic scanOrganic:
{
BodyAddress systemBodyId = new()
{
SystemAddress = scanOrganic.SystemAddress,
BodyID = scanOrganic.Body
};
if (!BioPlanets.ContainsKey(systemBodyId))
{
// Unlikely to ever end up in here, but just in case create a new planet entry.
Dictionary<string, BioSampleDetail> bioSampleDetails = new();
bioSampleDetails.Add(scanOrganic.Species_Localised, new()
{
Genus = EnglishGenusByIdentifier.GetValueOrDefault(scanOrganic.Genus, scanOrganic.Genus_Localised),
Analysed = false
});
BioPlanets.Add(systemBodyId, new() {
BodyName = string.Empty,
BioTotal = 0,
SpeciesFound = bioSampleDetails
});
}
else
{
var bioPlanet = BioPlanets[systemBodyId];
switch (scanOrganic.ScanType)
{
case ScanOrganicType.Log:
case ScanOrganicType.Sample:
if (!Core.IsLogMonitorBatchReading && botanistSettings.OverlayEnabled)
{
var colonyDistance = GetColonyDistance(scanOrganic);
var sampleNum = scanOrganic.ScanType == ScanOrganicType.Log ? 1 : 2;
NotificationArgs args = new()
{
Title = scanOrganic.Species_Localised,
Detail = $"Sample {sampleNum} of 3{Environment.NewLine}Colony distance: {colonyDistance} m",
Rendering = NotificationRendering.NativeVisual,
Timeout = (botanistSettings.OverlayIsSticky ? 0 : -1),
Sender = ShortName,
};
if (samplerStatusNotification == null)
{
var notificationId = Core.SendNotification(args);
if (botanistSettings.OverlayIsSticky)
samplerStatusNotification = notificationId;
}
else
{
Core.UpdateNotification(samplerStatusNotification.Value, args);
}
}
if (!bioPlanet.SpeciesFound.ContainsKey(scanOrganic.Species_Localised))
{
bioPlanet.SpeciesFound.Add(scanOrganic.Species_Localised, new()
{
Genus = EnglishGenusByIdentifier.GetValueOrDefault(scanOrganic.Genus, scanOrganic.Genus_Localised),
Analysed = false
});
}
break;
case ScanOrganicType.Analyse:
if (!bioPlanet.SpeciesFound[scanOrganic.Species_Localised].Analysed)
{
bioPlanet.SpeciesFound[scanOrganic.Species_Localised].Analysed = true;
}
MaybeCloseSamplerStatusNotification();
break;
}
}
UpdateUIGrid();
}
break;
case LeaveBody:
case FSDJump:
case Shutdown:
// These are all good reasons to kill any open notification. Note that SupercruiseEntry is NOT a
// suitable reason to close the notification as the player hopping out only to double check the
// DSS map for another location. Note that a game client crash will not close the status notification.
MaybeCloseSamplerStatusNotification();
break;
}
}
private object GetColonyDistance(ScanOrganic scan)
{
// Map the Genus to a Genus name then lookup colony distance.
return ColonyDistancesByGenus.GetValueOrDefault(EnglishGenusByIdentifier.GetValueOrDefault(scan.Genus, String.Empty), DEFAULT_COLONY_DISTANCE);
}
private void MaybeCloseSamplerStatusNotification()
{
if (samplerStatusNotification != null)
{
Core.CancelNotification(samplerStatusNotification.Value);
samplerStatusNotification = null;
}
}
public void Load(IObservatoryCore observatoryCore)
{
GridCollection = new();
BotanistGrid uiObject = new();
GridCollection.Add(uiObject);
pluginUI = new PluginUI(GridCollection);
BioPlanets = new();
Core = observatoryCore;
}
public void LogMonitorStateChanged(LogMonitorStateChangedEventArgs args)
{
if (LogMonitorStateChangedEventArgs.IsBatchRead(args.NewState))
{
// Beginning a batch read. Clear grid.
Core.ClearGrid(this, new BotanistGrid());
}
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;
BotanistGrid uiObject = new();
Core.ClearGrid(this, uiObject);
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);
}
bool firstRow = true;
foreach (var entry in bioPlanet.SpeciesFound)
{
int 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;
}
BodyAddress other = (BodyAddress)obj;
return other.SystemAddress == SystemAddress
&& other.BodyID == BodyID;
}
public override int GetHashCode()
{
return HashCode.Combine(SystemAddress, BodyID);
}
}
class BioPlanetDetail
{
public string BodyName { get; set; }
public int BioTotal { get; set; }
public Dictionary<string, BioSampleDetail> SpeciesFound { get; set; }
}
class BioSampleDetail
{
public string Genus { get; set; }
public bool Analysed { get; set; }
}
public class BotanistGrid
{
[ColumnSuggestedWidth(300)]
public string Body { get; set; }
[ColumnSuggestedWidth(100)]
public string BioTotal { get; set; }
[ColumnSuggestedWidth(300)]
public string Species { get; set; }
[ColumnSuggestedWidth(100)]
public string Analysed { get; set; }
[ColumnSuggestedWidth(100)]
public string ColonyDistance { get; set; }
}
}

View file

@ -1,18 +0,0 @@
using Observatory.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Observatory.Botanist
{
[SettingSuggestedColumnWidth(450)]
class BotanistSettings
{
[SettingDisplayName("Enable Sampler Status Overlay")]
public bool OverlayEnabled { get; set; }
[SettingDisplayName("Status Overlay is sticky until sampling is complete")]
public bool OverlayIsSticky { get; set; }
}
}

View file

@ -1,30 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<SignAssembly>false</SignAssembly>
<AssemblyOriginatorKeyFile>ObservatoryKey.snk</AssemblyOriginatorKeyFile>
<Configurations>Debug;Release;Portable</Configurations>
</PropertyGroup>
<PropertyGroup>
<VersionSuffix>0.22.$([System.DateTime]::UtcNow.Year.ToString().Substring(2))$([System.DateTime]::UtcNow.DayOfYear.ToString().PadLeft(3, "0")).$([System.DateTime]::UtcNow.ToString(HHmm))</VersionSuffix>
<AssemblyVersion Condition=" '$(VersionSuffix)' == '' ">0.0.0.1</AssemblyVersion>
<AssemblyVersion Condition=" '$(VersionSuffix)' != '' ">$(VersionSuffix)</AssemblyVersion>
<Version Condition=" '$(VersionSuffix)' == '' ">0.0.1.0</Version>
<Version Condition=" '$(VersionSuffix)' != '' ">$(VersionSuffix)</Version>
</PropertyGroup>
<ItemGroup>
<Reference Include="ObservatoryFramework">
<HintPath>..\ObservatoryFramework\bin\Release\net6.0\ObservatoryFramework.dll</HintPath>
</Reference>
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Condition=" '$(OS)' == 'Windows_NT' " Command="xcopy &quot;$(TargetPath)&quot; &quot;$(ProjectDir)..\ObservatoryCore\$(OutDir)..\net6.0-windows\plugins\&quot; /y" />
<Exec Condition=" '$(OS)' != 'Windows_NT' " Command="cp &quot;$(TargetPath)&quot; &quot;$(ProjectDir)../ObservatoryCore/$(OutDir)plugins/&quot; -f" />
</Target>
</Project>

View file

@ -1,73 +0,0 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace Observatory.Assets {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Observatory.Assets.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized resource of type System.Drawing.Icon similar to (Icon).
/// </summary>
internal static System.Drawing.Icon EOCIcon_Presized {
get {
object obj = ResourceManager.GetObject("EOCIcon_Presized", resourceCulture);
return ((System.Drawing.Icon)(obj));
}
}
}
}

View file

@ -1,124 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
<data name="EOCIcon_Presized" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>EOCIcon-Presized.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
</root>

View file

@ -1,79 +0,0 @@
using Observatory.Framework;
using Observatory.UI;
using System;
namespace Observatory.NativeNotification
{
public class NativePopup
{
private Dictionary<Guid, NotificationForm> notifications;
public NativePopup()
{
notifications = new();
}
public Guid InvokeNativeNotification(NotificationArgs notificationArgs)
{
var notificationGuid = Guid.NewGuid();
Application.OpenForms[0].Invoke(() =>
{
var notification = new NotificationForm(notificationGuid, notificationArgs);
notification.FormClosed += NotifyWindow_Closed;
foreach(var notificationForm in notifications)
{
notificationForm.Value.AdjustOffset(true);
}
notifications.Add(notificationGuid, notification);
notification.Show();
});
return notificationGuid;
}
private void NotifyWindow_Closed(object? sender, EventArgs e)
{
if (sender != null)
{
var currentNotification = (NotificationForm)sender;
foreach (var notification in notifications.Where(n => n.Value.CreationTime < currentNotification.CreationTime))
{
notification.Value.AdjustOffset(false);
}
if (notifications.ContainsKey(currentNotification.Guid))
{
notifications.Remove(currentNotification.Guid);
}
}
}
public void CloseNotification(Guid guid)
{
if (notifications.ContainsKey(guid))
{
notifications[guid].Close();
}
}
public void UpdateNotification(Guid guid, NotificationArgs notificationArgs)
{
if (notifications.ContainsKey(guid))
{
notifications[guid].Update(notificationArgs);
}
}
public void CloseAll()
{
foreach (var notification in notifications)
{
notification.Value?.Close();
}
}
}
}

Some files were not shown because too many files have changed in this diff Show more