2023-03-03 16:23:46 +01:00
import { inject , injectable } from "tsyringe" ;
import { IPmcData } from "../models/eft/common/IPmcData" ;
import { Item , Location , Upd } from "../models/eft/common/tables/IItem" ;
import { AddItem , IAddItemRequestData } from "../models/eft/inventory/IAddItemRequestData" ;
import { IAddItemTempObject } from "../models/eft/inventory/IAddItemTempObject" ;
import { IInventoryMergeRequestData } from "../models/eft/inventory/IInventoryMergeRequestData" ;
import { IInventoryMoveRequestData } from "../models/eft/inventory/IInventoryMoveRequestData" ;
import { IInventorySplitRequestData } from "../models/eft/inventory/IInventorySplitRequestData" ;
import { IItemEventRouterResponse } from "../models/eft/itemEvent/IItemEventRouterResponse" ;
import { BaseClasses } from "../models/enums/BaseClasses" ;
import { ConfigTypes } from "../models/enums/ConfigTypes" ;
import { Traders } from "../models/enums/Traders" ;
import { IInventoryConfig , RewardDetails } from "../models/spt/config/IInventoryConfig" ;
import { ILogger } from "../models/spt/utils/ILogger" ;
import { ConfigServer } from "../servers/ConfigServer" ;
import { DatabaseServer } from "../servers/DatabaseServer" ;
import { FenceService } from "../services/FenceService" ;
import { LocalisationService } from "../services/LocalisationService" ;
import { HashUtil } from "../utils/HashUtil" ;
import { HttpResponseUtil } from "../utils/HttpResponseUtil" ;
import { JsonUtil } from "../utils/JsonUtil" ;
import { ContainerHelper } from "./ContainerHelper" ;
import { DialogueHelper } from "./DialogueHelper" ;
import { ItemHelper } from "./ItemHelper" ;
import { PaymentHelper } from "./PaymentHelper" ;
import { ProfileHelper } from "./ProfileHelper" ;
import { TraderAssortHelper } from "./TraderAssortHelper" ;
export interface OwnerInventoryItems
{
from : Item [ ]
to : Item [ ]
sameInventory : boolean ,
isMail : boolean
}
@injectable ( )
export class InventoryHelper
{
protected inventoryConfig : IInventoryConfig ;
constructor (
@inject ( "WinstonLogger" ) protected logger : ILogger ,
@inject ( "JsonUtil" ) protected jsonUtil : JsonUtil ,
@inject ( "HashUtil" ) protected hashUtil : HashUtil ,
@inject ( "HttpResponseUtil" ) protected httpResponse : HttpResponseUtil ,
@inject ( "FenceService" ) protected fenceService : FenceService ,
@inject ( "DatabaseServer" ) protected databaseServer : DatabaseServer ,
@inject ( "PaymentHelper" ) protected paymentHelper : PaymentHelper ,
@inject ( "TraderAssortHelper" ) protected traderAssortHelper : TraderAssortHelper ,
@inject ( "DialogueHelper" ) protected dialogueHelper : DialogueHelper ,
@inject ( "ItemHelper" ) protected itemHelper : ItemHelper ,
@inject ( "ContainerHelper" ) protected containerHelper : ContainerHelper ,
@inject ( "ProfileHelper" ) protected profileHelper : ProfileHelper ,
@inject ( "LocalisationService" ) protected localisationService : LocalisationService ,
@inject ( "ConfigServer" ) protected configServer : ConfigServer
)
{
this . inventoryConfig = this . configServer . getConfig ( ConfigTypes . INVENTORY ) ;
}
/ * *
* BUG : Passing the same item multiple times with a count of 1 will cause multiples of that item to be added ( e . g . x3 separate objects of tar cola with count of 1 = 9 tarcolas being added to inventory )
* @param pmcData Profile to add items to
* @param request request data to add items
* @param output response to send back to client
* @param sessionID Session id
* @param callback Code to execute later ( function )
* @param foundInRaid Will results added to inventory be set as found in raid
* @param addUpd Additional upd properties for items being added to inventory
* @returns IItemEventRouterResponse
* /
public addItem ( pmcData : IPmcData , request : IAddItemRequestData , output : IItemEventRouterResponse , sessionID : string , callback : { ( ) : void } , foundInRaid = false , addUpd = null ) : IItemEventRouterResponse
{
const itemLib : Item [ ] = [ ] ; // TODO: what is the purpose of this property
const itemsToAdd : IAddItemTempObject [ ] = [ ] ;
for ( const requestItem of request . items )
{
if ( requestItem . item_id in this . databaseServer . getTables ( ) . globals . ItemPresets )
{
const presetItems = this . jsonUtil . clone ( this . databaseServer . getTables ( ) . globals . ItemPresets [ requestItem . item_id ] . _items ) ;
itemLib . push ( . . . presetItems ) ;
requestItem . isPreset = true ;
requestItem . item_id = presetItems [ 0 ] . _id ;
}
else if ( this . paymentHelper . isMoneyTpl ( requestItem . item_id ) )
{
itemLib . push ( { _id : requestItem.item_id , _tpl : requestItem.item_id } ) ;
}
else if ( request . tid === Traders . FENCE )
{
const fenceItems = this . fenceService . getRawFenceAssorts ( ) . items ;
const itemIndex = fenceItems . findIndex ( i = > i . _id === requestItem . item_id ) ;
if ( itemIndex === - 1 )
{
this . logger . debug ( ` Tried to buy item ${ requestItem . item_id } from fence that no longer exists ` ) ;
const message = this . localisationService . getText ( "ragfair-offer_no_longer_exists" ) ;
return this . httpResponse . appendErrorToOutput ( output , message ) ;
}
// handle when item being bought is a preset
const item = fenceItems [ itemIndex ] ;
if ( item . upd ? . sptPresetId )
{
const presetItems = this . jsonUtil . clone ( this . databaseServer . getTables ( ) . globals . ItemPresets [ item . upd . sptPresetId ] . _items ) ;
itemLib . push ( . . . presetItems ) ;
requestItem . isPreset = true ;
requestItem . item_id = presetItems [ 0 ] . _id ;
addUpd = item . upd ; // Must persist the fence upd properties (e.g. durability/currentHp)
}
else
{
addUpd = item . upd ; // Must persist the fence upd properties (e.g. durability/currentHp)
itemLib . push ( { _id : requestItem.item_id , _tpl : item._tpl } ) ;
}
}
else if ( request . tid === "RandomLootContainer" )
{
itemLib . push ( { _id : requestItem.item_id , _tpl : requestItem.item_id } ) ;
}
else
{
// Only grab the relevant trader items and add unique values
const traderItems = this . traderAssortHelper . getAssort ( sessionID , request . tid ) . items ;
const relevantItems = this . itemHelper . findAndReturnChildrenAsItems ( traderItems , requestItem . item_id ) ;
const toAdd = relevantItems . filter ( traderItem = > ! itemLib . some ( item = > traderItem . _id === item . _id ) ) ;
itemLib . push ( . . . toAdd ) ;
}
// Split stacks into allowed sizes if needed
// e.g. when buying 300 ammo from flea but max stack size is 50
this . splitStackIntoSmallerStacks ( itemLib , requestItem , itemsToAdd ) ;
}
// Find an empty slot in stash for each of the items being added
let stashFS2D = this . getStashSlotMap ( pmcData , sessionID ) ;
for ( const itemToAdd of itemsToAdd )
{
const itemSize = this . getItemSize ( itemToAdd . itemRef . _tpl , itemToAdd . itemRef . _id , itemLib ) ;
const findSlotResult = this . containerHelper . findSlotForItem ( stashFS2D , itemSize [ 0 ] , itemSize [ 1 ] ) ;
if ( findSlotResult . success )
{
/ * F i l l i n t h e S t a s h F S _ 2 D w i t h a n i m a g i n a r y i t e m , t o s i m u l a t e i t a l r e a d y b e i n g a d d e d
* so the next item to search for a free slot won ' t find the same one * /
const itemSizeX = findSlotResult . rotation ? itemSize [ 1 ] : itemSize [ 0 ] ;
const itemSizeY = findSlotResult . rotation ? itemSize [ 0 ] : itemSize [ 1 ] ;
try
{
stashFS2D = this . containerHelper . fillContainerMapWithItem ( stashFS2D , findSlotResult . x , findSlotResult . y , itemSizeX , itemSizeY , false ) ; // TODO: rotation not passed in, bad?
}
catch ( err )
{
const errorText = typeof err === "string" ? ` -> ${ err } ` : "" ;
this . logger . error ( this . localisationService . getText ( "inventory-fill_container_failed" , errorText ) ) ;
return this . httpResponse . appendErrorToOutput ( output , this . localisationService . getText ( "inventory-no_stash_space" ) ) ;
}
itemToAdd . location = {
x : findSlotResult.x ,
y : findSlotResult.y ,
r : findSlotResult.rotation ? 1 : 0 ,
rotation : findSlotResult.rotation } ;
}
else
{
return this . httpResponse . appendErrorToOutput ( output , this . localisationService . getText ( "inventory-no_stash_space" ) ) ;
}
}
// Successfully found slot for every item, run callback, catch if it fails (e.g. payMoney() might fail)
try
{
if ( typeof callback === "function" )
{
callback ( ) ;
}
}
catch ( err )
{
const message = typeof err === "string"
? err
: this . localisationService . getText ( "http-unknown_error" ) ;
return this . httpResponse . appendErrorToOutput ( output , message ) ;
}
// Update UPD properties and add to output.profileChanges/pmcData.Inventory.items arrays
for ( const itemToAdd of itemsToAdd )
{
let idForItemToAdd = this . hashUtil . generate ( ) ;
const toDo : string [ ] [ ] = [ [ itemToAdd . itemRef . _id , idForItemToAdd ] ] ; // WHAT IS THIS?!
let upd : Upd = { StackObjectsCount : itemToAdd.count } ;
// If item being added is preset, load preset's upd data too.
if ( itemToAdd . isPreset )
{
for ( const updID in itemToAdd . itemRef . upd )
{
upd [ updID ] = itemToAdd . itemRef . upd [ updID ] ;
}
}
// Item has buff, add to item being sent to player
if ( itemToAdd . itemRef . upd ? . Buff )
{
upd . Buff = this . jsonUtil . clone ( itemToAdd . itemRef . upd . Buff ) ;
}
// add ragfair upd properties
if ( addUpd )
{
upd = { . . . addUpd , . . . upd } ;
}
// Hideout items need to be marked as found in raid
// Or in case people want all items to be marked as found in raid
if ( foundInRaid || this . inventoryConfig . newItemsMarkedFound )
{
upd . SpawnedInSession = true ;
}
// Remove invalid property prior to adding to inventory
if ( upd . UnlimitedCount )
{
delete upd . UnlimitedCount ;
}
output . profileChanges [ sessionID ] . items . new . push ( {
_id : idForItemToAdd ,
_tpl : itemToAdd.itemRef._tpl ,
parentId : pmcData.Inventory.stash ,
slotId : "hideout" ,
location : { x : itemToAdd.location.x , y : itemToAdd.location.y , r : itemToAdd.location.rotation ? 1 : 0 } ,
upd : upd
} ) ;
pmcData . Inventory . items . push ( {
_id : idForItemToAdd ,
_tpl : itemToAdd.itemRef._tpl ,
parentId : pmcData.Inventory.stash ,
slotId : "hideout" ,
location : { x : itemToAdd.location.x , y : itemToAdd.location.y , r : itemToAdd.location.rotation ? 1 : 0 } ,
upd : this.jsonUtil.clone ( upd ) // Clone upd to prevent multi-purchases of same item referencing same upd object in memory
} ) ;
if ( this . itemHelper . isOfBaseclass ( itemToAdd . itemRef . _tpl , BaseClasses . AMMO_BOX ) )
{
this . hydrateAmmoBoxWithAmmo ( pmcData , itemToAdd , toDo [ 0 ] [ 1 ] , sessionID , output ) ;
}
while ( toDo . length > 0 )
{
for ( const tmpKey in itemLib )
{
if ( itemLib [ tmpKey ] . parentId && itemLib [ tmpKey ] . parentId === toDo [ 0 ] [ 0 ] )
{
idForItemToAdd = this . hashUtil . generate ( ) ;
const slotID = itemLib [ tmpKey ] . slotId ;
// if it is from ItemPreset, load preset's upd data too.
if ( itemToAdd . isPreset )
{
upd = { "StackObjectsCount" : itemToAdd . count } ;
for ( const updID in itemLib [ tmpKey ] . upd )
{
upd [ updID ] = itemLib [ tmpKey ] . upd [ updID ] ;
}
if ( foundInRaid || this . inventoryConfig . newItemsMarkedFound )
{
upd . SpawnedInSession = true ;
}
}
if ( slotID === "hideout" )
{
output . profileChanges [ sessionID ] . items . new . push ( {
_id : idForItemToAdd ,
_tpl : itemLib [ tmpKey ] . _tpl ,
parentId : toDo [ 0 ] [ 1 ] ,
slotId : slotID ,
location : {
x : itemToAdd.location.x ,
y : itemToAdd.location.y ,
r : "Horizontal" } ,
upd : upd
} ) ;
pmcData . Inventory . items . push ( {
_id : idForItemToAdd ,
_tpl : itemLib [ tmpKey ] . _tpl ,
parentId : toDo [ 0 ] [ 1 ] ,
slotId : itemLib [ tmpKey ] . slotId ,
location : {
x : itemToAdd.location.x ,
y : itemToAdd.location.y ,
r : "Horizontal" } ,
upd : upd
} ) ;
}
else
{
const itemLocation = { } ;
if ( itemLib [ tmpKey ] [ "location" ] !== undefined )
{
itemLocation [ "location" ] = itemLib [ tmpKey ] [ "location" ] ;
}
output . profileChanges [ sessionID ] . items . new . push ( {
_id : idForItemToAdd ,
_tpl : itemLib [ tmpKey ] . _tpl ,
parentId : toDo [ 0 ] [ 1 ] ,
slotId : slotID ,
. . . itemLocation ,
upd : upd
} ) ;
pmcData . Inventory . items . push ( {
_id : idForItemToAdd ,
_tpl : itemLib [ tmpKey ] . _tpl ,
parentId : toDo [ 0 ] [ 1 ] ,
slotId : itemLib [ tmpKey ] . slotId ,
. . . itemLocation ,
upd : upd
} ) ;
2023-06-20 17:07:05 +02:00
this . logger . debug ( ` Added ${ itemLib [ tmpKey ] . _tpl } with id: ${ idForItemToAdd } to inventory ` ) ;
2023-03-03 16:23:46 +01:00
}
toDo . push ( [ itemLib [ tmpKey ] . _id , idForItemToAdd ] ) ;
}
}
toDo . splice ( 0 , 1 ) ;
}
}
return output ;
}
/ * *
* Add ammo to ammo boxes
* @param itemToAdd Item to check is ammo box
* @param parentId Ammo box parent id
* @param output IItemEventRouterResponse object
* @param sessionID Session id
* @param pmcData Profile to add ammobox to
* /
protected hydrateAmmoBoxWithAmmo ( pmcData : IPmcData , itemToAdd : IAddItemTempObject , parentId : string , sessionID : string , output : IItemEventRouterResponse ) : void
{
const itemInfo = this . itemHelper . getItem ( itemToAdd . itemRef . _tpl ) [ 1 ] ;
const stackSlots = itemInfo . _props . StackSlots ;
if ( stackSlots !== undefined )
{
// Cartridge info seems to be an array of size 1 for some reason... (See AmmoBox constructor in client code)
let maxCount = stackSlots [ 0 ] . _max_count ;
const ammoTpl = stackSlots [ 0 ] . _props . filters [ 0 ] . Filter [ 0 ] ;
const ammoStackMaxSize = this . itemHelper . getItem ( ammoTpl ) [ 1 ] . _props . StackMaxSize ;
const ammos = [ ] ;
let location = 0 ;
// Place stacks in ammo box no larger than StackMaxSize, prevents player when opening item getting stack of ammo > StackMaxSize
while ( maxCount > 0 )
{
const ammoStackSize = maxCount <= ammoStackMaxSize ? maxCount : ammoStackMaxSize ;
ammos . push ( {
_id : this.hashUtil.generate ( ) ,
_tpl : ammoTpl ,
parentId : parentId ,
slotId : "cartridges" ,
location : location ,
upd : { "StackObjectsCount" : ammoStackSize }
} ) ;
location ++ ;
maxCount -= ammoStackMaxSize ;
}
for ( const item of [ output . profileChanges [ sessionID ] . items . new , pmcData . Inventory . items ] )
{
item . push ( . . . ammos ) ;
}
}
}
/ * *
*
* @param assortItems Items to add to inventory
* @param requestItem Details of purchased item to add to inventory
* @param result Array split stacks are added to
* /
protected splitStackIntoSmallerStacks ( assortItems : Item [ ] , requestItem : AddItem , result : IAddItemTempObject [ ] ) : void
{
for ( const item of assortItems )
{
if ( item . _id === requestItem . item_id )
{
// Get item details from db
const itemDetails = this . itemHelper . getItem ( item . _tpl ) [ 1 ] ;
const itemToAdd : IAddItemTempObject = {
itemRef : item ,
count : requestItem.count ,
isPreset : requestItem.isPreset } ;
// Split stacks if the size is higher than allowed by items StackMaxSize property
let maxStackCount = 1 ;
if ( requestItem . count > itemDetails . _props . StackMaxSize )
{
let remainingCountOfItemToAdd = requestItem . count ;
const calc = requestItem . count - ( Math . floor ( requestItem . count / itemDetails . _props . StackMaxSize ) * itemDetails . _props . StackMaxSize ) ;
maxStackCount = ( calc > 0 )
? maxStackCount + Math . floor ( remainingCountOfItemToAdd / itemDetails . _props . StackMaxSize )
: Math . floor ( remainingCountOfItemToAdd / itemDetails . _props . StackMaxSize ) ;
// Iterate until totalCountOfPurchasedItem is 0
for ( let i = 0 ; i < maxStackCount ; i ++ )
{
// Keep splitting items into stacks until none left
if ( remainingCountOfItemToAdd > 0 )
{
const newItemToAdd = this . jsonUtil . clone ( itemToAdd ) ;
if ( remainingCountOfItemToAdd > itemDetails . _props . StackMaxSize )
{
// Reduce total count of item purchased by stack size we're going to add to inventory
remainingCountOfItemToAdd -= itemDetails . _props . StackMaxSize ;
newItemToAdd . count = itemDetails . _props . StackMaxSize ;
}
else
{
newItemToAdd . count = remainingCountOfItemToAdd ;
}
result . push ( newItemToAdd ) ;
}
}
}
else
{
// Item count is within allowed stack size, just add it
result . push ( itemToAdd ) ;
}
}
}
}
/ * *
2023-07-15 12:00:35 +02:00
* Remove item from player inventory + insured items array
2023-03-03 16:23:46 +01:00
* @param pmcData Profile to remove item from
* @param itemId Items id to remove
* @param sessionID Session id
* @param output Existing IItemEventRouterResponse object to append data to , creates new one by default if not supplied
* @returns IItemEventRouterResponse
* /
public removeItem ( pmcData : IPmcData , itemId : string , sessionID : string , output : IItemEventRouterResponse = undefined ) : IItemEventRouterResponse
{
if ( ! itemId )
return output ;
const childIds = this . itemHelper . findAndReturnChildrenByItems ( pmcData . Inventory . items , itemId ) ;
const inventoryItems = pmcData . Inventory . items ;
const insuredItems = pmcData . InsuredItems ;
if ( output )
output . profileChanges [ sessionID ] . items . del . push ( { "_id" : itemId } ) ;
for ( const childId of childIds )
{
// We expect that each inventory item and each insured item has unique "_id", respective "itemId".
// Therefore we want to use a NON-Greedy function and escape the iteration as soon as we find requested item.
const inventoryIndex = inventoryItems . findIndex ( item = > item . _id === childId ) ;
if ( inventoryIndex > - 1 )
{
inventoryItems . splice ( inventoryIndex , 1 ) ;
}
const insuredIndex = insuredItems . findIndex ( item = > item . itemId === childId ) ;
if ( insuredIndex > - 1 )
{
insuredItems . splice ( insuredIndex , 1 ) ;
}
}
return output ;
}
public removeItemByCount ( pmcData : IPmcData , itemId : string , count : number , sessionID : string , output : IItemEventRouterResponse = undefined ) : IItemEventRouterResponse
{
if ( ! itemId )
return output ;
const itemsToReduce = this . itemHelper . findAndReturnChildrenAsItems ( pmcData . Inventory . items , itemId ) ;
let remainingCount = count ;
for ( const itemToReduce of itemsToReduce )
{
const itemCount = this . itemHelper . getItemStackSize ( itemToReduce ) ;
// remove whole stack
if ( remainingCount >= itemCount )
{
remainingCount -= itemCount ;
this . removeItem ( pmcData , itemToReduce . _id , sessionID , output ) ;
}
else
{
itemToReduce . upd . StackObjectsCount -= remainingCount ;
remainingCount = 0 ;
if ( output )
output . profileChanges [ sessionID ] . items . change . push ( itemToReduce ) ;
}
if ( remainingCount === 0 )
break ;
}
return output ;
}
/ * C a l c u l a t e S i z e o f i t e m i n p u t
* inputs Item template ID , Item Id , InventoryItem ( item from inventory having _id and _tpl )
* outputs [ width , height ]
* /
2023-04-12 16:51:52 +02:00
public getItemSize ( itemTpl : string , itemID : string , inventoryItem : Item [ ] ) : number [ ]
2023-03-03 16:23:46 +01:00
{
// -> Prepares item Width and height returns [sizeX, sizeY]
return this . getSizeByInventoryItemHash ( itemTpl , itemID , this . getInventoryItemHash ( inventoryItem ) ) ;
}
// note from 2027: there IS a thing i didn't explore and that is Merges With Children
// -> Prepares item Width and height returns [sizeX, sizeY]
2023-04-12 16:51:52 +02:00
protected getSizeByInventoryItemHash ( itemTpl : string , itemID : string , inventoryItemHash : InventoryHelper.InventoryItemHash ) : number [ ]
2023-03-03 16:23:46 +01:00
{
const toDo = [ itemID ] ;
const result = this . itemHelper . getItem ( itemTpl ) ;
const tmpItem = result [ 1 ] ;
2023-04-12 16:51:52 +02:00
// Invalid item or no object
2023-03-03 16:23:46 +01:00
if ( ! ( result [ 0 ] && result [ 1 ] ) )
{
this . logger . error ( this . localisationService . getText ( "inventory-invalid_item_missing_from_db" , itemTpl ) ) ;
}
2023-04-12 16:51:52 +02:00
// item found but no _props property
if ( tmpItem && ! tmpItem . _props )
2023-03-03 16:23:46 +01:00
{
2023-04-12 16:51:52 +02:00
this . logger . error ( ` Item ${ itemTpl } ${ tmpItem ? . _name } is missing a props field, a size for it cannot be acquired ` ) ;
}
// No item object or getItem() returned false
if ( ! ( tmpItem && result [ 0 ] ) )
{
// return default size of 1x1
this . logger . error ( this . localisationService . getText ( "inventory-return_default_size" , itemTpl ) ) ;
return [ 1 , 1 ] ;
2023-03-03 16:23:46 +01:00
}
const rootItem = inventoryItemHash . byItemId [ itemID ] ;
const foldableWeapon = tmpItem . _props . Foldable ;
const foldedSlot = tmpItem . _props . FoldedSlot ;
let sizeUp = 0 ;
let sizeDown = 0 ;
let sizeLeft = 0 ;
let sizeRight = 0 ;
let forcedUp = 0 ;
let forcedDown = 0 ;
let forcedLeft = 0 ;
let forcedRight = 0 ;
let outX = tmpItem . _props . Width ;
const outY = tmpItem . _props . Height ;
const skipThisItems : string [ ] = [
BaseClasses . BACKPACK ,
BaseClasses . SEARCHABLE_ITEM ,
BaseClasses . SIMPLE_CONTAINER
] ;
const rootFolded = rootItem . upd ? . Foldable && rootItem . upd . Foldable . Folded === true ;
//The item itself is collapsible
if ( foldableWeapon && ( foldedSlot === undefined || foldedSlot === "" ) && rootFolded )
{
outX -= tmpItem . _props . SizeReduceRight ;
}
if ( ! skipThisItems . includes ( tmpItem . _parent ) )
{
while ( toDo . length > 0 )
{
if ( toDo [ 0 ] in inventoryItemHash . byParentId )
{
for ( const item of inventoryItemHash . byParentId [ toDo [ 0 ] ] )
{
//Filtering child items outside of mod slots, such as those inside containers, without counting their ExtraSize attribute
if ( item . slotId . indexOf ( "mod_" ) < 0 )
{
continue ;
}
toDo . push ( item . _id ) ;
// If the barrel is folded the space in the barrel is not counted
const itemResult = this . itemHelper . getItem ( item . _tpl ) ;
if ( ! itemResult [ 0 ] )
{
this . logger . error ( ` getSizeByInventoryItemHash() Item with tpl: ${ item . _tpl } not found ` ) ;
}
const itm = itemResult [ 1 ] ;
const childFoldable = itm . _props . Foldable ;
const childFolded = item . upd ? . Foldable && item . upd . Foldable . Folded === true ;
if ( foldableWeapon && foldedSlot === item . slotId && ( rootFolded || childFolded ) )
{
continue ;
}
else if ( childFoldable && rootFolded && childFolded )
{
continue ;
}
// Calculating child ExtraSize
if ( itm . _props . ExtraSizeForceAdd === true )
{
forcedUp += itm . _props . ExtraSizeUp ;
forcedDown += itm . _props . ExtraSizeDown ;
forcedLeft += itm . _props . ExtraSizeLeft ;
forcedRight += itm . _props . ExtraSizeRight ;
}
else
{
sizeUp = sizeUp < itm . _props . ExtraSizeUp ? itm._props.ExtraSizeUp : sizeUp ;
sizeDown = sizeDown < itm . _props . ExtraSizeDown ? itm._props.ExtraSizeDown : sizeDown ;
sizeLeft = sizeLeft < itm . _props . ExtraSizeLeft ? itm._props.ExtraSizeLeft : sizeLeft ;
sizeRight = sizeRight < itm . _props . ExtraSizeRight ? itm._props.ExtraSizeRight : sizeRight ;
}
}
}
toDo . splice ( 0 , 1 ) ;
}
}
return [
outX + sizeLeft + sizeRight + forcedLeft + forcedRight ,
outY + sizeUp + sizeDown + forcedUp + forcedDown
] ;
}
protected getInventoryItemHash ( inventoryItem : Item [ ] ) : InventoryHelper . InventoryItemHash
{
const inventoryItemHash : InventoryHelper.InventoryItemHash = {
"byItemId" : { } ,
"byParentId" : { }
} ;
for ( let i = 0 ; i < inventoryItem . length ; i ++ )
{
const item = inventoryItem [ i ] ;
inventoryItemHash . byItemId [ item . _id ] = item ;
if ( ! ( "parentId" in item ) )
{
continue ;
}
if ( ! ( item . parentId in inventoryItemHash . byParentId ) )
{
inventoryItemHash . byParentId [ item . parentId ] = [ ] ;
}
inventoryItemHash . byParentId [ item . parentId ] . push ( item ) ;
}
return inventoryItemHash ;
}
public getContainerMap ( containerW : number , containerH : number , itemList : Item [ ] , containerId : string ) : number [ ] [ ]
{
const container2D : number [ ] [ ] = Array ( containerH ) . fill ( 0 ) . map ( ( ) = > Array ( containerW ) . fill ( 0 ) ) ;
const inventoryItemHash = this . getInventoryItemHash ( itemList ) ;
const containerItemHash = inventoryItemHash . byParentId [ containerId ] ;
if ( ! containerItemHash )
{
// No items in the container
return container2D ;
}
for ( const item of containerItemHash )
{
if ( ! ( "location" in item ) )
{
continue ;
}
const tmpSize = this . getSizeByInventoryItemHash ( item . _tpl , item . _id , inventoryItemHash ) ;
const iW = tmpSize [ 0 ] ; // x
const iH = tmpSize [ 1 ] ; // y
const fH = ( ( ( item . location as Location ) . r === 1 || ( item . location as Location ) . r === "Vertical" || ( item . location as Location ) . rotation === "Vertical" ) ? iW : iH ) ;
const fW = ( ( ( item . location as Location ) . r === 1 || ( item . location as Location ) . r === "Vertical" || ( item . location as Location ) . rotation === "Vertical" ) ? iH : iW ) ;
const fillTo = ( item . location as Location ) . x + fW ;
for ( let y = 0 ; y < fH ; y ++ )
{
try
{
container2D [ ( item . location as Location ) . y + y ] . fill ( 1 , ( item . location as Location ) . x , fillTo ) ;
}
catch ( e )
{
this . logger . error ( this . localisationService . getText ( "inventory-unable_to_fill_container" , { id : item._id , error : e } ) ) ;
}
}
}
return container2D ;
}
/ * *
* Based on the item action , determine whose inventories we should be looking at for from and to .
* /
public getOwnerInventoryItems ( body : IInventoryMoveRequestData | IInventorySplitRequestData | IInventoryMergeRequestData , sessionID : string ) : OwnerInventoryItems
{
let isSameInventory = false ;
const pmcItems = this . profileHelper . getPmcProfile ( sessionID ) . Inventory . items ;
const scavData = this . profileHelper . getScavProfile ( sessionID ) ;
let fromInventoryItems = pmcItems ;
let fromType = "pmc" ;
if ( "fromOwner" in body )
{
if ( body . fromOwner . id === scavData . _id )
{
fromInventoryItems = scavData . Inventory . items ;
fromType = "scav" ;
}
else if ( body . fromOwner . type . toLocaleLowerCase ( ) === "mail" )
{
fromInventoryItems = this . dialogueHelper . getMessageItemContents ( body . fromOwner . id , sessionID , body . item ) ;
fromType = "mail" ;
}
}
// Don't need to worry about mail for destination because client doesn't allow
// users to move items back into the mail stash.
let toInventoryItems = pmcItems ;
let toType = "pmc" ;
if ( "toOwner" in body && body . toOwner . id === scavData . _id )
{
toInventoryItems = scavData . Inventory . items ;
toType = "scav" ;
}
if ( fromType === toType )
{
isSameInventory = true ;
}
return {
from : fromInventoryItems ,
to : toInventoryItems ,
sameInventory : isSameInventory ,
isMail : fromType === "mail"
} ;
}
/ * *
* Made a 2 d array table with 0 - free slot and 1 - used slot
* @param { Object } pmcData
* @param { string } sessionID
* @returns Array
* /
protected getStashSlotMap ( pmcData : IPmcData , sessionID : string ) : number [ ] [ ]
{
const playerStashSize = this . getPlayerStashSize ( sessionID ) ;
return this . getContainerMap ( playerStashSize [ 0 ] , playerStashSize [ 1 ] , pmcData . Inventory . items , pmcData . Inventory . stash ) ;
}
protected getStashType ( sessionID : string ) : string
{
const pmcData = this . profileHelper . getPmcProfile ( sessionID ) ;
const stashObj = pmcData . Inventory . items . find ( item = > item . _id === pmcData . Inventory . stash ) ;
if ( ! stashObj )
{
this . logger . error ( this . localisationService . getText ( "inventory-unable_to_find_stash" ) ) ;
return "" ;
}
return stashObj . _tpl ;
}
/ * G e t P l a y e r S t a s h P r o p e r S i z e
* input : null
* output : [ stashSizeWidth , stashSizeHeight ]
* * /
protected getPlayerStashSize ( sessionID : string ) : Record < number , number >
{
//this sets automatically a stash size from items.json (its not added anywhere yet cause we still use base stash)
const stashTPL = this . getStashType ( sessionID ) ;
const stashX = this . databaseServer . getTables ( ) . templates . items [ stashTPL ] . _props . Grids [ 0 ] . _props . cellsH !== 0
? this . databaseServer . getTables ( ) . templates . items [ stashTPL ] . _props . Grids [ 0 ] . _props . cellsH
: 10 ;
const stashY = this . databaseServer . getTables ( ) . templates . items [ stashTPL ] . _props . Grids [ 0 ] . _props . cellsV !== 0
? this . databaseServer . getTables ( ) . templates . items [ stashTPL ] . _props . Grids [ 0 ] . _props . cellsV
: 66 ;
return [ stashX , stashY ] ;
}
/ * *
* Internal helper function to transfer an item from one profile to another .
* fromProfileData : Profile of the source .
* toProfileData : Profile of the destination .
* body : Move request
* /
public moveItemToProfile ( fromItems : Item [ ] , toItems : Item [ ] , body : IInventoryMoveRequestData ) : void
{
this . handleCartridges ( fromItems , body ) ;
const idsToMove = this . itemHelper . findAndReturnChildrenByItems ( fromItems , body . item ) ;
for ( const itemId of idsToMove )
{
for ( const itemIndex in fromItems )
{
if ( fromItems [ itemIndex ] . _id && fromItems [ itemIndex ] . _id === itemId )
{
if ( itemId === body . item )
{
fromItems [ itemIndex ] . parentId = body . to . id ;
fromItems [ itemIndex ] . slotId = body . to . container ;
if ( "location" in body . to )
{
fromItems [ itemIndex ] . location = body . to . location ;
}
else
{
if ( fromItems [ itemIndex ] . location )
{
delete fromItems [ itemIndex ] . location ;
}
}
}
toItems . push ( fromItems [ itemIndex ] ) ;
fromItems . splice ( parseInt ( itemIndex ) , 1 ) ;
}
}
}
}
/ * *
* Internal helper function to move item within the same profile_f .
* /
2023-03-03 18:53:28 +01:00
public moveItemInternal ( pmcData : IPmcData , inventoryItems : Item [ ] , moveRequest : IInventoryMoveRequestData ) : void
2023-03-03 16:23:46 +01:00
{
2023-03-03 18:53:28 +01:00
this . handleCartridges ( inventoryItems , moveRequest ) ;
2023-03-03 16:23:46 +01:00
2023-06-30 20:30:49 +02:00
// Find item we want to 'move'
const matchingInventoryItem = inventoryItems . find ( x = > x . _id === moveRequest . item ) ;
if ( matchingInventoryItem )
2023-03-03 16:23:46 +01:00
{
2023-06-30 20:30:49 +02:00
this . logger . debug ( ` ${ moveRequest . Action } item: ${ moveRequest . item } from slotid: ${ matchingInventoryItem . slotId } to container: ${ moveRequest . to . container } ` ) ;
2023-03-03 16:23:46 +01:00
2023-06-30 20:30:49 +02:00
// don't move shells from camora to cartridges (happens when loading shells into mts-255 revolver shotgun)
if ( matchingInventoryItem . slotId . includes ( "camora_" ) && moveRequest . to . container === "cartridges" )
{
this . logger . warning ( this . localisationService . getText ( "inventory-invalid_move_to_container" , { slotId : matchingInventoryItem.slotId , container : moveRequest.to.container } ) ) ;
return ;
}
2023-03-03 16:23:46 +01:00
2023-06-30 20:30:49 +02:00
// Edit items details to match its new location
matchingInventoryItem . parentId = moveRequest . to . id ;
matchingInventoryItem . slotId = moveRequest . to . container ;
2023-03-03 16:23:46 +01:00
2023-06-30 20:30:49 +02:00
this . updateFastPanelBinding ( pmcData , matchingInventoryItem ) ;
2023-03-03 18:53:28 +01:00
2023-06-30 20:30:49 +02:00
if ( "location" in moveRequest . to )
{
matchingInventoryItem . location = moveRequest . to . location ;
}
else
{
if ( matchingInventoryItem . location )
2023-03-03 16:23:46 +01:00
{
2023-06-30 20:30:49 +02:00
delete matchingInventoryItem . location ;
2023-03-03 16:23:46 +01:00
}
}
}
}
2023-03-03 18:53:28 +01:00
/ * *
* Update fast panel bindings when an item is moved into a container that doesnt allow quick slot access
* @param pmcData Player profile
* @param itemBeingMoved item being moved
* /
protected updateFastPanelBinding ( pmcData : IPmcData , itemBeingMoved : Item ) : void
{
// Find matching itemid in fast panel
for ( const itemKey in pmcData . Inventory . fastPanel )
{
if ( pmcData . Inventory . fastPanel [ itemKey ] === itemBeingMoved . _id )
{
// Get moved items parent
const itemParent = pmcData . Inventory . items . find ( x = > x . _id === itemBeingMoved . parentId ) ;
// Empty out id if item is moved to a container other than pocket/rig
if ( itemParent && ! ( itemParent . slotId ? . startsWith ( "Pockets" ) || itemParent . slotId === "TacticalVest" ) )
{
pmcData . Inventory . fastPanel [ itemKey ] = "" ;
}
break ;
}
}
}
2023-03-03 16:23:46 +01:00
/ * *
* Internal helper function to handle cartridges in inventory if any of them exist .
* /
protected handleCartridges ( items : Item [ ] , body : IInventoryMoveRequestData ) : void
{
// -> Move item to different place - counts with equipping filling magazine etc
if ( body . to . container === "cartridges" )
{
let tmpCounter = 0 ;
for ( const itemAmmo in items )
{
if ( body . to . id === items [ itemAmmo ] . parentId )
{
tmpCounter ++ ;
}
}
// wrong location for first cartridge
body . to . location = tmpCounter ;
}
}
/ * *
* Get details for how a random loot container should be handled , max rewards , possible reward tpls
* @param itemTpl Container being opened
* @returns Reward details
* /
public getRandomLootContainerRewardDetails ( itemTpl : string ) : RewardDetails
{
return this . inventoryConfig . randomLootContainers [ itemTpl ] ;
}
2023-06-20 17:07:05 +02:00
public getInventoryConfig ( ) : IInventoryConfig
{
return this . inventoryConfig ;
}
2023-03-03 16:23:46 +01:00
}
namespace InventoryHelper
{
export interface InventoryItemHash
{
byItemId : Record < string , Item >
byParentId : Record < string , Item [ ] >
}
}