diff --git a/src/EVEMon.Common/Constants/DBConstants.cs b/src/EVEMon.Common/Constants/DBConstants.cs index 77910f0e5..8f066f7c1 100644 --- a/src/EVEMon.Common/Constants/DBConstants.cs +++ b/src/EVEMon.Common/Constants/DBConstants.cs @@ -731,6 +731,8 @@ public static ReadOnlyCollection StrategicComponentsMarketGroupIDs public const int FrekiBlueprintID = 32208; public const int MimirBlueprintID = 32210; public const int MiningFrigateSkillID = 32918; + public const int LargeSkillInjectorID = 40520; + public const int SmallSkillInjectorID = 45635; public const int AlphaDataAnalyzerIBlueprintID = 22330; public const int DaemonDataAnalyzerIBlueprintID = 22326; diff --git a/src/EVEMon.Common/Constants/NetworkConstants.resx b/src/EVEMon.Common/Constants/NetworkConstants.resx index bde9ff4f2..03a622a3d 100644 --- a/src/EVEMon.Common/Constants/NetworkConstants.resx +++ b/src/EVEMon.Common/Constants/NetworkConstants.resx @@ -607,4 +607,7 @@ /types/{0:D}/render?tenant=tranquility&size={1:D} + + https://market.fuzzwork.co.uk/aggregates/?region=10000002&types= + \ No newline at end of file diff --git a/src/EVEMon.Common/Constants/NetworkConstants1.Designer.cs b/src/EVEMon.Common/Constants/NetworkConstants1.Designer.cs index b34c7c3fa..d1ac4b21f 100644 --- a/src/EVEMon.Common/Constants/NetworkConstants1.Designer.cs +++ b/src/EVEMon.Common/Constants/NetworkConstants1.Designer.cs @@ -1059,6 +1059,15 @@ public static string ForumThreadBase { } } + /// + /// Looks up a localized string similar to https://market.fuzzwork.co.uk/aggregates/?region=10000002&types=. + /// + public static string FuzzworksMarketUrl { + get { + return ResourceManager.GetString("FuzzworksMarketUrl", resourceCulture); + } + } + /// /// Looks up a localized string similar to https://raw.githubusercontent.com/peterhaneve/evemon/main. /// diff --git a/src/EVEMon.Common/EVEMon.Common.csproj b/src/EVEMon.Common/EVEMon.Common.csproj index 9a1f965fd..c71048152 100644 --- a/src/EVEMon.Common/EVEMon.Common.csproj +++ b/src/EVEMon.Common/EVEMon.Common.csproj @@ -221,6 +221,7 @@ + @@ -239,6 +240,8 @@ + + @@ -667,7 +670,6 @@ - diff --git a/src/EVEMon.Common/MarketPricer/EveMarketData/EMDItemPricer.cs b/src/EVEMon.Common/MarketPricer/EveMarketData/EMDItemPricer.cs deleted file mode 100644 index 0b4f29a81..000000000 --- a/src/EVEMon.Common/MarketPricer/EveMarketData/EMDItemPricer.cs +++ /dev/null @@ -1,218 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using EVEMon.Common.Constants; -using EVEMon.Common.Net; -using EVEMon.Common.Serialization.EveMarketData.MarketPricer; -using EVEMon.Common.Service; - -namespace EVEMon.Common.MarketPricer.EveMarketdata -{ - public sealed class EMDItemPricer : ItemPricer - { - #region Fields - - private const string Filename = "emd_item_prices"; - - private static bool s_queryPending; - - #endregion - - - /// - /// Gets the name. - /// - public override string Name => "Eve-MarketData"; - - /// - /// Gets a value indicating whether this is enabled. - /// - /// - /// true if enabled; otherwise, false. - /// - protected override bool Enabled => true; - - /// - /// Gets the price by type ID. - /// - /// The id. - /// - public override double GetPriceByTypeID(int id) - { - // Ensure list importation - EnsureImportation(); - - double result; - PriceByItemID.TryGetValue(id, out result); - return result; - } - - - #region Importation - Exportation Methods - - /// - /// Ensures the list has been imported. - /// - private void EnsureImportation() - { - // Quit if query is pending - if (s_queryPending) - return; - - // Check the selected provider - if (!string.IsNullOrWhiteSpace(SelectedProviderName)) - { - if (SelectedProviderName != Name) - { - Loaded = false; - CachedUntil = DateTime.MinValue; - SelectedProviderName = Name; - } - } - else - SelectedProviderName = Name; - - string file = LocalXmlCache.GetFileInfo(Filename).FullName; - - // Update the file if we don't have it or the data have expired - if ((!Loaded && !File.Exists(file)) || (Loaded && CachedUntil < DateTime.UtcNow)) - { - Task.WhenAll(GetPricesAsync()); - return; - } - - // Exit if we have already imported the list - if (Loaded) - return; - - if (File.Exists(file)) - LoadFromFile(file); - else - { - Loaded = true; - CachedUntil = DateTime.UtcNow.AddHours(1); - PriceByItemID.Clear(); - } - } - - /// - /// Loads from file. - /// - /// The file. - private void LoadFromFile(string file) - { - CachedUntil = File.GetLastWriteTimeUtc(file).AddDays(1); - - // Deserialize the xml file - SerializableEMDItemPrices result = Util.DeserializeXmlFromFile(file); - - // In case the file is an old one, we try to get a fresh copy - if (result == null || CachedUntil < DateTime.UtcNow) - { - Task.WhenAll(GetPricesAsync()); - return; - } - - PriceByItemID.Clear(); - Loaded = false; - - // Import the data - Import(result.Result.ItemPrices); - } - - /// - /// Import the query result list. - /// - /// The item prices. - private static void Import(IEnumerable itemPrices) - { - EveMonClient.Trace("begin"); - - foreach (IGrouping item in itemPrices.GroupBy(item => item.ID)) - { - double buyPrice = item.First(x => x.BuySell == "b").Price; - double sellPrice = item.First(x => x.BuySell == "s").Price; - - if (Math.Abs(buyPrice) <= double.Epsilon) - PriceByItemID[item.Key] = sellPrice; - else if (Math.Abs(sellPrice) <= double.Epsilon) - PriceByItemID[item.Key] = buyPrice; - else - PriceByItemID[item.Key] = (buyPrice + sellPrice) / 2; - } - - Loaded = true; - - EveMonClient.Trace("done"); - } - - /// - /// Downloads the item prices list. - /// - protected override async Task GetPricesAsync() - { - // Quit if query is pending - if (s_queryPending) - return; - - s_queryPending = true; - - PriceByItemID.Clear(); - Loaded = false; - EveMonClient.Trace("begin"); - var url = new Uri(NetworkConstants.EVEMarketDataBaseUrl + NetworkConstants. - EVEMarketDataAPIItemPrices); - // This appears to be paginated, find out how to request more pages - var result = await Util.DownloadXmlAsync(url, - new RequestParams() - { - AcceptEncoded = true - }); - OnPricesDownloaded(result); - } - - /// - /// Called when data downloaded. - /// - /// The result. - private static void OnPricesDownloaded(DownloadResult result) - { - bool error = true; - SerializableEMDItemPriceList prices = null; - var downloadResult = result?.Result; - // Reset query pending flag - s_queryPending = false; - Loaded = true; - if (downloadResult == null) - // No result returned - EveMonClient.Trace("No result"); - else if (result.Error != null) - // Error - EveMonClient.Trace(result.Error.Message); - else if ((prices = downloadResult.Result) == null || !prices.ItemPrices.Any()) - // Empty result - EveMonClient.Trace("Empty result"); - else - error = false; - if (error || prices == null) - // Retry in 1 hour, indicate error - CachedUntil = DateTime.UtcNow.AddHours(1); - else - { - // Retry in 1 day - CachedUntil = DateTime.UtcNow.AddDays(1); - Import(prices.ItemPrices); - EveMonClient.Trace("done"); - } - EveMonClient.OnPricesDownloaded(null, string.Empty); - // Save the file in cache - if (downloadResult != null) - SaveAsync(Filename, Util.SerializeToXmlDocument(downloadResult)). - ConfigureAwait(false); - } - - #endregion - } -} diff --git a/src/EVEMon.Common/MarketPricer/EveMarketer/EMItemPricer.cs b/src/EVEMon.Common/MarketPricer/EveMarketer/EMItemPricer.cs index 0337da4e2..7106fdfd1 100644 --- a/src/EVEMon.Common/MarketPricer/EveMarketer/EMItemPricer.cs +++ b/src/EVEMon.Common/MarketPricer/EveMarketer/EMItemPricer.cs @@ -1,15 +1,15 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using EVEMon.Common.Collections; +using EVEMon.Common.Collections; using EVEMon.Common.Constants; using EVEMon.Common.Data; using EVEMon.Common.Net; using EVEMon.Common.Serialization.EveMarketer.MarketPricer; using EVEMon.Common.Service; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; namespace EVEMon.Common.MarketPricer.EveMarketer { @@ -18,12 +18,11 @@ public sealed class EMItemPricer : ItemPricer #region Fields private const string Filename = "ec_item_prices"; + private const int MAX_QUERY = 60; - private static Queue s_queue; - private static List s_queryMonitorList; - private static int s_queryCounter; + private static readonly Queue s_queue = new Queue(); + private static readonly HashSet s_requested = new HashSet(); private static bool s_queryPending; - private static int s_queryStep; #endregion @@ -53,6 +52,19 @@ public override double GetPriceByTypeID(int id) double result; PriceByItemID.TryGetValue(id, out result); + lock (s_queue) + { + if (!s_requested.Contains(id)) + { + s_requested.Add(id); + s_queue.Enqueue(id); + if (!s_queryPending) + { + s_queryPending = true; + Task.WhenAll(QueryIDs()); + } + } + } return result; } @@ -71,7 +83,6 @@ private void EnsureImportation() if (SelectedProviderName != Name) { Loaded = false; - CachedUntil = DateTime.MinValue; SelectedProviderName = Name; } } @@ -80,12 +91,6 @@ private void EnsureImportation() string file = LocalXmlCache.GetFileInfo(Filename).FullName; - if ((!Loaded && !File.Exists(file)) || (Loaded && CachedUntil < DateTime.UtcNow)) - { - Task.WhenAll(GetPricesAsync()); - return; - } - // Exit if we have already imported the list if (Loaded) return; @@ -95,7 +100,6 @@ private void EnsureImportation() else { Loaded = true; - CachedUntil = DateTime.UtcNow.AddHours(1); PriceByItemID.Clear(); } } @@ -106,20 +110,12 @@ private void EnsureImportation() /// The file. private void LoadFromFile(string file) { - CachedUntil = File.GetLastWriteTimeUtc(file).AddDays(1); - // Deserialize the xml file var result = Util.DeserializeXmlFromFile(file); - // In case the file is an old one, we try to get a fresh copy - if (result == null || CachedUntil < DateTime.UtcNow) - { - Task.WhenAll(GetPricesAsync()); - return; - } - PriceByItemID.Clear(); Loaded = false; + s_requested.Clear(); // Import the data Import(result.ItemPrices); @@ -131,48 +127,10 @@ private void LoadFromFile(string file) /// The item prices. private static void Import(IEnumerable itemPrices) { - if (!PriceByItemID.Any()) - EveMonClient.Trace("begin"); - foreach (SerializableECItemPriceListItem item in itemPrices) { PriceByItemID[item.ID] = item.Prices.Average; } - - if (((s_queue == null) || (s_queue.Count == 0)) && !s_queryPending) - Loaded = true; - - if (Loaded) - EveMonClient.Trace("done"); - } - - /// - /// Gets the prices asynchronous. - /// - /// Gets the item prices list. - protected override async Task GetPricesAsync() - { - // Quit if query is pending - if (s_queryPending) - return; - - s_queryPending = true; - - PriceByItemID.Clear(); - Loaded = false; - s_queryStep = 200; - - var marketItems = StaticItems.AllItems.Where(item => - !item.MarketGroup.BelongsIn(DBConstants.RootNonMarketGroupID) && - !item.MarketGroup.BelongsIn(DBConstants.BlueprintRootNonMarketGroupID) && - !item.MarketGroup.BelongsIn(DBConstants.UniqueDesignsRootNonMarketGroupID)). - Select(item => item.ID).OrderBy(id => id); - s_queue = new Queue(marketItems); - s_queryMonitorList = marketItems.ToList(); - - EveMonClient.Trace("begin"); - - await QueryIDs(); } /// @@ -185,13 +143,18 @@ private async Task QueryIDs() var url = new Uri(NetworkConstants.EVEMarketerBaseUrl + NetworkConstants. EVEMarketerAPIItemPrices); - while (s_queue.Count > 0) + while (true) { - idsToQuery.Clear(); - for (int i = 0; i < s_queryStep && s_queue.Count > 0; i++) - idsToQuery.Add(s_queue.Dequeue()); + lock (s_queue) + { + // Cannot await inside lock, this is the cleanest way to do it + if (s_queue.Count == 0) + return; + idsToQuery.Clear(); + for (int i = 0; i < MAX_QUERY && s_queue.Count > 0; i++) + idsToQuery.Add(s_queue.Dequeue()); + } - s_queryCounter++; var result = await Util.DownloadXmlAsync(url, new RequestParams(GetQueryString(idsToQuery)) { @@ -208,7 +171,7 @@ private async Task QueryIDs() /// private static string GetQueryString(IReadOnlyCollection idsToQuery) { - var sb = new StringBuilder(); + var sb = new StringBuilder(256); foreach (int i in idsToQuery) { sb.Append($"typeid={i}"); @@ -232,10 +195,9 @@ private void OnPricesDownloaded(DownloadResult result) return; if (EveMonClient.IsDebugBuild) - EveMonClient.Trace($"Remaining ids: {string.Join(", ", s_queryMonitorList)}", printMethod: false); + EveMonClient.Trace($"Remaining ids: {string.Join(", ", s_queue)}", printMethod: false); Loaded = true; - CachedUntil = DateTime.UtcNow.AddDays(1); // Reset query pending flag s_queryPending = false; @@ -255,42 +217,28 @@ private void OnPricesDownloaded(DownloadResult result) /// private bool CheckQueryStatus(DownloadResult result) { - s_queryCounter--; - - if (result == null || result.Error != null) + if (result?.Error != null) { - if (result?.Error != null) - { - EveMonClient.Trace(result.Error.Message); - - // Abort further attempts - if (result.Error.Status == HttpWebClientServiceExceptionStatus.Timeout || - result.Error.Status == HttpWebClientServiceExceptionStatus.ServerError) - { - s_queue.Clear(); - - // Set a retry - Loaded = true; - CachedUntil = DateTime.UtcNow.AddHours(1); + EveMonClient.Trace(result.Error.Message); - // Reset query pending flag - s_queryPending = false; - EveMonClient.OnPricesDownloaded(null, string.Empty); + // Abort further attempts if it is a connection issue + if (result.Error.Status == HttpWebClientServiceExceptionStatus.Timeout || + result.Error.Status == HttpWebClientServiceExceptionStatus.ServerError) + { + s_queue.Clear(); - // We return 'true' to avoid saving a file - return true; - } + // Reset query pending flag + s_queryPending = false; + EveMonClient.OnPricesDownloaded(null, string.Empty); - // If it's a 'Bad Request' just return - // We'll check those items later on a lower query step - if (result.Error.Status == HttpWebClientServiceExceptionStatus.Exception && - result.Error.Message.Contains("400 (Bad Request)") && s_queue.Count != 0) - { - return true; - } + // We return 'true' to avoid saving a file + return true; + } + lock (s_queue) + { // If we are done set the proper flags - if (!s_queryMonitorList.Any() || s_queryStep <= 1) + if (s_queue.Count < 1) { Loaded = true; EveMonClient.Trace("ECItemPricer.Import - done", printMethod: false); @@ -299,32 +247,18 @@ private bool CheckQueryStatus(DownloadResult result) } } - // When the query succeeds import the data and remove the ids from the monitoring list + // When the query succeeds import the data if (result?.Result != null) - { - foreach (SerializableECItemPriceListItem item in result.Result.ItemPrices) - { - s_queryMonitorList.Remove(item.ID); - } - Import(result.Result.ItemPrices); - } - - // If all items where queried we are done - if (s_queryCounter == 0 && s_queue.Count == 0 && s_queryStep <= 1) - return false; - // If there are still items in queue just continue - if (s_queryCounter != 0 || !s_queryMonitorList.Any() || s_queue.Count != 0) - return true; - - // if there are ids still to query repeat the query on a lower query step - s_queryStep = s_queryStep / 2; - s_queue = new Queue(s_queryMonitorList); - - Task.WhenAll(QueryIDs()); + // If all items where queried we are done (false = save file) + bool hasMore; + lock (s_queue) + { + hasMore = s_queue.Count > 0; + } - return true; + return hasMore; } /// diff --git a/src/EVEMon.Common/MarketPricer/Fuzzworks/FuzzworksItemPricer.cs b/src/EVEMon.Common/MarketPricer/Fuzzworks/FuzzworksItemPricer.cs new file mode 100644 index 000000000..a2101e814 --- /dev/null +++ b/src/EVEMon.Common/MarketPricer/Fuzzworks/FuzzworksItemPricer.cs @@ -0,0 +1,305 @@ +using EVEMon.Common.Collections; +using EVEMon.Common.Constants; +using EVEMon.Common.Net; +using EVEMon.Common.Serialization; +using EVEMon.Common.Serialization.EveMarketer.MarketPricer; +using EVEMon.Common.Serialization.Fuzzworks; +using EVEMon.Common.Service; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +using FuzzworksResult = System.Collections.Generic.Dictionary; + +namespace EVEMon.Common.MarketPricer.Fuzzworks +{ + public sealed class FuzzworksItemPricer : ItemPricer + { + #region Fields + + private const string Filename = "fuzzy_item_prices"; + private const int MAX_QUERY = 60; + + private static readonly Queue s_queue = new Queue(); + private static readonly HashSet s_requested = new HashSet(); + private static bool s_queryPending; + + #endregion + + + /// + /// Gets the name. + /// + public override string Name => "Fuzzworks"; + + /// + /// Gets a value indicating whether this is enabled. + /// + /// + /// true if enabled; otherwise, false. + /// + protected override bool Enabled => true; + + /// + /// Gets the price by type ID. + /// + /// The id. + /// + public override double GetPriceByTypeID(int id) + { + // Ensure list importation + EnsureImportation(); + + double result; + PriceByItemID.TryGetValue(id, out result); + lock (s_queue) + { + if (!s_requested.Contains(id)) + { + s_requested.Add(id); + s_queue.Enqueue(id); + if (!s_queryPending) + { + s_queryPending = true; + Task.WhenAll(QueryIDs()); + } + } + } + return result; + } + + /// + /// Ensures the importation. + /// + private void EnsureImportation() + { + // Quit if query is pending + if (s_queryPending) + return; + + // Check the selected provider + if (!string.IsNullOrWhiteSpace(SelectedProviderName)) + { + if (SelectedProviderName != Name) + { + Loaded = false; + SelectedProviderName = Name; + } + } + else + SelectedProviderName = Name; + + string file = LocalXmlCache.GetFileInfo(Filename).FullName; + + // Exit if we have already imported the list + if (Loaded) + return; + + if (File.Exists(file)) + LoadFromFile(file); + else + { + Loaded = true; + PriceByItemID.Clear(); + } + } + + /// + /// Loads from file. + /// + /// The file. + private void LoadFromFile(string file) + { + // Deserialize the xml file + var result = Util.DeserializeXmlFromFile(file); + + PriceByItemID.Clear(); + Loaded = false; + s_requested.Clear(); + + // Import the data + Import(result.ItemPrices); + } + + /// + /// Imports the specified item prices. + /// + /// The item prices. + private static void Import(IEnumerable itemPrices) + { + foreach (SerializableECItemPriceListItem item in itemPrices) + { + PriceByItemID[item.ID] = item.Prices.Average; + } + } + + /// + /// Imports the specified item prices. + /// + /// The item prices. + private static void Import(IDictionary itemPrices) + { + foreach (var pair in itemPrices) + // IDs in JSON cannot be integers + if (int.TryParse(pair.Key, out int id)) + { + var item = pair.Value; + + if (item.Sell != null) + // JSV Min + PriceByItemID[id] = item.Sell.MinPrice; + else if (item.Buy != null) + // JBV Max + PriceByItemID[id] = item.Buy.MaxPrice; + } + } + + /// + /// Queries the ids. + /// + /// + private async Task QueryIDs() + { + var idsToQuery = new List(); + + while (true) + { + lock (s_queue) + { + // Cannot await inside lock, this is the cleanest way to do it + if (s_queue.Count == 0) + return; + idsToQuery.Clear(); + for (int i = 0; i < MAX_QUERY && s_queue.Count > 0; i++) + idsToQuery.Add(s_queue.Dequeue()); + } + + var url = new Uri(NetworkConstants.FuzzworksMarketUrl + GetQueryString( + idsToQuery)); + var result = await Util.DownloadJsonAsync(url, + new RequestParams() + { + AcceptEncoded = true + }); + OnPricesDownloaded(result); + } + } + + /// + /// Gets the query string. + /// + /// The ids to query. + /// + private static string GetQueryString(IReadOnlyCollection idsToQuery) + { + var sb = new StringBuilder(256); + foreach (int i in idsToQuery) + { + sb.Append(i); + + if (idsToQuery.Last() != i) + sb.Append(","); + } + return sb.ToString(); + } + + /// + /// Called when prices downloaded. + /// + /// The result. + private void OnPricesDownloaded(JsonResult result) + { + if (CheckQueryStatus(result)) + return; + + if (EveMonClient.IsDebugBuild) + EveMonClient.Trace($"Remaining ids: {string.Join(", ", s_queue)}", printMethod: false); + + Loaded = true; + + // Reset query pending flag + s_queryPending = false; + + EveMonClient.Trace("done"); + + EveMonClient.OnPricesDownloaded(null, string.Empty); + + // Save the file in cache + SaveAsync(Filename, Util.SerializeToXmlDocument(Export())).ConfigureAwait(false); + } + + /// + /// Checks the query status. + /// + /// The result. + /// + private bool CheckQueryStatus(JsonResult result) + { + if (result == null || result.HasError) + { + // Abort further attempts if it is a connection issue + if (result != null) + { + EveMonClient.Trace(result.ErrorMessage); + s_queue.Clear(); + + // Reset query pending flag + s_queryPending = false; + EveMonClient.OnPricesDownloaded(null, string.Empty); + + // We return 'true' to avoid saving a file + return true; + } + + lock (s_queue) + { + // If we are done set the proper flags + if (s_queue.Count < 1) + { + Loaded = true; + EveMonClient.Trace("ECItemPricer.Import - done", printMethod: false); + return false; + } + } + } + else + // When the query succeeds import the data + Import(result.Result); + + // If all items where queried we are done (false = save file) + bool hasMore; + lock (s_queue) + { + hasMore = s_queue.Count > 0; + } + + return hasMore; + } + + /// + /// Exports the cache list to a serializable object. + /// + /// + private static SerializableECItemPrices Export() + { + IEnumerable entitiesList = PriceByItemID + .OrderBy(x => x.Key) + .Select( + item => + new SerializableECItemPriceListItem + { + ID = item.Key, + Prices = new SerializableECItemPriceItem { Average = item.Value } + }); + + SerializableECItemPrices serial = new SerializableECItemPrices(); + serial.ItemPrices.AddRange(entitiesList); + + return serial; + } + } +} diff --git a/src/EVEMon.Common/MarketPricer/ItemPricer.cs b/src/EVEMon.Common/MarketPricer/ItemPricer.cs index bdd240180..ff5635a0c 100644 --- a/src/EVEMon.Common/MarketPricer/ItemPricer.cs +++ b/src/EVEMon.Common/MarketPricer/ItemPricer.cs @@ -1,10 +1,10 @@ -using System; +using EVEMon.Common.Service; +using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Threading.Tasks; using System.Xml.XPath; -using EVEMon.Common.Service; namespace EVEMon.Common.MarketPricer { @@ -12,8 +12,6 @@ public abstract class ItemPricer { protected static readonly Dictionary PriceByItemID = new Dictionary(); - protected static DateTime CachedUntil; - protected static string SelectedProviderName; protected static bool Loaded; @@ -56,13 +54,7 @@ public abstract class ItemPricer /// The id. /// public abstract double GetPriceByTypeID(int id); - - /// - /// Gets the prices asynchronous. - /// - /// Gets the item prices list. - protected abstract Task GetPricesAsync(); - + /// /// Saves the xml document to the specified filename. /// diff --git a/src/EVEMon.Common/Models/BaseCharacter.cs b/src/EVEMon.Common/Models/BaseCharacter.cs index 3caebec03..f232dca5f 100644 --- a/src/EVEMon.Common/Models/BaseCharacter.cs +++ b/src/EVEMon.Common/Models/BaseCharacter.cs @@ -371,9 +371,9 @@ public TimeSpan GetTrainingTimeToMultipleSkills(IEnumerable trainin /// public struct SkillInjectorsRequired { - int Large { get; } + public int Large { get; } - int Small { get; } + public int Small { get; } public int Total { diff --git a/src/EVEMon.Common/Resources/MD5Sums.txt b/src/EVEMon.Common/Resources/MD5Sums.txt index bd28bc3ce..fedaa5406 100644 --- a/src/EVEMon.Common/Resources/MD5Sums.txt +++ b/src/EVEMon.Common/Resources/MD5Sums.txt @@ -1,8 +1,8 @@ -3a807bb1d48460c57e10ef7fddcaf619 *eve-blueprints-en-US.xml.gzip +df91ac86ac9954dfaff13ab77681a1cc *eve-blueprints-en-US.xml.gzip 540000f7c111354769a54fa31e57d411 *eve-certificates-en-US.xml.gzip 3077e700023d84a267b744a6ee44acc7 *eve-geography-en-US.xml.gzip -0abab62dde07fbf0bc8eafe0811c9d18 *eve-items-en-US.xml.gzip +4c9cb667e22421fad8ca08777be7fd8a *eve-items-en-US.xml.gzip 3668b50996fc40a0c0853fec427b5712 *eve-masteries-en-US.xml.gzip -93b8499c86acdad46607c697f221970a *eve-properties-en-US.xml.gzip -46b56972972a308d90c153770ce46000 *eve-reprocessing-en-US.xml.gzip -84843e1f2e8946cb8f7f7588d2c72562 *eve-skills-en-US.xml.gzip +1f6ae9f613fd26b08d5ec206d0958d79 *eve-properties-en-US.xml.gzip +adb83f4fc55f61a58548dcb924a91cfe *eve-reprocessing-en-US.xml.gzip +84a8c259c438f2735735fc2e32e6bf93 *eve-skills-en-US.xml.gzip diff --git a/src/EVEMon.Common/Resources/eve-blueprints-en-US.xml.gzip b/src/EVEMon.Common/Resources/eve-blueprints-en-US.xml.gzip index c983f2faa..c7d5fe310 100644 Binary files a/src/EVEMon.Common/Resources/eve-blueprints-en-US.xml.gzip and b/src/EVEMon.Common/Resources/eve-blueprints-en-US.xml.gzip differ diff --git a/src/EVEMon.Common/Resources/eve-items-en-US.xml.gzip b/src/EVEMon.Common/Resources/eve-items-en-US.xml.gzip index fdb72f1a0..31715da1b 100644 Binary files a/src/EVEMon.Common/Resources/eve-items-en-US.xml.gzip and b/src/EVEMon.Common/Resources/eve-items-en-US.xml.gzip differ diff --git a/src/EVEMon.Common/Resources/eve-properties-en-US.xml.gzip b/src/EVEMon.Common/Resources/eve-properties-en-US.xml.gzip index c602323ff..9d1537ccb 100644 Binary files a/src/EVEMon.Common/Resources/eve-properties-en-US.xml.gzip and b/src/EVEMon.Common/Resources/eve-properties-en-US.xml.gzip differ diff --git a/src/EVEMon.Common/Resources/eve-reprocessing-en-US.xml.gzip b/src/EVEMon.Common/Resources/eve-reprocessing-en-US.xml.gzip index ef85f782b..3fceec093 100644 Binary files a/src/EVEMon.Common/Resources/eve-reprocessing-en-US.xml.gzip and b/src/EVEMon.Common/Resources/eve-reprocessing-en-US.xml.gzip differ diff --git a/src/EVEMon.Common/Resources/eve-skills-en-US.xml.gzip b/src/EVEMon.Common/Resources/eve-skills-en-US.xml.gzip index 23c1a83d4..4a3f00506 100644 Binary files a/src/EVEMon.Common/Resources/eve-skills-en-US.xml.gzip and b/src/EVEMon.Common/Resources/eve-skills-en-US.xml.gzip differ diff --git a/src/EVEMon.Common/Serialization/Fuzzworks/SerializableFuzzworksPriceItem.cs b/src/EVEMon.Common/Serialization/Fuzzworks/SerializableFuzzworksPriceItem.cs new file mode 100644 index 000000000..d5f4f06e7 --- /dev/null +++ b/src/EVEMon.Common/Serialization/Fuzzworks/SerializableFuzzworksPriceItem.cs @@ -0,0 +1,14 @@ +using System.Runtime.Serialization; + +namespace EVEMon.Common.Serialization.Fuzzworks +{ + [DataContract] + public sealed class SerializableFuzzworksPriceItem + { + [DataMember(Name = "buy")] + public SerializableFuzzworksPriceListItem Buy { get; set; } + + [DataMember(Name = "sell")] + public SerializableFuzzworksPriceListItem Sell { get; set; } + } +} diff --git a/src/EVEMon.Common/Serialization/Fuzzworks/SerializableFuzzworksPriceListItem.cs b/src/EVEMon.Common/Serialization/Fuzzworks/SerializableFuzzworksPriceListItem.cs new file mode 100644 index 000000000..f2ac44f37 --- /dev/null +++ b/src/EVEMon.Common/Serialization/Fuzzworks/SerializableFuzzworksPriceListItem.cs @@ -0,0 +1,20 @@ +using System.Runtime.Serialization; + +namespace EVEMon.Common.Serialization.Fuzzworks +{ + [DataContract] + public sealed class SerializableFuzzworksPriceListItem + { + [DataMember(Name = "weightedAverage")] + public double AveragePrice { get; set; } + + [DataMember(Name = "max")] + public double MaxPrice { get; set; } + + [DataMember(Name = "min")] + public double MinPrice { get; set; } + + [DataMember(Name = "median")] + public double MedianPrice { get; set; } + } +} diff --git a/src/EVEMon/CharacterMonitoring/CharacterAssetsList.cs b/src/EVEMon/CharacterMonitoring/CharacterAssetsList.cs index 99f7d6aad..1e042c629 100644 --- a/src/EVEMon/CharacterMonitoring/CharacterAssetsList.cs +++ b/src/EVEMon/CharacterMonitoring/CharacterAssetsList.cs @@ -349,8 +349,12 @@ private async Task UpdateContentAsync() lvAssets.BeginUpdate(); try { - var assets = m_list.Where(x => x.Item != null && x.SolarSystem != null). - Where(x => IsTextMatching(x, m_textFilter)).ToList(); + List assets; + lock (m_list) + { + assets = m_list.Where(x => x.Item != null && x.SolarSystem != null). + Where(x => IsTextMatching(x, m_textFilter)).ToList(); + } UpdateSort(); @@ -799,7 +803,10 @@ private async Task UpdateAssetLocationAsync() await TaskHelper.RunCPUBoundTaskAsync(() => { Character.Assets.UpdateLocation(); - Assets = Character.Assets; + lock (m_list) + { + Assets = Character.Assets; + } }); await UpdateColumnsAsync(); diff --git a/src/EVEMon/SkillPlanner/PlanEditorControl.cs b/src/EVEMon/SkillPlanner/PlanEditorControl.cs index a4ba2bc12..1c617a12b 100644 --- a/src/EVEMon/SkillPlanner/PlanEditorControl.cs +++ b/src/EVEMon/SkillPlanner/PlanEditorControl.cs @@ -95,6 +95,7 @@ public PlanEditorControl() EveMonClient.CharacterUpdated += EveMonClient_CharacterUpdated; EveMonClient.CharacterSkillQueueUpdated += EveMonClient_CharacterSkillQueueUpdated; EveMonClient.CharacterImplantSetCollectionChanged += EveMonClient_CharacterImplantSetCollectionChanged; + EveMonClient.ItemPricesUpdated += EveMonClient_ItemPricesUpdated; EveMonClient.PlanChanged += EveMonClient_PlanChanged; EveMonClient.SettingsChanged += EveMonClient_SettingsChanged; EveMonClient.TimerTick += EveMonClient_TimerTick; @@ -144,6 +145,7 @@ private void OnDisposed(object sender, EventArgs e) EveMonClient.CharacterUpdated -= EveMonClient_CharacterUpdated; EveMonClient.CharacterSkillQueueUpdated -= EveMonClient_CharacterSkillQueueUpdated; EveMonClient.CharacterImplantSetCollectionChanged -= EveMonClient_CharacterImplantSetCollectionChanged; + EveMonClient.ItemPricesUpdated -= EveMonClient_ItemPricesUpdated; EveMonClient.PlanChanged -= EveMonClient_PlanChanged; EveMonClient.SettingsChanged -= EveMonClient_SettingsChanged; EveMonClient.TimerTick -= EveMonClient_TimerTick; @@ -251,6 +253,16 @@ private void EveMonClient_CharacterSkillQueueUpdated(object sender, CharacterCha UpdateDisplayPlan(); } + /// + /// Occurs when global item prices are loaded (this updates the skill injector costs). + /// + /// The source of the event. + /// The instance containing the event data. + private void EveMonClient_ItemPricesUpdated(object sender, EventArgs e) + { + UpdateStatusBar(); + } + /// /// When the character implant sets changed, update the implant set selection. /// diff --git a/src/EVEMon/SkillPlanner/PlanWindow.cs b/src/EVEMon/SkillPlanner/PlanWindow.cs index 0e9896502..99f1a9f66 100644 --- a/src/EVEMon/SkillPlanner/PlanWindow.cs +++ b/src/EVEMon/SkillPlanner/PlanWindow.cs @@ -96,6 +96,7 @@ protected override void OnLoad(EventArgs e) // Global events (unsubscribed on window closing) EveMonClient.PlanNameChanged += EveMonClient_PlanNameChanged; EveMonClient.SettingsChanged += EveMonClient_SettingsChanged; + EveMonClient.ItemPricesUpdated += EveMonClient_ItemPricesUpdated; // Compatibility mode : Mac OS if (Settings.Compatibility == CompatibilityMode.Wine) @@ -142,6 +143,7 @@ protected override void OnFormClosing(FormClosingEventArgs e) // Unsubscribe global events EveMonClient.PlanNameChanged -= EveMonClient_PlanNameChanged; EveMonClient.SettingsChanged -= EveMonClient_SettingsChanged; + EveMonClient.ItemPricesUpdated -= EveMonClient_ItemPricesUpdated; // Save settings if this one is the last activated and up-to-date if (s_lastActivated == this) @@ -704,6 +706,16 @@ private bool CheckClipboardSkillQueue(string text, List clipbo #region Global events + /// + /// Occurs when global item prices are loaded (this updates the skill injector costs). + /// + /// The source of the event. + /// The instance containing the event data. + private void EveMonClient_ItemPricesUpdated(object sender, EventArgs e) + { + UpdateStatusBar(); + } + /// /// Occurs when a plan name changed. /// @@ -786,19 +798,35 @@ internal void UpdateCostStatusLabel(bool selected, long totalcost, long cost) /// The skill points. internal void UpdateSkillPointsStatusLabel(bool selected, int skillCount, long skillPoints) { + var skillInjectorsCount = m_plan.Character.GetRequiredSkillInjectorsForSkillPoints( + skillPoints); + SkillPointsStatusLabel.AutoToolTip = skillPoints <= 0; if (skillPoints > 0) { - SkillPointsStatusLabel.ToolTipText = - $"{skillPoints:N0} skill points required to train " + + // If a market pricer is set up and it has prices for injectors, use it + var pricer = Settings.MarketPricer?.Pricer; + double cost = 0.0; + if (pricer != null) + { + double cs = pricer.GetPriceByTypeID(DBConstants.SmallSkillInjectorID), + cl = pricer.GetPriceByTypeID(DBConstants.LargeSkillInjectorID); + if (cs > 0.0 && cl > 0.0) + cost = skillInjectorsCount.Large * cl + skillInjectorsCount.Small * cs; + } + + string tooltip = $"{skillPoints:N0} skill points required to train " + (selected ? "selected" : "all") + $" skill{skillCount.S()}"; + if (cost > 0.0) + tooltip += $"\n{cost:N2} ISK required to purchase these injectors"; + SkillPointsStatusLabel.ToolTipText = tooltip; } + else + SkillPointsStatusLabel.ToolTipText = ""; - var skillInjectorsCount = m_plan.Character.GetRequiredSkillInjectorsForSkillPoints( - skillPoints); SkillPointsStatusLabel.Text = skillPoints <= 0 ? "No SP required" : - $"{skillPoints:N0} SP required ({skillInjectorsCount.ToString()})"; + $"{skillPoints:N0} SP required ({skillInjectorsCount})"; } /// diff --git a/tools/XmlGenerator/Datafiles/Items.cs b/tools/XmlGenerator/Datafiles/Items.cs index d0fffd202..fb60a3fb8 100644 --- a/tools/XmlGenerator/Datafiles/Items.cs +++ b/tools/XmlGenerator/Datafiles/Items.cs @@ -211,8 +211,10 @@ private static void CreateItem(InvTypes srcItem, ICollection g InvGroups itemGroup = Database.InvGroupsTable[srcItem.GroupID]; - // Creates the item with base information - SerializableItem item = new SerializableItem + // Creates the item with base information + var categories = Database.InvCategoriesTable; + int cID = itemGroup.CategoryID; + SerializableItem item = new SerializableItem { ID = srcItem.ID, Name = srcItem.Name, @@ -222,7 +224,8 @@ private static void CreateItem(InvTypes srcItem, ICollection g PortionSize = srcItem.PortionSize, MetaGroup = ItemMetaGroup.None, Group = itemGroup.Name, - Category = Database.InvCategoriesTable[itemGroup.CategoryID].Name, + Category = categories.HasValue(cID) ? Database.InvCategoriesTable[cID].Name : + "", Race = (Race)Enum.ToObject(typeof(Race), srcItem.RaceID ?? 0) }; diff --git a/tools/XmlGenerator/Providers/Database.cs b/tools/XmlGenerator/Providers/Database.cs index a79901e0d..20a475be3 100644 --- a/tools/XmlGenerator/Providers/Database.cs +++ b/tools/XmlGenerator/Providers/Database.cs @@ -1090,6 +1090,13 @@ private static BagCollection MarketGroups() collection.Items.Add(item); } + // CCPLease missing market groups + collection.Items.Add(new InvMarketGroups() + { + ID = 2763, Name = "Mobile Cynosural Beacons", ParentID = 404, + Description = "", IconID = 0 + }); + return collection.ToBag(); }