diff --git a/Core/Callbacks/RagfairCallbacks.cs b/Core/Callbacks/RagfairCallbacks.cs index ca17a33b..0f8fbc29 100644 --- a/Core/Callbacks/RagfairCallbacks.cs +++ b/Core/Callbacks/RagfairCallbacks.cs @@ -36,20 +36,19 @@ public class RagfairCallbacks( public async Task OnUpdate(long timeSinceLastRun) { - // if (timeSinceLastRun > this.ragfairConfig.runIntervalSeconds) { - // // There is a flag inside this class that only makes it run once. - // this.ragfairServer.addPlayerOffers(); - // - // // Check player offers and mail payment to player if sold - // this.ragfairController.update(); - // - // // Process all offers / expire offers - // await this.ragfairServer.update(); - // - // return true; - // } - // return false; - throw new NotImplementedException(); + if (timeSinceLastRun > _ragfairConfig.RunIntervalSeconds) { + // There is a flag inside this class that only makes it run once. + _ragfairServer.AddPlayerOffers(); + + // Check player offers and mail payment to player if sold + _ragfairController.Update(); + + // Process all offers / expire offers + await _ragfairServer.Update(); + + return true; + } + return false; } /// diff --git a/Core/Controllers/RagfairController.cs b/Core/Controllers/RagfairController.cs index 328d3c92..9a0cd083 100644 --- a/Core/Controllers/RagfairController.cs +++ b/Core/Controllers/RagfairController.cs @@ -1,48 +1,868 @@ using Core.Annotations; using Core.Models.Eft.Common; +using Core.Models.Eft.Common.Tables; using Core.Models.Eft.ItemEvent; using Core.Models.Eft.Ragfair; +using Core.Models.Utils; +using Core.Servers; +using Core.Services; +using Core.Helpers; +using Core.Models.Eft.Profile; +using Core.Models.Enums; +using Core.Routers; +using Core.Utils; +using Core.Models.Spt.Config; +using Core.Models.Common; +using Core.Models.Eft.Trade; +using Core.Generators; namespace Core.Controllers; [Injectable] -public class RagfairController( - - ) +public class RagfairController { - // TODO - public GetOffersResult GetOffers(string sessionId, SearchRequestData info) + private readonly ISptLogger _logger; + private readonly TimeUtil _timeUtil; + private readonly JsonUtil _jsonUtil; + private readonly HttpResponseUtil _httpResponseUtil; + private readonly EventOutputHolder _eventOutputHolder; + private readonly RagfairServer _ragfairServer; + private readonly ItemHelper _itemHelper; + private readonly InventoryHelper _inventoryHelper; + private readonly RagfairSellHelper _ragfairSellHelper; + private readonly HandbookHelper _handbookHelper; + private readonly ProfileHelper _profileHelper; + private readonly PaymentHelper _paymentHelper; + private readonly RagfairHelper _ragfairHelper; + private readonly RagfairSortHelper _ragfairSortHelper; + private readonly RagfairOfferHelper _ragfairOfferHelper; + private readonly TraderHelper _traderHelper; + private readonly DatabaseService _databaseService; + private readonly LocalisationService _localisationService; + private readonly RagfairTaxService _ragfairTaxService; + private readonly RagfairOfferService _ragfairOfferService; + private readonly PaymentService _paymentService; + private readonly RagfairPriceService _ragfairPriceService; + private readonly RagfairOfferGenerator _ragfairOfferGenerator; + private readonly ConfigServer _configServer; + + private readonly RagfairConfig _ragfairConfig; + + public RagfairController( + ISptLogger logger, + TimeUtil timeUtil, + JsonUtil jsonUtil, + HttpResponseUtil httpResponseUtil, + EventOutputHolder eventOutputHolder, + RagfairServer ragfairServer, + ItemHelper itemHelper, + InventoryHelper inventoryHelper, + RagfairSellHelper ragfairSellHelper, + HandbookHelper handbookHelper, + ProfileHelper profileHelper, + PaymentHelper paymentHelper, + RagfairHelper ragfairHelper, + RagfairSortHelper ragfairSortHelper, + RagfairOfferHelper ragfairOfferHelper, + TraderHelper traderHelper, + DatabaseService databaseService, + LocalisationService localisationService, + RagfairTaxService ragfairTaxService, + RagfairOfferService ragfairOfferService, + PaymentService paymentService, + RagfairPriceService ragfairPriceService, + RagfairOfferGenerator ragfairOfferGenerator, + ConfigServer configServer + ) + { + _logger = logger; + _timeUtil = timeUtil; + _jsonUtil = jsonUtil; + _httpResponseUtil = httpResponseUtil; + _eventOutputHolder = eventOutputHolder; + _ragfairServer = ragfairServer; + _itemHelper = itemHelper; + _inventoryHelper = inventoryHelper; + _ragfairSellHelper = ragfairSellHelper; + _handbookHelper = handbookHelper; + _profileHelper = profileHelper; + _paymentHelper = paymentHelper; + _ragfairHelper = ragfairHelper; + _ragfairSortHelper = ragfairSortHelper; + _ragfairOfferHelper = ragfairOfferHelper; + _traderHelper = traderHelper; + _databaseService = databaseService; + _localisationService = localisationService; + _ragfairTaxService = ragfairTaxService; + _ragfairOfferService = ragfairOfferService; + _paymentService = paymentService; + _ragfairPriceService = ragfairPriceService; + _ragfairOfferGenerator = ragfairOfferGenerator; + _configServer = configServer; + + _ragfairConfig = _configServer.GetConfig(); + } + + /** + * Check all profiles and sell player offers / send player money for listing if it sold + */ + public void Update() + { + foreach (var (sessionId, profile) in _profileHelper.GetProfiles()) { + // Check profile is capable of creating offers + var pmcProfile = profile.CharacterData.PmcData; + if ( + pmcProfile.RagfairInfo is not null && pmcProfile.Info.Level >= _databaseService.GetGlobals().Configuration.RagFair.MinUserLevel + ) + { + _ragfairOfferHelper.ProcessOffersOnProfile(sessionId); + } + } + } + + /** + * Handles client/ragfair/find + * Returns flea offers that match required search parameters + * @param sessionID Player id + * @param searchRequest Search request data + * @returns IGetOffersResult + */ + public GetOffersResult GetOffers(string sessionID, SearchRequestData searchRequest) + { + var profile = _profileHelper.GetFullProfile(sessionID); + + var itemsToAdd = _ragfairHelper.FilterCategories(sessionID, searchRequest); + var traderAssorts = _ragfairHelper.GetDisplayableAssorts(sessionID); + var result = new GetOffersResult{ + Offers = new List(), + OffersCount = searchRequest.Limit, + SelectedCategory = searchRequest.HandbookId, + }; + + result.Offers = GetOffersForSearchType(searchRequest, itemsToAdd, traderAssorts, profile.CharacterData.PmcData); + + // Client requested a category refresh + if (searchRequest.UpdateOfferCount is not null) + { + result.Categories = GetSpecificCategories(profile.CharacterData.PmcData, searchRequest, result.Offers); + } + + AddIndexValueToOffers(result.Offers); + + // Sort offers + result.Offers = _ragfairSortHelper.SortOffers( + result.Offers, + searchRequest.SortType.Value, + searchRequest.SortDirection.Value); + + // Match offers with quests and lock unfinished quests - get offers from traders + foreach (var traderOffer in result.Offers.Where(offer => _ragfairOfferHelper.OfferIsFromTrader(offer))) + { + // For the items, check the barter schemes. The method getDisplayableAssorts sets a flag sptQuestLocked + // to true if the quest is not completed yet + if (_ragfairOfferHelper.TraderOfferItemQuestLocked(traderOffer, traderAssorts)) + { + traderOffer.Locked = true; + } + + // Update offers BuyRestrictionCurrent/BuyRestrictionMax values + SetTraderOfferPurchaseLimits(traderOffer, profile); + SetTraderOfferStackSize(traderOffer); + } + + result.OffersCount = result.Offers.Count; + + // Handle paging before returning results only if searching for general items, not preset items + if (searchRequest.BuildCount == 0) + { + var start = searchRequest.Page * searchRequest.Limit; + var end = (int)Math.Min((double)((searchRequest.Page + 1) * searchRequest.Limit), result.Offers.Count); + result.Offers = result.Offers.Slice(start.Value, end); + } + return result; + } + + /** + * Adjust ragfair offer stack count to match same value as traders assort stack count + * @param offer Flea offer to adjust stack size of + */ + private void SetTraderOfferStackSize(RagfairOffer offer) + { + var firstItem = offer.Items[0]; + var traderAssorts = _traderHelper.GetTraderAssortsByTraderId(offer.User.Id).Items; + + var assortPurchased = traderAssorts.FirstOrDefault(x => x.Id == offer.Items.First().Id); + if (assortPurchased is null) + { + _logger.Warning( + _localisationService.GetText("ragfair-unable_to_adjust_stack_count_assort_not_found", new { + offerId = offer.Items.First().Id, + traderId = offer.User.Id, + })); + + return; + } + + firstItem.Upd.StackObjectsCount = assortPurchased.Upd.StackObjectsCount; + } + + /** + * Update a trader flea offer with buy restrictions stored in the traders assort + * @param offer Flea offer to update + * @param fullProfile Players full profile + */ + private void SetTraderOfferPurchaseLimits(RagfairOffer offer, SptProfile fullProfile) + { + // No trader found, create a blank record for them + fullProfile.TraderPurchases[offer.User.Id] ??= new(); + + var traderAssorts = _traderHelper.GetTraderAssortsByTraderId(offer.User.Id).Items; + var assortId = offer.Items.First().Id; + var assortData = traderAssorts.FirstOrDefault((item) => item.Id == assortId); + + // Use value stored in profile, otherwise use value directly from in-memory trader assort data + offer.BuyRestrictionCurrent = fullProfile.TraderPurchases[offer.User.Id][assortId] is not null + ? fullProfile.TraderPurchases[offer.User.Id][assortId].PurchaseCount + : assortData.Upd.BuyRestrictionCurrent; + + offer.BuyRestrictionMax = assortData.Upd.BuyRestrictionMax; + } + + /** + * Add index to all offers passed in (0-indexed) + * @param offers Offers to add index value to + */ + private void AddIndexValueToOffers(List offers) + { + var counter = 0; + + foreach (var offer in offers) { + offer.InternalId = ++counter; + } + } + + /** + * Get categories for the type of search being performed, linked/required/all + * @param searchRequest Client search request data + * @param offers Ragfair offers to get categories for + * @returns record with templates + counts + */ + private Dictionary? GetSpecificCategories(PmcData pmcProfile, SearchRequestData searchRequest, List offers) + { + // Linked/required search categories + var playerHasFleaUnlocked = + pmcProfile.Info.Level >= _databaseService.GetGlobals().Configuration.RagFair.MinUserLevel; + List offerPool = []; + if (IsLinkedSearch(searchRequest) || IsRequiredSearch(searchRequest)) + { + offerPool = offers; + } + else if (!(IsLinkedSearch(searchRequest) || IsRequiredSearch(searchRequest))) + { + // Get all categories + offerPool = _ragfairOfferService.GetOffers(); + } + else + { + _logger.Error(_localisationService.GetText("ragfair-unable_to_get_categories")); + _logger.Debug(_jsonUtil.Serialize(searchRequest)); + return new Dictionary(); + } + + return _ragfairServer.GetAllActiveCategories(playerHasFleaUnlocked, searchRequest, offerPool); + } + + /** + * Is the flea search being performed a 'linked' search type + * @param info Search request + * @returns True if it is a 'linked' search type + */ + private bool IsLinkedSearch(SearchRequestData searchRequest) + { + return searchRequest.LinkedSearchId != ""; + } + + /** + * Is the flea search being performed a 'required' search type + * @param info Search request + * @returns True if it is a 'required' search type + */ + private bool IsRequiredSearch(SearchRequestData searchRequest) + { + return searchRequest.NeededSearchId != ""; + } + + /** + * Get offers for the client based on type of search being performed + * @param searchRequest Client search request data + * @param itemsToAdd Comes from ragfairHelper.filterCategories() + * @param traderAssorts Trader assorts + * @param pmcProfile Player profile + * @returns array of offers + */ + private List GetOffersForSearchType(SearchRequestData searchRequest, List itemsToAdd, Dictionary traderAssorts, PmcData pmcProfile) + { + // Searching for items in preset menu + if (searchRequest.BuildCount is not null) + { + return _ragfairOfferHelper.GetOffersForBuild(searchRequest, itemsToAdd, traderAssorts, pmcProfile); + } + + if (searchRequest.NeededSearchId?.Length > 0) + { + return _ragfairOfferHelper.GetOffersThatRequireItem(searchRequest, pmcProfile); + } + + // Searching for general items + return _ragfairOfferHelper.GetValidOffers(searchRequest, itemsToAdd, traderAssorts, pmcProfile); + } + + /** + * Called when creating an offer on flea, fills values in top right corner + * @param getPriceRequest Client request object + * @param ignoreTraderOffers Should trader offers be ignored in the calcualtion + * @returns min/avg/max values for an item based on flea offers available + */ + public GetItemPriceResult GetItemMinAvgMaxFleaPriceValues(GetMarketPriceRequestData getPriceRequest, bool ignoreTraderOffers = true) + { + // Get all items of tpl + var offers = _ragfairOfferService.GetOffersOfType(getPriceRequest.TemplateId); + + // Offers exist for item, get averages of what's listed + if (offers.Count > 0) + { + // These get calculated while iterating through the list below + var minMax = new MinMax(0, int.MaxValue); + + // Get the average offer price, excluding barter offers + var average = GetAveragePriceFromOffers(offers, minMax, ignoreTraderOffers); + + return new GetItemPriceResult{ Avg = Math.Round(average), Min = minMax.Min, Max = minMax.Max }; + } + + // No offers listed, get price from live ragfair price list prices.json + // No flea price, get handbook price + var fleaPrices = _databaseService.GetPrices(); + if (!fleaPrices.TryGetValue(getPriceRequest.TemplateId, out var tplPrice)) + { + tplPrice = _handbookHelper.GetTemplatePrice(getPriceRequest.TemplateId); + } + + return new GetItemPriceResult{ Avg = tplPrice, Min = tplPrice, Max = tplPrice }; + } + + private double GetAveragePriceFromOffers(List offers, MinMax minMax, bool ignoreTraderOffers) + { + var sum = 0d; + var totalOfferCount = 0; + + foreach (var offer in offers) + { + // Exclude barter items, they tend to have outrageous equivalent prices + if (offer.Requirements.Any(req => !_paymentHelper.IsMoneyTpl(req.Template))) + { + continue; + } + + if (ignoreTraderOffers && _ragfairOfferHelper.OfferIsFromTrader(offer)) + { + continue; + } + + // Figure out how many items the requirementsCost is applying to, and what the per-item price is + var offerItemCount = offer.SellInOnePiece.GetValueOrDefault(false) + ? (offer.Items.First().Upd?.StackObjectsCount ?? 1) + : 1; + var perItemPrice = offer.RequirementsCost / offerItemCount; + + // Handle min/max calculations based on the per-item price + if (perItemPrice < minMax.Min) + { + minMax.Min = perItemPrice; + } + else if (perItemPrice > minMax.Max) + { + minMax.Max = perItemPrice; + } + + sum += perItemPrice.Value; + totalOfferCount++; + } + + if (totalOfferCount == 0) + { + return -1d; + } + + return sum / totalOfferCount; + } + + /** + * List item(s) on flea for sale + * @param pmcData Player profile + * @param offerRequest Flea list creation offer + * @param sessionID Session id + * @returns IItemEventRouterResponse + */ + public ItemEventRouterResponse AddPlayerOffer(PmcData pmcData, AddOfferRequestData offerRequest, string sessionID) + { + var output = _eventOutputHolder.GetOutput(sessionID); + var fullProfile = _profileHelper.GetFullProfile(sessionID); + + var validationMessage = ""; + if (!IsValidPlayerOfferRequest(offerRequest, validationMessage)) + { + return _httpResponseUtil.AppendErrorToOutput(output, validationMessage); + } + + var typeOfOffer = GetOfferType(offerRequest); + if (typeOfOffer == FleaOfferType.UNKNOWN) + { + return _httpResponseUtil.AppendErrorToOutput(output, "Unknown offer type, cannot list item on flea"); + } + + switch (typeOfOffer) + { + case FleaOfferType.SINGLE: + return CreateSingleOffer(sessionID, offerRequest, fullProfile, output); + case FleaOfferType.MULTI: + return CreateMultiOffer(sessionID, offerRequest, fullProfile, output); + case FleaOfferType.PACK: + return CreatePackOffer(sessionID, offerRequest, fullProfile, output); + case FleaOfferType.UNKNOWN: + default: + throw new ArgumentOutOfRangeException(); + } + } + + /** + * Is the item to be listed on the flea valid + * @param offerRequest Client offer request + * @param errorMessage message to show to player when offer is invalid + * @returns Is offer valid + */ + private bool IsValidPlayerOfferRequest(AddOfferRequestData offerRequest, string validationMessage) + { + if (offerRequest?.Items is null || offerRequest.Items.Count == 0) + { + _logger.Error(_localisationService.GetText("ragfair-invalid_player_offer_request")); + + return false; + } + + if (offerRequest.Requirements is null) + { + _logger.Error(_localisationService.GetText("ragfair-unable_to_place_offer_with_no_requirements")); + + return false; + } + + return true; + } + + /** + * Given a client request, determine what type of offer is being created + * single/multi/pack + * @param offerRequest Client request + * @returns FleaOfferType + */ + private FleaOfferType GetOfferType(AddOfferRequestData offerRequest) + { + var sellInOncePiece = offerRequest.SellInOnePiece.GetValueOrDefault(false); + + if (!sellInOncePiece) + { + if (offerRequest.Items.Count == 1) + { + return FleaOfferType.SINGLE; + } + if (offerRequest.Items.Count > 1) + { + return FleaOfferType.MULTI; + } + } + + if (sellInOncePiece) + { + return FleaOfferType.PACK; + } + + return FleaOfferType.UNKNOWN; + } + + /** + * Create a flea offer for multiples of the same item, can be single items or items with multiple in the stack + * e.g. 2 ammo stacks of 30 cartridges each + * Each item can be purchased individually + * @param sessionID Session id + * @param offerRequest Offer request from client + * @param fullProfile Full profile of player + * @param output Response to send to client + * @returns IItemEventRouterResponse + */ + private ItemEventRouterResponse CreateMultiOffer(string sessionId, AddOfferRequestData offerRequest, SptProfile fullProfile, ItemEventRouterResponse output) { throw new NotImplementedException(); } - public GetItemPriceResult GetItemMinAvgMaxFleaPriceValues(GetMarketPriceRequestData info) + /** + * Create a flea offer for multiple items, can be single items or items with multiple in the stack + * e.g. 2 ammo stacks of 30 cartridges each + * The entire package must be purchased in one go + * @param sessionID Session id + * @param offerRequest Offer request from client + * @param fullProfile Full profile of player + * @param output Response to send to client + * @returns IItemEventRouterResponse + */ + private ItemEventRouterResponse CreatePackOffer(string sessionId, AddOfferRequestData offerRequest, SptProfile fullProfile, ItemEventRouterResponse output) { throw new NotImplementedException(); } - public ItemEventRouterResponse AddPlayerOffer(PmcData pmcData, AddOfferRequestData info, string sessionId) + /** + * Create a flea offer for a single item - includes an item with > 1 sized stack + * e.g. 1 ammo stack of 30 cartridges + * @param sessionID Session id + * @param offerRequest Offer request from client + * @param fullProfile Full profile of player + * @param output Response to send to client + * @returns IItemEventRouterResponse + */ + private ItemEventRouterResponse CreateSingleOffer(string sessionID, AddOfferRequestData offerRequest, SptProfile fullProfile, ItemEventRouterResponse output) { - throw new NotImplementedException(); + var pmcData = fullProfile.CharacterData.PmcData; + //var itemsToListCount = offerRequest.Items.Count; // Does not count stack size, only items + + // Find items to be listed on flea from player inventory + var result = GetItemsToListOnFleaFromInventory(pmcData, offerRequest.Items); + if (result.Items is null || result.error is not null) + { + _httpResponseUtil.AppendErrorToOutput(output, result.errorMessage); + } + + // Total count of items summed using their stack counts + var stackCountTotal = _ragfairOfferHelper.GetTotalStackCountSize(result.Items); + + // Checks are done, create the offer + var playerListedPriceInRub = CalculateRequirementsPriceInRub(offerRequest.Requirements); + var offer = CreatePlayerOffer( + sessionID, + offerRequest.Requirements, + result.Items.First(), + false); + var rootItem = offer.Items.First(); + + // Get average of items quality+children + var qualityMultiplier = _itemHelper.GetItemQualityModifierForItems(offer.Items, true); + + // Average offer price for single item (or whole weapon) + var averages = GetItemMinAvgMaxFleaPriceValues(new GetMarketPriceRequestData{ TemplateId = rootItem.Template }); + var averageOfferPriceSingleItem = averages.Avg; + + // Check for and apply item price modifer if it exists in config + if (_ragfairConfig.Dynamic.ItemPriceMultiplier.TryGetValue(rootItem.Template, out double itemPriceModifer)) + { + averageOfferPriceSingleItem *= itemPriceModifer; + } + + // Multiply single item price by quality + averageOfferPriceSingleItem *= qualityMultiplier; + + // Packs are reduced to the average price of a single item in the pack vs the averaged single price of an item + var sellChancePercent = _ragfairSellHelper.CalculateSellChance( + averageOfferPriceSingleItem.Value, + playerListedPriceInRub, + qualityMultiplier); + offer.SellResult = _ragfairSellHelper.RollForSale(sellChancePercent, stackCountTotal); + + // Subtract flea market fee from stash + if (_ragfairConfig.Sell.Fees) + { + var taxFeeChargeFailed = ChargePlayerTaxFee( + sessionID, + rootItem, + pmcData, + playerListedPriceInRub, + stackCountTotal, + offerRequest, + output); + if (taxFeeChargeFailed) + { + return output; + } + } + + // Add offer to players profile + add to client response + fullProfile.CharacterData.PmcData.RagfairInfo.Offers.Add(offer); + output.ProfileChanges[sessionID].RagFairOffers.Add(offer); + + // Remove items from inventory after creating offer + foreach (var itemToRemove in offerRequest.Items) { + _inventoryHelper.RemoveItem(pmcData, itemToRemove, sessionID, output); + } + + return output; } - public ItemEventRouterResponse RemoveOffer(PmcData pmcData, RemoveOfferRequestData info, string sessionId) + /** + * Charge player a listing fee for using flea, pulls charge from data previously sent by client + * @param sessionID Player id + * @param rootItem Base item being listed (used when client tax cost not found and must be done on server) + * @param pmcData Player profile + * @param requirementsPriceInRub Rouble cost player chose for listing (used when client tax cost not found and must be done on server) + * @param itemStackCount How many items were listed by player (used when client tax cost not found and must be done on server) + * @param offerRequest Add offer request object from client + * @param output IItemEventRouterResponse + * @returns True if charging tax to player failed + */ + private bool ChargePlayerTaxFee( + string sessionId, + Item rootItem, + PmcData pmcData, + double requirementsPriceInRub, + int itemStackCount, + AddOfferRequestData offerRequest, + ItemEventRouterResponse output) { - throw new NotImplementedException(); + // Get tax from cache hydrated earlier by client, if that's missing fall back to server calculation (inaccurate) + var storedClientTaxValue = _ragfairTaxService.GetStoredClientOfferTaxValueById(offerRequest.Items[0]); + var tax = storedClientTaxValue is not null + ? storedClientTaxValue.Fee + : _ragfairTaxService.CalculateTax( + rootItem, + pmcData, + requirementsPriceInRub, + itemStackCount, + offerRequest.SellInOnePiece.GetValueOrDefault(false)); + + _logger.Debug($"Offer tax to charge: { tax}, pulled from client: { storedClientTaxValue.Count is not null}"); + + // cleanup of cache now we've used the tax value from it + _ragfairTaxService.ClearStoredOfferTaxById(offerRequest.Items.First()); + + var buyTradeRequest = CreateBuyTradeRequestObject("RUB", tax.Value); + _paymentService.PayMoney(pmcData, buyTradeRequest, sessionId, output); + if (output.Warnings.Count > 0) + { + _httpResponseUtil.AppendErrorToOutput( + output, + _localisationService.GetText("ragfair-unable_to_pay_commission_fee", tax)); + return true; + } + + return false; } - public ItemEventRouterResponse ExtendOffer(PmcData pmcData, ExtendOfferRequestData info, string sessionId) + private RagfairOffer CreatePlayerOffer(string sessionId, List requirements, List items, bool sellInOnePiece) { - throw new NotImplementedException(); + var loyalLevel = 1; + var formattedItems = items.Select(item => { + var isChild = items.Any((subItem) => subItem.Id == item.ParentId); + + return new Item { + Id = item.Id, + Template = item.Template, + ParentId = isChild ? item.ParentId: "hideout", + SlotId = isChild ? item.SlotId: "hideout", + Upd = item.Upd }; + }); + + var formattedRequirements = requirements.Select(item => new BarterScheme{ + Template = item.Template, + Count = item.Count, + OnlyFunctional = item.OnlyFunctional, + } + ); + + return _ragfairOfferGenerator.CreateAndAddFleaOffer( + sessionId, + _timeUtil.GetTimeStamp(), + formattedItems.ToList(), + formattedRequirements.ToList(), + loyalLevel, + sellInOnePiece); + } + + /** + * Get the handbook price in roubles for the items being listed + * @param requirements + * @returns Rouble price + */ + private double CalculateRequirementsPriceInRub(List requirements) + { + var requirementsPriceInRub = 0d; + foreach (var item in requirements) { + var requestedItemTpl = item.Template; + + if (_paymentHelper.IsMoneyTpl(requestedItemTpl)) + { + requirementsPriceInRub += _handbookHelper.InRUB(item.Count.Value, requestedItemTpl); + } + else + { + requirementsPriceInRub += _ragfairPriceService.GetDynamicPriceForItem(requestedItemTpl) * item.Count.Value; + } + } + + return requirementsPriceInRub; + } + + private dynamic GetItemsToListOnFleaFromInventory(PmcData pmcData, List itemIdsFromFleaOfferRequest) + { + List > itemsToReturn = []; + var errorMessage = string.Empty; + + // Count how many items are being sold and multiply the requested amount accordingly + foreach (var itemId in itemIdsFromFleaOfferRequest) { + var item = pmcData.Inventory.Items.FirstOrDefault((i) => i.Id == itemId); + if (item is null) + { + errorMessage = _localisationService.GetText("ragfair-unable_to_find_item_in_inventory", new { id = itemId}); + _logger.Error(errorMessage); + + return new { itemsToReturn, errorMessage }; + } + + item = _itemHelper.FixItemStackCount(item); + itemsToReturn.Add(_itemHelper.FindAndReturnChildrenAsItems(pmcData.Inventory.Items, itemId)); + } + + if (itemsToReturn?.Count == 0) + { + errorMessage = _localisationService.GetText("ragfair-unable_to_find_requested_items_in_inventory"); + _logger.Error(errorMessage); + + return new { ErrorMessage = errorMessage }; + } + + return new { Items = itemsToReturn, ErrorMessage = errorMessage }; + } + + public ItemEventRouterResponse RemoveOffer(RemoveOfferRequestData removeRequest, string sessionId) + { + var output = _eventOutputHolder.GetOutput(sessionId); + + var pmcData = _profileHelper.GetPmcProfile(sessionId); + var playerProfileOffers = pmcData.RagfairInfo.Offers; + if (playerProfileOffers is null) + { + _logger.Warning( + _localisationService.GetText("ragfair-unable_to_remove_offer_not_found_in_profile", new { + profileId = sessionId, + offerId = removeRequest.OfferId })); + + pmcData.RagfairInfo.Offers = new List(); + } + + var playerOfferIndex = playerProfileOffers.FindIndex(offer => offer.Id == removeRequest.OfferId); + if (playerOfferIndex == -1) + { + _logger.Error( + _localisationService.GetText("ragfair-offer_not_found_in_profile", new { + offerId = removeRequest.OfferId })); + return _httpResponseUtil.AppendErrorToOutput( + output, + _localisationService.GetText("ragfair-offer_not_found_in_profile_short")); + } + + var differenceInSeconds = playerProfileOffers[playerOfferIndex].EndTime - _timeUtil.GetTimeStamp(); + if (differenceInSeconds > _ragfairConfig.Sell.ExpireSeconds) + { + // `expireSeconds` Default is 71 seconds + var newEndTime = _ragfairConfig.Sell.ExpireSeconds + _timeUtil.GetTimeStamp(); + playerProfileOffers[playerOfferIndex].EndTime = (long?)Math.Round((double)newEndTime); + } + + return output; + } + + public ItemEventRouterResponse ExtendOffer(ExtendOfferRequestData extendRequest, string sessionId) + { + var output = _eventOutputHolder.GetOutput(sessionId); + + var pmcData = _profileHelper.GetPmcProfile(sessionId); + var playerOffers = pmcData.RagfairInfo.Offers; + var playerOfferIndex = playerOffers.FindIndex((offer) => offer.Id == extendRequest.OfferId); + var secondsToAdd = extendRequest.RenewalTime * TimeUtil.OneHourAsSeconds; + + if (playerOfferIndex == -1) + { + _logger.Warning( + _localisationService.GetText("ragfair-offer_not_found_in_profile", new { + offerId = extendRequest.OfferId })); + return _httpResponseUtil.AppendErrorToOutput(output, _localisationService.GetText("ragfair-offer_not_found_in_profile_short")); + } + + var playerOffer = playerOffers[playerOfferIndex]; + + // MOD: Pay flea market fee + if (_ragfairConfig.Sell.Fees) + { + var count = 1; + var sellInOncePiece = playerOffer.SellInOnePiece.GetValueOrDefault(false); + if (!sellInOncePiece) + { + count = playerOffer.Items.Sum(offerItem => offerItem.Upd?.StackObjectsCount ?? 0); + } + + var tax = _ragfairTaxService.CalculateTax( + playerOffer.Items.First(), + pmcData, + playerOffer.RequirementsCost.Value, + count, + sellInOncePiece); + + var request = CreateBuyTradeRequestObject("RUB", tax); + _paymentService.PayMoney(pmcData, request, sessionId, output); + if (output.Warnings.Count > 0) + { + return _httpResponseUtil.AppendErrorToOutput( + output, + _localisationService.GetText("ragfair-unable_to_pay_commission_fee")); + } + } + + // Add extra time to offer + playerOffers[playerOfferIndex].EndTime += (long?)Math.Round((decimal)secondsToAdd); + + return output; + } + + /** + * Create a basic trader request object with price and currency type + * @param currency What currency: RUB, EURO, USD + * @param value Amount of currency + * @returns IProcessBuyTradeRequestData + */ + private ProcessBuyTradeRequestData CreateBuyTradeRequestObject(string currency, double value) + { + return new ProcessBuyTradeRequestData + { + TId = "ragfair", + Action = "TradingConfirm", + SchemeItems = [new SchemeItem { Id = _paymentHelper.GetCurrency(currency), Count = Math.Round(value) }], + Type = "", + ItemId = "", + Count = 0, + SchemeId = 0, + }; } public Dictionary GetAllFleaPrices() { - throw new NotImplementedException(); + return _ragfairPriceService.GetAllFleaPrices(); } - public RagfairOffer GetOfferById(string sessionId, GetRagfairOfferByIdRequest info) + public Dictionary GetStaticPrices() { + return _ragfairPriceService.GetAllStaticPrices(); + } + + public RagfairOffer? GetOfferById(string sessionId, GetRagfairOfferByIdRequest request) { - throw new NotImplementedException(); + var offers = _ragfairOfferService.GetOffers(); + var offerToReturn = offers.FirstOrDefault((offer) => offer.InternalId == request.Id); + + return offerToReturn; } } diff --git a/Core/Generators/BotLevelGenerator.cs b/Core/Generators/BotLevelGenerator.cs index 2ae56f7b..d36ed3c9 100644 --- a/Core/Generators/BotLevelGenerator.cs +++ b/Core/Generators/BotLevelGenerator.cs @@ -89,10 +89,6 @@ public class BotLevelGenerator( maxLevel = Math.Min(Math.Max(maxLevel, minPossibleLevel), maxPossibleLevel); minLevel = Math.Min(Math.Max(minLevel, minPossibleLevel), maxPossibleLevel); - return new MinMax - { - Min = minLevel, - Max = maxLevel, - }; + return new MinMax(minLevel, maxLevel); } } diff --git a/Core/Helpers/ItemHelper.cs b/Core/Helpers/ItemHelper.cs index 774172b6..bbe128c4 100644 --- a/Core/Helpers/ItemHelper.cs +++ b/Core/Helpers/ItemHelper.cs @@ -677,7 +677,7 @@ public class ItemHelper( * @param modsOnly Include only mod items, exclude items stored inside root item * @returns A list of Item objects */ - public List FindAndReturnChildrenAsItems(List? items, string baseItemId, bool modsOnly = false) + public List FindAndReturnChildrenAsItems(List items, string baseItemId, bool modsOnly = false) { List list = []; foreach (var childItem in items) diff --git a/Core/Helpers/RagfairOfferHelper.cs b/Core/Helpers/RagfairOfferHelper.cs index 5b56ceea..e3707582 100644 --- a/Core/Helpers/RagfairOfferHelper.cs +++ b/Core/Helpers/RagfairOfferHelper.cs @@ -1,9 +1,10 @@ -using Core.Annotations; +using Core.Annotations; using Core.Models.Eft.Common; using Core.Models.Eft.Common.Tables; using Core.Models.Eft.ItemEvent; using Core.Models.Eft.Profile; using Core.Models.Eft.Ragfair; +using Core.Models.Enums; using Core.Models.Spt.Config; namespace Core.Helpers; @@ -21,7 +22,7 @@ public class RagfairOfferHelper /// Offers the player should see public List GetValidOffers( SearchRequestData searchRequest, - string[] itemsToAdd, + List itemsToAdd, Dictionary traderAssorts, PmcData pmcData) { @@ -65,7 +66,7 @@ public class RagfairOfferHelper /// RagfairOffer array public List GetOffersForBuild( SearchRequestData searchRequest, - string[] itemsToAdd, + List itemsToAdd, Dictionary traderAssorts, PmcData pmcData) { @@ -279,8 +280,8 @@ public class RagfairOfferHelper /// /// Offer to check /// True = from trader - public bool OfferFromTrader(RagfairOffer offer) + public bool OfferIsFromTrader(RagfairOffer offer) { - throw new NotImplementedException(); + return offer.User.MemberType == MemberCategory.TRADER; } } diff --git a/Core/Helpers/TraderHelper.cs b/Core/Helpers/TraderHelper.cs index 5308a3fe..1ba199a2 100644 --- a/Core/Helpers/TraderHelper.cs +++ b/Core/Helpers/TraderHelper.cs @@ -209,7 +209,7 @@ public class TraderHelper( // create temporary entry to prevent logger spam { TraderId = traderId, - Seconds = new MinMax { Min = _traderConfig.UpdateTimeDefault, Max = _traderConfig.UpdateTimeDefault } + Seconds = new MinMax(_traderConfig.UpdateTimeDefault, _traderConfig.UpdateTimeDefault) } ); diff --git a/Core/Models/Common/MinMax.cs b/Core/Models/Common/MinMax.cs index 1ab1487f..04f23661 100644 --- a/Core/Models/Common/MinMax.cs +++ b/Core/Models/Common/MinMax.cs @@ -1,9 +1,20 @@ -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; namespace Core.Models.Common; public record MinMax { + public MinMax(double min, double max) + { + Min = min; + Max = max; + } + + public MinMax() + { + + } + [JsonPropertyName("type")] public string? Type { get; set; } diff --git a/Core/Models/Eft/Common/Tables/Item.cs b/Core/Models/Eft/Common/Tables/Item.cs index 859d1b5f..2299556f 100644 --- a/Core/Models/Eft/Common/Tables/Item.cs +++ b/Core/Models/Eft/Common/Tables/Item.cs @@ -50,7 +50,7 @@ public record Upd public string? SptPresetId { get; set; } public UpdFaceShield? FaceShield { get; set; } - public double? StackObjectsCount { get; set; } // can be a string, double or int + public int? StackObjectsCount { get; set; } // can be a string, double or int public bool? UnlimitedCount { get; set; } public UpdRepairable? Repairable { get; set; } public UpdRecodableComponent? RecodableComponent { get; set; } diff --git a/Core/Models/Eft/Ragfair/GetItemPriceResult.cs b/Core/Models/Eft/Ragfair/GetItemPriceResult.cs index 5491608c..e2d26202 100644 --- a/Core/Models/Eft/Ragfair/GetItemPriceResult.cs +++ b/Core/Models/Eft/Ragfair/GetItemPriceResult.cs @@ -6,5 +6,5 @@ namespace Core.Models.Eft.Ragfair; public record GetItemPriceResult : MinMax { [JsonPropertyName("avg")] - public int? Avg { get; set; } + public double? Avg { get; set; } } diff --git a/Core/Models/Eft/Ragfair/RagfairOffer.cs b/Core/Models/Eft/Ragfair/RagfairOffer.cs index e8a9637e..92c311ff 100644 --- a/Core/Models/Eft/Ragfair/RagfairOffer.cs +++ b/Core/Models/Eft/Ragfair/RagfairOffer.cs @@ -1,4 +1,4 @@ -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; using Core.Models.Eft.Common.Tables; using Core.Models.Enums; @@ -30,7 +30,7 @@ public record RagfairOffer /** Rouble price per item */ [JsonPropertyName("requirementsCost")] - public decimal? RequirementsCost { get; set; } + public double? RequirementsCost { get; set; } [JsonPropertyName("startTime")] public long? StartTime { get; set; } diff --git a/Core/Models/Eft/Trade/ProcessBuyTradeRequestData.cs b/Core/Models/Eft/Trade/ProcessBuyTradeRequestData.cs index 8c765d63..f51ef665 100644 --- a/Core/Models/Eft/Trade/ProcessBuyTradeRequestData.cs +++ b/Core/Models/Eft/Trade/ProcessBuyTradeRequestData.cs @@ -33,5 +33,5 @@ public record SchemeItem public string? Id { get; set; } [JsonPropertyName("count")] - public int? Count { get; set; } + public double? Count { get; set; } } diff --git a/Core/Models/Enums/FleaOfferType.cs b/Core/Models/Enums/FleaOfferType.cs new file mode 100644 index 00000000..b41d2b29 --- /dev/null +++ b/Core/Models/Enums/FleaOfferType.cs @@ -0,0 +1,10 @@ +namespace Core.Models.Enums +{ + public enum FleaOfferType + { + SINGLE = 0, + MULTI = 1, + PACK = 2, + UNKNOWN = 3 + } +} diff --git a/Core/Servers/RagfairServer.cs b/Core/Servers/RagfairServer.cs index 808b2a25..603728f0 100644 --- a/Core/Servers/RagfairServer.cs +++ b/Core/Servers/RagfairServer.cs @@ -46,7 +46,7 @@ namespace Core.Servers await Update(); } - private async Task Update() + public async Task Update() { _ragfairOfferService.ExpireStaleOffers(); diff --git a/Core/Services/PaymentService.cs b/Core/Services/PaymentService.cs index 172f949d..3ce95d52 100644 --- a/Core/Services/PaymentService.cs +++ b/Core/Services/PaymentService.cs @@ -2,6 +2,7 @@ using Core.Models.Eft.Common; using Core.Models.Eft.Common.Tables; using Core.Models.Eft.ItemEvent; +using Core.Models.Eft.Trade; namespace Core.Services; @@ -65,4 +66,16 @@ public class PaymentService { throw new NotImplementedException(); } + + /** + * Take money and insert items into return to server request + * @param pmcData Pmc profile + * @param request Buy item request + * @param sessionID Session id + * @param output Client response + */ + public void PayMoney(PmcData pmcData, ProcessBuyTradeRequestData request, string sessionID, ItemEventRouterResponse output) + { + throw new NotImplementedException(); + } } diff --git a/Core/Services/RagfairTaxService.cs b/Core/Services/RagfairTaxService.cs index 24a4c87d..d6541aa8 100644 --- a/Core/Services/RagfairTaxService.cs +++ b/Core/Services/RagfairTaxService.cs @@ -1,4 +1,6 @@ -using Core.Annotations; +using Core.Annotations; +using Core.Models.Eft.Common; +using Core.Models.Eft.Common.Tables; using Core.Models.Eft.Ragfair; namespace Core.Services; @@ -16,7 +18,7 @@ public class RagfairTaxService throw new NotImplementedException(); } - public Dictionary GetStoredClientOfferTaxValueById(string offerIdToGet) + public StorePlayerOfferTaxAmountRequestData GetStoredClientOfferTaxValueById(string offerIdToGet) { throw new NotImplementedException(); } @@ -32,8 +34,8 @@ public class RagfairTaxService * @returns Tax in roubles */ public double CalculateTax( - Dictionary item, - Dictionary pmcData, + Item item, + PmcData pmcData, double requirementsValue, int offerItemCount, bool sellInOnePiece)