diff --git a/Libraries/Core/Generators/LootGenerator.cs b/Libraries/Core/Generators/LootGenerator.cs index 516c63a3..097f835a 100644 --- a/Libraries/Core/Generators/LootGenerator.cs +++ b/Libraries/Core/Generators/LootGenerator.cs @@ -1,15 +1,28 @@ -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; +using Core.Helpers; using SptCommon.Annotations; using Core.Models.Common; using Core.Models.Eft.Common; using Core.Models.Eft.Common.Tables; +using Core.Models.Enums; using Core.Models.Spt.Config; using Core.Models.Spt.Services; +using Core.Models.Utils; +using Core.Services; +using Core.Utils; namespace Core.Generators; [Injectable] public class LootGenerator( + ISptLogger _logger, + RandomUtil _randomUtil, + HashUtil _hashUtil, + ItemHelper _itemHelper, + PresetHelper _presetHelper, + DatabaseService _databaseService, + ItemFilterService _itemFilterService + ) { @@ -20,7 +33,105 @@ public class LootGenerator( /// An array of loot items public List CreateRandomLoot(LootRequest options) { - throw new NotImplementedException(); + var result = new List(); + var itemTypeCounts = InitItemLimitCounter(options.ItemLimits); + + // Handle sealed weapon containers + var sealedWeaponCrateCount = _randomUtil.GetDouble( + options.WeaponCrateCount.Min.Value, + options.WeaponCrateCount.Max.Value); + if (sealedWeaponCrateCount > 0) { + // Get list of all sealed containers from db - they're all the same, just for flavor + var itemsDb = _itemHelper.GetItems(); + var sealedWeaponContainerPool = (itemsDb).Where((item) => + item.Name.Contains("event_container_airdrop")); + + for (var index = 0; index < sealedWeaponCrateCount; index++) { + // Choose one at random + add to results array + var chosenSealedContainer = _randomUtil.GetArrayValue(sealedWeaponContainerPool); + result.Add( new Item{ + Id = _hashUtil.Generate(), + Template = chosenSealedContainer.Id, + Upd = new Upd{ + StackObjectsCount = 1, + SpawnedInSession = true + }, + }); + } + } + + // Get items from items.json that have a type of item + not in global blacklist + base type is in whitelist + var rewardPoolResults = GetItemRewardPool( + options.ItemBlacklist, + options.ItemTypeWhitelist, + options.UseRewardItemBlacklist.GetValueOrDefault(false), + options.AllowBossItems.GetValueOrDefault(false)); + + // Pool has items we could add as loot, proceed + if (rewardPoolResults.ItemPool.Count > 0) { + var randomisedItemCount = _randomUtil.GetDouble(options.ItemCount.Min.Value, options.ItemCount.Max.Value); + for (var index = 0; index < randomisedItemCount; index++) { + if (!FindAndAddRandomItemToLoot(rewardPoolResults.ItemPool, itemTypeCounts, options, result)) { + // Failed to add, reduce index so we get another attempt + index--; + } + } + } + + var globalDefaultPresets = _presetHelper.GetDefaultPresets().Values; + + // Filter default presets to just weapons + var randomisedWeaponPresetCount = _randomUtil.GetDouble( + options.WeaponPresetCount.Min.Value, + options.WeaponPresetCount.Max.Value); + if (randomisedWeaponPresetCount > 0) { + var weaponDefaultPresets = globalDefaultPresets.Where((preset) => + _itemHelper.IsOfBaseclass(preset.Encyclopedia, BaseClasses.WEAPON)).ToList(); + + if (weaponDefaultPresets.Any()) { + for (var index = 0; index < randomisedWeaponPresetCount; index++) { + if ( + !FindAndAddRandomPresetToLoot( + weaponDefaultPresets, + itemTypeCounts, + rewardPoolResults.Blacklist, + result) + ) { + // Failed to add, reduce index so we get another attempt + index--; + } + } + } + } + + // Filter default presets to just armors and then filter again by protection level + var randomisedArmorPresetCount = _randomUtil.GetDouble( + options.ArmorPresetCount.Min.Value, + options.ArmorPresetCount.Max.Value); + if (randomisedArmorPresetCount > 0) { + var armorDefaultPresets = globalDefaultPresets.Where((preset) => + _itemHelper.ArmorItemCanHoldMods(preset.Encyclopedia)); + var levelFilteredArmorPresets = armorDefaultPresets.Where((armor) => + IsArmorOfDesiredProtectionLevel(armor, options)).ToList(); + + // Add some armors to rewards + if (levelFilteredArmorPresets.Any()) { + for (var index = 0; index < randomisedArmorPresetCount; index++) { + if ( + !FindAndAddRandomPresetToLoot( + levelFilteredArmorPresets, + itemTypeCounts, + rewardPoolResults.Blacklist, + result) + ) { + // Failed to add, reduce index so we get another attempt + index--; + } + } + } + } + + return result; } /// @@ -31,7 +142,29 @@ public class LootGenerator( /// Array of Item public List CreateForcedLoot(Dictionary forcedLootDict) { - throw new NotImplementedException(); + var result = new List(); + + var forcedItems = forcedLootDict; + + foreach (var forcedItemKvP in forcedItems) { + var details = forcedLootDict[forcedItemKvP.Key]; + var randomisedItemCount = _randomUtil.GetDouble(details.Min.Value, details.Max.Value); + + // Add forced loot item to result + var newLootItem = new Item{ + Id = _hashUtil.Generate(), + Template = forcedItemKvP.Key, + Upd = new Upd{ + StackObjectsCount = randomisedItemCount, + SpawnedInSession = true, + }, + }; + + var splitResults = _itemHelper.SplitStack(newLootItem); + result.AddRange(splitResults); + } + + return result; } /// @@ -42,22 +175,78 @@ public class LootGenerator( /// Should item.json reward item config be used /// Should boss items be allowed in result /// results of filtering + blacklist used - protected object GetItemRewardPool(List itemTplBlacklist, List itemTypeWhitelist, - bool useRewardItemBlacklist, // TODO: type fuckery, return type was { itemPool: [string, ITemplateItem][]; blacklist: Set } + protected ItemRewardPoolResults GetItemRewardPool(List itemTplBlacklist, List itemTypeWhitelist, + bool useRewardItemBlacklist, bool allowBossItems) { - throw new NotImplementedException(); + var itemsDb = _databaseService.GetItems().Values; + var itemBlacklist = new HashSet(); + itemBlacklist.UnionWith(_itemFilterService.GetBlacklistedItems()); + itemBlacklist.UnionWith(itemTplBlacklist); + + if (useRewardItemBlacklist) + { + var itemsToAdd = _itemFilterService.GetItemRewardBlacklist(); + + // Get all items that match the blacklisted types and fold into item blacklist + var itemTypeBlacklist = _itemFilterService.GetItemRewardBaseTypeBlacklist(); + var itemsMatchingTypeBlacklist = (itemsDb) + .Where((templateItem) => _itemHelper.IsOfBaseclasses(templateItem.Parent, itemTypeBlacklist)) + .Select((templateItem) => templateItem.Id); + + // Clear out blacklist + itemBlacklist = []; + itemBlacklist.UnionWith(itemBlacklist); + itemBlacklist.UnionWith(itemsToAdd); + itemBlacklist.UnionWith(itemsMatchingTypeBlacklist); + } + + if (!allowBossItems) + { + foreach (var bossItem in _itemFilterService.GetBossItems()) { + itemBlacklist.Add(bossItem); + } + } + + var items = itemsDb.Where( + (item) => + !itemBlacklist.Contains(item.Id) && + item.Type.ToLower() == "item" && + !item.Properties.QuestItem.GetValueOrDefault(false) && + itemTypeWhitelist.Contains(item.Parent)).ToList(); + + return new ItemRewardPoolResults{ ItemPool = items, Blacklist = itemBlacklist }; + } + + public record ItemRewardPoolResults + { + public List ItemPool { get; set; } + public HashSet Blacklist { get; set; } } /// - /// Filter armor items by their front plates protection level - top if its a helmet + /// Filter armor items by their front plates protection level - top if it's a helmet /// /// Armor preset to check /// Loot request options - armor level etc /// True if item has desired armor level - protected bool ArmorOfDesiredProtectionLevel(Preset armor, LootRequest options) + protected bool IsArmorOfDesiredProtectionLevel(Preset armor, LootRequest options) { - throw new NotImplementedException(); + string[] relevantSlots = ["front_plate", "helmet_top", "soft_armor_front"]; + foreach (var slotId in relevantSlots) { + var armorItem = armor.Items.FirstOrDefault((item) => item?.SlotId?.ToLower() == slotId); + if (armorItem is null) + { + continue; + } + + var armorDetails = _itemHelper.GetItem(armorItem.Template).Value; + var armorClass = armorDetails.Properties.ArmorClass; + + return options.ArmorLevelWhitelist.Contains((int)armorClass.Value); + } + + return false; } /// @@ -65,7 +254,7 @@ public class LootGenerator( /// /// limits as defined in config /// record, key: item tplId, value: current/max item count allowed - protected Dictionary InitItemLimitCounter(Dictionary limits) + protected Dictionary InitItemLimitCounter(Dictionary limits) { throw new NotImplementedException(); } @@ -104,8 +293,9 @@ public class LootGenerator( /// Items to skip /// List to add chosen preset to /// true if preset was valid and added to pool - protected bool FindAndAddRandomPresetToLoot(List presetPool, object itemTypeCounts, - List itemBlacklist, // TODO: type fuckery, itemTypeCounts was Record + protected bool FindAndAddRandomPresetToLoot(List presetPool, + Dictionary itemTypeCounts, + HashSet itemBlacklist, List result) { throw new NotImplementedException(); diff --git a/Libraries/Core/Helpers/NotifierHelper.cs b/Libraries/Core/Helpers/NotifierHelper.cs index fe14af5f..fdd3f72b 100644 --- a/Libraries/Core/Helpers/NotifierHelper.cs +++ b/Libraries/Core/Helpers/NotifierHelper.cs @@ -25,7 +25,7 @@ public class NotifierHelper(HttpServerHelper _httpServerHelper) EventIdentifier = dialogueMessage.Id, OfferId = ragfairData.OfferId, HandbookId = ragfairData.HandbookId, - Count = ragfairData.Count + Count = (int)ragfairData.Count }; } diff --git a/Libraries/Core/Helpers/RagfairOfferHelper.cs b/Libraries/Core/Helpers/RagfairOfferHelper.cs index 89d8fb15..027aceef 100644 --- a/Libraries/Core/Helpers/RagfairOfferHelper.cs +++ b/Libraries/Core/Helpers/RagfairOfferHelper.cs @@ -1,11 +1,14 @@ using Core.Models.Eft.Common; using Core.Models.Eft.Common.Tables; +using Core.Models.Eft.Hideout; using Core.Models.Eft.ItemEvent; +using Core.Models.Eft.Player; using Core.Models.Eft.Profile; using Core.Models.Eft.Ragfair; using Core.Models.Enums; using Core.Models.Spt.Config; using Core.Models.Utils; +using Core.Routers; using Core.Servers; using Core.Services; using Core.Utils; @@ -18,16 +21,21 @@ namespace Core.Helpers; public class RagfairOfferHelper( ISptLogger _logger, TimeUtil _timeUtil, + HashUtil _hashUtil, RagfairSortHelper _ragfairSortHelper, PresetHelper _presetHelper, RagfairHelper _ragfairHelper, PaymentHelper _paymentHelper, TraderHelper _traderHelper, + QuestHelper _questHelper, + RagfairServerHelper _ragfairServerHelper, ItemHelper _itemHelper, DatabaseService _databaseService, RagfairOfferService _ragfairOfferService, + MailSendService _mailSendService, RagfairRequiredItemsService _ragfairRequiredItemsService, ProfileHelper _profileHelper, + EventOutputHolder _eventOutputHolder, ConfigServer _configServer) { protected RagfairConfig _ragfairConfig = _configServer.GetConfig(); @@ -687,9 +695,75 @@ public class RagfairOfferHelper( * @param boughtAmount Amount item was purchased for * @returns ItemEventRouterResponse */ - public ItemEventRouterResponse CompleteOffer(string sessionID, RagfairOffer offer, int boughtAmount) + public ItemEventRouterResponse CompleteOffer(string offerOwnerSessionId, RagfairOffer offer, int boughtAmount) { - throw new NotImplementedException(); + var itemTpl = offer.Items[0].Template; + var paymentItemsToSendToPlayer = new List(); + var offerStackCount = offer.Items[0].Upd.StackObjectsCount; + var sellerProfile = _profileHelper.GetPmcProfile(offerOwnerSessionId); + + // Pack or ALL items of a multi-offer were bought - remove entire ofer + if (offer.SellInOnePiece.GetValueOrDefault(false) || boughtAmount == offerStackCount) + { + DeleteOfferById(offerOwnerSessionId, offer.Id); + } + else + { + var offerRootItem = offer.Items[0]; + + // Reduce offer root items stack count + offerRootItem.Upd.StackObjectsCount -= boughtAmount; + } + + // Assemble payment to send to seller now offer was purchased + foreach (var requirement in offer.Requirements) { + // Create an item template item + var requestedItem = new Item{ + Id = _hashUtil.Generate(), + Template = requirement.Template, + Upd = new Upd{ StackObjectsCount = requirement.Count * boughtAmount }, + }; + + var stacks = _itemHelper.SplitStack(requestedItem); + foreach (var item in stacks) { + var outItems = new List { item }; + + // TODO - is this code used?, may have been when adding barters to flea was still possible for player + if (requirement.OnlyFunctional.GetValueOrDefault(false)) + { + var presetItems = _ragfairServerHelper.GetPresetItemsByTpl(item); + if (presetItems.Count > 0) + { + outItems.Add(presetItems[0]); + } + } + + paymentItemsToSendToPlayer.AddRange(outItems); + } + } + + var ragfairDetails = new MessageContentRagfair{ + OfferId = offer.Id, + // pack-offers NEED to be the full item count, + // otherwise it only removes 1 from the pack, leaving phantom offer on client ui + Count = offer.SellInOnePiece.GetValueOrDefault(false) ? offerStackCount.Value : boughtAmount, + HandbookId = itemTpl }; + + _mailSendService.SendDirectNpcMessageToPlayer( + offerOwnerSessionId, + _traderHelper.GetTraderById(Traders.RAGMAN).ToString(), + MessageType.FLEAMARKET_MESSAGE, + GetLocalisedOfferSoldMessage(itemTpl, boughtAmount), + paymentItemsToSendToPlayer, + _timeUtil.GetHoursAsSeconds((int)_questHelper.GetMailItemRedeemTimeHoursForProfile(sellerProfile).Value), + null, + ragfairDetails); + + // Adjust sellers sell sum values + sellerProfile.RagfairInfo.SellSum ??= 0; + sellerProfile.RagfairInfo.SellSum += offer.SummaryCost; + + return _eventOutputHolder.GetOutput(offerOwnerSessionId); } /** diff --git a/Libraries/Core/Models/Eft/Profile/MessageContentRagfair.cs b/Libraries/Core/Models/Eft/Profile/MessageContentRagfair.cs index 3594204d..fa35f9e2 100644 --- a/Libraries/Core/Models/Eft/Profile/MessageContentRagfair.cs +++ b/Libraries/Core/Models/Eft/Profile/MessageContentRagfair.cs @@ -1,4 +1,4 @@ -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; namespace Core.Models.Eft.Profile; @@ -8,7 +8,7 @@ public record MessageContentRagfair public string? OfferId { get; set; } [JsonPropertyName("count")] - public int? Count { get; set; } + public double? Count { get; set; } [JsonPropertyName("handbookId")] public string? HandbookId { get; set; } diff --git a/Libraries/Core/Services/LocationLifecycleService.cs b/Libraries/Core/Services/LocationLifecycleService.cs index efceb569..08567b7d 100644 --- a/Libraries/Core/Services/LocationLifecycleService.cs +++ b/Libraries/Core/Services/LocationLifecycleService.cs @@ -1004,7 +1004,7 @@ public class LocationLifecycleService MessageType.BTR_ITEMS_DELIVERY, messageId, items, - messageStoreTime); + (int)messageStoreTime); } protected void HandleInsuredItemLostEvent( diff --git a/Libraries/Core/Services/MailSendService.cs b/Libraries/Core/Services/MailSendService.cs index 1ce0a808..dedc4a0f 100644 --- a/Libraries/Core/Services/MailSendService.cs +++ b/Libraries/Core/Services/MailSendService.cs @@ -40,11 +40,11 @@ public class MailSendService( */ public void SendDirectNpcMessageToPlayer( string sessionId, - string trader, + string? trader, MessageType messageType, string message, List? items, - long? maxStorageTimeSeconds, + double? maxStorageTimeSeconds, SystemData? systemData, MessageContentRagfair? ragfair ) @@ -72,14 +72,14 @@ public class MailSendService( DialogType = MessageType.NPC_TRADER, Trader = trader, MessageText = message, - Items = new() + Items = [] }; // Add items to message if (items?.Count > 0) { details.Items.AddRange(items); - details.ItemsMaxStorageLifetimeSeconds = maxStorageTimeSeconds ?? 172800; + details.ItemsMaxStorageLifetimeSeconds = (long?)(maxStorageTimeSeconds ?? 172800); } if (systemData is not null)