Compare commits

...

14 Commits

21 changed files with 489 additions and 83 deletions

View File

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

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

@ -602,15 +602,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 {
private static Dictionary<string, ITransactionParserPart> ParserParts { get; } = new()
{
@ -669,6 +660,11 @@ public class TransactionParser {
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) {
TransactionList transactions = new();
TransactionParserContext context = new();
@ -686,6 +682,9 @@ public class TransactionParser {
transactionParserPart.Parse(entry, context, options, transactions);
}
// Copy out list of commanders seen
Commanders = context.Commanders;
return transactions.ToList();
}
}

View File

@ -3,6 +3,11 @@
namespace EDPlayerJournal.BGS;
internal class TransactionParserContext {
/// <summary>
/// List of commander names seen in the logs. May be empty.
/// </summary>
public List<string> Commanders { get; } = new();
/// <summary>
/// Name of the current system the player is in.
/// </summary>

View File

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

View File

@ -1,8 +1,23 @@
namespace EDPlayerJournal.Entries;
namespace EDPlayerJournal.Entries;
public class MultiSellExplorationDataEntry : Entry {
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

@ -6,8 +6,17 @@ namespace EDPlayerJournal.Entries;
public class SellExplorationDataEntry : Entry {
public long BaseValue { 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 Total {
get { return BaseValue + Bonus; }
}
public List<string> Systems { get; set; } = new List<string>();
public List<string> Discovered { get; set; } = new List<string>();

View File

@ -1,5 +1,15 @@
# EliteBGS changelog
## 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
* 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();
}
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>
<TargetFramework>net7.0-windows</TargetFramework>
<OutputType>WinExe</OutputType>
<Version>0.4.2</Version>
<Version>0.4.3</Version>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<UseWindowsForms>true</UseWindowsForms>
<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 override string ToString() {
@ -8,4 +11,8 @@ public class GenericDiscordLog : DiscordLogGenerator {
public override string Name {
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.Filter = "Log files (*.log)|*.log|All files (*.*)|*";
var location = config.Global.DefaultJournalLocation;
var location = AppConfig.DefaultJournalLocation;
if (Directory.Exists(location)) {
dialog.InitialDirectory = location;
}

View File

@ -5,9 +5,13 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
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"
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>
<local:MinusFortyFiveConverter x:Key="MinusFortyFiveConverter" />
<Style x:Key="StretchingTreeViewStyle" TargetType="TreeViewItem" BasedOn="{StaticResource {x:Type TreeViewItem}}">
@ -15,6 +19,23 @@
</Style>
<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}">
<Grid
@ -162,6 +183,11 @@
<ComboBox x:Name="LogType" VerticalAlignment="Stretch" Margin="0,3,0,3" Width="140" SelectionChanged="LogType_SelectionChanged" />
<Separator />
<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>
<TreeView CheckBox.Checked="TreeView_CheckBox_Updated"
CheckBox.Unchecked="TreeView_CheckBox_Updated"
@ -189,6 +215,7 @@
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
@ -244,6 +271,35 @@
<mah:ToggleSwitch x:Name="NoFleetCarrier" Grid.Row="2" Grid.ColumnSpan="2" Content="Ignore transactions done on a Fleet Carrier" Toggled="NoFleetCarrier_Toggled" />
</Grid>
</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>
</TabItem>
<TabItem Header="Event Log">

View File

@ -46,6 +46,9 @@ public partial class MainWindow : MetroWindow {
/* ignored */
}
Webhooks.ItemsSource = Config.Global.Webhooks;
RefreshPostMenu();
foreach (DiscordLogGenerator type in logtypes) {
LogType.Items.Add(type);
}
@ -182,6 +185,11 @@ public partial class MainWindow : MetroWindow {
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
transactions = transactions
.Where(t => t.CompletedAtDateTime >= start && t.CompletedAtDateTime <= end)
@ -580,4 +588,129 @@ public partial class MainWindow : MetroWindow {
}
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 {
get { return "NovaNavy"; }
}
public override string[] SplitLog(string log, int maxcount = 2000) {
return SplitLogWithHeader(log, ":clock2: `Date:`", maxcount);
}
}

View File

@ -1,6 +1,9 @@
using EliteBGS.LogGenerator;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Documents;
namespace EliteBGS;
@ -25,6 +28,30 @@ public class OneLineDiscordLog : DiscordLogGenerator {
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) {
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
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("0.4.2.0")]
[assembly: AssemblyFileVersion("0.4.2.0")]
[assembly: AssemblyVersion("0.4.3.0")]
[assembly: AssemblyFileVersion("0.4.3.0")]

View File

@ -1,71 +1,79 @@
using Newtonsoft.Json;
using System.Collections.Generic;
using System.ComponentModel;
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";
namespace EliteBGS.Util;
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 {
get {
if (journal_location == null) {
return DefaultJournalLocation;
}
return journal_location;
}
set {
journal_location = value;
public static string DefaultJournalLocation => default_journal_location;
public string LastUsedDiscordTemplate { get; set; }
public string JournalLocation {
get {
if (journal_location == null) {
return DefaultJournalLocation;
}
return journal_location;
}
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;
[JsonIgnore]
public string FullTheme {
get { return Theme + "." + Colour; }
set {
journal_location = value;
}
}
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.IO;
using Newtonsoft.Json;
using System.ComponentModel;
namespace EliteBGS.Util {
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;
}