Compare commits

..

82 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
75 changed files with 1726 additions and 937 deletions

View File

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

View File

@ -43,6 +43,12 @@ public class CombatZone : Transaction {
/// </summary> /// </summary>
public bool? CapitalShip { get; set; } 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> /// <summary>
/// How many optional objectives were completed? /// How many optional objectives were completed?
/// </summary> /// </summary>
@ -86,6 +92,13 @@ public class CombatZone : Transaction {
get { return string.Compare(Type, CombatZones.AXCombatZone) == 0; } get { return string.Compare(Type, CombatZones.AXCombatZone) == 0; }
} }
/// <summary>
/// Returns true if it is a power combat zone
/// </summary>
public bool IsPower {
get { return string.Compare(Type, CombatZones.PowerCombatZone) == 0; }
}
public override int CompareTo(Transaction? obj) { public override int CompareTo(Transaction? obj) {
if (obj == null || obj.GetType() != typeof(CombatZone)) { if (obj == null || obj.GetType() != typeof(CombatZone)) {
return -1; return -1;

View File

@ -9,7 +9,7 @@ namespace EDPlayerJournal.BGS;
/// faction to another. Both sometimes gain influence. /// faction to another. Both sometimes gain influence.
/// </summary> /// </summary>
public class InfluenceSupport : Transaction { public class InfluenceSupport : Transaction {
public string Influence { get; set; } = ""; public MissionInfluence? Influence { get; set; } = null;
/// <summary> /// <summary>
/// Relevant mission completed entry /// Relevant mission completed entry
@ -46,7 +46,7 @@ public class InfluenceSupport : Transaction {
builder.AppendFormat("Influence gained from \"{0}\": \"{1}\"", builder.AppendFormat("Influence gained from \"{0}\": \"{1}\"",
missionname, missionname,
string.IsNullOrEmpty(Influence) ? "NONE" : Influence Influence == null ? "NONE" : Influence.TrendAdjustedInfluence
); );
return builder.ToString(); 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 "";
} }
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); var influence = CompletedEntry.Mission.GetInfluenceForFaction(Faction, SystemAddress);
builder.AppendFormat("{0}", MissionName); builder.AppendFormat("{0}", MissionName);
if (influence != "") { if (influence != null && influence.Length > 0) {
builder.AppendFormat(", Influence: {0}", influence); builder.AppendFormat(", Influence: {0}",
influence.Select(x => x.InfluenceAmount).Sum()
);
} }
return builder.ToString(); return builder.ToString();

View File

@ -13,6 +13,21 @@ public class MissionFailed : Transaction {
Failed = failed; 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) { public override int CompareTo(Transaction? other) {
if (other == null || other.GetType() != typeof(MissionFailed)) { if (other == null || other.GetType() != typeof(MissionFailed)) {
return -1; return -1;

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

@ -49,10 +49,11 @@ public class SellCargo : Transaction {
} }
foreach (MarketSellEntry sell in sold) { 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, sell.Count,
Cargo, Cargo,
Market Market,
Station
); );
if (Profit != 0) { if (Profit != 0) {

View File

@ -16,6 +16,12 @@ public class TransactionParserOptions {
/// </summary> /// </summary>
public bool IgnoreInfluenceSupport { get; set; } = false; public bool IgnoreInfluenceSupport { get; set; } = false;
/// <summary>
/// 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 bool IgnorePowerplay { get; set; } = true;
/// <summary> /// <summary>
/// Whether to ignore market buy. Buying from a market gives a small amount /// 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 /// of INF if it is sold to a high demand market, but generally one buys from
@ -28,6 +34,14 @@ public class TransactionParserOptions {
/// Whether we should ignore things done for the fleet carrier faction. /// Whether we should ignore things done for the fleet carrier faction.
/// </summary> /// </summary>
public bool IgnoreFleetCarrierFaction { get; set; } = true; public bool IgnoreFleetCarrierFaction { get; set; } = true;
/// <summary>
/// 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 bool FilterDoubleRedeemVouchers { get; set; } = true;
} }
public class TransactionList : List<Transaction> { public class TransactionList : List<Transaction> {
@ -298,20 +312,20 @@ internal class MissionCompletedParser : ITransactionParserPart {
if (context.CurrentSystemAddress == null) { if (context.CurrentSystemAddress == null) {
continue; 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 // Mission gave no influence to the target faction, so we assume
// the target faction was in the same system. // the target faction was in the same system.
} else if (string.Compare(source_faction_name, faction, true) == 0) { } else if (string.Compare(source_faction_name, faction, true) == 0) {
// This happens if the source faction is not getting any influence // 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 // This could be if the source faction is in a conflict, and thus does
// not gain any influence at all. // 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 // Just check if the target/source faction are the same, in which case
// we also have to make an additional entry // we also have to make an additional entry
if (string.Compare(source_faction_name, target_faction_name, true) == 0 && if (string.Compare(source_faction_name, target_faction_name, true) == 0 &&
context.CurrentSystemAddress != null) { context.CurrentSystemAddress != null) {
other.Value.Add(context.CurrentSystemAddress.Value, ""); other.Value.Add(context.CurrentSystemAddress.Value, new MissionInfluence());
} }
} }
} }
@ -422,63 +436,6 @@ internal class MissionFailedParser : ITransactionParserPart {
} }
} }
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) {
transactions.Add(new Vouchers(entry) {
System = context.CurrentSystem,
Station = context.CurrentStation,
Faction = relevantFaction,
ControllingFaction = context.ControllingFaction,
IsLegacy = context.IsLegacy,
});
}
}
}
}
internal class SellMicroResourcesParser : ITransactionParserPart { internal class SellMicroResourcesParser : ITransactionParserPart {
public void Parse(Entry e, TransactionParserContext context, TransactionParserOptions options, TransactionList transactions) { public void Parse(Entry e, TransactionParserContext context, TransactionParserOptions options, TransactionList transactions) {
SellMicroResourcesEntry? entry = e as SellMicroResourcesEntry; SellMicroResourcesEntry? entry = e as SellMicroResourcesEntry;
@ -630,14 +587,25 @@ internal class ReceiveTextParser : ITransactionParserPart {
} }
} }
internal class SelfDestructParser : ITransactionParserPart {
public void Parse(Entry entry, TransactionParserContext context, TransactionParserOptions options, TransactionList transactions) {
context.SelfDestruct = true;
}
}
internal class DiedParser : ITransactionParserPart { internal class DiedParser : ITransactionParserPart {
public void Parse(Entry entry, TransactionParserContext context, TransactionParserOptions options, TransactionList transactions) { public void Parse(Entry entry, TransactionParserContext context, TransactionParserOptions options, TransactionList transactions) {
// Death only matters in ship. On foot you can just redeploy with the dropship. // Death only matters in ship. On foot you can just redeploy with the dropship.
if (context.IsOnFoot) { if (context.IsOnFoot) {
return; return;
} }
// You can't complete a combat zone if you die in it. Others might keep it open if (context.SelfDestruct != null && context.SelfDestruct == true) {
// for you, but still you will not have completed it unless you jump back in. // Some people just suicide to fast track back to stations after a CZ,
// especially since combat bonds don't disappear on death. So count the CZ
// on self destruct
context.DiscernCombatZone(transactions, entry);
context.SelfDestruct = null;
}
context.ResetCombatZone(); context.ResetCombatZone();
// Dying also moves you back to another instance // Dying also moves you back to another instance
context.LeftInstance(); context.LeftInstance();
@ -651,19 +619,12 @@ internal class DropshipDeployParser : ITransactionParserPart {
} }
} }
internal class CommanderParser : ITransactionParserPart {
public void Parse(Entry entry, TransactionParserContext context, TransactionParserOptions options, TransactionList transactions) {
// A commander entry happens when you log out, and log back in again
// for example when switching from Open, to Solo or PG.
context.DiscernCombatZone(transactions, entry);
context.ResetCombatZone();
}
}
public class TransactionParser { public class TransactionParser {
private static Dictionary<string, ITransactionParserPart> ParserParts { get; } = new() private static Dictionary<string, ITransactionParserPart> ParserParts { get; } = new()
{ {
{ Events.ApproachSettlement, new ApproachSettlementParser() },
{ Events.CapShipBond, new CapShipBondParser() }, { Events.CapShipBond, new CapShipBondParser() },
{ Events.CarrierJump, new CarrierJumpParser() },
{ Events.Commander, new CommanderParser() }, { Events.Commander, new CommanderParser() },
{ Events.CommitCrime, new CommitCrimeParser() }, { Events.CommitCrime, new CommitCrimeParser() },
{ Events.Died, new DiedParser() }, { Events.Died, new DiedParser() },
@ -682,9 +643,12 @@ public class TransactionParser {
{ Events.MissionFailed, new MissionFailedParser() }, { Events.MissionFailed, new MissionFailedParser() },
{ Events.Missions, new MissionsParser() }, { Events.Missions, new MissionsParser() },
{ Events.MultiSellExplorationData, new MultiSellExplorationDataParser() }, { Events.MultiSellExplorationData, new MultiSellExplorationDataParser() },
{ Events.Music, new MusicParser() },
{ Events.PowerplayMerits, new PowerplayMeritsParser() },
{ Events.ReceiveText, new ReceiveTextParser() }, { Events.ReceiveText, new ReceiveTextParser() },
{ Events.RedeemVoucher, new RedeemVoucherParser() }, { Events.RedeemVoucher, new RedeemVoucherParser() },
{ Events.SearchAndRescue, new SearchAndRescueParser() }, { Events.SearchAndRescue, new SearchAndRescueParser() },
{ Events.SelfDestruct, new SelfDestructParser() },
{ Events.SellExplorationData, new SellExplorationDataParser() }, { Events.SellExplorationData, new SellExplorationDataParser() },
{ Events.SellMicroResources, new SellMicroResourcesParser() }, { Events.SellMicroResources, new SellMicroResourcesParser() },
{ Events.SellOrganicData, new SellOrganicDataParser() }, { Events.SellOrganicData, new SellOrganicDataParser() },
@ -715,6 +679,11 @@ public class TransactionParser {
return Parse(entries, defaultOptions); return Parse(entries, defaultOptions);
} }
/// <summary>
/// List of commanders seen during parsing.
/// </summary>
public List<string> Commanders { get; set; } = new();
public List<Transaction>? Parse(IEnumerable<Entry> entries, TransactionParserOptions options) { public List<Transaction>? Parse(IEnumerable<Entry> entries, TransactionParserOptions options) {
TransactionList transactions = new(); TransactionList transactions = new();
TransactionParserContext context = new(); TransactionParserContext context = new();
@ -732,6 +701,9 @@ public class TransactionParser {
transactionParserPart.Parse(entry, context, options, transactions); transactionParserPart.Parse(entry, context, options, transactions);
} }
// Copy out list of commanders seen
Commanders = context.Commanders;
return transactions.ToList(); return transactions.ToList();
} }
} }

View File

@ -3,6 +3,11 @@
namespace EDPlayerJournal.BGS; namespace EDPlayerJournal.BGS;
internal class TransactionParserContext { internal class TransactionParserContext {
/// <summary>
/// List of commander names seen in the logs. May be empty.
/// </summary>
public List<string> Commanders { get; } = new();
/// <summary> /// <summary>
/// Name of the current system the player is in. /// Name of the current system the player is in.
/// </summary> /// </summary>
@ -57,6 +62,23 @@ internal class TransactionParserContext {
public bool HaveSeenAlliedCorrespondent { get; set; } = false; public bool HaveSeenAlliedCorrespondent { get; set; } = false;
public bool HaveSeenEnemyCorrespondent { get; set; } = false; public bool HaveSeenEnemyCorrespondent { get; set; } = false;
public bool? SelfDestruct { get; set; } = null;
/// <summary>
/// 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> /// <summary>
/// Returns true if the current session is legacy /// Returns true if the current session is legacy
/// </summary> /// </summary>
@ -115,6 +137,23 @@ internal class TransactionParserContext {
/// </summary> /// </summary>
public void LeftInstance() { public void LeftInstance() {
CurrentInstanceType = null; 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) { public void DiscernCombatZone(TransactionList transactions, Entry e) {
@ -123,10 +162,7 @@ internal class TransactionParserContext {
ulong highest = HighestCombatBond ?? 0; ulong highest = HighestCombatBond ?? 0;
string? faction = LastRecordedAwardingFaction; string? faction = LastRecordedAwardingFaction;
if (HighestCombatBond == null && if (!HadCombatZone()) {
LastRecordedAwardingFaction == null &&
HaveSeenAXWarzoneNPC == false &&
CurrentInstanceType == null) {
return; return;
} }
@ -146,7 +182,8 @@ internal class TransactionParserContext {
return; return;
} }
if (LastRecordedAwardingFaction == null && if (LastRecordedAwardingFaction == null &&
Instances.IsHumanWarzone(CurrentInstanceType)) { (Instances.IsHumanWarzone(CurrentInstanceType) ||
Instances.IsPowerWarzone(CurrentInstanceType))) {
transactions.AddIncomplete(new CombatZone(), transactions.AddIncomplete(new CombatZone(),
"Could not discern for whom you fought for, " + "Could not discern for whom you fought for, " +
"as it seems you haven't killed anyone in the ship combat zone.", "as it seems you haven't killed anyone in the ship combat zone.",
@ -176,6 +213,9 @@ internal class TransactionParserContext {
} else if (Instances.IsInstance(CurrentInstanceType, Instances.WarzoneThargoidVeryHigh)) { } else if (Instances.IsInstance(CurrentInstanceType, Instances.WarzoneThargoidVeryHigh)) {
cztype = CombatZones.AXCombatZone; cztype = CombatZones.AXCombatZone;
grade = CombatZones.DifficultyVeryHigh; grade = CombatZones.DifficultyVeryHigh;
} else if (Instances.IsInstance(CurrentInstanceType, Instances.PowerWarzoneMedium)) {
cztype = CombatZones.PowerCombatZone;
grade = CombatZones.DifficultyMedium;
} else { } else {
transactions.AddIncomplete(new CombatZone(), transactions.AddIncomplete(new CombatZone(),
"Unknown conflict zone difficulty", "Unknown conflict zone difficulty",
@ -223,6 +263,7 @@ internal class TransactionParserContext {
System = CurrentSystem, System = CurrentSystem,
Faction = faction, Faction = faction,
IsLegacy = IsLegacy, IsLegacy = IsLegacy,
Settlement = Settlement,
Grade = grade, Grade = grade,
Type = cztype, Type = cztype,
// Sad truth is, if HaveSeenXXX is false, we just don't know for certain // Sad truth is, if HaveSeenXXX is false, we just don't know for certain

View File

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

View File

@ -44,6 +44,6 @@ public class Credits {
return string.Format("{0:0.00}M", millions); return string.Format("{0:0.00}M", millions);
} }
return ""; return string.Format("{0}", amount);
} }
} }

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") ?? ""; Name = JSON.Value<string>("Name") ?? "";
FID = JSON.Value<string>("FID") ?? ""; FID = JSON.Value<string>("FID") ?? "";
} }
public string FullName {
get {
if (string.IsNullOrEmpty(Name)) {
return string.Empty;
}
return "CMDR " + Name;
}
}
} }

View File

@ -12,8 +12,10 @@ namespace EDPlayerJournal.Entries;
/// </summary> /// </summary>
public class Entry { public class Entry {
private static readonly Dictionary<string, Type> classes = new Dictionary<string, Type> { private static readonly Dictionary<string, Type> classes = new Dictionary<string, Type> {
{ Events.ApproachSettlement, typeof(ApproachSettlementEntry) },
{ Events.Bounty, typeof(BountyEntry) }, { Events.Bounty, typeof(BountyEntry) },
{ Events.CapShipBond, typeof(CapShipBondEntry) }, { Events.CapShipBond, typeof(CapShipBondEntry) },
{ Events.CarrierJump, typeof(CarrierJump) },
{ Events.Commander, typeof(CommanderEntry) }, { Events.Commander, typeof(CommanderEntry) },
{ Events.CommitCrime, typeof(CommitCrimeEntry) }, { Events.CommitCrime, typeof(CommitCrimeEntry) },
{ Events.Died, typeof(DiedEntry) }, { Events.Died, typeof(DiedEntry) },
@ -36,9 +38,16 @@ public class Entry {
{ Events.MissionRedirected, typeof(MissionRedirectedEntry) }, { Events.MissionRedirected, typeof(MissionRedirectedEntry) },
{ Events.Missions, typeof(MissionsEntry) }, { Events.Missions, typeof(MissionsEntry) },
{ Events.MultiSellExplorationData, typeof(MultiSellExplorationDataEntry) }, { 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.ReceiveText, typeof(ReceiveTextEntry) },
{ Events.RedeemVoucher, typeof(RedeemVoucherEntry) }, { Events.RedeemVoucher, typeof(RedeemVoucherEntry) },
{ Events.SearchAndRescue, typeof(SearchAndRescueEntry) }, { Events.SearchAndRescue, typeof(SearchAndRescueEntry) },
{ Events.SelfDestruct, typeof(SelfDestructEntry) },
{ Events.SellExplorationData, typeof(SellExplorationDataEntry) }, { Events.SellExplorationData, typeof(SellExplorationDataEntry) },
{ Events.SellMicroResources, typeof(SellMicroResourcesEntry) }, { Events.SellMicroResources, typeof(SellMicroResourcesEntry) },
{ Events.SellOrganicData, typeof(SellOrganicDataEntry) }, { Events.SellOrganicData, typeof(SellOrganicDataEntry) },
@ -63,7 +72,15 @@ public class Entry {
public static Entry? Parse(string journalline) { public static Entry? Parse(string journalline) {
using (JsonReader reader = new JsonTextReader(new StringReader(journalline))) { using (JsonReader reader = new JsonTextReader(new StringReader(journalline))) {
reader.DateParseHandling = DateParseHandling.None; 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) { if (json == null) {
return null; return null;
} }

View File

@ -1,8 +1,10 @@
namespace EDPlayerJournal.Entries; namespace EDPlayerJournal.Entries;
public class Events { public class Events {
public static readonly string ApproachSettlement = "ApproachSettlement";
public static readonly string Bounty = "Bounty"; public static readonly string Bounty = "Bounty";
public static readonly string CapShipBond = "CapShipBond"; public static readonly string CapShipBond = "CapShipBond";
public static readonly string CarrierJump = "CarrierJump";
public static readonly string Commander = "Commander"; public static readonly string Commander = "Commander";
public static readonly string CommitCrime = "CommitCrime"; public static readonly string CommitCrime = "CommitCrime";
public static readonly string Died = "Died"; public static readonly string Died = "Died";
@ -26,9 +28,16 @@ public class Events {
public static readonly string MissionRedirected = "MissionRedirected"; public static readonly string MissionRedirected = "MissionRedirected";
public static readonly string Missions = "Missions"; public static readonly string Missions = "Missions";
public static readonly string MultiSellExplorationData = "MultiSellExplorationData"; 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 ReceiveText = "ReceiveText";
public static readonly string RedeemVoucher = "RedeemVoucher"; public static readonly string RedeemVoucher = "RedeemVoucher";
public static readonly string SearchAndRescue = "SearchAndRescue"; public static readonly string SearchAndRescue = "SearchAndRescue";
public static readonly string SelfDestruct = "SelfDestruct";
public static readonly string SellExplorationData = "SellExplorationData"; public static readonly string SellExplorationData = "SellExplorationData";
public static readonly string SellMicroResources = "SellMicroResources"; public static readonly string SellMicroResources = "SellMicroResources";
public static readonly string SellOrganicData = "SellOrganicData"; public static readonly string SellOrganicData = "SellOrganicData";

View File

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

View File

@ -0,0 +1,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 class SellExplorationDataEntry : Entry {
public long BaseValue { get; set; } public long BaseValue { get; set; }
public long Bonus { get; set; } public long Bonus { get; set; }
/// <summary>
/// Total Earnings are the actual earnings without bonus. So this
/// value is BaseValue minus any percent a crew pilot takes.
/// </summary>
public long TotalEarnings { get; set; } public long TotalEarnings { get; set; }
public long Total {
get { return BaseValue + Bonus; }
}
public List<string> Systems { get; set; } = new List<string>(); public List<string> Systems { get; set; } = new List<string>();
public List<string> Discovered { get; set; } = new List<string>(); public List<string> Discovered { get; set; } = new List<string>();

View File

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

View File

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

View File

@ -17,6 +17,11 @@ public class JournalFile : IComparable<JournalFile>
private static Regex update11regex = new Regex("Journal\\.([^\\.]+)\\.(\\d+).log"); private static Regex update11regex = new Regex("Journal\\.([^\\.]+)\\.(\\d+).log");
private static string iso8601 = "yyyyMMddTHHmmss"; 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) { public static bool VerifyFile(string path) {
string filename = Path.GetFileName(path); string filename = Path.GetFileName(path);
@ -125,15 +130,20 @@ public class JournalFile : IComparable<JournalFile>
} }
entries.Clear(); entries.Clear();
foreach(var line in lines) { Errors.Clear();
foreach (var line in lines) {
// Skip empty lines // Skip empty lines
if (line.Trim().Length == 0) { if (line.Trim().Length == 0) {
continue; continue;
} }
try {
Entry? entry = Entry.Parse(line); Entry? entry = Entry.Parse(line);
if (entry != null) { if (entry != null) {
entries.Add(entry); entries.Add(entry);
} }
} catch (Exception ex) {
Errors.Add(ex);
}
} }
} }
} }

View File

@ -36,6 +36,31 @@ public class MissionInfluence {
/// </summary> /// </summary>
public string Influence { get; set; } = string.Empty; 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) { public static MissionInfluence FromJSON(JObject obj) {
MissionInfluence missionInfluence = new MissionInfluence(); MissionInfluence missionInfluence = new MissionInfluence();
@ -394,27 +419,29 @@ public class Mission : IComparable<Mission> {
/// <param name="faction">Faction name in question.</param> /// <param name="faction">Faction name in question.</param>
/// <param name="systemaddr">Star System address</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> /// <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 var results = FactionEffects
.Where(x => string.Compare(x.Faction, faction) == 0) .Where(x => string.Compare(x.Faction, faction) == 0)
.SelectMany(x => x.Influences) .SelectMany(x => x.Influences)
.Where(x => (x.SystemAddress != null && x.SystemAddress == systemaddr)) .Where(x => (x.SystemAddress != null && x.SystemAddress == systemaddr))
.Select(x => x.Influence) .Select(x => x)
.ToArray() .ToArray()
; ;
if (results == null || results.Length == 0) { if (results == null || results.Length == 0) {
return null; return new MissionInfluence[0];
} }
return string.Join("", results); return results;
} }
/// <summary> /// <summary>
/// A convenient Dictionary containing all influences given out by faction, /// 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> /// </summary>
public Dictionary<string, Dictionary<ulong, string>> Influences { public Dictionary<string, Dictionary<ulong, MissionInfluence>> Influences {
get { get {
return FactionEffects return FactionEffects
.Where(x => x.Faction != null) .Where(x => x.Faction != null)
@ -422,7 +449,10 @@ public class Mission : IComparable<Mission> {
x => (x.Faction ?? string.Empty), x => (x.Faction ?? string.Empty),
x => x.Influences x => x.Influences
.Where(x => x.SystemAddress != null) .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(); ScanFiles();
} }
public List<Exception> AllErrors {
get {
return Files.SelectMany(x => x.Errors).ToList();
}
}
public List<JournalFile> Files { public List<JournalFile> Files {
get { return journalfiles; } 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,11 +8,16 @@ public enum ThargoidVessel {
Basilisk = 4, Basilisk = 4,
Medusa = 5, Medusa = 5,
Hydra = 6, Hydra = 6,
Glaive = 7, // Includes Glaive and Scythe
Hunter = 7,
/// <summary> /// <summary>
/// Thargoid drone /// Thargoid drone
/// </summary> /// </summary>
Revenant = 8, Revenant = 8,
/// <summary>
/// New thargoid drone in U17
/// </summary>
Banshee = 9,
} }
public class Thargoid { public class Thargoid {
@ -23,8 +28,10 @@ public class Thargoid {
{ 25000, ThargoidVessel.Revenant }, { 25000, ThargoidVessel.Revenant },
{ 65000, ThargoidVessel.Scout }, { 65000, ThargoidVessel.Scout },
{ 75000, ThargoidVessel.Scout }, { 75000, ThargoidVessel.Scout },
// New in Update 17
{ 100000, ThargoidVessel.Banshee },
// New in Update 15 // New in Update 15
{ 4500000, ThargoidVessel.Glaive }, { 4500000, ThargoidVessel.Hunter },
{ 6500000, ThargoidVessel.Cyclops }, { 6500000, ThargoidVessel.Cyclops },
{ 20000000, ThargoidVessel.Basilisk }, { 20000000, ThargoidVessel.Basilisk },
//{ 25000000, ThargoidVessel.Orthrus }, //{ 25000000, ThargoidVessel.Orthrus },
@ -46,7 +53,7 @@ public class Thargoid {
}; };
public static Dictionary<ThargoidVessel, string?> VesselNames { get; } = new() { public static Dictionary<ThargoidVessel, string?> VesselNames { get; } = new() {
{ ThargoidVessel.Unknown, null }, { ThargoidVessel.Unknown, "(Unknown)" },
{ ThargoidVessel.Revenant, "Revenant" }, { ThargoidVessel.Revenant, "Revenant" },
{ ThargoidVessel.Scout, "Scout" }, { ThargoidVessel.Scout, "Scout" },
{ ThargoidVessel.Orthrus, "Orthrus" }, { ThargoidVessel.Orthrus, "Orthrus" },
@ -54,7 +61,8 @@ public class Thargoid {
{ ThargoidVessel.Basilisk, "Basilisk" }, { ThargoidVessel.Basilisk, "Basilisk" },
{ ThargoidVessel.Medusa, "Medusa" }, { ThargoidVessel.Medusa, "Medusa" },
{ ThargoidVessel.Hydra, "Hydra" }, { ThargoidVessel.Hydra, "Hydra" },
{ ThargoidVessel.Glaive, "Glaive" }, { ThargoidVessel.Hunter, "Hunter" },
{ ThargoidVessel.Banshee, "Banshee" },
}; };
public static ThargoidVessel GetVesselByPayout(ulong payout) { public static ThargoidVessel GetVesselByPayout(ulong payout) {

View File

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

View File

@ -19,7 +19,13 @@ public class TestTransactionParser {
return; 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.IsNotNull(transactions, "could not parse entries");
Assert.AreEqual(transactions.Count, 3); Assert.AreEqual(transactions.Count, 3);
@ -144,7 +150,14 @@ public class TestTransactionParser {
return; 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.IsNotNull(transactions, "could not parse entries");
Assert.AreEqual(transactions.Count, 1); Assert.AreEqual(transactions.Count, 1);
Assert.IsInstanceOfType(transactions[0], typeof(OrganicData), "result is not of type Organic Data"); 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.IsNotNull(transactions, "could not parse entries");
Assert.AreEqual(transactions.Count, 3); 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.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.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.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] [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

@ -1,5 +1,71 @@
# EliteBGS changelog # EliteBGS changelog
## 0.4.4 on ??.??.202?
* Add support for Power Conflict Zones
* Show no inf as "Zero INF", so the Nova Navy bot can parse it properly
* 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 ## 0.3.4 on 18.06.2023
* Added possibility to specify allied, as well as enemy captain and correspondent. * Added possibility to specify allied, as well as enemy captain and correspondent.

View File

@ -4,14 +4,12 @@ using System.Collections.Generic;
using System.Text; using System.Text;
using EliteBGS.LogGenerator; using EliteBGS.LogGenerator;
using System.Reflection; using System.Reflection;
using System.Runtime.CompilerServices;
namespace EliteBGS; namespace EliteBGS;
public class DiscordLogGenerator { public class DiscordLogGenerator {
protected List<LogFormatter> formatters = new List<LogFormatter>() { protected List<LogFormatter> formatters = new List<LogFormatter>() {
new MissionFormat(), new MissionFormat(),
new FailedMissionFormat(),
new MurderFormat(), new MurderFormat(),
new VoucherFormat(), new VoucherFormat(),
new ThargoidFormatter(), new ThargoidFormatter(),
@ -23,6 +21,7 @@ public class DiscordLogGenerator {
new CargoSoldFormatter(), new CargoSoldFormatter(),
new VistaGenomicsFormat(), new VistaGenomicsFormat(),
new SearchAndRescueFormat(), new SearchAndRescueFormat(),
new MeritsGainedFormat(),
}; };
protected virtual string GetToolVersion() { protected virtual string GetToolVersion() {
@ -113,10 +112,6 @@ public class DiscordLogGenerator {
string summary = GenerateSummary(objective); string summary = GenerateSummary(objective);
log.AppendFormat("**Log Generated:** {0} by {1}\n",
DateTime.Now.ToString("dd/MM/yyyy"),
GetToolVersion()
);
var earliest = GetDateOfEarliestEntry(objective); var earliest = GetDateOfEarliestEntry(objective);
var latest = GetDateOfLatestEntry(objective); var latest = GetDateOfLatestEntry(objective);
if (earliest != null && latest != null) { if (earliest != null && latest != null) {
@ -127,7 +122,7 @@ public class DiscordLogGenerator {
} }
log.AppendFormat("**Target:** {0}\n", location); log.AppendFormat("**Target:** {0}\n", location);
if (!string.IsNullOrEmpty(summary)) { if (!string.IsNullOrEmpty(summary)) {
log.AppendFormat("**Summary**: {0}\n", summary); log.AppendFormat("**Summary:** {0}\n", summary);
} }
if (legacycount > 0) { if (legacycount > 0) {
log.AppendFormat("**Warning:** Some actions were performed on ED Legacy\n"); log.AppendFormat("**Warning:** Some actions were performed on ED Legacy\n");
@ -151,6 +146,16 @@ public class DiscordLogGenerator {
return log; 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) { public virtual string GenerateDiscordLog(Report report) {
StringBuilder log = new StringBuilder(); StringBuilder log = new StringBuilder();
@ -166,6 +171,7 @@ public class DiscordLogGenerator {
return ""; return "";
} }
log.AppendFormat("{0}", BotHeader());
log.AppendFormat("{0}", GenerateHeader()); log.AppendFormat("{0}", GenerateHeader());
foreach (Objective objective in objectives) { foreach (Objective objective in objectives) {
@ -193,4 +199,65 @@ public class DiscordLogGenerator {
return log.ToString().Trim(); return log.ToString().Trim();
} }
public virtual string[] SplitLog(string log, int maxcount = 2000) {
throw new NotImplementedException();
}
protected string[] SplitLogWithHeader(string log, string header, int maxcount = 2000) {
string[] lines = log.Split("\n");
List<string> chunks = new();
string chunk = string.Empty;
// Optimisation
if (log.Length <= maxcount) {
return new string[] { log };
}
// First split the log into its headers
// skip first bot header line
for (int i = 1; i < lines.Length; i++) {
string line = lines[i];
if (line.StartsWith(header) && !string.IsNullOrEmpty(chunk)) {
chunks.Add(chunk.Trim());
chunk = string.Empty;
}
chunk = chunk + "\n" + line;
}
int curchunk = 0;
string botheader = BotHeader().Trim() + "\n";
// Leave room for botheader and some leeway
int maxlength = (2000 - botheader.Length - 10);
// Then try to collate chunks
for (curchunk = 0; curchunk < chunks.Count; ++curchunk) {
int count = chunks[curchunk].Length;
while (count < maxlength && (curchunk+1) < chunks.Count) {
count += chunks[curchunk + 1].Length + 2;
if (count < maxlength) {
chunks[curchunk] += "\n";
chunks[curchunk] += chunks[curchunk + 1];
chunks.RemoveAt(curchunk + 1);
}
}
}
// Readd bott headers
for (int i = 0; i < chunks.Count; i++) {
chunks[i] = chunks[i].Insert(0, botheader);
}
// Now finally check if any one chunk is bigger than max length
for (int i = 0; i < chunks.Count; i++) {
if (chunks[i].Length > maxcount) {
throw new ApplicationException(
string.Format("a chunk turned out bigger than {0}", maxcount));
}
}
return chunks.ToArray();
}
} }

32
EliteBGS/DiscordPoster.cs Normal file
View File

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

View File

@ -1,86 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0-windows</TargetFramework>
<OutputType>WinExe</OutputType>
<Version>0.2.6</Version>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<UseWindowsForms>true</UseWindowsForms>
<UseWPF>true</UseWPF>
<ImportWindowsDesktopTargets>true</ImportWindowsDesktopTargets>
</PropertyGroup>
<PropertyGroup>
<StartupObject>EliteBGSApplication</StartupObject>
</PropertyGroup>
<PropertyGroup>
<ApplicationIcon>Salus.ico</ApplicationIcon>
<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>
<PackageTags>ED;Elite Dangerous;BGS</PackageTags>
<PackageProjectUrl>https://bgs.n0la.org</PackageProjectUrl>
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>
<ItemGroup>
<Resource Include="main-page.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Resource>
<None Update="README.md">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
<None Update="docs\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>
</Content>
</ItemGroup>
<ItemGroup>
<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="Microsoft.CSharp" Version="4.7.0" />
<PackageReference Include="Microsoft.Windows.Compatibility" Version="7.0.0" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
<PackageReference Include="Ookii.Dialogs.Wpf" Version="5.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\EDPlayerJournal\EDPlayerJournal.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Resources.resx">
<Generator>PublicResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
</Project>

View File

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net7.0-windows</TargetFramework> <TargetFramework>net7.0-windows</TargetFramework>
<OutputType>WinExe</OutputType> <OutputType>WinExe</OutputType>
<Version>0.3.4</Version> <Version>0.4.3</Version>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<UseWindowsForms>true</UseWindowsForms> <UseWindowsForms>true</UseWindowsForms>
<UseWPF>true</UseWPF> <UseWPF>true</UseWPF>
@ -18,12 +18,15 @@
<Copyright>Copyright 2019 by Florian Stinglmayr</Copyright> <Copyright>Copyright 2019 by Florian Stinglmayr</Copyright>
<RepositoryUrl>https://codeberg.org/nola/EDBGS</RepositoryUrl> <RepositoryUrl>https://codeberg.org/nola/EDBGS</RepositoryUrl>
<PackageTags>ED;Elite Dangerous;BGS</PackageTags> <PackageTags>ED;Elite Dangerous;BGS</PackageTags>
<PackageProjectUrl>https://bgs.n0la.org</PackageProjectUrl> <PackageProjectUrl>https://salusinvicta.org/bgstool/</PackageProjectUrl>
<PackageReadmeFile>README.md</PackageReadmeFile> <PackageReadmeFile>README.md</PackageReadmeFile>
<Description>Elite: Dangerous BGS logging and reporting tool <Description>Elite: Dangerous BGS logging and reporting tool
</Description> </Description>
<PackageIcon>logo_v5.png</PackageIcon> <PackageIcon>logo_v5.png</PackageIcon>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<None Remove="main-page.png" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<Resource Include="main-page.png"> <Resource Include="main-page.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
@ -37,15 +40,10 @@
<Pack>True</Pack> <Pack>True</Pack>
<PackagePath>\</PackagePath> <PackagePath>\</PackagePath>
</None> </None>
<None Update="docs\CHANGELOG.md"> <None Update="CHANGELOG.md">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
</ItemGroup> </ItemGroup>
<ItemGroup>
<Resource Include="docs\main-page.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Resource>
</ItemGroup>
<ItemGroup> <ItemGroup>
<Content Include="LICENCE.txt"> <Content Include="LICENCE.txt">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
@ -55,12 +53,12 @@
<Resource Include="Salus.ico" /> <Resource Include="Salus.ico" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="MahApps.Metro" Version="2.4.9" /> <PackageReference Include="MahApps.Metro" Version="2.4.10" />
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" /> <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>
<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" /> <PackageReference Include="Ookii.Dialogs.Wpf" Version="5.0.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -1,7 +1,18 @@
namespace EliteBGS; using System.Collections.Generic;
using System.Windows.Documents;
namespace EliteBGS;
public class GenericDiscordLog : DiscordLogGenerator { public class GenericDiscordLog : DiscordLogGenerator {
public override string ToString() { public override string ToString() {
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.DefaultExt = ".log";
dialog.Filter = "Log files (*.log)|*.log|All files (*.*)|*"; dialog.Filter = "Log files (*.log)|*.log|All files (*.*)|*";
var location = config.Global.DefaultJournalLocation; var location = AppConfig.DefaultJournalLocation;
if (Directory.Exists(location)) { if (Directory.Exists(location)) {
dialog.InitialDirectory = location; dialog.InitialDirectory = location;
} }

View File

@ -10,7 +10,7 @@ class CombatZoneFormat : LogFormatter {
var logs = objective var logs = objective
.EnabledOfType<CombatZone>() .EnabledOfType<CombatZone>()
.OrderBy(x => (CombatZones.DifficultyRank(x.Grade) ?? 0)) .OrderBy(x => (CombatZones.DifficultyRank(x.Grade) ?? 0))
.GroupBy(x => new { x.Type, x.Grade }) .GroupBy(x => new { x.Type, x.Grade, x.Settlement })
.ToDictionary(x => x.Key, x => x.ToList()) .ToDictionary(x => x.Key, x => x.ToList())
; ;
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
@ -23,6 +23,11 @@ class CombatZoneFormat : LogFormatter {
int optionals = log.Value int optionals = log.Value
.Sum(x => x.OptionalObjectivesCompleted) .Sum(x => x.OptionalObjectivesCompleted)
; ;
var settlements = log.Value
.Select(x => x.Settlement)
.Distinct()
;
string settl = string.Join(", ", settlements);
if (!string.IsNullOrEmpty(log.Key.Grade)) { if (!string.IsNullOrEmpty(log.Key.Grade)) {
builder.AppendFormat("Won {0}x {1} {2} Combat Zone(s)", builder.AppendFormat("Won {0}x {1} {2} Combat Zone(s)",
log.Value.Count, log.Value.Count,
@ -39,6 +44,9 @@ class CombatZoneFormat : LogFormatter {
if (optionals > 0) { if (optionals > 0) {
builder.AppendFormat(" (with {0} optional objectives)", optionals); builder.AppendFormat(" (with {0} optional objectives)", optionals);
} }
if (!string.IsNullOrEmpty(settl)) {
builder.AppendFormat(" (at {0})", settl);
}
builder.Append("\n"); builder.Append("\n");
} }

View File

@ -1,60 +0,0 @@
using System.Linq;
using System.Text;
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();
}
public string GenerateSummary(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();
}
}

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

@ -6,17 +6,73 @@ using EDPlayerJournal.BGS;
namespace EliteBGS.LogGenerator; namespace EliteBGS.LogGenerator;
public class MissionFormat : LogFormatter { 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) { public string GenerateLog(Objective objective) {
Dictionary<string, Dictionary<string, int>> collated = new(); Dictionary<string, Dictionary<string, int>> collated = new();
Dictionary<string, ulong> passengers = new(); Dictionary<string, ulong> passengers = new();
StringBuilder output = new StringBuilder(); StringBuilder output = new StringBuilder();
int total_influence = 0; long total_influence = 0;
bool hadmissions = false;
var missions = objective.EnabledOfType<MissionCompleted>(); var missions = objective.EnabledOfType<MissionCompleted>();
var support = objective.EnabledOfType<InfluenceSupport>(); var support = objective.EnabledOfType<InfluenceSupport>();
var failed = objective.EnabledOfType<MissionFailed>();
if ((missions == null || missions.Count == 0) && if ((missions == null || missions.Count == 0) &&
(support == null || support.Count == 0)) { (support == null || support.Count == 0) &&
(failed == null || failed.Count == 0)) {
return ""; return "";
} }
@ -30,6 +86,7 @@ public class MissionFormat : LogFormatter {
++collated[m.MissionName][m.Influence]; ++collated[m.MissionName][m.Influence];
hadmissions = true;
total_influence += m.Influence.Length; total_influence += m.Influence.Length;
if (m.AcceptedEntry != null && if (m.AcceptedEntry != null &&
@ -60,17 +117,29 @@ public class MissionFormat : LogFormatter {
output.Append("\n"); 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) { foreach (InfluenceSupport inf in support) {
output.Append(inf.ToString()); output.Append(inf.ToString());
output.Append("\n"); 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"); output.Append("\n");
} }
if (total_influence > 0) { if (hadmissions) {
output.AppendFormat("Total Influence: {0}", total_influence); output.AppendFormat("Total Influence: {0}", total_influence);
} }
@ -78,19 +147,34 @@ public class MissionFormat : LogFormatter {
} }
public string GenerateSummary(Objective objective) { public string GenerateSummary(Objective objective) {
bool hadmissions = objective
.EnabledOfType<MissionCompleted>()
.Count > 0
;
long influence = objective long influence = objective
.EnabledOfType<MissionCompleted>() .EnabledOfType<MissionCompleted>()
.Sum(x => x.Influence.Length) .Sum(x => x.Influence.Length)
; ;
long support = objective long support = objective
.EnabledOfType<InfluenceSupport>() .EnabledOfType<InfluenceSupport>()
.Sum(x => x.Influence.Length) .Sum(x => x.Influence.InfluenceAmount)
;
long failed = objective
.EnabledOfType<MissionFailed>()
.Sum(x => x.InfluenceAmount)
; ;
if (influence + support <= 0) { if (influence == 0 && support == 0 && failed == 0 && !hadmissions) {
return ""; return "";
} }
return string.Format("INF: {0}", influence + support); 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

@ -5,19 +5,138 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:EliteBGS" xmlns:local="clr-namespace:EliteBGS"
xmlns:Util="clr-namespace:EliteBGS.Util" d:DataContext="{d:DesignInstance Type=Util:AppConfig}" x:Name="window" x:Class="EliteBGS.MainWindow" xmlns:Util="clr-namespace:EliteBGS.Util"
d:DataContext="{d:DesignInstance Type=Util:AppConfig}"
x:Name="window"
x:Class="EliteBGS.MainWindow"
mc:Ignorable="d" mc:Ignorable="d"
Title="Elite: Dangerous BGS Helper" Height="520" Width="950" Icon="Salus.ico" Closing="window_Closing"> Title="Elite: Dangerous BGS Helper" Height="620" Width="950" Icon="Salus.ico" Closing="window_Closing"
>
<Window.Resources> <Window.Resources>
<local:MinusFortyFiveConverter x:Key="MinusFortyFiveConverter" />
<Style x:Key="StretchingTreeViewStyle" TargetType="TreeViewItem" BasedOn="{StaticResource {x:Type TreeViewItem}}"> <Style x:Key="StretchingTreeViewStyle" TargetType="TreeViewItem" BasedOn="{StaticResource {x:Type TreeViewItem}}">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/> <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
</Style> </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> </Window.Resources>
<mah:MetroWindow.RightWindowCommands> <mah:MetroWindow.RightWindowCommands>
<mah:WindowCommands ShowSeparators="False"> <mah:WindowCommands ShowSeparators="False">
<TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" Margin="5,0,5,0"> <TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" Margin="5,0,5,0">
<Hyperlink x:Name="URL" NavigateUri="https://bgs.n0la.org/" RequestNavigate="URL_RequestNavigate">Homepage</Hyperlink> <Hyperlink x:Name="URL" NavigateUri="https://salusinvicta.org/bgstool/" RequestNavigate="URL_RequestNavigate">Homepage</Hyperlink>
</TextBlock> </TextBlock>
<TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" Margin="5,0,15,0"> <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> <Hyperlink x:Name="SRC" NavigateUri="https://codeberg.org/nola/EDBGS" RequestNavigate="URL_RequestNavigate">Source</Hyperlink>
@ -63,95 +182,24 @@
<Button x:Name="GenerateDiscord" Content="Generate Discord Report" VerticalAlignment="Stretch" Margin="0,0,0,0" VerticalContentAlignment="Center" Click="GenerateDiscord_Click"/> <Button x:Name="GenerateDiscord" Content="Generate Discord Report" VerticalAlignment="Stretch" Margin="0,0,0,0" VerticalContentAlignment="Center" Click="GenerateDiscord_Click"/>
<Separator /> <Separator />
<ComboBox x:Name="LogType" VerticalAlignment="Stretch" Margin="0,3,0,3" Width="140" SelectionChanged="LogType_SelectionChanged" /> <ComboBox x:Name="LogType" VerticalAlignment="Stretch" Margin="0,3,0,3" Width="140" SelectionChanged="LogType_SelectionChanged" />
<Separator />
<CheckBox x:Name="SelectAll" Content="Select All" IsChecked="True" Click="SelectAll_Click"/>
<Separator />
<Label Content="Post log as" VerticalAlignment="Center" Margin="0,3,0,3" />
<ComboBox x:Name="Commanders" VerticalAlignment="Stretch" Margin="0,3,0,3" Width="180" />
<Label Content="to" VerticalAlignment="Center" Margin="0,3,0,3" />
<mah:DropDownButton x:Name="PostToDiscord" Content="Discord" />
</ToolBar> </ToolBar>
<TreeView CheckBox.Checked="TreeView_CheckBox_Updated" <TreeView CheckBox.Checked="TreeView_CheckBox_Updated"
CheckBox.Unchecked="TreeView_CheckBox_Updated" CheckBox.Unchecked="TreeView_CheckBox_Updated"
x:Name="entries" Margin="0,0,0,0" x:Name="entries" Margin="0,0,0,0"
Grid.ColumnSpan="3" Grid.Row="2" Grid.ColumnSpan="3"
Grid.Row="2"
KeyUp="entries_KeyUp" KeyUp="entries_KeyUp"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
HorizontalContentAlignment="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="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"/>
</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> <TreeView.ItemContainerStyle>
<Style TargetType="TreeViewItem"> <Style TargetType="TreeViewItem">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" /> <Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
@ -168,6 +216,7 @@
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/> <RowDefinition Height="*"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
@ -213,6 +262,7 @@
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/>
@ -221,6 +271,36 @@
<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="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="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="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> </Grid>
</GroupBox> </GroupBox>
</Grid> </Grid>

View File

@ -32,8 +32,8 @@ public partial class MainWindow : MetroWindow {
private LoadEntriesWindow loadentries = null; private LoadEntriesWindow loadentries = null;
private static readonly List<DiscordLogGenerator> logtypes = new List<DiscordLogGenerator>() { private static readonly List<DiscordLogGenerator> logtypes = new List<DiscordLogGenerator>() {
new NonaDiscordLog(),
new GenericDiscordLog(), new GenericDiscordLog(),
new NonaDiscordLog(),
new OneLineDiscordLog(), new OneLineDiscordLog(),
}; };
@ -46,6 +46,9 @@ public partial class MainWindow : MetroWindow {
/* ignored */ /* ignored */
} }
Webhooks.ItemsSource = Config.Global.Webhooks;
RefreshPostMenu();
foreach (DiscordLogGenerator type in logtypes) { foreach (DiscordLogGenerator type in logtypes) {
LogType.Items.Add(type); LogType.Items.Add(type);
} }
@ -61,6 +64,7 @@ public partial class MainWindow : MetroWindow {
this.NoInfluenceSupport.IsOn = Config.Global.IgnoreInfluenceSupport; this.NoInfluenceSupport.IsOn = Config.Global.IgnoreInfluenceSupport;
this.NoMarketBuy.IsOn = Config.Global.IgnoreMarketBuy; this.NoMarketBuy.IsOn = Config.Global.IgnoreMarketBuy;
this.NoFleetCarrier.IsOn = Config.Global.IgnoreFleetCarrier; this.NoFleetCarrier.IsOn = Config.Global.IgnoreFleetCarrier;
this.NoPowerplay.IsOn = Config.Global.IgnorePowerplay;
// Apply theme // Apply theme
try { try {
@ -107,6 +111,10 @@ public partial class MainWindow : MetroWindow {
//{ "HouseSalus", Color.FromRgb(0xBC, 0x94, 0x39) }, //{ "HouseSalus", Color.FromRgb(0xBC, 0x94, 0x39) },
{ "HouseSalus", Color.FromRgb(0xED, 0xDA, 0x70) }, { "HouseSalus", Color.FromRgb(0xED, 0xDA, 0x70) },
{ "NovaNavy", Color.FromRgb(0xA1, 0xA4, 0xDB) }, { "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) { foreach (var colourtheme in colorThemes) {
@ -175,9 +183,15 @@ public partial class MainWindow : MetroWindow {
options.IgnoreInfluenceSupport = Config.Global.IgnoreInfluenceSupport; options.IgnoreInfluenceSupport = Config.Global.IgnoreInfluenceSupport;
options.IgnoreMarketBuy = Config.Global.IgnoreMarketBuy; options.IgnoreMarketBuy = Config.Global.IgnoreMarketBuy;
options.IgnoreFleetCarrierFaction = Config.Global.IgnoreFleetCarrier; options.IgnoreFleetCarrierFaction = Config.Global.IgnoreFleetCarrier;
options.IgnorePowerplay = Config.Global.IgnorePowerplay;
List<Transaction> transactions = parser.Parse(entries, options); List<Transaction> transactions = parser.Parse(entries, options);
Commanders.ItemsSource = parser.Commanders;
if (Commanders.Items.Count > 0) {
Commanders.SelectedIndex = 0;
}
// Filter the transactions down to the given time frame // Filter the transactions down to the given time frame
transactions = transactions transactions = transactions
.Where(t => t.CompletedAtDateTime >= start && t.CompletedAtDateTime <= end) .Where(t => t.CompletedAtDateTime >= start && t.CompletedAtDateTime <= end)
@ -192,7 +206,7 @@ public partial class MainWindow : MetroWindow {
transactions.RemoveAll(x => incompletes.Contains(x)); transactions.RemoveAll(x => incompletes.Contains(x));
report = new Report(transactions); report = new Report(transactions);
this.entries.ItemsSource = report.Objectives; this.entries.ItemsSource = report.SystemObjectives;
} catch (Exception exception) { } catch (Exception exception) {
Log("Something went terribly wrong while parsing the E:D player journal."); Log("Something went terribly wrong while parsing the E:D player journal.");
Log("Please send this to CMDR Hekateh:"); Log("Please send this to CMDR Hekateh:");
@ -233,6 +247,12 @@ public partial class MainWindow : MetroWindow {
HandleEntries(entries, start, end); HandleEntries(entries, start, end);
GenerateLog(); 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) { } catch (Exception exception) {
Log("Something went terribly wrong while parsing the E:D player journal."); Log("Something went terribly wrong while parsing the E:D player journal.");
Log("Please send this to CMDR Hekateh:"); Log("Please send this to CMDR Hekateh:");
@ -265,15 +285,27 @@ public partial class MainWindow : MetroWindow {
object obj = entries.SelectedItem; object obj = entries.SelectedItem;
bool removed = false; bool removed = false;
if (obj.GetType() == typeof(Objective)) { if (obj.GetType() == typeof(SystemObjectives)) {
removed = report.Objectives.Remove(obj as Objective); removed = report.SystemObjectives.Remove(obj as SystemObjectives);
} else if (obj.GetType() == typeof(UITransaction) || } else if (obj.GetType() == typeof(Objective)) {
obj.GetType().IsSubclassOf(typeof(UITransaction))) { report
foreach (Objective parent in report.Objectives) { .SystemObjectives
if (parent.UITransactions.Remove(obj as UITransaction)) { .ForEach(x => {
if (x.Objectives.Remove(obj as Objective)) {
removed = true; removed = true;
} }
} });
} else if (obj.GetType() == typeof(UITransaction) ||
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) { if (removed) {
@ -446,6 +478,16 @@ public partial class MainWindow : MetroWindow {
RefreshView(); RefreshView();
} }
private void Power_Click(object sender, RoutedEventArgs e) {
CombatZone transaction = GetTransaction<CombatZone>(sender);
if (transaction == null) {
return;
}
transaction.Type = CombatZones.PowerCombatZone;
RefreshView();
}
private void Thargoid_Click(object sender, RoutedEventArgs e) { private void Thargoid_Click(object sender, RoutedEventArgs e) {
CombatZone transaction = GetTransaction<CombatZone>(sender); CombatZone transaction = GetTransaction<CombatZone>(sender);
if (transaction == null) { if (transaction == null) {
@ -543,6 +585,10 @@ public partial class MainWindow : MetroWindow {
Config.Global.IgnoreFleetCarrier = this.NoFleetCarrier.IsOn; 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) { private void OpenInExplorer_Click(object sender, RoutedEventArgs e) {
try { try {
Process.Start(new ProcessStartInfo(Config.Global.JournalLocation) { Process.Start(new ProcessStartInfo(Config.Global.JournalLocation) {
@ -551,4 +597,136 @@ public partial class MainWindow : MetroWindow {
} catch (Exception) { } 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/> /// <inheritdoc/>
public object Convert( public object Convert(
object value, Type targetType, object parameter, CultureInfo culture) { object value, Type targetType, object parameter, CultureInfo culture) {
return (double)value - 80; return (double)value - 110;
} }
/// <inheritdoc/> /// <inheritdoc/>

View File

@ -7,6 +7,12 @@ using System.Linq;
namespace EliteBGS.BGS; namespace EliteBGS.BGS;
public class NonaDiscordLog : DiscordLogGenerator { 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() { private string FormatDate() {
CultureInfo cultureInfo = CultureInfo.InvariantCulture; CultureInfo cultureInfo = CultureInfo.InvariantCulture;
StringBuilder date = new StringBuilder(); StringBuilder date = new StringBuilder();
@ -79,6 +85,14 @@ public class NonaDiscordLog : DiscordLogGenerator {
} }
public override string ToString() { 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; return Visibility.Hidden;
} }
if (string.Compare(combat.Type, CombatZones.ShipCombatZone) == 0) { // Both normal and power combat zones have special objectives
if (string.Compare(combat.Type, CombatZones.ShipCombatZone) == 0 ||
string.Compare(combat.Type, CombatZones.PowerCombatZone) == 0) {
return Visibility.Visible; return Visibility.Visible;
} }
@ -242,7 +244,7 @@ public class UITransaction : INotifyPropertyChanged {
} }
public class Objective : IComparable<Objective> { public class Objective : IComparable<Objective> {
public bool IsEnabled { get; set; } public bool IsEnabled { get; set; } = true;
public List<UITransaction> UITransactions { get; } = new List<UITransaction>(); public List<UITransaction> UITransactions { get; } = new List<UITransaction>();

View File

@ -1,6 +1,9 @@
using EliteBGS.LogGenerator; using EliteBGS.LogGenerator;
using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Windows.Documents;
namespace EliteBGS; namespace EliteBGS;
@ -25,6 +28,30 @@ public class OneLineDiscordLog : DiscordLogGenerator {
return ""; return "";
} }
public override string[] SplitLog(string log, int maxcount = 2000) {
string[] lines = log.Split('\n');
List<string> chunks = new();
string chunk = string.Empty;
// Optimisation
if (log.Length <= maxcount) {
return new string[] { log };
}
for (int i = 0; i < lines.Length; i++) {
string line = lines[i];
if ((chunk.Length + line.Length) > maxcount || i == lines.Length - 1) {
chunks.Add(chunk.Trim());
chunk = string.Empty;
chunk = chunk.Insert(0, BotHeader()).Trim();
} else {
chunk = chunk + "\n" + line;
}
}
return chunks.ToArray();
}
public override string GenerateDiscordLog(Report report) { public override string GenerateDiscordLog(Report report) {
StringBuilder log = new StringBuilder(); StringBuilder log = new StringBuilder();
@ -40,6 +67,8 @@ public class OneLineDiscordLog : DiscordLogGenerator {
return ""; return "";
} }
log.AppendFormat("{0}", BotHeader());
foreach (Objective objective in objectives) { foreach (Objective objective in objectives) {
log.AppendFormat("{0}", GenerateObjectiveHeader(objective)); log.AppendFormat("{0}", GenerateObjectiveHeader(objective));
@ -60,6 +89,10 @@ public class OneLineDiscordLog : DiscordLogGenerator {
} }
public override string ToString() { 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.Reflection;
using System.Resources;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Windows; 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 // You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below: // by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")] // [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("0.3.4.0")] [assembly: AssemblyVersion("0.4.3.0")]
[assembly: AssemblyFileVersion("0.3.4.0")] [assembly: AssemblyFileVersion("0.4.3.0")]

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). 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 ## How To

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; using EDPlayerJournal.BGS;
namespace EliteBGS; 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 class Report {
public List<Objective> Objectives { get; set; } = new List<Objective>(); public List<SystemObjectives> SystemObjectives { get; set; } = new();
public Report() { } public Report() { }
@ -12,29 +40,35 @@ public class Report {
Populate(transactions); Populate(transactions);
} }
public List<Objective> Objectives {
get {
return SystemObjectives
.Where(t => t.IsEnabled)
.SelectMany(x => x.Objectives)
.ToList()
;
}
}
private void Populate(List<Transaction> transactions) { private void Populate(List<Transaction> transactions) {
if (transactions == null || transactions.Count == 0) { if (transactions == null || transactions.Count == 0) {
return; return;
} }
foreach (Transaction t in transactions) { foreach (Transaction t in transactions) {
Objective o; var o = SystemObjectives.Find(x => string.Compare(x.SystemName, t.System) == 0);
if (t.SystemContribution) {
o = Objectives.Find(x => x.Matches(t.System));
} else {
o = Objectives.Find(x => x.Matches(t.System, t.Faction));
}
if (o == null) { if (o == null) {
if (t.SystemContribution) { o = new SystemObjectives() { SystemName = t.System };
o = new Objective() { System = t.System }; SystemObjectives.Add(o);
} else {
o = new Objective() { Faction = t.Faction, System = t.System };
} }
Objectives.Add(o); 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));
} }
o.UITransactions.Add(new UITransaction(t)); SystemObjectives.Sort();
}
} }
} }

View File

@ -1,13 +1,17 @@
using Newtonsoft.Json; using Newtonsoft.Json;
using System.Collections.Generic;
using System.ComponentModel;
namespace EliteBGS.Util { namespace EliteBGS.Util;
public class AppConfig {
public class AppConfig {
private static readonly string default_journal_location = "%UserProfile%\\Saved Games\\Frontier Developments\\Elite Dangerous"; private static readonly string default_journal_location = "%UserProfile%\\Saved Games\\Frontier Developments\\Elite Dangerous";
private string journal_location = default_journal_location; private string journal_location = default_journal_location;
public string DefaultJournalLocation => default_journal_location;
private string colour = "Amber"; private string colour = "Amber";
private string theme = "Dark"; private string theme = "Dark";
public static string DefaultJournalLocation => default_journal_location;
public string LastUsedDiscordTemplate { get; set; } public string LastUsedDiscordTemplate { get; set; }
public string JournalLocation { public string JournalLocation {
@ -63,9 +67,18 @@ namespace EliteBGS.Util {
/// </summary> /// </summary>
public bool IgnoreFleetCarrier { get; set; } = true; 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] [JsonIgnore]
public string FullTheme { public string FullTheme {
get { return Theme + "." + Colour; } get { return Theme + "." + Colour; }
} }
}
} }

View File

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

View File

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

BIN
EliteBGS/combatzone.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 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)

View File

@ -1,240 +0,0 @@
# EliteBGS
This tool is meant to help people contributing to the BGS effort to create BGS reports.
The tool allows you to configure BGS objectives, and will then parse your player journal
for tasks you completed relating to that BGS objective. Once the JSON player journal has
been parsed, you may then generate a BGS report you can copy/paste into Discord.
Source code is available [here](https://codeberg.org/nola/EDBGS).
Binary downloads can be found here: [https://bgs.n0la.org/](https://bgs.n0la.org/).
## How To
Press "Parse Journal", which will check your Elite Dangerous player journal for completed
missions. Currently the tool recognises the following completed tasks:
* Buying of cargo from stations (new in Update 10)
* Completed missions
* Failed missions
* Murders
* Search and Rescue contributions
* Selling cartography data
* Selling of cargo to stations
* Selling of micro resources (Odyssey only)
* Vouchers, including bounty vouchers, combat bonds, and settlement vouchers (aka intel packages)
* Thargoid kills
* Combat zones (experimental)
The following transactions are recognised but not listed, because they do not affect BGS:
* Selling of organic data (Odyssey only)
Combat zone detection is highly wonky at this time. There is no direct event for detecting
combat zones, and so the tool makes a few assumptions and goes from there. If you disembark
it will assume on foot combat zones, and if you don't, it will assume a ship CZ. This
detection can, and will be wrong, so caution is advised.
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.
Selling cargo attempts to discern the profit and/or loss, which is helpful to gauge BGS
impact. But the player journal does not tell the amount of profit in the sell message.
So the tool looks for a buy a message related to the same commodity, and calculates loss
and/or profit from that. If the buy of the commodity is not within the time and date range,
or some other shenanigans happen that the tool does not yet support, the profit/loss could
be wrong. You can use the "Adjust Trade Profit" button to manually adjust the trade profit,
or you could simply edit the discord log manually.
Please note that cartography data, and micro resources only help the controlling faction
of a station. The tool is clever enough to exclude these if the station you turn them in at, is not
controlled by the faction you specified in the objective.
Some missions may show up having zero influence for the given faction. This happens if you do
missions for a faction which is currently in an election state. You do not gain influence for
the faction so the influence reads as zero. But you contribute towards the election, so the
missions are selected anyway.
There is no entry in the journal if you win a combat zone. So you have to add those manually. Select
an objective for which you wish to log a combat zone. The faction in the objective, must be the
faction you fought for in the combat zone. Then click "Add Combat Zone Win". Select type,
either "On Foot" for Odyssey, or "Ship" for regular ones. Then select the grade (low, medium or
high), and how many you won. Then press "Accept". Select "Cancel" to abort. You can of course remove
the combat zone entries by selecting them, and pressing "DEL".
If you deliberately fail a mission (to log negative INF towards a faction), the tool cannot detect
it, if the day you accepted the mission is outside of the given date range. It needs the journal
entry where you accept the mission to connect the mission to a faction, system and station. The tool
will warn you if this happens, with a message in the error log in the fourth tab.
When committing murder, the journal entry contains the faction information of the faction that gave
you the bounty. And not the faction of the victim. The tool will look for an event in which you
scanned your victim, and gleem the victim's faction from that. If you did not scan your victim, then
sadly the tool cannot connect the victim's faction to the victim.
![Main Window with entries](main-page.png)
The window will then list all the journal entries it has found, and group them by objectives. You
can select which objectives you wish to report, by using the checkmarks.
You can exclude a specific entry within an objective by deselecting the checkbox next to them.
This way said entry will not appear in the final log. You can also remove individual entries
(if you think the tool detected something you thought was wrong), by selecting the entry,
and pressing the "DEL" key.
Once you are satisfied with the result, you can copy and paste the final report to the discord
server of your choice. Before you copy/paste it into the discord of your squadron, you should
check the log. You can of course also edit it, either if something is wrong because the tool
missed something, or you just wish to add a note the report itself.
If you wish to regenerate the discord log, simply click "Generate Log".
## Known Issues and Bugs
### Settlement Vouchers
Settlement vouchers (aka Intel Packages) help every faction aligned with the given superpower.
So if you turn in an Imperial intel package on an imperial station, all factions aligned with
the Empire will gain a bit of INF boost. The tool currently cannot handle that. All intel packages
are displayed instead.
### Bugged bounty vouchers
Sometimes bounty vouchers are not properly recognised. This is a bug in the player journal, where
the faction information is not properly written out in the journal:
```
{
"timestamp":"2021-10-07T14:57:50Z", "event":"RedeemVoucher",
"Type":"bounty", "Amount":20750,
"Factions":[ { "Faction":"", "Amount":500 }, { "Faction":"", "Amount":20250 }]
}
```
Since the tool does not know for which faction these bounties were redeemed for, it cannot assign
it to an objective.
### Combat Zones
The player journal currently does not make an entry when you win or lose a combat zone. This is a
an ommission from FDev:
* [https://issues.frontierstore.net/issue-detail/43509](https://issues.frontierstore.net/issue-detail/43509)
Please upvote the issue to get it fixed. Until then, you have to add combat zone wins manually.
### On-Foot NPC givers
Up until update 13 missions accepted from NPCs in Odyssey concourses do not get a player journal entry.
This has been fixed in update 13. Any on foot missions from NPCs accepted before update 13, do not have
an entry in the player journal.
### Failed vs. Abandoned Missions
The tool also currently cannot differentiate between missions you have abandoned in the transaction
tab before it was completed, and those that you have failed - either delibaretly or by time-out. So
it will find and add them all, and you simply can remove those that you have abandoned manually.
### Influence given to empty/non-existent faction
Sometimes the log will state that it gave positive or negative influence to a faction, but the
faction name is empty:
```
"FactionEffects": [
{
"Faction": "",
"Effects": [
{
"Effect": "$MISSIONUTIL_Interaction_Summary_EP_down;",
"Effect_Localised": "The economic status of $#MinorFaction; has declined in the $#System; system.",
"Trend": "DownBad"
}
],
"Influence": [
{
"SystemAddress": 251012319587,
"Trend": "DownBad",
"Influence": "+"
}
],
"ReputationTrend": "DownBad",
"Reputation": "+"
}
]
```
This happens for example if you do a scan/heist mission from a surface POI, but no one owns said
surface POI. Randomly generated surface POIs sometimes have no owner, and said non-existant owner
then gets the negative influence.
### Mission Completed but no one gains influence
Sometimes missions are completed but no one gains any influence:
```
{
"timestamp": "2022-02-25T21:30:45Z",
"event": "MissionCompleted",
"Faction": "Social LHS 6103 Confederation",
"Name": "Mission_Courier_Elections_name",
"MissionID": 850025233,
"TargetFaction": "Delphin Blue Federal PLC",
"DestinationSystem": "Delphin",
"DestinationStation": "Aristotle Orbital",
"Reward": 122300,
"FactionEffects": [
{
"Faction": "Social LHS 6103 Confederation",
"Effects": [
{
"Effect": "$MISSIONUTIL_Interaction_Summary_EP_up;",
"Effect_Localised": "The economic status of $#MinorFaction; has improved in the $#System; system.",
"Trend": "UpGood"
}
],
"Influence": [],
"ReputationTrend": "UpGood",
"Reputation": "+"
},
{
"Faction": "Delphin Blue Federal PLC",
"Effects": [],
"Influence": [],
"ReputationTrend": "UpGood",
"Reputation": "+"
}
]
}
```
Here the is known that at the time of completion the Confederation was in an Election and could not
have gained any influence regardless. It is unclear whether this also holds true for Delphin Blue
Federal PLC. So to be save, the tool assumes that if no influence was gained for the source faction,
it still has to make an entry for the source system. The same applies for the target faction: if no
influence is gained for the target faction, still add an entry for the target faction in the missions
target system.
Since it is not possible to differentiate between missions that give no influence no matter what, and
no influence gained because of an election, we have to assume it *gave* influence and let the user
decide whether it was because of an election, or not.
Future tool versions should probably take faction states into account in such matters.
## Nothing's Perfect
The tool itself is still a work in progress, and it might miss something. If you think the tool
missed a task you have done, please contact `Hekateh` on the Elite Dangerous community discord.
It would be helpful if you included the JSON player journal. This player journal can be found here:
```
%userprofile%\saved Games\Frontier Developments\Elite Dangerous\
```
## Build Dependencies
The project also requires `Ookii.Dialogs.WPF` controls, which contains the auto complete text box.
And of course, `Newtonsoft.Json` as the JSON parser.
`MahApps` is used to generate a prettier look, and for its dark themes.

View File

@ -1,90 +0,0 @@
## FAQ
Most frequently asked questions:
### Windows complains that it does not wish to run the application, what gives?
The tool contains no viruses, but it is not seen as "trustworthy". You can however
right-click EliteBGS.exe and "Unblock" the application.
### Does this work for console players?
Sorry, no. Console players don't have a player journal per se, and the tool does
not support Frontier Commander API.
### Why won't the tool start anymore?
Open the file explorer, and go to the path `%AppData%`. Once there, delete the
folder called `EliteBGS` to delete the tool's configuration and cache. If it
still doesn't work, contact me directly.
### Why is it unable to find my player journal?
Usually your player journal lives in the Saved Games folder in your home
directory. If, for some reason, this doesn't match up, you can point the
tool towards your player journal in the third tab.
### Why do some of the objective not show up in the final discord log?
Only objectives with the little checkbox enabled show up there. Those
that the tool generates by itself are not enabled per default.
### Can I delete an objective or an entry?
Click on an objective or entry and press the Delete key.
### I deleted something I didn't want to. What now?
Just press "Parse Journal" again, and the tool will generate all
the entries again.
### What are micro resources?
Odyssey cargo that you sell at the bartender. Just like normal cargo,
they aid the controlling faction of the station where you sold them.
### Why are missions accepted in a concourse or in a settlement from an NPC missing?
Because up until Update 13, they did not show up in player journal. This should
now be fixed.
### Some mission names are weird. What gives?
That's because the tool uses the game generated mission name, if it doesn't
have a clean and nice mission name on file for the certain mission type. The
fourth tab "Event Log" should have an entry about it, so please post those
names into this channel.
### Some missions say they have 0 influence?
That happens for missions that aid an Election. The faction in question does
not gain influence during an election, as influence is locked during conflicts.
But since you are contributing towards the election win of that faction,
the tool picks them anyway.
### Why are some failed missions not showing up?
The time span you specify must include the day where you accepted the mission,
as well as the day where you failed the mission. Otherwise the tool cannot handle
that failed mission.
Prior to update 15 missions only failed once you dismissed them from your transaction
tab. With update 15, this behaviour should be fixed.
### The tool complains about missing factions for an NPC I murdered.
The player journal only tells the faction that issued the bounty upon murder, and
not the faction of the NPC killed. The tool has to fetch that from you scanning the
hip. If you didn't fully scan the ship before murdering it, the tool won't know
the faction of the NPC.
### Why does cartography data, and sold cargo show up for the wrong faction, but for the right station/system?
Because they only aid the controlling faction of the station.
### Why are some of my bounty vouchers missing?
Sometimes, due to a bug, the bounty vouchers in the journal have no faction information
associated with them. Here the tool simply cannot associate the vouchers to a faction
or station. If you are sure they aided in BGS, simply add them by editing the Discord
report.

View File

@ -1,89 +0,0 @@
# EliteBGS
EliteBGS is a Windows desktop application, that helps you sum up your BGS related actions.
It then creates a report from your actions, so you can post it your Squadron's discord.
The tool originated from the [Nova Navy](https://inara.cz/elite/squadron/5058/), which required
BGS contributions to be posted to the Navy's discord, in a very specific format. Writing those
logs manually was a lot of work, so CMDR Hekateh created a tool to automate this process.
## Downloads
The tool requires .NET 7.0, you can download it from Microsoft here:
* [https://dotnet.microsoft.com/en-us/download/dotnet/7.0](https://dotnet.microsoft.com/en-us/download/dotnet/7.0)
If you have problems with the installer, you might try running the following in the Windows
command line:
```
winget install Microsoft.DotNet.DesktopRuntime.7
```
You can download the **latest** version **0.3.4** at CodeBerg:
* [https://codeberg.org/nola/EDBGS/releases](https://codeberg.org/nola/EDBGS/releases)
Or alternatively from my server:
* [https://bgs.n0la.org/elitebgs-0.3.4.zip](https://bgs.n0la.org/elitebgs-0.3.4.zip)
## Old Versions
The latest version of the **old** EliteBGS **0.1.7** is available for download here:
* [https://bgs.n0la.org/archive/elitebgs-0.1.7.zip](https://bgs.n0la.org/archive/elitebgs-0.1.7.zip)
Older versions are available in the archive:
* [https://bgs.n0la.org/archive/](https://bgs.n0la.org/archive/)
## Overview
EliteBGS reads through your player journal for BGS relevant activity, and sorts them into
"categories". These are based upon the star system, station and the faction for which the
action was taken. So for example if you contributed bounty vouchers for Nova Paresa in
Paresa, but also did some missions for Nova Paresa in Adachit, those actions will be
split into two categories.
You can then select which of the two actions goes into the final log.
### What it detects:
* Buying of cargo from stations (BGS relevant since Update 10)
* Completed missions
* Failed missions
* Murders
* Search and Rescue contributions
* Selling cartography data
* Selling of cargo to stations
* Selling of micro resources (Odyssey only)
* Vouchers, including bounty vouchers, combat bonds, and settlement vouchers (aka intel packages)
* Thargoid kills
* Combat zones (experimental feature)
The following transactions are recognised but not listed, because they do not affect BGS:
* Selling of organic data (Odyssey only)
### What it does not detect:
* Combat zone objectives
* Megaship scenarios
* On foot missions accepted by NPCs in stations (pre Update 13)
* Murders of NPCs you haven't fully scanned
## Open Source
The tool itself is Open Source, licenced unter the GPLv3.
The source code can be found here:
* [https://codeberg.org/nola/edbgs](https://codeberg.org/nola/edbgs)
## Contact
I can be reached over discord: `nola#2457`
Or by joining either the [Salus Invicta](https://discord.com/invite/FeEtjqBRkg) or the
[Nova Navy](https://discord.gg/WEJeFQw) discord.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 74 KiB

View File

@ -1,15 +0,0 @@
site_name: EliteBGS
markdown_extensions:
- pymdownx.snippets:
check_paths: true
theme:
name: lumen
nav:
- Overview: 'index.md'
- "Detailed Description": 'description.md'
- "Combat Zones": 'combatzones.md'
- FAQ: 'faq.md'
- Changelog: 'CHANGELOG.md'

View File

@ -3,7 +3,7 @@
EDBGS is a project containing the EliteBGS BGS application. It also contains the dotnet EDBGS is a project containing the EliteBGS BGS application. It also contains the dotnet
class library EDPlayerJournal, which reads and parses Elite Dangerous player journals. class library EDPlayerJournal, which reads and parses Elite Dangerous player journals.
See [https://bgs.n0la.org/](https://bgs.n0la.org) for further details. See [https://salusinvicta.org/bgstool/](https://salusinvicta.org/bgstool/) for further details.
The tool helps with creating BGS reports for squadrons that support factions in The tool helps with creating BGS reports for squadrons that support factions in
Elite: Dangerous that can be copy and pasted into a Discord server. It finds all BGS Elite: Dangerous that can be copy and pasted into a Discord server. It finds all BGS