Initial Commit
This commit is contained in:
parent
8e178cbb7b
commit
63ed43f4af
459 changed files with 7968 additions and 20433 deletions
379
Botanist/Botanist.cs
Normal file
379
Botanist/Botanist.cs
Normal 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
15
Botanist/Botanist.csproj
Normal 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
25
Botanist/Botanist.sln
Normal 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
|
||||||
12
Botanist/BotanistSettings.cs
Normal file
12
Botanist/BotanistSettings.cs
Normal 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; }
|
||||||
|
}
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
* @xjph
|
|
||||||
13
Explorer/CriteriaLoadException.cs
Normal file
13
Explorer/CriteriaLoadException.cs
Normal 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;
|
||||||
|
}
|
||||||
382
Explorer/CustomCriteriaManager.cs
Normal file
382
Explorer/CustomCriteriaManager.cs
Normal 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
389
Explorer/DefaultCriteria.cs
Normal 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
304
Explorer/Explorer.cs
Normal 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
17
Explorer/Explorer.csproj
Normal 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
25
Explorer/Explorer.sln
Normal 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
|
||||||
78
Explorer/ExplorerSettings.cs
Normal file
78
Explorer/ExplorerSettings.cs
Normal 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; }
|
||||||
|
}
|
||||||
12
Explorer/ExplorerUIResults.cs
Normal file
12
Explorer/ExplorerUIResults.cs
Normal 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
121
Explorer/Worker.cs
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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 "$(TargetPath)" "$(ProjectDir)..\ObservatoryCore\$(OutDir)..\net6.0-windows\plugins\" /y" />
|
|
||||||
<Exec Condition=" '$(OS)' != 'Windows_NT' " Command="cp "$(TargetPath)" "$(ProjectDir)../ObservatoryCore/$(OutDir)plugins/" -f" />
|
|
||||||
</Target>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
73
ObservatoryCore/Assets/Resources.Designer.cs
generated
73
ObservatoryCore/Assets/Resources.Designer.cs
generated
|
|
@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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
Loading…
Reference in a new issue