using SPTarkov.DI.Annotations; using SPTarkov.Server.Core.Exceptions.Helpers; using SPTarkov.Server.Core.Models.Common; using SPTarkov.Server.Core.Models.Eft.Common.Tables; using SPTarkov.Server.Core.Models.Enums; using SPTarkov.Server.Core.Models.Spt.Config; using SPTarkov.Server.Core.Models.Spt.Logging; using SPTarkov.Server.Core.Models.Utils; using SPTarkov.Server.Core.Servers; using SPTarkov.Server.Core.Services; using SPTarkov.Server.Core.Utils.Cloners; namespace SPTarkov.Server.Core.Helpers; [Injectable(InjectionType.Singleton)] public class HandbookHelper(ISptLogger logger, DatabaseService databaseService, ConfigServer configServer, ICloner cloner) { private LookupCollection? _handbookPriceCache; protected virtual LookupCollection HandbookPriceCache { get { return _handbookPriceCache ??= HydrateHandbookCache(); } } protected readonly ItemConfig ItemConfig = configServer.GetConfig(); /// /// Create an in-memory cache of all items with associated handbook price in handbookPriceCache class /// protected LookupCollection HydrateHandbookCache() { var result = new LookupCollection(); var handbook = databaseService.GetHandbook(); // Add handbook overrides found in items.json config into db foreach (var (key, priceOverride) in ItemConfig.HandbookPriceOverride) { var itemToUpdate = handbook.Items.FirstOrDefault(item => item.Id == key); if (itemToUpdate is null) { handbook.Items.Add( new HandbookItem { Id = key, ParentId = priceOverride.ParentId, Price = priceOverride.Price, } ); itemToUpdate = handbook.Items.FirstOrDefault(item => item.Id == key); } itemToUpdate!.Price = priceOverride.Price; itemToUpdate.ParentId = priceOverride.ParentId; } var handbookDbClone = cloner.Clone(handbook)!; foreach (var handbookItem in handbookDbClone.Items) { result.Items.ById.TryAdd(handbookItem.Id, handbookItem.Price ?? 0); if (!result.Items.ByParent.TryGetValue(handbookItem.ParentId, out _)) { result.Items.ByParent.TryAdd(handbookItem.ParentId, []); } if (!result.Items.ByParent.TryGetValue(handbookItem.ParentId, out var itemIds)) { throw new HandbookHelperException( $"Cannot add item id `{handbookItem.Id}` to parent id `{handbookItem.ParentId}`. Parent does not exist." ); } itemIds.Add(handbookItem.Id); } foreach (var handbookCategory in handbookDbClone.Categories) { if (!result.Categories.ById.TryAdd(handbookCategory.Id, handbookCategory.ParentId)) { var message = $"Unable to add `{handbookCategory.Id}`. Key already exists."; logger.Error(message); throw new HandbookHelperException(message); } if (handbookCategory.ParentId is not null) { if (!result.Categories.ByParent.TryGetValue(handbookCategory.ParentId.Value, out _)) { result.Categories.ByParent.TryAdd(handbookCategory.ParentId.Value, []); } if (!result.Categories.ByParent.TryGetValue(handbookCategory.ParentId.Value, out var itemIds)) { throw new HandbookHelperException( $"Cannot add item id `{handbookCategory.Id}` to parent id `{handbookCategory.ParentId.Value}`. Parent does not exist." ); } itemIds.Add(handbookCategory.Id); } } return result; } /// /// Get price from internal cache, if cache empty look up price directly in handbook (expensive) /// If no values found, return 0 /// /// Item tpl to look up price for /// price in roubles public double GetTemplatePrice(MongoId tpl) { if (HandbookPriceCache.Items.ById.TryGetValue(tpl, out var itemPrice)) { return itemPrice; } var handbookItem = databaseService.GetHandbook().Items?.FirstOrDefault(item => item.Id == tpl); if (handbookItem is null) { const int newValue = 0; if (!HandbookPriceCache.Items.ById.TryAdd(tpl, newValue)) { // Overwrite HandbookPriceCache.Items.ById[tpl] = newValue; } return newValue; } if (!HandbookPriceCache.Items.ById.TryAdd(tpl, handbookItem.Price ?? 0)) { // Overwrite HandbookPriceCache.Items.ById[tpl] = handbookItem.Price ?? 0; } return handbookItem.Price.Value; } /// /// Sum price of supplied items with handbook prices /// /// Items to Sum /// public double GetTemplatePriceForItems(IEnumerable items) { return items.Sum(item => GetTemplatePrice(item.Template)); } /// /// Get all items in template with the given parent category /// /// /// string array public List TemplatesWithParent(MongoId parentId) { if (HandbookPriceCache.Items.ByParent.TryGetValue(parentId, out var templates)) { return templates; } if (logger.IsLogEnabled(LogLevel.Debug)) { logger.Debug($"Template ids with parent id `{parentId}` not found when trying to get templates by parent"); } return []; } /// /// Does category exist in handbook cache /// /// /// true if exists in cache public bool IsCategory(MongoId category) { return HandbookPriceCache.Categories.ById.TryGetValue(category, out _); } /// /// Get all items associated with a categories parent /// /// /// string array public List ChildrenCategories(MongoId categoryParent) { if (HandbookPriceCache.Categories.ByParent.TryGetValue(categoryParent, out var childrenCategories)) { return childrenCategories; } if (logger.IsLogEnabled(LogLevel.Debug)) { logger.Debug($"Children categories with parent id `{categoryParent}` not found when trying to get children categories"); } return []; } /// /// Convert non-roubles into roubles /// /// Currency count to convert /// What current currency is /// Count in roubles public double InRoubles(double nonRoubleCurrencyCount, MongoId currencyTypeFrom) { return currencyTypeFrom == Money.ROUBLES ? nonRoubleCurrencyCount : Math.Round(nonRoubleCurrencyCount * GetTemplatePrice(currencyTypeFrom)); } /// /// Convert roubles into another currency /// /// roubles to convert /// Currency to convert roubles into /// currency count in desired type public double FromRoubles(double roubleCurrencyCount, MongoId currencyTypeTo) { if (currencyTypeTo == Money.ROUBLES) { return roubleCurrencyCount; } // Get price of currency from handbook var price = GetTemplatePrice(currencyTypeTo); return price > 0 ? Math.Max(1, Math.Round(roubleCurrencyCount / price)) : 0; } public HandbookCategory? GetCategoryById(MongoId handbookId) { return databaseService.GetHandbook().Categories.FirstOrDefault(category => category.Id == handbookId); } protected record LookupItem { public LookupItem() { ById = new Dictionary(); ByParent = new Dictionary>(); } public Dictionary ById { get; set; } public Dictionary> ByParent { get; set; } } protected record LookupCollection { public LookupCollection() { Items = new LookupItem(); Categories = new LookupItem(); } public LookupItem Items { get; set; } public LookupItem Categories { get; set; } } }