From aaa272b151fe47cd7d18986eac2dc056a838db36 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 24 Jan 2025 17:06:43 +0000 Subject: [PATCH] mooooaaaaaaaaaaar --- .../Core/Generators/RagfairAssortGenerator.cs | 164 ++- .../Core/Generators/RagfairOfferGenerator.cs | 959 ++++++++++++++---- Libraries/Core/Helpers/HandbookHelper.cs | 12 +- Libraries/Core/Helpers/RagfairServerHelper.cs | 257 +++-- .../Core/Models/Eft/Common/Tables/Trader.cs | 2 +- .../Core/Models/Eft/Ragfair/RagfairOffer.cs | 6 +- .../Models/Spt/Ragfair/TplWithFleaPrice.cs | 2 +- Libraries/Core/Services/FenceService.cs | 8 +- Libraries/Core/Services/PaymentService.cs | 4 +- Libraries/Core/Utils/RagfairOfferHolder.cs | 6 +- Libraries/Core/Utils/RandomUtil.cs | 1 + .../SptAssets/Assets/configs/ragfair.json | 3 +- 12 files changed, 1132 insertions(+), 292 deletions(-) diff --git a/Libraries/Core/Generators/RagfairAssortGenerator.cs b/Libraries/Core/Generators/RagfairAssortGenerator.cs index e970342a..30da3261 100644 --- a/Libraries/Core/Generators/RagfairAssortGenerator.cs +++ b/Libraries/Core/Generators/RagfairAssortGenerator.cs @@ -1,59 +1,159 @@ -using SptCommon.Annotations; +using Core.Helpers; +using SptCommon.Annotations; using Core.Models.Eft.Common; using Core.Models.Eft.Common.Tables; +using Core.Models.Enums; +using Core.Models.Spt.Config; +using Core.Servers; +using Core.Services; +using Core.Utils; namespace Core.Generators; [Injectable] -public class RagfairAssortGenerator() +public class RagfairAssortGenerator( + HashUtil hashUtil, + ItemHelper itemHelper, + PresetHelper presetHelper, + SeasonalEventService seasonalEventService, + ConfigServer configServer +) { + protected List> generatedAssortItems = []; + protected RagfairConfig ragfairConfig = configServer.GetConfig(); - /// - /// Get an array of arrays that can be sold on the flea - /// Each sub array contains item + children (if any) - /// - /// List of Lists + protected List ragfairItemInvalidBaseTypes = + [ + BaseClasses.LOOT_CONTAINER, // Safe, barrel cache etc + BaseClasses.STASH, // Player inventory stash + BaseClasses.SORTING_TABLE, + BaseClasses.INVENTORY, + BaseClasses.STATIONARY_CONTAINER, + BaseClasses.POCKETS, + BaseClasses.BUILT_IN_INSERTS, + ]; + + /** + * Get an array of arrays that can be sold on the flea + * Each sub array contains item + children (if any) + * @returns array of arrays + */ public List> GetAssortItems() { - throw new NotImplementedException(); + if (!AssortsAreGenerated()) + { + generatedAssortItems = GenerateRagfairAssortItems(); + } + + return generatedAssortItems; } - /// - /// Check internal generatedAssortItems array has objects - /// - /// true if array has objects + /** + * Check internal generatedAssortItems array has objects + * @returns true if array has objects + */ protected bool AssortsAreGenerated() { - throw new NotImplementedException(); + return generatedAssortItems.Count > 0; } - /// - /// Generate an array of arrays (item + children) the flea can sell - /// - /// List of Lists (item + children) + /** + * Generate an array of arrays (item + children) the flea can sell + * @returns array of arrays (item + children) + */ protected List> GenerateRagfairAssortItems() { - throw new NotImplementedException(); + List> results = []; + + /** Get cloned items from db */ + var dbItemsClone = itemHelper.GetItems().Where((item) => item.Type != "Node"); + + /** Store processed preset tpls so we dont add them when procesing non-preset items */ + List processedArmorItems = []; + var seasonalEventActive = seasonalEventService.SeasonalEventEnabled(); + var seasonalItemTplBlacklist = seasonalEventService.GetInactiveSeasonalEventItems(); + + var presets = GetPresetsToAdd(); + foreach (var preset in presets) + { + // Update Ids and clone + var presetAndMods = itemHelper.ReplaceIDs(preset.Items); + itemHelper.RemapRootItemId(presetAndMods); + + // Add presets base item tpl to the processed list so its skipped later on when processing items + processedArmorItems.Add(preset.Items[0].Template); + + presetAndMods[0].ParentId = "hideout"; + presetAndMods[0].SlotId = "hideout"; + presetAndMods[0].Upd = new Upd() + { StackObjectsCount = 99999999, UnlimitedCount = true, SptPresetId = preset.Id }; + + results.Add(presetAndMods); + } + + foreach (var item in dbItemsClone) + { + if (!itemHelper.IsValidItem(item.Id, ragfairItemInvalidBaseTypes)) + { + continue; + } + + // Skip seasonal items when not in-season + if ( + ragfairConfig.Dynamic.RemoveSeasonalItemsWhenNotInEvent && + !seasonalEventActive && + seasonalItemTplBlacklist.Contains(item.Id) + ) + { + continue; + } + + if (processedArmorItems.Contains(item.Id)) + { + // Already processed + continue; + } + + var ragfairAssort = CreateRagfairAssortRootItem( + item.Id, + item.Id + ); // tplid and id must be the same so hideout recipe rewards work + + results.Add([ragfairAssort]); + } + + return results; } - /// - /// Get presets from globals to add to flea - /// ragfairConfig.dynamic.showDefaultPresetsOnly decides if its all presets or just defaults - /// - /// Dictionary + /** + * Get presets from globals to add to flea + * ragfairConfig.dynamic.showDefaultPresetsOnly decides if its all presets or just defaults + * @returns IPreset array + */ protected List GetPresetsToAdd() { - throw new NotImplementedException(); + return ragfairConfig.Dynamic.ShowDefaultPresetsOnly + ? presetHelper.GetDefaultPresets().Values.ToList() + : presetHelper.GetAllPresets(); } - /// - /// Create a base assort item and return it with populated values + 999999 stack count + unlimited count = true - /// - /// tplid to add to item - /// id to add to item - /// Hydrated Item object - protected Item CreateRagfairAssortRootItem(string tplId, string id_checkTodoComment) // TODO: string id = this.hashUtil.generate() + /** + * Create a base assort item and return it with populated values + 999999 stack count + unlimited count = true + * @param tplId tplid to add to item + * @param id id to add to item + * @returns Hydrated Item object + */ + protected Item CreateRagfairAssortRootItem(string tplId, string? id = null) { - throw new NotImplementedException(); + if (string.IsNullOrEmpty(id)) + id = hashUtil.Generate(); + return new Item() + { + Id = id, + Template = tplId, + ParentId = "hideout", + SlotId = "hideout", + Upd = new Upd() { StackObjectsCount = 99999999, UnlimitedCount = true } + }; } } diff --git a/Libraries/Core/Generators/RagfairOfferGenerator.cs b/Libraries/Core/Generators/RagfairOfferGenerator.cs index e08fafcc..00ab6b28 100644 --- a/Libraries/Core/Generators/RagfairOfferGenerator.cs +++ b/Libraries/Core/Generators/RagfairOfferGenerator.cs @@ -1,208 +1,624 @@ -using SptCommon.Annotations; +using System.Runtime.InteropServices.JavaScript; +using Core.Helpers; +using SptCommon.Annotations; using Core.Models.Eft.Common.Tables; using Core.Models.Eft.Ragfair; +using Core.Models.Enums; using Core.Models.Spt.Config; using Core.Models.Spt.Ragfair; +using Core.Models.Utils; +using Core.Servers; +using Core.Services; +using Core.Utils; +using Core.Utils.Cloners; +using Server; +using SptCommon.Extensions; namespace Core.Generators; [Injectable] -public class RagfairOfferGenerator() +public class RagfairOfferGenerator( + ISptLogger logger, + HashUtil hashUtil, + RandomUtil randomUtil, + TimeUtil timeUtil, + DatabaseService databaseService, + RagfairServerHelper ragfairServerHelper, + ProfileHelper profileHelper, + HandbookHelper handbookHelper, + BotHelper botHelper, + SaveServer saveServer, + PresetHelper presetHelper, + RagfairAssortGenerator ragfairAssortGenerator, + RagfairOfferService ragfairOfferService, + RagfairPriceService ragfairPriceService, + LocalisationService localisationService, + PaymentHelper paymentHelper, + FenceService fenceService, + ItemHelper itemHelper, + ConfigServer configServer, + ICloner cloner +) { + protected RagfairConfig ragfairConfig = configServer.GetConfig(); + protected TraderConfig traderConfig = configServer.GetConfig(); + protected BotConfig botConfig = configServer.GetConfig(); + protected List? allowedFleaPriceItemsForBarter; - /// - /// Create a flea offer and store it in the Ragfair server offers list - /// - /// Owner of the offer - /// Time offer is listed at - /// Items in the offer - /// Cost of item (currency or barter) - /// Loyalty level needed to buy item - /// Flags sellInOnePiece to be true - /// Created flea offer + /** Internal counter to ensure each offer created has a unique value for its intId property */ + protected int offerCounter = 0; + + /** + * Create a flea offer and store it in the Ragfair server offers array + * @param userID Owner of the offer + * @param time Time offer is listed at + * @param items Items in the offer + * @param barterScheme Cost of item (currency or barter) + * @param loyalLevel Loyalty level needed to buy item + * @param sellInOnePiece Flags sellInOnePiece to be true + * @returns Created flea offer + */ public RagfairOffer CreateAndAddFleaOffer( string userID, - double time, + long time, List items, List barterScheme, int loyalLevel, - bool sellInOnePiece = false) + bool sellInOnePiece = false + ) { - throw new NotImplementedException(); + var offer = CreateOffer(userID, time, items, barterScheme, loyalLevel, sellInOnePiece); + ragfairOfferService.AddOffer(offer); + + return offer; } - /// - /// Create an offer object ready to send to ragfairOfferService.addOffer() - /// - /// Owner of the offer - /// Time offer is listed at - /// Items in the offer - /// Cost of item (currency or barter) - /// Loyalty level needed to buy item - /// Is offer being created flagged as a pack - /// RagfairOffer + /** + * Create an offer object ready to send to ragfairOfferService.addOffer() + * @param userID Owner of the offer + * @param time Time offer is listed at + * @param items Items in the offer + * @param barterScheme Cost of item (currency or barter) + * @param loyalLevel Loyalty level needed to buy item + * @param isPackOffer Is offer being created flaged as a pack + * @returns IRagfairOffer + */ protected RagfairOffer CreateOffer( string userID, - double time, + long time, List items, List barterScheme, int loyalLevel, - bool isPackOffer = false) + bool isPackOffer = false + ) { - throw new NotImplementedException(); + var isTrader = ragfairServerHelper.IsTrader(userID); + + var offerRequirements = barterScheme.Select((barter) => { + var offerRequirement = new OfferRequirement(){ + Template = barter.Template, + Count = Math.Round((double) barter.Count, 2), + OnlyFunctional = barter.OnlyFunctional ?? false, + }; + + // Dogtags define level and side + if (barter.Level != null) { + offerRequirement.Level = barter.Level; + offerRequirement.Side = barter.Side; + } + + return offerRequirement; + }).ToList(); + + // Clone to avoid modifying original array + var itemsClone = cloner.Clone(items); + 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 + if (itemHelper.IsOfBaseclass(itemsClone[0].Template, BaseClasses.AMMO_BOX) && itemsClone.Count == 1) { + itemHelper.AddCartridgesToAmmoBox(itemsClone, itemHelper.GetItem(items[0].Template).Value); + } + + var roubleListingPrice = Math.Round((double) ConvertOfferRequirementsIntoRoubles(offerRequirements)); + var singleItemListingPrice = isPackOffer ? roubleListingPrice / itemStackCount : roubleListingPrice; + + var offer = new RagfairOffer() { + Id= hashUtil.Generate(), + InternalId= offerCounter, + User= CreateUserDataForFleaOffer(userID, isTrader), + Root= items[0].Id, + Items= itemsClone, + ItemsCost= Math.Round((double) handbookHelper.GetTemplatePrice(items[0].Template)), // Handbook price + Requirements= offerRequirements, + RequirementsCost= Math.Round((double) singleItemListingPrice), + SummaryCost= roubleListingPrice, + StartTime= time, + EndTime= GetOfferEndTime(userID, time), + LoyaltyLevel= loyalLevel, + SellInOnePiece= isPackOffer, + Locked= false, + }; + + offerCounter++; + + return offer; } - /// - /// Create the user object stored inside each flea offer object - /// - /// user creating the offer - /// Is the user creating the offer a trader - /// RagfairOfferUser + /** + * Create the user object stored inside each flea offer object + * @param userID user creating the offer + * @param isTrader Is the user creating the offer a trader + * @returns IRagfairOfferUser + */ protected RagfairOfferUser CreateUserDataForFleaOffer(string userID, bool isTrader) { - throw new NotImplementedException(); + // Trader offer + if (isTrader) { + return new RagfairOfferUser(){ + Id = userID, + MemberType = MemberCategory.TRADER + }; + } + + var isPlayerOffer = profileHelper.IsPlayer(userID); + if (isPlayerOffer) { + var playerProfile = profileHelper.GetPmcProfile(userID); + return new RagfairOfferUser { + Id= playerProfile.Id, + 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, + }; + } + + // Fake pmc offer + return new RagfairOfferUser(){ + Id= userID, + MemberType= MemberCategory.Default, + Nickname= botHelper.GetPmcNicknameOfMaxLength(botConfig.BotNameLengthLimit), + Rating= randomUtil.GetDouble( + (double) ragfairConfig.Dynamic.Rating.Min, + (double) ragfairConfig.Dynamic.Rating.Max + ), + IsRatingGrowing= randomUtil.GetBool(), + Avatar= null, + Aid= hashUtil.GenerateAccountId(), + }; } - /// - /// Calculate the offer price that's listed on the flea listing - /// - /// barter requirements for offer - /// rouble cost of offer - protected decimal ConvertOfferRequirementsIntoRoubles(List offerRequirements) - { - throw new NotImplementedException(); + /** + * Calculate the offer price that's listed on the flea listing + * @param offerRequirements barter requirements for offer + * @returns rouble cost of offer + */ + protected int ConvertOfferRequirementsIntoRoubles(List offerRequirements) { + var roublePrice = 0; + foreach (var requirement in offerRequirements) { + roublePrice += (int) (paymentHelper.IsMoneyTpl(requirement.Template) + ? Math.Round((double) CalculateRoublePrice((int) requirement.Count, requirement.Template)) + : ragfairPriceService.GetFleaPriceForItem(requirement.Template) * requirement.Count); // get flea price for barter offer items + } + + return roublePrice; } - /// - /// Get avatar url from trader table in db - /// - /// Is user we're getting avatar for a trader - /// persons id to get avatar of - /// url of avatar + /** + * Get avatar url from trader table in db + * @param isTrader Is user we're getting avatar for a trader + * @param userId persons id to get avatar of + * @returns url of avatar + */ protected string GetAvatarUrl(bool isTrader, string userId) { - throw new NotImplementedException(); + if (isTrader) { + return databaseService.GetTrader(userId).Base.Avatar; + } + + return "/files/trader/avatar/unknown.jpg"; } - /// - /// Convert a count of currency into roubles - /// - /// amount of currency to convert into roubles - /// Type of currency (euro/dollar/rouble) - /// count of roubles - protected double CalculateRoublePrice(decimal currencyCount, string currencyType) + /** + * Convert a count of currency into roubles + * @param currencyCount amount of currency to convert into roubles + * @param currencyType Type of currency (euro/dollar/rouble) + * @returns count of roubles + */ + protected int CalculateRoublePrice(int currencyCount, string currencyType) { - throw new NotImplementedException(); + if (currencyType == Money.ROUBLES) { + return currencyCount; + } + + return handbookHelper.InRUB(currencyCount, currencyType); } - /// - /// Check userId, if its a player, return their pmc _id, otherwise return userId parameter - /// - /// Users Id to check - /// Users Id + /** + * Check userId, if its a player, return their pmc _id, otherwise return userId parameter + * @param userId Users Id to check + * @returns Users Id + */ protected string GetTraderId(string userId) { - throw new NotImplementedException(); + if (profileHelper.IsPlayer(userId)) { + return saveServer.GetProfile(userId).CharacterData.PmcData.Id; + } + + return userId; } - /// - /// Get a flea trading rating for the passed in user - /// - /// User to get flea rating of - /// Flea rating value - protected double GetRating(string userId) + /** + * Get a flea trading rating for the passed in user + * @param userId User to get flea rating of + * @returns Flea rating value + */ + protected double? GetRating(string userId) { - throw new NotImplementedException(); + if (profileHelper.IsPlayer(userId)) { + // Player offer + return saveServer.GetProfile(userId).CharacterData?.PmcData?.RagfairInfo?.Rating; + } + + if (ragfairServerHelper.IsTrader(userId)) { + // Trader offer + return 1; + } + + // Generated pmc offer + return randomUtil.GetDouble((double) ragfairConfig.Dynamic.Rating.Min, (double) ragfairConfig.Dynamic.Rating.Max); } - /// - /// Is the offers user rating growing - /// - /// user to check rating of - /// true if its growing + /** + * Is the offers user rating growing + * @param userID user to check rating of + * @returns true if its growing + */ protected bool GetRatingGrowing(string userID) { - throw new NotImplementedException(); + if (profileHelper.IsPlayer(userID)) + { + // player offer + return saveServer.GetProfile(userID).CharacterData?.PmcData?.RagfairInfo?.IsRatingGrowing ?? false; + } + + if (ragfairServerHelper.IsTrader(userID)) { + // trader offer + return true; + } + + // generated offer + // 50/50 growing/falling + return randomUtil.GetBool(); } - /// - /// Get number of section until offer should expire - /// - /// Id of the offer owner - /// Time the offer is posted - /// number of seconds until offer expires - protected double GetOfferEndTime(string userID, double time) + /** + * Get number of section until offer should expire + * @param userID Id of the offer owner + * @param time Time the offer is posted + * @returns number of seconds until offer expires + */ + protected long GetOfferEndTime(string userID, long time) { - throw new NotImplementedException(); + if (profileHelper.IsPlayer(userID)) { + // Player offer = current time + offerDurationTimeInHour; + var offerDurationTimeHours = databaseService.GetGlobals().Configuration.RagFair.OfferDurationTimeInHour; + return (long) (timeUtil.GetTimeStamp() + Math.Round((double) offerDurationTimeHours * TimeUtil.OneHourAsSeconds)); + } + + if (ragfairServerHelper.IsTrader(userID)) { + // Trader offer + return (long) databaseService.GetTrader(userID).Base.NextResupply; + } + + // Generated fake-player offer + return (long) Math.Round((double) (time + randomUtil.GetInt((int) ragfairConfig.Dynamic.EndTimeSeconds.Min, (int) ragfairConfig.Dynamic.EndTimeSeconds.Max))); } - /// - /// Create multiple offers for items by using a unique list of items we've generated previously - /// - /// optional, expired offers to regenerate - public void GenerateDynamicOffers(List> expiredOffers = null) + /** + * Create multiple offers for items by using a unique list of items we've generated previously + * @param expiredOffers optional, expired offers to regenerate + */ + public void GenerateDynamicOffers(List>? expiredOffers = null) { - Console.WriteLine($"actually implement me plz: owo: GenerateDynamicOffers"); + var replacingExpiredOffers = (expiredOffers?.Count ?? 0) > 0; + + // get assort items from param if they exist, otherwise grab freshly generated assorts + var assortItemsToProcess = replacingExpiredOffers + ? expiredOffers ?? [] + : ragfairAssortGenerator.GetAssortItems(); + + + assortItemsToProcess.ForEach(assortItemWithChildren => CreateOffersFromAssort(assortItemWithChildren, replacingExpiredOffers, ragfairConfig.Dynamic)); + } - /// - /// - /// Item with its children to process into offers - /// is an expired offer - /// Ragfair dynamic config - protected void CreateOffersFromAssort(List assortItemWithChildren, bool isExpiredOffer, Dynamic config) + /** + * @param assortItemWithChildren Item with its children to process into offers + * @param isExpiredOffer is an expired offer + * @param config Ragfair dynamic config + */ + protected void CreateOffersFromAssort( + List assortItemWithChildren, + bool isExpiredOffer, + Dynamic config + ) { - throw new NotImplementedException(); + var itemToSellDetails = itemHelper.GetItem(assortItemWithChildren[0].Template); + var isPreset = presetHelper.IsPreset(assortItemWithChildren[0].Upd.SptPresetId); + + // 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 + if (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 + : randomUtil.GetInt((int) config.OfferItemCount.Min, (int) config.OfferItemCount.Max); + + /* TODO: ??????? + if (ProgramStatics.DEBUG && !ProgramStatics.COMPILED) { + offerCount = 2; + } + */ + + 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(hashUtil.Generate(), clonedAssort, isPreset, itemToSellDetails.Value); + } } - /// - /// iterate over an items chidren and look for plates above desired level and remove them - /// - /// preset to check for plates - /// Settings - /// True if plate removed - protected bool RemoveBannedPlatesFromPreset(List presetWithChildren, ArmorPlateBlacklistSettings plateSettings) + /** + * iterate over an items chidren and look for plates above desired level and remove them + * @param presetWithChildren preset to check for plates + * @param plateSettings Settings + * @returns True if plate removed + */ + protected bool RemoveBannedPlatesFromPreset( + List presetWithChildren, + ArmorPlateBlacklistSettings plateSettings + ) { - throw new NotImplementedException(); + if (!itemHelper.ArmorItemCanHoldMods(presetWithChildren[0].Template)) { + // Cant hold armor inserts, skip + return false; + } + + var plateSlots = presetWithChildren.Where((item) => itemHelper.GetRemovablePlateSlotIds().Contains(item.SlotId?.ToLower())). 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.ToLower())) { + continue; + } + + var plateArmorLevel = plateDetails.Properties.ArmorClass ?? 0; + if (plateArmorLevel > plateSettings.MaxProtectionLevel) { + presetWithChildren.Splice(presetWithChildren.IndexOf(plateSlot), 1); + removedPlate = true; + } + } + + return removedPlate; } - /// - /// Create one flea offer for a specific item - /// - /// Id of seller - /// Item to create offer for - /// Is item a weapon preset - /// Raw db item details - /// Item array - protected async Task CreateSingleOfferForItem(string sellerId, List itemWithChildren, bool isPreset, TemplateItem itemToSellDetails) + /** + * Create one flea offer for a specific item + * @param sellerId Id of seller + * @param itemWithChildren Item to create offer for + * @param isPreset Is item a weapon preset + * @param itemToSellDetails Raw db item details + * @returns Item array + */ + protected void CreateSingleOfferForItem( + string sellerId, + List itemWithChildren, + bool isPreset, + TemplateItem itemToSellDetails + ) { - throw new NotImplementedException(); + // Set stack size to random value + itemWithChildren[0].Upd.StackObjectsCount = ragfairServerHelper.CalculateDynamicStackCount( + itemWithChildren[0].Template, + isPreset + ); + + var isBarterOffer = randomUtil.GetChance100(ragfairConfig.Dynamic.Barter.ChancePercent); + var isPackOffer = + randomUtil.GetChance100(ragfairConfig.Dynamic.Pack.ChancePercent) && + !isBarterOffer && + itemWithChildren.Count == 1 && + itemHelper.IsOfBaseclasses( + itemWithChildren[0].Template, + ragfairConfig.Dynamic.Pack.ItemTypeWhitelist + ); + + // Remove removable plates if % check passes + 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()) + ); + + foreach (var plateItem in offerItemPlatesToRemove) { + itemWithChildren.Splice(itemWithChildren.IndexOf(plateItem), 1); + } + } + } + + List barterScheme; + if (isPackOffer) { + // Set pack size + var stackSize = randomUtil.GetInt( + ragfairConfig.Dynamic.Pack.ItemCountMin, + ragfairConfig.Dynamic.Pack.ItemCountMax + ); + itemWithChildren[0].Upd.StackObjectsCount = stackSize; + + // Don't randomise pack items + barterScheme = CreateCurrencyBarterScheme(itemWithChildren, isPackOffer, stackSize); + } else if (isBarterOffer) { + // Apply randomised properties + RandomiseOfferItemUpdProperties(sellerId, itemWithChildren, itemToSellDetails); + barterScheme = CreateBarterBarterScheme(itemWithChildren, ragfairConfig.Dynamic.Barter); + if (ragfairConfig.Dynamic.Barter.MakeSingleStackOnly) { + itemWithChildren[0].Upd.StackObjectsCount = 1; + } + } else { + // Apply randomised properties + RandomiseOfferItemUpdProperties(sellerId, itemWithChildren, itemToSellDetails); + barterScheme = CreateCurrencyBarterScheme(itemWithChildren, isPackOffer); + } + + var offer = CreateAndAddFleaOffer( + sellerId, + timeUtil.GetTimeStamp(), + itemWithChildren, + barterScheme, + 1, + isPackOffer // sellAsOnePiece - pack offer + ); } - /// - /// Generate trader offers on flea using the traders assort data - /// - /// Trader to generate offers for + /** + * Generate trader offers on flea using the traders assort data + * @param traderID Trader to generate offers for + */ public void GenerateFleaOffersForTrader(string traderID) { - Console.WriteLine($"actually implement me plz: owo: GenerateFleaOffersForTrader"); + // 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; + 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, assortsClone.Items)]; + + 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 = assortsClone.BarterScheme[item.Id][0]; + var loyalLevel = assortsClone.LoyalLevelItems[item.Id]; + + var offer = CreateAndAddFleaOffer(traderID, time, items, barterSchemeItems, loyalLevel, false); + + // Refresh complete, reset flag to false + trader.Base.RefreshTraderRagfairOffers = false; + } } - /// - /// Get array of an item with its mods + condition properties (e.g durability) - /// Apply randomisation adjustments to condition if item base is found in ragfair.json/dynamic/condition - /// - /// id of owner of item - /// Item and mods, get condition of first item (only first array item is modified) - /// db details of first item + /** + * Get array of an item with its mods + condition properties (e.g durability) + * Apply randomisation adjustments to condition if item base is found in ragfair.json/dynamic/condition + * @param userID id of owner of item + * @param itemWithMods Item and mods, get condition of first item (only first array item is modified) + * @param itemDetails db details of first item + */ protected void RandomiseOfferItemUpdProperties(string userID, List itemWithMods, TemplateItem itemDetails) { - throw new NotImplementedException(); + // Add any missing properties to first item in array + AddMissingConditions(itemWithMods[0]); + + if (!(profileHelper.IsPlayer(userID) || ragfairServerHelper.IsTrader(userID))) { + var parentId = GetDynamicConditionIdForTpl(itemDetails.Id); + if (string.IsNullOrEmpty(parentId)) { + // 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].ConditionChance * 100)) { + RandomiseItemCondition(parentId, itemWithMods, itemDetails); + } + } } + /** + * Get the relevant condition id if item tpl matches in ragfair.json/condition + * @param tpl Item to look for matching condition object + * @returns condition id + */ protected string? GetDynamicConditionIdForTpl(string tpl) { - throw new NotImplementedException(); + // 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; } /** @@ -211,9 +627,80 @@ public class RagfairOfferGenerator() * @param itemWithMods Item to adjust condition details of * @param itemDetails db item details of first item in array */ - protected void RandomiseItemCondition(string conditionSettingsId, List itemWithMods, TemplateItem itemDetails) + protected void RandomiseItemCondition( + string conditionSettingsId, + List itemWithMods, + TemplateItem itemDetails + ) { - throw new NotImplementedException(); + var rootItem = itemWithMods[0]; + + var itemConditionValues = ragfairConfig.Dynamic.Condition[conditionSettingsId]; + var maxMultiplier = randomUtil.GetDouble((double) itemConditionValues.Max.Min, (double) itemConditionValues.Max.Min); + var currentMultiplier = randomUtil.GetDouble( + (double) itemConditionValues.Current.Min, + (double) 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 && item.SlotId == "mod_equipment_000"); + if (randomUtil.GetChance100(25) && visorMod != null) { + itemHelper.AddUpdObjectToItem(visorMod); + + visorMod.Upd.FaceShield = new UpdFaceShield() { Hits = randomUtil.GetInt(1, 3) }; + } + + return; + } + + // Randomise Weapons + if (itemHelper.IsOfBaseclass(itemDetails.Id, BaseClasses.WEAPON)) { + RandomiseWeaponDurability(itemWithMods[0], 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 = Math.Round((double) itemDetails.Properties.MaximumNumberOfUsage * (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 }; + } } /** @@ -223,62 +710,194 @@ public class RagfairOfferGenerator() * @param maxMultiplier Value to multiply max durability by * @param currentMultiplier Value to multiply current durability by */ - protected void RandomiseWeaponDurability(Item item, TemplateItem itemDbDetails, double maxMultiplier, double currentMultiplier) + protected void RandomiseWeaponDurability( + Item item, + TemplateItem itemDbDetails, + double maxMultiplier, + double currentMultiplier + ) { - throw new NotImplementedException(); + // Max + var baseMaxDurability = itemDbDetails.Properties.MaxDurability; + var lowestMaxDurability = randomUtil.GetDouble(maxMultiplier, 1) * baseMaxDurability; + var chosenMaxDurability = Math.Round(randomUtil.GetDouble((double) lowestMaxDurability, (double) baseMaxDurability)); + + // Current + var lowestCurrentDurability = randomUtil.GetDouble(currentMultiplier, 1) * chosenMaxDurability; + var chosenCurrentDurability = Math.Round(randomUtil.GetDouble(lowestCurrentDurability, chosenMaxDurability)); + + item.Upd.Repairable.Durability = chosenCurrentDurability == 0 ? 1D : chosenCurrentDurability; // Never var value become 0 + item.Upd.Repairable.MaxDurability = chosenMaxDurability; } /** - * Randomise the durability values for an armors plates and soft inserts + * Randomise the durabiltiy values for an armors plates and soft inserts * @param armorWithMods Armor item with its child mods - * @param currentMultiplier Chosen multiplier to use for current durability value - * @param maxMultiplier Chosen multiplier to use for max durability value + * @param currentMultiplier Chosen multipler to use for current durability value + * @param maxMultiplier Chosen multipler to use for max durability value */ - protected void RandomiseArmorDurabilityValues(List armorWithMods, double currentMultiplier, double maxMultiplier) + protected void RandomiseArmorDurabilityValues( + List armorWithMods, + double currentMultiplier, + double maxMultiplier + ) { - throw new NotImplementedException(); + foreach (var armorItem in armorWithMods) { + var itemDbDetails = itemHelper.GetItem(armorItem.Template).Value; + if (itemDbDetails.Properties.ArmorClass > 1) { + itemHelper.AddUpdObjectToItem(armorItem); + + var baseMaxDurability = itemDbDetails.Properties.MaxDurability; + var lowestMaxDurability = randomUtil.GetDouble(maxMultiplier, 1) * baseMaxDurability; + var chosenMaxDurability = Math.Round(randomUtil.GetDouble((double) lowestMaxDurability,(double) baseMaxDurability)); + + var lowestCurrentDurability = randomUtil.GetDouble(currentMultiplier, 1) * chosenMaxDurability; + var chosenCurrentDurability = Math.Round(randomUtil.GetDouble(lowestCurrentDurability, chosenMaxDurability)); + + armorItem.Upd.Repairable = new UpdRepairable() { + Durability = chosenCurrentDurability == 0D ? 1D : chosenCurrentDurability, // Never var value become 0 + MaxDurability = chosenMaxDurability + }; + } + } } - /// - /// Add missing conditions to an item if needed - /// Durability for repairable items - /// HpResource for medical items - /// - /// item to add conditions to - protected void AddMissingConditions(Item item) - { - throw new NotImplementedException(); + /** + * Add missing conditions to an item if needed + * Durabiltiy for repairable items + * HpResource for medical items + * @param item item to add conditions to + */ + protected void AddMissingConditions(Item item) { + var props = itemHelper.GetItem(item.Template).Value.Properties; + var isRepairable = props.Durability != null; + var isMedkit = props.MaxHpResource != null; + var isKey = props.MaximumNumberOfUsage != null; + var isConsumable = props.MaxResource > 1 && props.FoodUseTime != null; + var isRepairKit = props.MaxRepairResource != null; + + if (isRepairable && props.Durability > 0) { + item.Upd.Repairable = new UpdRepairable() + { Durability = props.Durability, MaxDurability = props.Durability }; + + return; + } + + if (isMedkit && props.MaxHpResource > 0) + { + item.Upd.MedKit = new UpdMedKit() { HpResource = props.MaxHpResource }; + + return; + } + + if (isKey) { + item.Upd.Key = new UpdKey(){ NumberOfUsages = 0 }; + + return; + } + + // Food/drink + if (isConsumable) { + item.Upd.FoodDrink = new UpdFoodDrink() { HpPercent = props.MaxResource }; + + return; + } + + if (isRepairKit) { + item.Upd.RepairKit = new UpdRepairKit() { Resource = props.MaxRepairResource }; + } } - /// - /// Create a barter-based barter scheme, if not possible, fall back to making barter scheme currency based - /// - /// Items for sale in offer - /// Barter config from ragfairConfig.dynamic.barter - /// Barter scheme + /** + * Create a barter-based barter scheme, if not possible, fall back to making barter scheme currency based + * @param offerItems Items for sale in offer + * @param barterConfig Barter config from ragfairConfig.Dynamic.barter + * @returns Barter scheme + */ protected List CreateBarterBarterScheme(List offerItems, BarterDetails barterConfig) { - throw new NotImplementedException(); + // Get flea price of item being sold + var priceOfOfferItem = ragfairPriceService.GetDynamicOfferPriceForOffer( + offerItems, + Money.ROUBLES, + false + ); + + // Dont 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 itemsInsidePriceBounds = itemFleaPrices.Where( + itemAndPrice => + itemAndPrice.Price >= desiredItemCostRouble - offerCostVarianceRoubles && + itemAndPrice.Price <= desiredItemCostRouble + offerCostVarianceRoubles && + itemAndPrice.Tpl != offerItems[0].Template // Don't allow the item being sold to be chosen + ).ToList(); + + // No items on flea have a matching price, fall back to currency + if (itemsInsidePriceBounds.Count == 0) { + return CreateCurrencyBarterScheme(offerItems, false); + } + + // Choose random item from price-filtered flea items + var randomItem = randomUtil.GetArrayValue(itemsInsidePriceBounds); + + return [new BarterScheme() { Count = barterItemCount, Template = randomItem.Tpl }]; } - /// - /// Get a list of flea prices + item tpl, cached in generator class inside `allowedFleaPriceItemsForBarter` - /// - /// list with tpl/price values - protected List GetFleaPricesAsList() + /** + * Get an array of flea prices + item tpl, cached in generator class inside `allowedFleaPriceItemsForBarter` + * @returns array with tpl/price values + */ + protected List GetFleaPricesAsArray() { - throw new NotImplementedException(); + // Generate if needed + if (allowedFleaPriceItemsForBarter == null) { + var fleaPrices = databaseService.GetPrices(); + + // Only get prices for items that also exist in items.json + var filteredFleaItems = fleaPrices + .Select(kvTpl => new TplWithFleaPrice { Tpl = kvTpl.Key, Price = kvTpl.Value }) + .Where(item => itemHelper.GetItem(item.Tpl).Key); + + var itemTypeBlacklist = ragfairConfig.Dynamic.Barter.ItemTypeBlacklist; + allowedFleaPriceItemsForBarter = filteredFleaItems.Where(item => !itemHelper.IsOfBaseclasses(item.Tpl, itemTypeBlacklist)).ToList(); + } + + return allowedFleaPriceItemsForBarter; } - /// - /// Create a random currency-based barter scheme for a list of items - /// - /// Items on offer - /// Is the barter scheme being created for a pack offer - /// What to multiply the resulting price by - /// Barter scheme for offer - protected List CreateCurrencyBarterScheme(List offerWithChildren, bool isPackOffer, double multipler = 1) + /** + * Create a random currency-based barter scheme for an array of items + * @param offerWithChildren Items on offer + * @param isPackOffer Is the barter scheme being created for a pack offer + * @param multipler What to multiply the resulting price by + * @returns Barter scheme for offer + */ + protected List CreateCurrencyBarterScheme( + List offerWithChildren, + bool isPackOffer, + double multipler = 1 + ) { - throw new NotImplementedException(); + var currency = ragfairServerHelper.GetDynamicOfferCurrency(); + var price = ragfairPriceService.GetDynamicOfferPriceForOffer(offerWithChildren, currency, isPackOffer) * multipler; + + return [new BarterScheme() { Count = price, Template = currency }]; } } + diff --git a/Libraries/Core/Helpers/HandbookHelper.cs b/Libraries/Core/Helpers/HandbookHelper.cs index c4fd35c0..41ee11f5 100644 --- a/Libraries/Core/Helpers/HandbookHelper.cs +++ b/Libraries/Core/Helpers/HandbookHelper.cs @@ -154,11 +154,11 @@ public class HandbookHelper( /// Currency count to convert /// What current currency is /// Count in roubles - public double InRUB(double nonRoubleCurrencyCount, string currencyTypeFrom) + public int InRUB(double nonRoubleCurrencyCount, string currencyTypeFrom) { - return currencyTypeFrom == Money.ROUBLES + return (int) (currencyTypeFrom == Money.ROUBLES ? nonRoubleCurrencyCount - : Math.Round(nonRoubleCurrencyCount * (GetTemplatePrice(currencyTypeFrom) ?? 0)); + : Math.Round(nonRoubleCurrencyCount * (GetTemplatePrice(currencyTypeFrom) ?? 0))); } /// @@ -167,15 +167,15 @@ public class HandbookHelper( /// roubles to convert /// Currency to convert roubles into /// currency count in desired type - public double FromRUB(double roubleCurrencyCount, string currencyTypeTo) + public int FromRUB(double roubleCurrencyCount, string currencyTypeTo) { if (currencyTypeTo == Money.ROUBLES) { - return roubleCurrencyCount; + return (int) roubleCurrencyCount; } // Get price of currency from handbook var price = GetTemplatePrice(currencyTypeTo); - return price is not null ? Math.Max(1, Math.Round((double)(roubleCurrencyCount / price))) : 0; + return (int) (price is not null ? Math.Max(1, Math.Round((double)(roubleCurrencyCount / price))) : 0); } public HandbookCategory GetCategoryById(string handbookId) diff --git a/Libraries/Core/Helpers/RagfairServerHelper.cs b/Libraries/Core/Helpers/RagfairServerHelper.cs index 237c4978..d258cb21 100644 --- a/Libraries/Core/Helpers/RagfairServerHelper.cs +++ b/Libraries/Core/Helpers/RagfairServerHelper.cs @@ -1,92 +1,215 @@ -using SptCommon.Annotations; +using System.Runtime.InteropServices.JavaScript; +using SptCommon.Annotations; using Core.Models.Eft.Common.Tables; +using Core.Models.Enums; +using Core.Models.Spt.Config; +using Core.Models.Utils; +using Core.Servers; +using Core.Services; +using Core.Utils; +using Core.Utils.Cloners; namespace Core.Helpers; [Injectable] -public class RagfairServerHelper +public class RagfairServerHelper( + ISptLogger logger, + RandomUtil randomUtil, + TimeUtil timeUtil, + SaveServer saveServer, + DatabaseService databaseService, + ProfileHelper profileHelper, + ItemHelper itemHelper, + TraderHelper traderHelper, + MailSendService mailSendService, + LocalisationService localisationService, + ItemFilterService itemFilterService, + ConfigServer configServer, + ICloner cloner +) { - /// - /// Is item valid / on blacklist / quest item - /// - /// - /// boolean - public bool IsItemValidRagfairItem(bool[] itemDetails) + protected RagfairConfig ragfairConfig = configServer.GetConfig(); + protected QuestConfig questConfig = configServer.GetConfig(); + protected static string goodsReturnedTemplate = "5bdabfe486f7743e1665df6e 0"; // Your item was not sold + + /** + * Is item valid / on blacklist / quest item + * @param itemDetails + * @returns boolean + */ + public bool IsItemValidRagfairItem(KeyValuePair itemDetails) { - throw new NotImplementedException(); + var blacklistConfig = ragfairConfig.Dynamic.Blacklist; + + // Skip invalid items + if (!itemDetails.Key) { + return false; + } + + if (!itemHelper.IsValidItem(itemDetails.Value.Id)) { + return false; + } + + // Skip bsg blacklisted items + if (blacklistConfig.EnableBsgList && !(itemDetails.Value?.Properties?.CanSellOnRagfair ?? false)) { + return false; + } + + // Skip custom blacklisted items and flag as unsellable by players + if (IsItemOnCustomFleaBlacklist(itemDetails.Value.Id)) { + itemDetails.Value.Properties.CanSellOnRagfair = false; + + return false; + } + + // Skip custom category blacklisted items + if ( + blacklistConfig.EnableCustomItemCategoryList && + IsItemCategoryOnCustomFleaBlacklist(itemDetails.Value.Parent) + ) { + return false; + } + + // Skip quest items + if (blacklistConfig.EnableQuestList && itemHelper.IsQuestItem(itemDetails.Value.Id)) { + return false; + } + + // Don't include damaged ammo packs + if ( + ragfairConfig.Dynamic.Blacklist.DamagedAmmoPacks && + itemDetails.Value.Parent == BaseClasses.AMMO_BOX && + itemDetails[1]._name.includes("_damaged") + ) { + return false; + } + + return true; } - /// - /// Is supplied item tpl on the ragfair custom blacklist from configs/ragfair.json/dynamic - /// - /// Item tpl to check is blacklisted - /// True if its blacklsited - protected bool IsItemOnCustomFleaBlacklist(string itemTemplateId) - { - throw new NotImplementedException(); + /** + * Is supplied item tpl on the ragfair custom blacklist from configs/ragfair.json/dynamic + * @param itemTemplateId Item tpl to check is blacklisted + * @returns True if its blacklsited + */ + protected isItemOnCustomFleaBlacklist(itemTemplateId: string): boolean { + return ragfairConfig.dynamic.blacklist.custom.includes(itemTemplateId); } - /// - /// Is supplied parent id on the ragfair custom item category blacklist - /// - /// Parent Id to check is blacklisted - /// true if blacklisted - protected bool IsItemCategoryOnCustomFleaBlacklist(string itemParentId) - { - throw new NotImplementedException(); + /** + * Is supplied parent id on the ragfair custom item category blacklist + * @param parentId Parent Id to check is blacklisted + * @returns true if blacklisted + */ + protected isItemCategoryOnCustomFleaBlacklist(itemParentId: string): boolean { + return ragfairConfig.dynamic.blacklist.customItemCategoryList.includes(itemParentId); } - /// - /// is supplied id a trader - /// - /// - /// True if id was a trader - public bool IsTrader(string traderId) - { - throw new NotImplementedException(); + /** + * is supplied id a trader + * @param traderId + * @returns True if id was a trader + */ + public isTrader(traderId: string): boolean { + return traderId in databaseService.getTraders(); } - /// - /// Send items back to player - /// - /// Player to send items to - /// Items to send to player - public void ReturnItems(string sessionID, List returnedItems) - { - throw new NotImplementedException(); + /** + * Send items back to player + * @param sessionID Player to send items to + * @param returnedItems Items to send to player + */ + public returnItems(sessionID: string, returnedItems: IItem[]): void { + mailSendService.sendLocalisedNpcMessageToPlayer( + sessionID, + traderHelper.getTraderById(Traders.RAGMAN), + MessageType.MESSAGE_WITH_ITEMS, + RagfairServerHelper.goodsReturnedTemplate, + returnedItems, + timeUtil.getHoursAsSeconds( + databaseService.getGlobals().config.RagFair.yourOfferDidNotSellMaxStorageTimeInHour, + ), + ); } - public int CalculateDynamicStackCount(string tplId, bool isWeaponPreset) - { - throw new NotImplementedException(); + public calculateDynamicStackCount(tplId: string, isWeaponPreset: boolean): number { + var config = ragfairConfig.dynamic; + + // Lookup item details - check if item not found + var itemDetails = itemHelper.getItem(tplId); + if (!itemDetails[0]) { + throw new JSType.Error( + localisationService.getText( + "ragfair-item_not_in_db_unable_to_generate_dynamic_stack_count", + tplId, + ), + ); + } + + // Item Types to return one of + if ( + isWeaponPreset || + itemHelper.isOfBaseclasses(itemDetails[1]._id, ragfairConfig.dynamic.showAsSingleStack) + ) { + return 1; + } + + // Get max stack count + var maxStackCount = itemDetails[1]._props.StackMaxSize; + + // non-stackable - use different values to calculate stack size + if (!maxStackCount || maxStackCount === 1) { + return Math.round(randomUtil.getInt(config.nonStackableCount.min, config.nonStackableCount.max)); + } + + var stackPercent = Math.round( + randomUtil.getInt(config.stackablePercent.min, config.stackablePercent.max), + ); + + return Math.round((maxStackCount / 100) * stackPercent); } - /// - /// Choose a currency at random with bias - /// - /// currency tpl - public string GetDynamicOfferCurrency() - { - throw new NotImplementedException(); + /** + * Choose a currency at random with bias + * @returns currency tpl + */ + public getDynamicOfferCurrency(): string { + var currencies = ragfairConfig.dynamic.currencies; + var bias: string[] = []; + + for (var item in currencies) { + for (let i = 0; i < currencies[item]; i++) { + bias.push(item); + } + } + + return bias[Math.floor(Math.random() * bias.length)]; } - /// - /// Given a preset id from globals.json, return a list of items with unique ids - /// - /// Preset item - /// List of weapon and its children - public List GetPresetItems(Item item) - { - throw new NotImplementedException(); + /** + * Given a preset id from globals.json, return an array of items[] with unique ids + * @param item Preset item + * @returns Array of weapon and its children + */ + public getPresetItems(item: IItem): IItem[] { + var preset = cloner.clone(databaseService.getGlobals().ItemPresets[item._id]._items); + return itemHelper.reparentItemAndChildren(item, preset); } - /// - /// Possible bug, returns all items associated with an items tpl, could be multiple presets from globals.json - /// - /// Preset item - /// - public List GetPresetItemsByTpl(Item item) - { - throw new NotImplementedException(); + /** + * Possible bug, returns all items associated with an items tpl, could be multiple presets from globals.json + * @param item Preset item + * @returns + */ + public getPresetItemsByTpl(item: IItem): IItem[] { + var presets = []; + for (var itemId in databaseService.getGlobals().ItemPresets) { + if (databaseService.getGlobals().ItemPresets[itemId]._items[0]._tpl === item._tpl) { + var presetItems = cloner.clone(databaseService.getGlobals().ItemPresets[itemId]._items); + presets.push(itemHelper.reparentItemAndChildren(item, presetItems)); + } + } + + return presets; } } diff --git a/Libraries/Core/Models/Eft/Common/Tables/Trader.cs b/Libraries/Core/Models/Eft/Common/Tables/Trader.cs index 6548199f..931334f2 100644 --- a/Libraries/Core/Models/Eft/Common/Tables/Trader.cs +++ b/Libraries/Core/Models/Eft/Common/Tables/Trader.cs @@ -233,7 +233,7 @@ public record BarterScheme public bool? SptQuestLocked { get; set; } [JsonPropertyName("level")] - public double? Level { get; set; } + public int? Level { get; set; } [JsonPropertyName("side")] [JsonConverter(typeof(JsonStringEnumConverter))] diff --git a/Libraries/Core/Models/Eft/Ragfair/RagfairOffer.cs b/Libraries/Core/Models/Eft/Ragfair/RagfairOffer.cs index 71d466c8..262f3410 100644 --- a/Libraries/Core/Models/Eft/Ragfair/RagfairOffer.cs +++ b/Libraries/Core/Models/Eft/Ragfair/RagfairOffer.cs @@ -26,7 +26,7 @@ public record RagfairOffer /** Handbook price */ [JsonPropertyName("itemsCost")] - public decimal? ItemsCost { get; set; } + public double? ItemsCost { get; set; } /** Rouble price per item */ [JsonPropertyName("requirementsCost")] @@ -72,7 +72,7 @@ public record OfferRequirement public string? Template { get; set; } [JsonPropertyName("count")] - public int? Count { get; set; } + public double? Count { get; set; } [JsonPropertyName("onlyFunctional")] public bool? OnlyFunctional { get; set; } @@ -93,7 +93,7 @@ public record RagfairOfferUser public string? Nickname { get; set; } [JsonPropertyName("rating")] - public decimal? Rating { get; set; } + public double? Rating { get; set; } [JsonPropertyName("memberType")] public MemberCategory? MemberType { get; set; } diff --git a/Libraries/Core/Models/Spt/Ragfair/TplWithFleaPrice.cs b/Libraries/Core/Models/Spt/Ragfair/TplWithFleaPrice.cs index 682fac88..74dc0711 100644 --- a/Libraries/Core/Models/Spt/Ragfair/TplWithFleaPrice.cs +++ b/Libraries/Core/Models/Spt/Ragfair/TplWithFleaPrice.cs @@ -9,5 +9,5 @@ public record TplWithFleaPrice // Roubles [JsonPropertyName("price")] - public decimal? Price { get; set; } + public double? Price { get; set; } } diff --git a/Libraries/Core/Services/FenceService.cs b/Libraries/Core/Services/FenceService.cs index 292f7061..9151dd06 100644 --- a/Libraries/Core/Services/FenceService.cs +++ b/Libraries/Core/Services/FenceService.cs @@ -1222,7 +1222,7 @@ public class FenceService( foreach (var plateSlot in plateSlots) { var plateTpl = plateSlot.Props.Filters[0].Plate; - if (plateTpl == null) + if (string.IsNullOrEmpty(plateTpl)) { // Bsg data lacks a default plate, skip randomisng for this mod continue; @@ -1233,13 +1233,11 @@ public class FenceService( var modItemDbDetails = itemHelper.GetItem(plateTpl).Value; // Chance to remove plate - var plateExistsChance = - traderConfig.Fence.ChancePlateExistsInArmorPercent[ - modItemDbDetails?.Properties?.ArmorClass?.ToString() ?? "3"]; + var plateExistsChance = traderConfig.Fence.ChancePlateExistsInArmorPercent[modItemDbDetails?.Properties?.ArmorClass?.ToString() ?? "3"]; if (!randomUtil.GetChance100(plateExistsChance)) { // Remove plate from armor - armorWithMods = armorItemAndMods.Where(item => item.SlotId.ToLower() != plateSlot.Name.ToLower()) + armorItemAndMods = armorItemAndMods.Where(item => item.SlotId.ToLower() != plateSlot.Name.ToLower()) .ToList(); continue; diff --git a/Libraries/Core/Services/PaymentService.cs b/Libraries/Core/Services/PaymentService.cs index 329b2e73..ce420f98 100644 --- a/Libraries/Core/Services/PaymentService.cs +++ b/Libraries/Core/Services/PaymentService.cs @@ -175,7 +175,7 @@ public class PaymentService( if (item.Upd.StackObjectsCount < currencyMaxStackSize) { if (item.Upd.StackObjectsCount + calcAmount > currencyMaxStackSize) { // calculate difference - calcAmount -= (currencyMaxStackSize - item.Upd.StackObjectsCount) ?? 0; + calcAmount -= (int) ((currencyMaxStackSize - item.Upd.StackObjectsCount) ?? 0); item.Upd.StackObjectsCount = currencyMaxStackSize; } else { skipSendingMoneyToStash = true; @@ -195,7 +195,7 @@ public class PaymentService( Item rootCurrencyReward = new Item { Id = _hashUtil.Generate(), Template = currencyTpl, - Upd = new Upd { StackObjectsCount = Math.Round(calcAmount) }, + Upd = new Upd { StackObjectsCount = Math.Round((double) calcAmount) } }; // Ensure money is properly split to follow its max stack size limit diff --git a/Libraries/Core/Utils/RagfairOfferHolder.cs b/Libraries/Core/Utils/RagfairOfferHolder.cs index 06486c0d..48a61481 100644 --- a/Libraries/Core/Utils/RagfairOfferHolder.cs +++ b/Libraries/Core/Utils/RagfairOfferHolder.cs @@ -13,9 +13,9 @@ public class RagfairOfferHolder( ConfigServer configServer) { - protected Dictionary _offersById; - protected Dictionary> _offersByTemplate; - protected Dictionary> _offersByTrader; + protected Dictionary _offersById = new(); + protected Dictionary> _offersByTemplate = new(); + protected Dictionary> _offersByTrader = new(); protected int _maxOffersPerTemplate = (int) configServer.GetConfig().Dynamic.OfferItemCount.Max; public RagfairOffer? GetOfferById(string id) diff --git a/Libraries/Core/Utils/RandomUtil.cs b/Libraries/Core/Utils/RandomUtil.cs index 477e9913..c64c44fd 100644 --- a/Libraries/Core/Utils/RandomUtil.cs +++ b/Libraries/Core/Utils/RandomUtil.cs @@ -55,6 +55,7 @@ public class RandomUtil(ISptLogger _logger, ICloner _cloner) return (float)GetSecureRandomNumber() * (max - min) + min; } + /// /// Generates a random floating-point number within the specified range ~15-17 digits (8 bytes). /// diff --git a/Libraries/SptAssets/Assets/configs/ragfair.json b/Libraries/SptAssets/Assets/configs/ragfair.json index 999424d8..6e9bed90 100644 --- a/Libraries/SptAssets/Assets/configs/ragfair.json +++ b/Libraries/SptAssets/Assets/configs/ragfair.json @@ -27,8 +27,7 @@ "5a7c2eca46aef81a7ca2145d": true, "5ac3b934156ae10c4430e83c": true, "5c0647fdd443bc2504c2d371": true, - "6617beeaa9cfa777ca915b7c": true, - "ragfair": false + "6617beeaa9cfa777ca915b7c": true }, "dynamic": { "purchasesAreFoundInRaid": false,