628 lines
		
	
	
		
			24 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			628 lines
		
	
	
		
			24 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
using EDPlayerJournal.Entries;
 | 
						|
using System.Collections.Generic;
 | 
						|
using System.Reflection.Metadata.Ecma335;
 | 
						|
 | 
						|
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; }
 | 
						|
 | 
						|
    /// <summary>
 | 
						|
    /// A list of accepted missions index by their mission ID
 | 
						|
    /// </summary>
 | 
						|
    public Dictionary<ulong, MissionAcceptedEntry> 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 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 accepted) {
 | 
						|
        if (CurrentSystem == null || CurrentSystemAddress == null) {
 | 
						|
            throw new Exception("Mission accepted without knowing where.");
 | 
						|
        }
 | 
						|
 | 
						|
        if (accepted.Mission == null) {
 | 
						|
            throw new Exception("Mission is null");
 | 
						|
        }
 | 
						|
 | 
						|
        AcceptedMissions.TryAdd(accepted.Mission.MissionID, accepted);
 | 
						|
 | 
						|
        Location location = new() {
 | 
						|
            StarSystem = CurrentSystem,
 | 
						|
            SystemAddress = CurrentSystemAddress.Value,
 | 
						|
            Station = (CurrentStation ?? ""),
 | 
						|
        };
 | 
						|
 | 
						|
        AcceptedMissionLocation.TryAdd(accepted.Mission.MissionID, location);
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
public class TransactionList : List<Transaction> {
 | 
						|
    public void AddIncomplete(Transaction underlying, string reason) {
 | 
						|
        Add(new IncompleteTransaction(underlying, reason));
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
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();
 | 
						|
        }
 | 
						|
 | 
						|
        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);
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
/// <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";
 | 
						|
        }
 | 
						|
 | 
						|
        if (!context.NPCFaction.ContainsKey(victim)) {
 | 
						|
            transactions.AddIncomplete(
 | 
						|
                new FoulMurder(),
 | 
						|
                "Crime victim was not properly scanned."
 | 
						|
                );
 | 
						|
            return;
 | 
						|
        }
 | 
						|
 | 
						|
        string faction = context.NPCFaction[victim];
 | 
						|
 | 
						|
        transactions.Add(new FoulMurder(entry) {
 | 
						|
            System = context.CurrentSystem,
 | 
						|
            Faction = faction,
 | 
						|
        });
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
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."
 | 
						|
                );
 | 
						|
            return;
 | 
						|
        }
 | 
						|
 | 
						|
        try {
 | 
						|
            context.MissionAccepted(entry);
 | 
						|
        } catch (Exception exception) {
 | 
						|
            transactions.AddIncomplete(new MissionCompleted(),
 | 
						|
                exception.Message
 | 
						|
                );
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
internal class MissionCompletedParser : TransactionParserPart {
 | 
						|
    public void Parse(Entry e, TransactionParserContext context, TransactionList transactions) {
 | 
						|
        MissionCompletedEntry? entry = e as MissionCompletedEntry;
 | 
						|
        if (entry == null) {
 | 
						|
            throw new NotImplementedException();
 | 
						|
        }
 | 
						|
 | 
						|
        if (entry.MissionID == null) {
 | 
						|
            throw new InvalidJournalEntryException("mission completed has no mission ID");
 | 
						|
        }
 | 
						|
 | 
						|
        MissionAcceptedEntry? accepted = null;
 | 
						|
        Location? accepted_location = null;
 | 
						|
        string? target_faction_name = entry.TargetFaction;
 | 
						|
        string? source_faction_name = entry.Faction;
 | 
						|
 | 
						|
        // We did not find when the mission was accepted.
 | 
						|
        if (!context.AcceptedMissions.TryGetValue(entry.MissionID.Value, out accepted)) {
 | 
						|
            transactions.AddIncomplete(new MissionCompleted(),
 | 
						|
                String.Format("Mission acceptance for mission id {0} was not found",
 | 
						|
                entry.MissionID.Value));
 | 
						|
            return;
 | 
						|
        }
 | 
						|
 | 
						|
        if (!context.AcceptedMissionLocation.TryGetValue(entry.MissionID.Value, out accepted_location)) {
 | 
						|
            transactions.AddIncomplete(new MissionCompleted(),
 | 
						|
                String.Format("Location for acceptance for mission id {0} was not found",
 | 
						|
                entry.MissionID.Value));
 | 
						|
            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.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)
 | 
						|
                        );
 | 
						|
                    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(entry) {
 | 
						|
                        System = accepted_location.StarSystem,
 | 
						|
                        Faction = source_faction_name,
 | 
						|
                        SystemAddress = accepted_location.SystemAddress,
 | 
						|
                        Station = accepted_location.Station,
 | 
						|
                    });
 | 
						|
                } 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() {
 | 
						|
                        Faction = faction,
 | 
						|
                        Influence = influences.Value,
 | 
						|
                        System = system,
 | 
						|
                        SystemAddress = system_address,
 | 
						|
                        RelevantMission = entry,
 | 
						|
                    });
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
internal class MissionFailedParser : TransactionParserPart {
 | 
						|
    public void Parse(Entry e, TransactionParserContext context, TransactionList transactions) {
 | 
						|
        MissionAcceptedEntry? accepted = 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 accepted)) {
 | 
						|
            transactions.AddIncomplete(new MissionFailed(),
 | 
						|
                "Mission acceptance was not found"
 | 
						|
                );
 | 
						|
            return;
 | 
						|
        }
 | 
						|
 | 
						|
        if (!context.AcceptedMissionLocation.TryGetValue(entry.Mission.MissionID, out accepted_location)) {
 | 
						|
            transactions.AddIncomplete(new MissionFailed(),
 | 
						|
                "Unable to figure out where failed mission was accepted"
 | 
						|
                );
 | 
						|
            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"
 | 
						|
                );
 | 
						|
            return;
 | 
						|
        }
 | 
						|
 | 
						|
        transactions.Add(new MissionFailed() {
 | 
						|
            Accepted = accepted,
 | 
						|
            Faction = accepted.Mission?.Faction,
 | 
						|
            Failed = entry,
 | 
						|
            Station = accepted_location.Station,
 | 
						|
            System = accepted_location.StarSystem,
 | 
						|
            SystemAddress = accepted_location.SystemAddress,
 | 
						|
        });
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
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,
 | 
						|
        });
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
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,
 | 
						|
        });
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
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,
 | 
						|
        });
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
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"
 | 
						|
                );
 | 
						|
            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");
 | 
						|
            return;
 | 
						|
        }
 | 
						|
 | 
						|
        foreach (string faction in entry.Factions) {
 | 
						|
            if (current_factions.Find(x => string.Compare(x.Name, faction, true) == 0) == null) {
 | 
						|
                transactions.AddIncomplete(new Vouchers(),
 | 
						|
                    string.Format("Vouchers for {0} were ignored in {1} since said " +
 | 
						|
                    "faction is not present here", faction, context.CurrentSystem)
 | 
						|
                    );
 | 
						|
                continue;
 | 
						|
            }
 | 
						|
 | 
						|
            transactions.Add(new Vouchers(entry) {
 | 
						|
                System = context.CurrentSystem,
 | 
						|
                Station = context.CurrentStation,
 | 
						|
                Faction = faction,
 | 
						|
                ControllingFaction = context.ControllingFaction,
 | 
						|
            });
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
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,
 | 
						|
        });
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
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,
 | 
						|
        });
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
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,
 | 
						|
        });
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
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,
 | 
						|
        });
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
public class TransactionParser {
 | 
						|
    private static Dictionary<string, TransactionParserPart> ParserParts { get; } = new()
 | 
						|
    {
 | 
						|
        { Events.CommitCrime, new CommitCrimeParser() },
 | 
						|
        { Events.Docked, new DockedParser() },
 | 
						|
        { 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.MultiSellExplorationData, new MultiSellExplorationDataParser() },
 | 
						|
        { Events.RedeemVoucher, new RedeemVoucherParser() },
 | 
						|
        { Events.SearchAndRescue, new SearchAndRescueParser() },
 | 
						|
        { Events.SellExplorationData, new SellExplorationDataParser() },
 | 
						|
        { Events.SellMicroResources, new SellMicroResourcesParser() },
 | 
						|
        { Events.SellOrganicData, new SellOrganicDataParser() },
 | 
						|
        { Events.ShipTargeted, new ShipTargetedParser() },
 | 
						|
    };
 | 
						|
 | 
						|
    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();
 | 
						|
    }
 | 
						|
}
 |