EDBGS/EDPlayerJournal/BGS/TransactionParser.cs

1023 lines
38 KiB
C#

using EDPlayerJournal;
using EDPlayerJournal.Entries;
namespace EDPlayerJournal.BGS;
internal class TransactionParserContext {
public string? CurrentSystem { get; set; }
public ulong? CurrentSystemAddress { get; set; }
public string? CurrentStation { get; set; }
public string? ControllingFaction { get; set; }
public bool IsOnFoot { get; set; } = false;
public string? LastRecordedAwardingFaction { get; set; }
public ulong? HighestCombatBond { get; set; }
public bool HaveSeenCapShip { get; set; } = false;
public bool HaveSeenCaptain { get; set; } = false;
public bool HaveSeenSpecOps { get; set; } = false;
public bool HaveSeenCorrespondent { get; set; } = false;
/// <summary>
/// Returns true if the current session is legacy
/// </summary>
public bool IsLegacy { get; set; } = false;
/// <summary>
/// How many on foot kills were done.
/// </summary>
public ulong OnFootKills { get; set; } = 0;
/// <summary>
/// How many ship kills were done.
/// </summary>
public ulong ShipKills { get; set; } = 0;
/// <summary>
/// Thargoid scouts killed
/// </summary>
public ulong ThargoidScoutKills { get; set; } = 0;
/// <summary>
/// Thargoid interceptor kills
/// </summary>
public ulong ThargoidInterceptorKills { get; set; } = 0;
/// <summary>
/// Whether we have seen an AX warzone NPC talk to us with ReceiveText
/// </summary>
public bool HaveSeenAXWarzoneNPC { get; set; } = false;
/// <summary>
/// A list of accepted missions index by their mission ID
/// </summary>
public Dictionary<ulong, Mission> AcceptedMissions { get; } = new();
public Dictionary<ulong, Location> AcceptedMissionLocation { get; } = new();
/// <summary>
/// A way to lookup a system by its system id
/// </summary>
public Dictionary<ulong, string> SystemsByID { get; } = new();
/// <summary>
/// A list of factions present in the given star system
/// </summary>
public Dictionary<string, List<Faction>> SystemFactions { get; } = new();
/// <summary>
/// To which faction a given named NPC belonged to.
/// </summary>
public Dictionary<string, string> NPCFaction { get; } = new();
/// <summary>
/// Buy costs for a given commodity
/// </summary>
public Dictionary<string, long> BuyCost = new();
public void DiscernCombatZone(TransactionList transactions, Entry e) {
string? grade = CombatZones.DifficultyLow;
string cztype;
ulong highest = HighestCombatBond ?? 0;
string? faction = LastRecordedAwardingFaction;
if (HighestCombatBond == null &&
LastRecordedAwardingFaction == null &&
HaveSeenAXWarzoneNPC == false) {
return;
}
if (OnFootKills > 0 || IsOnFoot == true) {
cztype = CombatZones.GroundCombatZone;
// High on foot combat zones have enforcers that bring 80k a pop
if (highest >= 60000) {
grade = CombatZones.DifficultyHigh;
} else if (highest >= 30000) {
// In medium conflict zones, the enforcers are worth 30k
grade = CombatZones.DifficultyMedium;
} else {
grade = CombatZones.DifficultyLow;
}
} else if (ShipKills > 0 && !IsOnFoot) {
// Ship combat zones can be identified by the amount of kills
if (ShipKills > 20) {
grade = CombatZones.DifficultyHigh;
} else if (ShipKills > 10) {
grade = CombatZones.DifficultyMedium;
}
// Cap ship, means a high conflict zone
if (HaveSeenCapShip) {
grade = CombatZones.DifficultyHigh;
} else {
int warzoneNpcs = new List<bool>() { HaveSeenCaptain, HaveSeenCorrespondent, HaveSeenSpecOps }
.Where(x => x == true)
.Count()
;
if (warzoneNpcs >= 1 && grade == CombatZones.DifficultyLow) {
grade = CombatZones.DifficultyMedium;
}
}
cztype = CombatZones.ShipCombatZone;
} else if ((ThargoidScoutKills > 0 && ThargoidInterceptorKills > 0) ||
HaveSeenAXWarzoneNPC == true) {
// Could be a thargoid combat zones if interceptors and scouts are there
cztype = CombatZones.AXCombatZone;
// Still unknown
grade = null;
} else {
transactions.AddIncomplete(new CombatZone(), "Failed to discern combat zone type", e);
return;
}
CombatZone zone = new CombatZone() {
System = CurrentSystem,
Faction = faction,
IsLegacy = IsLegacy,
Grade = grade,
Type = cztype,
// Sad truth is, if HaveSeenXXX is false, we just don't know for certain
CapitalShip = HaveSeenCapShip ? true : null,
SpecOps = HaveSeenSpecOps ? true : null,
Correspondent = HaveSeenCorrespondent ? true : null,
Captain = HaveSeenCaptain ? true : null,
};
zone.Entries.Add(e);
transactions.Add(zone);
}
public void RecordCombatBond(FactionKillBondEntry e) {
if (HighestCombatBond == null || e.Reward > HighestCombatBond) {
HighestCombatBond = e.Reward;
}
LastRecordedAwardingFaction = e.AwardingFaction;
if (IsOnFoot) {
++OnFootKills;
} else {
++ShipKills;
}
}
public void ResetCombatZone() {
HighestCombatBond = null;
HaveSeenCapShip = false;
HaveSeenCaptain = false;
HaveSeenCorrespondent = false;
HaveSeenSpecOps = false;
LastRecordedAwardingFaction = null;
OnFootKills = 0;
ShipKills = 0;
ThargoidInterceptorKills = 0;
ThargoidScoutKills = 0;
HaveSeenAXWarzoneNPC = false;
}
public void BoughtCargo(string? cargo, long? cost) {
if (cargo == null || cost == null) {
return;
}
BuyCost[cargo] = cost.Value;
}
public List<Faction>? GetFactions(string? system) {
if (system == null || !SystemFactions.ContainsKey(system)) {
return null;
}
return SystemFactions[system];
}
public void MissionAccepted(MissionAcceptedEntry? entry) {
if (entry == null) {
return;
}
MissionAccepted(entry.Mission);
}
public void MissionAccepted(Mission? mission) {
if (CurrentSystem == null || CurrentSystemAddress == null) {
throw new Exception("Mission accepted without knowing where.");
}
if (mission == null) {
throw new Exception("Mission is null");
}
AcceptedMissions.TryAdd(mission.MissionID, mission);
Location location = new() {
StarSystem = CurrentSystem,
SystemAddress = CurrentSystemAddress.Value,
Station = (CurrentStation ?? ""),
};
AcceptedMissionLocation.TryAdd(mission.MissionID, location);
}
}
public class TransactionList : List<Transaction> {
public void AddIncomplete(Transaction underlying, string reason, Entry entry) {
Add(new IncompleteTransaction(underlying, reason, entry));
}
}
internal interface TransactionParserPart{
/// <summary>
/// Parse a given entry of the entry type specified when declaring to implement this
/// interface. You must add your transaction to the passed list in case you did have
/// enough information to parse one or more. You may update the parser context
/// with new information in case the entry yielded new information.
/// Throw an exception if there was an error or a malformed entry. If you have an
/// incomplete entry, i.e. not enough information to complete one, add an
/// IncompleteTransaction to the list
/// </summary>
/// <param name="entry">The entry to parse</param>
/// <param name="context">Parsing context that may contain useful information</param>
/// <param name="transactions">List of parsed transactions</param>
public void Parse(Entry entry, TransactionParserContext context, TransactionList transactions);
}
/// <summary>
/// The location parser only updates the context with useful information, and does not
/// by itself generate any transactions. Location is the best information gatherer here
/// as we are getting controlling faction, system factions, address and station name.
/// </summary>
internal class LocationParser : TransactionParserPart {
public void Parse(Entry e, TransactionParserContext context, TransactionList transactions) {
LocationEntry? entry = e as LocationEntry;
if (entry == null) {
throw new NotImplementedException();
}
if (entry.StarSystem == null) {
throw new InvalidJournalEntryException();
}
context.CurrentSystem = entry.StarSystem;
context.CurrentSystemAddress = entry.SystemAddress;
context.SystemsByID.TryAdd(entry.SystemAddress, entry.StarSystem);
if (!string.IsNullOrEmpty(entry.SystemFaction)) {
context.ControllingFaction = entry.SystemFaction;
}
if (!string.IsNullOrEmpty(entry.StationName)) {
context.CurrentStation = entry.StationName;
}
if (!context.SystemFactions.ContainsKey(entry.StarSystem) &&
entry.SystemFactions != null && entry.SystemFactions.Count > 0) {
context.SystemFactions[entry.StarSystem] = entry.SystemFactions;
}
}
}
internal class FSDJumpParser : TransactionParserPart {
public void Parse(Entry e, TransactionParserContext context, TransactionList transactions) {
FSDJumpEntry? entry = e as FSDJumpEntry;
if (entry == null) {
throw new NotImplementedException();
}
if (entry.StarSystem == null) {
throw new InvalidJournalEntryException();
}
// If you FSD jump straight out of the combat zone into a different system
// then the combat zone will be placed in the wrong system otherwise.
// This call needs to be *before* changing the current system.
context.DiscernCombatZone(transactions, e);
context.ResetCombatZone();
context.CurrentSystem = entry.StarSystem;
context.CurrentSystemAddress = entry.SystemAddress;
context.SystemsByID.TryAdd(entry.SystemAddress, entry.StarSystem);
if (!string.IsNullOrEmpty(entry.SystemFaction)) {
context.ControllingFaction = entry.SystemFaction;
}
if (!context.SystemFactions.ContainsKey(entry.StarSystem) &&
entry.SystemFactions != null && entry.SystemFactions.Count > 0) {
context.SystemFactions[entry.StarSystem] = entry.SystemFactions;
}
}
}
internal class DockedParser : TransactionParserPart {
public void Parse(Entry e, TransactionParserContext context, TransactionList transactions) {
DockedEntry? entry = e as DockedEntry;
if (entry == null) {
throw new NotImplementedException();
}
if (entry.StarSystem == null || entry.SystemAddress == null) {
throw new InvalidJournalEntryException();
}
context.CurrentSystem = entry.StarSystem;
context.CurrentSystemAddress = entry.SystemAddress;
context.SystemsByID.TryAdd(entry.SystemAddress.Value, entry.StarSystem);
if (!string.IsNullOrEmpty(entry.StationFaction)) {
context.ControllingFaction = entry.StationFaction;
}
if (!string.IsNullOrEmpty(entry.StationName)) {
context.CurrentStation = entry.StationName;
}
}
}
/// <summary>
/// With ship targeted we might find out to which faction a given NPC belonged. This is
/// useful later when said NPC gets killed or murdered.
/// </summary>
internal class ShipTargetedParser : TransactionParserPart {
public void Parse(Entry e, TransactionParserContext context, TransactionList transactions) {
ShipTargetedEntry? entry = e as ShipTargetedEntry;
if (entry == null) {
throw new NotImplementedException();
}
// Scan happens in stages, and sometimes this information is not known
// yet. Do now throw an error, this is expected behaviour.
if (!string.IsNullOrEmpty(entry.PilotNameLocalised) &&
!string.IsNullOrEmpty(entry.Faction)) {
context.NPCFaction.TryAdd(entry.PilotNameLocalised, entry.Faction);
}
// We have seen a captain?
if (NPCs.IsWarzoneCaptain(entry.PilotName)) {
context.HaveSeenCaptain = true;
}
// Spec ops?
if (NPCs.IsSpecOps(entry.PilotName)) {
context.HaveSeenSpecOps = true;
}
// Correspondent?
if (NPCs.IsWarzoneCorrespondent(entry.PilotName)) {
context.HaveSeenCorrespondent = true;
}
}
}
/// <summary>
/// Commit crime can result in a transaction, especially if the crime committed is
/// murder.
/// </summary>
internal class CommitCrimeParser : TransactionParserPart {
public void Parse(Entry e, TransactionParserContext context, TransactionList transactions) {
CommitCrimeEntry? entry = e as CommitCrimeEntry;
if (entry == null) {
throw new NotImplementedException();
}
// Right now we only care for murder
if (!entry.IsMurder) {
return;
}
string? victim = entry.Victim;
if (victim == null) {
victim = entry.VictimLocalised;
}
// If they were not properly scanned prior to the murder we do not have
// This information. But in the end the name of the NPC does not matter.
if (victim == null) {
victim = "Unknown";
}
string faction;
if (entry.IsCrime(CrimeTypes.OnFootMurder)) {
if (entry.Faction == null) {
transactions.AddIncomplete(
new FoulMurder(),
"On foot murder victim did not have a faction", e
);
return;
}
// On foot murders are different, there the faction is
// the faction the NPC belonged too
faction = entry.Faction;
} else {
if (!context.NPCFaction.ContainsKey(victim)) {
transactions.AddIncomplete(
new FoulMurder(),
"Crime victim was not properly scanned.", e
);
return;
}
faction = context.NPCFaction[victim];
}
transactions.Add(new FoulMurder(entry) {
System = context.CurrentSystem,
IsLegacy = context.IsLegacy,
Faction = faction,
});
}
}
internal class MissionsParser : TransactionParserPart {
public void Parse(Entry entry, TransactionParserContext context, TransactionList transactions) {
MissionsEntry? missions = entry as MissionsEntry;
if (missions == null) {
return;
}
if (context.CurrentSystem == null || context.CurrentSystemAddress == null) {
transactions.AddIncomplete(new MissionCompleted(),
"Could not determine current location on Missions event.",
entry
);
return;
}
foreach (Mission mission in missions.Active) {
try {
context.MissionAccepted(mission);
} catch (Exception exception) {
transactions.AddIncomplete(new MissionCompleted(),
exception.Message,
entry
);
}
}
}
}
internal class MissionAcceptedParser : TransactionParserPart {
public void Parse(Entry e, TransactionParserContext context, TransactionList transactions) {
MissionAcceptedEntry? entry = e as MissionAcceptedEntry;
if (entry == null) {
throw new NotImplementedException();
}
if (context.CurrentSystem == null || context.CurrentSystemAddress == null) {
transactions.AddIncomplete(new MissionCompleted(),
"Could not determine current location on mission acceptance.",
e
);
return;
}
try {
context.MissionAccepted(entry);
} catch (Exception exception) {
transactions.AddIncomplete(new MissionCompleted(),
exception.Message,
e
);
}
}
}
internal class MissionCompletedParser : TransactionParserPart {
public void Parse(Entry e, TransactionParserContext context, TransactionList transactions) {
MissionCompletedEntry? entry = e as MissionCompletedEntry;
if (entry == null || entry.Mission == null) {
throw new NotImplementedException();
}
Mission? mission = null;
Location? accepted_location = null;
string? target_faction_name = entry.Mission.TargetFaction;
string? source_faction_name = entry.Mission.Faction;
// We did not find when the mission was accepted.
if (!context.AcceptedMissions.TryGetValue(entry.Mission.MissionID, out mission)) {
transactions.AddIncomplete(new MissionCompleted(),
String.Format("Mission acceptance for mission id {0} was not found",
entry.Mission.MissionID), e);
return;
}
if (!context.AcceptedMissionLocation.TryGetValue(entry.Mission.MissionID, out accepted_location)) {
transactions.AddIncomplete(new MissionCompleted(),
String.Format("Location for acceptance for mission id {0} was not found",
entry.Mission.MissionID), e);
return;
}
// This block does some preliminary "repairs" on the influences block of a completed
// mission. Sometimes these entries are broken, or are missing information for later
// parsing.
foreach (var other in entry.Mission.Influences) {
string faction = other.Key;
if (string.IsNullOrEmpty(faction)) {
// Target faction might be empty string, in special cases. For example if you
// scan a surface installation, and the target faction of the surface installation
// gets negative REP, but the surface installation is not owned by anyone.
continue;
}
// Fun ahead: sometimes the influence list is empty for a faction entry. Here
// we try to repair it.
if (other.Value.Count == 0) {
if (string.Compare(target_faction_name, faction, true) == 0) {
if (context.CurrentSystemAddress == null) {
continue;
}
other.Value.Add(context.CurrentSystemAddress.Value, "");
// Mission gave no influence to the target faction, so we assume
// the target faction was in the same system.
} else if (string.Compare(source_faction_name, faction, true) == 0) {
// This happens if the source faction is not getting any influence
// This could be if the source faction is in a conflict, and thus does
// not gain any influence at all.
other.Value.Add(accepted_location.SystemAddress, "");
// Just check if the target/source faction are the same, in which case
// we also have to make an additional entry
if (string.Compare(source_faction_name, target_faction_name, true) == 0 &&
context.CurrentSystemAddress != null) {
other.Value.Add(context.CurrentSystemAddress.Value, "");
}
}
}
// Now actually parse completed mission
foreach (var influences in other.Value) {
ulong system_address = influences.Key;
string? system;
if (!context.SystemsByID.TryGetValue(system_address, out system)) {
transactions.AddIncomplete(new MissionCompleted(),
string.Format("Unknown system {0}, unable to assign that mission a target", system_address),
e
);
continue;
}
if (string.Compare(faction, source_faction_name, true) == 0 &&
system_address == accepted_location.SystemAddress) {
// Source and target faction are the same, and this is the block
// for the source system. So we make a full mission completed entry.
transactions.Add(new MissionCompleted() {
CompletedEntry = entry,
Mission = mission,
System = accepted_location.StarSystem,
Faction = source_faction_name,
SystemAddress = accepted_location.SystemAddress,
Station = accepted_location.Station,
IsLegacy = context.IsLegacy,
});
} else if (string.Compare(faction, source_faction_name, true) != 0 ||
(string.Compare(faction, source_faction_name) == 0 &&
system_address != accepted_location.SystemAddress)) {
// Source or target faction are different, and/or the system
// differs. Sometimes missions go to different systems but to
// the same faction.
transactions.Add(new InfluenceSupport() {
Mission = mission,
Faction = faction,
Influence = influences.Value,
System = system,
SystemAddress = system_address,
RelevantMission = entry,
IsLegacy = context.IsLegacy,
});
}
}
}
}
}
internal class MissionFailedParser : TransactionParserPart {
public void Parse(Entry e, TransactionParserContext context, TransactionList transactions) {
Mission? mission = null;
Location? accepted_location = null;
string? accepted_system = null;
MissionFailedEntry? entry = e as MissionFailedEntry;
if (entry == null) {
throw new NotImplementedException();
}
if (entry.Mission == null) {
throw new InvalidJournalEntryException("No mission specified in mission failure");
}
if (!context.AcceptedMissions.TryGetValue(entry.Mission.MissionID, out mission)) {
transactions.AddIncomplete(new MissionFailed(),
"Mission acceptance was not found", e
);
return;
}
if (!context.AcceptedMissionLocation.TryGetValue(mission.MissionID, out accepted_location)) {
transactions.AddIncomplete(new MissionFailed(),
"Unable to figure out where failed mission was accepted", e
);
return;
}
if (!context.SystemsByID.TryGetValue(accepted_location.SystemAddress, out accepted_system)) {
transactions.AddIncomplete(new MissionFailed(),
"Unable to figure out in which system failed mission was accepted", e
);
return;
}
if (string.IsNullOrEmpty(mission.Faction)) {
transactions.AddIncomplete(new MissionFailed(),
"Could not determine for what faction you failed a mission. This happens if the " +
"mission acceptance is not within the given time frame.",
entry
);
}
transactions.Add(new MissionFailed(entry) {
Faction = mission?.Faction,
Mission = mission,
Station = accepted_location.Station,
System = accepted_location.StarSystem,
SystemAddress = accepted_location.SystemAddress,
IsLegacy = context.IsLegacy,
});
}
}
internal class SellExplorationDataParser : TransactionParserPart {
public void Parse(Entry e, TransactionParserContext context, TransactionList transactions) {
SellExplorationDataEntry? entry = e as SellExplorationDataEntry;
if (entry == null) {
throw new NotImplementedException();
}
transactions.Add(new Cartographics(entry) {
System = context.CurrentSystem,
Station = context.CurrentStation,
Faction = context.ControllingFaction,
IsLegacy = context.IsLegacy,
});
}
}
internal class SellOrganicDataParser : TransactionParserPart {
public void Parse(Entry e, TransactionParserContext context, TransactionList transactions) {
SellOrganicDataEntry? entry = e as SellOrganicDataEntry;
if (entry == null) {
throw new NotImplementedException();
}
transactions.Add(new OrganicData(entry) {
System = context.CurrentSystem,
Station = context.CurrentStation,
Faction = context.ControllingFaction,
IsLegacy = context.IsLegacy,
});
}
}
internal class MultiSellExplorationDataParser : TransactionParserPart {
public void Parse(Entry e, TransactionParserContext context, TransactionList transactions) {
MultiSellExplorationDataEntry? entry = e as MultiSellExplorationDataEntry;
if (entry == null) {
throw new NotImplementedException();
}
transactions.Add(new Cartographics(entry) {
System = context.CurrentSystem,
Station = context.CurrentStation,
Faction = context.ControllingFaction,
IsLegacy = context.IsLegacy,
});
}
}
internal class RedeemVoucherParser : TransactionParserPart {
public void Parse(Entry e, TransactionParserContext context, TransactionList transactions) {
RedeemVoucherEntry? entry = e as RedeemVoucherEntry;
if (entry == null) {
throw new NotImplementedException();
}
if (context.CurrentSystem == null) {
transactions.AddIncomplete(new Vouchers(),
"Could not find out where the vouchers were redeemed", e
);
return;
}
List<Faction>? current_factions = context.GetFactions(context.CurrentSystem);
if (current_factions == null) {
transactions.AddIncomplete(new Vouchers(),
"Current system factions are unknown, so vouchers were ineffective", e);
}
foreach (string faction in entry.Factions) {
bool relevantBond = false;
string relevantFaction = faction;
if (string.Compare(faction, Factions.PilotsFederationVouchers) == 0) {
// Target faction is pilots' federation, so we assume thargoid bonks
// Also assign this combat bond to the Pilots Federation
relevantFaction = Factions.PilotsFederation;
relevantBond = true;
}
if (current_factions != null && !relevantBond) {
// If we have local factions, and it ain't thargoid bonds see if the bonds were
// useful in the current system
if (current_factions.Find(x => string.Compare(x.Name, faction, true) == 0) != null) {
relevantBond = true;
} else {
transactions.AddIncomplete(new Vouchers(),
string.Format("Vouchers for \"{0}\" had no effect in \"{1}\" since said " +
"faction is not present there", faction, context.CurrentSystem), e
);
}
}
if (relevantBond) {
transactions.Add(new Vouchers(entry) {
System = context.CurrentSystem,
Station = context.CurrentStation,
Faction = relevantFaction,
ControllingFaction = context.ControllingFaction,
IsLegacy = context.IsLegacy,
});
}
}
}
}
internal class SellMicroResourcesParser : TransactionParserPart {
public void Parse(Entry e, TransactionParserContext context, TransactionList transactions) {
SellMicroResourcesEntry? entry = e as SellMicroResourcesEntry;
if (entry == null) {
throw new NotImplementedException();
}
transactions.Add(new SellMicroResources(entry) {
System = context.CurrentSystem,
Station = context.CurrentStation,
Faction = context.ControllingFaction,
IsLegacy = context.IsLegacy,
});
}
}
internal class SearchAndRescueParser : TransactionParserPart {
public void Parse(Entry e, TransactionParserContext context, TransactionList transactions) {
SearchAndRescueEntry? entry = e as SearchAndRescueEntry;
if (entry == null) {
throw new NotImplementedException();
}
transactions.Add(new SearchAndRescue(entry) {
Faction = context.ControllingFaction,
Station = context.CurrentStation,
System = context.CurrentSystem,
IsLegacy = context.IsLegacy,
});
}
}
internal class MarketBuyParser : TransactionParserPart {
public void Parse(Entry e, TransactionParserContext context, TransactionList transactions) {
MarketBuyEntry? entry = e as MarketBuyEntry;
if (entry == null) {
throw new NotImplementedException();
}
context.BoughtCargo(entry.Type, entry.BuyPrice);
transactions.Add(new BuyCargo(entry) {
Faction = context.ControllingFaction,
Station = context.CurrentStation,
System = context.CurrentSystem,
IsLegacy = context.IsLegacy,
});
}
}
internal class MarketSellParser : TransactionParserPart {
public void Parse(Entry e, TransactionParserContext context, TransactionList transactions) {
long profit = 0;
MarketSellEntry? entry = e as MarketSellEntry;
if (entry == null) {
throw new NotImplementedException();
}
if (entry.Type == null) {
throw new InvalidJournalEntryException("market sell contains no cargo type");
}
if (context.BuyCost.ContainsKey(entry.Type)) {
long avg = context.BuyCost[entry.Type];
profit = (long)entry.TotalSale - (avg * entry.Count);
}
transactions.Add(new SellCargo(entry) {
Faction = context.ControllingFaction,
Station = context.CurrentStation,
System = context.CurrentSystem,
Profit = profit,
IsLegacy = context.IsLegacy,
});
}
}
internal class FactionKillBondParser : TransactionParserPart {
public void Parse(Entry e, TransactionParserContext context, TransactionList transactions) {
FactionKillBondEntry? entry = e as FactionKillBondEntry;
if (entry == null) {
throw new NotImplementedException();
}
if (Factions.IsThargoid(entry.VictimFaction)) {
// Thargoid bonk
transactions.Add(new ThargoidKill(entry) {
System = context.CurrentSystem,
Faction = Factions.PilotsFederation,
Station = context.CurrentStation,
IsLegacy = context.IsLegacy,
});
ThargoidVessel vessel = Thargoid.GetVesselByPayout(entry.Reward);
if (vessel != ThargoidVessel.Unknown) {
if (vessel == ThargoidVessel.Scout) {
++context.ThargoidScoutKills;
} else {
++context.ThargoidInterceptorKills;
}
}
// We are done
return;
}
context.RecordCombatBond(entry);
}
}
internal class EmbarkDisembarkParser : TransactionParserPart {
public void Parse(Entry e, TransactionParserContext context, TransactionList transactions) {
if (e.Is(Events.Embark)) {
context.IsOnFoot = false;
} else if (e.Is(Events.Disembark)) {
context.IsOnFoot = true;
}
}
}
internal class SupercruiseEntryParser : TransactionParserPart {
public void Parse(Entry entry, TransactionParserContext context, TransactionList transactions) {
// After a super cruise entry we are no longer on foot.
context.IsOnFoot = false;
context.DiscernCombatZone(transactions, entry);
context.ResetCombatZone();
}
}
internal class ShutdownParser : TransactionParserPart {
public void Parse(Entry entry, TransactionParserContext context, TransactionList transactions) {
context.DiscernCombatZone(transactions, entry);
context.ResetCombatZone();
}
}
internal class CapShipBondParser : TransactionParserPart {
public void Parse(Entry entry, TransactionParserContext context, TransactionList transactions) {
if (entry.GetType() != typeof(CapShipBondEntry)) {
return;
}
context.HaveSeenCapShip = true;
}
}
internal class FileHeaderParser : TransactionParserPart {
public void Parse(Entry entry, TransactionParserContext context, TransactionList transactions) {
FileHeaderEntry? fileheader = entry as FileHeaderEntry;
if (fileheader == null) {
return;
}
context.IsLegacy = fileheader.IsLegacy;
}
}
internal class ReceiveTextParser : TransactionParserPart {
public void Parse(Entry entry, TransactionParserContext context, TransactionList transactions) {
ReceiveTextEntry? receivetext = entry as ReceiveTextEntry;
if (receivetext == null) {
return;
}
if (string.Compare(receivetext.Channel, Channels.NPC) != 0) {
return;
}
if (string.Compare(receivetext.NPCCategory, NPCs.AXMilitary) == 0) {
context.HaveSeenAXWarzoneNPC = true;
}
}
}
internal class DiedParser : TransactionParserPart {
public void Parse(Entry entry, TransactionParserContext context, TransactionList transactions) {
// You can't complete a combat zone if you die in it. Others might keep it open
// for you, but still you will not have completed it unless you jump back in.
context.ResetCombatZone();
}
}
internal class DropshipDeployParser : TransactionParserPart {
public void Parse(Entry entry, TransactionParserContext context, TransactionList transactions) {
// On drop ship deploy we are now on foot
context.IsOnFoot = true;
}
}
internal class CommanderParser : TransactionParserPart {
public void Parse(Entry entry, TransactionParserContext context, TransactionList transactions) {
// A commander entry happens when you log out, and log back in again
// for example when switching from Open, to Solo or PG.
context.DiscernCombatZone(transactions, entry);
context.ResetCombatZone();
}
}
public class TransactionParser {
private static Dictionary<string, TransactionParserPart> ParserParts { get; } = new()
{
{ Events.CapShipBond, new CapShipBondParser() },
{ Events.Commander, new CommanderParser() },
{ Events.CommitCrime, new CommitCrimeParser() },
{ Events.Died, new DiedParser() },
{ Events.Disembark, new EmbarkDisembarkParser() },
{ Events.Docked, new DockedParser() },
{ Events.DropshipDeploy, new DropshipDeployParser() },
{ Events.Embark, new EmbarkDisembarkParser() },
{ Events.FactionKillBond, new FactionKillBondParser() },
{ Events.FileHeader, new FileHeaderParser() },
{ Events.FSDJump, new FSDJumpParser() },
{ Events.Location, new LocationParser() },
{ Events.MarketBuy, new MarketBuyParser() },
{ Events.MarketSell, new MarketSellParser() },
{ Events.MissionAccepted, new MissionAcceptedParser() },
{ Events.MissionCompleted, new MissionCompletedParser() },
{ Events.MissionFailed, new MissionFailedParser() },
{ Events.Missions, new MissionsParser() },
{ Events.MultiSellExplorationData, new MultiSellExplorationDataParser() },
{ Events.ReceiveText, new ReceiveTextParser() },
{ Events.RedeemVoucher, new RedeemVoucherParser() },
{ Events.SearchAndRescue, new SearchAndRescueParser() },
{ Events.SellExplorationData, new SellExplorationDataParser() },
{ Events.SellMicroResources, new SellMicroResourcesParser() },
{ Events.SellOrganicData, new SellOrganicDataParser() },
{ Events.ShipTargeted, new ShipTargetedParser() },
{ Events.Shutdown, new ShutdownParser() },
{ Events.SupercruiseEntry, new SupercruiseEntryParser() },
};
public bool IsRelevant(string entry) {
return ParserParts.ContainsKey(entry);
}
public bool IsRelevant(Entry e) {
if (e.Event == null) {
return false;
}
return IsRelevant(e.Event);
}
public List<Transaction>? Parse(IEnumerable<Entry> entries) {
TransactionList transactions = new();
TransactionParserContext context = new();
foreach (Entry entry in entries) {
if (entry.Event == null) {
throw new InvalidJournalEntryException();
}
if (!ParserParts.ContainsKey(entry.Event)) {
continue;
}
TransactionParserPart transactionParserPart = ParserParts[entry.Event];
transactionParserPart.Parse(entry, context, transactions);
}
return transactions.ToList();
}
}