e54dcbd4d1
* Fix more warnings... * Fix mistake
947 lines
38 KiB
C#
947 lines
38 KiB
C#
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<RagfairOfferGenerator> 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<TplWithFleaPrice>? allowedFleaPriceItemsForBarter;
|
|
protected readonly BotConfig botConfig = configServer.GetConfig<BotConfig>();
|
|
|
|
/// Internal counter to ensure each offer created has a unique value for its intId property
|
|
protected int offerCounter;
|
|
|
|
protected readonly RagfairConfig ragfairConfig = configServer.GetConfig<RagfairConfig>();
|
|
|
|
/// <summary>
|
|
/// Create a flea offer and store it in the Ragfair server offers array
|
|
/// </summary>
|
|
/// <param name="details">Data needed to create a flea offer</param>
|
|
/// <returns>RagfairOffer</returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create an offer object ready to send to ragfairOfferService.addOffer()
|
|
/// </summary>
|
|
/// <param name="details">Data needed to create a flea offer</param>
|
|
/// <returns>RagfairOffer</returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create the user object stored inside each flea offer object
|
|
/// </summary>
|
|
/// <param name="userId">User creating the offer</param>
|
|
/// <param name="isTrader">Is the user creating the offer a trader</param>
|
|
/// <returns>RagfairOfferUser</returns>
|
|
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(),
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create the user object stored inside each flea offer object
|
|
/// </summary>
|
|
/// <param name="userId">Player id</param>
|
|
/// <returns>OfferUser object</returns>
|
|
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,
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculate the offer price that's listed on the flea listing
|
|
/// </summary>
|
|
/// <param name="offerRequirements"> barter requirements for offer </param>
|
|
/// <returns> rouble cost of offer </returns>
|
|
protected double ConvertOfferRequirementsIntoRoubles(IEnumerable<OfferRequirement> offerRequirements)
|
|
{
|
|
var roublePrice = 0d;
|
|
foreach (var requirement in offerRequirements)
|
|
{
|
|
roublePrice += paymentHelper.IsMoneyTpl(requirement.TemplateId)
|
|
? Math.Round(CalculateRoublePrice(requirement.Count.Value, requirement.TemplateId))
|
|
: ragfairPriceService.GetFleaPriceForItem(requirement.TemplateId) * requirement.Count.Value; // Get flea price for barter offer items
|
|
}
|
|
|
|
return roublePrice;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get avatar url from trader table in db
|
|
/// </summary>
|
|
/// <param name="isTrader"> Is user we're getting avatar for a trader </param>
|
|
/// <param name="userId"> Persons id to get avatar of </param>
|
|
/// <returns> Url of avatar as String </returns>
|
|
protected string GetAvatarUrl(bool isTrader, MongoId userId)
|
|
{
|
|
if (isTrader)
|
|
{
|
|
return databaseService.GetTrader(userId).Base.Avatar;
|
|
}
|
|
|
|
return "/files/trader/avatar/unknown.jpg";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Convert a count of currency into roubles
|
|
/// </summary>
|
|
/// <param name="currencyCount"> Amount of currency to convert into roubles </param>
|
|
/// <param name="currencyType"> Type of currency (euro/dollar/rouble) </param>
|
|
/// <returns> Count of roubles </returns>
|
|
protected double CalculateRoublePrice(double currencyCount, MongoId currencyType)
|
|
{
|
|
if (currencyType == Money.ROUBLES)
|
|
{
|
|
return currencyCount;
|
|
}
|
|
|
|
return handbookHelper.InRoubles(currencyCount, currencyType);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get a flea trading rating for the passed in user
|
|
/// </summary>
|
|
/// <param name="userId"> User to get flea rating of </param>
|
|
/// <returns> Flea rating value </returns>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get number of section until offer should expire
|
|
/// </summary>
|
|
/// <param name="creatorType"></param>
|
|
/// <param name="userID"> ID of the offer owner </param>
|
|
/// <param name="time"> Time the offer is posted in seconds </param>
|
|
/// <returns> Number of seconds until offer expires </returns>
|
|
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((double)offerDurationTimeHours * TimeUtil.OneHourAsSeconds));
|
|
}
|
|
|
|
if (creatorType == OfferCreator.Trader)
|
|
// Trader offer
|
|
{
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create multiple offers for items by using a unique list of items we've generated previously
|
|
/// </summary>
|
|
/// <param name="expiredOffers"> Optional, expired offers to regenerate </param>
|
|
public void GenerateDynamicOffers(IEnumerable<List<Item>>? 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<Task>();
|
|
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");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generates offers from an item and it's children on the flea market
|
|
/// </summary>
|
|
/// <param name="assortItemWithChildren"> Item with its children to process into offers </param>
|
|
/// <param name="isExpiredOffer"> Is an expired offer </param>
|
|
/// <param name="config"> Ragfair dynamic config </param>
|
|
protected void CreateOffersFromAssort(List<Item> 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
|
|
);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Iterate over an items children and look for plates above desired level and remove them
|
|
/// </summary>
|
|
/// <param name="presetWithChildren"> Preset to check for plates </param>
|
|
/// <param name="plateSettings"> Settings </param>
|
|
/// <returns> True if plates removed </returns>
|
|
protected bool RemoveBannedPlatesFromPreset(List<Item> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create one flea offer for a specific item
|
|
/// </summary>
|
|
/// <param name="sellerId"> ID of seller</param>
|
|
/// <param name="itemWithChildren"> Item to create offer for</param>
|
|
/// <param name="isPreset"> Is item a weapon preset</param>
|
|
/// <param name="itemToSellDetails"> Raw DB item details </param>
|
|
/// <param name="isExpiredOffer">Offer being created is to replace an expired, existing offer</param>
|
|
/// <param name="offerCreator">What type of entity created this offer</param>
|
|
protected void CreateSingleOfferForItem(
|
|
MongoId sellerId,
|
|
List<Item> 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> 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Run % check to remove removable armor plates from item
|
|
/// </summary>
|
|
/// <param name="itemWithChildren">Armor item</param>
|
|
/// <param name="rootItem">Root armor item</param>
|
|
protected void RemoveArmorPlates(List<Item> 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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generate trader offers on flea using the traders assort data
|
|
/// </summary>
|
|
/// <param name="traderId"> Trader to generate offers for </param>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get array of an item with its mods + condition properties (e.g. durability) <br />
|
|
/// Apply randomisation adjustments to condition if item base is found in ragfair.json/dynamic/condition
|
|
/// </summary>
|
|
/// <param name="userID"> ID of owner of item </param>
|
|
/// <param name="itemWithMods"> Item and mods, get condition of first item (only first array item is modified) </param>
|
|
/// <param name="itemDetails"> DB details of first item</param>
|
|
/// <param name="offerCreator"></param>
|
|
protected void RandomiseOfferItemUpdProperties(
|
|
MongoId userID,
|
|
IEnumerable<Item> 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);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the relevant condition id if item tpl matches in ragfair.json/condition
|
|
/// </summary>
|
|
/// <param name="tpl"> Item to look for matching condition object</param>
|
|
/// <returns> Condition ID </returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Alter an items condition based on its item base type
|
|
/// </summary>
|
|
/// <param name="conditionSettingsId"> Also the parentID of item being altered </param>
|
|
/// <param name="itemWithMods"> Item to adjust condition details of </param>
|
|
/// <param name="itemDetails"> DB Item details of first item in list </param>
|
|
protected void RandomiseItemCondition(MongoId conditionSettingsId, IEnumerable<Item> 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 (randomUtil.GetChance100(25) && visorMod != null)
|
|
{
|
|
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 };
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adjust an items durability/maxDurability value
|
|
/// </summary>
|
|
/// <param name="item"> Item (weapon/armor) to adjust </param>
|
|
/// <param name="itemDbDetails"> Item details from DB </param>
|
|
/// <param name="maxMultiplier"> Value to multiply max durability by </param>
|
|
/// <param name="currentMultiplier"> Value to multiply current durability by </param>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Randomise the durability values for an armors plates and soft inserts
|
|
/// </summary>
|
|
/// <param name="armorWithMods"> Armor item with its child mods </param>
|
|
/// <param name="currentMultiplier"> Chosen multiplier to use for current durability value </param>
|
|
/// <param name="maxMultiplier"> Chosen multiplier to use for max durability value </param>
|
|
protected void RandomiseArmorDurabilityValues(IEnumerable<Item> 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,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Add missing conditions to an item if needed. <br />
|
|
/// Durabiltiy for repairable items. <br />
|
|
/// HpResource for medical items.
|
|
/// </summary>
|
|
/// <param name="item"> Item to add conditions to </param>
|
|
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 };
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a barter-based barter scheme, if not possible, fall back to making barter scheme currency based
|
|
/// </summary>
|
|
/// <param name="offerItems"> Items for sale in offer </param>
|
|
/// <param name="barterConfig"> Barter config from ragfairConfig.Dynamic.barter </param>
|
|
/// <returns> Barter scheme </returns>
|
|
protected List<BarterScheme> CreateBarterBarterScheme(IEnumerable<Item> 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 }];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get an array of flea prices + item tpl, cached in generator class inside `allowedFleaPriceItemsForBarter`
|
|
/// </summary>
|
|
/// <returns> List with tpl/price values </returns>
|
|
protected List<TplWithFleaPrice> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a random currency-based barter scheme for an array of items
|
|
/// </summary>
|
|
/// <param name="offerWithChildren"> Items on offer </param>
|
|
/// <param name="isPackOffer"> Is the barter scheme being created for a pack offer </param>
|
|
/// <param name="multiplier"> What to multiply the resulting price by </param>
|
|
/// <returns> Barter scheme for offer </returns>
|
|
protected List<BarterScheme> CreateCurrencyBarterScheme(IEnumerable<Item> 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 }];
|
|
}
|
|
}
|