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; /// /// Returns true if the current session is legacy /// public bool IsLegacy { get; set; } = false; /// /// How many on foot kills were done. /// public ulong OnFootKills { get; set; } = 0; /// /// How many ship kills were done. /// public ulong ShipKills { get; set; } = 0; /// /// Thargoid scouts killed /// public ulong ThargoidScoutKills { get; set; } = 0; /// /// Thargoid interceptor kills /// public ulong ThargoidInterceptorKills { get; set; } = 0; /// /// Whether we have seen an AX warzone NPC talk to us with ReceiveText /// public bool HaveSeenAXWarzoneNPC { get; set; } = false; /// /// A list of accepted missions index by their mission ID /// public Dictionary AcceptedMissions { get; } = new(); public Dictionary AcceptedMissionLocation { get; } = new(); /// /// A way to lookup a system by its system id /// public Dictionary SystemsByID { get; } = new(); /// /// A list of factions present in the given star system /// public Dictionary> SystemFactions { get; } = new(); /// /// To which faction a given named NPC belonged to. /// public Dictionary NPCFaction { get; } = new(); /// /// Buy costs for a given commodity /// public Dictionary 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() { 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? 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 { public void AddIncomplete(Transaction underlying, string reason, Entry entry) { Add(new IncompleteTransaction(underlying, reason, entry)); } } internal interface TransactionParserPart{ /// /// 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 /// /// The entry to parse /// Parsing context that may contain useful information /// List of parsed transactions public void Parse(Entry entry, TransactionParserContext context, TransactionList transactions); } /// /// 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. /// 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; } } } /// /// 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. /// 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; } } } /// /// Commit crime can result in a transaction, especially if the crime committed is /// murder. /// 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? 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 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? Parse(IEnumerable 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(); } }