400 lines
15 KiB
C#
400 lines
15 KiB
C#
using Core.Helpers;
|
|
using SptCommon.Annotations;
|
|
using Core.Models.Eft.Common;
|
|
using Core.Models.Eft.Common.Tables;
|
|
using Core.Models.Eft.Inventory;
|
|
using Core.Models.Eft.ItemEvent;
|
|
using Core.Models.Eft.Trade;
|
|
using Core.Models.Enums;
|
|
using Core.Models.Spt.Config;
|
|
using Core.Models.Utils;
|
|
using Core.Servers;
|
|
using Core.Utils;
|
|
|
|
namespace Core.Services;
|
|
|
|
[Injectable(InjectionType.Singleton)]
|
|
public class PaymentService(
|
|
ISptLogger<PaymentService> _logger,
|
|
HashUtil _hashUtil,
|
|
HttpResponseUtil _httpResponseUtil,
|
|
DatabaseService _databaseService,
|
|
HandbookHelper _handbookHelper,
|
|
TraderHelper _traderHelper,
|
|
ItemHelper _itemHelper,
|
|
InventoryHelper _inventoryHelper,
|
|
LocalisationService _localisationService,
|
|
PaymentHelper _paymentHelper,
|
|
ConfigServer _configServer
|
|
)
|
|
{
|
|
protected InventoryConfig _inventoryConfig = _configServer.GetConfig<InventoryConfig>();
|
|
|
|
/**
|
|
* Take money and insert items into return to server request
|
|
* @param pmcData Pmc profile
|
|
* @param request Buy item request
|
|
* @param sessionID Session id
|
|
* @param output Client response
|
|
*/
|
|
public void PayMoney(PmcData pmcData, ProcessBuyTradeRequestData request, string sessionID, ItemEventRouterResponse output)
|
|
{
|
|
// May need to convert to trader currency
|
|
var trader = _traderHelper.GetTrader(request.TransactionId, sessionID);
|
|
var payToTrader = _traderHelper.TraderEnumHasValue(request.TransactionId);
|
|
|
|
// Track the amounts of each type of currency involved in the trade.
|
|
Dictionary<string, double?> currencyAmounts = new Dictionary<string, double?>();
|
|
|
|
// Delete barter items and track currencies
|
|
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 the item is not money, remove it from the inventory.
|
|
_inventoryHelper.RemoveItemByCount(
|
|
pmcData,
|
|
item.Id,
|
|
(int)itemRequest.Count,
|
|
sessionID,
|
|
output
|
|
);
|
|
itemRequest.Count = 0;
|
|
} 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 {
|
|
// Used by `SptInsure`
|
|
// Handle differently, `id` is the money type tpl
|
|
var currencyTpl = itemRequest.Id;
|
|
currencyAmounts.TryAdd(currencyTpl, (currencyAmounts.GetValueOrDefault(currencyTpl, 0)) + itemRequest.Count);
|
|
}
|
|
}
|
|
|
|
// Track the total amount of all currencies.
|
|
var totalCurrencyAmount = 0;
|
|
|
|
// Loop through each type of currency involved in the trade.
|
|
foreach (var currencyTpl in currencyAmounts) {
|
|
var currencyAmount = currencyTpl.Value;
|
|
totalCurrencyAmount += (int)currencyAmount;
|
|
|
|
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) {
|
|
return;
|
|
}
|
|
|
|
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),
|
|
_paymentHelper.GetCurrency(trader.Currency)
|
|
);
|
|
|
|
// Only update traders
|
|
pmcData.TradersInfo[request.TransactionId].SalesSum += costOfPurchaseInCurrency;
|
|
}
|
|
}
|
|
}
|
|
|
|
// If no currency-based payment is involved, handle it separately
|
|
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.
|
|
var costOfPurchaseInCurrency = _handbookHelper.FromRUB(
|
|
GetTraderItemHandbookPriceRouble(request.ItemId, request.TransactionId) ?? 0,
|
|
_paymentHelper.GetCurrency(trader.Currency)
|
|
);
|
|
|
|
pmcData.TradersInfo[request.TransactionId].SalesSum += costOfPurchaseInCurrency;
|
|
}
|
|
|
|
if (payToTrader) {
|
|
_traderHelper.LevelUp(request.TransactionId, pmcData);
|
|
}
|
|
|
|
_logger.Debug("Item(s) taken. Status OK.");
|
|
}
|
|
|
|
private double? GetTraderItemHandbookPriceRouble(string? traderAssortId, string traderId)
|
|
{
|
|
var purchasedAssortItem = _traderHelper.GetTraderAssortItemByAssortId(traderId, traderAssortId);
|
|
if (purchasedAssortItem is null) {
|
|
return 1;
|
|
}
|
|
|
|
var assortItemPriceRouble = _handbookHelper.GetTemplatePrice(purchasedAssortItem.Template);
|
|
if (assortItemPriceRouble is null) {
|
|
_logger.Debug($"No item price found for {purchasedAssortItem.Template} on trader: {traderId} in assort: {traderAssortId}");
|
|
|
|
return 1;
|
|
}
|
|
|
|
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) {
|
|
_logger.Error($"Unable to add currency to profile as trader: {request.TransactionId} does not exist");
|
|
|
|
return;
|
|
}
|
|
|
|
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) {
|
|
_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) {
|
|
// Item is not currency
|
|
if (item.Template != currencyTpl) {
|
|
continue;
|
|
}
|
|
|
|
// Item is not in the stash
|
|
if (!_inventoryHelper.IsItemInStash(pmcData, item)) {
|
|
continue;
|
|
}
|
|
|
|
// Found currency item
|
|
if (item.Upd.StackObjectsCount < currencyMaxStackSize) {
|
|
if (item.Upd.StackObjectsCount + calcAmount > currencyMaxStackSize) {
|
|
// calculate difference
|
|
calcAmount -= (currencyMaxStackSize - item.Upd.StackObjectsCount) ?? 0;
|
|
item.Upd.StackObjectsCount = currencyMaxStackSize;
|
|
} else {
|
|
skipSendingMoneyToStash = true;
|
|
item.Upd.StackObjectsCount += calcAmount;
|
|
}
|
|
|
|
// Inform client of change to items StackObjectsCount
|
|
output.ProfileChanges[sessionID].Items.ChangedItems.Add(_itemHelper.GetProductFromItem(item));
|
|
|
|
if (skipSendingMoneyToStash) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create single currency item with all currency on it
|
|
Item rootCurrencyReward = new Item {
|
|
Id = _hashUtil.Generate(),
|
|
Template = currencyTpl,
|
|
Upd = new Upd { StackObjectsCount = Math.Round(calcAmount) },
|
|
};
|
|
|
|
// Ensure money is properly split to follow its max stack size limit
|
|
var rewards = _itemHelper.SplitStackIntoSeparateItems(rootCurrencyReward);
|
|
|
|
if (!skipSendingMoneyToStash) {
|
|
AddItemsDirectRequest addItemToStashRequest = new AddItemsDirectRequest {
|
|
ItemsWithModsToAdd = rewards,
|
|
FoundInRaid = false,
|
|
Callback = null,
|
|
UseSortingTable = true,
|
|
};
|
|
_inventoryHelper.AddItemsToStash(sessionID, addItemToStashRequest, pmcData, output);
|
|
}
|
|
|
|
// Calcualte new total sale sum with trader item sold to
|
|
var saleSum = pmcData.TradersInfo[request.TransactionId].SalesSum + amountToSend;
|
|
|
|
pmcData.TradersInfo[request.TransactionId].SalesSum = saleSum;
|
|
_traderHelper.LevelUp(request.TransactionId, pmcData);
|
|
}
|
|
|
|
/**
|
|
* Remove currency from player stash/inventory and update client object with changes
|
|
* @param pmcData Player profile to find and remove currency from
|
|
* @param currencyTpl Type of currency to pay
|
|
* @param amountToPay money value to pay
|
|
* @param sessionID Session id
|
|
* @param output output object to send to client
|
|
*/
|
|
public void AddPaymentToOutput(
|
|
PmcData pmcData,
|
|
string currencyTpl,
|
|
double amountToPay,
|
|
string sessionID,
|
|
ItemEventRouterResponse output
|
|
)
|
|
{
|
|
var moneyItemsInInventory = GetSortedMoneyItemsInInventory(
|
|
pmcData,
|
|
currencyTpl,
|
|
pmcData.Inventory.Stash
|
|
);
|
|
|
|
//Ensure all money items found have a upd
|
|
foreach (var moneyStack in moneyItemsInInventory) {
|
|
moneyStack.Upd ??= new Upd { StackObjectsCount = 1 };
|
|
}
|
|
|
|
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) {
|
|
_logger.Error(
|
|
_localisationService.GetText("payment-not_enough_money_to_complete_transation", new {
|
|
amountToPay = amountToPay,
|
|
amountAvailable = amountAvailable,
|
|
})
|
|
);
|
|
_httpResponseUtil.AppendErrorToOutput(
|
|
output,
|
|
_localisationService.GetText("payment-not_enough_money_to_complete_transation_short", amountToPay),
|
|
BackendErrorCodes.UnknownTradingError
|
|
);
|
|
|
|
return;
|
|
}
|
|
|
|
var leftToPay = amountToPay;
|
|
foreach (var profileMoneyItem in moneyItemsInInventory) {
|
|
var itemAmount = profileMoneyItem.Upd.StackObjectsCount;
|
|
if (leftToPay >= itemAmount) {
|
|
leftToPay -= itemAmount ?? 0;
|
|
_inventoryHelper.RemoveItem(pmcData, profileMoneyItem.Id, sessionID, output);
|
|
} else {
|
|
profileMoneyItem.Upd.StackObjectsCount -= leftToPay;
|
|
leftToPay = 0;
|
|
output.ProfileChanges[sessionID].Items.ChangedItems.Add(_itemHelper.GetProductFromItem(profileMoneyItem));
|
|
}
|
|
|
|
if (leftToPay == 0) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* TODO - ensure money in containers inside secure container are LAST
|
|
* Get all money stacks in inventory and prioritise items in stash
|
|
* @param pmcData Player profile
|
|
* @param currencyTpl
|
|
* @param playerStashId Players stash id
|
|
* @returns Sorting money items
|
|
*/
|
|
protected List<Item> GetSortedMoneyItemsInInventory(PmcData pmcData, string currencyTpl, string playerStashId)
|
|
{
|
|
var moneyItemsInInventory = _itemHelper.FindBarterItems("tpl", pmcData.Inventory.Items, currencyTpl);
|
|
if (moneyItemsInInventory.Count == 0) {
|
|
_logger.Debug($"No {currencyTpl} money items found in inventory");
|
|
}
|
|
|
|
// Prioritise items in stash to top of array
|
|
moneyItemsInInventory.Sort((a, b) => PrioritiseStashSort(a, b, pmcData.Inventory.Items, playerStashId));
|
|
|
|
return moneyItemsInInventory;
|
|
}
|
|
|
|
/**
|
|
* Prioritise player stash first over player inventory
|
|
* Post-raid healing would often take money out of the players pockets/secure container
|
|
* @param a First money stack item
|
|
* @param b Second money stack item
|
|
* @param inventoryItems players inventory items
|
|
* @param playerStashId Players stash id
|
|
* @returns sort order
|
|
*/
|
|
protected int PrioritiseStashSort(Item a, Item b, List<Item> inventoryItems, string playerStashId)
|
|
{
|
|
// a in root of stash, prioritise
|
|
if (a.ParentId == playerStashId && b.ParentId != playerStashId) {
|
|
return -1;
|
|
}
|
|
|
|
// b in root stash, prioritise
|
|
if (a.ParentId != playerStashId && b.ParentId == playerStashId) {
|
|
return 1;
|
|
}
|
|
|
|
// both in containers
|
|
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) {
|
|
return -1;
|
|
}
|
|
|
|
// b in stash in container, prioritise
|
|
if (!aInStash && bInStash) {
|
|
return 1;
|
|
}
|
|
|
|
// Both in stash in containers
|
|
if (aInStash && bInStash) {
|
|
// Containers where taking money from would inconvinence player
|
|
var deprioritisedContainers = _inventoryConfig.DeprioritisedMoneyContainers;
|
|
var aImmediateParent = inventoryItems.FirstOrDefault((item) => item.Id == a.ParentId);
|
|
var bImmediateParent = inventoryItems.FirstOrDefault((item) => item.Id == b.ParentId);
|
|
|
|
// A is not a deprioritised container, B is
|
|
if (
|
|
!deprioritisedContainers.Contains(aImmediateParent.Template) &&
|
|
deprioritisedContainers.Contains(bImmediateParent.Template)
|
|
) {
|
|
return -1;
|
|
}
|
|
|
|
// B is not a deprioritised container, A is
|
|
if (
|
|
deprioritisedContainers.Contains(aImmediateParent.Template) &&
|
|
!deprioritisedContainers.Contains(bImmediateParent.Template)
|
|
) {
|
|
return 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
// they match
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Recursively check items parents to see if it is inside the players inventory, not stash
|
|
* @param itemId item id to check
|
|
* @param inventoryItems player inventory
|
|
* @param playerStashId Players stash id
|
|
* @returns true if its in inventory
|
|
*/
|
|
protected bool IsInStash(string itemId, List<Item> inventoryItems, string playerStashId)
|
|
{
|
|
var itemParent = inventoryItems.FirstOrDefault((item) => item.Id == itemId);
|
|
|
|
if (itemParent is not null) {
|
|
if (itemParent.SlotId == "hideout") {
|
|
return true;
|
|
}
|
|
|
|
if (itemParent.Id == playerStashId) {
|
|
return true;
|
|
}
|
|
|
|
return IsInStash(itemParent.ParentId, inventoryItems, playerStashId);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|