From 5d3af78a695c727a921c4e111c286fe994d7465c Mon Sep 17 00:00:00 2001 From: Florian Stinglmayr Date: Fri, 9 Jul 2021 11:03:30 +0200 Subject: [PATCH] Add project files. --- App.config | 6 + App.xaml | 9 ++ App.xaml.cs | 18 +++ BGS/Cartographics.cs | 38 +++++ BGS/CombatZone.cs | 11 ++ BGS/DiscordLogGenerator.cs | 11 ++ BGS/LogEntry.cs | 24 +++ BGS/MissionCompleted.cs | 42 +++++ BGS/NonaDiscordLog.cs | 158 +++++++++++++++++++ BGS/Objective.cs | 103 ++++++++++++ BGS/Report.cs | 121 ++++++++++++++ BGS/SellMicroResources.cs | 28 ++++ BGS/Vouchers.cs | 44 ++++++ Journal/DockedEntry.cs | 13 ++ Journal/EliteDangerous.cs | 15 ++ Journal/Entry.cs | 88 +++++++++++ Journal/Events.cs | 17 ++ Journal/FSDJumpEntry.cs | 23 +++ Journal/JournalException.cs | 11 ++ Journal/JournalFile.cs | 112 +++++++++++++ Journal/MarketSellEntry.cs | 17 ++ Journal/MissionCompletedEntry.cs | 146 +++++++++++++++++ Journal/MultiSellExplorationDataEntry.cs | 17 ++ Journal/PlayerJournal.cs | 56 +++++++ Journal/RedeemVoucherEntry.cs | 19 +++ Journal/SellMicroResourcesEntry.cs | 17 ++ MainWindow.xaml | 122 ++++++++++++++ MainWindow.xaml.cs | 192 +++++++++++++++++++++++ Properties/AssemblyInfo.cs | 55 +++++++ Properties/Resources.Designer.cs | 63 ++++++++ Properties/Resources.resx | 117 ++++++++++++++ Properties/Settings.Designer.cs | 26 +++ Properties/Settings.settings | 7 + Util/AppConfig.cs | 43 +++++ Util/Config.cs | 85 ++++++++++ Util/EDDB.cs | 48 ++++++ nonabgs.csproj | 140 +++++++++++++++++ nonabgs.sln | 25 +++ packages.config | 5 + 39 files changed, 2092 insertions(+) create mode 100644 App.config create mode 100644 App.xaml create mode 100644 App.xaml.cs create mode 100644 BGS/Cartographics.cs create mode 100644 BGS/CombatZone.cs create mode 100644 BGS/DiscordLogGenerator.cs create mode 100644 BGS/LogEntry.cs create mode 100644 BGS/MissionCompleted.cs create mode 100644 BGS/NonaDiscordLog.cs create mode 100644 BGS/Objective.cs create mode 100644 BGS/Report.cs create mode 100644 BGS/SellMicroResources.cs create mode 100644 BGS/Vouchers.cs create mode 100644 Journal/DockedEntry.cs create mode 100644 Journal/EliteDangerous.cs create mode 100644 Journal/Entry.cs create mode 100644 Journal/Events.cs create mode 100644 Journal/FSDJumpEntry.cs create mode 100644 Journal/JournalException.cs create mode 100644 Journal/JournalFile.cs create mode 100644 Journal/MarketSellEntry.cs create mode 100644 Journal/MissionCompletedEntry.cs create mode 100644 Journal/MultiSellExplorationDataEntry.cs create mode 100644 Journal/PlayerJournal.cs create mode 100644 Journal/RedeemVoucherEntry.cs create mode 100644 Journal/SellMicroResourcesEntry.cs create mode 100644 MainWindow.xaml create mode 100644 MainWindow.xaml.cs create mode 100644 Properties/AssemblyInfo.cs create mode 100644 Properties/Resources.Designer.cs create mode 100644 Properties/Resources.resx create mode 100644 Properties/Settings.Designer.cs create mode 100644 Properties/Settings.settings create mode 100644 Util/AppConfig.cs create mode 100644 Util/Config.cs create mode 100644 Util/EDDB.cs create mode 100644 nonabgs.csproj create mode 100644 nonabgs.sln create mode 100644 packages.config diff --git a/App.config b/App.config new file mode 100644 index 0000000..56efbc7 --- /dev/null +++ b/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/App.xaml b/App.xaml new file mode 100644 index 0000000..041c236 --- /dev/null +++ b/App.xaml @@ -0,0 +1,9 @@ + + + + + diff --git a/App.xaml.cs b/App.xaml.cs new file mode 100644 index 0000000..e7ace89 --- /dev/null +++ b/App.xaml.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Configuration; +using System.Data; +using System.Linq; +using System.Threading.Tasks; +using System.Windows; +using NonaBGS.Journal; + +namespace NonaBGS +{ + /// + /// Interaction logic for App.xaml + /// + public partial class NonaBGSApplication : Application + { + } +} diff --git a/BGS/Cartographics.cs b/BGS/Cartographics.cs new file mode 100644 index 0000000..c0e2cdf --- /dev/null +++ b/BGS/Cartographics.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Globalization; +using System.Threading.Tasks; +using NonaBGS.Journal; + +namespace NonaBGS.BGS { + public class Cartographics : LogEntry { + public Cartographics(MultiSellExplorationDataEntry e, string current_system, string current_station) { + this.Entries.Add(e); + this.System = current_system; + this.Station = current_station; + } + + public int TotalSum { + get { + return (from entry in Entries + where entry.Is(Events.MultiSellExplorationData) + select (entry as MultiSellExplorationDataEntry).TotalEarnings) + .Sum() + ; + } + } + + public override string ToString() { + StringBuilder builder = new StringBuilder(); + builder.AppendFormat("Sold {0} CR worth of Cartographics Data", TotalSum); + return builder.ToString(); + } + + /// + /// Cartographics only help the controlling faction. + /// + public override bool OnlyControllingFaction => true; + } +} diff --git a/BGS/CombatZone.cs b/BGS/CombatZone.cs new file mode 100644 index 0000000..4c009fa --- /dev/null +++ b/BGS/CombatZone.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NonaBGS.BGS { + public class CombatZone { + private int level; + } +} diff --git a/BGS/DiscordLogGenerator.cs b/BGS/DiscordLogGenerator.cs new file mode 100644 index 0000000..40974e2 --- /dev/null +++ b/BGS/DiscordLogGenerator.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NonaBGS.BGS { + public interface IDiscordLogGenerator { + string GenerateDiscordLog(Report report); + } +} diff --git a/BGS/LogEntry.cs b/BGS/LogEntry.cs new file mode 100644 index 0000000..8aa3c76 --- /dev/null +++ b/BGS/LogEntry.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using NonaBGS.Journal; + +namespace NonaBGS.BGS { + public class LogEntry { + private List entries = new List(); + + public List Entries => entries; + public string Station { get; set; } + public string System { get; set; } + public string Faction { get; set; } + + /// + /// Whether this entry only benefits the controlling faction or not, default: no + /// + public virtual bool OnlyControllingFaction { + get { return false; } + } + } +} diff --git a/BGS/MissionCompleted.cs b/BGS/MissionCompleted.cs new file mode 100644 index 0000000..3e29a24 --- /dev/null +++ b/BGS/MissionCompleted.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using NonaBGS.Journal; +using Newtonsoft.Json.Linq; + +namespace NonaBGS.BGS { + public class MissionCompleted : LogEntry { + public MissionCompleted(MissionCompletedEntry e, string system, string station) { + this.Entries.Add(e); + this.Faction = e.JSON.GetValue("Faction").ToString(); + this.System = system; + this.Station = station; + } + + public string MissionName { + get { return (Entries[0] as MissionCompletedEntry).HumanReadableName; } + } + + public string Influence { + get { return (Entries[0] as MissionCompletedEntry).GetInfluenceForFaction(Faction); } + } + + public override string ToString() { + if (Entries.Count <= 0) { + return ""; + } + StringBuilder builder = new StringBuilder(); + var entry = Entries[0] as MissionCompletedEntry; + var influence = entry.GetInfluenceForFaction(Faction); + + builder.AppendFormat("{0}", entry.HumanReadableName); + if (influence != "") { + builder.AppendFormat(", Influence: {0}", influence); + } + + return builder.ToString(); + } + } +} diff --git a/BGS/NonaDiscordLog.cs b/BGS/NonaDiscordLog.cs new file mode 100644 index 0000000..1fbf8d4 --- /dev/null +++ b/BGS/NonaDiscordLog.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Globalization; +using System.Threading.Tasks; +using NonaBGS.Journal; + +namespace NonaBGS.BGS { + public class NonaDiscordLog : IDiscordLogGenerator { + private string FormatDate() { + StringBuilder date = new StringBuilder(); + DateTime today = DateTime.Now; + string suffix; + + if (today.Day == 1 || today.Day == 21 || today.Day == 31) { + suffix = "st"; + } else if (today.Day == 2 || today.Day == 22) { + suffix = "nd"; + } else { + suffix = "th"; + } + + date.AppendFormat("{0} {1}{2}, {3}", + today.ToString("MMMM"), today.Day, suffix, + today.Year + EliteDangerous.YearOffset + ); + + return date.ToString(); + } + + private string BuildCartoGraphics(Objective objective) { + var total = from entries in objective.LogEntries + where entries.GetType() == typeof(Cartographics) + select entries + ; + var pages = total.Count(); + var sum = total.Sum(x => (x as Cartographics).TotalSum); + + if (pages <= 0 || sum <= 0) { + return ""; + } + + return string.Format("Sold {0} page(s) worth of universal cartographics\n" + + "(Total value: {1} CR)\n", pages, sum); + } + + private string BuildMicroResourcesSold(Objective objective) { + var total = from entries in objective.LogEntries + where entries.GetType() == typeof(SellMicroResources) + select entries + ; + var sum = total.Sum(x => (x as SellMicroResources).TotalSum); + + if (sum <= 0) { + return ""; + } + + return string.Format("Sold {0} CR worth of Micro Resources\n", sum); + } + + private string BuildVouchers(Objective objective) { + StringBuilder builder = new StringBuilder(); + var missions = from entries in objective.LogEntries + where entries.GetType() == typeof(Vouchers) + select entries + ; + + if (missions == null || missions.Count() <= 0) { + return ""; + } + + foreach (var mission in missions) { + var m = mission as Vouchers; + builder.AppendFormat("Handed in {0} vouchers for {1}\n", m.Type, m.Faction); + builder.AppendFormat("(Total value: {0} CR)\n", m.TotalSum); + builder.AppendFormat("\n"); + } + + return builder.ToString(); + } + + private string BuildMissionList(Objective objective) { + Dictionary> collated = new Dictionary>(); + StringBuilder output = new StringBuilder(); + + var missions = from entries in objective.LogEntries + where entries.GetType() == typeof(MissionCompleted) + select entries + ; + + if (missions == null) { + return ""; + } + + foreach (MissionCompleted m in missions) { + if (!collated.ContainsKey(m.MissionName)) { + collated[m.MissionName] = new Dictionary(); + } + if (!collated[m.MissionName].ContainsKey(m.Influence)) { + collated[m.MissionName][m.Influence] = 0; + } + + ++collated[m.MissionName][m.Influence]; + } + + foreach (var mission in collated) { + if (objective.Faction != null) { + output.AppendFormat("{0} for {1}\n", mission.Key, objective.Faction); + } else { + output.AppendFormat("{0}\n", mission.Key); + } + output.Append("("); + foreach (var influence in mission.Value.OrderBy(x => x.Key.Length)) { + output.AppendFormat("Inf{0} x{1}, ", influence.Key, influence.Value); + } + output.Remove(output.Length - 2, 2); // remove last ", " + output.Append(")\n\n"); + } + + return output.ToString(); + } + + public string GenerateDiscordLog(Report report) { + StringBuilder log = new StringBuilder(); + + log.AppendFormat(":clock2: `Date:` {0}\n", FormatDate()); + foreach (var objective in report.Objectives) { + if (objective.LogEntries.Count <= 0) { + continue; + } + + log.AppendFormat(":globe_with_meridians: `Location:` {0}\n", objective.ToShortString()); + log.Append(":clipboard: `Conducted:`\n"); + log.Append("```\n"); + + StringBuilder entries = new StringBuilder(); + + var missions = BuildMissionList(objective); + entries.Append(missions); + + var vouchers = BuildVouchers(objective); + entries.Append(vouchers); + + var carto = BuildCartoGraphics(objective); + entries.Append(carto); + + var micro = BuildMicroResourcesSold(objective); + entries.Append(micro); + + log.Append(entries.ToString().Trim()); + log.Append("\n```\n"); + } + + return log.ToString(); + } + } +} diff --git a/BGS/Objective.cs b/BGS/Objective.cs new file mode 100644 index 0000000..aee1b17 --- /dev/null +++ b/BGS/Objective.cs @@ -0,0 +1,103 @@ +using System.Collections.Generic; +using System.Text; +using Newtonsoft.Json; + +namespace NonaBGS.BGS { + public class Objective { + private string system; + private string station; + private string faction; + + private List entries = new List(); + + [JsonIgnore] + public List LogEntries { + get => entries; + set => entries = value; + } + + public int Matches(LogEntry e) { + int match_count = 0; + + if (e.OnlyControllingFaction) { + if (Faction == null || (e.Faction != Faction)) { + return 0; + } + } + + if (e.System != null && system != null && + e.System == system) { + ++match_count; + } + + if (e.Station != null && station != null && + e.Station == station) { + ++match_count; + } + + if (e.Faction != null && faction != null && + e.Faction == faction) { + ++match_count; + } + + return match_count; + } + + public int CompareTo(Objective other) { + return (other.System == System && + other.Station == Station && + other.Faction == Faction) ? 0 : -1; + } + + public bool IsValid => System != null && Faction != null; + + public string System { + get { return system; } + set { system = value; } + } + + public string Station { + get { return station; } + set { station = value; } + } + + public string Faction { + get { return faction; } + set { faction = value; } + } + + public override string ToString() { + StringBuilder str = new StringBuilder(); + if (system != null && system.Length > 0) { + str.AppendFormat("System: {0}", system); + } + if (station != null && station.Length > 0) { + if (str.Length > 0) { + str.Append(", "); + } + str.AppendFormat("Station: {0}", station); + } + if (faction != null && faction.Length > 0) { + if (str.Length > 0) { + str.Append(", "); + } + str.AppendFormat("Faction: {0}", faction); + } + return str.ToString(); + } + + public string ToShortString() { + StringBuilder str = new StringBuilder(); + if (system != null && system.Length > 0) { + str.AppendFormat("{0}", system); + } + if (station != null && station.Length > 0) { + if (str.Length > 0) { + str.Append(", "); + } + str.AppendFormat("{0}", station); + } + return str.ToString(); + } + } +} diff --git a/BGS/Report.cs b/BGS/Report.cs new file mode 100644 index 0000000..f6c42fb --- /dev/null +++ b/BGS/Report.cs @@ -0,0 +1,121 @@ +using System; +using System.ComponentModel; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using NonaBGS.Journal; + +namespace NonaBGS.BGS { + public class Report { + private List objectives = new List(); + + public delegate void OnLogDelegate(string log); + + public event OnLogDelegate OnLog; + + public List Objectives { + get { return objectives; } + set { objectives = value; } + } + + public bool AddObjective(Objective objective) { + var found = objectives.Find(x => x == objective); + bool added = false; + + if (found == null) { + objectives.Add(objective); + added = true; + } + + return added; + } + + private bool IsRelevant(Entry e) { + return e.Is(Events.MissionCompleted) || + e.Is(Events.Docked) || + e.Is(Events.FSDJump) || + e.Is(Events.MultiSellExplorationData) || + e.Is(Events.SellMicroResources) || + e.Is(Events.RedeemVoucher) + ; + } + + public void Scan(PlayerJournal journal, DateTime start, DateTime end) { + var entries = from file in journal.Files + where file.NormalisedTimestamp >= start && file.NormalisedTimestamp <= end + select file.Entries + ; + var relevant = from log in entries.SelectMany(array => array) + where IsRelevant(log) + select log + ; + + string current_system = null; + string current_station = null; + string controlling_faction = null; + + this.objectives.ForEach(x => x.LogEntries.Clear()); + + foreach (var e in relevant) { + LogEntry entry = null; + + if (e.Is(Events.Docked)) { + /* gleem the current station from this message + */ + current_station = (e as DockedEntry).StationName; + } else if (e.Is(Events.FSDJump)) { + current_system = (e as FSDJumpEntry).StarSystem; + controlling_faction = (e as FSDJumpEntry).SystemFaction; + } else if (e.Is(Events.MissionCompleted)) { + var completed = e as MissionCompletedEntry; + entry = new MissionCompleted(completed, current_system, current_station); + if (completed.HumanReadableNameWasGenerated) { + OnLog?.Invoke("Human readable name for mission \"" + + completed.Name + + "\" was generated, please report this."); + } + } else if (e.Is(Events.MultiSellExplorationData)) { + entry = new Cartographics(e as MultiSellExplorationDataEntry, current_system, current_station); + entry.Faction = controlling_faction; + } else if (e.Is(Events.RedeemVoucher)) { + entry = new Vouchers(); + entry.Entries.Add(e); + entry.System = current_system; + entry.Station = current_station; + entry.Faction = controlling_faction; + } else if (e.Is(Events.SellMicroResources)) { + entry = new SellMicroResources(current_system, current_station); + entry.Entries.Add(e); + } + + if (entry == null) { + continue; + } + + var matches = objectives + .Where(x => x.Matches(entry) > 0) + .OrderBy(x => x.Matches(entry)) + ; + if (matches == null || matches.Count() <= 0) { + continue; + } + + var objective = matches + .OrderBy(x => x.Matches(entry)) + .Reverse() + .First() + ; + + if (objective != null) { + objective.LogEntries.Add(entry); + } + } + } + + public void Scan(PlayerJournal journal) { + Scan(journal, DateTime.Now, DateTime.Now); + } + } +} diff --git a/BGS/SellMicroResources.cs b/BGS/SellMicroResources.cs new file mode 100644 index 0000000..5daa3e3 --- /dev/null +++ b/BGS/SellMicroResources.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using NonaBGS.Journal; + +namespace NonaBGS.BGS { + public class SellMicroResources : LogEntry { + public SellMicroResources(string system, string station) { + System = system; + Station = Station; + } + + public int TotalSum { + get { + return Entries + .Where(x => x.GetType() == typeof(SellMicroResourcesEntry)) + .Sum(x => (x as SellMicroResourcesEntry).Price) + ; + } + } + + public override string ToString() { + return string.Format("Sell Micro Resources: {0} CR", TotalSum); + } + } +} diff --git a/BGS/Vouchers.cs b/BGS/Vouchers.cs new file mode 100644 index 0000000..b5832d8 --- /dev/null +++ b/BGS/Vouchers.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using NonaBGS.Journal; + +namespace NonaBGS.BGS { + public class Vouchers : LogEntry { + + public int TotalSum { + get { + return Entries + .Where(x => x.GetType() == typeof(RedeemVoucherEntry)) + .Sum(x => (x as RedeemVoucherEntry).Amount) + ; + } + } + + public string Type { + get { + string v = Entries + .Where(x => x.GetType() == typeof(RedeemVoucherEntry)) + .GroupBy(x => (x as RedeemVoucherEntry).Type) + .Select(x => x.Key) + .First(); + return v; + } + } + + public override string ToString() { + StringBuilder builder = new StringBuilder(); + + builder.AppendFormat("{0} Vouchers: {1} CR", TotalSum, Type); + + return builder.ToString(); + } + + /// + /// Vouchers only help the controlling faction + /// + public override bool OnlyControllingFaction => true; + } +} diff --git a/Journal/DockedEntry.cs b/Journal/DockedEntry.cs new file mode 100644 index 0000000..40bbdd8 --- /dev/null +++ b/Journal/DockedEntry.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NonaBGS.Journal { + public class DockedEntry : Entry { + public string StationName { + get { return JSON.GetValue("StationName").ToString(); } + } + } +} diff --git a/Journal/EliteDangerous.cs b/Journal/EliteDangerous.cs new file mode 100644 index 0000000..3e99fcd --- /dev/null +++ b/Journal/EliteDangerous.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NonaBGS.Journal { + public class EliteDangerous { + /// + /// The ingame universe is 1286 years in the future. This is needed to convert dates + /// and times to ingame dates and times + /// + public static readonly int YearOffset = 1286; + } +} diff --git a/Journal/Entry.cs b/Journal/Entry.cs new file mode 100644 index 0000000..86a27ea --- /dev/null +++ b/Journal/Entry.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace NonaBGS.Journal { + /// + /// Base class for a single entry within the player journal. If no specific sub class is available + /// this class gives basic information, such as EventType or date when it happened. It also allows + /// base classes access to the underlying JSON object. Base classes should be named after the event + /// type that they map + Entry. So "FSDJump" event is handled by FSDJumpEntry. + /// + public class Entry { + private static readonly Dictionary classes = new Dictionary { + { Events.Docked, typeof(DockedEntry) }, + { Events.FSDJump, typeof(FSDJumpEntry) }, + { Events.MissionCompleted, typeof(MissionCompletedEntry) }, + { Events.MultiSellExplorationData, typeof(MultiSellExplorationDataEntry) }, + { Events.MarketSell, typeof(MarketSellEntry) }, + { Events.SellMicroResources, typeof(SellMicroResourcesEntry) }, + { Events.RedeemVoucher, typeof(RedeemVoucherEntry) }, + }; + + private string eventtype = null; + private string datetime = null; + private DateTime timestamp; + + private string jsonstr = null; + protected JObject json = null; + + public Entry() { + } + + public static Entry Parse(string journalline) { + var json = JObject.Parse(journalline); + return Parse(json); + } + + public static Entry Parse(JObject json) { + string event_name = json.GetValue("event").ToString(); + + classes.TryGetValue(event_name, out Type classhandler); + if (classhandler == null) { + classhandler = typeof(Entry); + } + + var obj = (Entry)Activator.CreateInstance(classhandler); + obj.InternalInitialise(json); + obj.Initialise(); + return obj; + } + + private void InternalInitialise(JObject jobject) { + this.json = jobject; + this.jsonstr = json.ToString(formatting: Formatting.None); + + this.eventtype = json.GetValue("event").ToString(); + this.datetime = json.GetValue("timestamp").ToString(); + this.timestamp = DateTime.Parse(this.datetime); + } + + protected virtual void Initialise() { + } + + public bool Is(string eventtype) { + if (eventtype == null || this.eventtype == null) { + return false; + } + + return String.Equals(this.eventtype, eventtype, StringComparison.OrdinalIgnoreCase); + } + + public string Event { + get { return eventtype; } + } + + public DateTime Timestamp { + get { return timestamp; } + } + + public JObject JSON { + get { return this.json; } + } + } +} diff --git a/Journal/Events.cs b/Journal/Events.cs new file mode 100644 index 0000000..3c93a33 --- /dev/null +++ b/Journal/Events.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NonaBGS.Journal { + public class Events { + public static readonly string MissionCompleted = "MissionCompleted"; + public static readonly string Docked = "Docked"; + public static readonly string FSDJump = "FSDJump"; + public static readonly string MultiSellExplorationData = "MultiSellExplorationData"; + public static readonly string MarketSell = "MarketSell"; + public static readonly string SellMicroResources = "SellMicroResources"; + public static readonly string RedeemVoucher = "RedeemVoucher"; + } +} diff --git a/Journal/FSDJumpEntry.cs b/Journal/FSDJumpEntry.cs new file mode 100644 index 0000000..42a8322 --- /dev/null +++ b/Journal/FSDJumpEntry.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; + +namespace NonaBGS.Journal { + public class FSDJumpEntry : Entry { + private string starsystem = null; + private string systemfaction = null; + protected override void Initialise() { + starsystem = JSON.Value("StarSystem"); + var faction = JSON.Value("SystemFaction"); + if (faction != null) { + systemfaction = faction.Value("Name"); + } + } + public string StarSystem => starsystem; + + public string SystemFaction => systemfaction; + } +} diff --git a/Journal/JournalException.cs b/Journal/JournalException.cs new file mode 100644 index 0000000..179298b --- /dev/null +++ b/Journal/JournalException.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace NonaBGS.Journal { + public class JournalException : Exception { + public JournalException(string message) : base(message) { + } + } +} diff --git a/Journal/JournalFile.cs b/Journal/JournalFile.cs new file mode 100644 index 0000000..2b35776 --- /dev/null +++ b/Journal/JournalFile.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.RegularExpressions; +using System.IO; +using System.Globalization; + +namespace NonaBGS.Journal +{ + public class JournalFile : IComparable + { + private string fullpath = null; + private string part = null; + private int intpart = 0; + private DateTime datetime; + private DateTime normalised; + private List entries = new List(); + + private static Regex fileregex = new Regex("Journal\\.(\\d+)\\.(\\d+)\\.log"); + private static string iso8601 = "yyyyMMddTHHmmss"; + private static string iso8601_notime = "yyyyMMdd"; + + private void SetFilename(string path) { + string filename = Path.GetFileName(path); + if (!File.Exists(path) || filename == null) { + throw new JournalException(string.Format("Invalid journal file: {0}", filename)); + } + + var matches = fileregex.Matches(filename); + if (matches.Count < 1) { + throw new JournalException(string.Format("Invalid journal file: {0}", filename)); + } + + this.fullpath = path; + this.part = matches[0].Groups[2].ToString(); + this.intpart = int.Parse(part); + // The ISO is not quite correct on journal filenames, so build + // a proper ISO 8601 date time stamp out of it. + var date = new StringBuilder(); + date.Append("20"); // Add the missing century in front. + date.Append(matches[0].Groups[1].ToString()); + date.Insert(8, "T"); // Add the "T" separating date and time + this.datetime = DateTime.ParseExact(date.ToString(), iso8601, CultureInfo.InvariantCulture); + this.normalised = DateTime.Parse(this.datetime.ToShortDateString()); + } + + public int CompareTo(JournalFile other) { + if (datetime == null || other.datetime == null) { + return 0; + } + var comp = DateTime.Compare(datetime, other.datetime); + if (comp == 0) { + /* If we have the exact same datetime we compare by parts. + */ + return intpart - other.intpart; + } else { + return comp; + } + } + + public JournalFile(string path) { + SetFilename(path); + } + + public DateTime Timestamp { + get { return datetime; } + } + + public DateTime NormalisedTimestamp { + get { return normalised; } + } + + public int Part { + get { return intpart; } + } + + public IEnumerable Entries { + get { + if (entries == null || entries.Count == 0) { + try { + LoadEntries(); + } catch (IOException) { + entries = new List(); + } + } + return entries; + } + } + + public void LoadEntries() { + List lines = new List(); + + /* This needs to be done this way, otherwise, if the game is still running the journal files cannot + * be accessed. And it is very much convenient to generate logs while the game is still running. + */ + using (var fs = new FileStream(this.fullpath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) { + using (var sr = new StreamReader(fs, Encoding.UTF8)) { + string line = null; + while ((line = sr.ReadLine()) != null) { + lines.Add(line); + } + } + } + + entries.Clear(); + foreach(var line in lines) { + Entry entry = Entry.Parse(line); + entries.Add(entry); + } + } + } +} diff --git a/Journal/MarketSellEntry.cs b/Journal/MarketSellEntry.cs new file mode 100644 index 0000000..0551aae --- /dev/null +++ b/Journal/MarketSellEntry.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NonaBGS.Journal { + public class MarketSellEntry : Entry { + private int totalsale = 0; + + protected override void Initialise() { + totalsale = JSON.Value("TotalSale"); + } + + public int TotalSale => totalsale; + } +} diff --git a/Journal/MissionCompletedEntry.cs b/Journal/MissionCompletedEntry.cs new file mode 100644 index 0000000..4ad147d --- /dev/null +++ b/Journal/MissionCompletedEntry.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace NonaBGS.Journal { + public class MissionCompletedEntry : Entry { + private Dictionary influences = new Dictionary(); + private string readable_name = null; + private bool readable_name_generated = false; + private string name = null; + private string commodity = null; + private int count = 0; + private int donated = 0; + + private readonly Dictionary humanreadable = new Dictionary { + { "Mission_AltruismCredits_name", "Donate Credits" }, + { "Mission_Collect_name", "Collect" }, + { "Mission_Courier_Elections_name", "Courier (Elections)" }, + { "Mission_Courier_name", "Courier" }, + { "Mission_Courier_RankEmp_name", "Courier (Empire)" }, + { "Mission_Delivery_Boom_name", "Delivery (Boom)" }, + { "Mission_Delivery_Retreat_name", "Delivery (Retreat)" }, + { "Mission_Delivery_name", "Delivery" }, + { "Mission_Delivery_Agriculture_name", "Delivery (Agriculture)" }, + { "Mission_HackMegaship_name", "Hack Megaship" }, + { "Mission_MassacreWing_name", "Massacre (Wing)" }, + { "Mission_OnFoot_Onslaught_MB_name", "On Foot Onslaught" }, + { "Mission_OnFoot_RebootRestore_MB_name", "On Foot Reboot/Restore" }, + { "Mission_OnFoot_Reboot_MB_name", "On Foot Reboot" }, + { "Mission_Rescue_Planet_name", "Planet Rescue" }, + { "Mission_Salvage_name", "Salvage" }, + { "MISSION_Salvage_Illegal_name", "Salvage (Illegal)" }, + { "MISSION_Salvage_Retreat_name", "Salvage (Retreat)" }, + { "MISSION_Salvage_Expansion_name", "Salvage (Expansion)" }, + { "Mission_Salvage_RankEmp_name", "Salvage (Imperial Navy)" }, + { "MISSION_Scan_name", "Scan" }, + }; + + protected override void Initialise() { + MakeHumanReadableName(); + name = JSON.Value("Name"); + if (JSON.ContainsKey("Commodity_Localised")) { + commodity = JSON.Value("Commodity_Localised"); + } + if (JSON.ContainsKey("Count")) { + count = JSON.Value("Count"); + } + if (JSON.ContainsKey("Donated")) { + donated = JSON.Value("Donated"); + } + } + + public string Name => name; + public string Commodity => commodity; + public int Count => count; + + private void MakeHumanReadableName() { + if (readable_name != null && readable_name.Length > 0) { + return; + } + + if (Name == null) { + return; + } + + StringBuilder builder; + + if (humanreadable.ContainsKey(Name)) { + builder = new StringBuilder(humanreadable[Name]); + readable_name_generated = false; + } else { + builder = new StringBuilder(Name); + builder.Replace("Mission_", ""); + builder.Replace("_name", ""); + builder.Replace("_MB", ""); + builder.Replace('_', ' '); + builder.Replace("Illegal", " (Illegal)"); + builder.Replace("OnFoot", "On Foot"); + builder.Replace(" BS", ""); + builder.Replace("HackMegaship", "Hack Megaship"); + builder.Replace("Boom", ""); + builder.Replace("RebootRestore", "Reboot/Restore"); + builder.Replace("CivilLiberty", ""); + readable_name_generated = true; + } + + if (count > 0 && commodity != null) { + builder.AppendFormat(" ({0} {1})", count, commodity); + } + + if (donated > 0) { + builder.AppendFormat(" ({0} CR)", donated); + } + + readable_name = builder.ToString().Trim(); + } + + public string HumanReadableName { + get { + MakeHumanReadableName(); + return readable_name; + } + } + + public bool HumanReadableNameWasGenerated { + get { + MakeHumanReadableName(); + return readable_name_generated; + } + } + + public string GetInfluenceForFaction(string faction) { + if (influences.ContainsKey(faction)) { + return influences[faction]; + } + + var effects = JSON.Value("FactionEffects"); + foreach (var effect in effects.Children()) { + if (effect.GetValue("Faction").ToString() != faction) { + continue; + } + + var influence = effect.Value("Influence"); + if (influence == null || influence.Count == 0) { + // No influence reward, happens on courier missions sometimes. + // There is always one point of rep, even if the mission won't state it + influences.Add(faction, "+"); + } + + foreach (var infl in influence.Children()) { + infl.TryGetValue("Influence", out JToken result); + if (result != null && result.Type == JTokenType.String) { + influences.Add(faction, result.ToString()); + return result.ToString(); + } + } + } + + return ""; + } + } +} diff --git a/Journal/MultiSellExplorationDataEntry.cs b/Journal/MultiSellExplorationDataEntry.cs new file mode 100644 index 0000000..139a546 --- /dev/null +++ b/Journal/MultiSellExplorationDataEntry.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NonaBGS.Journal { + public class MultiSellExplorationDataEntry : Entry { + private int totalearnings = 0; + + protected override void Initialise() { + totalearnings = JSON.Value("TotalEarnings"); + } + + public int TotalEarnings => totalearnings; + } +} diff --git a/Journal/PlayerJournal.cs b/Journal/PlayerJournal.cs new file mode 100644 index 0000000..24f09ff --- /dev/null +++ b/Journal/PlayerJournal.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading.Tasks; + +namespace NonaBGS.Journal { + public class PlayerJournal { + public static string DefaultPath = "%UserProfile%\\Saved Games\\Frontier Developments\\Elite Dangerous"; + + private List journalfiles = new List(); + private string basepath = null; + + public PlayerJournal() { + Initialise(DefaultPath); + } + + public PlayerJournal(string path) { + Initialise(path); + } + + private void Initialise(string path) { + basepath = Environment.ExpandEnvironmentVariables(path); + } + + public List Files { + get { return journalfiles; } + } + + public void Open() { + if (!Directory.Exists(basepath)) { + throw new JournalException("Invalid base path, path could not be found"); + } + this.ScanFiles(); + } + + public void ScanFiles() { + var files = Directory.EnumerateFiles(basepath); + + journalfiles.Clear(); + + foreach (var file in files) { + string full = Path.Combine(basepath, file); + try { + JournalFile journalfile = new JournalFile(full); + journalfiles.Add(journalfile); + } catch (JournalException) { + /* invalid journal file, or one of the other json files in there */ + continue; + } + } + + journalfiles.Sort(); + } + } +} diff --git a/Journal/RedeemVoucherEntry.cs b/Journal/RedeemVoucherEntry.cs new file mode 100644 index 0000000..f172359 --- /dev/null +++ b/Journal/RedeemVoucherEntry.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NonaBGS.Journal { + public class RedeemVoucherEntry : Entry { + private int amount = 0; + private string type = null; + protected override void Initialise() { + amount = JSON.Value("Amount"); + type = JSON.Value("Type"); + } + + public int Amount => amount; + public string Type => type; + } +} diff --git a/Journal/SellMicroResourcesEntry.cs b/Journal/SellMicroResourcesEntry.cs new file mode 100644 index 0000000..dc23bda --- /dev/null +++ b/Journal/SellMicroResourcesEntry.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NonaBGS.Journal { + public class SellMicroResourcesEntry : Entry { + private int price; + + protected override void Initialise() { + price = JSON.Value("Price"); + } + + public int Price => price; + } +} diff --git a/MainWindow.xaml b/MainWindow.xaml new file mode 100644 index 0000000..32cea3c --- /dev/null +++ b/MainWindow.xaml @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + + +