Files
SPT-Server-Build/Libraries/SPTarkov.Server.Core/Helpers/RagfairOfferHelper.cs
T
2025-07-05 12:35:03 +01:00

1147 lines
39 KiB
C#

using SPTarkov.Common.Extensions;
using SPTarkov.DI.Annotations;
using SPTarkov.Server.Core.Extensions;
using SPTarkov.Server.Core.Models.Common;
using SPTarkov.Server.Core.Models.Eft.Common;
using SPTarkov.Server.Core.Models.Eft.Common.Tables;
using SPTarkov.Server.Core.Models.Eft.ItemEvent;
using SPTarkov.Server.Core.Models.Eft.Profile;
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.Utils;
using SPTarkov.Server.Core.Routers;
using SPTarkov.Server.Core.Servers;
using SPTarkov.Server.Core.Services;
using SPTarkov.Server.Core.Utils;
namespace SPTarkov.Server.Core.Helpers;
[Injectable]
public class RagfairOfferHelper(
ISptLogger<RagfairOfferHelper> _logger,
TimeUtil _timeUtil,
BotHelper _botHelper,
RagfairSortHelper _ragfairSortHelper,
PresetHelper _presetHelper,
RagfairHelper _ragfairHelper,
PaymentHelper _paymentHelper,
TraderHelper _traderHelper,
QuestHelper _questHelper,
RagfairServerHelper _ragfairServerHelper,
ItemHelper _itemHelper,
DatabaseService _databaseService,
RagfairOfferService _ragfairOfferService,
LocaleService _localeService,
ServerLocalisationService _serverLocalisationService,
MailSendService _mailSendService,
RagfairRequiredItemsService _ragfairRequiredItemsService,
ProfileHelper _profileHelper,
EventOutputHolder _eventOutputHolder,
ConfigServer _configServer
)
{
protected const string _goodSoldTemplate = "5bdabfb886f7743e152e867e 0"; // Your {soldItem} {itemCount} items were bought by {buyerNickname}.
protected readonly BotConfig _botConfig = _configServer.GetConfig<BotConfig>();
protected readonly RagfairConfig _ragfairConfig = _configServer.GetConfig<RagfairConfig>();
/// <summary>
/// Pass through to ragfairOfferService.getOffers(), get flea offers a player should see
/// </summary>
/// <param name="searchRequest">Data from client</param>
/// <param name="itemsToAdd">ragfairHelper.filterCategories()</param>
/// <param name="traderAssorts">Trader assorts</param>
/// <param name="pmcData">Player profile</param>
/// <returns>Offers the player should see</returns>
public List<RagfairOffer> GetValidOffers(
SearchRequestData searchRequest,
List<string> itemsToAdd,
Dictionary<string, TraderAssort> traderAssorts,
PmcData pmcData
)
{
var playerIsFleaBanned = pmcData.PlayerIsFleaBanned(_timeUtil.GetTimeStamp());
var tieredFlea = _ragfairConfig.TieredFlea;
var tieredFleaLimitTypes = tieredFlea.UnlocksType;
return _ragfairOfferService
.GetOffers()
.Where(offer =>
{
if (!PassesSearchFilterCriteria(searchRequest, offer, pmcData))
{
return false;
}
var isDisplayable = IsDisplayableOffer(
searchRequest,
itemsToAdd,
traderAssorts,
offer,
pmcData,
playerIsFleaBanned
);
if (!isDisplayable)
{
return false;
}
// Not trader offer + tiered flea enabled
if (tieredFlea.Enabled && !OfferIsFromTrader(offer))
{
CheckAndLockOfferFromPlayerTieredFlea(
tieredFlea,
offer,
tieredFleaLimitTypes.Keys.ToList(),
pmcData.Info.Level.Value
);
}
return true;
})
.ToList();
}
/// <summary>
/// Disable offer if item is flagged by tiered flea config based on player level
/// </summary>
/// <param name="tieredFlea">Tiered flea settings from ragfair config</param>
/// <param name="offer">Ragfair offer to evaluate</param>
/// <param name="tieredFleaLimitTypes">List of item types flagged with a required player level</param>
/// <param name="playerLevel">Current level of player viewing offer</param>
protected void CheckAndLockOfferFromPlayerTieredFlea(
TieredFlea tieredFlea,
RagfairOffer offer,
List<MongoId> tieredFleaLimitTypes,
int playerLevel
)
{
var offerItemTpl = offer.Items.FirstOrDefault().Template;
// Check if offer item is ammo
if (
tieredFlea.AmmoTplUnlocks is not null
&& _itemHelper.IsOfBaseclass(offerItemTpl, BaseClasses.AMMO)
)
{
// Check if ammo is flagged with a level requirement
if (
tieredFlea.AmmoTplUnlocks.TryGetValue(offerItemTpl, out var unlockLevel)
&& playerLevel < unlockLevel
)
{
// Lock the offer if player's level is below the ammo's unlock requirement
offer.Locked = true;
return;
}
}
// Check for a direct level requirement for the offer item
if (tieredFlea.UnlocksTpl.TryGetValue(offerItemTpl, out var itemLevelRequirement))
{
if (playerLevel < itemLevelRequirement)
{
// Lock the offer if player's level is below the item's specific requirement
offer.Locked = true;
return;
}
}
// Optimisation - Skip further checks if the item type isn't in the restricted types list
if (!_itemHelper.IsOfBaseclasses(offerItemTpl, tieredFleaLimitTypes))
{
return;
}
// Check if the item belongs to any restricted type and if player level is insufficient
if (
tieredFleaLimitTypes
.Where(tieredItemType => _itemHelper.IsOfBaseclass(offerItemTpl, tieredItemType))
.Any(tieredItemType => playerLevel < tieredFlea.UnlocksType[tieredItemType])
)
{
// Players level is below matching types requirement, flag as locked
offer.Locked = true;
}
}
/// <summary>
/// Get matching offers that require the desired item and filter out offers from non traders if player is below ragfair
/// unlock level
/// </summary>
/// <param name="searchRequest">Search request from client</param>
/// <param name="pmcData">Player profile</param>
/// <returns>Matching RagfairOffer objects</returns>
public List<RagfairOffer> GetOffersThatRequireItem(
SearchRequestData searchRequest,
PmcData pmcData
)
{
// Get all offers that require the desired item and filter out offers from non traders if player below ragfair unlock
var offerIDsForItem = _ragfairRequiredItemsService.GetRequiredOffersById(
searchRequest.NeededSearchId
);
var tieredFlea = _ragfairConfig.TieredFlea;
var tieredFleaLimitTypes = tieredFlea.UnlocksType;
var result = new List<RagfairOffer>();
foreach (
var offer in offerIDsForItem
.Select(_ragfairOfferService.GetOfferByOfferId)
.Where(offer => PassesSearchFilterCriteria(searchRequest, offer, pmcData))
)
{
if (tieredFlea.Enabled && !OfferIsFromTrader(offer))
{
CheckAndLockOfferFromPlayerTieredFlea(
tieredFlea,
offer,
tieredFleaLimitTypes.Keys.ToList(),
pmcData.Info.Level.Value
);
}
result.Add(offer);
}
return result;
}
/// <summary>
/// Get offers from flea/traders specifically when building weapon preset
/// </summary>
/// <param name="searchRequest">Search request data</param>
/// <param name="itemsToAdd">string array of item tpls to search for</param>
/// <param name="traderAssorts">All trader assorts player can access/buy</param>
/// <param name="pmcData">Player profile</param>
/// <returns>RagfairOffer array</returns>
public List<RagfairOffer> GetOffersForBuild(
SearchRequestData searchRequest,
List<string> itemsToAdd,
Dictionary<string, TraderAssort> traderAssorts,
PmcData pmcData
)
{
var offersMap = new Dictionary<string, List<RagfairOffer>>();
var offersToReturn = new List<RagfairOffer>();
var playerIsFleaBanned = pmcData.PlayerIsFleaBanned(_timeUtil.GetTimeStamp());
var tieredFlea = _ragfairConfig.TieredFlea;
var tieredFleaLimitTypes = tieredFlea.UnlocksType;
foreach (var desiredItemTpl in searchRequest.BuildItems)
{
var matchingOffers = _ragfairOfferService.GetOffersOfType(desiredItemTpl.Key);
if (matchingOffers is null)
// No offers found for this item, skip
{
continue;
}
foreach (var offer in matchingOffers)
{
// Don't show pack offers
if (offer.SellInOnePiece.GetValueOrDefault(false))
{
continue;
}
if (!PassesSearchFilterCriteria(searchRequest, offer, pmcData))
{
continue;
}
if (
!IsDisplayableOffer(
searchRequest,
itemsToAdd,
traderAssorts,
offer,
pmcData,
playerIsFleaBanned
)
)
{
continue;
}
if (OfferIsFromTrader(offer))
{
if (TraderBuyRestrictionReached(offer))
{
continue;
}
if (TraderOutOfStock(offer))
{
continue;
}
if (TraderOfferItemQuestLocked(offer, traderAssorts))
{
continue;
}
if (TraderOfferLockedBehindLoyaltyLevel(offer, pmcData))
{
continue;
}
}
// Tiered flea and not trader offer
if (tieredFlea.Enabled && !OfferIsFromTrader(offer))
{
CheckAndLockOfferFromPlayerTieredFlea(
tieredFlea,
offer,
tieredFleaLimitTypes.Keys.ToList(),
pmcData.Info.Level.Value
);
// Do not add offer to build if user does not have access to it
if (offer.Locked.GetValueOrDefault(false))
{
continue;
}
}
var key = offer.Items[0].Template;
if (!offersMap.ContainsKey(key))
{
offersMap.Add(key, []);
}
offersMap[key].Add(offer);
}
}
// Get best offer for each item to show on screen
var offersToSort = new List<RagfairOffer>();
foreach (var possibleOffers in offersMap.Values)
{
// prepare temp list for offers
offersToSort.Clear();
offersToSort.AddRange(possibleOffers);
// Remove offers with locked = true (quest locked) when > 1 possible offers
// single trader item = shows greyed out
// multiple offers for item = is greyed out
if (possibleOffers.Count > 1)
{
var lockedOffers = GetLoyaltyLockedOffers(possibleOffers, pmcData);
// Exclude locked offers + above loyalty locked offers if at least 1 was found
offersToSort = possibleOffers
.Where(offer =>
!(offer.Locked.GetValueOrDefault(false) || lockedOffers.Contains(offer.Id))
)
.ToList();
// Exclude trader offers over their buy restriction limit
offersToSort = GetOffersInsideBuyRestrictionLimits(offersToSort);
}
// Sort offers by price and pick the best
var offer = _ragfairSortHelper.SortOffers(offersToSort, RagfairSort.PRICE)[0];
offersToReturn.Add(offer);
}
return offersToReturn;
}
/// <summary>
/// Should a ragfair offer be visible to the player
/// </summary>
/// <param name="searchRequest">Client request</param>
/// <param name="itemsToAdd"></param>
/// <param name="traderAssorts">Trader assort items - used for filtering out locked trader items</param>
/// <param name="offer">Flea offer</param>
/// <param name="pmcProfile">Player profile</param>
/// <param name="playerIsFleaBanned">Player cannot view flea yet/ever</param>
/// <returns>True = should be shown to player</returns>
private bool IsDisplayableOffer(
SearchRequestData searchRequest,
List<string> itemsToAdd,
Dictionary<string, TraderAssort> traderAssorts,
RagfairOffer offer,
PmcData pmcProfile,
bool playerIsFleaBanned = false
)
{
var offerRootItem = offer.Items.FirstOrDefault();
// Currency offer is sold for
var moneyTypeTpl = offer.Requirements.FirstOrDefault().Template;
var isTraderOffer = _databaseService.GetTraders().ContainsKey(offer.User.Id);
if (!isTraderOffer && playerIsFleaBanned)
{
return false;
}
// Offer root items tpl not in searched for array
if (!itemsToAdd.Contains(offerRootItem.Template))
// skip items we shouldn't include
{
return false;
}
// Performing a required search and offer doesn't have requirement for item
if (
!string.IsNullOrEmpty(searchRequest.NeededSearchId)
&& !offer.Requirements.Any(requirement =>
requirement.Template == searchRequest.NeededSearchId
)
)
{
return false;
}
// Weapon/equipment search + offer is preset
if (
searchRequest.BuildItems.Count == 0
&& // Prevent equipment loadout searches filtering out presets
searchRequest.BuildCount.GetValueOrDefault(0) > 0
&& _presetHelper.HasPreset(offerRootItem.Template)
)
{
return false;
}
// commented out as required search "which is for checking offers that are barters"
// has info.removeBartering as true, this if statement removed barter items.
if (
searchRequest.RemoveBartering.GetValueOrDefault(false)
&& !_paymentHelper.IsMoneyTpl(moneyTypeTpl)
)
// Don't include barter offers
{
return false;
}
if (offer.RequirementsCost is null)
// Don't include offers with undefined or NaN in it
{
return false;
}
// Handle trader items to remove items that are not available to the user right now
// e.g. required search for "lamp" shows 4 items, 3 of which are not available to a new player
// filter those out
if (isTraderOffer)
{
if (!traderAssorts.TryGetValue(offer.User.Id, out var assort))
// trader not visible on flea market
{
return false;
}
if (
!assort.Items.Any(item =>
{
return item.Id == offer.Root;
})
)
// skip (quest) locked items
{
return false;
}
}
return true;
}
/// <summary>
/// Get offers that have not exceeded buy limits
/// </summary>
/// <param name="possibleOffers">offers to process</param>
/// <returns>Offers</returns>
protected List<RagfairOffer> GetOffersInsideBuyRestrictionLimits(
List<RagfairOffer> possibleOffers
)
{
// Check offer has buy limit + is from trader + current buy count is at or over max
return possibleOffers
.Where(offer =>
{
if (
offer.BuyRestrictionMax is null
&& OfferIsFromTrader(offer)
&& offer.BuyRestrictionCurrent >= offer.BuyRestrictionMax
)
{
if (offer.BuyRestrictionCurrent >= offer.BuyRestrictionMax)
{
return false;
}
}
// Doesnt have buy limits, retrun offer
return true;
})
.ToList();
}
/// <summary>
/// Check if offer is from trader standing the player does not have
/// </summary>
/// <param name="offer">Offer to check</param>
/// <param name="pmcProfile">Player profile</param>
/// <returns>True if item is locked, false if item is purchaseable</returns>
protected bool TraderOfferLockedBehindLoyaltyLevel(RagfairOffer offer, PmcData pmcProfile)
{
if (!pmcProfile.TradersInfo.TryGetValue(offer.User.Id, out var userTraderSettings))
{
_logger.Warning(
$"Trader: {offer.User.Id} not found in profile, assuming offer is not locked being loyalty level"
);
return false;
}
return userTraderSettings.LoyaltyLevel < offer.LoyaltyLevel;
}
/// <summary>
/// Check if offer item is quest locked for current player by looking at sptQuestLocked property in traders
/// barter_scheme
/// </summary>
/// <param name="offer">Offer to check is quest locked</param>
/// <param name="traderAssorts">all trader assorts for player</param>
/// <returns>true if quest locked</returns>
public bool TraderOfferItemQuestLocked(
RagfairOffer offer,
Dictionary<string, TraderAssort> traderAssorts
)
{
var itemIds = offer.Items.Select(x => x.Id).ToHashSet();
//foreach (var item in offer.Items)
//{
// traderAssorts.TryGetValue(offer.User.Id, out var assorts);
// foreach (var barterKvP in assorts.BarterScheme.Where(x => itemIds.Contains(x.Key)))
// {
// foreach (var subBarter in barterKvP.Value)
// {
// if (subBarter.Any(subBarter => subBarter.SptQuestLocked.GetValueOrDefault(false)))
// {
// return true;
// }
// }
// }
//}
foreach (var _ in offer.Items)
{
traderAssorts.TryGetValue(offer.User.Id, out var assorts);
if (
assorts
.BarterScheme.Where(x => itemIds.Contains(x.Key))
.Any(barterKvP =>
barterKvP.Value.Any(subBarter =>
subBarter.Any(subBarter =>
subBarter.SptQuestLocked.GetValueOrDefault(false)
)
)
)
)
{
return true;
}
}
// Fallback, nothing found
return false;
}
/// <summary>
/// Has trader offer ran out of stock to sell to player
/// </summary>
/// <param name="offer">Offer to check stock of</param>
/// <returns>true if out of stock</returns>
protected bool TraderOutOfStock(RagfairOffer offer)
{
if (offer?.Items?.Count == 0)
{
return true;
}
return offer.Items[0]?.Upd?.StackObjectsCount == 0;
}
/// <summary>
/// Check if trader offers' BuyRestrictionMax value has been reached
/// </summary>
/// <param name="offer">Offer to check restriction properties of</param>
/// <returns>true if restriction reached, false if no restrictions/not reached</returns>
protected bool TraderBuyRestrictionReached(RagfairOffer offer)
{
var traderAssorts = _traderHelper.GetTraderAssortsByTraderId(offer.User.Id).Items;
// Find item being purchased from traders assorts
var assortData = traderAssorts.FirstOrDefault(item => item.Id == offer.Items[0].Id);
if (assortData is null)
{
// No trader assort data
_logger.Warning(
$"Unable to find trader: "
+ $"${offer.User.Nickname}assort for item: {_itemHelper.GetItemName(offer.Items[0].Template)} "
+ $"{offer.Items[0].Template}, cannot check if buy restriction reached"
);
return false;
}
if (assortData.Upd is null)
// No Upd = no chance of limits
{
return false;
}
// No restriction values
// Can't use !assortData.upd.BuyRestrictionX as value could be 0
if (
assortData.Upd.BuyRestrictionMax is null
|| assortData.Upd.BuyRestrictionCurrent is null
)
{
return false;
}
// Current equals max, limit reached
if (assortData.Upd.BuyRestrictionCurrent >= assortData.Upd.BuyRestrictionMax)
{
return true;
}
return false;
}
protected HashSet<string> GetLoyaltyLockedOffers(List<RagfairOffer> offers, PmcData pmcProfile)
{
var loyaltyLockedOffers = new HashSet<string>();
foreach (var offer in offers.Where(offer => OfferIsFromTrader(offer)))
{
if (
pmcProfile.TradersInfo.TryGetValue(offer.User.Id, out var traderDetails)
&& traderDetails.LoyaltyLevel < offer.LoyaltyLevel
)
{
loyaltyLockedOffers.Add(offer.Id);
}
}
return loyaltyLockedOffers;
}
/**
* Process all player-listed flea offers for a desired profile
* @param sessionId Session id to process offers for
* @returns true = complete
*/
public bool ProcessOffersOnProfile(string sessionId)
{
var currentTimestamp = _timeUtil.GetTimeStamp();
var profileOffers = GetProfileOffers(sessionId);
// No offers, don't do anything
if (profileOffers?.Count == 0)
{
return true;
}
// Index backwards as CompleteOffer() can delete offer object
for (var index = profileOffers.Count - 1; index >= 0; index--)
{
var offer = profileOffers[index];
if (currentTimestamp > offer.EndTime)
{
// Offer has expired before selling, skip as it will be processed in RemoveExpiredOffers()
continue;
}
if (
offer.SellResults is null
|| offer.SellResults.Count == 0
|| currentTimestamp < offer.SellResults.FirstOrDefault()?.SellTime
)
{
// Not sold / too early to check
continue;
}
var firstSellResult = offer.SellResults?.FirstOrDefault();
if (firstSellResult is null)
{
continue;
}
// Checks first item, first is spliced out of array after being processed
// Item sold
var totalItemsCount = 1d;
var boughtAmount = 1;
// Does item need to be re-stacked
if (!offer.SellInOnePiece.GetValueOrDefault(false))
{
// offer.items.reduce((sum, item) => sum + item.upd?.StackObjectsCount ?? 0, 0);
totalItemsCount = GetTotalStackCountSize([offer.Items]);
boughtAmount = firstSellResult.Amount ?? boughtAmount;
}
var ratingToAdd = offer.SummaryCost / totalItemsCount * boughtAmount;
IncreaseProfileRagfairRating(
_profileHelper.GetFullProfile(sessionId),
ratingToAdd.Value
);
// Remove the sell result object now it has been processed
offer.SellResults.Remove(firstSellResult);
// Can delete offer object, must run last
CompleteOffer(sessionId, offer, boughtAmount);
}
return true;
}
/// <summary>
/// Count up all root item StackObjectsCount properties of an array of items
/// </summary>
/// <param name="itemsInInventoryToSumStackCount">items to sum up</param>
/// <returns>Total stack count</returns>
public double GetTotalStackCountSize(List<List<Item>> itemsInInventoryToSumStackCount)
{
return itemsInInventoryToSumStackCount.Sum(itemAndChildren =>
itemAndChildren.FirstOrDefault()?.Upd?.StackObjectsCount.GetValueOrDefault(1) ?? 1
);
}
/// <summary>
/// Add amount to players ragfair rating
/// </summary>
/// <param name="profile">Profile to update</param>
/// <param name="amountToIncrementBy">Raw amount to add to players ragfair rating (excluding the reputation gain multiplier)</param>
public void IncreaseProfileRagfairRating(SptProfile profile, double? amountToIncrementBy)
{
var ragfairGlobalsConfig = _databaseService.GetGlobals().Configuration.RagFair;
profile.CharacterData.PmcData.RagfairInfo.IsRatingGrowing = true;
if (amountToIncrementBy is null)
{
_logger.Warning(
$"Unable to increment ragfair rating, value was not a number: {amountToIncrementBy}"
);
return;
}
profile.CharacterData.PmcData.RagfairInfo.Rating +=
ragfairGlobalsConfig.RatingIncreaseCount
/ ragfairGlobalsConfig.RatingSumForIncrease
* amountToIncrementBy;
}
/// <summary>
/// Return all offers a player has listed on a desired profile
/// </summary>
/// <param name="sessionId">Session/Player id</param>
/// <returns>List of ragfair offers</returns>
protected List<RagfairOffer> GetProfileOffers(string sessionId)
{
var profile = _profileHelper.GetPmcProfile(sessionId);
if (profile.RagfairInfo?.Offers is null)
{
return [];
}
return profile.RagfairInfo.Offers;
}
/**
* Delete an offer from a desired profile and from ragfair offers
* @param sessionId Session id of profile to delete offer from
* @param offerId Id of offer to delete
*/
protected void DeleteOfferById(string sessionId, string offerId)
{
var profileRagfairInfo = _profileHelper.GetPmcProfile(sessionId).RagfairInfo;
var offerIndex = profileRagfairInfo.Offers.FindIndex(o => o.Id == offerId);
if (offerIndex == -1)
{
_logger.Warning(
$"Unable to find offer: {offerId} in profile: {sessionId}, unable to delete"
);
}
if (offerIndex >= 0)
{
profileRagfairInfo.Offers.Splice(offerIndex, 1);
}
// Also delete from ragfair
_ragfairOfferService.RemoveOfferById(offerId);
}
/// <summary>
/// Complete the selling of players' offer
/// </summary>
/// <param name="offerOwnerSessionId">Session/Player id</param>
/// <param name="offer">Sold offer details</param>
/// <param name="boughtAmount">Amount item was purchased for</param>
/// <returns>ItemEventRouterResponse</returns>
public ItemEventRouterResponse CompleteOffer(
string offerOwnerSessionId,
RagfairOffer offer,
int boughtAmount
)
{
// Pack or ALL items of a multi-offer were bought - remove entire offer
if (offer.SellInOnePiece.GetValueOrDefault(false) || boughtAmount == offer.Quantity)
{
DeleteOfferById(offerOwnerSessionId, offer.Id);
}
else
{
// Partial purchase, reduce quantity by amount purchased
offer.Quantity -= boughtAmount;
}
// Assemble payment to send to seller now offer was purchased
var sellerProfile = _profileHelper.GetPmcProfile(offerOwnerSessionId);
var rootItem = offer.Items.FirstOrDefault();
var itemTpl = rootItem.Template;
var offerStackCount = rootItem.Upd.StackObjectsCount;
var paymentItemsToSendToPlayer = new List<Item>();
foreach (var requirement in offer.Requirements)
{
// Create an item template item
var requestedItem = new Item
{
Id = new MongoId(),
Template = requirement.Template,
Upd = new Upd { StackObjectsCount = requirement.Count * boughtAmount },
};
var stacks = _itemHelper.SplitStack(requestedItem);
foreach (var item in stacks)
{
var outItems = new List<Item> { item };
// TODO - is this code used?, may have been when adding barters to flea was still possible for player
if (requirement.OnlyFunctional.GetValueOrDefault(false))
{
var presetItems = _ragfairServerHelper.GetPresetItemsByTpl(item);
if (presetItems.Count > 0)
{
outItems.Add(presetItems[0]);
}
}
paymentItemsToSendToPlayer.AddRange(outItems);
}
}
var ragfairDetails = new MessageContentRagfair
{
OfferId = offer.Id,
// pack-offers NEED to be the full item count,
// otherwise it only removes 1 from the pack, leaving phantom offer on client ui
Count = offer.SellInOnePiece.GetValueOrDefault(false)
? offerStackCount.Value
: boughtAmount,
HandbookId = itemTpl,
};
var storageTimeSeconds = _timeUtil.GetHoursAsSeconds(
(int)_questHelper.GetMailItemRedeemTimeHoursForProfile(sellerProfile)
);
_mailSendService.SendDirectNpcMessageToPlayer(
offerOwnerSessionId,
Traders.RAGMAN,
MessageType.FleamarketMessage,
GetLocalisedOfferSoldMessage(itemTpl, boughtAmount),
paymentItemsToSendToPlayer,
storageTimeSeconds,
null,
ragfairDetails
);
// Adjust sellers sell sum values
sellerProfile.RagfairInfo.SellSum ??= 0;
sellerProfile.RagfairInfo.SellSum += offer.SummaryCost;
return _eventOutputHolder.GetOutput(offerOwnerSessionId);
}
/**
* Get a localised message for when players offer has sold on flea
* @param itemTpl Item sold
* @param boughtAmount How many were purchased
* @returns Localised message text
*/
protected string GetLocalisedOfferSoldMessage(MongoId itemTpl, int boughtAmount)
{
// Generate a message to inform that item was sold
var globalLocales = _localeService.GetLocaleDb();
if (!globalLocales.TryGetValue(_goodSoldTemplate, out var soldMessageLocaleGuid))
{
_logger.Error(
_serverLocalisationService.GetText(
"ragfair-unable_to_find_locale_by_key",
_goodSoldTemplate
)
);
}
// Used to replace tokens in sold message sent to player
var messageKey = $"{itemTpl.ToString()} Name";
var hasKey = globalLocales.TryGetValue(messageKey, out var value);
var tplVars = new SystemData
{
SoldItem = hasKey ? value : itemTpl,
BuyerNickname = _botHelper.GetPmcNicknameOfMaxLength(_botConfig.BotNameLengthLimit),
ItemCount = boughtAmount,
};
// Node searches for anything inside {property}: e.g.: "Your {soldItem} {itemCount} items were bought by {buyerNickname}."
// each part the takes the inside "Key" and gets it from the tplVars object
// 'Your Kalashnikov AKS-74U 5.45x39 assault rifle 1 items were bought by HB.'
// then seems to replace any " with nothing
// Seems to be much simpler just replacing each key like this.
soldMessageLocaleGuid = soldMessageLocaleGuid.Replace("{soldItem}", tplVars.SoldItem);
soldMessageLocaleGuid = soldMessageLocaleGuid.Replace(
"{itemCount}",
tplVars.ItemCount.ToString()
);
soldMessageLocaleGuid = soldMessageLocaleGuid.Replace(
"{buyerNickname}",
tplVars.BuyerNickname
);
return soldMessageLocaleGuid;
}
/**
* Check an offer passes the various search criteria the player requested
* @param searchRequest Client search request
* @param offer Offer to check
* @param pmcData Player profile
* @returns True if offer passes criteria
*/
protected bool PassesSearchFilterCriteria(
SearchRequestData searchRequest,
RagfairOffer offer,
PmcData pmcData
)
{
var isDefaultUserOffer = offer.User.MemberType == MemberCategory.Default;
var offerRootItem = offer.Items.FirstOrDefault();
var offerMoneyTypeTpl = offer.Requirements.FirstOrDefault().Template;
if (
pmcData.Info.Level < _databaseService.GetGlobals().Configuration.RagFair.MinUserLevel
&& isDefaultUserOffer
)
// Skip item if player is < global unlock level (default is 15) and item is from a dynamically generated source
{
return false;
}
var isTraderOffer = OfferIsFromTrader(offer);
if (searchRequest.OfferOwnerType == OfferOwnerType.TRADEROWNERTYPE && !isTraderOffer)
// don't include player offers
{
return false;
}
if (searchRequest.OfferOwnerType == OfferOwnerType.PLAYEROWNERTYPE && isTraderOffer)
// don't include trader offers
{
return false;
}
if (
searchRequest.OneHourExpiration.GetValueOrDefault(false)
&& offer.EndTime - _timeUtil.GetTimeStamp() > TimeUtil.OneHourAsSeconds
)
// offer expires within an hour
{
return false;
}
if (
searchRequest.QuantityFrom > 0
&& offerRootItem.Upd.StackObjectsCount < searchRequest.QuantityFrom
)
// Too few items to offer
{
return false;
}
if (
searchRequest.QuantityTo > 0
&& offerRootItem.Upd.StackObjectsCount > searchRequest.QuantityTo
)
// Too many items to offer
{
return false;
}
if (
searchRequest.OnlyFunctional.GetValueOrDefault(false)
&& !IsItemFunctional(offerRootItem, offer)
)
// Don't include non-functional items
{
return false;
}
if (offer.Items.Count == 1)
{
// Counts quality % using the offer items current durability compared to its possible max, not current max
// Single item
if (
IsConditionItem(offerRootItem)
&& !ItemQualityInRange(
offerRootItem,
searchRequest.ConditionFrom.Value,
searchRequest.ConditionTo.Value
)
)
{
return false;
}
}
else
{
var itemQualityPercent = _itemHelper.GetItemQualityModifierForItems(offer.Items) * 100;
if (itemQualityPercent < searchRequest.ConditionFrom)
{
return false;
}
if (itemQualityPercent > searchRequest.ConditionTo)
{
return false;
}
}
if (searchRequest.Currency > 0 && _paymentHelper.IsMoneyTpl(offerMoneyTypeTpl))
{
// Only want offers with specific currency
if (
_ragfairHelper.GetCurrencyTag(offerMoneyTypeTpl)
!= _ragfairHelper.GetCurrencyTag(searchRequest.Currency.GetValueOrDefault(0))
)
{
// Offer is for different currency to what search params allow, skip
return false;
}
}
if (searchRequest.PriceFrom > 0 && searchRequest.PriceFrom >= offer.RequirementsCost)
// price is too low
{
return false;
}
if (searchRequest.PriceTo > 0 && searchRequest.PriceTo <= offer.RequirementsCost)
// price is too high
{
return false;
}
// Passes above checks, search criteria filters have not filtered offer out
return true;
}
/**
* Check that the passed in offer item is functional
* @param offerRootItem The root item of the offer
* @param offer Flea offer to check
* @returns True if the given item is functional
*/
public bool IsItemFunctional(Item offerRootItem, RagfairOffer offer)
{
// Non-preset weapons/armor are always functional
if (!_presetHelper.HasPreset(offerRootItem.Template))
{
return true;
}
// For armor items that can hold mods, make sure the item count is at least the amount of required plates
if (_itemHelper.ArmorItemCanHoldMods(offerRootItem.Template))
{
var offerRootTemplate = _itemHelper.GetItem(offerRootItem.Template).Value;
var requiredPlateCount = offerRootTemplate
.Properties.Slots?.Where(item => item.Required.GetValueOrDefault(false))
?.Count();
return offer.Items.Count > requiredPlateCount;
}
// For other presets, make sure the offer has more than 1 item
return offer.Items.Count > 1;
}
/// <summary>
/// Does the passed in item have a condition property
/// </summary>
/// <param name="item">Item to check</param>
/// <returns>True if has condition</returns>
protected bool IsConditionItem(Item item)
{
// thanks typescript, undefined assertion is not returnable since it
// tries to return a multi-type object
if (item.Upd is null)
{
return false;
}
return item.Upd.MedKit is not null
|| item.Upd.Repairable is not null
|| item.Upd.Resource is not null
|| item.Upd.FoodDrink is not null
|| item.Upd.Key is not null
|| item.Upd.RepairKit is not null;
}
/// <summary>
/// Is items quality value within desired range
/// </summary>
/// <param name="item">Item to check quality of</param>
/// <param name="min">Desired minimum quality</param>
/// <param name="max">Desired maximum quality</param>
/// <returns>True if in range</returns>
protected bool ItemQualityInRange(Item item, int min, int max)
{
var itemQualityPercentage = 100 * _itemHelper.GetItemQualityModifier(item);
if (min > 0 && min > itemQualityPercentage)
// Item condition too low
{
return false;
}
if (max < 100 && max <= itemQualityPercentage)
// Item condition too high
{
return false;
}
return true;
}
/// <summary>
/// Does this offer come from a trader
/// </summary>
/// <param name="offer">Offer to check</param>
/// <returns>True = from trader</returns>
public bool OfferIsFromTrader(RagfairOffer offer)
{
return offer.User.MemberType == MemberCategory.Trader;
}
}