Compare commits

..

174 Commits

Author SHA1 Message Date
4cefc393b5 add merits to the log 2025-04-16 15:09:56 +02:00
693d49be14 add new entries
the content is not yet known, but we do know the event name
2025-03-06 09:58:44 +01:00
32dbde2c82 add file to identify powers 2025-03-06 09:58:18 +01:00
894918b5ba Merge branch 'master' of codeberg.org:nola/EDBGS 2024-12-18 09:24:11 +01:00
41fca4fa8c delete old obsolete docs 2024-12-18 09:03:57 +01:00
2acb63025c remove backup file 2024-11-18 09:07:50 +01:00
eaa4bda098 update changelog 2024-11-14 14:08:36 +01:00
9d8f0c1b9a strip out tailing semi-colon 2024-11-14 13:55:03 +01:00
88b770e5ec add format for merits gained in log 2024-11-14 13:52:17 +01:00
d9dd2cc524 add option to ignore power play to UI 2024-11-14 13:40:25 +01:00
2905696641 add option to ignore powerplay for now 2024-11-14 13:33:49 +01:00
aeaa6b5220 add preliminary support for merits gained 2024-11-14 13:30:31 +01:00
cdbca10f2d add support for Powerplay message 2024-11-14 12:14:36 +01:00
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
0203008202 make placement of colons consistent 2024-05-02 20:26:14 +02:00
03621721b8 bump version to 0.4.2.0 2024-05-02 20:13:02 +02:00
ccba55ac35 move colon into fixed with formatting 2024-05-02 20:12:23 +02:00
0708880284 don't add a newline after bot header for one line 2024-05-02 20:11:51 +02:00
18c3073635 update changelog 2024-05-02 20:09:19 +02:00
c43d2ff1d3 add a bot header for discord log parsing 2024-05-02 20:07:10 +02:00
a3b7623557 bump version 2024-04-28 12:48:42 +02:00
1c2fc1e2e6 update changelog 2024-04-28 12:47:00 +02:00
53da6b4bc2 add Ermeni Blue 2024-04-28 12:46:01 +02:00
bc44ceb205 update changelog 2024-04-28 12:42:12 +02:00
4d3048a37d add the market name to trading entries 2024-04-28 12:41:50 +02:00
463598c779 update changelog 2024-04-28 12:36:58 +02:00
bf56f3a2d5 add a filter for doubly redeemed vouchers 2024-04-28 12:35:14 +02:00
450824733d polska gurom 2024-04-13 12:18:33 +02:00
110f909c6f update screenshot for combat zone 2024-04-13 12:13:08 +02:00
a1099628a0 update URL to Salus homepage 2024-04-13 12:12:14 +02:00
7e85159fd5 fix removing of entries 2024-04-13 12:07:38 +02:00
2e8daca61c add new main image 2024-04-13 12:07:30 +02:00
05b714a607 remove mkdocs.yml 2024-04-13 11:52:29 +02:00
3b0492c70f adapt the offset for the new levels 2024-04-13 11:49:13 +02:00
c20280eb13 remove mkdocs
The web page for the tool has moved to the Salus home page, so we no longer need a mkdocs file to generate the home page. But we keep the other docs around for reference.
2024-04-13 11:46:57 +02:00
dd863c326e bump version 2024-04-13 11:44:05 +02:00
2012aab7b3 update changelog 2024-04-13 11:43:17 +02:00
abb7954614 sort systems by name 2024-04-12 13:50:13 +02:00
0f44a6a9a7 add a select all button
also wished for by Shak
2024-04-12 13:48:55 +02:00
34e0a0c8ba change layout of tree to go: system / faction / bgs
This was requested by some very special commanders who, after a massive BGS bender, have very long lists in terms of BGS actions taken
2024-04-11 16:25:20 +02:00
b0b82811cc update project dependencies 2024-04-11 10:21:49 +02:00
4855d78823 forgot to fix the URL 2024-01-29 19:34:21 +01:00
5ea288ee86 update webpage 2024-01-29 19:29:28 +01:00
f3fc99a3f3 bump version to 0.3.7 2024-01-29 19:26:59 +01:00
2bee03dbc2 update changelog 2024-01-29 19:26:15 +01:00
16b579688d identify cap ship by its music 2024-01-29 19:24:53 +01:00
9994a45d06 fix location not updating on carrier jumps 2024-01-29 18:23:19 +01:00
d6e2280a00 update website 2023-10-25 11:39:39 +02:00
160f4f8370 bump version information 2023-10-25 11:38:56 +02:00
4400418d30 update changelog 2023-10-25 11:38:47 +02:00
43037b0a5b don't return null for unknown 2023-10-25 11:35:30 +02:00
afc831cf31 add the new banshee 2023-10-25 11:35:19 +02:00
4ab54ee576 show Errors found by journal parser 2023-10-25 11:30:44 +02:00
d6842115c5 no longer completely fail on one wrong entry
U17 might have caused some journal bugs so don't fail immediately on one wrong entry, instead keep them in an Errors collection for later processing
2023-10-25 11:30:34 +02:00
acb60e0a08 bump version 2023-09-11 10:51:17 +02:00
dac9b7b8c7 bump assembly version 2023-09-08 11:51:51 +02:00
3338f573c8 update changelog 2023-09-08 11:50:00 +02:00
204d6b8914 rename glaive to hunter
Glaive and Scythe can no longer be distinguished from the bounty claims you get.
2023-09-08 11:49:55 +02:00
4c75515a70 include failed missions in mission format
Since we now have to calculate negative INF with the normal INF (in case both happens), mission formatter now also handles failed missions.
2023-09-08 11:43:05 +02:00
c43c6f742a introduce negative influence
Sometimes missions actually tell us how much negative influence a faction got through secondary influences. We now count this, and present is as negative influence via minuses. Summaries have been updated to reflect this change.
2023-09-08 11:20:49 +02:00
c7a70598c4 don't lose small credit amounts in formatting 2023-09-08 10:21:10 +02:00
cdd7eb33de add settlements for ground CZs to the combat log 2023-08-26 10:53:25 +02:00
dab39b9a4e fix note in FAQ 2023-06-25 19:27:44 +02:00
20c44d41ca fix latest/earliest entry functions 2023-06-18 15:33:07 +02:00
4f339944c6 bump version 2023-06-18 15:25:09 +02:00
ae4e181e7f update changelog 2023-06-18 15:23:38 +02:00
be59588351 improve standard format by adding date range and tool version 2023-06-18 15:22:32 +02:00
1b2a162ac5 add allied to the combat zone object 2023-06-18 15:09:40 +02:00
f490d4ba4d add code to detect which captain/correspondent it was 2023-06-18 15:07:29 +02:00
6b09ec4db3 split captain and correspondent to enemy/allied 2023-06-18 14:55:22 +02:00
d7a1ac5ef2 add another English name 2023-06-13 18:50:58 +02:00
3643dda39e update English mission names 2023-06-01 09:04:28 +02:00
c7be0ef3f5 thargoid bonds are not a system contribution 2023-05-25 18:09:50 +02:00
6a36458026 death only matters on foot 2023-05-25 17:33:59 +02:00
d7dc9bd904 change site to new version number 2023-05-15 19:00:08 +02:00
af66983497 bump version number 2023-05-15 18:59:06 +02:00
3f82e335aa add one more English mission name 2023-05-15 18:56:46 +02:00
77fcbfbba7 update changelog 2023-05-15 18:54:32 +02:00
a45ca9f5bc add option to browse journal location 2023-05-15 18:44:11 +02:00
c9e87958ae implement ignore fleet carrier faction into UI 2023-05-15 18:38:03 +02:00
12b15bb910 fix wrong incomplete type in MarketBuyParser 2023-05-15 18:29:29 +02:00
bec73931b9 add an option to ignore Fleet Carrier faction 2023-05-15 18:15:38 +02:00
b3fca4a63e add faction name for fleet carriers 2023-05-15 17:58:19 +02:00
6a61aa4946 update changelog 2023-05-14 20:22:21 +02:00
b005edc27f don't ignore new system factions
If a factions expands or retreats into a system we see, we won't pick it up if we remain with old data.
2023-05-14 20:20:43 +02:00
8f9f4f3e35 add one more English mission name 2023-05-13 20:19:09 +02:00
7b4176fce5 add drones to the thargoid log 2023-05-13 17:50:26 +02:00
434756695a add information regarding Revenant 2023-05-13 17:47:21 +02:00
a50f0c2313 update description 2023-05-11 20:49:44 +02:00
6dce0116ec update changelog 2023-05-11 20:45:19 +02:00
79914919e5 add combat zone detection through SupercruiseDestinationDrop 2023-05-11 20:43:48 +02:00
c23e8627f6 update instance code 2023-05-11 20:43:30 +02:00
1e36eb3419 fix naming of the event 2023-05-11 20:33:17 +02:00
d49612e7e5 rename class properly 2023-05-11 20:02:53 +02:00
b19a576515 add internal names for combat zones 2023-05-10 09:36:02 +02:00
9ffca16a78 add the new supercruise destination drop event 2023-05-10 09:22:01 +02:00
73a1975964 add payout value for glaive 2023-05-09 21:43:00 +02:00
f9f1842cb7 remove extra bought from log 2023-04-27 16:24:48 +02:00
5487cb3d37 fix log format for murders 2023-04-27 16:24:10 +02:00
e11572a565 update assembly version 2023-04-19 15:52:20 +02:00
16ee7047cd make a release 2023-04-19 15:49:20 +02:00
cedc576b47 mind spelling 2023-04-19 15:08:56 +02:00
58b8ed07c4 fix changelog 2023-04-19 10:15:49 +02:00
ef85f51734 update changelog 2023-04-19 10:15:15 +02:00
00deaf20fc better name market buy/market sold 2023-04-19 10:13:35 +02:00
5fe652f98c add profit to search and rescue 2023-04-19 10:13:02 +02:00
8cf8d7f529 change one line discord log to only show summaries 2023-04-19 10:06:10 +02:00
fce534c88f remove old ico from source files 2023-04-19 09:38:04 +02:00
25338c8754 bump version 2023-04-19 09:29:21 +02:00
139cbd05f8 update changelog 2023-04-19 09:27:57 +02:00
b0302572f8 forgot to load the setting 2023-04-19 09:25:36 +02:00
87314d0258 we still want profit info, just not BGS info 2023-04-19 09:19:37 +02:00
f4bbd3df2b add GUI elements for ignoring market buy 2023-04-19 09:17:06 +02:00
d6acbda55c add option to ignore market buy 2023-04-19 09:06:54 +02:00
5c9d9c9153 correct black market trade by properly setting StationOwner 2023-04-19 09:01:32 +02:00
da3a355695 provide station faction via Location entry 2023-04-19 08:37:22 +02:00
5224dd4555 split out some classes to avoid clutter 2023-04-19 08:31:23 +02:00
15fda7692e format credits with millions 2023-04-11 19:17:29 +02:00
4cd0c02c67 write config file in indented JSON 2023-03-29 18:41:10 +02:00
e39dd6ffda add setting to ignore influence support 2023-03-29 18:40:36 +02:00
f666a2c7c5 add additional english names 2023-03-27 21:34:13 +02:00
8574b74494 add more English mission names 2023-03-25 12:21:34 +01:00
1255bf9fa5 add two more English mission names 2023-03-18 00:57:41 +01:00
781bb7a209 more thargoid payout changes 2023-03-18 00:56:16 +01:00
24817a02a9 add more English mission names 2023-03-08 15:45:24 +01:00
d00c8d5c06 allow possibility to ignore influence support 2023-03-03 15:59:41 +01:00
b588050fb4 ignoring exo biology is now handled in the parser 2023-03-02 20:10:49 +01:00
f9eb0d34f9 introduce parser options
And use it to ignore exo biology data.
2023-03-02 20:09:19 +01:00
2a393809fc add additional mission names 2023-03-01 13:43:13 +01:00
7ee734bc33 bugfix release 2023-02-26 22:16:01 +01:00
5799b3ed16 fix summary for failed missions 2023-02-26 22:03:31 +01:00
a13e8446d2 fix summary for AX combat zones 2023-02-26 22:01:13 +01:00
0665e64459 fix VH type 2023-02-26 21:57:45 +01:00
d8ac2a7ee7 grade can be null 2023-02-26 21:57:24 +01:00
1a8c163fc4 update readmes to include mahapps 2023-02-26 10:24:01 +01:00
7b7fdf0ceb update README 2023-02-26 10:21:55 +01:00
bf86ce3fe8 update documentation 2023-02-26 10:20:11 +01:00
bbf3d6c6a4 add another mission name 2023-02-26 10:10:48 +01:00
d1c232df9d if there is a bug, use a default skin 2023-02-26 10:09:50 +01:00
82281ecf1b English is hard 2023-02-25 18:14:00 +01:00
7bf2b028c3 add link to homepage & source code to window 2023-02-25 18:12:33 +01:00
d2300fa088 update package information 2023-02-25 17:54:42 +01:00
32d7bccadc update changelog 2023-02-25 17:51:23 +01:00
831bf38e78 sort combat zones 2023-02-25 17:48:01 +01:00
e864db8488 add english names 2023-02-25 17:41:51 +01:00
0250878ee7 add more english names 2023-02-25 11:00:25 +01:00
5120c7991f implement a summary line
For Pony's bot, implement a summary line that just shows the highlights of the log
2023-02-23 21:44:55 +01:00
3c1abe5e8c change colour 2023-02-23 20:57:43 +01:00
637b4f85d0 use brighter colour 2023-02-23 20:57:28 +01:00
53bf0d22b8 add custom themes for NovaNavy & Salus 2023-02-23 20:55:17 +01:00
1c0c864e62 remove exo biology from reports
Multiple confirmations tell that the exo biology donations don't affect BGS at all.
2023-02-22 20:51:51 +01:00
52aa2706c0 switch to MahApps controls and style
Why didn't I find this sooner?
2023-02-22 20:39:40 +01:00
fd10b86c79 add more mission names 2023-02-22 18:25:12 +01:00
2f8d32f57a add some more mission names 2023-02-13 17:26:20 +01:00
926150cd18 update webpage with releases from CodeBerg 2023-02-04 16:15:42 +01:00
108 changed files with 3417 additions and 1295 deletions

View File

@ -1,10 +1,11 @@
using System.Linq;
using System.Text;
using System.Text;
using EDPlayerJournal.Entries;
namespace EDPlayerJournal.BGS;
public class Cartographics : Transaction {
public Cartographics() { }
public Cartographics(MultiSellExplorationDataEntry e) {
Entries.Add(e);
}
@ -34,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

@ -19,20 +19,36 @@ public class CombatZone : Transaction {
public bool? SpecOps { get; set; }
/// <summary>
/// Whether captain was won
/// Whether allied captain objective was won
/// </summary>
public bool? Captain { get; set; }
public bool? AlliedCaptain { get; set; }
/// <summary>
/// Whether correspondent objective was won
/// Whether enemy captain objective was won
/// </summary>
public bool? Correspondent { get; set; }
public bool? EnemyCaptain { get; set; }
/// <summary>
/// Whether the allied correspondent objective was won
/// </summary>
public bool? AlliedCorrespondent { get; set; }
/// <summary>
/// Whether the enemy correspondent objective was won
/// </summary>
public bool? EnemyCorrespondent { get; set; }
/// <summary>
/// Whether cap ship objective was won
/// </summary>
public bool? CapitalShip { get; set; }
/// <summary>
/// If we have a combat zone, this might point to the settlement
/// in question.
/// </summary>
public string? Settlement { get; set; }
/// <summary>
/// How many optional objectives were completed?
/// </summary>
@ -41,7 +57,14 @@ public class CombatZone : Transaction {
if (IsGround) {
return 0;
}
return new List<bool?>() { SpecOps, Captain, Correspondent, CapitalShip }
return new List<bool?>() {
SpecOps,
AlliedCaptain,
EnemyCaptain,
AlliedCorrespondent,
EnemyCorrespondent,
CapitalShip
}
.Where(x => x != null && x == true)
.Count()
;
@ -69,6 +92,13 @@ public class CombatZone : Transaction {
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) {
if (obj == null || obj.GetType() != typeof(CombatZone)) {
return -1;

View File

@ -9,7 +9,7 @@ namespace EDPlayerJournal.BGS;
/// faction to another. Both sometimes gain influence.
/// </summary>
public class InfluenceSupport : Transaction {
public string Influence { get; set; } = "";
public MissionInfluence? Influence { get; set; } = null;
/// <summary>
/// Relevant mission completed entry
@ -46,7 +46,7 @@ public class InfluenceSupport : Transaction {
builder.AppendFormat("Influence gained from \"{0}\": \"{1}\"",
missionname,
string.IsNullOrEmpty(Influence) ? "NONE" : Influence
Influence == null ? "NONE" : Influence.TrendAdjustedInfluence
);
return builder.ToString();

View File

@ -0,0 +1,32 @@
using EDPlayerJournal.Entries;
namespace EDPlayerJournal.BGS;
public class MeritsGained : Transaction {
public MeritsGained() { }
public MeritsGained(Entry entry) {
Entries.Add(entry);
}
/// <summary>
/// Number of merits gained
/// </summary>
public long Merits {
get {
return Entries
.OfType<PowerplayMeritsEntry>()
.Sum(x => x.MeritsGained)
;
}
}
/// <summary>
/// For what power those merits were gained
/// </summary>
public string Power { get; set; } = string.Empty;
public override string ToString() {
return string.Format("{0} Merits gained for {1}", Merits, Power);
}
}

View File

@ -38,7 +38,14 @@ public class MissionCompleted : Transaction {
return "";
}
return (CompletedEntry.Mission.GetInfluenceForFaction(Faction, SystemAddress) ?? "");
return string.Join("",
CompletedEntry
.Mission
.GetInfluenceForFaction(Faction, SystemAddress)
.Select(x => x.Influence)
.ToArray()
)
;
}
}
@ -70,8 +77,10 @@ public class MissionCompleted : Transaction {
var influence = CompletedEntry.Mission.GetInfluenceForFaction(Faction, SystemAddress);
builder.AppendFormat("{0}", MissionName);
if (influence != "") {
builder.AppendFormat(", Influence: {0}", influence);
if (influence != null && influence.Length > 0) {
builder.AppendFormat(", Influence: {0}",
influence.Select(x => x.InfluenceAmount).Sum()
);
}
return builder.ToString();

View File

@ -13,6 +13,21 @@ public class MissionFailed : Transaction {
Failed = failed;
}
/// <summary>
/// Returns the amount of influence generated by failing this mission. The
/// system returns no influence for one INF missions, but sadly also for
/// when the influence had no affect at all (i.e. during a war).
/// </summary>
public long InfluenceAmount {
get {
if (Mission == null || string.IsNullOrEmpty(Mission.Influence)) {
return -1;
} else {
return Mission.Influence.Length * -1;
}
}
}
public override int CompareTo(Transaction? other) {
if (other == null || other.GetType() != typeof(MissionFailed)) {
return -1;

View File

@ -4,6 +4,8 @@ using EDPlayerJournal.Entries;
namespace EDPlayerJournal.BGS;
public class OrganicData : Transaction {
public OrganicData() { }
public OrganicData(SellOrganicDataEntry e) {
Entries.Add(e);
}
@ -36,6 +38,8 @@ public class OrganicData : Transaction {
/* Selling organic data only helps the controlling faction, just like
* selling cartographic data.
*
* Right now: Organic data helps no one.
*/
public override bool OnlyControllingFaction => true;
}

View File

@ -0,0 +1,15 @@
using EDPlayerJournal.Entries;
namespace EDPlayerJournal.BGS.Parsers;
internal class ApproachSettlementParser : ITransactionParserPart {
public void Parse(Entry entry, TransactionParserContext context, TransactionParserOptions options, TransactionList transactions) {
ApproachSettlementEntry? approach = entry as ApproachSettlementEntry;
if (approach == null || string.IsNullOrEmpty(approach.Name)) {
return;
}
context.Settlement = approach.Name;
}
}

View File

@ -0,0 +1,30 @@
using EDPlayerJournal.Entries;
namespace EDPlayerJournal.BGS.Parsers;
internal class CarrierJumpParser : ITransactionParserPart {
public void Parse(Entry entry, TransactionParserContext context, TransactionParserOptions options, TransactionList transactions) {
CarrierJump? jump = entry as CarrierJump;
if (jump == null) {
return;
}
if (!jump.Docked) {
return;
}
context.CurrentSystem = jump.StarSystem;
context.CurrentSystemAddress = jump.SystemAddress;
context.SystemsByID.TryAdd(jump.SystemAddress, jump.StarSystem);
if (!string.IsNullOrEmpty(jump.SystemFaction)) {
context.ControllingFaction = jump.SystemFaction;
}
if (jump.SystemFactions != null && jump.SystemFactions.Count > 0) {
context.SystemFactions[jump.StarSystem] = jump.SystemFactions;
}
}
}

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

@ -0,0 +1,44 @@
using EDPlayerJournal.Entries;
namespace EDPlayerJournal.BGS;
internal class MarketBuyParser : ITransactionParserPart {
public void Parse(Entry e, TransactionParserContext context, TransactionParserOptions options, TransactionList transactions) {
MarketBuyEntry? entry = e as MarketBuyEntry;
if (entry == null) {
throw new NotImplementedException();
}
context.BoughtCargo(entry.Type, entry.BuyPrice);
// We still want the information on buy price for profit,
// but if the option is on, we don't care for parsing it
// further.
// TODO: might be wise to split this parser into two; one for
// determining profit, the other for the BGS information
if (options.IgnoreMarketBuy) {
return;
}
if (context.StationOwner == null) {
transactions.AddIncomplete(
new BuyCargo(),
"Could not discern the station owner for market buy.",
e);
return;
}
// Ignore if its a fleet carrier faction.
if (string.Compare(context.StationOwner, Factions.FleetCarrier, StringComparison.OrdinalIgnoreCase) == 0 &&
options.IgnoreFleetCarrierFaction) {
return;
}
transactions.Add(new BuyCargo(entry) {
System = context.CurrentSystem,
Station = context.CurrentStation,
Faction = context.StationOwner,
IsLegacy = context.IsLegacy,
});
}
}

View File

@ -0,0 +1,45 @@
using EDPlayerJournal.Entries;
namespace EDPlayerJournal.BGS;
internal class MarketSellParser : ITransactionParserPart {
public void Parse(Entry e, TransactionParserContext context, TransactionParserOptions options, TransactionList transactions) {
long profit = 0;
if (context.StationOwner == null) {
transactions.AddIncomplete(
new SellCargo(),
"Could not discern the station owner market sell.",
e);
return;
}
// Ignore if its a fleet carrier faction.
if (string.Compare(context.StationOwner, Factions.FleetCarrier, StringComparison.OrdinalIgnoreCase) == 0 &&
options.IgnoreFleetCarrierFaction) {
return;
}
MarketSellEntry? entry = e as MarketSellEntry;
if (entry == null) {
throw new NotImplementedException();
}
if (entry.Type == null) {
throw new InvalidJournalEntryException("market sell contains no cargo type");
}
if (context.BuyCost.ContainsKey(entry.Type)) {
long avg = context.BuyCost[entry.Type];
profit = (long)entry.TotalSale - (avg * entry.Count);
}
transactions.Add(new SellCargo(entry) {
System = context.CurrentSystem,
Station = context.CurrentStation,
Faction = context.StationOwner,
Profit = profit,
IsLegacy = context.IsLegacy,
});
}
}

View File

@ -0,0 +1,33 @@
using EDPlayerJournal.Entries;
namespace EDPlayerJournal.BGS;
internal class MultiSellExplorationDataParser : ITransactionParserPart {
public void Parse(Entry e, TransactionParserContext context, TransactionParserOptions options, TransactionList transactions) {
MultiSellExplorationDataEntry? entry = e as MultiSellExplorationDataEntry;
if (entry == null) {
throw new NotImplementedException();
}
if (context.StationOwner == null) {
transactions.AddIncomplete(
new Cartographics(),
"Could not discern the station owner for cartographics sell.",
e);
return;
}
// Ignore if its a fleet carrier faction.
if (string.Compare(context.StationOwner, Factions.FleetCarrier, StringComparison.OrdinalIgnoreCase) == 0 &&
options.IgnoreFleetCarrierFaction) {
return;
}
transactions.Add(new Cartographics(entry) {
System = context.CurrentSystem,
Station = context.CurrentStation,
Faction = context.StationOwner,
IsLegacy = context.IsLegacy,
});
}
}

View File

@ -0,0 +1,17 @@
using EDPlayerJournal.Entries;
namespace EDPlayerJournal.BGS.Parsers;
internal class MusicParser : ITransactionParserPart {
public void Parse(Entry entry, TransactionParserContext context, TransactionParserOptions options, TransactionList transactions) {
MusicEntry? entryMusic = (MusicEntry)entry;
if (entryMusic == null) {
return;
}
if (string.Compare(entryMusic.MusicTrack, "Combat_CapitalShip") == 0) {
context.HaveSeenCapShip = true;
}
}
}

View File

@ -0,0 +1,31 @@
using EDPlayerJournal.Entries;
namespace EDPlayerJournal.BGS.Parsers;
internal class PowerplayMeritsParser : ITransactionParserPart {
public void Parse(Entry entry, TransactionParserContext context, TransactionParserOptions options, TransactionList transactions) {
PowerplayMeritsEntry? e = entry as PowerplayMeritsEntry;
if (e == null) {
throw new ApplicationException("not a valid PowerplayMerits entry");
}
MeritsGained? transaction = null;
transaction = transactions
.OfType<MeritsGained>()
.Where(x => x.System == context.CurrentSystem &&
x.Power == e.Power)
.FirstOrDefault()
;
if (transaction == null) {
transaction = new MeritsGained(e) {
System = context.CurrentSystem,
Power = e.Power,
Faction = e.Power,
};
transactions.Add(transaction);
} else {
transaction.Entries.Add(e);
}
}
}

View File

@ -0,0 +1,84 @@
using EDPlayerJournal.Entries;
namespace EDPlayerJournal.BGS.Parsers;
internal class RedeemVoucherParser : ITransactionParserPart {
public void Parse(Entry e, TransactionParserContext context, TransactionParserOptions options, TransactionList transactions) {
RedeemVoucherEntry? entry = e as RedeemVoucherEntry;
if (entry == null) {
throw new NotImplementedException();
}
if (context.CurrentSystem == null) {
transactions.AddIncomplete(new Vouchers(),
"Could not find out where the vouchers were redeemed", e
);
return;
}
List<Faction>? current_factions = context.GetFactions(context.CurrentSystem);
if (current_factions == null) {
transactions.AddIncomplete(new Vouchers(),
"Current system factions are unknown, so vouchers were ineffective", e);
}
foreach (string faction in entry.Factions) {
bool relevantBond = false;
string relevantFaction = faction;
if (string.Compare(faction, Factions.PilotsFederationVouchers) == 0) {
// Target faction is pilots' federation, so we assume thargoid bonks
// Also assign this combat bond to the Pilots Federation
relevantFaction = Factions.PilotsFederation;
relevantBond = true;
}
if (current_factions != null && !relevantBond) {
// If we have local factions, and it ain't thargoid bonds see if the bonds were
// useful in the current system
if (current_factions.Find(x => string.Compare(x.Name, faction, true) == 0) != null) {
relevantBond = true;
} else {
transactions.AddIncomplete(new Vouchers(),
string.Format("Vouchers for \"{0}\" had no effect in \"{1}\" since said " +
"faction is not present there", faction, context.CurrentSystem), e
);
}
}
if (!relevantBond) {
continue;
}
var voucher = new Vouchers(entry) {
System = context.CurrentSystem,
Station = context.CurrentStation,
Faction = relevantFaction,
ControllingFaction = context.ControllingFaction,
IsLegacy = context.IsLegacy,
};
if (options.FilterDoubleRedeemVouchers) {
// To filter out doubly redeemed vouchers, find another redeem voucher
// event that happened in the same system, same total sum, and also the
// same faction. If there is one, filter this one out.
var doubledEntry = transactions
.OfType<Vouchers>()
.Where(x => x.TotalSum == voucher.TotalSum &&
x.System == voucher.System &&
x.Faction == voucher.Faction)
.ToList()
;
if (doubledEntry.Count > 0) {
transactions.AddIncomplete(
voucher,
string.Format("A doubled redeem voucher for {0} valued {1} was detected",
voucher.Faction, Credits.FormatMillions(voucher.TotalSum)),
e);
return;
}
}
transactions.Add(voucher);
}
}
}

View File

@ -0,0 +1,33 @@
using EDPlayerJournal.Entries;
namespace EDPlayerJournal.BGS;
internal class SellExplorationDataParser : ITransactionParserPart {
public void Parse(Entry e, TransactionParserContext context, TransactionParserOptions options, TransactionList transactions) {
SellExplorationDataEntry? entry = e as SellExplorationDataEntry;
if (entry == null) {
throw new NotImplementedException();
}
if (context.StationOwner == null) {
transactions.AddIncomplete(
new Cartographics(),
"Could not discern the station owner for cartographics sell.",
e);
return;
}
// Ignore if its a fleet carrier faction.
if (string.Compare(context.StationOwner, Factions.FleetCarrier, StringComparison.OrdinalIgnoreCase) == 0 &&
options.IgnoreFleetCarrierFaction) {
return;
}
transactions.Add(new Cartographics(entry) {
System = context.CurrentSystem,
Station = context.CurrentStation,
Faction = context.StationOwner,
IsLegacy = context.IsLegacy,
});
}
}

View File

@ -0,0 +1,38 @@
using EDPlayerJournal.Entries;
namespace EDPlayerJournal.BGS;
internal class SellOrganicDataParser : ITransactionParserPart {
public void Parse(Entry e, TransactionParserContext context, TransactionParserOptions options, TransactionList transactions) {
// If exo biology is ignored, simply do nothing
if (options.IgnoreExoBiology) {
return;
}
SellOrganicDataEntry? entry = e as SellOrganicDataEntry;
if (entry == null) {
throw new NotImplementedException();
}
if (context.StationOwner == null) {
transactions.AddIncomplete(
new OrganicData(),
"Could not discern the station owner for organic data sell.",
e);
return;
}
// Ignore if its a fleet carrier faction.
if (string.Compare(context.StationOwner, Factions.FleetCarrier, StringComparison.OrdinalIgnoreCase) == 0 &&
options.IgnoreFleetCarrierFaction) {
return;
}
transactions.Add(new OrganicData(entry) {
System = context.CurrentSystem,
Station = context.CurrentStation,
Faction = context.StationOwner,
IsLegacy = context.IsLegacy,
});
}
}

View File

@ -0,0 +1,65 @@
using EDPlayerJournal.Entries;
namespace EDPlayerJournal.BGS;
/// <summary>
/// With ship targeted we might find out to which faction a given NPC belonged. This is
/// useful later when said NPC gets killed or murdered.
/// </summary>
internal class ShipTargetedParser : ITransactionParserPart {
public void Parse(Entry e, TransactionParserContext context, TransactionParserOptions options, TransactionList transactions) {
ShipTargetedEntry? entry = e as ShipTargetedEntry;
if (entry == null) {
throw new NotImplementedException();
}
// Scan happens in stages, and sometimes this information is not known
// yet. Do now throw an error, this is expected behaviour.
if (!string.IsNullOrEmpty(entry.PilotNameLocalised) &&
!string.IsNullOrEmpty(entry.Faction)) {
context.NPCFaction.TryAdd(entry.PilotNameLocalised, entry.Faction);
}
string? faction = context.LastRecordedAwardingFaction;
// We have seen a captain?
if (NPCs.IsWarzoneCaptain(entry.PilotName)) {
// if we have faction information, we can compare it to figure out
// whether it is the enemy or allied faction. but this is not always
// possible. In such a case we assume we have seen the enemy captain.
if (!string.IsNullOrEmpty(entry.Faction) &&
!string.IsNullOrEmpty(faction)) {
if (string.Compare(faction, entry.Faction) != 0) {
context.HaveSeenEnemyCaptain = true;
} else {
context.HaveSeenAlliedCaptain = true;
}
} else {
context.HaveSeenEnemyCaptain = true;
}
}
// Spec ops?
if (NPCs.IsSpecOps(entry.PilotName)) {
context.HaveSeenSpecOps = true;
}
// Correspondent?
if (NPCs.IsWarzoneCorrespondent(entry.PilotName)) {
// if we have faction information, we can compare it to figure out
// whether it is the enemy or allied faction. but this is not always
// possible. In such a case we assume we have seen the enemy
// correspondent.
if (!string.IsNullOrEmpty(entry.Faction) &&
!string.IsNullOrEmpty(faction)) {
if (string.Compare(faction, entry.Faction) != 0) {
context.HaveSeenEnemyCorrespondent = true;
} else {
context.HaveSeenAlliedCorrespondent = true;
}
} else {
context.HaveSeenEnemyCorrespondent = true;
}
}
}
}

View File

@ -0,0 +1,14 @@
using EDPlayerJournal.Entries;
namespace EDPlayerJournal.BGS.Parsers;
internal class SupercruiseDestinationDropParser : ITransactionParserPart {
public void Parse(Entry entry, TransactionParserContext context, TransactionParserOptions options, TransactionList transactions) {
SupercruiseDestinationDropEntry? drop = entry as SupercruiseDestinationDropEntry;
if (drop == null || drop.Type == null) {
return;
}
context.CurrentInstanceType = drop.Type;
}
}

View File

@ -49,10 +49,11 @@ public class SellCargo : Transaction {
}
foreach (MarketSellEntry sell in sold) {
builder.AppendFormat("Sold {0} {1} to the {2}",
builder.AppendFormat("Sold {0} {1} to the {2} of {3}",
sell.Count,
Cargo,
Market
Market,
Station
);
if (Profit != 0) {

View File

@ -1,220 +1,47 @@
using EDPlayerJournal;
using EDPlayerJournal.BGS.Parsers;
using EDPlayerJournal.Entries;
namespace EDPlayerJournal.BGS;
namespace EDPlayerJournal.BGS;
internal class TransactionParserContext {
public string? CurrentSystem { get; set; }
public ulong? CurrentSystemAddress { get; set; }
public string? CurrentStation { get; set; }
public string? ControllingFaction { get; set; }
public bool IsOnFoot { get; set; } = false;
public string? LastRecordedAwardingFaction { get; set; }
public ulong? HighestCombatBond { get; set; }
public bool HaveSeenCapShip { get; set; } = false;
public bool HaveSeenCaptain { get; set; } = false;
public bool HaveSeenSpecOps { get; set; } = false;
public bool HaveSeenCorrespondent { get; set; } = false;
public class TransactionParserOptions {
/// <summary>
/// Whether to ignore exo biology. It does not contribute to BGS, so this
/// is true per default.
/// </summary>
public bool IgnoreExoBiology { get; set; } = true;
/// <summary>
/// Returns true if the current session is legacy
/// Whether to ignore influence support. Usually one only cares about the
/// primary faction for the influence.
/// </summary>
public bool IsLegacy { get; set; } = false;
public bool IgnoreInfluenceSupport { get; set; } = false;
/// <summary>
/// How many on foot kills were done.
/// Whether we ignore power play and merits gained for now. Support for this
/// is experimental at the moment, so that is why it ist `true`.
/// </summary>
public ulong OnFootKills { get; set; } = 0;
public bool IgnorePowerplay { get; set; } = true;
/// <summary>
/// How many ship kills were done.
/// Whether to ignore market buy. Buying from a market gives a small amount
/// of INF if it is sold to a high demand market, but generally one buys from
/// a market to aid the faction the stuff is being sold to. So allow it to be
/// disabled.
/// </summary>
public ulong ShipKills { get; set; } = 0;
public bool IgnoreMarketBuy { get; set; } = false;
/// <summary>
/// Thargoid scouts killed
/// Whether we should ignore things done for the fleet carrier faction.
/// </summary>
public ulong ThargoidScoutKills { get; set; } = 0;
public bool IgnoreFleetCarrierFaction { get; set; } = true;
/// <summary>
/// Thargoid interceptor kills
/// Filter out double redeem vouchers that happen when you redeem a specific
/// voucher, and then redeem the rest of your vouchers (say from a KWS) in
/// bulk. The bulk redeem will also list the first voucher redeem again in
/// its bulk list.
/// </summary>
public ulong ThargoidInterceptorKills { get; set; } = 0;
/// <summary>
/// Whether we have seen an AX warzone NPC talk to us with ReceiveText
/// </summary>
public bool HaveSeenAXWarzoneNPC { get; set; } = false;
/// <summary>
/// A list of accepted missions index by their mission ID
/// </summary>
public Dictionary<ulong, Mission> AcceptedMissions { get; } = new();
public Dictionary<ulong, Location> AcceptedMissionLocation { get; } = new();
/// <summary>
/// A way to lookup a system by its system id
/// </summary>
public Dictionary<ulong, string> SystemsByID { get; } = new();
/// <summary>
/// A list of factions present in the given star system
/// </summary>
public Dictionary<string, List<Faction>> SystemFactions { get; } = new();
/// <summary>
/// To which faction a given named NPC belonged to.
/// </summary>
public Dictionary<string, string> NPCFaction { get; } = new();
/// <summary>
/// Buy costs for a given commodity
/// </summary>
public Dictionary<string, long> BuyCost = new();
public void DiscernCombatZone(TransactionList transactions, Entry e) {
string? grade = CombatZones.DifficultyLow;
string cztype;
ulong highest = HighestCombatBond ?? 0;
string? faction = LastRecordedAwardingFaction;
if (HighestCombatBond == null &&
LastRecordedAwardingFaction == null &&
HaveSeenAXWarzoneNPC == false) {
return;
}
if (OnFootKills > 0 || IsOnFoot == true) {
cztype = CombatZones.GroundCombatZone;
// High on foot combat zones have enforcers that bring 80k a pop
if (highest >= 60000) {
grade = CombatZones.DifficultyHigh;
} else if (highest >= 30000) {
// In medium conflict zones, the enforcers are worth 30k
grade = CombatZones.DifficultyMedium;
} else {
grade = CombatZones.DifficultyLow;
}
} else if (ShipKills > 0 && !IsOnFoot) {
// Ship combat zones can be identified by the amount of kills
if (ShipKills > 20) {
grade = CombatZones.DifficultyHigh;
} else if (ShipKills > 10) {
grade = CombatZones.DifficultyMedium;
}
// Cap ship, means a high conflict zone
if (HaveSeenCapShip) {
grade = CombatZones.DifficultyHigh;
} else {
int warzoneNpcs = new List<bool>() { HaveSeenCaptain, HaveSeenCorrespondent, HaveSeenSpecOps }
.Where(x => x == true)
.Count()
;
if (warzoneNpcs >= 1 && grade == CombatZones.DifficultyLow) {
grade = CombatZones.DifficultyMedium;
}
}
cztype = CombatZones.ShipCombatZone;
} else if ((ThargoidScoutKills > 0 && ThargoidInterceptorKills > 0) ||
HaveSeenAXWarzoneNPC == true) {
// Could be a thargoid combat zones if interceptors and scouts are there
cztype = CombatZones.AXCombatZone;
// Still unknown
grade = null;
} else {
transactions.AddIncomplete(new CombatZone(), "Failed to discern combat zone type", e);
return;
}
CombatZone zone = new CombatZone() {
System = CurrentSystem,
Faction = faction,
IsLegacy = IsLegacy,
Grade = grade,
Type = cztype,
// Sad truth is, if HaveSeenXXX is false, we just don't know for certain
CapitalShip = HaveSeenCapShip ? true : null,
SpecOps = HaveSeenSpecOps ? true : null,
Correspondent = HaveSeenCorrespondent ? true : null,
Captain = HaveSeenCaptain ? true : null,
};
zone.Entries.Add(e);
transactions.Add(zone);
}
public void RecordCombatBond(FactionKillBondEntry e) {
if (HighestCombatBond == null || e.Reward > HighestCombatBond) {
HighestCombatBond = e.Reward;
}
LastRecordedAwardingFaction = e.AwardingFaction;
if (IsOnFoot) {
++OnFootKills;
} else {
++ShipKills;
}
}
public void ResetCombatZone() {
HighestCombatBond = null;
HaveSeenCapShip = false;
HaveSeenCaptain = false;
HaveSeenCorrespondent = false;
HaveSeenSpecOps = false;
LastRecordedAwardingFaction = null;
OnFootKills = 0;
ShipKills = 0;
ThargoidInterceptorKills = 0;
ThargoidScoutKills = 0;
HaveSeenAXWarzoneNPC = false;
}
public void BoughtCargo(string? cargo, long? cost) {
if (cargo == null || cost == null) {
return;
}
BuyCost[cargo] = cost.Value;
}
public List<Faction>? GetFactions(string? system) {
if (system == null || !SystemFactions.ContainsKey(system)) {
return null;
}
return SystemFactions[system];
}
public void MissionAccepted(MissionAcceptedEntry? entry) {
if (entry == null) {
return;
}
MissionAccepted(entry.Mission);
}
public void MissionAccepted(Mission? mission) {
if (CurrentSystem == null || CurrentSystemAddress == null) {
throw new Exception("Mission accepted without knowing where.");
}
if (mission == null) {
throw new Exception("Mission is null");
}
AcceptedMissions.TryAdd(mission.MissionID, mission);
Location location = new() {
StarSystem = CurrentSystem,
SystemAddress = CurrentSystemAddress.Value,
Station = (CurrentStation ?? ""),
};
AcceptedMissionLocation.TryAdd(mission.MissionID, location);
}
public bool FilterDoubleRedeemVouchers { get; set; } = true;
}
public class TransactionList : List<Transaction> {
@ -223,29 +50,13 @@ public class TransactionList : List<Transaction> {
}
}
internal interface TransactionParserPart{
/// <summary>
/// Parse a given entry of the entry type specified when declaring to implement this
/// interface. You must add your transaction to the passed list in case you did have
/// enough information to parse one or more. You may update the parser context
/// with new information in case the entry yielded new information.
/// Throw an exception if there was an error or a malformed entry. If you have an
/// incomplete entry, i.e. not enough information to complete one, add an
/// IncompleteTransaction to the list
/// </summary>
/// <param name="entry">The entry to parse</param>
/// <param name="context">Parsing context that may contain useful information</param>
/// <param name="transactions">List of parsed transactions</param>
public void Parse(Entry entry, TransactionParserContext context, TransactionList transactions);
}
/// <summary>
/// The location parser only updates the context with useful information, and does not
/// by itself generate any transactions. Location is the best information gatherer here
/// as we are getting controlling faction, system factions, address and station name.
/// </summary>
internal class LocationParser : TransactionParserPart {
public void Parse(Entry e, TransactionParserContext context, TransactionList transactions) {
internal class LocationParser : ITransactionParserPart {
public void Parse(Entry e, TransactionParserContext context, TransactionParserOptions options, TransactionList transactions) {
LocationEntry? entry = e as LocationEntry;
if (entry == null) {
throw new NotImplementedException();
@ -268,15 +79,20 @@ internal class LocationParser : TransactionParserPart {
context.CurrentStation = entry.StationName;
}
if (!context.SystemFactions.ContainsKey(entry.StarSystem) &&
entry.SystemFactions != null && entry.SystemFactions.Count > 0) {
if (!string.IsNullOrEmpty(entry.StationFaction)) {
context.StationOwner = entry.StationFaction;
} else {
context.StationOwner = null;
}
if (entry.SystemFactions != null && entry.SystemFactions.Count > 0) {
context.SystemFactions[entry.StarSystem] = entry.SystemFactions;
}
}
}
internal class FSDJumpParser : TransactionParserPart {
public void Parse(Entry e, TransactionParserContext context, TransactionList transactions) {
internal class FSDJumpParser : ITransactionParserPart {
public void Parse(Entry e, TransactionParserContext context, TransactionParserOptions options, TransactionList transactions) {
FSDJumpEntry? entry = e as FSDJumpEntry;
if (entry == null) {
throw new NotImplementedException();
@ -292,6 +108,8 @@ internal class FSDJumpParser : TransactionParserPart {
context.DiscernCombatZone(transactions, e);
context.ResetCombatZone();
context.LeftInstance();
context.CurrentSystem = entry.StarSystem;
context.CurrentSystemAddress = entry.SystemAddress;
@ -301,15 +119,14 @@ internal class FSDJumpParser : TransactionParserPart {
context.ControllingFaction = entry.SystemFaction;
}
if (!context.SystemFactions.ContainsKey(entry.StarSystem) &&
entry.SystemFactions != null && entry.SystemFactions.Count > 0) {
if (entry.SystemFactions != null && entry.SystemFactions.Count > 0) {
context.SystemFactions[entry.StarSystem] = entry.SystemFactions;
}
}
}
internal class DockedParser : TransactionParserPart {
public void Parse(Entry e, TransactionParserContext context, TransactionList transactions) {
internal class DockedParser : ITransactionParserPart {
public void Parse(Entry e, TransactionParserContext context, TransactionParserOptions options, TransactionList transactions) {
DockedEntry? entry = e as DockedEntry;
if (entry == null) {
throw new NotImplementedException();
@ -325,7 +142,7 @@ internal class DockedParser : TransactionParserPart {
context.SystemsByID.TryAdd(entry.SystemAddress.Value, entry.StarSystem);
if (!string.IsNullOrEmpty(entry.StationFaction)) {
context.ControllingFaction = entry.StationFaction;
context.StationOwner = entry.StationFaction;
}
if (!string.IsNullOrEmpty(entry.StationName)) {
@ -334,47 +151,12 @@ internal class DockedParser : TransactionParserPart {
}
}
/// <summary>
/// With ship targeted we might find out to which faction a given NPC belonged. This is
/// useful later when said NPC gets killed or murdered.
/// </summary>
internal class ShipTargetedParser : TransactionParserPart {
public void Parse(Entry e, TransactionParserContext context, TransactionList transactions) {
ShipTargetedEntry? entry = e as ShipTargetedEntry;
if (entry == null) {
throw new NotImplementedException();
}
// Scan happens in stages, and sometimes this information is not known
// yet. Do now throw an error, this is expected behaviour.
if (!string.IsNullOrEmpty(entry.PilotNameLocalised) &&
!string.IsNullOrEmpty(entry.Faction)) {
context.NPCFaction.TryAdd(entry.PilotNameLocalised, entry.Faction);
}
// We have seen a captain?
if (NPCs.IsWarzoneCaptain(entry.PilotName)) {
context.HaveSeenCaptain = true;
}
// Spec ops?
if (NPCs.IsSpecOps(entry.PilotName)) {
context.HaveSeenSpecOps = true;
}
// Correspondent?
if (NPCs.IsWarzoneCorrespondent(entry.PilotName)) {
context.HaveSeenCorrespondent = true;
}
}
}
/// <summary>
/// Commit crime can result in a transaction, especially if the crime committed is
/// murder.
/// </summary>
internal class CommitCrimeParser : TransactionParserPart {
public void Parse(Entry e, TransactionParserContext context, TransactionList transactions) {
internal class CommitCrimeParser : ITransactionParserPart {
public void Parse(Entry e, TransactionParserContext context, TransactionParserOptions options, TransactionList transactions) {
CommitCrimeEntry? entry = e as CommitCrimeEntry;
if (entry == null) {
throw new NotImplementedException();
@ -429,8 +211,8 @@ internal class CommitCrimeParser : TransactionParserPart {
}
}
internal class MissionsParser : TransactionParserPart {
public void Parse(Entry entry, TransactionParserContext context, TransactionList transactions) {
internal class MissionsParser : ITransactionParserPart {
public void Parse(Entry entry, TransactionParserContext context, TransactionParserOptions options, TransactionList transactions) {
MissionsEntry? missions = entry as MissionsEntry;
if (missions == null) {
@ -458,8 +240,8 @@ internal class MissionsParser : TransactionParserPart {
}
}
internal class MissionAcceptedParser : TransactionParserPart {
public void Parse(Entry e, TransactionParserContext context, TransactionList transactions) {
internal class MissionAcceptedParser : ITransactionParserPart {
public void Parse(Entry e, TransactionParserContext context, TransactionParserOptions options, TransactionList transactions) {
MissionAcceptedEntry? entry = e as MissionAcceptedEntry;
if (entry == null) {
throw new NotImplementedException();
@ -484,15 +266,15 @@ internal class MissionAcceptedParser : TransactionParserPart {
}
}
internal class MissionCompletedParser : TransactionParserPart {
public void Parse(Entry e, TransactionParserContext context, TransactionList transactions) {
internal class MissionCompletedParser : ITransactionParserPart {
public void Parse(Entry e, TransactionParserContext context, TransactionParserOptions options, TransactionList transactions) {
MissionCompletedEntry? entry = e as MissionCompletedEntry;
if (entry == null || entry.Mission == null) {
throw new NotImplementedException();
}
Mission? mission = null;
Location? accepted_location = null;
Mission? mission;
Location? accepted_location;
string? target_faction_name = entry.Mission.TargetFaction;
string? source_faction_name = entry.Mission.Faction;
@ -530,20 +312,20 @@ internal class MissionCompletedParser : TransactionParserPart {
if (context.CurrentSystemAddress == null) {
continue;
}
other.Value.Add(context.CurrentSystemAddress.Value, "");
other.Value.Add(context.CurrentSystemAddress.Value, new MissionInfluence());
// Mission gave no influence to the target faction, so we assume
// the target faction was in the same system.
} else if (string.Compare(source_faction_name, faction, true) == 0) {
// This happens if the source faction is not getting any influence
// This could be if the source faction is in a conflict, and thus does
// not gain any influence at all.
other.Value.Add(accepted_location.SystemAddress, "");
other.Value.Add(accepted_location.SystemAddress, new MissionInfluence());
// Just check if the target/source faction are the same, in which case
// we also have to make an additional entry
if (string.Compare(source_faction_name, target_faction_name, true) == 0 &&
context.CurrentSystemAddress != null) {
other.Value.Add(context.CurrentSystemAddress.Value, "");
other.Value.Add(context.CurrentSystemAddress.Value, new MissionInfluence());
}
}
}
@ -577,6 +359,10 @@ internal class MissionCompletedParser : TransactionParserPart {
} else if (string.Compare(faction, source_faction_name, true) != 0 ||
(string.Compare(faction, source_faction_name) == 0 &&
system_address != accepted_location.SystemAddress)) {
// Whether we ignore influence support
if (options.IgnoreInfluenceSupport) {
continue;
}
// Source or target faction are different, and/or the system
// differs. Sometimes missions go to different systems but to
// the same faction.
@ -595,11 +381,11 @@ internal class MissionCompletedParser : TransactionParserPart {
}
}
internal class MissionFailedParser : TransactionParserPart {
public void Parse(Entry e, TransactionParserContext context, TransactionList transactions) {
Mission? mission = null;
Location? accepted_location = null;
string? accepted_system = null;
internal class MissionFailedParser : ITransactionParserPart {
public void Parse(Entry e, TransactionParserContext context, TransactionParserOptions options, TransactionList transactions) {
Mission? mission;
Location? accepted_location;
string? accepted_system;
MissionFailedEntry? entry = e as MissionFailedEntry;
if (entry == null) {
@ -650,191 +436,56 @@ internal class MissionFailedParser : TransactionParserPart {
}
}
internal class SellExplorationDataParser : TransactionParserPart {
public void Parse(Entry e, TransactionParserContext context, TransactionList transactions) {
SellExplorationDataEntry? entry = e as SellExplorationDataEntry;
if (entry == null) {
throw new NotImplementedException();
}
transactions.Add(new Cartographics(entry) {
System = context.CurrentSystem,
Station = context.CurrentStation,
Faction = context.ControllingFaction,
IsLegacy = context.IsLegacy,
});
}
}
internal class SellOrganicDataParser : TransactionParserPart {
public void Parse(Entry e, TransactionParserContext context, TransactionList transactions) {
SellOrganicDataEntry? entry = e as SellOrganicDataEntry;
if (entry == null) {
throw new NotImplementedException();
}
transactions.Add(new OrganicData(entry) {
System = context.CurrentSystem,
Station = context.CurrentStation,
Faction = context.ControllingFaction,
IsLegacy = context.IsLegacy,
});
}
}
internal class MultiSellExplorationDataParser : TransactionParserPart {
public void Parse(Entry e, TransactionParserContext context, TransactionList transactions) {
MultiSellExplorationDataEntry? entry = e as MultiSellExplorationDataEntry;
if (entry == null) {
throw new NotImplementedException();
}
transactions.Add(new Cartographics(entry) {
System = context.CurrentSystem,
Station = context.CurrentStation,
Faction = context.ControllingFaction,
IsLegacy = context.IsLegacy,
});
}
}
internal class RedeemVoucherParser : TransactionParserPart {
public void Parse(Entry e, TransactionParserContext context, TransactionList transactions) {
RedeemVoucherEntry? entry = e as RedeemVoucherEntry;
if (entry == null) {
throw new NotImplementedException();
}
if (context.CurrentSystem == null) {
transactions.AddIncomplete(new Vouchers(),
"Could not find out where the vouchers were redeemed", e
);
return;
}
List<Faction>? current_factions = context.GetFactions(context.CurrentSystem);
if (current_factions == null) {
transactions.AddIncomplete(new Vouchers(),
"Current system factions are unknown, so vouchers were ineffective", e);
}
foreach (string faction in entry.Factions) {
bool relevantBond = false;
string relevantFaction = faction;
if (string.Compare(faction, Factions.PilotsFederationVouchers) == 0) {
// Target faction is pilots' federation, so we assume thargoid bonks
// Also assign this combat bond to the Pilots Federation
relevantFaction = Factions.PilotsFederation;
relevantBond = true;
}
if (current_factions != null && !relevantBond) {
// If we have local factions, and it ain't thargoid bonds see if the bonds were
// useful in the current system
if (current_factions.Find(x => string.Compare(x.Name, faction, true) == 0) != null) {
relevantBond = true;
} else {
transactions.AddIncomplete(new Vouchers(),
string.Format("Vouchers for \"{0}\" had no effect in \"{1}\" since said " +
"faction is not present there", faction, context.CurrentSystem), e
);
}
}
if (relevantBond) {
transactions.Add(new Vouchers(entry) {
System = context.CurrentSystem,
Station = context.CurrentStation,
Faction = relevantFaction,
ControllingFaction = context.ControllingFaction,
IsLegacy = context.IsLegacy,
});
}
}
}
}
internal class SellMicroResourcesParser : TransactionParserPart {
public void Parse(Entry e, TransactionParserContext context, TransactionList transactions) {
internal class SellMicroResourcesParser : ITransactionParserPart {
public void Parse(Entry e, TransactionParserContext context, TransactionParserOptions options, TransactionList transactions) {
SellMicroResourcesEntry? entry = e as SellMicroResourcesEntry;
if (entry == null) {
throw new NotImplementedException();
}
if (context.StationOwner == null) {
transactions.AddIncomplete(
new SellMicroResources(),
"Could not discern the station owner for micro resources sell.",
e);
return;
}
transactions.Add(new SellMicroResources(entry) {
System = context.CurrentSystem,
Station = context.CurrentStation,
Faction = context.ControllingFaction,
Faction = context.StationOwner,
IsLegacy = context.IsLegacy,
});
}
}
internal class SearchAndRescueParser : TransactionParserPart {
public void Parse(Entry e, TransactionParserContext context, TransactionList transactions) {
internal class SearchAndRescueParser : ITransactionParserPart {
public void Parse(Entry e, TransactionParserContext context, TransactionParserOptions options, TransactionList transactions) {
SearchAndRescueEntry? entry = e as SearchAndRescueEntry;
if (entry == null) {
throw new NotImplementedException();
}
if (context.StationOwner == null) {
transactions.AddIncomplete(
new OrganicData(),
"Could not discern the station owner for S&R operations.",
e);
return;
}
transactions.Add(new SearchAndRescue(entry) {
Faction = context.ControllingFaction,
Station = context.CurrentStation,
System = context.CurrentSystem,
Station = context.CurrentStation,
Faction = context.StationOwner,
IsLegacy = context.IsLegacy,
});
}
}
internal class MarketBuyParser : TransactionParserPart {
public void Parse(Entry e, TransactionParserContext context, TransactionList transactions) {
MarketBuyEntry? entry = e as MarketBuyEntry;
if (entry == null) {
throw new NotImplementedException();
}
context.BoughtCargo(entry.Type, entry.BuyPrice);
transactions.Add(new BuyCargo(entry) {
Faction = context.ControllingFaction,
Station = context.CurrentStation,
System = context.CurrentSystem,
IsLegacy = context.IsLegacy,
});
}
}
internal class MarketSellParser : TransactionParserPart {
public void Parse(Entry e, TransactionParserContext context, TransactionList transactions) {
long profit = 0;
MarketSellEntry? entry = e as MarketSellEntry;
if (entry == null) {
throw new NotImplementedException();
}
if (entry.Type == null) {
throw new InvalidJournalEntryException("market sell contains no cargo type");
}
if (context.BuyCost.ContainsKey(entry.Type)) {
long avg = context.BuyCost[entry.Type];
profit = (long)entry.TotalSale - (avg * entry.Count);
}
transactions.Add(new SellCargo(entry) {
Faction = context.ControllingFaction,
Station = context.CurrentStation,
System = context.CurrentSystem,
Profit = profit,
IsLegacy = context.IsLegacy,
});
}
}
internal class FactionKillBondParser : TransactionParserPart {
public void Parse(Entry e, TransactionParserContext context, TransactionList transactions) {
internal class FactionKillBondParser : ITransactionParserPart {
public void Parse(Entry e, TransactionParserContext context, TransactionParserOptions options, TransactionList transactions) {
FactionKillBondEntry? entry = e as FactionKillBondEntry;
if (entry == null) {
throw new NotImplementedException();
@ -866,8 +517,8 @@ internal class FactionKillBondParser : TransactionParserPart {
}
}
internal class EmbarkDisembarkParser : TransactionParserPart {
public void Parse(Entry e, TransactionParserContext context, TransactionList transactions) {
internal class EmbarkDisembarkParser : ITransactionParserPart {
public void Parse(Entry e, TransactionParserContext context, TransactionParserOptions options, TransactionList transactions) {
if (e.Is(Events.Embark)) {
context.IsOnFoot = false;
} else if (e.Is(Events.Disembark)) {
@ -876,24 +527,28 @@ internal class EmbarkDisembarkParser : TransactionParserPart {
}
}
internal class SupercruiseEntryParser : TransactionParserPart {
public void Parse(Entry entry, TransactionParserContext context, TransactionList transactions) {
internal class SupercruiseEntryParser : ITransactionParserPart {
public void Parse(Entry entry, TransactionParserContext context, TransactionParserOptions options, TransactionList transactions) {
// After a super cruise entry we are no longer on foot.
context.IsOnFoot = false;
context.DiscernCombatZone(transactions, entry);
context.ResetCombatZone();
// Supercruise entry means you left the current local instance
context.LeftInstance();
}
}
internal class ShutdownParser : TransactionParserPart {
public void Parse(Entry entry, TransactionParserContext context, TransactionList transactions) {
internal class ShutdownParser : ITransactionParserPart {
public void Parse(Entry entry, TransactionParserContext context, TransactionParserOptions options, TransactionList transactions) {
context.DiscernCombatZone(transactions, entry);
context.ResetCombatZone();
// Shutdown (logout) means you left the instance
context.LeftInstance();
}
}
internal class CapShipBondParser : TransactionParserPart {
public void Parse(Entry entry, TransactionParserContext context, TransactionList transactions) {
internal class CapShipBondParser : ITransactionParserPart {
public void Parse(Entry entry, TransactionParserContext context, TransactionParserOptions options, TransactionList transactions) {
if (entry.GetType() != typeof(CapShipBondEntry)) {
return;
}
@ -902,8 +557,8 @@ internal class CapShipBondParser : TransactionParserPart {
}
}
internal class FileHeaderParser : TransactionParserPart {
public void Parse(Entry entry, TransactionParserContext context, TransactionList transactions) {
internal class FileHeaderParser : ITransactionParserPart {
public void Parse(Entry entry, TransactionParserContext context, TransactionParserOptions options, TransactionList transactions) {
FileHeaderEntry? fileheader = entry as FileHeaderEntry;
if (fileheader == null) {
@ -914,8 +569,8 @@ internal class FileHeaderParser : TransactionParserPart {
}
}
internal class ReceiveTextParser : TransactionParserPart {
public void Parse(Entry entry, TransactionParserContext context, TransactionList transactions) {
internal class ReceiveTextParser : ITransactionParserPart {
public void Parse(Entry entry, TransactionParserContext context, TransactionParserOptions options, TransactionList transactions) {
ReceiveTextEntry? receivetext = entry as ReceiveTextEntry;
if (receivetext == null) {
@ -932,34 +587,44 @@ internal class ReceiveTextParser : TransactionParserPart {
}
}
internal class DiedParser : TransactionParserPart {
public void Parse(Entry entry, TransactionParserContext context, TransactionList transactions) {
// You can't complete a combat zone if you die in it. Others might keep it open
// for you, but still you will not have completed it unless you jump back in.
context.ResetCombatZone();
internal class SelfDestructParser : ITransactionParserPart {
public void Parse(Entry entry, TransactionParserContext context, TransactionParserOptions options, TransactionList transactions) {
context.SelfDestruct = true;
}
}
internal class DropshipDeployParser : TransactionParserPart {
public void Parse(Entry entry, TransactionParserContext context, TransactionList transactions) {
internal class DiedParser : ITransactionParserPart {
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.
if (context.IsOnFoot) {
return;
}
if (context.SelfDestruct != null && context.SelfDestruct == true) {
// 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();
// Dying also moves you back to another instance
context.LeftInstance();
}
}
internal class DropshipDeployParser : ITransactionParserPart {
public void Parse(Entry entry, TransactionParserContext context, TransactionParserOptions options, TransactionList transactions) {
// On drop ship deploy we are now on foot
context.IsOnFoot = true;
}
}
internal class CommanderParser : TransactionParserPart {
public void Parse(Entry entry, TransactionParserContext context, 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, TransactionParserPart> ParserParts { get; } = new()
private static Dictionary<string, ITransactionParserPart> ParserParts { get; } = new()
{
{ Events.ApproachSettlement, new ApproachSettlementParser() },
{ Events.CapShipBond, new CapShipBondParser() },
{ Events.CarrierJump, new CarrierJumpParser() },
{ Events.Commander, new CommanderParser() },
{ Events.CommitCrime, new CommitCrimeParser() },
{ Events.Died, new DiedParser() },
@ -978,14 +643,18 @@ public class TransactionParser {
{ Events.MissionFailed, new MissionFailedParser() },
{ Events.Missions, new MissionsParser() },
{ Events.MultiSellExplorationData, new MultiSellExplorationDataParser() },
{ Events.Music, new MusicParser() },
{ Events.PowerplayMerits, new PowerplayMeritsParser() },
{ Events.ReceiveText, new ReceiveTextParser() },
{ Events.RedeemVoucher, new RedeemVoucherParser() },
{ Events.SearchAndRescue, new SearchAndRescueParser() },
{ Events.SelfDestruct, new SelfDestructParser() },
{ Events.SellExplorationData, new SellExplorationDataParser() },
{ Events.SellMicroResources, new SellMicroResourcesParser() },
{ Events.SellOrganicData, new SellOrganicDataParser() },
{ Events.ShipTargeted, new ShipTargetedParser() },
{ Events.Shutdown, new ShutdownParser() },
{ Events.SupercruiseDestinationDrop, new SupercruiseDestinationDropParser() },
{ Events.SupercruiseEntry, new SupercruiseEntryParser() },
};
@ -1000,7 +669,22 @@ public class TransactionParser {
return IsRelevant(e.Event);
}
/// <summary>
/// Parses a list of entries with default options.
/// </summary>
/// <param name="entries"></param>
/// <returns></returns>
public List<Transaction>? Parse(IEnumerable<Entry> entries) {
TransactionParserOptions defaultOptions = new();
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();
@ -1013,10 +697,13 @@ public class TransactionParser {
continue;
}
TransactionParserPart transactionParserPart = ParserParts[entry.Event];
transactionParserPart.Parse(entry, context, transactions);
ITransactionParserPart transactionParserPart = ParserParts[entry.Event];
transactionParserPart.Parse(entry, context, options, transactions);
}
// Copy out list of commanders seen
Commanders = context.Commanders;
return transactions.ToList();
}
}

View File

@ -0,0 +1,354 @@
using EDPlayerJournal.Entries;
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>
public string? CurrentSystem { get; set; }
/// <summary>
/// 64 bit address of the current system.
/// </summary>
public ulong? CurrentSystemAddress { get; set; }
/// <summary>
/// Controlling faction of the current system.
/// </summary>
public string? ControllingFaction { get; set; }
/// <summary>
/// Name of the current station the player is docked at.
/// </summary>
public string? CurrentStation { get; set; }
/// <summary>
/// Faction that owns the current station.
/// </summary>
public string? StationOwner { get; set; }
/// <summary>
/// Whether the player is currently on foot, or in an SRV/ship.
/// </summary>
public bool IsOnFoot { get; set; } = false;
/// <summary>
/// Type of the current instance after a SupercruiseDestinationDropEntry.
/// This is null if there is no current instance, or the current instance
/// is not known.
/// </summary>
public string? CurrentInstanceType { get; set; } = null;
/// <summary>
/// Last faction that awarded the player with combat bonds.
/// </summary>
public string? LastRecordedAwardingFaction { get; set; }
/// <summary>
/// Highest combat bond seen so far.
/// </summary>
public ulong? HighestCombatBond { get; set; }
public bool HaveSeenCapShip { get; set; } = false;
public bool HaveSeenAlliedCaptain { get; set; } = false;
public bool HaveSeenEnemyCaptain { get; set; } = false;
public bool HaveSeenSpecOps { get; set; } = false;
public bool HaveSeenAlliedCorrespondent { get; set; } = false;
public bool HaveSeenEnemyCorrespondent { get; set; } = false;
public bool? SelfDestruct { get; set; } = null;
/// <summary>
/// Current Odyssey settlement.
/// </summary>
public string? Settlement { get; set; } = null;
/// <summary>
/// Current Merits
/// </summary>
public long? CurrentMerits { get; set; } = null;
/// <summary>
/// Merits from last login
/// </summary>
public long? LastMerits { get; set; } = null;
/// <summary>
/// Returns true if the current session is legacy
/// </summary>
public bool IsLegacy { get; set; } = false;
/// <summary>
/// How many on foot kills were done.
/// </summary>
public ulong OnFootKills { get; set; } = 0;
/// <summary>
/// How many ship kills were done.
/// </summary>
public ulong ShipKills { get; set; } = 0;
/// <summary>
/// Thargoid scouts killed
/// </summary>
public ulong ThargoidScoutKills { get; set; } = 0;
/// <summary>
/// Thargoid interceptor kills
/// </summary>
public ulong ThargoidInterceptorKills { get; set; } = 0;
/// <summary>
/// Whether we have seen an AX warzone NPC talk to us with ReceiveText
/// </summary>
public bool HaveSeenAXWarzoneNPC { get; set; } = false;
/// <summary>
/// A list of accepted missions index by their mission ID
/// </summary>
public Dictionary<ulong, Mission> AcceptedMissions { get; } = new();
public Dictionary<ulong, Location> AcceptedMissionLocation { get; } = new();
/// <summary>
/// A way to lookup a system by its system id
/// </summary>
public Dictionary<ulong, string> SystemsByID { get; } = new();
/// <summary>
/// A list of factions present in the given star system
/// </summary>
public Dictionary<string, List<Faction>> SystemFactions { get; } = new();
/// <summary>
/// To which faction a given named NPC belonged to.
/// </summary>
public Dictionary<string, string> NPCFaction { get; } = new();
/// <summary>
/// Buy costs for a given commodity
/// </summary>
public Dictionary<string, long> BuyCost = new();
/// <summary>
/// Called when the player leaves an instance, either through logout, FSD jump or
/// supercruise instance.
/// </summary>
public void LeftInstance() {
CurrentInstanceType = 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) {
string? grade = CombatZones.DifficultyLow;
string cztype;
ulong highest = HighestCombatBond ?? 0;
string? faction = LastRecordedAwardingFaction;
if (!HadCombatZone()) {
return;
}
if (OnFootKills > 0 || IsOnFoot == true) {
cztype = CombatZones.GroundCombatZone;
// High on foot combat zones have enforcers that bring 80k a pop
if (highest >= 60000) {
grade = CombatZones.DifficultyHigh;
} else if (highest >= 30000) {
// In medium conflict zones, the enforcers are worth 30k
grade = CombatZones.DifficultyMedium;
} else {
grade = CombatZones.DifficultyLow;
}
} else if (CurrentInstanceType != null) {
if (!Instances.IsWarzone(CurrentInstanceType)) {
return;
}
if (LastRecordedAwardingFaction == null &&
(Instances.IsHumanWarzone(CurrentInstanceType) ||
Instances.IsPowerWarzone(CurrentInstanceType))) {
transactions.AddIncomplete(new CombatZone(),
"Could not discern for whom you fought for, " +
"as it seems you haven't killed anyone in the ship combat zone.",
e);
return;
}
// If we have information about the current instance being a warship use that
// information to determine the warzone.
if (Instances.IsInstance(CurrentInstanceType, Instances.WarzoneLow)) {
cztype = CombatZones.ShipCombatZone;
grade = CombatZones.DifficultyLow;
} else if (Instances.IsInstance(CurrentInstanceType, Instances.WarzoneMedium)) {
cztype = CombatZones.ShipCombatZone;
grade = CombatZones.DifficultyMedium;
} else if (Instances.IsInstance(CurrentInstanceType, Instances.WarzoneHigh)) {
cztype = CombatZones.ShipCombatZone;
grade = CombatZones.DifficultyHigh;
} else if (Instances.IsInstance(CurrentInstanceType, Instances.WarzoneThargoidLow)) {
cztype = CombatZones.AXCombatZone;
grade = CombatZones.DifficultyLow;
} else if (Instances.IsInstance(CurrentInstanceType, Instances.WarzoneThargoidMedium)) {
cztype = CombatZones.AXCombatZone;
grade = CombatZones.DifficultyMedium;
} else if (Instances.IsInstance(CurrentInstanceType, Instances.WarzoneThargoidHigh)) {
cztype = CombatZones.AXCombatZone;
grade = CombatZones.DifficultyHigh;
} else if (Instances.IsInstance(CurrentInstanceType, Instances.WarzoneThargoidVeryHigh)) {
cztype = CombatZones.AXCombatZone;
grade = CombatZones.DifficultyVeryHigh;
} else if (Instances.IsInstance(CurrentInstanceType, Instances.PowerWarzoneMedium)) {
cztype = CombatZones.PowerCombatZone;
grade = CombatZones.DifficultyMedium;
} else {
transactions.AddIncomplete(new CombatZone(),
"Unknown conflict zone difficulty",
e);
return;
}
} else if (ShipKills > 0 && !IsOnFoot) {
// Ship combat zones can be identified by the amount of kills
if (ShipKills > 20) {
grade = CombatZones.DifficultyHigh;
} else if (ShipKills > 10) {
grade = CombatZones.DifficultyMedium;
}
// Cap ship, means a high conflict zone
if (HaveSeenCapShip) {
grade = CombatZones.DifficultyHigh;
} else {
int warzoneNpcs = new List<bool>() {
HaveSeenEnemyCaptain,
HaveSeenEnemyCorrespondent,
HaveSeenSpecOps
}
.Where(x => x == true)
.Count()
;
if (warzoneNpcs >= 1 && grade == CombatZones.DifficultyLow) {
grade = CombatZones.DifficultyMedium;
}
}
cztype = CombatZones.ShipCombatZone;
} else if ((ThargoidScoutKills > 0 && ThargoidInterceptorKills > 0) ||
HaveSeenAXWarzoneNPC == true) {
// Could be a thargoid combat zones if interceptors and scouts are there
cztype = CombatZones.AXCombatZone;
// Still unknown
grade = null;
} else {
transactions.AddIncomplete(new CombatZone(), "Failed to discern combat zone type", e);
return;
}
CombatZone zone = new CombatZone() {
System = CurrentSystem,
Faction = faction,
IsLegacy = IsLegacy,
Settlement = Settlement,
Grade = grade,
Type = cztype,
// Sad truth is, if HaveSeenXXX is false, we just don't know for certain
CapitalShip = HaveSeenCapShip ? true : null,
SpecOps = HaveSeenSpecOps ? true : null,
EnemyCorrespondent = HaveSeenEnemyCorrespondent ? true : null,
AlliedCorrespondent = HaveSeenAlliedCorrespondent ? true : null,
EnemyCaptain = HaveSeenEnemyCaptain ? true : null,
AlliedCaptain = HaveSeenAlliedCaptain ? true : null,
};
zone.Entries.Add(e);
transactions.Add(zone);
}
public void RecordCombatBond(FactionKillBondEntry e) {
if (HighestCombatBond == null || e.Reward > HighestCombatBond) {
HighestCombatBond = e.Reward;
}
LastRecordedAwardingFaction = e.AwardingFaction;
if (IsOnFoot) {
++OnFootKills;
} else {
++ShipKills;
}
}
public void ResetCombatZone() {
HighestCombatBond = null;
HaveSeenCapShip = false;
HaveSeenAlliedCaptain = false;
HaveSeenEnemyCaptain = false;
HaveSeenAlliedCorrespondent = false;
HaveSeenEnemyCorrespondent = false;
HaveSeenSpecOps = false;
LastRecordedAwardingFaction = null;
OnFootKills = 0;
ShipKills = 0;
ThargoidInterceptorKills = 0;
ThargoidScoutKills = 0;
HaveSeenAXWarzoneNPC = false;
}
public void BoughtCargo(string? cargo, long? cost) {
if (cargo == null || cost == null) {
return;
}
BuyCost[cargo] = cost.Value;
}
public List<Faction>? GetFactions(string? system) {
if (system == null || !SystemFactions.ContainsKey(system)) {
return null;
}
return SystemFactions[system];
}
public void MissionAccepted(MissionAcceptedEntry? entry) {
if (entry == null) {
return;
}
MissionAccepted(entry.Mission);
}
public void MissionAccepted(Mission? mission) {
if (CurrentSystem == null || CurrentSystemAddress == null) {
throw new Exception("Mission accepted without knowing where.");
}
if (mission == null) {
throw new Exception("Mission is null");
}
AcceptedMissions.TryAdd(mission.MissionID, mission);
Location location = new() {
StarSystem = CurrentSystem,
SystemAddress = CurrentSystemAddress.Value,
Station = (CurrentStation ?? ""),
};
AcceptedMissionLocation.TryAdd(mission.MissionID, location);
}
}

View File

@ -0,0 +1,19 @@
using EDPlayerJournal.Entries;
namespace EDPlayerJournal.BGS;
internal interface ITransactionParserPart {
/// <summary>
/// Parse a given entry of the entry type specified when declaring to implement this
/// interface. You must add your transaction to the passed list in case you did have
/// enough information to parse one or more. You may update the parser context
/// with new information in case the entry yielded new information.
/// Throw an exception if there was an error or a malformed entry. If you have an
/// incomplete entry, i.e. not enough information to complete one, add an
/// IncompleteTransaction to the list
/// </summary>
/// <param name="entry">The entry to parse</param>
/// <param name="context">Parsing context that may contain useful information</param>
/// <param name="transactions">List of parsed transactions</param>
public void Parse(Entry entry, TransactionParserContext context, TransactionParserOptions options, TransactionList transactions);
}

View File

@ -13,16 +13,6 @@ public class Vouchers : Transaction {
Entries.Add(e);
}
public override bool SystemContribution {
get {
if (Faction == Factions.PilotsFederation && Type == "Combat Bond") {
return true;
}
return false;
}
}
public long TotalSum {
get {
if (Faction == null) {

View File

@ -14,6 +14,11 @@ public class CombatZones {
/// </summary>
public static readonly string ShipCombatZone = "Ship";
/// <summary>
/// Power combat zones, new in Ascendancy update.
/// </summary>
public static readonly string PowerCombatZone = "Power";
/// <summary>
/// AX combat zone
/// </summary>
@ -38,4 +43,27 @@ public class CombatZones {
/// Very high difficulty, so far AX combat zone only
/// </summary>
public static readonly string DifficultyVeryHigh = "Very High";
/// <summary>
/// Returns the given combat zone difficulty as an integer, so it can be sorted.
/// 0 = lowest difficulty, 1 = medium and so forth.
/// </summary>
public static int? DifficultyRank(string? difficulty) {
Dictionary<string, int> ranks = new() {
{ DifficultyLow, 0 },
{ DifficultyMedium, 1 },
{ DifficultyHigh, 2 },
{ DifficultyVeryHigh, 3 }
};
if (difficulty == null ) {
return null;
}
if (ranks.TryGetValue(difficulty, out int rank)) {
return rank;
}
return null;
}
}

View File

@ -34,4 +34,16 @@ public class Credits {
return string.Format("{0} CR", amount.ToString("N", format));
}
public static string FormatMillions(long amount) {
double millions = (amount / 1000000.0);
if (amount >= 100000) {
return string.Format("{0:0.0}M", millions);
} else if (amount >= 10000) {
return string.Format("{0:0.00}M", millions);
}
return string.Format("{0}", amount);
}
}

View File

@ -15,6 +15,7 @@ public class EnglishMissionNames {
{"Mission_AltruismCredits_name", "Donate Credits"},
{"Mission_AltruismCredits_Outbreak_name", "Donate Credits (Outbreak)" },
{"Mission_Assassinate_Illegal_BLOPS_name", "Assassination (Illegal)"},
{"MISSION_assassinate_Illegal_Planetary_name", "Assassination (Illegal, Planetary)" },
{"Mission_Assassinate_Legal_Bust_name", "Assassination (Bust, Legal)" },
{"MISSION_assassinate_legal_CivilUnrest_name", "Assassination (Civil Unrest, Legal)" },
{"Mission_Assassinate_Legal_Communism_name", "Assassination (Communism, Legal)" },
@ -51,6 +52,10 @@ public class EnglishMissionNames {
{"Mission_Delivery_Retreat_name", "Delivery (Retreat)"},
{"Mission_DeliveryWing_name", "Delivery (Wing)"},
{"Mission_DeliveryWing_War_name", "Delivery (Wing) (War)"},
{"Mission_Disable_BLOPS_Expansion_name", "Disable Surface Installation (Expansion) (Black Ops)" },
{"MISSION_Disable_BLOPS_name", "Disable Surface Installation (Black Ops)" },
{"MISSION_Disable_name", "Disable Surface Installation" },
{"MISSION_Hack_name", "Hack Surface Installation" },
{"Mission_Hack_BLOPS_Boom_name", "Hack Surface Installation (Boom)"},
{"Mission_Hack_BLOPS_Elections_name", "Hack Surface Installation (Elections)"},
{"Mission_Hack_BLOPS_Expansion_name", "Hack Surface Installation (Expansion)"},
@ -65,24 +70,30 @@ public class EnglishMissionNames {
{"Mission_MassacreWing_name", "Massacre (Wing)"},
{"Mission_Mining_name", "Mining" },
{"Mission_OnFoot_Assassination_MB_name", "On Foot Assassination"},
{"Mission_OnFoot_Assassination_Hard_MB_name", "On Foot Assassination (Flight-Risk)" },
{"Mission_OnFoot_Assassination_Covert_MB_name", "On Foot Assassination (Covert)" },
{"Mission_OnFoot_AssassinationIllegal_MB_name", "On Foot Assassination (Illegal)"},
{"Mission_OnFoot_AssassinationIllegal_NCD_MB_name", "On Foot Assassination (Illegal)" },
{"Mission_OnFoot_Collect_Contact_MB_name", "On Foot Collect"},
{"Mission_OnFoot_Collect_MB_name", "On Foot Collection"},
{"Mission_OnFoot_Defence_MacGuffin_MB_StandardCanister_name", "On Foot Cargo Defence" },
{"Mission_OnFoot_Delivery_Contact_MB_name", "On Foot Delivery (Contact)"},
{"Mission_OnFoot_Hack_Upload_Covert_MB_name", "On Foot Hack (Covert Upload)"},
{"Mission_OnFoot_Hack_Upload_MB_name", "On Foot Hack (Upload)"},
{"Mission_OnFoot_Heist_MB_name", "On Foot Heist" },
{"Mission_OnFoot_Heist_POI_MB_name", "On Foot Heist (POI)"},
{"Mission_OnFoot_Massacre_MB_name", "On Foot Massacre" },
{"Mission_OnFoot_MassacreIllegal_MB_name", "On Foot Massacre (Illegal)" },
{"Mission_OnFoot_MassacreIllegal_MB_name", "On Foot Massacre (Illegal)"},
{"Mission_OnFoot_Onslaught_MB_name", "On Foot Onslaught"},
{"Mission_OnFoot_Onslaught_Offline_MB_name", "On Foot Onslaught (Offline)"},
{"Mission_OnFoot_ProductionHeist_Covert_MB_name", "On Foot Production Heist (Covert)"},
{"Mission_OnFoot_ProductionHeist_MB_name", "On Foot Production Heist"},
{"Mission_OnFoot_Reboot_MB_name", "On Foot Reboot"},
{"Mission_OnFoot_RebootRestore_MB_name", "On Foot Reboot/Restore"},
{"Mission_OnFoot_Sabotage_Production_Covert_MB_name", "On Foot Sabotage Production (Covert)"},
{"Mission_OnFoot_Sabotage_Power_MB_name", "On Foot Power Sabotage"},
{"Mission_OnFoot_Sabotage_Power_Covert_MB_name", "On Foot Power Sabotage (Covert)"},
{"Mission_OnFoot_Sabotage_Production_MB_name", "On Foot Production Sabotage"},
{"Mission_OnFoot_Sabotage_Production_Covert_MB_name", "On Foot Production Sabotage (Covert)"},
{"Mission_OnFoot_Salvage_MB_name", "On Foot Salvage"},
{"Mission_OnFoot_SalvageIllegal_MB_name", "On Foot Salvage (Illegal)"},
{"Mission_OnFoot_Smuggle_Contact_MB_name", "On Foot Smuggling" },
@ -93,8 +104,11 @@ public class EnglishMissionNames {
{"Mission_PassengerVIP_name", "Passenger (VIP)"},
{"Mission_PassengerVIP_Scientist_FAMINE_name", "Passenger Scientist (VIP) (Famine)"},
{"Mission_PassengerVIP_Tourist_BOOM_name", "Passenger Tourist (VIP) (Boom)"},
{"Mission_Rescue_Planet_name", "Planet Rescue"},
{"Mission_Rescue_Elections_name", "Liberate Hostages (Election)" },
{"Mission_Rescue_name", "Liberate Hostages" },
{"Mission_Rescue_Planet_Expansion_name", "Planet Rescue (Expansion)" },
{"Mission_Rescue_Planet_Retreat_name", "Planet Rescue (Retreat)" },
{"Mission_Rescue_Planet_name", "Planet Rescue"},
{"MISSION_Salvage_CivilUnrest_name", "Salvage (Civil Unrest)"},
{"MISSION_Salvage_Expansion_name", "Salvage (Expansion)"},
{"MISSION_Salvage_Illegal_name", "Salvage (Illegal)"},
@ -106,11 +120,18 @@ public class EnglishMissionNames {
{"Mission_Sightseeing_Criminal_FAMINE_name", "Sightseeing (Criminal) (Famine)"},
{"Mission_Sightseeing_name", "Sightseeing"},
{"Mission_Smuggle_Anarchy_name", "Smuggling (Anarchy)" },
{"Mission_TW_Massacre_Basilisk_Plural_name", "Kill Basilisk" },
{"Mission_TW_Massacre_Basilisk_Plural_name", "Kill Basilisks" },
{"Mission_TW_Massacre_Basilisk_Singular_name", "Kill Basilisk" },
{"Mission_TW_Massacre_Cyclops_Plural_name", "Kill Cyclops" },
{"Mission_TW_Massacre_Cyclops_Singular_name", "Kill Cyclops" },
{"Mission_TW_Massacre_Scout_Plural_name", "Kill Scouts (Wing)" },
{"Mission_TW_Massacre_Cyclops_Singular_name", "Kill Cyclop" },
{"Mission_TW_Massacre_Hydra_Plural_name", "Kill Hydras" },
{"Mission_TW_Massacre_Hydra_Singular_name", "Kill Hydra" },
{"Mission_TW_Massacre_Medusa_Plural_name", "Kill Medusas" },
{"Mission_TW_Massacre_Medusa_Singular_name", "Kill Medusa" },
{"Mission_TW_Massacre_Scout_Plural_name", "Kill Scouts" },
{"Mission_TW_OnFoot_Reboot_Occupied_MB_name", "Reboot Settlement (Thargoid)" },
{"Mission_TW_OnFoot_Reboot_NR_name", "Reboot Settlement (Thargoid)" },
{"Mission_TW_OnFoot_Reboot_MB_name", "Reboot Settlement (Thargoid)" },
{"Mission_TW_PassengerEvacuation_Burning_name", "Passenger Evacuation (Significant Damage)" },
{"Mission_TW_PassengerEvacuation_UnderAttack_name", "Passenger Evacuation (Thargoid Invasion)" },
{"Mission_TW_Rescue_UnderAttack_name", "Rescue (Thargoid Attack)" },

View File

@ -0,0 +1,48 @@
namespace EDPlayerJournal.Entries;
public class ApproachSettlementEntry : Entry {
/// <summary>
/// Settlement name
/// </summary>
public string? Name { get; set; } = null;
/// <summary>
/// Market ID of the settlement
/// </summary>
public long? MarketID { get; set; } = null;
/// <summary>
/// System ID
/// </summary>
public long? SystemAddress { get; set; } = null;
/// <summary>
/// Body ID
/// </summary>
public long? BodyID { get; set; } = null;
/// <summary>
/// Name of the planet
/// </summary>
public string? BodyName { get; set; } = null;
/// <summary>
/// Planet latitude
/// </summary>
public double Latitude { get; set; } = 0.0;
/// <summary>
/// Planet longitude
/// </summary>
public double Longitude { get; set; } = 0.0;
protected override void Initialise() {
Name = JSON.Value<string?>("Name");
MarketID = JSON.Value<long?>("MarketID");
SystemAddress = JSON.Value<long?>("SystemID");
BodyID = JSON.Value<long?>("BodyID");
BodyName = JSON.Value<string?>("BodyName");
Longitude = JSON.Value<double?>("Longitude") ?? 0.0;
Latitude = JSON.Value<double?>("Latitude") ?? 0.0;
}
}

View File

@ -0,0 +1,44 @@
using Newtonsoft.Json.Linq;
namespace EDPlayerJournal.Entries;
public class CarrierJump : Entry {
public bool Docked { get; set; } = false;
public string? StationName { get; set; } = null;
public string? StationType { get; set; } = null;
public string? StarSystem { get; set; } = null;
public ulong SystemAddress { get; set; } = 0;
public string? SystemFaction { get; set; } = null;
public List<Faction> SystemFactions { get; set; } = new List<Faction>();
protected override void Initialise() {
Docked = JSON.Value<bool?>("Docked") ?? false;
StarSystem = JSON.Value<string>("StarSystem");
SystemAddress = JSON.Value<ulong?>("SystemAddress") ?? 0;
StationName = JSON.Value<string?>("StationName");
StationType = JSON.Value<string?>("StationType");
var faction = JSON.Value<JObject>("SystemFaction");
if (faction != null) {
SystemFaction = faction.Value<string>("Name");
}
var factions = JSON.Value<JArray>("Factions");
if (factions != null) {
foreach (JObject system_faction in factions) {
Faction? f = Faction.FromJSON(system_faction);
if (f != null) {
SystemFactions.Add(f);
}
}
}
}
}

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

@ -12,8 +12,10 @@ namespace EDPlayerJournal.Entries;
/// </summary>
public class Entry {
private static readonly Dictionary<string, Type> classes = new Dictionary<string, Type> {
{ Events.ApproachSettlement, typeof(ApproachSettlementEntry) },
{ Events.Bounty, typeof(BountyEntry) },
{ Events.CapShipBond, typeof(CapShipBondEntry) },
{ Events.CarrierJump, typeof(CarrierJump) },
{ Events.Commander, typeof(CommanderEntry) },
{ Events.CommitCrime, typeof(CommitCrimeEntry) },
{ Events.Died, typeof(DiedEntry) },
@ -36,14 +38,22 @@ public class Entry {
{ Events.MissionRedirected, typeof(MissionRedirectedEntry) },
{ Events.Missions, typeof(MissionsEntry) },
{ Events.MultiSellExplorationData, typeof(MultiSellExplorationDataEntry) },
{ Events.Music, typeof(MusicEntry) },
{ Events.Powerplay, typeof(PowerplayEntry) },
{ Events.PowerplayCollect, typeof(PowerplayCollectEntry) },
{ Events.PowerplayDeliver, typeof(PowerplayDeliverEntry) },
{ Events.PowerplayMerits, typeof(PowerplayMeritsEntry) },
{ Events.PowerplayRank, typeof(PowerplayRankEntry) },
{ Events.ReceiveText, typeof(ReceiveTextEntry) },
{ Events.RedeemVoucher, typeof(RedeemVoucherEntry) },
{ Events.SearchAndRescue, typeof(SearchAndRescueEntry) },
{ Events.SelfDestruct, typeof(SelfDestructEntry) },
{ Events.SellExplorationData, typeof(SellExplorationDataEntry) },
{ Events.SellMicroResources, typeof(SellMicroResourcesEntry) },
{ Events.SellOrganicData, typeof(SellOrganicDataEntry) },
{ Events.ShieldState, typeof(ShieldStateEntry) },
{ Events.ShipTargeted, typeof(ShipTargetedEntry) },
{ Events.SupercruiseDestinationDrop, typeof(SupercruiseDestinationDropEntry) },
{ Events.SupercruiseEntry, typeof(SupercruiseEntryEntry) },
{ Events.SupercruiseExit, typeof(SupercruiseExitEntry) },
{ Events.UnderAttack, typeof(UnderAttackEntry) },
@ -62,7 +72,15 @@ public class Entry {
public static Entry? Parse(string journalline) {
using (JsonReader reader = new JsonTextReader(new StringReader(journalline))) {
reader.DateParseHandling = DateParseHandling.None;
var json = JObject.Load(reader);
JObject? json = null;
try {
json = JObject.Load(reader);
} catch (Exception e) {
throw new InvalidJournalEntryException(
"invalid JSON journal entry: " + journalline,
e
);
}
if (json == null) {
return null;
}

View File

@ -1,8 +1,10 @@
namespace EDPlayerJournal.Entries;
public class Events {
public static readonly string ApproachSettlement = "ApproachSettlement";
public static readonly string Bounty = "Bounty";
public static readonly string CapShipBond = "CapShipBond";
public static readonly string CarrierJump = "CarrierJump";
public static readonly string Commander = "Commander";
public static readonly string CommitCrime = "CommitCrime";
public static readonly string Died = "Died";
@ -26,15 +28,23 @@ public class Events {
public static readonly string MissionRedirected = "MissionRedirected";
public static readonly string Missions = "Missions";
public static readonly string MultiSellExplorationData = "MultiSellExplorationData";
public static readonly string Music = "Music";
public static readonly string Powerplay = "Powerplay";
public static readonly string PowerplayCollect = "PowerplayCollect";
public static readonly string PowerplayDeliver = "PowerplayDeliver";
public static readonly string PowerplayMerits = "PowerplayMerits";
public static readonly string PowerplayRank = "PowerplayRank";
public static readonly string ReceiveText = "ReceiveText";
public static readonly string RedeemVoucher = "RedeemVoucher";
public static readonly string SearchAndRescue = "SearchAndRescue";
public static readonly string SelfDestruct = "SelfDestruct";
public static readonly string SellExplorationData = "SellExplorationData";
public static readonly string SellMicroResources = "SellMicroResources";
public static readonly string SellOrganicData = "SellOrganicData";
public static readonly string ShieldState = "ShieldState";
public static readonly string ShipTargeted = "ShipTargeted";
public static readonly string Shutdown = "Shutdown";
public static readonly string SupercruiseDestinationDrop = "SupercruiseDestinationDrop";
public static readonly string SupercruiseEntry = "SupercruiseEntry";
public static readonly string SupercruiseExit = "SupercruiseExit";
public static readonly string UnderAttack = "UnderAttack";

View File

@ -1,18 +1,47 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Linq;
namespace EDPlayerJournal.Entries;
namespace EDPlayerJournal.Entries;
public class LocationEntry : Entry {
/// <summary>
/// Current star system.
/// </summary>
public string? StarSystem { get; set; }
/// <summary>
/// Faction in control of the current star system. Empty for unpopulated
/// systems.
/// </summary>
public string? SystemFaction { get; set; }
public string? StationName { get; set; }
/// <summary>
/// 64bit system address.
/// </summary>
public ulong SystemAddress { get; set; }
/// <summary>
/// Station name if docked at a station.
/// </summary>
public string? StationName { get; set; }
/// <summary>
/// Faction in control of the current station, if docked.
/// </summary>
public string? StationFaction { get; set; }
/// <summary>
/// Body within the system, might be null.
/// </summary>
public string? Body { get; set; }
/// <summary>
/// Returns true if the player is docked somewhere.
/// </summary>
public bool Docked { get; set; }
/// <summary>
/// Position of the star system.
/// </summary>
public long[]? StarPos { get; set; }
public List<Faction> SystemFactions { get; set; } = new List<Faction>();
@ -30,7 +59,12 @@ public class LocationEntry : Entry {
JObject? systemfaction = JSON.Value<JObject>("SystemFaction");
if (systemfaction != null) {
SystemFaction = systemfaction.Value<string>("Name") ?? "";
SystemFaction = systemfaction.Value<string>("Name");
}
JObject? stationfaction = JSON.Value<JObject>("StationFaction");
if (stationfaction != null) {
StationFaction = stationfaction.Value<string>("Name");
}
JArray? factions = JSON.Value<JArray>("Factions");

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

@ -0,0 +1,9 @@
namespace EDPlayerJournal.Entries;
public class MusicEntry : Entry {
public string? MusicTrack { get; set; } = null;
protected override void Initialise() {
MusicTrack = JSON.Value<string?>("MusicTrack");
}
}

View File

@ -0,0 +1,7 @@
namespace EDPlayerJournal.Entries;
public class PowerplayCollectEntry : Entry {
protected override void Initialise() {
// TODO
}
}

View File

@ -0,0 +1,7 @@
namespace EDPlayerJournal.Entries;
public class PowerplayDeliverEntry : Entry {
protected override void Initialise() {
// TODO
}
}

View File

@ -0,0 +1,32 @@
using System.Reflection;
namespace EDPlayerJournal.Entries {
public class PowerplayEntry : Entry {
/// <summary>
/// Name of the power
/// </summary>
public string Power { get; set; } = string.Empty;
/// <summary>
/// Player rank
/// </summary>
public int Rank { get; set; } = 0;
/// <summary>
/// Current merits of the player
/// </summary>
public long Merits { get; set; } = 0;
/// <summary>
/// Time pledged (in seconds?)
/// </summary>
public long TimePledged { get; set; } = 0;
protected override void Initialise() {
Power = JSON.Value<string>("Power") ?? string.Empty;
Rank = JSON.Value<int?>("Rank") ?? 0;
Merits = JSON.Value<long?>("Merits") ?? 0;
TimePledged = JSON.Value<long?>("TimePledged") ?? 0;
}
}
}

View File

@ -0,0 +1,15 @@
namespace EDPlayerJournal.Entries;
public class PowerplayMeritsEntry : Entry {
protected override void Initialise() {
Power = JSON.Value<string?>("Power") ?? string.Empty;
MeritsGained = JSON.Value<long?>("MeritsGained") ?? 0;
TotalMerits = JSON.Value<long?>("TotalMerits") ?? 0;
}
public string Power { get; set; } = string.Empty;
public long MeritsGained { get; set; } = 0;
public long TotalMerits { get; set; } = 0;
}

View File

@ -0,0 +1,7 @@
namespace EDPlayerJournal.Entries;
public class PowerplayRankEntry : Entry {
protected override void Initialise() {
// TODO
}
}

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 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

@ -0,0 +1,30 @@
namespace EDPlayerJournal.Entries;
public class SupercruiseDestinationDropEntry : Entry {
/// <summary>
/// Destination type, internal string.
/// </summary>
public string? Type { get; set; } = null;
/// <summary>
/// Destination type, localised string.
/// </summary>
public string? TypeLocalised { get; set; } = null;
/// <summary>
/// Threat of the destination, if known.
/// </summary>
public long? Threat { get; set; } = null;
/// <summary>
/// Market ID if it has one.
/// </summary>
public ulong? MarketID { get; set; } = null;
protected override void Initialise() {
Type = JSON.Value<string>("Type");
TypeLocalised = JSON.Value<string>("Type_Localised");
Threat = JSON.Value<long?>("Threat");
MarketID = JSON.Value<ulong?>("MarketID");
}
}

View File

@ -55,6 +55,11 @@ public class Factions {
/// </summary>
public static string Thargoid = "Thargoids";
/// <summary>
/// The faction the fleet carriers use.
/// </summary>
public static string FleetCarrier = "FleetCarrier";
public static bool IsPilotsFederation(string? name) {
if (name == null) {
return false;

View File

@ -0,0 +1,87 @@
namespace EDPlayerJournal;
/// <summary>
/// Contains information regarding instances you can supercruise into,
/// such as combat zones, installations and megaship scenarios.
/// </summary>
public class Instances {
/// <summary>
/// Low ship combat zone
/// </summary>
public static readonly string WarzoneLow = "$Warzone_PointRace_Low";
/// <summary>
/// Medium ship combat zone
/// </summary>
public static readonly string WarzoneMedium = "$Warzone_PointRace_Med";
/// <summary>
/// High ship combat zone.
/// </summary>
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>
/// Low Thargoid combat zone
/// </summary>
public static readonly string WarzoneThargoidLow = "$Warzone_TG_Low";
/// <summary>
/// Medium Thargoid combat zone
/// </summary>
public static readonly string WarzoneThargoidMedium = "$Warzone_TG_Med";
/// <summary>
/// High Thargoid combat zone
/// </summary>
public static readonly string WarzoneThargoidHigh = "$Warzone_TG_High";
/// <summary>
/// Very High Thargoid combat zone
/// </summary>
public static readonly string WarzoneThargoidVeryHigh = "$Warzone_TG_VeryHigh";
public static bool IsThargoidWarzone(string type) {
return
IsInstance(type, WarzoneThargoidLow) ||
IsInstance(type, WarzoneThargoidMedium) ||
IsInstance(type, WarzoneThargoidHigh) ||
IsInstance(type, WarzoneThargoidVeryHigh)
;
}
public static bool IsHumanWarzone(string type) {
return
IsInstance(type, WarzoneLow) ||
IsInstance(type, WarzoneMedium) ||
IsInstance(type, WarzoneHigh)
;
}
public static bool IsPowerWarzone(string type) {
return
IsInstance(type, PowerWarzoneMedium)
;
}
public static bool IsWarzone(string type) {
return IsHumanWarzone(type) ||
IsThargoidWarzone(type) ||
IsPowerWarzone(type)
;
}
public static bool IsInstance(string type, string instance) {
if (string.IsNullOrEmpty(type) || string.IsNullOrEmpty(instance)) {
return false;
}
// Instance names are split by a semi colon, with the remainder being
// additional info to such as index.
string[] parts = type.Split(":");
if (!parts[0].StartsWith("$")) {
return false;
}
return string.Compare(parts[0], instance, true) == 0;
}
}

View File

@ -6,6 +6,7 @@
public class InvalidJournalEntryException : Exception {
public InvalidJournalEntryException() { }
public InvalidJournalEntryException(string message) : base(message) { }
public InvalidJournalEntryException(string message, Exception inner) : base(message, inner) { }
}
/// <summary>

View File

@ -17,6 +17,11 @@ public class JournalFile : IComparable<JournalFile>
private static Regex update11regex = new Regex("Journal\\.([^\\.]+)\\.(\\d+).log");
private static string iso8601 = "yyyyMMddTHHmmss";
/// <summary>
/// A public list of errors encountered while parsing the journal files
/// </summary>
public List<Exception> Errors { get; private set; } = new List<Exception>();
public static bool VerifyFile(string path) {
string filename = Path.GetFileName(path);
@ -125,14 +130,19 @@ public class JournalFile : IComparable<JournalFile>
}
entries.Clear();
foreach(var line in lines) {
Errors.Clear();
foreach (var line in lines) {
// Skip empty lines
if (line.Trim().Length == 0) {
continue;
}
Entry? entry = Entry.Parse(line);
if (entry != null) {
entries.Add(entry);
try {
Entry? entry = Entry.Parse(line);
if (entry != null) {
entries.Add(entry);
}
} catch (Exception ex) {
Errors.Add(ex);
}
}
}

View File

@ -36,6 +36,31 @@ public class MissionInfluence {
/// </summary>
public string Influence { get; set; } = string.Empty;
public long InfluenceAmount {
get {
string trend = TrendAdjustedInfluence;
return (long)
(trend.Count(x => x == '-') * -1) +
trend.Count(x => x == '+')
;
}
}
/// <summary>
/// Returns how much influence was made, represented in pluses for positive influence,
/// and minuses with negative influence. This takes Trend (up, bad etc.) into account.
/// </summary>
public string TrendAdjustedInfluence {
get {
if (!string.IsNullOrEmpty(Trend) &&
Trend.Contains("bad", StringComparison.OrdinalIgnoreCase)) {
return new string('-', Influence.Length);
} else {
return new string('+', Influence.Length);
}
}
}
public static MissionInfluence FromJSON(JObject obj) {
MissionInfluence missionInfluence = new MissionInfluence();
@ -394,27 +419,29 @@ public class Mission : IComparable<Mission> {
/// <param name="faction">Faction name in question.</param>
/// <param name="systemaddr">Star System address</param>
/// <returns>null if no entry was found, or a string denoting pluses for the amount influence gained.</returns>
public string? GetInfluenceForFaction(string faction, ulong systemaddr) {
public MissionInfluence[]? GetInfluenceForFaction(string faction, ulong systemaddr) {
var results = FactionEffects
.Where(x => string.Compare(x.Faction, faction) == 0)
.SelectMany(x => x.Influences)
.Where(x => (x.SystemAddress != null && x.SystemAddress == systemaddr))
.Select(x => x.Influence)
.Select(x => x)
.ToArray()
;
if (results == null || results.Length == 0) {
return null;
return new MissionInfluence[0];
}
return string.Join("", results);
return results;
}
/// <summary>
/// A convenient Dictionary containing all influences given out by faction,
/// then by system address and then by influence handed out.
/// then by system address and then by influence handed out. Influence can
/// be either a series of "+" for positive influence, or "-" for negative
/// influence.
/// </summary>
public Dictionary<string, Dictionary<ulong, string>> Influences {
public Dictionary<string, Dictionary<ulong, MissionInfluence>> Influences {
get {
return FactionEffects
.Where(x => x.Faction != null)
@ -422,7 +449,10 @@ public class Mission : IComparable<Mission> {
x => (x.Faction ?? string.Empty),
x => x.Influences
.Where(x => x.SystemAddress != null)
.ToDictionary(x => (x.SystemAddress ?? 0), x => x.Influence)
.ToDictionary(
x => (x.SystemAddress ?? 0),
x => x
)
);
}
}

View File

@ -22,6 +22,12 @@ public class PlayerJournal {
ScanFiles();
}
public List<Exception> AllErrors {
get {
return Files.SelectMany(x => x.Errors).ToList();
}
}
public List<JournalFile> Files {
get { return journalfiles; }
}

View File

@ -0,0 +1,101 @@
namespace EDPlayerJournal.PowerPlay;
/// <summary>
/// Holds a list of all available powers in E:D, and helps
/// to translate commonly used short-hands and aliases to
/// journal approved power names.
/// </summary>
public class Powers {
public static readonly string
// Basking
ALavignyDuval = "A. Lavigny-Duval",
AislingDuval = "Aisling Duval",
DentonPatreus = "Denton Patreus",
ZeminaTorval = "Zemina Torval",
// Feds
JeromeArcher = "Jerome Archer",
FeliciaWinters = "Felicia Winters",
// Alliance
EdmunMahon = "Edmund Mahon",
NakatoKaine = "Nakato Kaine",
// Independents
YuriGrom = "Yuri Grom",
ArchonDelaine = "Archon Delaine",
LiYongRui = "Li Yong-Rui",
PranavAntal = "Pranav Antal"
;
public static readonly Dictionary<string, string> aliases = new() {
// ALD
{ Powers.ALavignyDuval, Powers.ALavignyDuval },
{ "ALD", Powers.ALavignyDuval },
{ "Arissa", Powers.ALavignyDuval },
{ "Emperor", Powers.ALavignyDuval },
{ "Kaiser", Powers.ALavignyDuval },
// AD
{ Powers.AislingDuval, Powers.AislingDuval },
{ "AD", Powers.AislingDuval },
{ "Aisling", Powers.AislingDuval },
// DP
{ Powers.DentonPatreus, Powers.DentonPatreus },
{ "DP", Powers.DentonPatreus },
{ "Denton", Powers.DentonPatreus },
// ZT
{ Powers.ZeminaTorval, Powers.ZeminaTorval },
{ "ZT", Powers.ZeminaTorval },
{ "Torval", Powers.ZeminaTorval },
// Archer
{ Powers.JeromeArcher, Powers.JeromeArcher },
{ "JA", Powers.JeromeArcher },
{ "Archer", Powers.JeromeArcher },
// Winters
{ Powers.FeliciaWinters, Powers.FeliciaWinters },
{ "FW", Powers.FeliciaWinters },
{ "Winters", Powers.FeliciaWinters },
// Mahon
{ Powers.EdmunMahon, Powers.EdmunMahon },
{ "EM", Powers.EdmunMahon },
{ "Mahon", Powers.EdmunMahon },
// Kaine
{ Powers.NakatoKaine, Powers.NakatoKaine },
{ "NK", Powers.NakatoKaine },
{ "Kaine", Powers.NakatoKaine },
// Grom
{ Powers.YuriGrom, Powers.YuriGrom },
{ "YG", Powers.YuriGrom },
{ "Grom", Powers.YuriGrom },
// Archon
{ Powers.ArchonDelaine, Powers.ArchonDelaine },
{ "Archon", Powers.ArchonDelaine },
{ "Kumo", Powers.ArchonDelaine },
{ "KumoBurger", Powers.ArchonDelaine },
// LYR
{ Powers.LiYongRui, Powers.LiYongRui },
{ "LYR", Powers.LiYongRui },
// Pranav
{ Powers.PranavAntal, Powers.PranavAntal },
{ "PA", Powers.PranavAntal },
{ "Pranav", Powers.PranavAntal },
};
public static bool IsValidPower(string power) {
try {
string p = GetPower(power);
return true;
} catch (Exception) {
return false;
}
}
public static string GetPower(string nameOrAlias) {
string? val = aliases
.Where(x => string.Compare(x.Key, nameOrAlias, StringComparison.InvariantCultureIgnoreCase) == 0)
.Select(x => x.Value)
.FirstOrDefault()
;
if (string.IsNullOrEmpty(val)) {
throw new ApplicationException($"not a valid power: {nameOrAlias}");
}
return val;
}
}

View File

@ -8,6 +8,16 @@ public enum ThargoidVessel {
Basilisk = 4,
Medusa = 5,
Hydra = 6,
// Includes Glaive and Scythe
Hunter = 7,
/// <summary>
/// Thargoid drone
/// </summary>
Revenant = 8,
/// <summary>
/// New thargoid drone in U17
/// </summary>
Banshee = 9,
}
public class Thargoid {
@ -15,34 +25,44 @@ public class Thargoid {
public static Dictionary<ulong, ThargoidVessel> VesselPayout { get; } = new() {
// Up to date values ever since 14.02
{ 25000, ThargoidVessel.Revenant },
{ 65000, ThargoidVessel.Scout },
{ 75000, ThargoidVessel.Scout },
// New in Update 17
{ 100000, ThargoidVessel.Banshee },
// New in Update 15
{ 4500000, ThargoidVessel.Hunter },
{ 6500000, ThargoidVessel.Cyclops },
{ 20000000, ThargoidVessel.Basilisk },
{ 25000000, ThargoidVessel.Orthrus },
//{ 25000000, ThargoidVessel.Orthrus },
{ 34000000, ThargoidVessel.Medusa },
// March, 16th 2023 the Orthrus payout was buffed again.
{ 40000000, ThargoidVessel.Orthrus },
{ 50000000, ThargoidVessel.Hydra },
// These are the old values pre Update 14.02
{ 80000, ThargoidVessel.Scout },
{ 8000000, ThargoidVessel.Cyclops },
{ 24000000, ThargoidVessel.Basilisk },
//{ 80000, ThargoidVessel.Scout },
//{ 8000000, ThargoidVessel.Cyclops },
//{ 24000000, ThargoidVessel.Basilisk },
// In Update 14.1 the payout for Orthrus has been rebalanced.
{ 30000000, ThargoidVessel.Orthrus },
{ 40000000, ThargoidVessel.Medusa },
//{ 30000000, ThargoidVessel.Orthrus },
//{ 40000000, ThargoidVessel.Medusa },
// This used to be the old payout value for Orthrus, it now conflicts
// with Post Update 14.02 Hydra values
//{ 50000000, ThargoidVessel.Orthrus },
{ 60000000, ThargoidVessel.Hydra },
//{ 60000000, ThargoidVessel.Hydra },
};
public static Dictionary<ThargoidVessel, string?> VesselNames { get; } = new() {
{ ThargoidVessel.Unknown, null },
{ ThargoidVessel.Scout, "Thargoid Scout" },
{ ThargoidVessel.Unknown, "(Unknown)" },
{ ThargoidVessel.Revenant, "Revenant" },
{ ThargoidVessel.Scout, "Scout" },
{ ThargoidVessel.Orthrus, "Orthrus" },
{ ThargoidVessel.Cyclops, "Cyclops" },
{ ThargoidVessel.Basilisk, "Basilisk" },
{ ThargoidVessel.Medusa, "Medusa" },
{ ThargoidVessel.Hydra, "Hydra" },
{ ThargoidVessel.Hunter, "Hunter" },
{ ThargoidVessel.Banshee, "Banshee" },
};
public static ThargoidVessel GetVesselByPayout(ulong payout) {

View File

@ -59,9 +59,9 @@ public class MissionTest {
Assert.IsTrue(e.IsEmptyFaction);
Assert.AreEqual(e.Faction, string.Empty);
string? influence = m.GetInfluenceForFaction("", 251012319587UL);
var influence = m.GetInfluenceForFaction("", 251012319587UL);
Assert.IsNotNull(influence);
Assert.AreEqual(influence, "+");
Assert.AreEqual(influence[0].Influence, "+");
e = m.FactionEffects[1];
Assert.AreEqual(e.Faction, "Social LHS 6103 Confederation");
@ -101,22 +101,25 @@ public class MissionTest {
Assert.AreEqual(effect.Reputation, "++");
string? influence;
influence = m.GetInfluenceForFaction("Salus Imperial Society", 1865919973739UL);
Assert.AreEqual(influence, "++");
var influence = m.GetInfluenceForFaction("Salus Imperial Society", 1865919973739UL);
Assert.IsNotNull(influence);
Assert.IsTrue(influence.Length > 0);
Assert.AreEqual(influence[0].Influence, "++");
influence = m.GetInfluenceForFaction("Salus Imperial Society", 1733186884306UL);
Assert.AreEqual(influence, "++");
Assert.IsNotNull(influence);
Assert.IsTrue(influence.Length > 0);
Assert.AreEqual(influence[0].Influence, "++");
influence = m.GetInfluenceForFaction("Saelishi Saxons", 1733186884306UL);
Assert.IsNull(influence);
Assert.IsNotNull(influence);
Assert.AreEqual(influence.Length, 0);
// Only one entry are we only have Salus
Assert.AreEqual(m.Influences.Count, 1);
Assert.AreEqual(m.Influences["Salus Imperial Society"].Count, 2);
Assert.AreEqual(m.Influences["Salus Imperial Society"][1865919973739UL], "++");
Assert.AreEqual(m.Influences["Salus Imperial Society"][1733186884306UL], "++");
Assert.AreEqual(m.Influences["Salus Imperial Society"][1865919973739UL].Influence, "++");
Assert.AreEqual(m.Influences["Salus Imperial Society"][1733186884306UL].Influence, "++");
}
[TestMethod]

View File

@ -19,7 +19,13 @@ public class TestTransactionParser {
return;
}
List<Transaction>? transactions = parser.Parse(entries);
var options = new TransactionParserOptions() {
IgnoreInfluenceSupport = false,
IgnoreExoBiology = false,
IgnoreFleetCarrierFaction = false,
IgnoreMarketBuy = false,
};
List<Transaction>? transactions = parser.Parse(entries, options);
Assert.IsNotNull(transactions, "could not parse entries");
Assert.AreEqual(transactions.Count, 3);
@ -144,7 +150,14 @@ public class TestTransactionParser {
return;
}
List<Transaction>? transactions = parser.Parse(entries);
var options = new TransactionParserOptions() {
IgnoreInfluenceSupport = false,
IgnoreExoBiology = false,
IgnoreFleetCarrierFaction = false,
IgnoreMarketBuy = false,
};
List<Transaction>? transactions = parser.Parse(entries, options);
Assert.IsNotNull(transactions, "could not parse entries");
Assert.AreEqual(transactions.Count, 1);
Assert.IsInstanceOfType(transactions[0], typeof(OrganicData), "result is not of type Organic Data");

View File

@ -22,14 +22,17 @@ public class ThargoidKills {
Assert.IsNotNull(transactions, "could not parse entries");
Assert.AreEqual(transactions.Count, 3);
// In recent updates the payout was changed, that's why this test reports unknown thargoid vessels
// This test makes sure the new parser does not conflict with legacy values
//
Assert.IsInstanceOfType(transactions[0], typeof(ThargoidKill), "result is not of type ThargoidKill");
Assert.AreEqual(transactions[0].ThargoidType, EDPlayerJournal.ThargoidVessel.Scout);
Assert.AreEqual(transactions[0].ThargoidType, EDPlayerJournal.ThargoidVessel.Unknown);
Assert.IsInstanceOfType(transactions[1], typeof(ThargoidKill), "result is not of type ThargoidKill");
Assert.AreEqual(transactions[1].ThargoidType, EDPlayerJournal.ThargoidVessel.Basilisk);
Assert.AreEqual(transactions[1].ThargoidType, EDPlayerJournal.ThargoidVessel.Unknown);
Assert.IsInstanceOfType(transactions[2], typeof(ThargoidKill), "result is not of type ThargoidKill");
Assert.AreEqual(transactions[2].ThargoidType, EDPlayerJournal.ThargoidVessel.Scout);
Assert.AreEqual(transactions[2].ThargoidType, EDPlayerJournal.ThargoidVessel.Unknown);
}
[TestMethod]

View File

@ -0,0 +1,7 @@
{"timestamp":"2024-04-27T13:27:08Z","event":"Fileheader","part":1,"language":"English/UK","Odyssey":true,"gameversion":"4.0.0.1803","build":"r301470/r0 "}
{"timestamp":"2024-04-27T13:27:34Z","event":"Commander","FID":"F9183790","Name":"Jeremaya"}
{"timestamp":"2024-04-27T15:06:31Z","event":"FSDJump","Taxi":false,"Multicrew":false,"StarSystem":"HIP 3318","SystemAddress":525890177387,"StarPos":[50.21875,-190.6875,37.5],"SystemAllegiance":"Empire","SystemEconomy":"$economy_Refinery;","SystemEconomy_Localised":"Refinery","SystemSecondEconomy":"$economy_Extraction;","SystemSecondEconomy_Localised":"Extraction","SystemGovernment":"$government_Patronage;","SystemGovernment_Localised":"Patronage","SystemSecurity":"$SYSTEM_SECURITY_high;","SystemSecurity_Localised":"High Security","Population":239484,"Body":"HIP 3318 A","BodyID":2,"BodyType":"Star","JumpDist":27.495,"FuelUsed":3.115176,"FuelLevel":13.228082,"Factions":[{"Name":"HIP 3318 Autocracy","FactionState":"None","Government":"Dictatorship","Influence":0.119192,"Allegiance":"Empire","Happiness":"$Faction_HappinessBand2;","Happiness_Localised":"Happy","MyReputation":0.0},{"Name":"Chakho Gold Galactic Limited","FactionState":"None","Government":"Corporate","Influence":0.09798,"Allegiance":"Empire","Happiness":"$Faction_HappinessBand2;","Happiness_Localised":"Happy","MyReputation":0.0},{"Name":"HIP 3318 Interstellar","FactionState":"None","Government":"Corporate","Influence":0.075758,"Allegiance":"Empire","Happiness":"$Faction_HappinessBand2;","Happiness_Localised":"Happy","MyReputation":0.0},{"Name":"HIP 3318 Values Party","FactionState":"None","Government":"Democracy","Influence":0.032323,"Allegiance":"Independent","Happiness":"$Faction_HappinessBand2;","Happiness_Localised":"Happy","MyReputation":0.0},{"Name":"Nationalists of HIP 3318","FactionState":"None","Government":"Dictatorship","Influence":0.035354,"Allegiance":"Empire","Happiness":"$Faction_HappinessBand2;","Happiness_Localised":"Happy","MyReputation":0.0},{"Name":"Nova Paresa","FactionState":"Boom","Government":"Patronage","Influence":0.434343,"Allegiance":"Empire","Happiness":"$Faction_HappinessBand2;","Happiness_Localised":"Happy","SquadronFaction":true,"MyReputation":100.0,"PendingStates":[{"State":"Expansion","Trend":0}]},{"Name":"Empire Consulate Ltd","FactionState":"None","Government":"Patronage","Influence":0.20505,"Allegiance":"Empire","Happiness":"$Faction_HappinessBand2;","Happiness_Localised":"Happy","MyReputation":94.474998}],"SystemFaction":{"Name":"Nova Paresa","FactionState":"Boom"}}
{"timestamp":"2024-04-27T15:12:23Z","event":"ApproachSettlement","Name":"Koh Biological Installation","MarketID":3803792128,"StationFaction":{"Name":"Nova Paresa","FactionState":"Boom"},"StationGovernment":"$government_Patronage;","StationGovernment_Localised":"Patronage","StationAllegiance":"Empire","StationServices":["dock","autodock","commodities","contacts","exploration","missions","refuel","repair","engineer","missionsgenerated","flightcontroller","stationoperations","searchrescue","stationMenu"],"StationEconomy":"$economy_HighTech;","StationEconomy_Localised":"High Tech","StationEconomies":[{"Name":"$economy_HighTech;","Name_Localised":"High Tech","Proportion":1.0}],"SystemAddress":525890177387,"BodyID":26,"BodyName":"HIP 3318 D 3","Latitude":-0.893061,"Longitude":-62.928345}
{"timestamp":"2024-04-27T15:14:08Z","event":"Docked","StationName":"Koh Biological Installation","StationType":"OnFootSettlement","Taxi":false,"Multicrew":false,"StarSystem":"HIP 3318","SystemAddress":525890177387,"MarketID":3803792128,"StationFaction":{"Name":"Nova Paresa","FactionState":"Boom"},"StationGovernment":"$government_Patronage;","StationGovernment_Localised":"Patronage","StationAllegiance":"Empire","StationServices":["dock","autodock","commodities","contacts","exploration","missions","refuel","repair","engineer","missionsgenerated","flightcontroller","stationoperations","searchrescue","stationMenu"],"StationEconomy":"$economy_HighTech;","StationEconomy_Localised":"High Tech","StationEconomies":[{"Name":"$economy_HighTech;","Name_Localised":"High Tech","Proportion":1.0}],"DistFromStarLS":11972.63409,"LandingPads":{"Small":1,"Medium":0,"Large":1}}
{"timestamp":"2024-04-27T15:15:10Z","event":"RedeemVoucher","Type":"bounty","Amount":9449329,"Factions":[{"Faction":"Nova Paresa","Amount":9449329}]}
{"timestamp":"2024-04-27T15:15:51Z","event":"RedeemVoucher","Type":"bounty","Amount":18780130,"Factions":[{"Faction":"","Amount":224449},{"Faction":"","Amount":730880},{"Faction":"","Amount":1272764},{"Faction":"","Amount":580384},{"Faction":"","Amount":10180261},{"Faction":"","Amount":1413153},{"Faction":"","Amount":1365934},{"Faction":"","Amount":202193},{"Faction":"Nova Paresa","Amount":9449329},{"Faction":"","Amount":104412},{"Faction":"","Amount":2453443},{"Faction":"","Amount":252257}]}

View File

@ -4,6 +4,14 @@
xmlns:local="clr-namespace:EliteBGS"
StartupUri="MainWindow.xaml">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<!-- MahApps.Metro resource dictionaries. Make sure that all file names are Case Sensitive! -->
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Controls.xaml" />
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Fonts.xaml" />
<!-- Theme setting -->
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Themes/Light.Blue.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@ -1,4 +1,5 @@
using System.Windows;
using ControlzEx.Theming;
using System.Windows;
namespace EliteBGS {
/// <summary>

View File

@ -1,5 +1,122 @@
# 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
* Add support for MeritsGained, but disable it for now, as the player
journal is massively lacking in terms of Powerplay 2.0 support.
## 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
well as the name of of the log format used. This makes it easier for bots
to parse these logs. Since the different formats have become popular, its
always good to make it easier for bots to parse the logs.
## 0.4.1 on 28.04.2024
* Filter out vouchers that are redeemed twice, due to bulk turn-in. If you
redeem a singular voucher for value X, and then redeem the rest of your
vouchers - say KWS vouchers - in bulk, the first voucher of value X will
appear again in the logs. It appears twice in the logs, but only counts
once.
* Add the market name to the trading log entries.
## 0.4.0 on 13.04.2024
* Change layout of the results into System -> Faction -> Transaction.
Many people who contribute *a lot* of things to the BGS have preferred such
a layout to find the right things to post to various discord guilds/threads.
* Sort all systems in the new overview by name. It just makes them easier to
find when there are a lot of entries.
* Add a button to deselect/select all buttons.
## 0.3.7 on 29.01.2024
* Fix wrong locations of BGS action if you remain on your carrier while its
jumping to a new system.
* Identify a capital ship in a high CZ by its music.
## 0.3.6 on 25.10.2023
* U17 introduced invalid JSON into the player journal. EliteBGS can now skip over
those and keep processing. This way players won't have to delete lines in their
journals anymore to keep using the tool.
* Banshee has been added.
## 0.3.5 on 11.09.2023
* Small bounty voucher formats are no longer suppressed.
* Glaive has been renamed to "Hunter" since Scythe has the same bounty
and they cannot be distinguished.
* Mission influence can now also be negative. Failed missions now properly
report the amount of negative influence given to a faction.
* Mission secondary influences now also support negative influences. This
for example happens if you take a mission to murder another faction's
civlians, which causes negative INF.
## 0.3.4 on 18.06.2023
* Added possibility to specify allied, as well as enemy captain and correspondent.
* Added date range, and tool version to standard log format.
* Added new English mission names.
## 0.3.3 on 15.05.2023
* Added payout for the Thargoid Glaive.
* Added payout and support for the Thargoid Revenant.
* Added more English mission names.
* Ship and AX combat zones can now be detected through a new event, which
allows the tool to properly determine the difficulty. Combat zone outcome,
additional objectives, or the name of the factions fighting are still guess
work however.
* Fixed a bug where newly added factions to a system weren't properly
recognised. Thanks to Shakaka.
* Added option to ignore FleetCarrier actions.
* Added an "Open in Explorer" button to journal locations.
## 0.3.2 on 19.04.2023
* Orthrus payout has been adapted to 14.02 values.
* Old Thargoid payout values have been removed, so very old logs now no
longer parse correctly. The old value clashed with new ones, making
detection of interceptor kills difficult.
* Fixed a problem regarding the proper detection of station owner, and
thus for whom the tool attributed market buy and market sell.
* Added GUI options to ignore some less important BGS entries.
* One line report has changed to show summaries
* Summaries have been streamlined:
* semi-colons now separate entries
* missing BGS actions (like S&R) have received a summary
* Trade has been renamed to "Sold" and "Bought"
* Sold now shows tons of cargo sold, plus profit if available
* Bought shows tons of cargo bought
## 0.3.1 on 26.02.2023
* Fixed: AX combat zones caused crashes with the summary.
* Fixed: Summaries for thargoid kills didn't render probably.
## 0.3.0 on 26.02.2023
* Move to MahApps toolkit, which replaces extended toolkit.
* Introduce themes (by MahApps) including dark mode.
* Add summary to the two bigger BGS log types.
* Remove organic data from BGS tool, as it has been confirmed that
it does not contribute to the BGS.
* Add a few more English mission names.
## 0.2.6 on 04.02.2023
* Update Post 14.02 Thargoid bounties

View File

@ -3,13 +3,13 @@ using System.Linq;
using System.Collections.Generic;
using System.Text;
using EliteBGS.LogGenerator;
using System.Reflection;
namespace EliteBGS;
public class DiscordLogGenerator {
protected List<LogFormatter> formatters = new List<LogFormatter>() {
new MissionFormat(),
new FailedMissionFormat(),
new MurderFormat(),
new VoucherFormat(),
new ThargoidFormatter(),
@ -21,8 +21,69 @@ public class DiscordLogGenerator {
new CargoSoldFormatter(),
new VistaGenomicsFormat(),
new SearchAndRescueFormat(),
new MeritsGainedFormat(),
};
protected virtual string GetToolVersion() {
Version v = Assembly.GetCallingAssembly().GetName().Version;
string ver;
if (v == null) {
ver = "v?.?.?";
} else {
ver = "v" + v.ToString(3);
}
return string.Format("EliteBGS {0}", ver);
}
protected virtual DateTime? GetDateOfEarliestEntry(Objective objective) {
var it = objective
.Transactions
.OrderBy(x => x.CompletedAtDateTime)
.FirstOrDefault()
;
if (it != null) {
return it.CompletedAtDateTime;
}
return null;
}
protected virtual DateTime? GetDateOfLatestEntry(Objective objective) {
var it = objective
.Transactions
.OrderByDescending(x => x.CompletedAtDateTime)
.FirstOrDefault()
;
if (it != null) {
return it.CompletedAtDateTime;
}
return null;
}
protected virtual string GenerateSummary(Objective objective) {
StringBuilder sb = new StringBuilder();
foreach (var formatter in formatters) {
string summary = "";
try {
summary = formatter.GenerateSummary(objective);
} catch (NotImplementedException) {
}
if (string.IsNullOrEmpty(summary)) {
continue;
}
if (sb.Length > 0) {
sb.Append("; ");
}
sb.Append(summary);
}
return sb.ToString();
}
protected virtual string GenerateHeader() {
return "";
}
@ -49,8 +110,20 @@ public class DiscordLogGenerator {
.Count()
;
log.AppendFormat("**Date:** {0}\n", DateTime.Now.ToString("dd/MM/yyyy"));
string summary = GenerateSummary(objective);
var earliest = GetDateOfEarliestEntry(objective);
var latest = GetDateOfLatestEntry(objective);
if (earliest != null && latest != null) {
log.AppendFormat("**Date:** {0} - {1}\n",
GetDateOfEarliestEntry(objective),
GetDateOfLatestEntry(objective)
);
}
log.AppendFormat("**Target:** {0}\n", location);
if (!string.IsNullOrEmpty(summary)) {
log.AppendFormat("**Summary:** {0}\n", summary);
}
if (legacycount > 0) {
log.AppendFormat("**Warning:** Some actions were performed on ED Legacy\n");
}
@ -73,6 +146,16 @@ public class DiscordLogGenerator {
return log;
}
public virtual string Name {
get { return "GenericLog"; }
}
protected virtual string BotHeader() {
var sb = new StringBuilder();
sb.AppendFormat("**Bot-Header:** {0}; {1}\n", GetToolVersion(), this.Name);
return sb.ToString();
}
public virtual string GenerateDiscordLog(Report report) {
StringBuilder log = new StringBuilder();
@ -88,6 +171,7 @@ public class DiscordLogGenerator {
return "";
}
log.AppendFormat("{0}", BotHeader());
log.AppendFormat("{0}", GenerateHeader());
foreach (Objective objective in objectives) {
@ -115,4 +199,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.2.6</Version>
<Version>0.4.3</Version>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<UseWindowsForms>true</UseWindowsForms>
<UseWPF>true</UseWPF>
@ -16,29 +16,34 @@
<Title>BGS reporting and logging tool for Elite:Dangerous</Title>
<Authors>nola</Authors>
<Copyright>Copyright 2019 by Florian Stinglmayr</Copyright>
<RepositoryUrl>https://git.aror.org/florian/EDBGS</RepositoryUrl>
<RepositoryUrl>https://codeberg.org/nola/EDBGS</RepositoryUrl>
<PackageTags>ED;Elite Dangerous;BGS</PackageTags>
<PackageProjectUrl>https://bgs.n0la.org</PackageProjectUrl>
<PackageProjectUrl>https://salusinvicta.org/bgstool/</PackageProjectUrl>
<PackageReadmeFile>README.md</PackageReadmeFile>
<Description>Elite: Dangerous BGS logging and reporting tool
</Description>
<PackageIcon>logo_v5.png</PackageIcon>
</PropertyGroup>
<ItemGroup>
<None Remove="main-page.png" />
</ItemGroup>
<ItemGroup>
<Resource Include="main-page.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Resource>
<None Update="logo_v5.png">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
<None Update="README.md">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
<None Update="docs\CHANGELOG.md">
<None Update="CHANGELOG.md">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<Resource Include="docs\main-page.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Resource>
</ItemGroup>
<ItemGroup>
<Content Include="LICENCE.txt">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
@ -48,23 +53,12 @@
<Resource Include="Salus.ico" />
</ItemGroup>
<ItemGroup>
<Resource Include="logo_v4.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Resource>
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\EliteBGS.ico" />
</ItemGroup>
<ItemGroup>
<Resource Include="EliteBGS.ico" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Extended.Wpf.Toolkit" Version="4.5.0" />
<PackageReference Include="MahApps.Metro" Version="2.4.10" />
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
<PackageReference Include="Microsoft.Windows.Compatibility" Version="7.0.0" />
<PackageReference Include="Microsoft.Windows.Compatibility" Version="8.0.4" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Ookii.Dialogs.Wpf" Version="5.0.1" />
</ItemGroup>
<ItemGroup>
@ -83,4 +77,7 @@
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<Folder Include="Resources\" />
</ItemGroup>
</Project>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 200 KiB

View File

@ -1,7 +1,18 @@
namespace EliteBGS;
using System.Collections.Generic;
using System.Windows.Documents;
namespace EliteBGS;
public class GenericDiscordLog : DiscordLogGenerator {
public override string ToString() {
return "Generic Log";
return "Generic";
}
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

@ -1,5 +1,6 @@
using System.Linq;
using System.Text;
using System.Windows.Documents;
using EDPlayerJournal;
using EDPlayerJournal.BGS;
@ -53,4 +54,24 @@ public class CargoSoldFormatter : LogFormatter {
return builder.ToString();
}
public string GenerateSummary(Objective objective) {
SellCargo[] sold = objective.EnabledOfType<SellCargo>().ToArray();
long totalProfit = sold.Sum(x => x.Profit);
long tons = sold.Sum(x => x.Amount);
if (tons <= 0) {
return "";
}
StringBuilder builder = new();
builder.Append("Sold: ");
builder.AppendFormat("{0}t", tons);
if (totalProfit >= 100000) {
builder.AppendFormat(", {0} profit", Credits.FormatMillions(totalProfit));
}
return builder.ToString();
}
}

View File

@ -19,4 +19,15 @@ public class CartographicsFormat : LogFormatter {
pages, Credits.FormatCredits(sum)
);
}
public string GenerateSummary(Objective objective) {
Cartographics[] sold = objective.EnabledOfType<Cartographics>().ToArray();
long totalProfit = sold.Sum(x => x.TotalSum);
if (totalProfit >= 100000) {
return string.Format("Explo: {0}", Credits.FormatMillions(totalProfit));
}
return "";
}
}

View File

@ -9,7 +9,8 @@ class CombatZoneFormat : LogFormatter {
public string GenerateLog(Objective objective) {
var logs = objective
.EnabledOfType<CombatZone>()
.GroupBy(x => new { x.Type, x.Grade })
.OrderBy(x => (CombatZones.DifficultyRank(x.Grade) ?? 0))
.GroupBy(x => new { x.Type, x.Grade, x.Settlement })
.ToDictionary(x => x.Key, x => x.ToList())
;
StringBuilder builder = new StringBuilder();
@ -22,6 +23,11 @@ class CombatZoneFormat : LogFormatter {
int optionals = log.Value
.Sum(x => x.OptionalObjectivesCompleted)
;
var settlements = log.Value
.Select(x => x.Settlement)
.Distinct()
;
string settl = string.Join(", ", settlements);
if (!string.IsNullOrEmpty(log.Key.Grade)) {
builder.AppendFormat("Won {0}x {1} {2} Combat Zone(s)",
log.Value.Count,
@ -38,9 +44,57 @@ class CombatZoneFormat : LogFormatter {
if (optionals > 0) {
builder.AppendFormat(" (with {0} optional objectives)", optionals);
}
if (!string.IsNullOrEmpty(settl)) {
builder.AppendFormat(" (at {0})", settl);
}
builder.Append("\n");
}
return builder.ToString().Trim();
}
public string GenerateSummary(Objective objective) {
var logs = objective
.EnabledOfType<CombatZone>()
.OrderBy(x => (CombatZones.DifficultyRank(x.Grade) ?? 0))
.GroupBy(x => new { x.Type, x.Grade })
.ToDictionary(x => x.Key, x => x.ToList())
;
StringBuilder builder = new StringBuilder();
if (logs == null || logs.Count() <= 0) {
return "";
}
foreach (var log in logs) {
int optionals = log.Value
.Sum(x => x.OptionalObjectivesCompleted)
;
if (builder.Length > 0) {
builder.Append(", ");
}
if (!string.IsNullOrEmpty(log.Key.Grade)) {
string grade = log.Key.Grade.Substring(0, 1);
if (log.Key.Grade == CombatZones.DifficultyVeryHigh) {
grade = "VH";
}
builder.AppendFormat("CZ: {0}x{1}{2}",
log.Value.Count,
grade,
log.Key.Type.Substring(0, 1)
);
} else {
builder.AppendFormat("CZ: {0}x?{1}",
log.Value.Count,
log.Key.Type.Substring(0, 1)
);
}
if (optionals > 0) {
builder.AppendFormat("+ {0} OPTS", optionals);
}
}
return builder.ToString().Trim();
}
}

View File

@ -1,34 +0,0 @@
using System.Linq;
using System.Text;
using EDPlayerJournal.Entries;
using EDPlayerJournal.BGS;
namespace EliteBGS.LogGenerator;
public class FailedMissionFormat : LogFormatter {
public string GenerateLog(Objective objective) {
var missions = objective.EnabledOfType<MissionFailed>();
if (missions.Count <= 0) {
return "";
}
StringBuilder builder = new StringBuilder();
var grouping = missions
.GroupBy(x => x.Mission.IsOnFoot)
;
foreach (var group in grouping) {
int amount = group.Count();
if (group.Key) {
builder.AppendFormat("Failed {0} On Foot Mission(s)\n", amount);
} else {
builder.AppendFormat("Failed {0} Ship Mission(s)\n", amount);
}
}
return builder.ToString().Trim();
}
}

View File

@ -25,4 +25,8 @@ public class GenericFormat<Type> : LogFormatter where Type : Transaction {
return builder.ToString();
}
public virtual string GenerateSummary(Objective objective) {
throw new System.NotImplementedException();
}
}

View File

@ -1,6 +1,38 @@
using EDPlayerJournal.BGS;
using EDPlayerJournal;
using EDPlayerJournal.BGS;
using System.Linq;
using System.Text;
namespace EliteBGS.LogGenerator;
public class KillBondsFormat : GenericFormat<FactionKillBonds> {
public override string GenerateSummary(Objective objective) {
StringBuilder builder = new StringBuilder();
var bonds = objective
.EnabledOfType<FactionKillBonds>()
.GroupBy(x => x.VictimFaction)
.ToDictionary(x => x.Key, x => x.ToList())
;
if (bonds.Count <= 0) {
return "";
}
builder.Append("Killbonds: ");
foreach (var entry in bonds) {
long sum = (long)entry.Value.Sum(x => (decimal)x.TotalSum);
builder.AppendFormat("{0} against {1}, ",
Credits.FormatMillions(sum),
entry.Key
);
}
if (builder.Length > 2) {
// Remove trailing comma
builder.Remove(builder.Length - 2, 2);
}
return builder.ToString();
}
}

View File

@ -2,4 +2,5 @@
public interface LogFormatter {
string GenerateLog(Objective objective);
string GenerateSummary(Objective objective);
}

View File

@ -1,6 +1,17 @@
using EDPlayerJournal.BGS;
using System.Linq;
namespace EliteBGS.LogGenerator;
public class MarketBuyFormat : GenericFormat<BuyCargo> {
public override string GenerateSummary(Objective objective) {
long tons = objective
.EnabledOfType<BuyCargo>()
.Sum(x => x.Amount)
;
if (tons <= 0) {
return "";
}
return string.Format("Bought: {0}t", tons);
}
}

View File

@ -0,0 +1,47 @@
using System.Linq;
using System.Text;
using EDPlayerJournal.BGS;
namespace EliteBGS.LogGenerator;
public class MeritsGainedFormat : LogFormatter {
public string GenerateLog(Objective objective) {
var builder = new StringBuilder();
var merits = objective
.EnabledOfType<MeritsGained>()
.GroupBy(x => x.Power)
.ToDictionary(x => x.Key, x => x.Sum(x => x.Merits))
;
if (merits == null || merits.Count == 0) {
return "";
}
foreach (var merit in merits) {
builder.AppendFormat("{0} merits gained for {1}\n", merit.Value, merit.Key);
}
return builder.ToString().Trim();
}
public string GenerateSummary(Objective objective) {
var builder = new StringBuilder();
var merits = objective
.EnabledOfType<MeritsGained>()
.GroupBy(x => x.Power)
.ToDictionary(x => x.Key, x => x.Sum(x => x.Merits))
;
if (merits == null || merits.Count == 0) {
return "";
}
foreach (var merit in merits) {
builder.AppendFormat("MRT: {0}, {1}; ", merit.Key, merit.Value);
}
return builder.ToString().Trim().TrimEnd(';');
}
}

View File

@ -16,4 +16,8 @@ public class MicroResourcesFormat : LogFormatter {
return string.Format("Sold {0} worth of Micro Resources\n",
Credits.FormatCredits(sum));
}
public string GenerateSummary(Objective objective) {
return "";
}
}

View File

@ -6,17 +6,73 @@ using EDPlayerJournal.BGS;
namespace EliteBGS.LogGenerator;
public class MissionFormat : LogFormatter {
private string GenerateFailedLog(Objective objective) {
var missions = objective.EnabledOfType<MissionFailed>();
if (missions.Count <= 0) {
return "";
}
StringBuilder builder = new StringBuilder();
var grouping = missions
.GroupBy(x => x.Mission.IsOnFoot)
;
foreach (var group in grouping) {
int amount = group.Count();
if (group.Key) {
builder.AppendFormat("Failed {0} On Foot Mission(s)\n", amount);
} else {
builder.AppendFormat("Failed {0} Ship Mission(s)\n", amount);
}
}
return builder.ToString().Trim();
}
private string GenerateFailedSummary(Objective objective) {
var missions = objective.EnabledOfType<MissionFailed>();
if (missions.Count <= 0) {
return "";
}
StringBuilder sb = new();
int onFootFails = missions.Where(x => x.Mission.IsOnFoot).Count();
int shipFails = missions.Where(x => !x.Mission.IsOnFoot).Count();
sb.Append("Fails: ");
if (onFootFails > 0) {
sb.AppendFormat("{0} Ground", onFootFails);
}
if (shipFails > 0) {
if (onFootFails > 0) {
sb.Append(", ");
}
sb.AppendFormat("{0} Ship", shipFails);
}
return sb.ToString();
}
public string GenerateLog(Objective objective) {
Dictionary<string, Dictionary<string, int>> collated = new();
Dictionary<string, ulong> passengers = new();
StringBuilder output = new StringBuilder();
int total_influence = 0;
long total_influence = 0;
bool hadmissions = false;
var missions = objective.EnabledOfType<MissionCompleted>();
var support = objective.EnabledOfType<InfluenceSupport>();
var failed = objective.EnabledOfType<MissionFailed>();
if ((missions == null || missions.Count == 0) &&
(support == null || support.Count == 0)) {
(support == null || support.Count == 0) &&
(failed == null || failed.Count == 0)) {
return "";
}
@ -30,6 +86,7 @@ public class MissionFormat : LogFormatter {
++collated[m.MissionName][m.Influence];
hadmissions = true;
total_influence += m.Influence.Length;
if (m.AcceptedEntry != null &&
@ -60,20 +117,64 @@ public class MissionFormat : LogFormatter {
output.Append("\n");
// Handle failed missions, and add them to the log and influence tally
string failedlog = GenerateFailedLog(objective);
if (!string.IsNullOrEmpty(failedlog)) {
output.Append(failedlog);
output.Append("\n");
}
if (failed.Count > 0) {
hadmissions = true;
}
total_influence += failed.Sum(x => x.InfluenceAmount);
foreach (InfluenceSupport inf in support) {
output.Append(inf.ToString());
output.Append("\n");
total_influence += inf.Influence.Length;
hadmissions = true;
total_influence += inf.Influence.InfluenceAmount;
}
if (support.Count() > 0) {
if (support.Count > 0) {
output.Append("\n");
}
if (total_influence > 0) {
if (hadmissions) {
output.AppendFormat("Total Influence: {0}", total_influence);
}
return output.ToString().Trim();
}
public string GenerateSummary(Objective objective) {
bool hadmissions = objective
.EnabledOfType<MissionCompleted>()
.Count > 0
;
long influence = objective
.EnabledOfType<MissionCompleted>()
.Sum(x => x.Influence.Length)
;
long support = objective
.EnabledOfType<InfluenceSupport>()
.Sum(x => x.Influence.InfluenceAmount)
;
long failed = objective
.EnabledOfType<MissionFailed>()
.Sum(x => x.InfluenceAmount)
;
if (influence == 0 && support == 0 && failed == 0 && !hadmissions) {
return "";
}
string failedsummary = GenerateFailedSummary(objective);
string summary = string.Format("INF: {0}", influence + support + failed);
if (!string.IsNullOrEmpty(failedsummary)) {
string.Join("; ", summary, failedsummary);
}
return summary;
}
}

View File

@ -1,9 +1,7 @@
using EDPlayerJournal.BGS;
using System.Collections.Generic;
using EDPlayerJournal;
using EDPlayerJournal.BGS;
using System.Text;
using System;
using System.Linq;
using EDPlayerJournal;
namespace EliteBGS.LogGenerator;
@ -36,13 +34,27 @@ public class MurderFormat : LogFormatter {
type = "person";
}
}
builder.AppendFormat("Murdered {0} {1} (Bounties: {2}, Fines: {3})",
long bounties = log.Value.Sum(x => x.Bounties);
builder.AppendFormat("Murdered {0} {1} (Bounties: {2})\n",
log.Value.Count, type,
log.Value.Sum(x => x.Bounties),
log.Value.Sum(x => x.Fines)
Credits.FormatMillions(bounties)
);
}
return builder.ToString();
return builder.ToString().Trim();
}
public string GenerateSummary(Objective objective) {
long murders = objective
.EnabledOfType<FoulMurder>()
.Where(x => x.CrimeType == CrimeTypes.Murder || x.CrimeType == CrimeTypes.OnFootMurder)
.Count()
;
if (murders <= 0) {
return "";
}
return string.Format("Kills: {0}", murders);
}
}

View File

@ -1,6 +1,22 @@
using EDPlayerJournal.BGS;
using EDPlayerJournal;
using EDPlayerJournal.BGS;
using System.Linq;
namespace EliteBGS.LogGenerator;
public class SearchAndRescueFormat : GenericFormat<SearchAndRescue> {
public override string GenerateSummary(Objective objective) {
long tons = objective
.EnabledOfType<SearchAndRescue>()
.Sum(x => x.Count)
;
long profit = objective
.EnabledOfType<SearchAndRescue>()
.Sum(x => x.Reward)
;
if (tons <= 0) {
return "";
}
return string.Format("S&R: {0}t, {1} profit", tons, Credits.FormatMillions(profit));
}
}

View File

@ -29,4 +29,37 @@ public class ThargoidFormatter : LogFormatter {
return builder.ToString();
}
public string GenerateSummary(Objective objective) {
List<ThargoidKill> kills = objective.EnabledOfType<ThargoidKill>().ToList();
if (kills.Count == 0 ) {
return "";
}
int drones = kills.Where(x => x.ThargoidType == ThargoidVessel.Revenant).Count();
int scouts = kills.Where(x => x.ThargoidType == ThargoidVessel.Scout).Count();
int interceptors = kills.Count - scouts - drones;
StringBuilder builder = new StringBuilder();
builder.Append("AX: ");
if (interceptors > 0) {
builder.AppendFormat("{0} INT", interceptors);
}
if (scouts > 0) {
if (interceptors > 0) {
builder.Append(", ");
}
builder.AppendFormat("{0} SCT", scouts);
}
if (drones > 0) {
if (interceptors > 0 || scouts > 0) {
builder.Append(", ");
}
builder.AppendFormat("{0} DRN", drones);
}
return builder.ToString();
}
}

View File

@ -1,6 +1,18 @@
using EDPlayerJournal.BGS;
using EDPlayerJournal;
using EDPlayerJournal.BGS;
using System.Linq;
namespace EliteBGS.LogGenerator;
class VistaGenomicsFormat : GenericFormat<OrganicData> {
public override string GenerateSummary(Objective objective) {
long profit = objective
.EnabledOfType<OrganicData>()
.Sum(x => x.TotalValue)
;
if (profit <= 0) {
return "";
}
return string.Format("Organic: {0} Profit", Credits.FormatMillions(profit));
}
}

View File

@ -19,10 +19,38 @@ public class VoucherFormat : LogFormatter {
}
foreach (var m in missions) {
ulong total = (ulong)m.Value.Sum(x => (decimal)x.TotalSum);
builder.AppendFormat("Handed in {0} vouchers: {1}\n", m.Key, Credits.FormatCredits(total));
long total = (long)m.Value.Sum(x => (decimal)x.TotalSum);
builder.AppendFormat("Handed in {0} vouchers: {1}\n", m.Key, Credits.FormatMillions(total));
}
return builder.ToString().Trim();
}
public string GenerateSummary(Objective objective) {
long bounties = objective
.EnabledOfType<Vouchers>()
.Where(x => x.Type == "Bounty")
.Sum(x => x.TotalSum)
;
long bonds = objective
.EnabledOfType<Vouchers>()
.Where(x => x.Type == "Combat Bond")
.Sum(x => x.TotalSum)
;
StringBuilder sb = new();
if (bounties > 0) {
sb.AppendFormat("Bounties: {0}", Credits.FormatMillions(bounties));
}
if (bonds > 0) {
if (sb.Length > 0) {
sb.Append(", ");
}
sb.AppendFormat("Bonds: {0}", Credits.FormatMillions(bonds));
}
return sb.ToString();
}
}

View File

@ -1,20 +1,149 @@
<Window
<mah:MetroWindow
xmlns:mah="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:abc="http://wpfcontrols.com/"
xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit"
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}}">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
</Style>
<local:MinusFortyFiveConverter x:Key="MinusFortyFiveConverter" />
<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
HorizontalAlignment="Stretch"
Width="{Binding ActualWidth, ElementName=entries, Converter={StaticResource MinusFortyFiveConverter}}"
>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center" Margin="0,2,0,2">
<CheckBox Focusable="False" IsChecked="{Binding IsEnabled}" VerticalAlignment="Center"/>
<TextBlock Text="System: " Margin="2,0,0,0"/>
<TextBlock Text="{Binding SystemName}" FontWeight="DemiBold"/>
</StackPanel>
</Grid>
</HierarchicalDataTemplate>
<HierarchicalDataTemplate DataType="{x:Type local:Objective}"
ItemsSource="{Binding Path=UITransactions}"
ItemContainerStyle="{StaticResource StretchingTreeViewStyle}">
<Grid
HorizontalAlignment="Stretch"
Width="{Binding ActualWidth, ElementName=entries, Converter={StaticResource MinusFortyFiveConverter}}"
>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center" Margin="0,2,0,2">
<CheckBox Focusable="False" IsChecked="{Binding IsEnabled}" VerticalAlignment="Center"/>
<TextBlock Text="Faction: " Margin="2,0,0,0"/>
<TextBlock Text="{Binding Faction}" FontWeight="DemiBold"/>
</StackPanel>
<Separator Visibility="Hidden" Grid.Column="1" HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch" />
<StackPanel Grid.Column="2" HorizontalAlignment="Right" VerticalAlignment="Center" Orientation="Horizontal">
<ToggleButton x:Name="ToggleAll" Content="Toggle All" Click="ToggleAll_Click" IsChecked="True" IsThreeState="False"/>
<Separator Margin="2,0,2,0" />
<Button x:Name="AddCombatZone" Content="Add Combat Zone" Click="AddCombatZone_Click" />
</StackPanel>
</Grid>
</HierarchicalDataTemplate>
<DataTemplate DataType="{x:Type local:UITransaction}">
<!-- This will stretch out the width of the item-->
<Grid Initialized="Transaction_Initialized"
HorizontalAlignment="Left"
Width="{Binding ActualWidth, ElementName=entries, Converter={StaticResource MinusFortyFiveConverter}}"
Margin="0,2,0,2"
>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" Orientation="Horizontal" HorizontalAlignment="Stretch" VerticalAlignment="Center">
<CheckBox Focusable="False" IsChecked="{Binding IsEnabled}" VerticalAlignment="Center"/>
<TextBlock Text="{Binding CompletedAt}" Margin="2,0,2,0" HorizontalAlignment="Right" TextAlignment="Center"/>
<TextBlock Text="{Binding Name}" FontWeight="DemiBold" TextAlignment="Center"/>
</StackPanel>
<StackPanel Grid.Column="2" Orientation="Horizontal" HorizontalAlignment="Right" x:Name="CombatZone" Visibility="{Binding IsCombatZone}">
<Expander Header="Optional Objectives" Visibility="{Binding IsShipCombatZone}">
<StackPanel Orientation="Vertical">
<ToggleButton x:Name="CapitalShip" Margin="2,0,2,0" Content="Capital Ship" IsChecked="{Binding HasCapitalShip, Mode=TwoWay}" IsThreeState="False"/>
<ToggleButton x:Name="AlliedCaptain" Margin="2,0,2,0" Content="Allied Captain" IsChecked="{Binding HasAlliedCaptain, Mode=TwoWay}" IsThreeState="False"/>
<ToggleButton x:Name="EnemyCaptain" Margin="2,0,2,0" Content="Enemy Captain" IsChecked="{Binding HasEnemyCaptain, Mode=TwoWay}" IsThreeState="False"/>
<ToggleButton x:Name="AlliedCorrespondent" Margin="2,0,2,0" Content="Allied Correspondent" IsChecked="{Binding HasAlliedCorrespondent, Mode=TwoWay}" IsThreeState="False"/>
<ToggleButton x:Name="EnemyCorrespondent" Margin="2,0,2,0" Content="Enemy Correspondent" IsChecked="{Binding HasEnemyCorrespondent, Mode=TwoWay}" IsThreeState="False"/>
<ToggleButton x:Name="SpecOps" Margin="2,0,2,0" Content="Spec Ops" IsChecked="{Binding HasSpecOps, Mode=TwoWay}" IsThreeState="False"/>
</StackPanel>
</Expander>
<Expander Header="Difficulty">
<StackPanel Orientation="Vertical">
<Button x:Name="Low" Content="Low" Click="Low_Click"/>
<Button x:Name="Med" Content="Med" Click="Med_Click"/>
<Button x:Name="High" Content="High" Click="High_Click"/>
<Button x:Name="VeryHigh" Content="Very High" Click="VeryHigh_Click" />
</StackPanel>
</Expander>
<Expander Header="Type">
<StackPanel Orientation="Vertical">
<Button Content="Ground" x:Name="Ground" Click="Ground_Click"/>
<Button Content="Ship" x:Name="Ship" Click="Ship_Click"/>
<Button Content="AX" x:Name="Thargoid" Click="Thargoid_Click"/>
<Button Content="Power" x:Name="Power" Click="Power_Click"/>
</StackPanel>
</Expander>
</StackPanel>
<StackPanel Grid.Column="2" Orientation="Horizontal" HorizontalAlignment="Right" x:Name="SellCargo" Visibility="{Binding IsSellCargo}">
<TextBlock Text="Adjust Profit: " TextAlignment="Center" />
<TextBox x:Name="Profit" MinWidth="80" HorizontalContentAlignment="Right" Text="{Binding Profit, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" LostFocus="Profit_LostFocus" KeyUp="Profit_KeyUp"/>
</StackPanel>
</Grid>
</DataTemplate>
</Window.Resources>
<mah:MetroWindow.RightWindowCommands>
<mah:WindowCommands ShowSeparators="False">
<TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" Margin="5,0,5,0">
<Hyperlink x:Name="URL" NavigateUri="https://salusinvicta.org/bgstool/" RequestNavigate="URL_RequestNavigate">Homepage</Hyperlink>
</TextBlock>
<TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" Margin="5,0,15,0">
<Hyperlink x:Name="SRC" NavigateUri="https://codeberg.org/nola/EDBGS" RequestNavigate="URL_RequestNavigate">Source</Hyperlink>
</TextBlock>
<mah:ToggleSwitch Content="Dark Theme" x:Name="SwitchTheme" IsOn="True" Toggled="SwitchTheme_Toggled"/>
</mah:WindowCommands>
</mah:MetroWindow.RightWindowCommands>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
@ -22,9 +151,9 @@
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TabControl>
<TabControl Style="{DynamicResource MahApps.Styles.TabControl.Animated}">
<TabItem Header="Current Objectives">
<Grid Background="#FFE5E5E5">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
@ -41,105 +170,36 @@
<Button x:Name="ParseJournal" Content="Parse Journal" VerticalAlignment="Center" Click="ParseJournal_Click" HorizontalAlignment="Center"/>
<Separator Margin="1" VerticalAlignment="Center" MinWidth="1" HorizontalAlignment="Center" MinHeight="22"/>
<Label Content="From (UTC):" VerticalAlignment="Center" VerticalContentAlignment="Center" HorizontalAlignment="Center"/>
<xctk:DateTimePicker x:Name="startdate" Height="26.2857142857143" VerticalAlignment="Center" HorizontalAlignment="Center"/>
<Label Content="To (UTC):" Height="26.2857142857143" VerticalAlignment="Top"/>
<xctk:DateTimePicker x:Name="enddate" Height="26.2857142857143" VerticalAlignment="Center" HorizontalAlignment="Center"/>
<mah:DateTimePicker x:Name="startdate" Height="26.2857142857143" VerticalAlignment="Center" HorizontalAlignment="Center"/>
<Label Content="To (UTC):" Height="26.2857142857143" VerticalAlignment="Center" HorizontalAlignment="Center"/>
<mah:DateTimePicker x:Name="enddate" Height="26.2857142857143" VerticalAlignment="Center" HorizontalAlignment="Center"/>
<Separator Margin="1" VerticalAlignment="Center" MinWidth="1" HorizontalAlignment="Center" MinHeight="22"/>
<Button x:Name="ResetTime" Content="Reset Time" Click="ResetTime_Click" />
<Separator Margin="1" VerticalAlignment="Center" MinWidth="1" HorizontalAlignment="Center" MinHeight="22"/>
<Button x:Name="ManuallyParse" Content="Manually Parse" Click="ManuallyParse_Click" />
</ToolBar>
<ToolBar Grid.Row="1" HorizontalAlignment="Left" Height="36" VerticalAlignment="Top" Width="Auto" Grid.ColumnSpan="3">
<Button x:Name="GenerateDiscord" Content="Generate Discord Report" VerticalAlignment="Stretch" Margin="0,0,0,0" VerticalContentAlignment="Center" Click="GenerateDiscord_Click" Height="26"/>
<ToolBar Grid.Row="1" HorizontalAlignment="Left" Height="Auto" VerticalAlignment="Top" Width="Auto" Grid.ColumnSpan="3">
<Button x:Name="GenerateDiscord" Content="Generate Discord Report" VerticalAlignment="Stretch" Margin="0,0,0,0" VerticalContentAlignment="Center" Click="GenerateDiscord_Click"/>
<Separator />
<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"
x:Name="entries" Margin="0,0,0,0"
Grid.ColumnSpan="3" Grid.Row="2"
Grid.ColumnSpan="3"
Grid.Row="2"
KeyUp="entries_KeyUp"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
ItemsSource="{Binding Source={StaticResource ObjectivesBasedOnSystem}}"
>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate DataType="{x:Type local:Objective}" ItemsSource="{Binding UITransactions}" ItemContainerStyle="{StaticResource StretchingTreeViewStyle}">
<Grid
HorizontalAlignment="Stretch"
Width="{Binding ActualWidth, ElementName=entries, Converter={StaticResource MinusFortyFiveConverter}}"
>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center" Margin="0,2,0,2">
<CheckBox Focusable="False" IsChecked="{Binding IsEnabled}" VerticalAlignment="Center"/>
<TextBlock Text="System: " Visibility="{Binding HasSystem}" Margin="2,0,0,0"/>
<TextBlock Text="{Binding System}" FontWeight="DemiBold" Visibility="{Binding HasSystem}"/>
<TextBlock Text="Faction: " Visibility="{Binding HasFaction}" Margin="2,0,0,0"/>
<TextBlock Text="{Binding Faction}" FontWeight="DemiBold" Visibility="{Binding HasFaction}"/>
</StackPanel>
<Separator Visibility="Hidden" Grid.Column="1" HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch" />
<StackPanel Grid.Column="2" HorizontalAlignment="Right" VerticalAlignment="Center" Orientation="Horizontal">
<ToggleButton x:Name="ToggleAll" Content="Toggle All" Click="ToggleAll_Click" IsChecked="True" IsThreeState="False"/>
<Separator Margin="2,0,2,0" />
<Button x:Name="AddCombatZone" Content="Add Combat Zone" Click="AddCombatZone_Click" />
</StackPanel>
</Grid>
<HierarchicalDataTemplate.ItemTemplate>
<HierarchicalDataTemplate>
<!-- This will stretch out the width of the item-->
<Grid Initialized="Transaction_Initialized"
HorizontalAlignment="Stretch"
Width="{Binding ActualWidth, ElementName=entries, Converter={StaticResource MinusFortyFiveConverter}}"
Margin="0,2,0,2"
>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" Orientation="Horizontal" HorizontalAlignment="Stretch" VerticalAlignment="Center">
<CheckBox Focusable="False" IsChecked="{Binding IsEnabled}" VerticalAlignment="Center"/>
<TextBlock Text="{Binding CompletedAt}" Margin="2,0,2,0" HorizontalAlignment="Right" TextAlignment="Center"/>
<TextBlock Text="{Binding Name}" FontWeight="DemiBold" TextAlignment="Center"/>
</StackPanel>
<StackPanel Grid.Column="2" Orientation="Horizontal" HorizontalAlignment="Right" x:Name="CombatZone" Visibility="{Binding IsCombatZone}">
<Expander Header="Optional Objectives" Visibility="{Binding IsShipCombatZone}">
<StackPanel Orientation="Vertical">
<ToggleButton x:Name="CapitalShip" Margin="2,0,2,0" Content="Capital Ship" IsChecked="{Binding HasCapitalShip, Mode=TwoWay}" IsThreeState="False"/>
<ToggleButton x:Name="Captain" Margin="2,0,2,0" Content="Captain" IsChecked="{Binding HasCaptain, Mode=TwoWay}" IsThreeState="False"/>
<ToggleButton x:Name="Correspondent" Margin="2,0,2,0" Content="Correspondent" IsChecked="{Binding HasCorrespondent, Mode=TwoWay}" IsThreeState="False"/>
<ToggleButton x:Name="SpecOps" Margin="2,0,2,0" Content="Spec Ops" IsChecked="{Binding HasSpecOps, Mode=TwoWay}" IsThreeState="False"/>
</StackPanel>
</Expander>
<Expander Header="Difficulty">
<StackPanel Orientation="Vertical">
<Button x:Name="Low" Content="Low" Click="Low_Click"/>
<Button x:Name="Med" Content="Med" Click="Med_Click"/>
<Button x:Name="High" Content="High" Click="High_Click"/>
<Button x:Name="VeryHigh" Content="Very High" Click="VeryHigh_Click" />
</StackPanel>
</Expander>
<Expander Header="Type">
<StackPanel Orientation="Vertical">
<Button Content="Ground" x:Name="Ground" Click="Ground_Click"/>
<Button Content="Ship" x:Name="Ship" Click="Ship_Click"/>
<Button Content="AX" x:Name="Thargoid" Click="Thargoid_Click"/>
</StackPanel>
</Expander>
</StackPanel>
<StackPanel Grid.Column="2" Orientation="Horizontal" HorizontalAlignment="Right" x:Name="SellCargo" Visibility="{Binding IsSellCargo}">
<TextBlock Text="Adjust Profit: " TextAlignment="Center" />
<TextBox x:Name="Profit" MinWidth="80" HorizontalContentAlignment="Right" Text="{Binding Profit, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" LostFocus="Profit_LostFocus" KeyUp="Profit_KeyUp"/>
</StackPanel>
</Grid>
</HierarchicalDataTemplate>
</HierarchicalDataTemplate.ItemTemplate>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
<TreeView.ItemContainerStyle>
<Style TargetType="TreeViewItem">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
@ -150,9 +210,11 @@
<TextBox x:Name="DiscordLog" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Grid.Row="4" Height="Auto" TextWrapping="Wrap" FontFamily="Consolas" FontSize="14" Grid.ColumnSpan="3" AcceptsReturn="True" AcceptsTab="True"/>
</Grid>
</TabItem>
<TabItem Header="Settings" HorizontalAlignment="Left" Height="20" VerticalAlignment="Top" Width="53.7142857142857">
<Grid Background="#FFE5E5E5">
<TabItem Header="Settings">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
@ -171,16 +233,80 @@
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Label Content="Location on disk for the player journal. There is usually no need to change this setting." Grid.Row="0" Grid.ColumnSpan="2" HorizontalAlignment="Left" Margin="0,0,0,0" VerticalAlignment="Top" RenderTransformOrigin="-0.08,0.496"/>
<TextBox x:Name="journallocation" IsReadOnly="true" Text="" Grid.Row="1" Grid.Column="0" Margin="5,0,5,10" TextWrapping="Wrap" />
<Button x:Name="browsejournallocation" Content="Browse" Grid.Row="1" Grid.Column="1" Margin="0,0,0,0" Width="Auto" VerticalAlignment="Top" HorizontalAlignment="Left" Click="browsejournallocation_Click"/>
<Button x:Name="OpenInExplorer" Content="Open Folder" Grid.Row="1" Grid.Column="2" Margin="5,0,0,0" Width="Auto" VerticalAlignment="Top" HorizontalAlignment="Left" Click="OpenInExplorer_Click" />
</Grid>
</GroupBox>
<GroupBox Header="Theme Colour" Height="Auto" Grid.Row="1" VerticalAlignment="Top" Width="Auto" Grid.ColumnSpan="3" Margin="0,5,0,0">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Label Content="Colour of the current theme." Grid.Row="0" Grid.ColumnSpan="2" HorizontalAlignment="Left" Margin="0,0,0,0" VerticalAlignment="Top"/>
<ComboBox x:Name="Colour" IsEditable="False" Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" Width="Auto" SelectionChanged="Colour_SelectionChanged"/>
</Grid>
</GroupBox>
<GroupBox Header="Advanced Parsing Options" Grid.Row="2" VerticalAlignment="Top" Width="Auto" Grid.ColumnSpan="3" Margin="0,5,0,0">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<mah:ToggleSwitch x:Name="NoInfluenceSupport" Grid.Row="0" Grid.ColumnSpan="2" Content="Ignore secondary influence support given out by certain missions" Toggled="NoInfluenceSupport_Toggled"/>
<mah:ToggleSwitch x:Name="NoMarketBuy" Grid.Row="1" Grid.ColumnSpan="2" Content="Ignore commodities bought at stations" Toggled="NoMarketBuy_Toggled"/>
<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="NoPowerplay" Grid.Row="3" Grid.ColumnSpan="2" Content="Ignore Powerplay Merits" Toggled="NoPowerplay_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" HorizontalAlignment="Left" Height="20" VerticalAlignment="Top">
<Grid Background="#FFE5E5E5">
<TabItem Header="Event Log">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
@ -188,8 +314,8 @@
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBox IsReadOnly="True" Grid.Row="1" x:Name="log" Height="Auto" Margin="5" TextWrapping="Wrap" FontFamily="Courier New" Background="{x:Null}"/>
<RichTextBox IsReadOnly="True" HorizontalAlignment="Left" Height="Auto" Margin="5" Width="Auto" VerticalContentAlignment="Stretch" VerticalAlignment="Top" Background="{x:Null}" BorderBrush="{x:Null}" SelectionBrush="{x:Null}">
<TextBox IsReadOnly="True" Grid.Row="1" x:Name="log" Height="Auto" Margin="5" TextWrapping="Wrap" FontFamily="Courier New" />
<RichTextBox IsReadOnly="True" HorizontalAlignment="Left" Height="Auto" Margin="5" Width="Auto" VerticalContentAlignment="Stretch" VerticalAlignment="Top" BorderBrush="{x:Null}" SelectionBrush="{x:Null}">
<FlowDocument>
<Paragraph>
<Run Text="This tool does not recognise every option and/or configuration that E:D outputs through its JSON files."/>
@ -203,4 +329,4 @@
</TabItem>
</TabControl>
</Grid>
</Window>
</mah:MetroWindow>

View File

@ -12,15 +12,18 @@ using EDPlayerJournal.Entries;
using EliteBGS.BGS;
using EliteBGS.Util;
using System.Globalization;
using System.Windows.Forms;
using System.Windows.Controls.Primitives;
using MahApps.Metro.Controls;
using ControlzEx.Theming;
using System.Windows.Media;
using System.Diagnostics;
namespace EliteBGS;
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window {
public partial class MainWindow : MetroWindow {
private PlayerJournal journal;
private Report report;
@ -29,8 +32,8 @@ public partial class MainWindow : Window {
private LoadEntriesWindow loadentries = null;
private static readonly List<DiscordLogGenerator> logtypes = new List<DiscordLogGenerator>() {
new NonaDiscordLog(),
new GenericDiscordLog(),
new NonaDiscordLog(),
new OneLineDiscordLog(),
};
@ -43,6 +46,9 @@ public partial class MainWindow : Window {
/* ignored */
}
Webhooks.ItemsSource = Config.Global.Webhooks;
RefreshPostMenu();
foreach (DiscordLogGenerator type in logtypes) {
LogType.Items.Add(type);
}
@ -55,6 +61,43 @@ public partial class MainWindow : Window {
LogType.SelectedIndex = 0;
}
this.NoInfluenceSupport.IsOn = Config.Global.IgnoreInfluenceSupport;
this.NoMarketBuy.IsOn = Config.Global.IgnoreMarketBuy;
this.NoFleetCarrier.IsOn = Config.Global.IgnoreFleetCarrier;
this.NoPowerplay.IsOn = Config.Global.IgnorePowerplay;
// Apply theme
try {
AddCustomThemes();
string[] colours = ThemeManager.Current.Themes
.Select(x => x.ColorScheme)
.DistinctBy(x => x)
.OrderBy(x => x)
.ToArray()
;
foreach (var colour in colours) {
Colour.Items.Add(colour);
if (!string.IsNullOrEmpty(Config.Global.Colour) &&
string.Compare(colour, Config.Global.Colour, true) == 0) {
Colour.SelectedIndex = Colour.Items.Count - 1;
}
}
if (Colour.SelectedIndex < 0) {
Colour.SelectedIndex = 0;
}
SwitchTheme.IsOn = (string.Compare(Config.Global.Theme, "dark", true) == 0);
ThemeManager.Current.ChangeTheme(this, Config.Global.FullTheme);
} catch (Exception) {
// Theme is invalid, revert back to our standard dark theme
Config.Global.Colour = "Amber";
Config.Global.Theme = "Dark";
}
journal = new PlayerJournal(Config.Global.JournalLocation);
// Set both to now
@ -63,14 +106,54 @@ public partial class MainWindow : Window {
journallocation.Text = Config.Global.JournalLocation;
}
private void AddCustomThemes() {
Dictionary<string, Color> colorThemes = new() {
//{ "HouseSalus", Color.FromRgb(0xBC, 0x94, 0x39) },
{ "HouseSalus", Color.FromRgb(0xED, 0xDA, 0x70) },
{ "NovaNavy", Color.FromRgb(0xA1, 0xA4, 0xDB) },
// Official Red of the Polish Flag
{ "PolskaGurom", Color.FromRgb(0xD4, 0x21, 0x3D) },
// Official Blue in the Armenian Flag
{ "ArmeniaBlue", Color.FromRgb(0x00, 0x33, 0xA0) },
};
foreach (var colourtheme in colorThemes) {
var brush = new SolidColorBrush(colourtheme.Value);
// Add light theme
ThemeManager.Current.AddTheme(new Theme(
"Light." + colourtheme.Key,
"Light." + colourtheme.Key,
"Light",
colourtheme.Key,
colourtheme.Value,
brush,
true,
false)
);
// Add dark theme
ThemeManager.Current.AddTheme(new Theme(
"Dark." + colourtheme.Key,
"Dark." + colourtheme.Key,
"Dark",
colourtheme.Key,
colourtheme.Value,
brush,
true,
false)
);
}
}
private void InitialiseTime() {
DateTime today = DateTime.Today;
DateTime tomorrow = today.AddDays(1);
startdate.CultureInfo = enddate.CultureInfo = CultureInfo.InvariantCulture;
// HOCHKULTUR
startdate.Culture = enddate.Culture = CultureInfo.GetCultureInfo("de-AT");
startdate.Value = today;
enddate.Value = tomorrow;
startdate.SelectedDateTime = today;
enddate.SelectedDateTime = tomorrow;
}
private void TreeView_CheckBox_Updated(object sender, RoutedEventArgs args) {
@ -94,8 +177,20 @@ public partial class MainWindow : Window {
private void HandleEntries(List<Entry> entries, DateTime start, DateTime end) {
try {
TransactionParser parser = new TransactionParser();
List<Transaction> transactions = parser.Parse(entries);
TransactionParser parser = new();
TransactionParserOptions options = new();
options.IgnoreInfluenceSupport = Config.Global.IgnoreInfluenceSupport;
options.IgnoreMarketBuy = Config.Global.IgnoreMarketBuy;
options.IgnoreFleetCarrierFaction = Config.Global.IgnoreFleetCarrier;
options.IgnorePowerplay = Config.Global.IgnorePowerplay;
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
@ -111,7 +206,7 @@ public partial class MainWindow : Window {
transactions.RemoveAll(x => incompletes.Contains(x));
report = new Report(transactions);
this.entries.ItemsSource = report.Objectives;
this.entries.ItemsSource = report.SystemObjectives;
} catch (Exception exception) {
Log("Something went terribly wrong while parsing the E:D player journal.");
Log("Please send this to CMDR Hekateh:");
@ -120,7 +215,7 @@ public partial class MainWindow : Window {
}
private void HandleEntries(List<Entry> entries) {
HandleEntries(entries, startdate.Value ?? DateTime.Now, enddate.Value ?? DateTime.Now);
HandleEntries(entries, startdate.SelectedDateTime ?? DateTime.Now, enddate.SelectedDateTime ?? DateTime.Now);
}
private void Loadentries_EntriesLoaded(List<Entry> lines) {
@ -131,8 +226,8 @@ public partial class MainWindow : Window {
try {
TransactionParser parser = new TransactionParser();
DateTime start = startdate.Value ?? DateTime.Now;
DateTime end = enddate.Value ?? DateTime.Now;
DateTime start = startdate.SelectedDateTime ?? DateTime.Now;
DateTime end = enddate.SelectedDateTime ?? DateTime.Now;
journal.Open(); // Load all files
// Log files only get rotated if you restart the game client. This means that there might
@ -152,6 +247,12 @@ public partial class MainWindow : Window {
HandleEntries(entries, start, end);
GenerateLog();
var errors = journal.AllErrors;
foreach (var error in errors) {
Log("An error has occured in the Journal file, please send this to CMDR Hekateh:");
Log(error.ToString());
}
} catch (Exception exception) {
Log("Something went terribly wrong while parsing the E:D player journal.");
Log("Please send this to CMDR Hekateh:");
@ -184,15 +285,27 @@ public partial class MainWindow : Window {
object obj = entries.SelectedItem;
bool removed = false;
if (obj.GetType() == typeof(Objective)) {
removed = report.Objectives.Remove(obj as Objective);
if (obj.GetType() == typeof(SystemObjectives)) {
removed = report.SystemObjectives.Remove(obj as SystemObjectives);
} else if (obj.GetType() == typeof(Objective)) {
report
.SystemObjectives
.ForEach(x => {
if (x.Objectives.Remove(obj as Objective)) {
removed = true;
}
});
} else if (obj.GetType() == typeof(UITransaction) ||
obj.GetType().IsSubclassOf(typeof(UITransaction))) {
foreach (Objective parent in report.Objectives) {
if (parent.UITransactions.Remove(obj as UITransaction)) {
removed = true;
}
}
obj.GetType().IsSubclassOf(typeof(UITransaction))) {
report
.SystemObjectives
.SelectMany(x =>
x.Objectives
.Where(x => x.UITransactions.Contains(obj as UITransaction))
)
.ToList()
.ForEach(x => removed = x.UITransactions.Remove(obj as UITransaction))
;
}
if (removed) {
@ -200,7 +313,7 @@ public partial class MainWindow : Window {
}
}
private void entries_KeyUp(object sender, System.Windows.Input.KeyEventArgs e) {
private void entries_KeyUp(object sender, KeyEventArgs e) {
if (e.Key == Key.Delete) {
RemoveCurrentObjective();
}
@ -219,7 +332,7 @@ public partial class MainWindow : Window {
}
private Objective GetObjectiveFromControl(object sender) {
System.Windows.Controls.Control control = sender as System.Windows.Controls.Control;
Control control = sender as Control;
if (control == null || control.DataContext == null) {
return null;
}
@ -280,13 +393,19 @@ public partial class MainWindow : Window {
private void window_Closing(object sender, System.ComponentModel.CancelEventArgs e) {
loadentries?.Close();
loadentries = null;
try {
Config.SaveGlobal();
} catch (Exception error) {
MessageBox.Show("There was an error saving your settings: " + error.Message);
}
}
private void Transaction_Initialized(object sender, EventArgs e) {
}
private TransactionType GetTransaction<TransactionType>(object sender) where TransactionType : Transaction {
System.Windows.Controls.Control button = sender as System.Windows.Controls.Control;
Control button = sender as Control;
if (button == null || button.DataContext == null) {
return null;
}
@ -359,6 +478,16 @@ public partial class MainWindow : Window {
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) {
CombatZone transaction = GetTransaction<CombatZone>(sender);
if (transaction == null) {
@ -390,14 +519,14 @@ public partial class MainWindow : Window {
}
private void ResetTime_Click(object sender, RoutedEventArgs e) {
DateTime? d = startdate.Value;
DateTime? d = startdate.SelectedDateTime;
if (d != null) {
startdate.Value = ResetTimeToZero(d.Value);
startdate.SelectedDateTime = ResetTimeToZero(d.Value);
}
d = enddate.Value;
d = enddate.SelectedDateTime;
if (d != null) {
enddate.Value = ResetTimeToZero(d.Value);
enddate.SelectedDateTime = ResetTimeToZero(d.Value);
}
}
@ -412,4 +541,192 @@ public partial class MainWindow : Window {
.ForEach(x => x.IsEnabled = (button.IsChecked ?? true))
;
}
private void UpdateTheme() {
ThemeManager.Current.ChangeTheme(this, Config.Global.FullTheme);
}
private void SwitchTheme_Toggled(object sender, RoutedEventArgs e) {
ToggleSwitch toggle = sender as ToggleSwitch;
if (toggle.IsOn) {
Config.Global.Theme = "Dark";
} else {
Config.Global.Theme = "Light";
}
UpdateTheme();
}
private void Colour_SelectionChanged(object sender, SelectionChangedEventArgs e) {
if (Colour.SelectedItem == null) {
return;
}
Config.Global.Colour = Colour.SelectedItem.ToString();
UpdateTheme();
}
private void URL_RequestNavigate(object sender, System.Windows.Navigation.RequestNavigateEventArgs e) {
ProcessStartInfo info = new ProcessStartInfo();
info.UseShellExecute = true;
info.FileName = e.Uri.AbsoluteUri;
Process.Start(info);
e.Handled = true;
}
private void NoInfluenceSupport_Toggled(object sender, RoutedEventArgs e) {
Config.Global.IgnoreInfluenceSupport = this.NoInfluenceSupport.IsOn;
}
private void NoMarketBuy_Toggled(object sender, RoutedEventArgs e) {
Config.Global.IgnoreMarketBuy = this.NoMarketBuy.IsOn;
}
private void NoFleetCarrier_Toggled(object sender, RoutedEventArgs e) {
Config.Global.IgnoreFleetCarrier = this.NoFleetCarrier.IsOn;
}
private void NoPowerplay_Toggled(object sender, RoutedEventArgs e) {
Config.Global.IgnorePowerplay = this.NoPowerplay.IsOn;
}
private void OpenInExplorer_Click(object sender, RoutedEventArgs e) {
try {
Process.Start(new ProcessStartInfo(Config.Global.JournalLocation) {
UseShellExecute = true,
});
} catch (Exception) {
}
}
private void SelectAll_Click(object sender, RoutedEventArgs e) {
if (report == null) {
return;
}
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

@ -8,7 +8,7 @@ public class MinusFortyFiveConverter : IValueConverter {
/// <inheritdoc/>
public object Convert(
object value, Type targetType, object parameter, CultureInfo culture) {
return (double)value - 65;
return (double)value - 110;
}
/// <inheritdoc/>

View File

@ -7,6 +7,12 @@ using System.Linq;
namespace EliteBGS.BGS;
public class NonaDiscordLog : DiscordLogGenerator {
protected override string BotHeader() {
var sb = new StringBuilder();
sb.AppendFormat(":robot: `Bot-Header:` {0}; {1}\n", GetToolVersion(), this.Name);
return sb.ToString();
}
private string FormatDate() {
CultureInfo cultureInfo = CultureInfo.InvariantCulture;
StringBuilder date = new StringBuilder();
@ -51,7 +57,12 @@ public class NonaDiscordLog : DiscordLogGenerator {
.Count()
;
string summary = GenerateSummary(objective);
log.AppendFormat(":globe_with_meridians: `Target:` {0}\n", location);
if (!string.IsNullOrEmpty(summary)) {
log.AppendFormat(":scroll: `Summary:` {0}\n", summary);
}
if (legacycount > 0) {
log.Append(":rotating_light: `Warning`: Some actions were done in E:D Legacy\n");
}
@ -74,6 +85,14 @@ public class NonaDiscordLog : DiscordLogGenerator {
}
public override string ToString() {
return "Nova Navy Log";
return "Nova Navy";
}
public override string Name {
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;
}
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;
}
@ -120,14 +122,14 @@ public class UITransaction : INotifyPropertyChanged {
}
}
public bool HasCaptain {
public bool HasEnemyCaptain {
get {
CombatZone combat = Transaction as CombatZone;
if (combat == null) {
return false;
}
return combat.Captain ?? false;
return combat.EnemyCaptain ?? false;
}
set {
CombatZone combat = Transaction as CombatZone;
@ -135,18 +137,18 @@ public class UITransaction : INotifyPropertyChanged {
return;
}
combat.Captain = value;
combat.EnemyCaptain = value;
}
}
public bool HasCorrespondent {
public bool HasAlliedCaptain {
get {
CombatZone combat = Transaction as CombatZone;
if (combat == null) {
return false;
}
return combat.Correspondent ?? false;
return combat.AlliedCaptain ?? false;
}
set {
CombatZone combat = Transaction as CombatZone;
@ -154,7 +156,45 @@ public class UITransaction : INotifyPropertyChanged {
return;
}
combat.Correspondent = value;
combat.AlliedCaptain = value;
}
}
public bool HasEnemyCorrespondent {
get {
CombatZone combat = Transaction as CombatZone;
if (combat == null) {
return false;
}
return combat.EnemyCorrespondent ?? false;
}
set {
CombatZone combat = Transaction as CombatZone;
if (combat == null) {
return;
}
combat.EnemyCorrespondent = value;
}
}
public bool HasAlliedCorrespondent {
get {
CombatZone combat = Transaction as CombatZone;
if (combat == null) {
return false;
}
return combat.AlliedCorrespondent ?? false;
}
set {
CombatZone combat = Transaction as CombatZone;
if (combat == null) {
return;
}
combat.AlliedCorrespondent = value;
}
}
@ -204,7 +244,7 @@ public class UITransaction : INotifyPropertyChanged {
}
public class Objective : IComparable<Objective> {
public bool IsEnabled { get; set; }
public bool IsEnabled { get; set; } = true;
public List<UITransaction> UITransactions { get; } = new List<UITransaction>();

View File

@ -1,4 +1,11 @@
namespace EliteBGS;
using EliteBGS.LogGenerator;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Documents;
namespace EliteBGS;
public class OneLineDiscordLog : DiscordLogGenerator {
protected override string GenerateObjectiveHeader(Objective objective) {
@ -10,7 +17,7 @@ public class OneLineDiscordLog : DiscordLogGenerator {
}
protected override string GenerateObjectiveFooter(Objective objective) {
return "";
return "\n";
}
protected override string GenerateFooter() {
@ -21,12 +28,71 @@ public class OneLineDiscordLog : DiscordLogGenerator {
return "";
}
protected override string TransformFinalLogForObjective(Objective objective, string log) {
string[] lines = log.Split("\n", System.StringSplitOptions.RemoveEmptyEntries);
return string.Join(", ", lines);
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();
if (report == null) {
return "";
}
var objectives = report.Objectives
.Where(x => x.IsEnabled && x.Transactions.Count() > 0)
;
if (objectives == null || objectives.Count() <= 0) {
return "";
}
log.AppendFormat("{0}", BotHeader());
foreach (Objective objective in objectives) {
log.AppendFormat("{0}", GenerateObjectiveHeader(objective));
foreach (LogFormatter formatter in formatters) {
string text = formatter.GenerateSummary(objective);
text = text.Trim();
if (!string.IsNullOrEmpty(text)) {
log.AppendFormat("{0}; ", text);
}
}
log.AppendFormat("{0}", GenerateObjectiveFooter(objective));
}
log.AppendFormat("{0}", GenerateFooter());
return log.ToString().Trim();
}
public override string ToString() {
return "One Line Report";
return "One Line";
}
public override string Name {
get { return "OneLine"; }
}
}

View File

@ -1,6 +1,4 @@
using System.Reflection;
using System.Resources;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Windows;
@ -51,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.1.1.0")]
[assembly: AssemblyFileVersion("0.1.1.0")]
[assembly: AssemblyVersion("0.4.3.0")]
[assembly: AssemblyFileVersion("0.4.3.0")]

View File

@ -117,8 +117,4 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
<data name="EliteBGS" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\EliteBGS.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
</root>

View File

@ -7,7 +7,7 @@ been parsed, you may then generate a BGS report you can copy/paste into Discord.
Source code is available at [https://codeberg.org/nola/edbgs](https://codeberg.org/nola/edbgs).
Binary downloads can be found here: [https://bgs.n0la.org/](https://bgs.n0la.org/).
Binary downloads can be found here: [https://salusinvicta.org/bgstool/](https://salusinvicta.org/bgstool/).
## How To
@ -22,11 +22,14 @@ transactions. Currently the tool recognises the following transactions:
* Selling cartography data
* Selling of cargo to stations
* Selling of micro resources (Odyssey only)
* Selling of organic data (Odyssey only)
* Vouchers, including bounty vouchers, combat bonds, and settlement vouchers (aka intel packages)
* Thargoid kills
* Contributions to Thargoid war effort
The following transactions are recognised but not listed, because they do not affect BGS:
* Selling of organic data (Odyssey only)
Vouchers help the faction that is listed for them. If said faction is not present in the
current system, then there is no BGS impact. So the tool looks for all system factions, and
makes sure that your vouchers actually have a BGS impact, otherwise it won't list them.
@ -227,7 +230,7 @@ The project also requires:
* `Ookii.Dialogs.WPF`
* `Newtonsoft.Json`
* `Extended.Wpf.Toolkit`
* `MahApps`
## About

View File

@ -1,10 +1,38 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using EDPlayerJournal.BGS;
namespace EliteBGS;
public class SystemObjectives : INotifyPropertyChanged, IComparable<SystemObjectives> {
public event PropertyChangedEventHandler PropertyChanged;
private bool isenabled = true;
public bool IsEnabled {
get { return isenabled; }
set {
isenabled = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("IsEnabled"));
}
}
public bool IsExpanded { get; set; } = false;
public string SystemName { get; set; } = string.Empty;
public List<Objective> Objectives { get; set; } = new();
public int CompareTo(SystemObjectives other) {
if (other == null) return 1;
return string.Compare(SystemName, other.SystemName, StringComparison.OrdinalIgnoreCase);
}
}
public class Report {
public List<Objective> Objectives { get; set; } = new List<Objective>();
public List<SystemObjectives> SystemObjectives { get; set; } = new();
public Report() { }
@ -12,29 +40,35 @@ public class Report {
Populate(transactions);
}
public List<Objective> Objectives {
get {
return SystemObjectives
.Where(t => t.IsEnabled)
.SelectMany(x => x.Objectives)
.ToList()
;
}
}
private void Populate(List<Transaction> transactions) {
if (transactions == null || transactions.Count == 0) {
return;
}
foreach (Transaction t in transactions) {
Objective o;
if (t.SystemContribution) {
o = Objectives.Find(x => x.Matches(t.System));
} else {
o = Objectives.Find(x => x.Matches(t.System, t.Faction));
}
var o = SystemObjectives.Find(x => string.Compare(x.SystemName, t.System) == 0);
if (o == null) {
if (t.SystemContribution) {
o = new Objective() { System = t.System };
} else {
o = new Objective() { Faction = t.Faction, System = t.System };
}
Objectives.Add(o);
o = new SystemObjectives() { SystemName = t.System };
SystemObjectives.Add(o);
}
o.UITransactions.Add(new UITransaction(t));
var objective = o.Objectives.Find(x => x.Matches(t.System, t.Faction));
if (objective == null) {
objective = new Objective() { Faction = t.Faction, System = t.System };
o.Objectives.Add(objective);
}
objective.UITransactions.Add(new UITransaction(t));
}
SystemObjectives.Sort();
}
}

View File

@ -60,16 +60,6 @@ namespace EliteBGS {
}
}
/// <summary>
/// Looks up a localized resource of type System.Drawing.Icon similar to (Icon).
/// </summary>
public static System.Drawing.Icon EliteBGS {
get {
object obj = ResourceManager.GetObject("EliteBGS", resourceCulture);
return ((System.Drawing.Icon)(obj));
}
}
/// <summary>
/// Looks up a localized resource of type System.Drawing.Bitmap.
/// </summary>

View File

@ -118,9 +118,6 @@
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
<data name="EliteBGS" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>EliteBGS.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="logo_v5" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>logo_v5.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

View File

@ -1,38 +1,84 @@
using System.ComponentModel;
using Newtonsoft.Json;
using System.Collections.Generic;
using System.ComponentModel;
namespace EliteBGS.Util {
public class AppConfig : INotifyPropertyChanged {
private static readonly string default_journal_location = "%UserProfile%\\Saved Games\\Frontier Developments\\Elite Dangerous";
private string journal_location = default_journal_location;
private string lastdiscordlog;
namespace EliteBGS.Util;
public string DefaultJournalLocation => default_journal_location;
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 LastUsedDiscordTemplate {
get => lastdiscordlog;
set {
lastdiscordlog = value;
FirePropertyChanged("LastUsedDiscordTemplate");
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;
}
set {
journal_location = value;
}
}
public string Theme {
get {
return theme;
}
set {
if (string.IsNullOrEmpty(value)) {
theme = "Dark";
} else {
theme = value;
}
}
}
public string JournalLocation {
get {
if (journal_location == null) {
return DefaultJournalLocation;
}
return journal_location;
}
set {
journal_location = value;
FirePropertyChanged("JournalLocation");
public string Colour {
get {
return colour;
}
set {
if (string.IsNullOrEmpty(value)) {
colour = "Blue";
} else {
colour = value;
}
}
}
private void FirePropertyChanged(string property) {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
}
/// <summary>
/// Whether we ignore influence support scenarios form parsing.
/// </summary>
public bool IgnoreInfluenceSupport { get; set; } = false;
public event PropertyChangedEventHandler PropertyChanged;
/// <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>
/// Ignore power play?
/// </summary>
public bool IgnorePowerplay { 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 {
@ -12,15 +13,6 @@ namespace EliteBGS.Util {
public Config() {
DetermineConfigFolder();
global_config.PropertyChanged += Global_config_PropertyChanged;
}
private void Global_config_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) {
try {
SaveGlobal();
} catch (Exception) {
/* ignored */
}
}
public string ConfigPath => config_folder;
@ -45,6 +37,7 @@ namespace EliteBGS.Util {
filestream.Flush();
using (StreamWriter file = new StreamWriter(filestream, Encoding.UTF8)) {
var stream = new JsonTextWriter(file);
stream.Formatting = Formatting.Indented;
serializer.Serialize(stream, global_config);
}
}
@ -58,7 +51,6 @@ namespace EliteBGS.Util {
if (app != null) {
this.global_config = app;
global_config.PropertyChanged += Global_config_PropertyChanged;
}
}
}

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;
}

BIN
EliteBGS/combatzone.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

View File

@ -1,21 +0,0 @@
# Combat Zones
Starting with version 0.2.0, the EliteBGS tool will attempt to figure out when you
have participated in a combat zone.
Since there is no official journal entry for combat zones as of yet (Update 13), the
tool has to make an educated guess on what sort of combat zone it is.
For Odyssey on foot combat zones the tool can attempt to determine the difficulty
from the from the highest combat bond you have been awarded. Enforcers in high on foot
combat zones nets you roughly 88k credits, and the payout reduces from there.
Ship combat zones are more difficult. If you ship scan one of the warzone NPCs (either
a captain, spec ops, or a correspondent), the tool can assume you are either in a medium
or a high combat zone. If you get more than 10 kills, you are also either in a medium or
high combat zone (a low combat zone is complete with 8 kills).
None of this perfect however, and the tool *will* get it wrong. For your convenience there
are several small buttons next to the combat zone entry, where you can fix the result:
![combat zone](combatzone.png)

Some files were not shown because too many files have changed in this diff Show More