using System; using System.Collections.Generic; using System.Linq; using EDJournal; namespace EliteBGS.BGS { public class Report { private List objectives = new List(); public delegate void OnLogDelegate(string log); public event OnLogDelegate OnLog; public List Objectives { get { return objectives; } set { objectives = value; } } public bool AddObjective(Objective objective) { var found = objectives.Find(x => x.CompareTo(objective) == 0); bool added = false; if (found == null) { objectives.Add(objective); added = true; } return added; } public static bool IsRelevant(Entry e) { return e.Is(Events.CommitCrime) || e.Is(Events.Docked) || e.Is(Events.FactionKillBond) || e.Is(Events.FSDJump) || e.Is(Events.Location) || e.Is(Events.MarketBuy) || e.Is(Events.MarketSell) || e.Is(Events.MissionAccepted) || e.Is(Events.MissionFailed) || e.Is(Events.MultiSellExplorationData) || e.Is(Events.RedeemVoucher) || e.Is(Events.SearchAndRescue) || e.Is(Events.SellExplorationData) || e.Is(Events.SellMicroResources) || e.Is(Events.SellOrganicData) || e.Is(Events.ShipTargeted) || e.Is(Events.MissionCompleted) ; } public void Scan(PlayerJournal journal, DateTime start, DateTime end, bool CollateEntries = true) { /* Log files only get rotated if you restart the game client. This means that there might * be - say - entries from the 4th of May in the file with a timestamp of 3rd of May. This * happens if you happen to play a session late into the night. * At first I tried extracting the first and last line of a file to see the date range, but * if you have a lot of files this becomes quite slow, and quite the memory hog (as journal * files have to be read in their entirety to check this). So we assume that you can't play * three days straight, and keep the code fast. */ DateTime actualstart = start.AddDays(-3); List entries = journal.Files .Where(f => f.NormalisedDateTime >= actualstart && f.NormalisedDateTime <= end) .SelectMany(e => e.Entries) .ToList() ; // Now further sort the list down to entries that are actually within the given datetime // Note that entry datetimes are not normalised, so we have to sort until end + 1 day DateTime actualend = end.AddDays(1); entries = entries .Where(e => e.Timestamp >= start && e.Timestamp < actualend) .ToList() ; Scan(entries, CollateEntries); } public void Scan(List entries, bool CollateEntries = true) { if (entries.Count <= 0) { return; } List relevant = entries .Where(x => IsRelevant(x)) .ToList() ; Dictionary acceptedMissions = new Dictionary(); Dictionary buyCost = new Dictionary(); Dictionary systems = new Dictionary(); Dictionary npcfactions = new Dictionary(); Dictionary> system_factions = new Dictionary>(); // A dictionary resolving to a station at which each mission was accepted Dictionary acceptedStations = new Dictionary(); // A dictionary resolving to a system at which each mission was accepted Dictionary acceptedSystems = new Dictionary(); string current_system = null; ulong current_system_address = 0; string current_station = null; string controlling_faction = null; objectives.ForEach(x => x.Clear()); foreach (Entry e in relevant) { List results = new List(); bool collate = false; if (e.Is(Events.Docked)) { DockedEntry docked = e as DockedEntry; /* gleem the current station from this message */ current_station = docked.StationName; current_system = docked.StarSystem; controlling_faction = docked.StationFaction; current_system_address = docked.SystemAddress; if (!systems.ContainsKey(docked.SystemAddress)) { systems.Add(docked.SystemAddress, docked.StarSystem); } } else if (e.Is(Events.FSDJump)) { /* Gleem current system and controlling faction from this message. */ FSDJumpEntry fsd = e as FSDJumpEntry; current_system_address = fsd.SystemAddress; current_system = fsd.StarSystem; controlling_faction = fsd.SystemFaction; if (!systems.ContainsKey(fsd.SystemAddress)) { systems.Add(fsd.SystemAddress, fsd.StarSystem); } if (!system_factions.ContainsKey(fsd.StarSystem) && fsd.SystemFactions.Count > 0) { system_factions[fsd.StarSystem] = fsd.SystemFactions; } } else if (e.Is(Events.Location)) { /* Get current system, faction name and station from Location message */ LocationEntry location = e as LocationEntry; current_system = location.StarSystem; current_system_address = location.SystemAddress; if (!systems.ContainsKey(location.SystemAddress)) { systems.Add(location.SystemAddress, location.StarSystem); } if (!string.IsNullOrEmpty(location.SystemFaction)) { controlling_faction = location.SystemFaction; } if (!string.IsNullOrEmpty(location.StationName)) { current_station = location.StationName; } if (!system_factions.ContainsKey(location.StarSystem) && location.SystemFactions.Count > 0) { system_factions[location.StarSystem] = location.SystemFactions; } } else if (e.Is(Events.ShipTargeted)) { ShipTargetedEntry targeted = e as ShipTargetedEntry; if (string.IsNullOrEmpty(targeted.PilotNameLocalised) || string.IsNullOrEmpty(targeted.Faction)) { continue; } npcfactions[targeted.PilotNameLocalised] = targeted.Faction; } else if (e.Is(Events.CommitCrime)) { CommitCrimeEntry crime = e as CommitCrimeEntry; string faction = crime.Faction; if (!crime.IsMurder) { /* we don't care about anything but murder for now */ continue; } /* use localised victim name if we have it, otherwise use normal name */ string victim = crime.VictimLocalised; if (string.IsNullOrEmpty(victim)) { victim = crime.Victim; } if (!npcfactions.ContainsKey(victim)) { /* The faction in the crime report is the faction that issues the bounty, * and not the faction of the victim. */ OnLog?.Invoke(string.Format( "No faction found for victim \"{0}\", using faction that issued the bounty instead.", victim, crime.Faction )); } else { faction = npcfactions[victim]; } results.Add(new FoulMurder(crime) { System = current_system, Faction = faction, }); collate = CollateEntries; } else if (e.Is(Events.MissionCompleted)) { MissionCompletedEntry completed = e as MissionCompletedEntry; MissionAcceptedEntry accepted = null; MissionCompleted main_mission = null; ulong accepted_address; string accepted_system; string target_faction_name = completed.TargetFaction; string source_faction_name = completed.Faction; if (!acceptedMissions.TryGetValue(completed.MissionID, out accepted)) { OnLog?.Invoke(string.Format( "Unable to find mission acceptance for mission \"{0}\". " + "Please extend range to include the mission acceptance.", completed.HumanReadableName )); continue; } if (!acceptedSystems.TryGetValue(completed.MissionID, out accepted_address)) { OnLog?.Invoke(string.Format( "Unable to figure out in which system mission \"{0}\" was accepted.", completed.HumanReadableName )); continue; } if (!systems.TryGetValue(accepted_address, out accepted_system)) { OnLog?.Invoke(string.Format( "Unable to figure out in which system mission \"{0}\" was accepted.", completed.HumanReadableName )); continue; } if (completed.HumanReadableNameWasGenerated) { /* If the human readable name was generated, we send a log message. Because the * generated names all sort of suck, we should have more human readable names in * in the lookup dictionary. */ OnLog?.Invoke("Human readable name for mission \"" + completed.Name + "\" was generated, please report this."); } foreach (var other in completed.Influences) { string faction = other.Key; if (string.IsNullOrEmpty(faction)) { OnLog?.Invoke(string.Format( "Mission \"{0}\" has empty faction name in influence block, "+ "so this influence support was ignored. " + "Please check the README on why this happens.", completed.HumanReadableName) ); continue; } /* Now comes the fun part. Sometimes the influence list is empty for a faction. * This happens if the faction in question */ if (other.Value.Count() == 0) { OnLog?.Invoke(string.Format( "Mission \"{0}\" gave no influence to \"{1}\", so we assume this is because the " + "faction is in a conflict and cannot gain influence right now. " + "If this assessment is wrong, just remove the entry from the objective list.", completed.HumanReadableName, faction )); if (string.Compare(target_faction_name, faction, true) == 0) { /* here we assume that if the faction in question is the target faction, * that we gave said target faction no influence in the target system, aka * current system */ other.Value.Add(current_system_address, ""); OnLog?.Invoke(string.Format( "Mission \"{0}\" gave no influence to \"{1}\". Since \"{1}\" is the target faction " + "of the mission, we assume the influence was gained in \"{2}\". " + "Please remove the entry if this assumption is wrong.", completed.HumanReadableName, faction, current_system )); } else if (string.Compare(source_faction_name, faction, true) == 0) { /* source faction of the mission is not getting any influence. This could be because * the source faction is in an election state in its home system and cannot gain any * influence. It may also very well be that the source and target faction are the same * since the faction is present in both target and source system. In which case we add * both and hope for the best. */ other.Value.Add(accepted_address, ""); OnLog?.Invoke(string.Format( "Mission \"{0}\" gave no influence to \"{1}\". Since \"{1}\" is the source faction " + "of the mission, we assume the influence was gained in \"{2}\". " + "Please remove the entry if this assumption is wrong.", completed.HumanReadableName, faction, accepted_system )); /* check if source/target faction are equal, in which case we also need an entry * for the target system. As said factions can be present in two systems, and can * give missions that target each other. */ if (string.Compare(source_faction_name, target_faction_name, true) == 0) { other.Value.Add(current_system_address, ""); OnLog?.Invoke(string.Format( "Mission \"{0}\" gave no influence to \"{1}\". Since \"{1}\" is the source and target faction " + "of the mission, we assume the influence was also gained in target system \"{2}\". " + "Please remove the entry if this assumption is wrong.", completed.HumanReadableName, faction, current_system )); } } } foreach (var influences in other.Value) { ulong system_address = influences.Key; string system, accepted_station; if (!systems.TryGetValue(system_address, out system)) { OnLog?.Invoke(string.Format( "Unknown system \"{0}\" unable to assign that mission a target.", system_address )); continue; } if (!acceptedStations.TryGetValue(completed.MissionID, out accepted_station)) { OnLog?.Invoke(string.Format( "Unable to figure out in which station mission \"{0}\" was accepted.", completed.HumanReadableName )); continue; } if (faction.Equals(source_faction_name) && system_address == accepted_address) { /* This is the influence block for the origin of the mission. */ main_mission = new MissionCompleted(completed) { System = accepted_system, Faction = source_faction_name, SystemAddress = accepted_address, Station = accepted_station, }; results.Add(main_mission); } else if (!faction.Equals(source_faction_name) || (faction.Equals(source_faction_name) && system_address != accepted_address)) { /* This block is for secondary factions (first if), or if the secondary faction * is the same as the mission giver, but in another system (second if). */ results.Add(new InfluenceSupport() { Faction = faction, Influence = influences.Value, System = system, SystemAddress = system_address, RelevantMission = completed }); } } } } else if (e.Is(Events.MissionAccepted)) { MissionAcceptedEntry accepted = e as MissionAcceptedEntry; ulong id = accepted.MissionID; if (!acceptedMissions.ContainsKey(id)) { acceptedMissions[id] = accepted; } if (!acceptedStations.ContainsKey(id)) { acceptedStations[id] = current_station; } if (!acceptedSystems.ContainsKey(id)) { acceptedSystems[id] = current_system_address; } } else if (e.Is(Events.MissionFailed)) { MissionFailedEntry failed = e as MissionFailedEntry; MissionAcceptedEntry accepted = null; ulong accepted_address = 0; string accepted_system; string accepted_station; if (!acceptedMissions.TryGetValue(failed.MissionID, out accepted)) { OnLog?.Invoke("A mission failed which wasn't accepted in the given time frame. " + "Please adjust start date to when the mission was accepted to include it in the list."); continue; } if (!acceptedSystems.TryGetValue(failed.MissionID, out accepted_address)) { OnLog?.Invoke(string.Format( "Unable to figure out in which system mission \"{0}\" was accepted.", accepted.Name )); continue; } if (!systems.TryGetValue(accepted_address, out accepted_system)) { OnLog?.Invoke(string.Format( "Unable to figure out in which system mission \"{0}\" was accepted.", accepted.Name )); continue; } if (!acceptedStations.TryGetValue(failed.MissionID, out accepted_station)) { OnLog?.Invoke(string.Format( "Unable to figure out in which station mission \"{0}\" was accepted.", accepted.Name )); continue; } results.Add(new MissionFailed(accepted) { Failed = failed, System = accepted_system, Station = accepted_station, Faction = accepted.Faction, SystemAddress = accepted_address, }); if (failed.HumanReadableName == null) { OnLog?.Invoke("Human readable name for mission \"" + failed.Name + "\" was not recognised"); } /* Mission failed should be collated if they are in the same system/station */ collate = CollateEntries; } else if (e.Is(Events.SellExplorationData)) { results.Add(new Cartographics(e as SellExplorationDataEntry) { System = current_system, Station = current_station, Faction = controlling_faction, }); /* colate single cartographic selling into one */ collate = CollateEntries; } else if (e.Is(Events.SellOrganicData)) { /* organic data sold to Vista Genomics */ results.Add(new OrganicData(e as SellOrganicDataEntry) { System = current_system, Station = current_station, Faction = controlling_faction, }); collate = CollateEntries; } else if (e.Is(Events.MultiSellExplorationData)) { /* For multi-sell-exploraton-data only the controlling faction of the station sold to matters. */ results.Add(new Cartographics(e as MultiSellExplorationDataEntry) { System = current_system, Station = current_station, Faction = controlling_faction }); collate = CollateEntries; } else if (e.Is(Events.RedeemVoucher)) { RedeemVoucherEntry voucher = e as RedeemVoucherEntry; List current_factions = new List(); if (system_factions.ContainsKey(current_system)) { current_factions = system_factions[current_system]; } else { OnLog?.Invoke("There are no current system factions, so turned in vouchers were ignored."); continue; } foreach (string faction in voucher.Factions) { if (current_factions.Find(x => x.Name == faction) == null) { OnLog?.Invoke( string.Format("Vouchers for \"{0}\" were ignored in \"{1}\" since said " + "faction is not present there.", faction, current_system) ); continue; /* faction is not present, so it is ignored */ } /* Same for selling combat vouchers. Only the current controlling faction matters here. */ results.Add(new Vouchers(voucher) { System = current_system, Station = current_station, Faction = faction, ControllingFaction = controlling_faction, }); } collate = CollateEntries; } else if (e.Is(Events.SellMicroResources)) { results.Add(new SellMicroResources(e as SellMicroResourcesEntry) { Faction = controlling_faction, Station = current_station, System = current_system }); } else if (e.Is(Events.MarketBuy)) { MarketBuyEntry buy = e as MarketBuyEntry; if (string.IsNullOrEmpty(buy.Type) || buy.BuyPrice == 0) { continue; } buyCost[buy.Type] = buy.BuyPrice; results.Add(new BuyCargo(buy) { Faction = controlling_faction, Station = current_station, System = current_system, }); collate = CollateEntries; } else if (e.Is(Events.SearchAndRescue)) { results.Add(new SearchAndRescue(e as SearchAndRescueEntry) { Faction = controlling_faction, Station = current_station, System = current_system, }); collate = CollateEntries; } else if (e.Is(Events.MarketSell)) { MarketSellEntry sell = e as MarketSellEntry; long profit = 0; if (!buyCost.ContainsKey(sell.Type)) { OnLog?.Invoke("Could not find buy order for the given commodity. Please adjust profit manually."); } else { long avg = buyCost[sell.Type]; profit = (long)sell.TotalSale - (avg * sell.Count); } results.Add(new SellCargo(e as MarketSellEntry) { Faction = controlling_faction, Station = current_station, System = current_system, Profit = profit }); } if (results == null || results.Count <= 0) { continue; } foreach (LogEntry entry in results) { /* Find all objectives that generally match. */ var matches = objectives .Where(x => x.Matches(entry) >= 2) .OrderBy(x => x.Matches(entry)) ; Objective objective = null; if (matches != null && matches.Count() > 0) { /* Then select the one that matches the most. */ objective = matches .OrderBy(x => x.Matches(entry)) .Reverse() .First() ; } else { /* create a new objective if we don't have one */ objective = new Objective() { Station = entry.Station, Faction = entry.Faction, System = entry.System, }; objectives.Add(objective); } LogEntry existing = null; existing = objective.LogEntries.Find(x => { try { return x.CompareTo(entry) == 0; } catch (NotImplementedException) { return false; } }); if (collate && existing != null) { existing.Entries.Add(e); } else if (!collate || existing == null) { objective.LogEntries.Add(entry); } } } } public void Scan(PlayerJournal journal) { Scan(journal, DateTime.Now, DateTime.Now); } } }