diff --git a/Libraries/Core/Models/Spt/Mod/NewItemDetails.cs b/Libraries/Core/Models/Spt/Mod/NewItemDetails.cs index 94bb3e18..d3c19b04 100644 --- a/Libraries/Core/Models/Spt/Mod/NewItemDetails.cs +++ b/Libraries/Core/Models/Spt/Mod/NewItemDetails.cs @@ -16,7 +16,7 @@ public record NewItemFromCloneDetails : NewItemDetailsBase public string? ItemTplToClone { get; set; } [JsonPropertyName("overrideProperties")] - public ColliderProps? OverrideProperties { get; set; } + public Props? OverrideProperties { get; set; } [JsonPropertyName("parentId")] public string? ParentId { get; set; } @@ -28,10 +28,10 @@ public record NewItemFromCloneDetails : NewItemDetailsBase public record NewItemDetailsBase { [JsonPropertyName("fleaPriceRoubles")] - public float? FleaPriceRoubles { get; set; } + public double? FleaPriceRoubles { get; set; } [JsonPropertyName("handbookPriceRoubles")] - public float? HandbookPriceRoubles { get; set; } + public double? HandbookPriceRoubles { get; set; } [JsonPropertyName("handbookParentId")] public string? HandbookParentId { get; set; } diff --git a/Libraries/Core/Services/Mod/CustomItemService.cs b/Libraries/Core/Services/Mod/CustomItemService.cs index b40555a1..240784ca 100644 --- a/Libraries/Core/Services/Mod/CustomItemService.cs +++ b/Libraries/Core/Services/Mod/CustomItemService.cs @@ -1,9 +1,24 @@ -using Core.Models.Eft.Common.Tables; +using Core.Helpers; +using Core.Models.Eft.Common.Tables; +using Core.Models.Enums; using Core.Models.Spt.Mod; +using Core.Models.Utils; +using Core.Utils; +using Core.Utils.Cloners; +using SptCommon.Annotations; +using SptCommon.Extensions; namespace Core.Services.Mod; -public class CustomItemService +[Injectable] +public class CustomItemService( + ISptLogger _logger, + HashUtil _hashUtil, + DatabaseService _databaseService, + ItemHelper _itemHelper, + ItemBaseClassService _itemBaseClassService, + ICloner _cloner +) { /** * Create a new item from a cloned item base @@ -17,7 +32,50 @@ public class CustomItemService */ public CreateItemResult CreateItemFromClone(NewItemFromCloneDetails newItemDetails) { - throw new NotImplementedException(); + var result = new CreateItemResult(); + var tables = _databaseService.GetTables(); + + // Generate new id for item if none supplied + var newItemId = GetOrGenerateIdForItem(newItemDetails.NewId); + + // Fail if itemId already exists + if (tables.Templates.Items.ContainsKey(newItemId)) + { + result.Errors.Add($"ItemId already exists. {tables.Templates.Items[newItemId].Name}"); + result.Success = false; + result.ItemId = newItemId; + + return result; + } + + // Clone existing item + var itemClone = _cloner.Clone(tables.Templates.Items[newItemDetails.ItemTplToClone]); + + // Update id and parentId of item + itemClone.Id = newItemId; + itemClone.Parent = newItemDetails.ParentId; + + UpdateBaseItemPropertiesWithOverrides(newItemDetails.OverrideProperties, itemClone); + + AddToItemsDb(newItemId, itemClone); + + AddToHandbookDb(newItemId, newItemDetails.HandbookParentId, newItemDetails.HandbookPriceRoubles); + + AddToLocaleDbs(newItemDetails.Locales, newItemId); + + AddToFleaPriceDb(newItemId, newItemDetails.FleaPriceRoubles); + + _itemBaseClassService.HydrateItemBaseClassCache(); + + if (_itemHelper.IsOfBaseclass(itemClone.Id, BaseClasses.WEAPON)) + { + AddToWeaponShelf(newItemId); + } + + result.Success = true; + result.ItemId = newItemId; + + return result; } /** @@ -31,7 +89,37 @@ public class CustomItemService */ public CreateItemResult CreateItem(NewItemDetails newItemDetails) { - throw new NotImplementedException(); + var result = new CreateItemResult(); + var tables = _databaseService.GetTables(); + + var newItem = newItemDetails.NewItem; + + // Fail if itemId already exists + if (tables.Templates.Items.ContainsKey(newItem.Id)) + { + result.Errors.Add($"ItemId already exists. {tables.Templates.Items[newItem.Id].Name}"); + return result; + } + + AddToItemsDb(newItem.Id, newItem); + + AddToHandbookDb(newItem.Id, newItemDetails.HandbookParentId, newItemDetails.HandbookPriceRoubles); + + AddToLocaleDbs(newItemDetails.Locales, newItem.Id); + + AddToFleaPriceDb(newItem.Id, newItemDetails.FleaPriceRoubles); + + _itemBaseClassService.HydrateItemBaseClassCache(); + + if (_itemHelper.IsOfBaseclass(newItem.Id, BaseClasses.WEAPON)) + { + AddToWeaponShelf(newItem.Id); + } + + result.ItemId = newItemDetails.NewItem.Id; + result.Success = true; + + return result; } /** @@ -41,7 +129,7 @@ public class CustomItemService */ protected string GetOrGenerateIdForItem(string newId) { - throw new NotImplementedException(); + return newId == "" ? _hashUtil.Generate() : newId; } /** @@ -50,19 +138,26 @@ public class CustomItemService * @param overrideProperties new properties to apply * @param itemClone item to update */ - protected void UpdateBaseItemPropertiesWithOverrides(Props overrideProperties, TemplateItem itemClone) + protected void UpdateBaseItemPropertiesWithOverrides(Props? overrideProperties, TemplateItem itemClone) { + // for (const propKey in overrideProperties) { + // itemClone._props[propKey] = overrideProperties[propKey]; + // } + // TODO: this will need to be different throw new NotImplementedException(); } /** - * Addd a new item object to the in-memory representation of items.json + * Add a new item object to the in-memory representation of items.json * @param newItemId id of the item to add to items.json * @param itemToAdd Item to add against the new id */ protected void AddToItemsDb(string newItemId, TemplateItem itemToAdd) { - throw new NotImplementedException(); + if (!_databaseService.GetItems().TryAdd(newItemId, itemToAdd)) + { + _logger.Warning($"Unable to add: {newItemId} To Database"); + } } /** @@ -71,9 +166,12 @@ public class CustomItemService * @param parentId parent id of the item being added * @param priceRoubles price of the item being added */ - protected void AddToHandbookDb(string newItemId, string parentId, decimal priceRoubles) + protected void AddToHandbookDb(string newItemId, string parentId, double? priceRoubles) { - throw new NotImplementedException(); + _databaseService + .GetTemplates() + .Handbook.Items.Add(new HandbookItem { Id = newItemId, ParentId = parentId, Price = priceRoubles }); + // TODO: would we want to keep this the same or get them to send a HandbookItem } /** @@ -89,7 +187,23 @@ public class CustomItemService */ protected void AddToLocaleDbs(Dictionary localeDetails, string newItemId) { - throw new NotImplementedException(); + var languages = _databaseService.GetLocales().Languages; + foreach (var shortNameKey in languages) + { + // Get locale details passed in, if not provided by caller use first record in newItemDetails.locales + localeDetails.TryGetValue(shortNameKey.Key, out var newLocaleDetails); + + if (newLocaleDetails is null) + { + newLocaleDetails = localeDetails[localeDetails.Keys.FirstOrDefault()]; + } + + // Create new record in locale file + var globals = _databaseService.GetLocales(); + globals.Global[shortNameKey.Key].Value[$"{newItemId} Name"] = newLocaleDetails.Name; + globals.Global[shortNameKey.Key].Value[$"{newItemId} ShortName"] = newLocaleDetails.ShortName; + globals.Global[shortNameKey.Key].Value[$"{newItemId} Description"] = newLocaleDetails.Description; + } } /** @@ -97,9 +211,9 @@ public class CustomItemService * @param newItemId id of the new item * @param fleaPriceRoubles Price of the new item */ - protected void AddToFleaPriceDb(string newItemId, decimal fleaPriceRoubles) + protected void AddToFleaPriceDb(string newItemId, double? fleaPriceRoubles) { - throw new NotImplementedException(); + _databaseService.GetTemplates().Prices[newItemId] = fleaPriceRoubles ?? 0; } /** @@ -108,7 +222,21 @@ public class CustomItemService */ protected void AddToWeaponShelf(string newItemId) { - throw new NotImplementedException(); + // Ids for wall stashes in db + List wallStashIds = + [ + ItemTpl.HIDEOUTAREACONTAINER_WEAPONSTAND_STASH_1, + ItemTpl.HIDEOUTAREACONTAINER_WEAPONSTAND_STASH_2, + ItemTpl.HIDEOUTAREACONTAINER_WEAPONSTAND_STASH_3 + ]; + foreach (var wallId in wallStashIds) + { + var wall = _itemHelper.GetItem(wallId); + if (wall.Key) + { + wall.Value.Properties.Grids[0].Props.Filters[0].Filter.Add(newItemId); + } + } } /** @@ -117,8 +245,34 @@ public class CustomItemService * @param weaponWeight The weighting for the weapon to be picked vs other weapons * @param weaponSlot The slot the weapon should be added to (e.g. FirstPrimaryWeapon/SecondPrimaryWeapon/Holster) */ - public void AddCustomWeaponToPMCs(string weaponTpl, decimal weaponWeight, string weaponSlot) + public void AddCustomWeaponToPMCs(string weaponTpl, double weaponWeight, string weaponSlot) { - throw new NotImplementedException(); + var weapon = _itemHelper.GetItem(weaponTpl); + if (!weapon.Key) + { + _logger.Warning($"Unable to add custom weapon {weaponTpl} to PMCs as it cannot be found in the Item db"); + + return; + } + + Dictionary?> baseWeaponModObject = new Dictionary?>(); + + // Get all slots weapon has and create a dictionary of them with possible mods that slot into each + var weaponSlots = weapon.Value.Properties.Slots; + foreach (var slot in weaponSlots) + { + baseWeaponModObject[slot.Name] = new HashSet(slot.Props.Filters[0].Filter); + } + + // Get PMCs + var botTypes = _databaseService.GetBots().Types; + + // Add weapon base+mods into bear/usec data + botTypes["usec"].BotInventory.Mods[weaponTpl] = baseWeaponModObject; + botTypes["bear"].BotInventory.Mods[weaponTpl] = baseWeaponModObject; + + // Add weapon to array of allowed weapons + weighting to be picked + botTypes["usec"].BotInventory.Equipment.GetByJsonProp>(weaponSlot)[weaponTpl] = weaponWeight; + botTypes["bear"].BotInventory.Equipment.GetByJsonProp>(weaponSlot)[weaponTpl] = weaponWeight; } } diff --git a/Libraries/Core/Services/PaymentService.cs b/Libraries/Core/Services/PaymentService.cs index ce420f98..be33da3d 100644 --- a/Libraries/Core/Services/PaymentService.cs +++ b/Libraries/Core/Services/PaymentService.cs @@ -44,14 +44,17 @@ public class PaymentService( var payToTrader = _traderHelper.TraderEnumHasValue(request.TransactionId); // Track the amounts of each type of currency involved in the trade. - Dictionary currencyAmounts = new Dictionary(); + Dictionary currencyAmounts = new Dictionary(); // Delete barter items and track currencies - foreach (var itemRequest in request.SchemeItems) { + foreach (var itemRequest in request.SchemeItems) + { // Find the corresponding item in the player's inventory. var item = pmcData.Inventory.Items.FirstOrDefault((i) => i.Id == itemRequest.Id); - if (item is not null) { - if (!_paymentHelper.IsMoneyTpl(item.Template)) { + if (item is not null) + { + if (!_paymentHelper.IsMoneyTpl(item.Template)) + { // If the item is not money, remove it from the inventory. _inventoryHelper.RemoveItemByCount( pmcData, @@ -61,11 +64,15 @@ public class PaymentService( output ); itemRequest.Count = 0; - } else { + } + else + { // If the item is money, add its count to the currencyAmounts object. currencyAmounts.TryAdd(item.Template, (currencyAmounts.GetValueOrDefault(item.Template, 0)) + itemRequest.Count); } - } else { + } + else + { // Used by `SptInsure` // Handle differently, `id` is the money type tpl var currencyTpl = itemRequest.Id; @@ -77,20 +84,24 @@ public class PaymentService( var totalCurrencyAmount = 0; // Loop through each type of currency involved in the trade. - foreach (var currencyTpl in currencyAmounts) { + foreach (var currencyTpl in currencyAmounts) + { var currencyAmount = currencyTpl.Value; totalCurrencyAmount += (int)currencyAmount; - if (currencyAmount > 0) { + if (currencyAmount > 0) + { // Find money stacks in inventory and remove amount needed + update output object to inform client of changes AddPaymentToOutput(pmcData, currencyTpl.Key, (int)currencyAmount, sessionID, output); // If there are warnings, exit early. - if (output.Warnings?.Count > 0) { + if (output.Warnings?.Count > 0) + { return; } - if (payToTrader) { + if (payToTrader) + { // Convert the amount to the trader's currency and update the sales sum. var costOfPurchaseInCurrency = _handbookHelper.FromRUB( _handbookHelper.InRUB(currencyAmount ?? 0, currencyTpl.Key), @@ -104,7 +115,8 @@ public class PaymentService( } // If no currency-based payment is involved, handle it separately - if (totalCurrencyAmount == 0 && payToTrader) { + if (totalCurrencyAmount == 0 && payToTrader) + { _logger.Debug(_localisationService.GetText("payment-zero_price_no_payment")); // Convert the handbook price to the trader's currency and update the sales sum. @@ -116,7 +128,8 @@ public class PaymentService( pmcData.TradersInfo[request.TransactionId].SalesSum += costOfPurchaseInCurrency; } - if (payToTrader) { + if (payToTrader) + { _traderHelper.LevelUp(request.TransactionId, pmcData); } @@ -126,12 +139,14 @@ public class PaymentService( private double? GetTraderItemHandbookPriceRouble(string? traderAssortId, string traderId) { var purchasedAssortItem = _traderHelper.GetTraderAssortItemByAssortId(traderId, traderAssortId); - if (purchasedAssortItem is null) { + if (purchasedAssortItem is null) + { return 1; } var assortItemPriceRouble = _handbookHelper.GetTemplatePrice(purchasedAssortItem.Template); - if (assortItemPriceRouble is null) { + if (assortItemPriceRouble is null) + { _logger.Debug($"No item price found for {purchasedAssortItem.Template} on trader: {traderId} in assort: {traderAssortId}"); return 1; @@ -139,12 +154,13 @@ public class PaymentService( return assortItemPriceRouble; } - + public void GiveProfileMoney(PmcData pmcData, double? amountToSend, ProcessSellTradeRequestData request, ItemEventRouterResponse output, string sessionID) { var trader = _traderHelper.GetTrader(request.TransactionId, sessionID); - if (trader is null) { + if (trader is null) + { _logger.Error($"Unable to add currency to profile as trader: {request.TransactionId} does not exist"); return; @@ -153,31 +169,40 @@ public class PaymentService( var currencyTpl = _paymentHelper.GetCurrency(trader.Currency); var calcAmount = _handbookHelper.FromRUB(_handbookHelper.InRUB(amountToSend ?? 0, currencyTpl), currencyTpl); var currencyMaxStackSize = _itemHelper.GetItem(currencyTpl).Value.Properties?.StackMaxSize; - if (currencyMaxStackSize is null) { + if (currencyMaxStackSize is null) + { _logger.Error($"Unable to add currency: {currencyTpl} to profile as it lacks a _props property"); return; } + var skipSendingMoneyToStash = false; - foreach (var item in pmcData.Inventory.Items) { + foreach (var item in pmcData.Inventory.Items) + { // Item is not currency - if (item.Template != currencyTpl) { + if (item.Template != currencyTpl) + { continue; } // Item is not in the stash - if (!_inventoryHelper.IsItemInStash(pmcData, item)) { + if (!_inventoryHelper.IsItemInStash(pmcData, item)) + { continue; } // Found currency item - if (item.Upd.StackObjectsCount < currencyMaxStackSize) { - if (item.Upd.StackObjectsCount + calcAmount > currencyMaxStackSize) { + if (item.Upd.StackObjectsCount < currencyMaxStackSize) + { + if (item.Upd.StackObjectsCount + calcAmount > currencyMaxStackSize) + { // calculate difference - calcAmount -= (int) ((currencyMaxStackSize - item.Upd.StackObjectsCount) ?? 0); + calcAmount -= (int)((currencyMaxStackSize - item.Upd.StackObjectsCount) ?? 0); item.Upd.StackObjectsCount = currencyMaxStackSize; - } else { + } + else + { skipSendingMoneyToStash = true; item.Upd.StackObjectsCount += calcAmount; } @@ -185,24 +210,28 @@ public class PaymentService( // Inform client of change to items StackObjectsCount output.ProfileChanges[sessionID].Items.ChangedItems.Add(item); - if (skipSendingMoneyToStash) { + if (skipSendingMoneyToStash) + { break; } } } // Create single currency item with all currency on it - Item rootCurrencyReward = new Item { + Item rootCurrencyReward = new Item + { Id = _hashUtil.Generate(), Template = currencyTpl, - Upd = new Upd { StackObjectsCount = Math.Round((double) calcAmount) } + Upd = new Upd { StackObjectsCount = Math.Round((double)calcAmount) } }; // Ensure money is properly split to follow its max stack size limit var rewards = _itemHelper.SplitStackIntoSeparateItems(rootCurrencyReward); - if (!skipSendingMoneyToStash) { - AddItemsDirectRequest addItemToStashRequest = new AddItemsDirectRequest { + if (!skipSendingMoneyToStash) + { + AddItemsDirectRequest addItemToStashRequest = new AddItemsDirectRequest + { ItemsWithModsToAdd = rewards, FoundInRaid = false, Callback = null, @@ -241,21 +270,28 @@ public class PaymentService( ); //Ensure all money items found have a upd - foreach (var moneyStack in moneyItemsInInventory) { + foreach (var moneyStack in moneyItemsInInventory) + { moneyStack.Upd ??= new Upd { StackObjectsCount = 1 }; } - var amountAvailable = moneyItemsInInventory.Aggregate(0, + var amountAvailable = moneyItemsInInventory.Aggregate( + 0, (accumulator, item) => (int)(accumulator + item.Upd.StackObjectsCount) ); // If no money in inventory or amount is not enough we return false - if (moneyItemsInInventory.Count <= 0 || amountAvailable < amountToPay) { + if (moneyItemsInInventory.Count <= 0 || amountAvailable < amountToPay) + { _logger.Error( - _localisationService.GetText("payment-not_enough_money_to_complete_transation", new { - amountToPay = amountToPay, - amountAvailable = amountAvailable, - }) + _localisationService.GetText( + "payment-not_enough_money_to_complete_transation", + new + { + amountToPay = amountToPay, + amountAvailable = amountAvailable, + } + ) ); _httpResponseUtil.AppendErrorToOutput( output, @@ -267,18 +303,23 @@ public class PaymentService( } var leftToPay = amountToPay; - foreach (var profileMoneyItem in moneyItemsInInventory) { + foreach (var profileMoneyItem in moneyItemsInInventory) + { var itemAmount = profileMoneyItem.Upd.StackObjectsCount; - if (leftToPay >= itemAmount) { + if (leftToPay >= itemAmount) + { leftToPay -= itemAmount ?? 0; _inventoryHelper.RemoveItem(pmcData, profileMoneyItem.Id, sessionID, output); - } else { + } + else + { profileMoneyItem.Upd.StackObjectsCount -= leftToPay; leftToPay = 0; output.ProfileChanges[sessionID].Items.ChangedItems.Add(profileMoneyItem); } - if (leftToPay == 0) { + if (leftToPay == 0) + { break; } } @@ -295,7 +336,8 @@ public class PaymentService( protected List GetSortedMoneyItemsInInventory(PmcData pmcData, string currencyTpl, string playerStashId) { var moneyItemsInInventory = _itemHelper.FindBarterItems("tpl", pmcData.Inventory.Items, currencyTpl); - if (moneyItemsInInventory.Count == 0) { + if (moneyItemsInInventory.Count == 0) + { _logger.Debug($"No {currencyTpl} money items found in inventory"); } @@ -317,33 +359,39 @@ public class PaymentService( protected int PrioritiseStashSort(Item a, Item b, List inventoryItems, string playerStashId) { // a in root of stash, prioritise - if (a.ParentId == playerStashId && b.ParentId != playerStashId) { + if (a.ParentId == playerStashId && b.ParentId != playerStashId) + { return -1; } // b in root stash, prioritise - if (a.ParentId != playerStashId && b.ParentId == playerStashId) { + if (a.ParentId != playerStashId && b.ParentId == playerStashId) + { return 1; } // both in containers - if (a.SlotId == "main" && b.SlotId == "main") { + if (a.SlotId == "main" && b.SlotId == "main") + { // Both items are in containers var aInStash = this.IsInStash(a.ParentId, inventoryItems, playerStashId); var bInStash = this.IsInStash(b.ParentId, inventoryItems, playerStashId); // a in stash in container, prioritise - if (aInStash && !bInStash) { + if (aInStash && !bInStash) + { return -1; } // b in stash in container, prioritise - if (!aInStash && bInStash) { + if (!aInStash && bInStash) + { return 1; } // Both in stash in containers - if (aInStash && bInStash) { + if (aInStash && bInStash) + { // Containers where taking money from would inconvinence player var deprioritisedContainers = _inventoryConfig.DeprioritisedMoneyContainers; var aImmediateParent = inventoryItems.FirstOrDefault((item) => item.Id == a.ParentId); @@ -353,7 +401,8 @@ public class PaymentService( if ( !deprioritisedContainers.Contains(aImmediateParent.Template) && deprioritisedContainers.Contains(bImmediateParent.Template) - ) { + ) + { return -1; } @@ -361,7 +410,8 @@ public class PaymentService( if ( deprioritisedContainers.Contains(aImmediateParent.Template) && !deprioritisedContainers.Contains(bImmediateParent.Template) - ) { + ) + { return 1; } } @@ -382,12 +432,15 @@ public class PaymentService( { var itemParent = inventoryItems.FirstOrDefault((item) => item.Id == itemId); - if (itemParent is not null) { - if (itemParent.SlotId == "hideout") { + if (itemParent is not null) + { + if (itemParent.SlotId == "hideout") + { return true; } - if (itemParent.Id == playerStashId) { + if (itemParent.Id == playerStashId) + { return true; }