From 48228c6d2ea7cb6904eca6654e27102c5e3cca74 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 24 Jan 2025 13:35:28 +0000 Subject: [PATCH] more stuff fixed --- Libraries/Core/Callbacks/RagfairCallbacks.cs | 2 +- .../Generators/FenceBaseAssortGenerator.cs | 339 ++++++++++++++++-- Libraries/Core/Generators/PMCLootGenerator.cs | 6 +- .../RepeatableQuestRewardGenerator.cs | 2 +- Libraries/Core/Helpers/ItemHelper.cs | 2 +- .../Core/Models/Eft/Ragfair/RagfairOffer.cs | 2 +- Libraries/Core/Servers/RagfairServer.cs | 213 ++++++----- .../Core/Services/RagfairOfferService.cs | 334 +++++++++++++---- Libraries/Core/Utils/RagfairOfferHolder.cs | 31 +- 9 files changed, 680 insertions(+), 251 deletions(-) diff --git a/Libraries/Core/Callbacks/RagfairCallbacks.cs b/Libraries/Core/Callbacks/RagfairCallbacks.cs index 9c0add4c..038c20c2 100644 --- a/Libraries/Core/Callbacks/RagfairCallbacks.cs +++ b/Libraries/Core/Callbacks/RagfairCallbacks.cs @@ -26,7 +26,7 @@ public class RagfairCallbacks( public Task OnLoad() { - // await _ragfairServer.Load(); + _ragfairServer.Load(); return Task.CompletedTask; } diff --git a/Libraries/Core/Generators/FenceBaseAssortGenerator.cs b/Libraries/Core/Generators/FenceBaseAssortGenerator.cs index 1af60341..19ba0be1 100644 --- a/Libraries/Core/Generators/FenceBaseAssortGenerator.cs +++ b/Libraries/Core/Generators/FenceBaseAssortGenerator.cs @@ -1,63 +1,338 @@ -using SptCommon.Annotations; +using Core.Helpers; +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; namespace Core.Generators; [Injectable] public class FenceBaseAssortGenerator( - ConfigServer _configServer + ISptLogger logger, + HashUtil hashUtil, + DatabaseService databaseService, + HandbookHelper handbookHelper, + ItemHelper itemHelper, + PresetHelper presetHelper, + ItemFilterService itemFilterService, + SeasonalEventService seasonalEventService, + LocalisationService localisationService, + ConfigServer configServer, + FenceService fenceService ) { - protected TraderConfig _traderConfig = _configServer.GetConfig(); + protected TraderConfig traderConfig = configServer.GetConfig(); - /// - /// Create base fence assorts dynamically and store in memory - /// + /** + * Create base fence assorts dynamically and store in memory + */ public void GenerateFenceBaseAssorts() { - // TODO: actually implement - return; + var blockedSeasonalItems = seasonalEventService.GetInactiveSeasonalEventItems(); + var baseFenceAssort = databaseService.GetTrader(Traders.FENCE).Assort; + + foreach (var rootItemDb in itemHelper.GetItems().Where((item) => IsValidFenceItem(item))) + { + // Skip blacklisted items + if (itemFilterService.IsItemBlacklisted(rootItemDb.Id)) + { + continue; + } + + // Skip reward item blacklist + if (itemFilterService.IsItemRewardBlacklisted(rootItemDb.Id)) + { + continue; + } + + // Invalid + if (!itemHelper.IsValidItem(rootItemDb.Id)) + { + continue; + } + + // Item base type blacklisted + if (traderConfig.Fence.Blacklist.Count > 0) + { + if (traderConfig.Fence.Blacklist.Contains(rootItemDb.Id) || + itemHelper.IsOfBaseclasses(rootItemDb.Id, traderConfig.Fence.Blacklist) + ) + { + continue; + } + } + + // Only allow rigs with no slots (carrier rigs) + if (itemHelper.IsOfBaseclass(rootItemDb.Id, BaseClasses.VEST) && + (rootItemDb.Properties?.Slots?.Count ?? 0) > 0 + ) + { + continue; + } + + // Skip seasonal event items when not in seasonal event + if (traderConfig.Fence.BlacklistSeasonalItems && blockedSeasonalItems.Contains(rootItemDb.Id)) + { + continue; + } + + // Create item object in array + var itemWithChildrenToAdd = new List + { + new() + { + Id = hashUtil.Generate(), + Template = rootItemDb.Id, + ParentId = "hideout", + SlotId = "hideout", + Upd = new Upd { StackObjectsCount = 9999999 }, + } + }; + + // Ensure ammo is not above penetration limit value + if (itemHelper.IsOfBaseclasses(rootItemDb.Id, [BaseClasses.AMMO_BOX, BaseClasses.AMMO])) + { + if (IsAmmoAbovePenetrationLimit(rootItemDb)) + { + continue; + } + } + + if (itemHelper.IsOfBaseclass(rootItemDb.Id, BaseClasses.AMMO_BOX)) + { + // Only add cartridges to box if box has no children + if (itemWithChildrenToAdd.Count == 1) + { + itemHelper.AddCartridgesToAmmoBox(itemWithChildrenToAdd, rootItemDb); + } + } + + // Ensure IDs are unique + itemHelper.RemapRootItemId(itemWithChildrenToAdd); + if (itemWithChildrenToAdd.Count > 1) + { + itemHelper.ReparentItemAndChildren(itemWithChildrenToAdd[0], itemWithChildrenToAdd); + itemWithChildrenToAdd[0].ParentId = "hideout"; + } + + // Create barter scheme (price) + var barterSchemeToAdd = new BarterScheme() + { + Count = Math.Round((double)fenceService.GetItemPrice(rootItemDb.Id, itemWithChildrenToAdd)), + Template = Money.ROUBLES + }; + + // Add barter data to base + baseFenceAssort.BarterScheme[itemWithChildrenToAdd[0].Id] = [[barterSchemeToAdd]]; + + // Add item to base + baseFenceAssort.Items.AddRange(itemWithChildrenToAdd); + + // Add loyalty data to base + baseFenceAssort.LoyalLevelItems[itemWithChildrenToAdd[0].Id] = 1; + } + + // Add all default presets to base fence assort + var defaultPresets = presetHelper.GetDefaultPresets().Values; + foreach (var defaultPreset in defaultPresets) + { + // Skip presets we've already added + if (baseFenceAssort.Items.Any((item) => item.Upd != null && item.Upd.SptPresetId == defaultPreset.Id)) + { + continue; + } + + // Construct preset + mods + var itemAndChildren = itemHelper.ReplaceIDs(defaultPreset.Items); + + // Find root item and add some properties to it + for (var i = 0; i < itemAndChildren.Count; i++) + { + var mod = itemAndChildren[i]; + + // Build root Item info + if (string.IsNullOrEmpty(mod.ParentId)) + { + mod.ParentId = "hideout"; + mod.SlotId = "hideout"; + mod.Upd = new Upd() + { + StackObjectsCount = 1, + SptPresetId = + defaultPreset.Id, // Store preset id here so we can check it later to prevent preset dupes + }; + + // Updated root item, exit loop + break; + } + } + + // Add constructed preset to assorts + baseFenceAssort.Items.AddRange(itemAndChildren); + + // Calculate preset price (root item + child items) + var price = handbookHelper.GetTemplatePriceForItems(itemAndChildren); + var itemQualityModifier = itemHelper.GetItemQualityModifierForItems(itemAndChildren); + + // Multiply weapon+mods rouble price by quality modifier + baseFenceAssort.BarterScheme[itemAndChildren[0].Id] = [[]]; + baseFenceAssort.BarterScheme[itemAndChildren[0].Id][0][0] = new BarterScheme() + { + Template = Money.ROUBLES, + Count = Math.Round(price * itemQualityModifier), + }; + + baseFenceAssort.LoyalLevelItems[itemAndChildren[0].Id] = 1; + } } - /// - /// Check ammo in boxes and loose ammos has a penetration value above the configured value in trader.json / ammoMaxPenLimit - /// - /// Ammo box or ammo item from items.db - /// True if penetration value is above limit set in config + /** + * Check ammo in boxes + loose ammos has a penetration value above the configured value in trader.json / ammoMaxPenLimit + * @param rootItemDb Ammo box or ammo item from items.db + * @returns True if penetration value is above limit set in config + */ protected bool IsAmmoAbovePenetrationLimit(TemplateItem rootItemDb) { - throw new NotImplementedException(); + var ammoPenetrationPower = GetAmmoPenetrationPower(rootItemDb); + if (ammoPenetrationPower == null) + { + logger.Warning(localisationService.GetText("fence-unable_to_get_ammo_penetration_value", rootItemDb.Id)); + return false; + } + + return ammoPenetrationPower > traderConfig.Fence.AmmoMaxPenLimit; } - /// - /// Gets the penetration power value of an ammo, works with ammo boxes and raw ammos. - /// - /// Ammo box or ammo item from items.db - /// Penetration power of passed in item, undefined if it doesnt have a power + /** + * Get the penetration power value of an ammo, works with ammo boxes and raw ammos + * @param rootItemDb Ammo box or ammo item from items.db + * @returns Penetration power of passed in item, undefined if it doesnt have a power + */ protected double? GetAmmoPenetrationPower(TemplateItem rootItemDb) { - throw new NotImplementedException(); + if (itemHelper.IsOfBaseclass(rootItemDb.Id, BaseClasses.AMMO_BOX)) + { + // Get the cartridge tpl found inside ammo box + var cartridgeTplInBox = rootItemDb.Properties.StackSlots[0].Props.Filters[0].Filter[0]; + + // Look up cartridge tpl in db + var ammoItemDb = itemHelper.GetItem(cartridgeTplInBox); + if (!ammoItemDb.Key) + { + logger.Warning(localisationService.GetText("fence-ammo_not_found_in_db", cartridgeTplInBox)); + return null; + } + + return ammoItemDb.Value.Properties.PenetrationPower; + } + + // Plain old ammo, get its pen property + if (itemHelper.IsOfBaseclass(rootItemDb.Id, BaseClasses.AMMO)) + { + return rootItemDb.Properties.PenetrationPower; + } + + // Not an ammobox or ammo + return null; } - /// - /// Add soft inserts and armor plates to an armor. - /// - /// Armor item list to add mods into. - /// Armor items db template. + /** + * Add soft inserts + armor plates to an armor + * @param armor Armor item array to add mods into + * @param itemDbDetails Armor items db template + */ protected void AddChildrenToArmorModSlots(List armor, TemplateItem itemDbDetails) { - throw new NotImplementedException(); + // Armor has no mods, make no additions + var hasMods = itemDbDetails.Properties.Slots.Count > 0; + if (!hasMods) + { + return; + } + + // Check for and add required soft inserts to armors + var requiredSlots = itemDbDetails.Properties.Slots.Where((slot) => slot.Required ?? false).ToList(); + var hasRequiredSlots = requiredSlots.Count > 0; + if (hasRequiredSlots) + { + foreach (var requiredSlot in requiredSlots) + { + var modItemDbDetails = itemHelper.GetItem(requiredSlot.Props.Filters[0].Plate).Value; + var plateTpl = + requiredSlot.Props.Filters[0].Plate; // `Plate` property appears to be the 'default' item for slot + if (string.IsNullOrEmpty(plateTpl)) + { + // Some bsg plate properties are empty, skip mod + continue; + } + + var mod = new Item + { + Id = hashUtil.Generate(), + Template = plateTpl, + ParentId = armor[0].Id, + SlotId = requiredSlot.Name, + Upd = new() + { + Repairable = new() + { + Durability = modItemDbDetails.Properties.MaxDurability, + MaxDurability = modItemDbDetails.Properties.MaxDurability, + }, + }, + }; + + armor.Add(mod); + } + } + + // Check for and add plate items + var plateSlots = itemDbDetails.Properties.Slots.Where((slot) => itemHelper.IsRemovablePlateSlot(slot.Name)) + .ToList(); + if (plateSlots.Count > 0) + { + foreach (var plateSlot in plateSlots) + { + var plateTpl = plateSlot.Props.Filters[0].Plate; + if (string.IsNullOrEmpty(plateTpl)) + { + // Bsg data lacks a default plate, skip adding mod + continue; + } + + var modItemDbDetails = itemHelper.GetItem(plateTpl).Value; + armor.Add( + new Item() + { + Id = hashUtil.Generate(), + Template = plateSlot.Props.Filters[0].Plate, // `Plate` property appears to be the 'default' item for slot + ParentId = armor[0].Id, + SlotId = plateSlot.Name, + Upd = new() + { + Repairable = new() + { + Durability = modItemDbDetails.Properties.MaxDurability, + MaxDurability = modItemDbDetails.Properties.MaxDurability, + }, + }, + } + ); + } + } } - /// - /// Check if item is valid for being added to fence assorts - /// - /// Item to check - /// true if valid fence item + /** + * Check if item is valid for being added to fence assorts + * @param item Item to check + * @returns true if valid fence item + */ protected bool IsValidFenceItem(TemplateItem item) { - throw new NotImplementedException(); + return item.Type == "Item"; } } diff --git a/Libraries/Core/Generators/PMCLootGenerator.cs b/Libraries/Core/Generators/PMCLootGenerator.cs index 1f163be3..04af656b 100644 --- a/Libraries/Core/Generators/PMCLootGenerator.cs +++ b/Libraries/Core/Generators/PMCLootGenerator.cs @@ -71,7 +71,7 @@ public class PMCLootGenerator var itemsToAdd = items.Where( (item) => allowedItemTypeWhitelist.Contains(item.Value.Parent) && - _itemHelper.isValidItem(item.Value.Id) && + _itemHelper.IsValidItem(item.Value.Id) && !blacklist.Contains(item.Value.Id) && !blacklist.Contains(item.Value.Parent) && ItemFitsInto1By2Slot(item.Value) @@ -151,7 +151,7 @@ public class PMCLootGenerator var itemsToAdd = items.Where( (item) => allowedItemTypeWhitelist.Contains(item.Value.Parent) && - _itemHelper.isValidItem(item.Value.Id) && + _itemHelper.IsValidItem(item.Value.Id) && !blacklist.Contains(item.Value.Id) && !blacklist.Contains(item.Value.Parent) && ItemFitsInto2By2Slot(item.Value)); @@ -233,7 +233,7 @@ public class PMCLootGenerator var itemsToAdd = items.Where( (item) => allowedItemTypeWhitelist.Contains(item.Value.Parent) && - _itemHelper.isValidItem(item.Value.Id) && + _itemHelper.IsValidItem(item.Value.Id) && !blacklist.Contains(item.Value.Id) && !blacklist.Contains(item.Value.Parent)); diff --git a/Libraries/Core/Generators/RepeatableQuestRewardGenerator.cs b/Libraries/Core/Generators/RepeatableQuestRewardGenerator.cs index 879606c7..887ba056 100644 --- a/Libraries/Core/Generators/RepeatableQuestRewardGenerator.cs +++ b/Libraries/Core/Generators/RepeatableQuestRewardGenerator.cs @@ -695,7 +695,7 @@ public class RepeatableQuestRewardGenerator( List? itemBaseWhitelist = null) { // Return early if not valid item to give as reward - if (!_itemHelper.isValidItem(tpl)) + if (!_itemHelper.IsValidItem(tpl)) { return false; } diff --git a/Libraries/Core/Helpers/ItemHelper.cs b/Libraries/Core/Helpers/ItemHelper.cs index 206ff9f4..1dc7a259 100644 --- a/Libraries/Core/Helpers/ItemHelper.cs +++ b/Libraries/Core/Helpers/ItemHelper.cs @@ -243,7 +243,7 @@ public class ItemHelper( * @param tpl the template id / tpl * @returns boolean; true for items that may be in player possession and not quest items */ - public bool isValidItem(string tpl, List invalidBaseTypes = null) + public bool IsValidItem(string tpl, List invalidBaseTypes = null) { var baseTypes = invalidBaseTypes ?? _defaultInvalidBaseTypes; var itemDetails = GetItem(tpl); diff --git a/Libraries/Core/Models/Eft/Ragfair/RagfairOffer.cs b/Libraries/Core/Models/Eft/Ragfair/RagfairOffer.cs index 92c311ff..71d466c8 100644 --- a/Libraries/Core/Models/Eft/Ragfair/RagfairOffer.cs +++ b/Libraries/Core/Models/Eft/Ragfair/RagfairOffer.cs @@ -44,7 +44,7 @@ public record RagfairOffer /** Rouble price - same as requirementsCost */ [JsonPropertyName("summaryCost")] - public decimal? SummaryCost { get; set; } + public double? SummaryCost { get; set; } [JsonPropertyName("user")] public RagfairOfferUser? User { get; set; } diff --git a/Libraries/Core/Servers/RagfairServer.cs b/Libraries/Core/Servers/RagfairServer.cs index 301a7202..7d54b2b1 100644 --- a/Libraries/Core/Servers/RagfairServer.cs +++ b/Libraries/Core/Servers/RagfairServer.cs @@ -6,130 +6,119 @@ using Core.Models.Spt.Config; using Core.Models.Utils; using Core.Services; -namespace Core.Servers +namespace Core.Servers; + +[Injectable] +public class RagfairServer( + ISptLogger _logger, + RagfairOfferService _ragfairOfferService, + RagfairCategoriesService _ragfairCategoriesService, + RagfairRequiredItemsService _ragfairRequiredItemsService, + LocalisationService _localisationService, + RagfairOfferGenerator _ragfairOfferGenerator, + ConfigServer _configServer +) { - [Injectable] - public class RagfairServer + protected RagfairConfig _ragfairConfig = _configServer.GetConfig(); + + public void Load() { - protected ISptLogger _logger; - protected RagfairOfferService _ragfairOfferService; - protected RagfairCategoriesService _ragfairCategoriesService; - protected RagfairRequiredItemsService _ragfairRequiredItemsService; - protected LocalisationService _localisationService; - protected RagfairOfferGenerator _ragfairOfferGenerator; - protected ConfigServer _configServer; - protected RagfairConfig _ragfairConfig; + _logger.Info(_localisationService.GetText("ragfair-generating_offers")); + _ragfairOfferGenerator.GenerateDynamicOffers(); + Update(); + } - public RagfairServer( - ISptLogger logger, - RagfairOfferService ragfairOfferService, - RagfairCategoriesService ragfairCategoriesService, - RagfairRequiredItemsService ragfairRequiredItemsService, - LocalisationService localisationService, - RagfairOfferGenerator ragfairOfferGenerator, - ConfigServer configServer) + public void Update() + { + _ragfairOfferService.ExpireStaleOffers(); + // Generate trader offers + var traders = GetUpdateableTraders(); + foreach (var traderId in traders) { - _logger = logger; - _ragfairOfferService = ragfairOfferService; - _ragfairCategoriesService = ragfairCategoriesService; - _ragfairRequiredItemsService = ragfairRequiredItemsService; - _localisationService = localisationService; - _ragfairOfferGenerator = ragfairOfferGenerator; - _configServer = configServer; - - _ragfairConfig = _configServer.GetConfig(); - } - - public void Load(){ - _logger.Info(_localisationService.GetText("ragfair-generating_offers")); - _ragfairOfferGenerator.GenerateDynamicOffers(); - Update(); - } - - public void Update() - { - _logger.Info($"reimplement me: Ragfairserver.Update()"); - // _ragfairOfferService.ExpireStaleOffers(); - // - // // Generate trader offers - // var traders = GetUpdateableTraders(); - // foreach (var traderId in traders) { - // // Edge case - skip generating fence offers - // if (traderId == Traders.FENCE) - // { - // continue; - // } - // - // if (_ragfairOfferService.TraderOffersNeedRefreshing(traderId)) - // { - // _ragfairOfferGenerator.GenerateFleaOffersForTrader(traderId); - // } - // } - // - // // Regenerate expired offers when over threshold limit - // if (_ragfairOfferService.GetExpiredOfferCount() >= _ragfairConfig.Dynamic.ExpiredOfferThreshold) - // { - // var expiredAssortsWithChildren = _ragfairOfferService.GetExpiredOfferAssorts(); - // _ragfairOfferGenerator.GenerateDynamicOffers(expiredAssortsWithChildren); - // - // // Clear out expired offers now we've generated them - // _ragfairOfferService.ResetExpiredOffers(); - // } - // - // _ragfairRequiredItemsService.BuildRequiredItemTable(); - } - - /** - * Get traders who need to be periodically refreshed - * @returns string array of traders - */ - public List GetUpdateableTraders() - { - return _ragfairConfig.Traders.Keys.ToList(); - } - - public Dictionary GetAllActiveCategories( - bool fleaUnlocked, - SearchRequestData searchRequestData, - List offers){ - return _ragfairCategoriesService.GetCategoriesFromOffers(offers, searchRequestData, fleaUnlocked); - } - - /** - * Disable/Hide an offer from flea - * @param offerId - */ - public void HideOffer(string offerId){ - var offers = _ragfairOfferService.GetOffers(); - var offer = offers.FirstOrDefault((x) => x.Id == offerId); - - if (offer is null) { - _logger.Error(_localisationService.GetText("ragfair-offer_not_found_unable_to_hide", offerId)); - - return; + // Edge case - skip generating fence offers + if (traderId == Traders.FENCE) + { + continue; } - offer.Locked = true; + if (_ragfairOfferService.TraderOffersNeedRefreshing(traderId)) + { + _ragfairOfferGenerator.GenerateFleaOffersForTrader(traderId); + } } - public RagfairOffer? GetOffer(string offerId) { - return _ragfairOfferService.GetOfferByOfferId(offerId); + // Regenerate expired offers when over threshold limit + if (_ragfairOfferService.GetExpiredOfferCount() >= _ragfairConfig.Dynamic.ExpiredOfferThreshold) + { + var expiredAssortsWithChildren = _ragfairOfferService.GetExpiredOfferAssorts(); + _ragfairOfferGenerator.GenerateDynamicOffers(expiredAssortsWithChildren); + + // Clear out expired offers now we've generated them + _ragfairOfferService.ResetExpiredOffers(); } - public List GetOffers() { - return _ragfairOfferService.GetOffers(); + _ragfairRequiredItemsService.BuildRequiredItemTable(); + } + + /** + * Get traders who need to be periodically refreshed + * @returns string array of traders + */ + public List GetUpdateableTraders() + { + return _ragfairConfig.Traders.Keys.ToList(); + } + + public Dictionary GetAllActiveCategories( + bool fleaUnlocked, + SearchRequestData searchRequestData, + List offers + ) + { + return _ragfairCategoriesService.GetCategoriesFromOffers(offers, searchRequestData, fleaUnlocked); + } + + /** + * Disable/Hide an offer from flea + * @param offerId + */ + public void HideOffer(string offerId) + { + var offers = _ragfairOfferService.GetOffers(); + var offer = offers.FirstOrDefault((x) => x.Id == offerId); + + if (offer is null) + { + _logger.Error(_localisationService.GetText("ragfair-offer_not_found_unable_to_hide", offerId)); + + return; } - public void RemoveOfferStack(string offerId, int amount) { - _ragfairOfferService.RemoveOfferStack(offerId, amount); - } + offer.Locked = true; + } - public bool DoesOfferExist(string offerId) { - return _ragfairOfferService.DoesOfferExist(offerId); - } + public RagfairOffer? GetOffer(string offerId) + { + return _ragfairOfferService.GetOfferByOfferId(offerId); + } - public void AddPlayerOffers() { - _ragfairOfferService.AddPlayerOffers(); - } -} + public List GetOffers() + { + return _ragfairOfferService.GetOffers(); + } + + public void RemoveOfferStack(string offerId, int amount) + { + _ragfairOfferService.RemoveOfferStack(offerId, amount); + } + + public bool DoesOfferExist(string offerId) + { + return _ragfairOfferService.DoesOfferExist(offerId); + } + + public void AddPlayerOffers() + { + _ragfairOfferService.AddPlayerOffers(); + } } diff --git a/Libraries/Core/Services/RagfairOfferService.cs b/Libraries/Core/Services/RagfairOfferService.cs index 8646cc19..f0cc4bdb 100644 --- a/Libraries/Core/Services/RagfairOfferService.cs +++ b/Libraries/Core/Services/RagfairOfferService.cs @@ -2,168 +2,344 @@ using Core.Helpers; using SptCommon.Annotations; using Core.Models.Eft.Common.Tables; using Core.Models.Eft.Ragfair; +using Core.Models.Spt.Config; using Core.Models.Utils; +using Core.Routers; +using Core.Servers; using Core.Utils; +using Core.Utils.Cloners; +using SptCommon.Extensions; namespace Core.Services; [Injectable(InjectionType.Singleton)] public class RagfairOfferService( - ISptLogger _logger, - TimeUtil _timeUtil, - DatabaseService _databaseService, - RagfairOfferHelper _ragfairOfferHelper , - RagfairOfferHolder _ragfairOfferHolder, - LocalisationService _localisationService) + ISptLogger logger, + TimeUtil timeUtil, + HashUtil hashUtil, + DatabaseService databaseService, + SaveServer saveServer, + RagfairServerHelper ragfairServerHelper, + ItemHelper itemHelper, + ProfileHelper profileHelper, + LocalisationService localisationService, + ConfigServer configServer, + ICloner cloner, + RagfairOfferHolder ragfairOfferHolder +) { - /// - /// Get all offers - /// - /// List of RagfairOffer + protected bool playerOffersLoaded; + + /** Offer id + offer object */ + protected Dictionary expiredOffers = new(); + + protected RagfairConfig ragfairConfig = configServer.GetConfig(); + + /** + * Get all offers + * @returns IRagfairOffer array + */ public List GetOffers() { - throw new NotImplementedException(); + return ragfairOfferHolder.GetOffers(); } public RagfairOffer? GetOfferByOfferId(string offerId) { - throw new NotImplementedException(); + return ragfairOfferHolder.GetOfferById(offerId); } - public List GetOffersOfType(string templateId) + public List? GetOffersOfType(string templateId) { - throw new NotImplementedException(); + return ragfairOfferHolder.GetOffersByTemplate(templateId); } public void AddOffer(RagfairOffer offer) { - throw new NotImplementedException(); + ragfairOfferHolder.AddOffer(offer); } public void AddOfferToExpired(RagfairOffer staleOffer) { - throw new NotImplementedException(); + expiredOffers[staleOffer.Id] = staleOffer; } - /// - /// Get total count of current expired offers - /// - /// Number of expired offers + /** + * Get total count of current expired offers + * @returns Number of expired offers + */ public int GetExpiredOfferCount() { - Console.WriteLine($"actually implement me plz: owo: GetExpiredOfferCount"); - return 0; + return expiredOffers.Keys.Count; } - /// - /// Get a list of lists of expired offer items + children - /// - /// Expired offer assorts + /** + * Get an array of arrays of expired offer items + children + * @returns Expired offer assorts + */ public List> GetExpiredOfferAssorts() { - Console.WriteLine($"actually implement me plz: owo: GetExpiredOfferAssorts"); - return new List>(); + var expiredItems = new List>(); + + foreach (var expiredOfferId in expiredOffers.Keys) + { + var expiredOffer = expiredOffers[expiredOfferId]; + expiredItems.Add(expiredOffer.Items); + } + + return expiredItems; } - /// - /// Clear out internal expiredOffers dictionary of all items - /// + /** + * Clear out internal expiredOffers dictionary of all items + */ public void ResetExpiredOffers() { - Console.WriteLine($"actually implement me plz: owo: ResetExpiredOffers"); + expiredOffers = new(); } - /// - /// Does the offer exist on the ragfair - /// - /// offer id to check for - /// offer exists - true + /** + * Does the offer exist on the ragfair + * @param offerId offer id to check for + * @returns offer exists - true + */ public bool DoesOfferExist(string offerId) { - throw new NotImplementedException(); + return ragfairOfferHolder.GetOfferById(offerId) != null; } - /// - /// Remove an offer from ragfair by offer id - /// - /// Offer id to remove + /** + * Remove an offer from ragfair by offer id + * @param offerId Offer id to remove + */ public void RemoveOfferById(string offerId) { - throw new NotImplementedException(); + var offer = ragfairOfferHolder.GetOfferById(offerId); + if (offer == null) + { + logger.Warning(localisationService.GetText("ragfair-unable_to_remove_offer_doesnt_exist", offerId)); + return; + } + + ragfairOfferHolder.RemoveOffer(offer); } - /// - /// Reduce size of an offer stack by specified amount - /// - /// Offer to adjust stack size of - /// How much to deduct from offers stack size + /** + * Reduce size of an offer stack by specified amount + * @param offerId Offer to adjust stack size of + * @param amount How much to deduct from offers stack size + */ public void RemoveOfferStack(string offerId, int amount) { - throw new NotImplementedException(); + var offer = ragfairOfferHolder.GetOfferById(offerId); + if (offer != null) + { + offer.Items[0].Upd.StackObjectsCount -= amount; + if (offer.Items[0].Upd.StackObjectsCount <= 0) + { + ProcessStaleOffer(offer); + } + } } public void RemoveAllOffersByTrader(string traderId) { - throw new NotImplementedException(); + ragfairOfferHolder.RemoveAllOffersByTrader(traderId); } - /// - /// Do the trader offers on flea need to be refreshed - /// - /// Trader to check - /// true if they do - public bool TraderOffersNeedRefreshing(string traderId) + /** + * Do the trader offers on flea need to be refreshed + * @param traderID Trader to check + * @returns true if they do + */ + public bool TraderOffersNeedRefreshing(string traderID) { - var trader = _databaseService.GetTrader(traderId); - if (trader?.Base == null) { - _logger.Error(_localisationService.GetText("ragfair-trader_missing_base_file", traderId)); - + var trader = databaseService.GetTrader(traderID); + if (trader?.Base == null) + { + logger.Error(localisationService.GetText("ragfair-trader_missing_base_file", traderID)); return false; } // No value, occurs when first run, trader offers need to be added to flea trader.Base.RefreshTraderRagfairOffers ??= true; - return trader.Base.RefreshTraderRagfairOffers.GetValueOrDefault(false); + return trader.Base.RefreshTraderRagfairOffers.Value; } public void AddPlayerOffers() { - Console.WriteLine($"actually implement me plz: owo: AddPlayerOffers"); - // throw new NotImplementedException(); + if (!playerOffersLoaded) + { + foreach (var sessionID in saveServer.GetProfiles().Keys) + { + var pmcData = saveServer.GetProfile(sessionID)?.CharacterData?.PmcData; + + if (pmcData?.RagfairInfo == null || pmcData.RagfairInfo.Offers == null) + { + // Profile is wiped + continue; + } + + ragfairOfferHolder.AddOffers(pmcData.RagfairInfo.Offers); + } + + playerOffersLoaded = true; + } } public void ExpireStaleOffers() { - var time = _timeUtil.GetTimeStamp(); - foreach (var staleOffer in _ragfairOfferHolder.GetStaleOffers(time)) { + var time = timeUtil.GetTimeStamp(); + foreach (var staleOffer in ragfairOfferHolder.GetStaleOffers(time)) + { ProcessStaleOffer(staleOffer); } } - /// - /// Remove stale offer from flea - /// - /// Stale offer to process + /** + * Remove stale offer from flea + * @param staleOffer Stale offer to process + */ protected void ProcessStaleOffer(RagfairOffer staleOffer) { - throw new NotImplementedException(); + var staleOfferUserId = staleOffer.User.Id; + var isTrader = ragfairServerHelper.IsTrader(staleOfferUserId); + var isPlayer = profileHelper.IsPlayer(staleOfferUserId.RegexReplace("^pmc", "")); + + // Skip trader offers, managed by RagfairServer.update() + if (isTrader) + { + return; + } + + // Handle dynamic offer + if (!(isTrader || isPlayer)) + { + // Dynamic offer + AddOfferToExpired(staleOffer); + } + + // Handle player offer - items need returning/XP adjusting. Checking if offer has actually expired or not. + if (isPlayer && staleOffer.EndTime <= timeUtil.GetTimeStamp()) + { + ReturnPlayerOffer(staleOffer); + return; + } + + // Remove expired existing offer from global offers + RemoveOfferById(staleOffer.Id); } protected void ReturnPlayerOffer(RagfairOffer playerOffer) { - throw new NotImplementedException(); + var pmcId = playerOffer.User.Id; + var profile = profileHelper.GetProfileByPmcId(pmcId); + if (profile == null) + { + logger.Error($"Unable to return flea offer {playerOffer.Id} as the profile: {pmcId} could not be found"); + return; + } + + var offerinProfileIndex = profile.RagfairInfo.Offers.FindIndex((o) => o.Id == playerOffer.Id); + if (offerinProfileIndex == -1) + { + logger.Warning(localisationService.GetText("ragfair-unable_to_find_offer_to_remove", playerOffer.Id)); + return; + } + + // Reduce player ragfair rep + profile.RagfairInfo.Rating -= databaseService.GetGlobals().Configuration.RagFair.RatingDecreaseCount; + profile.RagfairInfo.IsRatingGrowing = false; + + // Increment players 'notSellSum' value + profile.RagfairInfo.NotSellSum ??= 0; + profile.RagfairInfo.NotSellSum += playerOffer.SummaryCost; + + var firstOfferItem = playerOffer.Items[0]; + if (firstOfferItem.Upd.StackObjectsCount > firstOfferItem.Upd.OriginalStackObjectsCount) + { + playerOffer.Items[0].Upd.StackObjectsCount = firstOfferItem.Upd.OriginalStackObjectsCount; + } + + playerOffer.Items[0].Upd.OriginalStackObjectsCount = null; + // Remove player offer from flea + ragfairOfferHolder.RemoveOffer(playerOffer); + + // Send failed offer items to player in mail + var unstackedItems = UnstackOfferItems(playerOffer.Items); + + // Need to regenerate Ids to ensure returned item(s) have correct parent values + var newParentId = hashUtil.Generate(); + foreach (var item in unstackedItems) + { + // Refresh root items' parentIds + if (item.ParentId == "hideout") + { + item.ParentId = newParentId; + } + } + + ragfairServerHelper.ReturnItems(profile.SessionId, unstackedItems); + profile.RagfairInfo.Offers.Splice(offerinProfileIndex, 1); } - /// - /// Flea offer items are stacked up often beyond the StackMaxSize limit - /// Unstack the items into a list of root items and their children - /// Will create new items equal to the - /// - /// Offer items to unstack - /// Unstacked list of items + /** + * Flea offer items are stacked up often beyond the StackMaxSize limit + * Un stack the items into an array of root items and their children + * Will create new items equal to the + * @param items Offer items to unstack + * @returns Unstacked array of items + */ protected List UnstackOfferItems(List items) { - throw new NotImplementedException(); + var result = new List(); + var rootItem = items[0]; + var itemDetails = itemHelper.GetItem(rootItem.Template); + var itemMaxStackSize = itemDetails.Value?.Properties?.StackMaxSize ?? 1; + + var totalItemCount = rootItem.Upd?.StackObjectsCount ?? 1; + + // Items within stack tolerance, return existing data - no changes needed + if (totalItemCount <= itemMaxStackSize) + { + // Edge case - Ensure items stack count isnt < 1 + if (items[0]?.Upd?.StackObjectsCount < 1) + { + items[0].Upd.StackObjectsCount = 1; + } + + return items; + } + + // Single item with no children e.g. ammo, use existing de-stacking code + if (items.Count == 1) + { + return itemHelper.SplitStack(rootItem); + } + + // Item with children, needs special handling + // Force new item to have stack size of 1 + for (var index = 0; index < totalItemCount; index++) + { + var itemAndChildrenClone = cloner.Clone(items); + + // Ensure upd object exits + itemAndChildrenClone[0].Upd ??= new(); + + // Force item to be singular + itemAndChildrenClone[0].Upd.StackObjectsCount = 1; + + // Ensure items IDs are unique to prevent collisions when added to player inventory + var reparentedItemAndChildren = itemHelper.ReparentItemAndChildren( + itemAndChildrenClone[0], + itemAndChildrenClone + ); + itemHelper.RemapRootItemId(reparentedItemAndChildren); + + result.AddRange(reparentedItemAndChildren); + } + + return result; } } diff --git a/Libraries/Core/Utils/RagfairOfferHolder.cs b/Libraries/Core/Utils/RagfairOfferHolder.cs index 07e39729..06486c0d 100644 --- a/Libraries/Core/Utils/RagfairOfferHolder.cs +++ b/Libraries/Core/Utils/RagfairOfferHolder.cs @@ -1,5 +1,7 @@ using Core.Helpers; using Core.Models.Eft.Ragfair; +using Core.Models.Spt.Config; +using Core.Servers; using SptCommon.Annotations; namespace Core.Utils; @@ -7,36 +9,23 @@ namespace Core.Utils; [Injectable(InjectionType.Singleton)] public class RagfairOfferHolder( RagfairServerHelper ragfairServerHelper, - ProfileHelper profileHelper) + ProfileHelper profileHelper, + ConfigServer configServer) { + protected Dictionary _offersById; protected Dictionary> _offersByTemplate; protected Dictionary> _offersByTrader; - protected int _maxOffersPerTemplate; //TODO - set from config? - - public void SetMaxOffersPerTemplate(int max) - { - _maxOffersPerTemplate = max; - } - + protected int _maxOffersPerTemplate = (int) configServer.GetConfig().Dynamic.OfferItemCount.Max; + public RagfairOffer? GetOfferById(string id) { - if (_offersById.ContainsKey(id)) - { - return _offersById[id]; - } - - return null; + return _offersById.GetValueOrDefault(id); } - public List GetOffersByTemplate(string templateId) + public List? GetOffersByTemplate(string templateId) { - if (_offersByTemplate.ContainsKey(templateId)) - { - return _offersByTemplate[templateId].Values.ToList(); - } - - return null; + return _offersByTemplate.TryGetValue(templateId, out var value) ? value.Values.ToList() : null; } public List GetOffersByTrader(string traderId)