using SptCommon.Annotations; using Core.Models.Eft.Common.Tables; using Core.Models.Enums; using Core.Models.Spt.Config; using Core.Servers; using Core.Services; using Core.Utils.Cloners; namespace Core.Helpers; [Injectable(InjectionType.Singleton)] public class HandbookHelper( DatabaseService _databaseService, ConfigServer _configServer, ICloner _cloner ) { protected ItemConfig _itemConfig = _configServer.GetConfig(); protected bool _lookupCacheGenerated = false; protected LookupCollection _handbookPriceCache = new(); /// /// Create an in-memory cache of all items with associated handbook price in handbookPriceCache class /// public void HydrateLookup() { var handbook = _databaseService.GetHandbook(); // Add handbook overrides found in items.json config into db foreach (var itemTplKey in _itemConfig.HandbookPriceOverride) { var data = _itemConfig.HandbookPriceOverride[itemTplKey.Key]; var itemToUpdate = handbook.Items.FirstOrDefault(item => item.Id == itemTplKey.Key); if (itemToUpdate is null) { handbook.Items.Add( new HandbookItem { Id = itemTplKey.Key, ParentId = data.ParentId, Price = data.Price, }); itemToUpdate = handbook.Items.FirstOrDefault(item => item.Id == itemTplKey.Key); } itemToUpdate.Price = data.Price; } var handbookDbClone = _cloner.Clone(handbook); foreach (var handbookItem in handbookDbClone.Items) { _handbookPriceCache.Items.ById.TryAdd(handbookItem.Id, handbookItem.Price ?? 0); if (!_handbookPriceCache.Items.ByParent.TryGetValue(handbookItem.ParentId, out var _)) { _handbookPriceCache.Items.ByParent.TryAdd(handbookItem.ParentId, []); } _handbookPriceCache.Items.ByParent.TryGetValue(handbookItem.ParentId, out var array); array.Add(handbookItem.Id); } foreach (var handbookCategory in handbookDbClone.Categories) { _handbookPriceCache.Categories.ById.TryAdd(handbookCategory.Id, handbookCategory.ParentId); if (handbookCategory.ParentId is not null) { if (!_handbookPriceCache.Categories.ByParent.TryGetValue(handbookCategory.ParentId, out var _)) { _handbookPriceCache.Categories.ByParent.TryAdd(handbookCategory.ParentId, []); } _handbookPriceCache.Categories.ByParent.TryGetValue(handbookCategory.ParentId, out var array); array.Add(handbookCategory.Id); } } } /// /// 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(string tpl) { if (!_lookupCacheGenerated) { HydrateLookup(); _lookupCacheGenerated = true; } if (_handbookPriceCache.Items.ById.TryGetValue(tpl, out var item)) { return item; } var handbookItem = _databaseService.GetHandbook().Items?.FirstOrDefault(item => item.Id == tpl); if (handbookItem is null) { var newValue = 0; if (!_handbookPriceCache.Items.ById.TryAdd(tpl, newValue)) { _handbookPriceCache.Items.ById[tpl] = newValue; } return newValue; } if (!_handbookPriceCache.Items.ById.TryAdd(tpl, handbookItem.Price ?? 0)) { _handbookPriceCache.Items.ById[tpl] = handbookItem.Price ?? 0; } return handbookItem.Price; } public double GetTemplatePriceForItems(List items) { var total = 0D; foreach (var item in items) { total += GetTemplatePrice(item.Template) ?? 0; } return total; } /// /// Get all items in template with the given parent category /// /// /// string array public List TemplatesWithParent(string parentId) { _handbookPriceCache.Items.ByParent.TryGetValue(parentId, out var template); return template ?? []; } /// /// Does category exist in handbook cache /// /// /// true if exists in cache public bool IsCategory(string category) { return _handbookPriceCache.Categories.ById.TryGetValue(category, out var _); } /// /// Get all items associated with a categories parent /// /// /// string array public List ChildrenCategories(string categoryParent) { _handbookPriceCache.Categories.ByParent.TryGetValue(categoryParent, out var category); return category ?? []; } /// /// Convert non-roubles into roubles /// /// Currency count to convert /// What current currency is /// Count in roubles public double InRUB(double nonRoubleCurrencyCount, string currencyTypeFrom) { return currencyTypeFrom == Money.ROUBLES ? nonRoubleCurrencyCount : Math.Round(nonRoubleCurrencyCount * (GetTemplatePrice(currencyTypeFrom) ?? 0)); } /// /// Convert roubles into another currency /// /// roubles to convert /// Currency to convert roubles into /// currency count in desired type public double FromRUB(double roubleCurrencyCount, string currencyTypeTo) { if (currencyTypeTo == Money.ROUBLES) { return roubleCurrencyCount; } // Get price of currency from handbook var price = GetTemplatePrice(currencyTypeTo); return price is not null ? Math.Max(1, Math.Round((double)(roubleCurrencyCount / price))) : 0; } public HandbookCategory GetCategoryById(string handbookId) { return _databaseService.GetHandbook().Categories.FirstOrDefault(category => category.Id == handbookId); } } public class LookupItem { public Dictionary ById { get; set; } public Dictionary> ByParent { get; set; } public LookupItem() { ById = new Dictionary(); ByParent = new Dictionary>(); } } public class LookupCollection { public LookupItem Items { get; set; } public LookupItem Categories { get; set; } public LookupCollection() { Items = new LookupItem(); Categories = new LookupItem(); } }