mirror of
https://github.com/9ParsonsB/Pulsar.git
synced 2025-07-02 17:03:41 -04:00
Initial Commit
This commit is contained in:
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; }
|
||||
}
|
Reference in New Issue
Block a user