20 Commits

Author SHA1 Message Date
1da6f41ec8 update changelog 2024-11-14 11:43:24 +01:00
2c6eb9190a count conflict zones you SelfDestruct out of 2024-11-14 11:32:04 +01:00
8a92cac02a add support for SelfDestruct 2024-11-14 11:31:46 +01:00
4fe77e6946 add support for power combat zones in UI 2024-11-14 11:18:01 +01:00
912e8b602f add support for power combat zones in parsing 2024-11-14 11:17:43 +01:00
be3bceb880 make sure missions read as 0 INF if no INF was generated 2024-11-14 10:51:18 +01:00
6cbe54ff73 update changelog 2024-09-18 21:40:07 +02:00
6fd5bbc582 all of them 2024-09-18 21:38:21 +02:00
262182cfaf allow selection of commander names to post as 2024-09-18 21:36:39 +02:00
007b391dc2 parse out commander names from logs 2024-09-18 21:33:20 +02:00
9918c7d559 extend Commander entry with full name CMDR + Name 2024-09-18 21:33:05 +02:00
2bf8d9018d further refine splitting of logs 2024-09-18 21:10:41 +02:00
8eaf94f634 optimise splitting code for short logs 2024-09-17 19:54:35 +02:00
20adf93d39 implement log splitting for discord posting 2024-09-17 19:53:17 +02:00
fd3e5f61cb allow override of username in posting 2024-09-17 18:57:38 +02:00
e617c3852b update changelog 2024-09-17 18:50:31 +02:00
9f013bed38 fix cartographics value, use Total instead of TotalEarnings 2024-09-17 18:49:18 +02:00
916afb2348 update changelog 2024-08-17 18:40:32 +02:00
6eb892151c bump version 2024-08-17 18:40:22 +02:00
6a9e4978aa add possibility to post logs to discord webhooks 2024-08-17 18:38:00 +02:00
29 changed files with 594 additions and 95 deletions

View File

@@ -35,10 +35,10 @@ public class Cartographics : Transaction {
/* add multi sell and normal ones together */ /* add multi sell and normal ones together */
long total = long total =
Entries.OfType<MultiSellExplorationDataEntry>() Entries.OfType<MultiSellExplorationDataEntry>()
.Sum(x => x.TotalEarnings) .Sum(x => x.Total)
+ +
Entries.OfType<SellExplorationDataEntry>() Entries.OfType<SellExplorationDataEntry>()
.Sum(x => x.TotalEarnings) .Sum(x => x.Total)
; ;
return total; return total;
} }

View File

@@ -92,6 +92,13 @@ public class CombatZone : Transaction {
get { return string.Compare(Type, CombatZones.AXCombatZone) == 0; } get { return string.Compare(Type, CombatZones.AXCombatZone) == 0; }
} }
/// <summary>
/// Returns true if it is a power combat zone
/// </summary>
public bool IsPower {
get { return string.Compare(Type, CombatZones.PowerCombatZone) == 0; }
}
public override int CompareTo(Transaction? obj) { public override int CompareTo(Transaction? obj) {
if (obj == null || obj.GetType() != typeof(CombatZone)) { if (obj == null || obj.GetType() != typeof(CombatZone)) {
return -1; return -1;

View File

@@ -0,0 +1,17 @@
using EDPlayerJournal.BGS;
using EDPlayerJournal.Entries;
internal class CommanderParser : ITransactionParserPart {
public void Parse(Entry entry, TransactionParserContext context, TransactionParserOptions options, TransactionList transactions) {
CommanderEntry commanderEntry = (CommanderEntry)entry;
if (commanderEntry != null && !string.IsNullOrEmpty(commanderEntry.FullName)) {
if (!context.Commanders.Contains(commanderEntry.FullName)) {
context.Commanders.Add(commanderEntry.FullName);
}
}
// A commander entry happens when you log out, and log back in again
// for example when switching from Open, to Solo or PG.
context.DiscernCombatZone(transactions, entry);
context.ResetCombatZone();
}
}

View File

@@ -581,14 +581,25 @@ internal class ReceiveTextParser : ITransactionParserPart {
} }
} }
internal class SelfDestructParser : ITransactionParserPart {
public void Parse(Entry entry, TransactionParserContext context, TransactionParserOptions options, TransactionList transactions) {
context.SelfDestruct = true;
}
}
internal class DiedParser : ITransactionParserPart { internal class DiedParser : ITransactionParserPart {
public void Parse(Entry entry, TransactionParserContext context, TransactionParserOptions options, TransactionList transactions) { public void Parse(Entry entry, TransactionParserContext context, TransactionParserOptions options, TransactionList transactions) {
// Death only matters in ship. On foot you can just redeploy with the dropship. // Death only matters in ship. On foot you can just redeploy with the dropship.
if (context.IsOnFoot) { if (context.IsOnFoot) {
return; return;
} }
// You can't complete a combat zone if you die in it. Others might keep it open if (context.SelfDestruct != null && context.SelfDestruct == true) {
// for you, but still you will not have completed it unless you jump back in. // Some people just suicide to fast track back to stations after a CZ,
// especially since combat bonds don't disappear on death. So count the CZ
// on self destruct
context.DiscernCombatZone(transactions, entry);
context.SelfDestruct = null;
}
context.ResetCombatZone(); context.ResetCombatZone();
// Dying also moves you back to another instance // Dying also moves you back to another instance
context.LeftInstance(); context.LeftInstance();
@@ -602,15 +613,6 @@ internal class DropshipDeployParser : ITransactionParserPart {
} }
} }
internal class CommanderParser : ITransactionParserPart {
public void Parse(Entry entry, TransactionParserContext context, TransactionParserOptions options, TransactionList transactions) {
// A commander entry happens when you log out, and log back in again
// for example when switching from Open, to Solo or PG.
context.DiscernCombatZone(transactions, entry);
context.ResetCombatZone();
}
}
public class TransactionParser { public class TransactionParser {
private static Dictionary<string, ITransactionParserPart> ParserParts { get; } = new() private static Dictionary<string, ITransactionParserPart> ParserParts { get; } = new()
{ {
@@ -639,6 +641,7 @@ public class TransactionParser {
{ Events.ReceiveText, new ReceiveTextParser() }, { Events.ReceiveText, new ReceiveTextParser() },
{ Events.RedeemVoucher, new RedeemVoucherParser() }, { Events.RedeemVoucher, new RedeemVoucherParser() },
{ Events.SearchAndRescue, new SearchAndRescueParser() }, { Events.SearchAndRescue, new SearchAndRescueParser() },
{ Events.SelfDestruct, new SelfDestructParser() },
{ Events.SellExplorationData, new SellExplorationDataParser() }, { Events.SellExplorationData, new SellExplorationDataParser() },
{ Events.SellMicroResources, new SellMicroResourcesParser() }, { Events.SellMicroResources, new SellMicroResourcesParser() },
{ Events.SellOrganicData, new SellOrganicDataParser() }, { Events.SellOrganicData, new SellOrganicDataParser() },
@@ -669,6 +672,11 @@ public class TransactionParser {
return Parse(entries, defaultOptions); return Parse(entries, defaultOptions);
} }
/// <summary>
/// List of commanders seen during parsing.
/// </summary>
public List<string> Commanders { get; set; } = new();
public List<Transaction>? Parse(IEnumerable<Entry> entries, TransactionParserOptions options) { public List<Transaction>? Parse(IEnumerable<Entry> entries, TransactionParserOptions options) {
TransactionList transactions = new(); TransactionList transactions = new();
TransactionParserContext context = new(); TransactionParserContext context = new();
@@ -686,6 +694,9 @@ public class TransactionParser {
transactionParserPart.Parse(entry, context, options, transactions); transactionParserPart.Parse(entry, context, options, transactions);
} }
// Copy out list of commanders seen
Commanders = context.Commanders;
return transactions.ToList(); return transactions.ToList();
} }
} }

View File

@@ -3,6 +3,11 @@
namespace EDPlayerJournal.BGS; namespace EDPlayerJournal.BGS;
internal class TransactionParserContext { internal class TransactionParserContext {
/// <summary>
/// List of commander names seen in the logs. May be empty.
/// </summary>
public List<string> Commanders { get; } = new();
/// <summary> /// <summary>
/// Name of the current system the player is in. /// Name of the current system the player is in.
/// </summary> /// </summary>
@@ -57,6 +62,8 @@ internal class TransactionParserContext {
public bool HaveSeenAlliedCorrespondent { get; set; } = false; public bool HaveSeenAlliedCorrespondent { get; set; } = false;
public bool HaveSeenEnemyCorrespondent { get; set; } = false; public bool HaveSeenEnemyCorrespondent { get; set; } = false;
public bool? SelfDestruct { get; set; } = null;
/// <summary> /// <summary>
/// Current Odyssey settlement. /// Current Odyssey settlement.
/// </summary> /// </summary>
@@ -123,16 +130,29 @@ internal class TransactionParserContext {
Settlement = null; Settlement = null;
} }
private bool HadCombatZone() {
if (CurrentInstanceType != null &&
Instances.IsInstance(CurrentInstanceType, Instances.PowerWarzoneMedium)) {
return true;
}
if (HighestCombatBond == null &&
LastRecordedAwardingFaction == null &&
HaveSeenAXWarzoneNPC == false &&
CurrentInstanceType == null) {
return false;
}
return true;
}
public void DiscernCombatZone(TransactionList transactions, Entry e) { public void DiscernCombatZone(TransactionList transactions, Entry e) {
string? grade = CombatZones.DifficultyLow; string? grade = CombatZones.DifficultyLow;
string cztype; string cztype;
ulong highest = HighestCombatBond ?? 0; ulong highest = HighestCombatBond ?? 0;
string? faction = LastRecordedAwardingFaction; string? faction = LastRecordedAwardingFaction;
if (HighestCombatBond == null && if (!HadCombatZone()) {
LastRecordedAwardingFaction == null &&
HaveSeenAXWarzoneNPC == false &&
CurrentInstanceType == null) {
return; return;
} }
@@ -152,7 +172,8 @@ internal class TransactionParserContext {
return; return;
} }
if (LastRecordedAwardingFaction == null && if (LastRecordedAwardingFaction == null &&
Instances.IsHumanWarzone(CurrentInstanceType)) { (Instances.IsHumanWarzone(CurrentInstanceType) ||
Instances.IsPowerWarzone(CurrentInstanceType))) {
transactions.AddIncomplete(new CombatZone(), transactions.AddIncomplete(new CombatZone(),
"Could not discern for whom you fought for, " + "Could not discern for whom you fought for, " +
"as it seems you haven't killed anyone in the ship combat zone.", "as it seems you haven't killed anyone in the ship combat zone.",
@@ -182,6 +203,9 @@ internal class TransactionParserContext {
} else if (Instances.IsInstance(CurrentInstanceType, Instances.WarzoneThargoidVeryHigh)) { } else if (Instances.IsInstance(CurrentInstanceType, Instances.WarzoneThargoidVeryHigh)) {
cztype = CombatZones.AXCombatZone; cztype = CombatZones.AXCombatZone;
grade = CombatZones.DifficultyVeryHigh; grade = CombatZones.DifficultyVeryHigh;
} else if (Instances.IsInstance(CurrentInstanceType, Instances.PowerWarzoneMedium)) {
cztype = CombatZones.PowerCombatZone;
grade = CombatZones.DifficultyMedium;
} else { } else {
transactions.AddIncomplete(new CombatZone(), transactions.AddIncomplete(new CombatZone(),
"Unknown conflict zone difficulty", "Unknown conflict zone difficulty",

View File

@@ -14,6 +14,11 @@ public class CombatZones {
/// </summary> /// </summary>
public static readonly string ShipCombatZone = "Ship"; public static readonly string ShipCombatZone = "Ship";
/// <summary>
/// Power combat zones, new in Ascendancy update.
/// </summary>
public static readonly string PowerCombatZone = "Power";
/// <summary> /// <summary>
/// AX combat zone /// AX combat zone
/// </summary> /// </summary>

View File

@@ -8,4 +8,13 @@ public class CommanderEntry : Entry {
Name = JSON.Value<string>("Name") ?? ""; Name = JSON.Value<string>("Name") ?? "";
FID = JSON.Value<string>("FID") ?? ""; FID = JSON.Value<string>("FID") ?? "";
} }
public string FullName {
get {
if (string.IsNullOrEmpty(Name)) {
return string.Empty;
}
return "CMDR " + Name;
}
}
} }

View File

@@ -42,6 +42,7 @@ public class Entry {
{ Events.ReceiveText, typeof(ReceiveTextEntry) }, { Events.ReceiveText, typeof(ReceiveTextEntry) },
{ Events.RedeemVoucher, typeof(RedeemVoucherEntry) }, { Events.RedeemVoucher, typeof(RedeemVoucherEntry) },
{ Events.SearchAndRescue, typeof(SearchAndRescueEntry) }, { Events.SearchAndRescue, typeof(SearchAndRescueEntry) },
{ Events.SelfDestruct, typeof(SelfDestructEntry) },
{ Events.SellExplorationData, typeof(SellExplorationDataEntry) }, { Events.SellExplorationData, typeof(SellExplorationDataEntry) },
{ Events.SellMicroResources, typeof(SellMicroResourcesEntry) }, { Events.SellMicroResources, typeof(SellMicroResourcesEntry) },
{ Events.SellOrganicData, typeof(SellOrganicDataEntry) }, { Events.SellOrganicData, typeof(SellOrganicDataEntry) },

View File

@@ -32,6 +32,7 @@ public class Events {
public static readonly string ReceiveText = "ReceiveText"; public static readonly string ReceiveText = "ReceiveText";
public static readonly string RedeemVoucher = "RedeemVoucher"; public static readonly string RedeemVoucher = "RedeemVoucher";
public static readonly string SearchAndRescue = "SearchAndRescue"; public static readonly string SearchAndRescue = "SearchAndRescue";
public static readonly string SelfDestruct = "SelfDestruct";
public static readonly string SellExplorationData = "SellExplorationData"; public static readonly string SellExplorationData = "SellExplorationData";
public static readonly string SellMicroResources = "SellMicroResources"; public static readonly string SellMicroResources = "SellMicroResources";
public static readonly string SellOrganicData = "SellOrganicData"; public static readonly string SellOrganicData = "SellOrganicData";

View File

@@ -1,8 +1,23 @@
namespace EDPlayerJournal.Entries; namespace EDPlayerJournal.Entries;
public class MultiSellExplorationDataEntry : Entry { public class MultiSellExplorationDataEntry : Entry {
protected override void Initialise() { protected override void Initialise() {
TotalEarnings = (JSON.Value<int?>("TotalEarnings") ?? 0); TotalEarnings = JSON.Value<long?>("TotalEarnings") ?? 0;
BaseValue = JSON.Value<long?>("BaseValue") ?? 0;
Bonus = JSON.Value<long?>("Bonus") ?? 0;
} }
public int TotalEarnings { get; set; } = 0; public long Total {
get { return BaseValue + Bonus; }
}
public long BaseValue { get; set; } = 0;
public long Bonus { get; set; } = 0;
/// <summary>
/// Total Earnings are the actual earnings without bonus. So this
/// value is BaseValue minus any percent a crew pilot takes.
/// </summary>
public long TotalEarnings { get; set; } = 0;
} }

View File

@@ -0,0 +1,5 @@
namespace EDPlayerJournal.Entries {
public class SelfDestructEntry : Entry {
// Has no data
}
}

View File

@@ -6,8 +6,17 @@ namespace EDPlayerJournal.Entries;
public class SellExplorationDataEntry : Entry { public class SellExplorationDataEntry : Entry {
public long BaseValue { get; set; } public long BaseValue { get; set; }
public long Bonus { get; set; } public long Bonus { get; set; }
/// <summary>
/// Total Earnings are the actual earnings without bonus. So this
/// value is BaseValue minus any percent a crew pilot takes.
/// </summary>
public long TotalEarnings { get; set; } public long TotalEarnings { get; set; }
public long Total {
get { return BaseValue + Bonus; }
}
public List<string> Systems { get; set; } = new List<string>(); public List<string> Systems { get; set; } = new List<string>();
public List<string> Discovered { get; set; } = new List<string>(); public List<string> Discovered { get; set; } = new List<string>();

View File

@@ -18,6 +18,11 @@ public class Instances {
/// </summary> /// </summary>
public static readonly string WarzoneHigh = "$Warzone_PointRace_High"; public static readonly string WarzoneHigh = "$Warzone_PointRace_High";
/// <summary>
/// Medium power play conflict zone, new in PP 2.0 Ascendancy update
/// </summary>
public static readonly string PowerWarzoneMedium = "$Warzone_Powerplay_Med";
/// <summary> /// <summary>
/// Low Thargoid combat zone /// Low Thargoid combat zone
/// </summary> /// </summary>
@@ -52,8 +57,17 @@ public class Instances {
; ;
} }
public static bool IsPowerWarzone(string type) {
return
IsInstance(type, PowerWarzoneMedium)
;
}
public static bool IsWarzone(string type) { public static bool IsWarzone(string type) {
return IsHumanWarzone(type) || IsThargoidWarzone(type); return IsHumanWarzone(type) ||
IsThargoidWarzone(type) ||
IsPowerWarzone(type)
;
} }
public static bool IsInstance(string type, string instance) { public static bool IsInstance(string type, string instance) {

View File

@@ -1,5 +1,20 @@
# EliteBGS changelog # EliteBGS changelog
## 0.4.4 on ??.??.202?
* Add support for Power Conflict Zones
* Show no inf as "Zero INF", so the Nova Navy bot can parse it properly
## 0.4.3 on 18.09.2024
* Add possibility to post log reports to Discord webhooks.
Logs are automatically split to fit discord length, and you can choose the
name of the Commander to post it as. There are some restrictions, so for
ultra long logs the one line log format might be necessary.
* Fix cartographics data value by igonring `TotalEarnings`. Total earnings
is the same as `BaseValue`, except any percentages taken by crew members
is deducted. Actual total value is `BaseValue` plus `Bonus`.
## 0.4.2 on 02.05.2024 ## 0.4.2 on 02.05.2024
* Add a bot header for all generated logs that shows the tool version, as * Add a bot header for all generated logs that shows the tool version, as

View File

@@ -198,4 +198,65 @@ public class DiscordLogGenerator {
return log.ToString().Trim(); return log.ToString().Trim();
} }
public virtual string[] SplitLog(string log, int maxcount = 2000) {
throw new NotImplementedException();
}
protected string[] SplitLogWithHeader(string log, string header, int maxcount = 2000) {
string[] lines = log.Split("\n");
List<string> chunks = new();
string chunk = string.Empty;
// Optimisation
if (log.Length <= maxcount) {
return new string[] { log };
}
// First split the log into its headers
// skip first bot header line
for (int i = 1; i < lines.Length; i++) {
string line = lines[i];
if (line.StartsWith(header) && !string.IsNullOrEmpty(chunk)) {
chunks.Add(chunk.Trim());
chunk = string.Empty;
}
chunk = chunk + "\n" + line;
}
int curchunk = 0;
string botheader = BotHeader().Trim() + "\n";
// Leave room for botheader and some leeway
int maxlength = (2000 - botheader.Length - 10);
// Then try to collate chunks
for (curchunk = 0; curchunk < chunks.Count; ++curchunk) {
int count = chunks[curchunk].Length;
while (count < maxlength && (curchunk+1) < chunks.Count) {
count += chunks[curchunk + 1].Length + 2;
if (count < maxlength) {
chunks[curchunk] += "\n";
chunks[curchunk] += chunks[curchunk + 1];
chunks.RemoveAt(curchunk + 1);
}
}
}
// Readd bott headers
for (int i = 0; i < chunks.Count; i++) {
chunks[i] = chunks[i].Insert(0, botheader);
}
// Now finally check if any one chunk is bigger than max length
for (int i = 0; i < chunks.Count; i++) {
if (chunks[i].Length > maxcount) {
throw new ApplicationException(
string.Format("a chunk turned out bigger than {0}", maxcount));
}
}
return chunks.ToArray();
}
} }

32
EliteBGS/DiscordPoster.cs Normal file
View File

@@ -0,0 +1,32 @@
using EliteBGS.Util;
using System;
using System.Text;
using System.Net.Http;
using System.Text.Json.Nodes;
namespace EliteBGS;
public class DiscordPoster {
public static readonly int DiscordLimit = 2000;
public static void PostToDiscord(DiscordWebhook webhook, string log, string username = "EDBGS") {
JsonObject obj = new();
obj.Add("content", log);
obj.Add("username", username);
using (var client = new HttpClient()) {
var content = new StringContent(obj.ToString(), Encoding.UTF8, "application/json");
var response = client.PostAsync(webhook.Webhook, content);
if (response == null) {
throw new Exception("failed to post content to Discord webhook");
}
response.Wait();
var resp = response.Result;
if (!resp.IsSuccessStatusCode) {
throw new Exception(string.Format(
"failed to post content to webhook {0}: {1} / {2}",
webhook.Name, resp.StatusCode, resp.Content.ToString()));
}
}
}
}

View File

@@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net7.0-windows</TargetFramework> <TargetFramework>net7.0-windows</TargetFramework>
<OutputType>WinExe</OutputType> <OutputType>WinExe</OutputType>
<Version>0.4.2</Version> <Version>0.4.3</Version>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<UseWindowsForms>true</UseWindowsForms> <UseWindowsForms>true</UseWindowsForms>
<UseWPF>true</UseWPF> <UseWPF>true</UseWPF>

View File

@@ -1,4 +1,7 @@
namespace EliteBGS; using System.Collections.Generic;
using System.Windows.Documents;
namespace EliteBGS;
public class GenericDiscordLog : DiscordLogGenerator { public class GenericDiscordLog : DiscordLogGenerator {
public override string ToString() { public override string ToString() {
@@ -8,4 +11,8 @@ public class GenericDiscordLog : DiscordLogGenerator {
public override string Name { public override string Name {
get { return "Generic"; } get { return "Generic"; }
} }
public override string[] SplitLog(string log, int maxcount = 2000) {
return SplitLogWithHeader(log, "**Date:**", maxcount);
}
} }

View File

@@ -60,7 +60,7 @@ public partial class LoadEntriesWindow : Window {
dialog.DefaultExt = ".log"; dialog.DefaultExt = ".log";
dialog.Filter = "Log files (*.log)|*.log|All files (*.*)|*"; dialog.Filter = "Log files (*.log)|*.log|All files (*.*)|*";
var location = config.Global.DefaultJournalLocation; var location = AppConfig.DefaultJournalLocation;
if (Directory.Exists(location)) { if (Directory.Exists(location)) {
dialog.InitialDirectory = location; dialog.InitialDirectory = location;
} }

View File

@@ -64,6 +64,7 @@ public class MissionFormat : LogFormatter {
Dictionary<string, ulong> passengers = new(); Dictionary<string, ulong> passengers = new();
StringBuilder output = new StringBuilder(); StringBuilder output = new StringBuilder();
long total_influence = 0; long total_influence = 0;
bool hadmissions = false;
var missions = objective.EnabledOfType<MissionCompleted>(); var missions = objective.EnabledOfType<MissionCompleted>();
var support = objective.EnabledOfType<InfluenceSupport>(); var support = objective.EnabledOfType<InfluenceSupport>();
@@ -85,6 +86,7 @@ public class MissionFormat : LogFormatter {
++collated[m.MissionName][m.Influence]; ++collated[m.MissionName][m.Influence];
hadmissions = true;
total_influence += m.Influence.Length; total_influence += m.Influence.Length;
if (m.AcceptedEntry != null && if (m.AcceptedEntry != null &&
@@ -121,19 +123,23 @@ public class MissionFormat : LogFormatter {
output.Append(failedlog); output.Append(failedlog);
output.Append("\n"); output.Append("\n");
} }
if (failed.Count > 0) {
hadmissions = true;
}
total_influence += failed.Sum(x => x.InfluenceAmount); total_influence += failed.Sum(x => x.InfluenceAmount);
foreach (InfluenceSupport inf in support) { foreach (InfluenceSupport inf in support) {
output.Append(inf.ToString()); output.Append(inf.ToString());
output.Append("\n"); output.Append("\n");
hadmissions = true;
total_influence += inf.Influence.InfluenceAmount; total_influence += inf.Influence.InfluenceAmount;
} }
if (support.Count() > 0) { if (support.Count > 0) {
output.Append("\n"); output.Append("\n");
} }
if (total_influence != 0) { if (hadmissions) {
output.AppendFormat("Total Influence: {0}", total_influence); output.AppendFormat("Total Influence: {0}", total_influence);
} }
@@ -141,6 +147,10 @@ public class MissionFormat : LogFormatter {
} }
public string GenerateSummary(Objective objective) { public string GenerateSummary(Objective objective) {
bool hadmissions = objective
.EnabledOfType<MissionCompleted>()
.Count > 0
;
long influence = objective long influence = objective
.EnabledOfType<MissionCompleted>() .EnabledOfType<MissionCompleted>()
.Sum(x => x.Influence.Length) .Sum(x => x.Influence.Length)
@@ -154,7 +164,7 @@ public class MissionFormat : LogFormatter {
.Sum(x => x.InfluenceAmount) .Sum(x => x.InfluenceAmount)
; ;
if (influence == 0 && support == 0 && failed == 0) { if (influence == 0 && support == 0 && failed == 0 && !hadmissions) {
return ""; return "";
} }

View File

@@ -5,9 +5,13 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:EliteBGS" xmlns:local="clr-namespace:EliteBGS"
xmlns:Util="clr-namespace:EliteBGS.Util" d:DataContext="{d:DesignInstance Type=Util:AppConfig}" x:Name="window" x:Class="EliteBGS.MainWindow" xmlns:Util="clr-namespace:EliteBGS.Util"
d:DataContext="{d:DesignInstance Type=Util:AppConfig}"
x:Name="window"
x:Class="EliteBGS.MainWindow"
mc:Ignorable="d" mc:Ignorable="d"
Title="Elite: Dangerous BGS Helper" Height="520" Width="950" Icon="Salus.ico" Closing="window_Closing"> Title="Elite: Dangerous BGS Helper" Height="620" Width="950" Icon="Salus.ico" Closing="window_Closing"
>
<Window.Resources> <Window.Resources>
<local:MinusFortyFiveConverter x:Key="MinusFortyFiveConverter" /> <local:MinusFortyFiveConverter x:Key="MinusFortyFiveConverter" />
<Style x:Key="StretchingTreeViewStyle" TargetType="TreeViewItem" BasedOn="{StaticResource {x:Type TreeViewItem}}"> <Style x:Key="StretchingTreeViewStyle" TargetType="TreeViewItem" BasedOn="{StaticResource {x:Type TreeViewItem}}">
@@ -15,6 +19,23 @@
</Style> </Style>
<local:Report x:Key="ObjectivesBasedOnSystem" /> <local:Report x:Key="ObjectivesBasedOnSystem" />
<Util:Config x:Key="Config" />
<DataTemplate DataType="{x:Type Util:DiscordWebhook}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Text="Name: " Grid.Column="0" Margin="2,0,0,0"/>
<TextBox Text="{Binding Name}" Grid.Column="1"/>
<TextBlock Text="Webhook URL: " Grid.Column="2" Margin="2,0,0,0"/>
<TextBox Text="{Binding Webhook}" Grid.Column="3"/>
</Grid>
</DataTemplate>
<HierarchicalDataTemplate DataType="{x:Type local:SystemObjectives}" ItemsSource="{Binding Path=Objectives}"> <HierarchicalDataTemplate DataType="{x:Type local:SystemObjectives}" ItemsSource="{Binding Path=Objectives}">
<Grid <Grid
@@ -100,6 +121,7 @@
<Button Content="Ground" x:Name="Ground" Click="Ground_Click"/> <Button Content="Ground" x:Name="Ground" Click="Ground_Click"/>
<Button Content="Ship" x:Name="Ship" Click="Ship_Click"/> <Button Content="Ship" x:Name="Ship" Click="Ship_Click"/>
<Button Content="AX" x:Name="Thargoid" Click="Thargoid_Click"/> <Button Content="AX" x:Name="Thargoid" Click="Thargoid_Click"/>
<Button Content="Power" x:Name="Power" Click="Power_Click"/>
</StackPanel> </StackPanel>
</Expander> </Expander>
</StackPanel> </StackPanel>
@@ -162,6 +184,11 @@
<ComboBox x:Name="LogType" VerticalAlignment="Stretch" Margin="0,3,0,3" Width="140" SelectionChanged="LogType_SelectionChanged" /> <ComboBox x:Name="LogType" VerticalAlignment="Stretch" Margin="0,3,0,3" Width="140" SelectionChanged="LogType_SelectionChanged" />
<Separator /> <Separator />
<CheckBox x:Name="SelectAll" Content="Select All" IsChecked="True" Click="SelectAll_Click"/> <CheckBox x:Name="SelectAll" Content="Select All" IsChecked="True" Click="SelectAll_Click"/>
<Separator />
<Label Content="Post log as" VerticalAlignment="Center" Margin="0,3,0,3" />
<ComboBox x:Name="Commanders" VerticalAlignment="Stretch" Margin="0,3,0,3" Width="180" />
<Label Content="to" VerticalAlignment="Center" Margin="0,3,0,3" />
<mah:DropDownButton x:Name="PostToDiscord" Content="Discord" />
</ToolBar> </ToolBar>
<TreeView CheckBox.Checked="TreeView_CheckBox_Updated" <TreeView CheckBox.Checked="TreeView_CheckBox_Updated"
CheckBox.Unchecked="TreeView_CheckBox_Updated" CheckBox.Unchecked="TreeView_CheckBox_Updated"
@@ -189,6 +216,7 @@
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/> <RowDefinition Height="*"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
@@ -244,6 +272,35 @@
<mah:ToggleSwitch x:Name="NoFleetCarrier" Grid.Row="2" Grid.ColumnSpan="2" Content="Ignore transactions done on a Fleet Carrier" Toggled="NoFleetCarrier_Toggled" /> <mah:ToggleSwitch x:Name="NoFleetCarrier" Grid.Row="2" Grid.ColumnSpan="2" Content="Ignore transactions done on a Fleet Carrier" Toggled="NoFleetCarrier_Toggled" />
</Grid> </Grid>
</GroupBox> </GroupBox>
<GroupBox Header="Discord Webhooks" Grid.Row="3" VerticalAlignment="Top" Width="Auto" Grid.ColumnSpan="3" Margin="0,5,0,0">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<DataGrid x:Name="Webhooks"
Grid.Row="0"
ItemsSource="{Binding Webhooks}"
AutoGenerateColumns="False"
ScrollViewer.CanContentScroll="True"
ScrollViewer.VerticalScrollBarVisibility="Auto"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
MaxHeight="100"
MinHeight="100"
KeyUp="Webhooks_KeyUp"
CellEditEnding="Webhooks_CellEditEnding"
>
<DataGrid.Columns>
<DataGridTextColumn Header="Name" Binding="{Binding Name}"/>
<DataGridTextColumn Header="Webhook URL" Binding="{Binding Webhook}" Width="*"/>
</DataGrid.Columns>
</DataGrid>
<StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Left" Margin="5,10,5,0">
<Button x:Name="AddWebHook" Content="Add" Click="AddWebHook_Click"/>
<Button x:Name="RemoveWebHook" Content="Remove" Click="RemoveWebHook_Click" />
</StackPanel>
</Grid>
</GroupBox>
</Grid> </Grid>
</TabItem> </TabItem>
<TabItem Header="Event Log"> <TabItem Header="Event Log">

View File

@@ -46,6 +46,9 @@ public partial class MainWindow : MetroWindow {
/* ignored */ /* ignored */
} }
Webhooks.ItemsSource = Config.Global.Webhooks;
RefreshPostMenu();
foreach (DiscordLogGenerator type in logtypes) { foreach (DiscordLogGenerator type in logtypes) {
LogType.Items.Add(type); LogType.Items.Add(type);
} }
@@ -182,6 +185,11 @@ public partial class MainWindow : MetroWindow {
List<Transaction> transactions = parser.Parse(entries, options); List<Transaction> transactions = parser.Parse(entries, options);
Commanders.ItemsSource = parser.Commanders;
if (Commanders.Items.Count > 0) {
Commanders.SelectedIndex = 0;
}
// Filter the transactions down to the given time frame // Filter the transactions down to the given time frame
transactions = transactions transactions = transactions
.Where(t => t.CompletedAtDateTime >= start && t.CompletedAtDateTime <= end) .Where(t => t.CompletedAtDateTime >= start && t.CompletedAtDateTime <= end)
@@ -468,6 +476,16 @@ public partial class MainWindow : MetroWindow {
RefreshView(); RefreshView();
} }
private void Power_Click(object sender, RoutedEventArgs e) {
CombatZone transaction = GetTransaction<CombatZone>(sender);
if (transaction == null) {
return;
}
transaction.Type = CombatZones.PowerCombatZone;
RefreshView();
}
private void Thargoid_Click(object sender, RoutedEventArgs e) { private void Thargoid_Click(object sender, RoutedEventArgs e) {
CombatZone transaction = GetTransaction<CombatZone>(sender); CombatZone transaction = GetTransaction<CombatZone>(sender);
if (transaction == null) { if (transaction == null) {
@@ -580,4 +598,130 @@ public partial class MainWindow : MetroWindow {
} }
report.SystemObjectives.ForEach(t => { t.IsEnabled = (bool)SelectAll.IsChecked; }); report.SystemObjectives.ForEach(t => { t.IsEnabled = (bool)SelectAll.IsChecked; });
} }
private void AddWebHook_Click(object sender, RoutedEventArgs e) {
Config.Global.Webhooks.Add(new DiscordWebhook {
Name = "Discord Server Name",
Webhook = "..."
});
Webhooks.Items.Refresh();
RefreshPostMenu();
}
private void RemoveWebHook_Click(object sender, RoutedEventArgs e) {
if (Webhooks.SelectedItems.Count <= 0) {
return;
}
var selection = Webhooks.SelectedItems
.OfType<DiscordWebhook>()
.ToList()
;
foreach (var item in selection) {
Config.Global.Webhooks.Remove(item);
}
Webhooks.Items.Refresh();
RefreshPostMenu();
}
private void Webhooks_KeyUp(object sender, KeyEventArgs e) {
DataGridCell cell = e.OriginalSource as DataGridCell;
/* We also get keypresses from DataGridCells that are currently
* editing their content. Filter those out. We don't want to delete
* the row when the user presses DEL while editing the cells content
*/
if (cell == null || cell.IsEditing) {
return;
}
if (e.Key == Key.Delete) {
RemoveWebHook_Click(this, new RoutedEventArgs());
}
}
private void Webhooks_CellEditEnding(object sender, DataGridCellEditEndingEventArgs e) {
try {
Config.SaveGlobal();
} catch (Exception) { }
e.Cancel = false;
RefreshPostMenu();
}
private void RefreshPostMenu() {
MenuItem menu;
PostToDiscord.Items.Clear();
if (Config.Global.Webhooks.Count <= 0) {
PostToDiscord.IsEnabled = false;
} else {
PostToDiscord.IsEnabled = true;
foreach (var item in Config.Global.Webhooks) {
menu = new MenuItem();
menu.Header = item.Name;
menu.Click += DiscordWebhook_Click;
PostToDiscord.Items.Add(menu);
}
PostToDiscord.Items.Add(new Separator());
menu = new MenuItem();
menu.Header = "All";
menu.Click += PostToAll_Click;
PostToDiscord.Items.Add(menu);
}
}
private void PostToDiscordWebhook(IEnumerable<DiscordWebhook> hooks) {
if (string.IsNullOrEmpty(DiscordLog.Text)) {
return;
}
DiscordLogGenerator discord = LogType.SelectedItem as DiscordLogGenerator;
string[] chunks;
try {
chunks = discord.SplitLog(DiscordLog.Text);
} catch (Exception) {
MessageBox.Show(
"The log could not be split into discord appropriate length.\n" +
"This happens with the bigger logs formats if you do lots of things in one system.\n" +
"Try posting the log in the OneLine format.",
"Sorry!", MessageBoxButton.OK, MessageBoxImage.Error
);
return;
}
string commander = "EDBGS";
if (Commanders.SelectedIndex >= 0) {
commander = Commanders.SelectedItem.ToString();
}
foreach (var hook in hooks) {
try {
foreach (var chunk in chunks) {
DiscordPoster.PostToDiscord(hook, chunk, commander);
}
Log(string.Format("successfully posted to discord webhook `{0}`", hook.Name));
} catch (Exception ex) {
Log(string.Format("failed to post to discord webhook `{0}`: {1}",
hook.Name, ex.Message));
}
}
}
private void DiscordWebhook_Click(object sender, RoutedEventArgs e) {
MenuItem item = sender as MenuItem;
if (item == null) {
return;
}
DiscordWebhook hook = Config.Global.Webhooks
.Find(x => string.Compare(x.Name, item.Header.ToString()) == 0)
;
if (hook == null) {
return;
}
PostToDiscordWebhook(new DiscordWebhook[] { hook });
}
private void PostToAll_Click(object sender, RoutedEventArgs e) {
PostToDiscordWebhook(Config.Global.Webhooks);
}
} }

View File

@@ -91,4 +91,8 @@ public class NonaDiscordLog : DiscordLogGenerator {
public override string Name { public override string Name {
get { return "NovaNavy"; } get { return "NovaNavy"; }
} }
public override string[] SplitLog(string log, int maxcount = 2000) {
return SplitLogWithHeader(log, ":clock2: `Date:`", maxcount);
}
} }

View File

@@ -59,7 +59,9 @@ public class UITransaction : INotifyPropertyChanged {
return Visibility.Hidden; return Visibility.Hidden;
} }
if (string.Compare(combat.Type, CombatZones.ShipCombatZone) == 0) { // Both normal and power combat zones have special objectives
if (string.Compare(combat.Type, CombatZones.ShipCombatZone) == 0 ||
string.Compare(combat.Type, CombatZones.PowerCombatZone) == 0) {
return Visibility.Visible; return Visibility.Visible;
} }

View File

@@ -1,6 +1,9 @@
using EliteBGS.LogGenerator; using EliteBGS.LogGenerator;
using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Windows.Documents;
namespace EliteBGS; namespace EliteBGS;
@@ -25,6 +28,30 @@ public class OneLineDiscordLog : DiscordLogGenerator {
return ""; return "";
} }
public override string[] SplitLog(string log, int maxcount = 2000) {
string[] lines = log.Split('\n');
List<string> chunks = new();
string chunk = string.Empty;
// Optimisation
if (log.Length <= maxcount) {
return new string[] { log };
}
for (int i = 0; i < lines.Length; i++) {
string line = lines[i];
if ((chunk.Length + line.Length) > maxcount || i == lines.Length - 1) {
chunks.Add(chunk.Trim());
chunk = string.Empty;
chunk = chunk.Insert(0, BotHeader()).Trim();
} else {
chunk = chunk + "\n" + line;
}
}
return chunks.ToArray();
}
public override string GenerateDiscordLog(Report report) { public override string GenerateDiscordLog(Report report) {
StringBuilder log = new StringBuilder(); StringBuilder log = new StringBuilder();

View File

@@ -49,5 +49,5 @@ using System.Windows;
// You can specify all the values or you can default the Build and Revision Numbers // You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below: // by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")] // [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("0.4.2.0")] [assembly: AssemblyVersion("0.4.3.0")]
[assembly: AssemblyFileVersion("0.4.2.0")] [assembly: AssemblyFileVersion("0.4.3.0")]

View File

@@ -1,71 +1,79 @@
using Newtonsoft.Json; using Newtonsoft.Json;
using System.Collections.Generic;
using System.ComponentModel;
namespace EliteBGS.Util { namespace EliteBGS.Util;
public class AppConfig {
private static readonly string default_journal_location = "%UserProfile%\\Saved Games\\Frontier Developments\\Elite Dangerous";
private string journal_location = default_journal_location;
public string DefaultJournalLocation => default_journal_location;
private string colour = "Amber";
private string theme = "Dark";
public string LastUsedDiscordTemplate { get; set; } public class AppConfig {
private static readonly string default_journal_location = "%UserProfile%\\Saved Games\\Frontier Developments\\Elite Dangerous";
private string journal_location = default_journal_location;
private string colour = "Amber";
private string theme = "Dark";
public string JournalLocation { public static string DefaultJournalLocation => default_journal_location;
get {
if (journal_location == null) { public string LastUsedDiscordTemplate { get; set; }
return DefaultJournalLocation;
} public string JournalLocation {
return journal_location; get {
} if (journal_location == null) {
set { return DefaultJournalLocation;
journal_location = value;
} }
return journal_location;
} }
set {
public string Theme { journal_location = value;
get {
return theme;
}
set {
if (string.IsNullOrEmpty(value)) {
theme = "Dark";
} else {
theme = value;
}
}
}
public string Colour {
get {
return colour;
}
set {
if (string.IsNullOrEmpty(value)) {
colour = "Blue";
} else {
colour = value;
}
}
}
/// <summary>
/// Whether we ignore influence support scenarios form parsing.
/// </summary>
public bool IgnoreInfluenceSupport { get; set; } = false;
/// <summary>
/// Whether we ignore market buy entries during parsing.
/// </summary>
public bool IgnoreMarketBuy { get; set; } = false;
/// <summary>
/// Whether to ignore fleet carrier stuff when parsing.
/// </summary>
public bool IgnoreFleetCarrier { get; set; } = true;
[JsonIgnore]
public string FullTheme {
get { return Theme + "." + Colour; }
} }
} }
public string Theme {
get {
return theme;
}
set {
if (string.IsNullOrEmpty(value)) {
theme = "Dark";
} else {
theme = value;
}
}
}
public string Colour {
get {
return colour;
}
set {
if (string.IsNullOrEmpty(value)) {
colour = "Blue";
} else {
colour = value;
}
}
}
/// <summary>
/// Whether we ignore influence support scenarios form parsing.
/// </summary>
public bool IgnoreInfluenceSupport { get; set; } = false;
/// <summary>
/// Whether we ignore market buy entries during parsing.
/// </summary>
public bool IgnoreMarketBuy { get; set; } = false;
/// <summary>
/// Whether to ignore fleet carrier stuff when parsing.
/// </summary>
public bool IgnoreFleetCarrier { get; set; } = true;
/// <summary>
/// List of Webhooks configured
/// </summary>
public List<DiscordWebhook> Webhooks { get; set; } = new List<DiscordWebhook>();
[JsonIgnore]
public string FullTheme {
get { return Theme + "." + Colour; }
}
} }

View File

@@ -2,6 +2,7 @@
using System.Text; using System.Text;
using System.IO; using System.IO;
using Newtonsoft.Json; using Newtonsoft.Json;
using System.ComponentModel;
namespace EliteBGS.Util { namespace EliteBGS.Util {
public class Config { public class Config {

View File

@@ -0,0 +1,13 @@
namespace EliteBGS.Util;
public class DiscordWebhook {
/// <summary>
/// Webhook URL
/// </summary>
public string Webhook { get; set; } = string.Empty;
/// <summary>
/// Human readable name for easier identification
/// </summary>
public string Name { get; set; } = string.Empty;
}