using System.Diagnostics; using SPTarkov.Common.Extensions; using SPTarkov.DI.Annotations; using SPTarkov.Server.Core.Extensions; using SPTarkov.Server.Core.Helpers; using SPTarkov.Server.Core.Models.Common; using SPTarkov.Server.Core.Models.Eft.Common.Tables; using SPTarkov.Server.Core.Models.Eft.Ragfair; using SPTarkov.Server.Core.Models.Enums; using SPTarkov.Server.Core.Models.Spt.Config; using SPTarkov.Server.Core.Models.Spt.Ragfair; using SPTarkov.Server.Core.Models.Utils; using SPTarkov.Server.Core.Servers; using SPTarkov.Server.Core.Services; using SPTarkov.Server.Core.Utils; using SPTarkov.Server.Core.Utils.Cloners; using LogLevel = SPTarkov.Server.Core.Models.Spt.Logging.LogLevel; namespace SPTarkov.Server.Core.Generators; [Injectable] public class RagfairOfferGenerator( ISptLogger logger, HashUtil hashUtil, RandomUtil randomUtil, TimeUtil timeUtil, DatabaseService databaseService, RagfairServerHelper ragfairServerHelper, ProfileHelper profileHelper, HandbookHelper handbookHelper, BotHelper botHelper, SaveServer saveServer, PresetHelper presetHelper, RagfairAssortGenerator ragfairAssortGenerator, RagfairOfferService ragfairOfferService, RagfairPriceService ragfairPriceService, ServerLocalisationService localisationService, PaymentHelper paymentHelper, ItemHelper itemHelper, ConfigServer configServer, ICloner cloner ) { protected List? AllowedFleaPriceItemsForBarter; protected readonly BotConfig BotConfig = configServer.GetConfig(); /// Internal counter to ensure each offer created has a unique value for its intId property protected int OfferCounter; protected readonly RagfairConfig RagfairConfig = configServer.GetConfig(); /// /// Create a flea offer and store it in the Ragfair server offers array /// /// Data needed to create a flea offer /// RagfairOffer public RagfairOffer CreateAndAddFleaOffer(CreateFleaOfferDetails details) { // Create offer object var offer = CreateOffer(details); // Flag offer with creator type offer.CreatedBy = details.Creator; // Add offer into server storage ragfairOfferService.AddOffer(offer); return offer; } /// /// Create an offer object ready to send to ragfairOfferService.addOffer() /// /// Data needed to create a flea offer /// RagfairOffer protected RagfairOffer CreateOffer(CreateFleaOfferDetails details) { var offerRequirements = details.BarterScheme.Select(barter => { var offerRequirement = new OfferRequirement { TemplateId = barter.Template, Count = Math.Round(barter.Count.Value, 2), OnlyFunctional = barter.OnlyFunctional ?? false, }; // Dogtags define level and side if (barter.Level != null) { offerRequirement.Level = barter.Level; offerRequirement.Side = barter.Side; } return offerRequirement; }); var rootItem = details.Items.FirstOrDefault(); // Hydrate ammo boxes with cartridges + ensure only 1 item is present (ammo box) // On offer refresh don't re-add cartridges to ammo box that already has cartridges if (details.Items.Count == 1 && itemHelper.IsOfBaseclass(details.Items[0].Template, BaseClasses.AMMO_BOX)) { itemHelper.AddCartridgesToAmmoBox(details.Items, itemHelper.GetItem(rootItem.Template).Value); } var roubleListingPrice = Math.Round(ConvertOfferRequirementsIntoRoubles(offerRequirements)); var singleItemListingPrice = details.SellInOnePiece ? roubleListingPrice / details.Quantity : roubleListingPrice; var offer = new RagfairOffer { Id = new MongoId(), InternalId = OfferCounter, User = details.Creator == OfferCreator.Player ? CreatePlayerUserDataForFleaOffer(details.UserId) : CreateUserDataForFleaOffer(details.UserId, details.Creator == OfferCreator.Trader), Root = rootItem.Id, Items = details.Items, ItemsCost = Math.Round(handbookHelper.GetTemplatePrice(rootItem.Template)), // Handbook price Requirements = offerRequirements, RequirementsCost = Math.Round(singleItemListingPrice), SummaryCost = roubleListingPrice, StartTime = details.Time, EndTime = GetOfferEndTime(details.Creator, details.UserId, details.Time), LoyaltyLevel = details.LoyalLevel, SellInOnePiece = details.SellInOnePiece, Locked = false, Quantity = details.Quantity, }; OfferCounter++; return offer; } /// /// Create the user object stored inside each flea offer object /// /// User creating the offer /// Is the user creating the offer a trader /// RagfairOfferUser protected RagfairOfferUser CreateUserDataForFleaOffer(MongoId userId, bool isTrader) { // Trader offer if (isTrader) { return new RagfairOfferUser { Id = userId, MemberType = MemberCategory.Trader }; } // 'Fake' pmc offer return new RagfairOfferUser { Id = userId, MemberType = MemberCategory.Default, Nickname = botHelper.GetPmcNicknameOfMaxLength(BotConfig.BotNameLengthLimit), Rating = randomUtil.GetDouble(RagfairConfig.Dynamic.Rating.Min, RagfairConfig.Dynamic.Rating.Max), IsRatingGrowing = randomUtil.GetBool(), Avatar = null, Aid = hashUtil.GenerateAccountId(), }; } /// /// Create the user object stored inside each flea offer object /// /// Player id /// OfferUser object protected RagfairOfferUser CreatePlayerUserDataForFleaOffer(MongoId userId) { var playerProfile = profileHelper.GetPmcProfile(userId); return new RagfairOfferUser { Id = playerProfile.Id.Value, MemberType = playerProfile.Info.MemberCategory, SelectedMemberCategory = playerProfile.Info.SelectedMemberCategory, Nickname = playerProfile.Info.Nickname, Rating = playerProfile.RagfairInfo.Rating ?? 0, IsRatingGrowing = playerProfile.RagfairInfo.IsRatingGrowing, Avatar = null, Aid = playerProfile.Aid, }; } /// /// Calculate the offer price that's listed on the flea listing /// /// barter requirements for offer /// rouble cost of offer protected double ConvertOfferRequirementsIntoRoubles(IEnumerable offerRequirements) { return offerRequirements.Sum(requirement => paymentHelper.IsMoneyTpl(requirement.TemplateId) ? Math.Round(CalculateRoublePrice(requirement.Count.Value, requirement.TemplateId)) : ragfairPriceService.GetFleaPriceForItem(requirement.TemplateId) * requirement.Count.Value ); } /// /// Get avatar url from trader table in db /// /// Is user we're getting avatar for a trader /// Persons id to get avatar of /// Url of avatar as String protected string GetAvatarUrl(bool isTrader, MongoId userId) { if (isTrader) { return databaseService.GetTrader(userId).Base.Avatar; } return "/files/trader/avatar/unknown.jpg"; } /// /// Convert a count of currency into roubles /// /// Amount of currency to convert into roubles /// Type of currency (euro/dollar/rouble) /// Count of roubles protected double CalculateRoublePrice(double currencyCount, MongoId currencyType) { if (currencyType == Money.ROUBLES) { return currencyCount; } return handbookHelper.InRoubles(currencyCount, currencyType); } /// /// Get a flea trading rating for the passed in user /// /// User to get flea rating of /// Flea rating value protected double? GetRating(MongoId userId) { // Player offer if (profileHelper.IsPlayer(userId)) { return saveServer.GetProfile(userId).CharacterData?.PmcData?.RagfairInfo?.Rating; } // Trader offer if (ragfairServerHelper.IsTrader(userId)) { return 1; } // Generated pmc offer return randomUtil.GetDouble(RagfairConfig.Dynamic.Rating.Min, RagfairConfig.Dynamic.Rating.Max); } /// /// Get number of section until offer should expire /// /// /// ID of the offer owner /// Time the offer is posted in seconds /// Number of seconds until offer expires protected long GetOfferEndTime(OfferCreator creatorType, MongoId userId, long time) { if (creatorType == OfferCreator.Player) { // Player offer = current time + offerDurationTimeInHour; var offerDurationTimeHours = databaseService.GetGlobals().Configuration.RagFair.OfferDurationTimeInHour; return (long)(timeUtil.GetTimeStamp() + Math.Round(offerDurationTimeHours * TimeUtil.OneHourAsSeconds)); } if (creatorType == OfferCreator.Trader) { return (long)databaseService.GetTrader(userId).Base.NextResupply; } var randomSpread = randomUtil.GetDouble(RagfairConfig.Dynamic.EndTimeSeconds.Min, RagfairConfig.Dynamic.EndTimeSeconds.Max); // Fake-player offer return (long)Math.Round(time + randomSpread); } /// /// Create multiple offers for items by using a unique list of items we've generated previously /// /// Optional, expired offers to regenerate public void GenerateDynamicOffers(IEnumerable>? expiredOffers = null) { var replacingExpiredOffers = expiredOffers is not null && expiredOffers.Any(); var stopwatch = Stopwatch.StartNew(); // get assort items from param if they exist, otherwise grab freshly generated assorts var assortItemsToProcess = replacingExpiredOffers ? expiredOffers ?? [] : ragfairAssortGenerator.GenerateRagfairAssortItems(); stopwatch.Stop(); if (logger.IsLogEnabled(LogLevel.Debug) && stopwatch.ElapsedMilliseconds > 0) { logger.Debug($"Took {stopwatch.ElapsedMilliseconds}ms to GetRagfairAssorts - {assortItemsToProcess.Count()} items"); } stopwatch.Restart(); var tasks = new List(); foreach (var assortItemWithChildren in assortItemsToProcess) { tasks.Add( Task.Factory.StartNew(() => { CreateOffersFromAssort(assortItemWithChildren, replacingExpiredOffers, RagfairConfig.Dynamic); }) ); } Task.WaitAll(tasks.ToArray()); stopwatch.Stop(); if (logger.IsLogEnabled(LogLevel.Debug)) { logger.Debug($"Took {stopwatch.ElapsedMilliseconds}ms to CreateOffersFromAssort"); } } /// /// Generates offers from an item and it's children on the flea market /// /// Item with its children to process into offers /// Is an expired offer /// Ragfair dynamic config protected void CreateOffersFromAssort(List assortItemWithChildren, bool isExpiredOffer, Dynamic config) { var rootItem = assortItemWithChildren.FirstOrDefault(); var itemToSellDetails = itemHelper.GetItem(rootItem.Template); // Only perform checks on newly generated items, skip expired items being refreshed if (!(isExpiredOffer || ragfairServerHelper.IsItemValidRagfairItem(itemToSellDetails))) { return; } // Armor presets can hold plates above the allowed flea level, remove if necessary var isPreset = rootItem?.Upd?.SptPresetId is not null && presetHelper.IsPreset(rootItem.Upd.SptPresetId.Value); if (!isExpiredOffer && isPreset && RagfairConfig.Dynamic.Blacklist.EnableBsgList) { RemoveBannedPlatesFromPreset(assortItemWithChildren, RagfairConfig.Dynamic.Blacklist.ArmorPlate); } // Get number of offers to create // Limit to 1 offer when processing expired - like-for-like replacement var offerCount = isExpiredOffer ? 1 : ragfairServerHelper.GetOfferCountByBaseType(itemToSellDetails.Value.Parent); for (var index = 0; index < offerCount; index++) { // Clone the item so we don't have shared references and generate new item IDs var clonedAssort = cloner.Clone(assortItemWithChildren); itemHelper.ReparentItemAndChildren(clonedAssort[0], clonedAssort); // Clear unnecessary properties clonedAssort[0].ParentId = null; clonedAssort[0].SlotId = null; CreateSingleOfferForItem( new MongoId(), clonedAssort, isPreset, itemToSellDetails.Value, isExpiredOffer, OfferCreator.FakePlayer ); } } /// /// Iterate over an items children and look for plates above desired level and remove them /// /// Preset to check for plates /// Settings /// True if plates removed protected bool RemoveBannedPlatesFromPreset(List presetWithChildren, ArmorPlateBlacklistSettings plateSettings) { if (!itemHelper.ArmorItemCanHoldMods(presetWithChildren[0].Template)) // Cant hold armor inserts, skip { return false; } var plateSlots = presetWithChildren .Where(item => itemHelper.GetRemovablePlateSlotIds().Contains(item.SlotId?.ToLowerInvariant())) .ToList(); if (plateSlots.Count == 0) // Has no plate slots e.g. "front_plate", exit { return false; } var removedPlate = false; foreach (var plateSlot in plateSlots) { var plateDetails = itemHelper.GetItem(plateSlot.Template).Value; if (plateSettings.IgnoreSlots.Contains(plateSlot.SlotId.ToLowerInvariant())) { continue; } var plateArmorLevel = plateDetails.Properties.ArmorClass ?? 0; if (plateArmorLevel > plateSettings.MaxProtectionLevel) { presetWithChildren.Splice(presetWithChildren.IndexOf(plateSlot), 1); removedPlate = true; } } return removedPlate; } /// /// Create one flea offer for a specific item /// /// ID of seller /// Item to create offer for /// Is item a weapon preset /// Raw DB item details /// Offer being created is to replace an expired, existing offer /// What type of entity created this offer protected void CreateSingleOfferForItem( MongoId sellerId, List itemWithChildren, bool isPreset, TemplateItem itemToSellDetails, bool isExpiredOffer, OfferCreator offerCreator ) { var rootItem = itemWithChildren.FirstOrDefault(); // Get randomised amount to list on flea var desiredStackSize = ragfairServerHelper.CalculateDynamicStackCount(rootItem.Template, isPreset); // Reset stack count to 1 from whatever it was prior rootItem.Upd.StackObjectsCount = 1; if (!isExpiredOffer && itemHelper.ArmorItemCanHoldMods(rootItem.Template)) { // Run randomised chance to remove removable plates from new offers(not expired) RemoveArmorPlates(itemWithChildren, rootItem); } var isBarterOffer = randomUtil.GetChance100(RagfairConfig.Dynamic.Barter.ChancePercent); var isPackOffer = !isBarterOffer && randomUtil.GetChance100(RagfairConfig.Dynamic.Pack.ChancePercent) && itemWithChildren.Count == 1 && itemHelper.IsOfBaseclasses(rootItem.Template, RagfairConfig.Dynamic.Pack.ItemTypeWhitelist); List barterScheme; if (isPackOffer) { // Set pack size desiredStackSize = randomUtil.GetInt(RagfairConfig.Dynamic.Pack.ItemCountMin, RagfairConfig.Dynamic.Pack.ItemCountMax); // Don't randomise pack items barterScheme = CreateCurrencyBarterScheme(itemWithChildren, isPackOffer, desiredStackSize); } else if (isBarterOffer) { // Apply randomised properties RandomiseOfferItemUpdProperties(sellerId, itemWithChildren, itemToSellDetails, offerCreator); barterScheme = CreateBarterBarterScheme(itemWithChildren, RagfairConfig.Dynamic.Barter); if (RagfairConfig.Dynamic.Barter.MakeSingleStackOnly) { var rootBarterItem = itemWithChildren.FirstOrDefault(); if (rootBarterItem?.Upd != null) { rootBarterItem.Upd.StackObjectsCount = 1; } } } else { // Not barter or pack offer // Apply randomised properties RandomiseOfferItemUpdProperties(sellerId, itemWithChildren, itemToSellDetails, offerCreator); barterScheme = CreateCurrencyBarterScheme(itemWithChildren, isPackOffer); } var createOfferDetails = new CreateFleaOfferDetails { UserId = sellerId, Time = timeUtil.GetTimeStamp(), Items = itemWithChildren, BarterScheme = barterScheme, LoyalLevel = 1, Quantity = desiredStackSize, Creator = offerCreator, SellInOnePiece = isPackOffer, // sellAsOnePiece - pack offer }; CreateAndAddFleaOffer(createOfferDetails); } /// /// Run % check to remove removable armor plates from item /// /// Armor item /// Root armor item protected void RemoveArmorPlates(List itemWithChildren, Item rootItem) { var armorConfig = RagfairConfig.Dynamic.Armor; var shouldRemovePlates = randomUtil.GetChance100(armorConfig.RemoveRemovablePlateChance); if (!shouldRemovePlates || !itemHelper.ArmorItemHasRemovablePlateSlots(rootItem.Template)) { return; } var offerItemPlatesToRemove = itemWithChildren.Where(item => armorConfig.PlateSlotIdToRemovePool.Contains(item.SlotId?.ToLowerInvariant()) ); // Latest first, to ensure we don't move later items off by 1 each time we remove an item below it var indexesToRemove = offerItemPlatesToRemove.Select(plateItem => itemWithChildren.IndexOf(plateItem)).ToHashSet(); foreach (var index in indexesToRemove.OrderByDescending(x => x)) { itemWithChildren.RemoveAt(index); } } /// /// Generate trader offers on flea using the traders assort data /// /// Trader to generate offers for public void GenerateFleaOffersForTrader(MongoId traderId) { // Purge ragfairOfferService.RemoveAllOffersByTrader(traderId); var time = timeUtil.GetTimeStamp(); var trader = databaseService.GetTrader(traderId); var assortsClone = cloner.Clone(trader.Assort); // Trader assorts / assort items are missing if (assortsClone?.Items?.Count is null or 0) { logger.Error(localisationService.GetText("ragfair-no_trader_assorts_cant_generate_flea_offers", trader.Base.Nickname)); return; } var blacklist = RagfairConfig.Dynamic.Blacklist; var childAssortItems = assortsClone.Items.Where(x => !string.Equals(x.ParentId, "hideout", StringComparison.Ordinal)).ToList(); foreach (var item in assortsClone.Items) { // We only want to process 'base/root' items, no children if (item.SlotId != "hideout") // skip mod items { continue; } // Run blacklist check on trader offers if (blacklist.TraderItems) { var itemDetails = itemHelper.GetItem(item.Template); if (!itemDetails.Key) { logger.Warning(localisationService.GetText("ragfair-tpl_not_a_valid_item", item.Template)); continue; } // Don't include items that BSG has blacklisted from flea if (blacklist.EnableBsgList && !(itemDetails.Value?.Properties?.CanSellOnRagfair ?? false)) { continue; } } var isPreset = presetHelper.IsPreset(item.Id); var items = isPreset ? ragfairServerHelper.GetPresetItems(item) : [item, .. itemHelper.FindAndReturnChildrenByAssort(item.Id, childAssortItems)]; if (!assortsClone.BarterScheme.TryGetValue(item.Id, out var barterScheme)) { logger.Warning( localisationService.GetText( "ragfair-missing_barter_scheme", new { itemId = item.Id, tpl = item.Template, name = trader.Base.Nickname, } ) ); continue; } var barterSchemeItems = barterScheme[0]; var loyalLevel = assortsClone.LoyalLevelItems[item.Id]; var createOfferDetails = new CreateFleaOfferDetails { UserId = traderId, Time = time, Items = items, BarterScheme = barterSchemeItems, LoyalLevel = loyalLevel, Quantity = (int?)item.Upd?.StackObjectsCount ?? 1, Creator = OfferCreator.Trader, }; CreateAndAddFleaOffer(createOfferDetails); // Refresh complete, reset flag to false trader.Base.RefreshTraderRagfairOffers = false; } } /// /// Get array of an item with its mods + condition properties (e.g. durability)
/// Apply randomisation adjustments to condition if item base is found in ragfair.json/dynamic/condition ///
/// ID of owner of item /// Item and mods, get condition of first item (only first array item is modified) /// DB details of first item /// protected void RandomiseOfferItemUpdProperties( MongoId userId, IEnumerable itemWithMods, TemplateItem itemDetails, OfferCreator offerCreator ) { // Add any missing properties to first item in array AddMissingConditions(itemWithMods.First()); if (offerCreator is OfferCreator.FakePlayer) { var parentId = GetDynamicConditionIdForTpl(itemDetails.Id); if (parentId == null) // No condition details found, don't proceed with modifying item conditions { return; } // Roll random chance to randomise item condition if (randomUtil.GetChance100(RagfairConfig.Dynamic.Condition[parentId.Value].ConditionChance * 100)) { RandomiseItemCondition(parentId.Value, itemWithMods, itemDetails); } } } /// /// Get the relevant condition id if item tpl matches in ragfair.json/condition /// /// Item to look for matching condition object /// Condition ID protected MongoId? GetDynamicConditionIdForTpl(MongoId tpl) { // Get keys from condition config dictionary var configConditions = RagfairConfig.Dynamic.Condition.Keys; foreach (var baseClass in configConditions) { if (itemHelper.IsOfBaseclass(tpl, baseClass)) { return baseClass; } } return null; } /// /// Alter an items condition based on its item base type /// /// Also the parentID of item being altered /// Item to adjust condition details of /// DB Item details of first item in list protected void RandomiseItemCondition(MongoId conditionSettingsId, IEnumerable itemWithMods, TemplateItem itemDetails) { var rootItem = itemWithMods.First(); var itemConditionValues = RagfairConfig.Dynamic.Condition[conditionSettingsId]; var maxMultiplier = randomUtil.GetDouble(itemConditionValues.Max.Min, itemConditionValues.Max.Min); var currentMultiplier = randomUtil.GetDouble(itemConditionValues.Current.Min, itemConditionValues.Current.Max); // Randomise armor + plates + armor related things if ( itemHelper.ArmorItemCanHoldMods(rootItem.Template) || itemHelper.IsOfBaseclasses(rootItem.Template, [BaseClasses.ARMOR_PLATE, BaseClasses.ARMORED_EQUIPMENT]) ) { RandomiseArmorDurabilityValues(itemWithMods, currentMultiplier, maxMultiplier); // Add hits to visor var visorMod = itemWithMods.FirstOrDefault(item => item.ParentId == BaseClasses.ARMORED_EQUIPMENT.ToString() && item.SlotId == "mod_equipment_000" ); if (visorMod != null && randomUtil.GetChance100(25)) { visorMod.AddUpd(); visorMod.Upd.FaceShield = new UpdFaceShield { Hits = randomUtil.GetInt(1, 3) }; } return; } // Randomise Weapons if (itemHelper.IsOfBaseclass(itemDetails.Id, BaseClasses.WEAPON)) { RandomiseWeaponDurability(itemWithMods.First(), itemDetails, maxMultiplier, currentMultiplier); return; } if (rootItem.Upd?.MedKit != null) { // Randomize health var hpResource = Math.Round((double)rootItem.Upd.MedKit.HpResource * maxMultiplier); rootItem.Upd.MedKit.HpResource = hpResource == 0D ? 1D : hpResource; return; } if (rootItem.Upd?.Key != null && itemDetails.Properties.MaximumNumberOfUsage > 1) { // Randomize key uses rootItem.Upd.Key.NumberOfUsages = (int?)Math.Round(itemDetails.Properties.MaximumNumberOfUsage.Value * (1 - maxMultiplier)); return; } if (rootItem.Upd?.FoodDrink != null) { // randomize food/drink value var hpPercent = Math.Round((double)itemDetails.Properties.MaxResource * maxMultiplier); rootItem.Upd.FoodDrink.HpPercent = hpPercent == 0D ? 1D : hpPercent; return; } if (rootItem.Upd?.RepairKit != null) { // randomize repair kit (armor/weapon) uses var resource = Math.Round((double)itemDetails.Properties.MaxRepairResource * maxMultiplier); rootItem.Upd.RepairKit.Resource = resource == 0D ? 1D : resource; return; } if (itemHelper.IsOfBaseclass(itemDetails.Id, BaseClasses.FUEL)) { var totalCapacity = itemDetails.Properties.MaxResource; var remainingFuel = Math.Round((double)totalCapacity * maxMultiplier); rootItem.Upd.Resource = new UpdResource { UnitsConsumed = totalCapacity - remainingFuel, Value = remainingFuel }; } } /// /// Adjust an items durability/maxDurability value /// /// Item (weapon/armor) to adjust /// Item details from DB /// Value to multiply max durability by /// Value to multiply current durability by protected void RandomiseWeaponDurability(Item item, TemplateItem itemDbDetails, double maxMultiplier, double currentMultiplier) { // Max var baseMaxDurability = itemDbDetails.Properties.MaxDurability; var lowestMaxDurability = randomUtil.GetDouble(maxMultiplier, 1) * baseMaxDurability; var chosenMaxDurability = Math.Round(randomUtil.GetDouble((double)lowestMaxDurability, (double)baseMaxDurability)); // Current var lowestCurrentDurability = randomUtil.GetDouble(currentMultiplier, 1) * chosenMaxDurability; var chosenCurrentDurability = Math.Round(randomUtil.GetDouble(lowestCurrentDurability, chosenMaxDurability)); item.Upd.Repairable.Durability = chosenCurrentDurability == 0 ? 1D : chosenCurrentDurability; // Never var value become 0 item.Upd.Repairable.MaxDurability = chosenMaxDurability; } /// /// Randomise the durability values for an armors plates and soft inserts /// /// Armor item with its child mods /// Chosen multiplier to use for current durability value /// Chosen multiplier to use for max durability value protected void RandomiseArmorDurabilityValues(IEnumerable armorWithMods, double currentMultiplier, double maxMultiplier) { foreach (var armorItem in armorWithMods) { var itemDbDetails = itemHelper.GetItem(armorItem.Template).Value; if (itemDbDetails.Properties.ArmorClass > 1) { armorItem.AddUpd(); var baseMaxDurability = itemDbDetails.Properties.MaxDurability; var lowestMaxDurability = randomUtil.GetDouble(maxMultiplier, 1) * baseMaxDurability; var chosenMaxDurability = Math.Round(randomUtil.GetDouble((double)lowestMaxDurability, (double)baseMaxDurability)); var lowestCurrentDurability = randomUtil.GetDouble(currentMultiplier, 1) * chosenMaxDurability; var chosenCurrentDurability = Math.Round(randomUtil.GetDouble(lowestCurrentDurability, chosenMaxDurability)); armorItem.Upd.Repairable = new UpdRepairable { Durability = chosenCurrentDurability == 0D ? 1D : chosenCurrentDurability, // Never var value become 0 MaxDurability = chosenMaxDurability, }; } } } /// /// Add missing conditions to an item if needed.
/// Durability for repairable items.
/// HpResource for medical items. ///
/// Item to add conditions to protected void AddMissingConditions(Item item) { var props = itemHelper.GetItem(item.Template).Value.Properties; var isRepairable = props.Durability != null; var isMedkit = props.MaxHpResource != null; var isKey = props.MaximumNumberOfUsage != null; var isConsumable = props.MaxResource > 1 && props.FoodUseTime != null; var isRepairKit = props.MaxRepairResource != null; if (isRepairable && props.Durability > 0) { item.Upd.Repairable = new UpdRepairable { Durability = props.Durability, MaxDurability = props.Durability }; return; } if (isMedkit && props.MaxHpResource > 0) { item.Upd.MedKit = new UpdMedKit { HpResource = props.MaxHpResource }; return; } if (isKey) { item.Upd.Key = new UpdKey { NumberOfUsages = 0 }; return; } // Food/drink if (isConsumable) { item.Upd.FoodDrink = new UpdFoodDrink { HpPercent = props.MaxResource }; return; } if (isRepairKit) { item.Upd.RepairKit = new UpdRepairKit { Resource = props.MaxRepairResource }; } } /// /// Create a barter-based barter scheme, if not possible, fall back to making barter scheme currency based /// /// Items for sale in offer /// Barter config from ragfairConfig.Dynamic.barter /// Barter scheme protected List CreateBarterBarterScheme(IEnumerable offerItems, BarterDetails barterConfig) { // Get flea price of item being sold var priceOfOfferItem = ragfairPriceService.GetDynamicOfferPriceForOffer(offerItems, Money.ROUBLES, false); // Don't make items under a designated rouble value into barter offers if (priceOfOfferItem < barterConfig.MinRoubleCostToBecomeBarter) { return CreateCurrencyBarterScheme(offerItems, false); } // Get a randomised number of barter items to list offer for var barterItemCount = randomUtil.GetInt(barterConfig.ItemCountMin, barterConfig.ItemCountMax); // Get desired cost of individual item offer will be listed for e.g. offer = 15k, item count = 3, desired item cost = 5k var desiredItemCostRouble = Math.Round(priceOfOfferItem / barterItemCount); // Rouble amount to go above/below when looking for an item (Wiggle cost of item a little) var offerCostVarianceRoubles = desiredItemCostRouble * barterConfig.PriceRangeVariancePercent / 100; // Dict of items and their flea price (cached on first use) var itemFleaPrices = GetFleaPricesAsArray(); // Filter possible barters to items that match the price range + not itself var min = desiredItemCostRouble - offerCostVarianceRoubles; var max = desiredItemCostRouble + offerCostVarianceRoubles; var rootOfferItem = offerItems.FirstOrDefault(); var itemsInsidePriceBounds = itemFleaPrices.Where(itemAndPrice => itemAndPrice.Price >= min && itemAndPrice.Price <= max && itemAndPrice.Tpl != rootOfferItem.Template // Don't allow the item being sold to be chosen ); // No items on flea have a matching price, fall back to currency if (!itemsInsidePriceBounds.Any()) { return CreateCurrencyBarterScheme(offerItems, false); } // Choose random item from price-filtered flea items var randomItem = randomUtil.GetArrayValue(itemsInsidePriceBounds); return [new BarterScheme { Count = barterItemCount, Template = randomItem.Tpl }]; } /// /// Get an array of flea prices + item tpl, cached in generator class inside `allowedFleaPriceItemsForBarter` /// /// List with tpl/price values protected List GetFleaPricesAsArray() { // Generate if needed if (AllowedFleaPriceItemsForBarter == null) { var fleaPrices = databaseService.GetPrices(); // Only get prices for items that also exist in items.json var filteredFleaItems = fleaPrices .Select(kvTpl => new TplWithFleaPrice { Tpl = kvTpl.Key, Price = kvTpl.Value }) .Where(item => itemHelper.GetItem(item.Tpl).Key); var itemTypeBlacklist = RagfairConfig.Dynamic.Barter.ItemTypeBlacklist; AllowedFleaPriceItemsForBarter = filteredFleaItems .Where(item => !itemHelper.IsOfBaseclasses(item.Tpl, itemTypeBlacklist)) .ToList(); } return AllowedFleaPriceItemsForBarter; } /// /// Create a random currency-based barter scheme for an array of items /// /// Items on offer /// Is the barter scheme being created for a pack offer /// What to multiply the resulting price by /// Barter scheme for offer protected List CreateCurrencyBarterScheme(IEnumerable offerWithChildren, bool isPackOffer, double multiplier = 1) { var currency = ragfairServerHelper.GetDynamicOfferCurrency(); var price = ragfairPriceService.GetDynamicOfferPriceForOffer(offerWithChildren, currency, isPackOffer) * multiplier; return [new BarterScheme { Count = price, Template = currency }]; } }