using EDPlayerJournal.BGS.Parsers; using EDPlayerJournal.Entries; namespace EDPlayerJournal.BGS; public class TransactionParserOptions { /// /// Whether to ignore exo biology. It does not contribute to BGS, so this /// is true per default. /// public bool IgnoreExoBiology { get; set; } = true; /// /// Whether to ignore influence support. Usually one only cares about the /// primary faction for the influence. /// public bool IgnoreInfluenceSupport { get; set; } = false; /// /// Whether to ignore market buy. Buying from a market gives a small amount /// of INF if it is sold to a high demand market, but generally one buys from /// a market to aid the faction the stuff is being sold to. So allow it to be /// disabled. /// public bool IgnoreMarketBuy { get; set; } = false; /// /// Whether we should ignore things done for the fleet carrier faction. /// public bool IgnoreFleetCarrierFaction { get; set; } = true; /// /// 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. /// public bool FilterDoubleRedeemVouchers { get; set; } = true; } public class TransactionList : List { public void AddIncomplete(Transaction underlying, string reason, Entry entry) { Add(new IncompleteTransaction(underlying, reason, entry)); } } /// /// 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 : ITransactionParserPart { public void Parse(Entry e, TransactionParserContext context, TransactionParserOptions options, 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 (!string.IsNullOrEmpty(entry.StationFaction)) { context.StationOwner = entry.StationFaction; } else { context.StationOwner = null; } if (entry.SystemFactions != null && entry.SystemFactions.Count > 0) { context.SystemFactions[entry.StarSystem] = entry.SystemFactions; } } } internal class FSDJumpParser : ITransactionParserPart { public void Parse(Entry e, TransactionParserContext context, TransactionParserOptions options, 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.LeftInstance(); 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 (entry.SystemFactions != null && entry.SystemFactions.Count > 0) { context.SystemFactions[entry.StarSystem] = entry.SystemFactions; } } } internal class DockedParser : ITransactionParserPart { public void Parse(Entry e, TransactionParserContext context, TransactionParserOptions options, TransactionList transactions) { DockedEntry? entry = e as DockedEntry; if (entry == null) { throw new NotImplementedException(); } 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.StationOwner = entry.StationFaction; } if (!string.IsNullOrEmpty(entry.StationName)) { context.CurrentStation = entry.StationName; } } } /// /// Commit crime can result in a transaction, especially if the crime committed is /// murder. /// internal class CommitCrimeParser : ITransactionParserPart { public void Parse(Entry e, TransactionParserContext context, TransactionParserOptions options, TransactionList transactions) { CommitCrimeEntry? entry = e as CommitCrimeEntry; if (entry == null) { throw new NotImplementedException(); } // 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 : ITransactionParserPart { public void Parse(Entry entry, TransactionParserContext context, TransactionParserOptions options, 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 : ITransactionParserPart { public void Parse(Entry e, TransactionParserContext context, TransactionParserOptions options, 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 : ITransactionParserPart { public void Parse(Entry e, TransactionParserContext context, TransactionParserOptions options, TransactionList transactions) { MissionCompletedEntry? entry = e as MissionCompletedEntry; if (entry == null || entry.Mission == null) { throw new NotImplementedException(); } Mission? mission; Location? accepted_location; 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, new MissionInfluence()); // Mission gave no influence to the target faction, so we assume // the target faction was in the same system. } else if (string.Compare(source_faction_name, faction, true) == 0) { // This happens if the source faction is not getting any influence // This could be if the source faction is in a conflict, and thus does // not gain any influence at all. other.Value.Add(accepted_location.SystemAddress, new MissionInfluence()); // Just check if the target/source faction are the same, in which case // we also have to make an additional entry if (string.Compare(source_faction_name, target_faction_name, true) == 0 && context.CurrentSystemAddress != null) { other.Value.Add(context.CurrentSystemAddress.Value, new MissionInfluence()); } } } // 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)) { // Whether we ignore influence support if (options.IgnoreInfluenceSupport) { continue; } // Source or target faction are different, and/or the system // differs. Sometimes missions go to different systems but to // the same faction. transactions.Add(new InfluenceSupport() { Mission = mission, Faction = faction, Influence = influences.Value, System = system, SystemAddress = system_address, RelevantMission = entry, IsLegacy = context.IsLegacy, }); } } } } } internal class MissionFailedParser : ITransactionParserPart { public void Parse(Entry e, TransactionParserContext context, TransactionParserOptions options, TransactionList transactions) { Mission? mission; Location? accepted_location; string? accepted_system; MissionFailedEntry? entry = e as MissionFailedEntry; if (entry == null) { 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 SellMicroResourcesParser : ITransactionParserPart { public void Parse(Entry e, TransactionParserContext context, TransactionParserOptions options, TransactionList transactions) { SellMicroResourcesEntry? entry = e as SellMicroResourcesEntry; if (entry == null) { throw new NotImplementedException(); } if (context.StationOwner == null) { transactions.AddIncomplete( new SellMicroResources(), "Could not discern the station owner for micro resources sell.", e); return; } transactions.Add(new SellMicroResources(entry) { System = context.CurrentSystem, Station = context.CurrentStation, Faction = context.StationOwner, IsLegacy = context.IsLegacy, }); } } internal class SearchAndRescueParser : ITransactionParserPart { public void Parse(Entry e, TransactionParserContext context, TransactionParserOptions options, TransactionList transactions) { SearchAndRescueEntry? entry = e as SearchAndRescueEntry; if (entry == null) { throw new NotImplementedException(); } if (context.StationOwner == null) { transactions.AddIncomplete( new OrganicData(), "Could not discern the station owner for S&R operations.", e); return; } transactions.Add(new SearchAndRescue(entry) { System = context.CurrentSystem, Station = context.CurrentStation, Faction = context.StationOwner, IsLegacy = context.IsLegacy, }); } } internal class FactionKillBondParser : ITransactionParserPart { public void Parse(Entry e, TransactionParserContext context, TransactionParserOptions options, TransactionList transactions) { FactionKillBondEntry? entry = e as FactionKillBondEntry; if (entry == null) { throw new NotImplementedException(); } 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 : ITransactionParserPart { public void Parse(Entry e, TransactionParserContext context, TransactionParserOptions options, TransactionList transactions) { if (e.Is(Events.Embark)) { context.IsOnFoot = false; } else if (e.Is(Events.Disembark)) { context.IsOnFoot = true; } } } internal class SupercruiseEntryParser : ITransactionParserPart { public void Parse(Entry entry, TransactionParserContext context, TransactionParserOptions options, TransactionList transactions) { // After a super cruise entry we are no longer on foot. context.IsOnFoot = false; context.DiscernCombatZone(transactions, entry); context.ResetCombatZone(); // Supercruise entry means you left the current local instance context.LeftInstance(); } } internal class ShutdownParser : ITransactionParserPart { public void Parse(Entry entry, TransactionParserContext context, TransactionParserOptions options, TransactionList transactions) { context.DiscernCombatZone(transactions, entry); context.ResetCombatZone(); // Shutdown (logout) means you left the instance context.LeftInstance(); } } internal class CapShipBondParser : ITransactionParserPart { public void Parse(Entry entry, TransactionParserContext context, TransactionParserOptions options, TransactionList transactions) { if (entry.GetType() != typeof(CapShipBondEntry)) { return; } context.HaveSeenCapShip = true; } } internal class FileHeaderParser : ITransactionParserPart { public void Parse(Entry entry, TransactionParserContext context, TransactionParserOptions options, TransactionList transactions) { FileHeaderEntry? fileheader = entry as FileHeaderEntry; if (fileheader == null) { return; } context.IsLegacy = fileheader.IsLegacy; } } internal class ReceiveTextParser : ITransactionParserPart { public void Parse(Entry entry, TransactionParserContext context, TransactionParserOptions options, 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 : ITransactionParserPart { public void Parse(Entry entry, TransactionParserContext context, TransactionParserOptions options, TransactionList transactions) { // Death only matters in ship. On foot you can just redeploy with the dropship. if (context.IsOnFoot) { return; } // 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(); // Dying also moves you back to another instance context.LeftInstance(); } } internal class DropshipDeployParser : ITransactionParserPart { public void Parse(Entry entry, TransactionParserContext context, TransactionParserOptions options, TransactionList transactions) { // On drop ship deploy we are now on foot context.IsOnFoot = true; } } public class TransactionParser { private static Dictionary ParserParts { get; } = new() { { Events.ApproachSettlement, new ApproachSettlementParser() }, { Events.CapShipBond, new CapShipBondParser() }, { Events.CarrierJump, new CarrierJumpParser() }, { Events.Commander, new CommanderParser() }, { Events.CommitCrime, new CommitCrimeParser() }, { Events.Died, new DiedParser() }, { 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.Music, new MusicParser() }, { 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.SupercruiseDestinationDrop, new SupercruiseDestinationDropParser() }, { 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); } /// /// Parses a list of entries with default options. /// /// /// public List? Parse(IEnumerable entries) { TransactionParserOptions defaultOptions = new(); return Parse(entries, defaultOptions); } /// /// List of commanders seen during parsing. /// public List Commanders { get; set; } = new(); public List? Parse(IEnumerable entries, TransactionParserOptions options) { TransactionList transactions = new(); TransactionParserContext context = new(); foreach (Entry entry in entries) { if (entry.Event == null) { throw new InvalidJournalEntryException(); } if (!ParserParts.ContainsKey(entry.Event)) { continue; } ITransactionParserPart transactionParserPart = ParserParts[entry.Event]; transactionParserPart.Parse(entry, context, options, transactions); } // Copy out list of commanders seen Commanders = context.Commanders; return transactions.ToList(); } }