diff --git a/Libraries/Core/Controllers/RagfairController.cs b/Libraries/Core/Controllers/RagfairController.cs index 75823257..accbb0e8 100644 --- a/Libraries/Core/Controllers/RagfairController.cs +++ b/Libraries/Core/Controllers/RagfairController.cs @@ -346,7 +346,7 @@ public class RagfairController /** * 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 + * @param ignoreTraderOffers Should trader offers be ignored in the calculation * @returns min/avg/max values for an item based on flea offers available */ public GetItemPriceResult GetItemMinAvgMaxFleaPriceValues(GetMarketPriceRequestData getPriceRequest, bool ignoreTraderOffers = true) @@ -358,7 +358,7 @@ public class RagfairController if (offers.Count > 0) { // These get calculated while iterating through the list below - var minMax = new MinMax(0, int.MaxValue); + var minMax = new MinMax(int.MaxValue, 0); // Get the average offer price, excluding barter offers var average = GetAveragePriceFromOffers(offers, minMax, ignoreTraderOffers); diff --git a/Libraries/Core/Generators/RagfairOfferGenerator.cs b/Libraries/Core/Generators/RagfairOfferGenerator.cs index df48ddfe..894ae356 100644 --- a/Libraries/Core/Generators/RagfairOfferGenerator.cs +++ b/Libraries/Core/Generators/RagfairOfferGenerator.cs @@ -13,6 +13,7 @@ using Core.Utils; using Core.Utils.Cloners; using Server; using SptCommon.Extensions; +using Core.Models.Eft.Player; namespace Core.Generators; @@ -95,7 +96,7 @@ public class RagfairOfferGenerator( var isTrader = ragfairServerHelper.IsTrader(userID); var offerRequirements = barterScheme.Select((barter) => { - var offerRequirement = new OfferRequirement(){ + var offerRequirement = new OfferRequirement{ Template = barter.Template, Count = Math.Round((double) barter.Count, 2), OnlyFunctional = barter.OnlyFunctional ?? false, @@ -115,7 +116,7 @@ public class RagfairOfferGenerator( var itemStackCount = itemsClone[0].Upd?.StackObjectsCount ?? 1; // Hydrate ammo boxes with cartridges + ensure only 1 item is present (ammo box) - // On offer refresh dont re-add cartridges to ammo box that already has cartridges + // On offer refresh don't re-add cartridges to ammo box that already has cartridges if (itemHelper.IsOfBaseclass(itemsClone[0].Template, BaseClasses.AMMO_BOX) && itemsClone.Count == 1) { itemHelper.AddCartridgesToAmmoBox(itemsClone, itemHelper.GetItem(items[0].Template).Value); } @@ -123,7 +124,7 @@ public class RagfairOfferGenerator( var roubleListingPrice = Math.Round((double) ConvertOfferRequirementsIntoRoubles(offerRequirements)); var singleItemListingPrice = isPackOffer ? roubleListingPrice / itemStackCount : roubleListingPrice; - var offer = new RagfairOffer() { + var offer = new RagfairOffer { Id= hashUtil.Generate(), InternalId= offerCounter, User= CreateUserDataForFleaOffer(userID, isTrader), @@ -131,7 +132,7 @@ public class RagfairOfferGenerator( Items= itemsClone, ItemsCost= Math.Round((double) handbookHelper.GetTemplatePrice(items[0].Template)), // Handbook price Requirements= offerRequirements, - RequirementsCost= Math.Round((double) singleItemListingPrice), + RequirementsCost= Math.Round(singleItemListingPrice), SummaryCost= roubleListingPrice, StartTime= time, EndTime= GetOfferEndTime(userID, time), @@ -458,17 +459,21 @@ public class RagfairOfferGenerator( ); // Remove removable plates if % check passes - if (itemHelper.ArmorItemCanHoldMods(itemWithChildren[0].Template)) { + if (itemHelper.ArmorItemCanHoldMods(itemWithChildren[0].Template)) + { var armorConfig = ragfairConfig.Dynamic.Armor; var shouldRemovePlates = randomUtil.GetChance100(armorConfig.RemoveRemovablePlateChance); - if (shouldRemovePlates && itemHelper.ArmorItemHasRemovablePlateSlots(itemWithChildren[0].Template)) { - var offerItemPlatesToRemove = itemWithChildren.Where((item) => - armorConfig.PlateSlotIdToRemovePool.Contains(item.SlotId?.ToLower()) + if (shouldRemovePlates && itemHelper.ArmorItemHasRemovablePlateSlots(itemWithChildren[0].Template)) + { + var offerItemPlatesToRemove = itemWithChildren.Where( + (item) => + armorConfig.PlateSlotIdToRemovePool.Contains(item.SlotId?.ToLower()) ); // 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)).ToList(); + var indexesToRemove = offerItemPlatesToRemove.Select(plateItem => itemWithChildren.IndexOf(plateItem)) + .ToList(); foreach (var index in indexesToRemove.OrderByDescending(x => x)) { itemWithChildren.RemoveAt(index); diff --git a/Libraries/Core/Helpers/ItemHelper.cs b/Libraries/Core/Helpers/ItemHelper.cs index 48dc5c85..6a1db994 100644 --- a/Libraries/Core/Helpers/ItemHelper.cs +++ b/Libraries/Core/Helpers/ItemHelper.cs @@ -579,7 +579,7 @@ public class ItemHelper( if (medkit is not null) { // Meds - result = medkit.HpResource ?? 0 / itemDetails.Properties.MaxHpResource ?? 0; + result = (medkit.HpResource ?? 0) / (itemDetails.Properties.MaxHpResource ?? 0); } else if (repairable is not null) { @@ -588,7 +588,7 @@ public class ItemHelper( else if (foodDrink is not null) { // food & drink - result = foodDrink.HpPercent ?? 0 / itemDetails.Properties.MaxResource ?? 0; + result = (foodDrink.HpPercent ?? 0) / (itemDetails.Properties.MaxResource ?? 0); } else if (key is not null && key.NumberOfUsages > 0 && itemDetails.Properties.MaximumNumberOfUsage > 0) { @@ -599,12 +599,12 @@ public class ItemHelper( else if (resource is not null && resource.UnitsConsumed > 0) { // Things like fuel tank - result = resource.Value ?? 0 / itemDetails.Properties.MaxResource ?? 0; + result = (resource.Value ?? 0) / (itemDetails.Properties.MaxResource ?? 0); } else if (repairKit is not null) { // Repair kits - result = repairKit.Resource ?? 0 / itemDetails.Properties.MaxRepairResource ?? 0; + result = (repairKit.Resource ?? 0) / (itemDetails.Properties.MaxRepairResource ?? 0); } if (result == 0) diff --git a/Libraries/Core/Helpers/PaymentHelper.cs b/Libraries/Core/Helpers/PaymentHelper.cs index 33ea966c..d866f51f 100644 --- a/Libraries/Core/Helpers/PaymentHelper.cs +++ b/Libraries/Core/Helpers/PaymentHelper.cs @@ -20,6 +20,7 @@ public class PaymentHelper(ConfigServer _configServer) var moneyTypes = new List { Money.DOLLARS, + Money.EUROS, Money.ROUBLES, Money.GP, diff --git a/Libraries/Core/Helpers/RagfairSellHelper.cs b/Libraries/Core/Helpers/RagfairSellHelper.cs index 40bcec6c..019ecc66 100644 --- a/Libraries/Core/Helpers/RagfairSellHelper.cs +++ b/Libraries/Core/Helpers/RagfairSellHelper.cs @@ -1,24 +1,57 @@ -using SptCommon.Annotations; +using SptCommon.Annotations; using Core.Models.Eft.Ragfair; +using Core.Models.Spt.Config; +using Core.Servers; +using Core.Models.Utils; +using Core.Services; +using Core.Utils; namespace Core.Helpers; [Injectable] -public class RagfairSellHelper +public class RagfairSellHelper( + ISptLogger _logger, + TimeUtil _timeUtil, + RandomUtil _randomUtil, + DatabaseService _databaseService, + ConfigServer _configServer) { + + protected RagfairConfig _ragfairConfig = _configServer.GetConfig(); /// /// Get the percent chance to sell an item based on its average listed price vs player chosen listing price /// /// Price of average offer in roubles /// Price player listed item for in roubles - /// Quality multipler of item being sold + /// Quality multiplier of item being sold /// percent value public double CalculateSellChance( double averageOfferPriceRub, double playerListedPriceRub, double qualityMultiplier) { - throw new NotImplementedException(); + var sellConfig = _ragfairConfig.Sell.Chance; + + // Base sell chance modified by items quality + var baseSellChancePercent = sellConfig.Base * qualityMultiplier; + + // Modifier gets applied twice to either penalize or incentivize over/under pricing (Probably a cleaner way to do this) + var sellModifier = (averageOfferPriceRub / playerListedPriceRub) * sellConfig.SellMultiplier; + var sellChance = Math.Round(baseSellChancePercent * sellModifier * Math.Pow(sellModifier, 3) + 10); // Power of 3 + + // Adjust sell chance if below config value + if (sellChance < sellConfig.MinSellChancePercent) + { + sellChance = sellConfig.MinSellChancePercent; + } + + // Adjust sell chance if above config value + if (sellChance > sellConfig.MaxSellChancePercent) + { + sellChance = sellConfig.MaxSellChancePercent; + } + + return sellChance; } /// @@ -28,8 +61,70 @@ public class RagfairSellHelper /// count of items to sell /// All items listed get sold at once /// List of purchases of item(s) listed - public List RollForSale(double sellChancePercent, int itemSellCount, bool sellInOneGo = false) + public List RollForSale(double? sellChancePercent, int itemSellCount, bool sellInOneGo = false) { - throw new NotImplementedException(); + var startTimestamp = _timeUtil.GetTimeStamp(); + + // Get a time in future to stop simulating sell chances at + var endTime = + startTimestamp + + _timeUtil.GetHoursAsSeconds((int)_databaseService.GetGlobals().Configuration.RagFair.OfferDurationTimeInHour.Value); + + var sellTimestamp = startTimestamp; + var remainingCount = itemSellCount; + var result = new List(); + + var effectiveSellChance = sellChancePercent; + + if (sellChancePercent is null) + { + effectiveSellChance = _ragfairConfig.Sell.Chance.Base; + _logger.Warning($"Sell chance was not a number: {sellChancePercent}, defaulting to {_ragfairConfig.Sell.Chance.Base}%"); + } + + _logger.Debug($"Rolling to sell: { itemSellCount}items(chance: { effectiveSellChance}%)"); + + // No point rolling for a sale on a 0% chance item, exit early + if (effectiveSellChance == 0) + { + return result; + } + + while (remainingCount > 0 && sellTimestamp < endTime) + { + var boughtAmount = sellInOneGo ? remainingCount : _randomUtil.GetInt(1, remainingCount); + if (_randomUtil.GetChance100(effectiveSellChance)) + { + // Passed roll check, item will be sold + // Weight time to sell towards selling faster based on how cheap the item sold + var weighting = (100 - effectiveSellChance) / 100; + var maximumTime = weighting * (_ragfairConfig.Sell.Time.Max * 60); + var minimumTime = _ragfairConfig.Sell.Time.Min * 60; + if (maximumTime < minimumTime) + { + maximumTime = minimumTime + 5; + } + // Sell time will be random between min/max + var random = new Random(); + var newSellTime = Math.Floor(random.NextDouble() * (maximumTime.Value - minimumTime.Value) + minimumTime.Value); + if (newSellTime == 0) + { + // Ensure all sales don't occur the same exact time + newSellTime += 1; + } + sellTimestamp += (long)newSellTime; + result.Add( new SellResult{ SellTime = sellTimestamp, Amount = boughtAmount }); + + _logger.Debug($"Offer will sell at: { _timeUtil.GetDateTimeFromTimeStamp(sellTimestamp).ToLocalTime().ToString()}, bought: {boughtAmount}"); + } + else + { + _logger.Debug($"Offer rolled not to sell, item count: { boughtAmount}"); + } + + remainingCount -= boughtAmount; + } + + return result; } } diff --git a/Libraries/Core/Utils/RagfairOfferHolder.cs b/Libraries/Core/Utils/RagfairOfferHolder.cs index 48a61481..5a17adc0 100644 --- a/Libraries/Core/Utils/RagfairOfferHolder.cs +++ b/Libraries/Core/Utils/RagfairOfferHolder.cs @@ -146,14 +146,18 @@ public class RagfairOfferHolder( } else { - var valueMapped = new Dictionary(); - valueMapped.Add(offer.Id, offer); + var valueMapped = new Dictionary { { offer.Id, offer } }; _offersByTrader.Add(trader, valueMapped); } } - protected bool IsStale(RagfairOffer offer, long time) + protected bool IsStale(RagfairOffer? offer, long time) { - return offer.EndTime < time || (offer.Items[0].Upd?.StackObjectsCount ?? 0) < 1; + if (offer is null) + { + return false; + } + + return offer.EndTime < time || (offer.Items.FirstOrDefault().Upd?.StackObjectsCount ?? 0) < 1; } }