2023-03-03 15:23:46 +00:00
import { inject , injectable } from "tsyringe" ;
import { DialogueHelper } from "../helpers/DialogueHelper" ;
import { ItemHelper } from "../helpers/ItemHelper" ;
import { ProfileHelper } from "../helpers/ProfileHelper" ;
2023-10-10 11:03:20 +00:00
import { TraderHelper } from "../helpers/TraderHelper" ;
2023-03-03 15:23:46 +00:00
import { IPmcData } from "../models/eft/common/IPmcData" ;
import { Item } from "../models/eft/common/tables/IItem" ;
2023-10-10 11:03:20 +00:00
import { ITemplateItem } from "../models/eft/common/tables/ITemplateItem" ;
2023-03-03 15:23:46 +00:00
import { IGetInsuranceCostRequestData } from "../models/eft/insurance/IGetInsuranceCostRequestData" ;
import {
IGetInsuranceCostResponseData
} from "../models/eft/insurance/IGetInsuranceCostResponseData" ;
import { IInsureRequestData } from "../models/eft/insurance/IInsureRequestData" ;
import { IItemEventRouterResponse } from "../models/eft/itemEvent/IItemEventRouterResponse" ;
2023-10-10 11:03:20 +00:00
import { Insurance } from "../models/eft/profile/IAkiProfile" ;
2023-03-03 15:23:46 +00:00
import { IProcessBuyTradeRequestData } from "../models/eft/trade/IProcessBuyTradeRequestData" ;
import { ConfigTypes } from "../models/enums/ConfigTypes" ;
2023-10-10 11:03:20 +00:00
import { MessageType } from "../models/enums/MessageType" ;
2023-03-03 15:23:46 +00:00
import { IInsuranceConfig } from "../models/spt/config/IInsuranceConfig" ;
import { ILogger } from "../models/spt/utils/ILogger" ;
import { EventOutputHolder } from "../routers/EventOutputHolder" ;
import { ConfigServer } from "../servers/ConfigServer" ;
import { DatabaseServer } from "../servers/DatabaseServer" ;
import { SaveServer } from "../servers/SaveServer" ;
import { InsuranceService } from "../services/InsuranceService" ;
2023-10-10 11:03:20 +00:00
import { MailSendService } from "../services/MailSendService" ;
2023-03-03 15:23:46 +00:00
import { PaymentService } from "../services/PaymentService" ;
import { RandomUtil } from "../utils/RandomUtil" ;
import { TimeUtil } from "../utils/TimeUtil" ;
@injectable ( )
export class InsuranceController
{
protected insuranceConfig : IInsuranceConfig ;
constructor (
@inject ( "WinstonLogger" ) protected logger : ILogger ,
@inject ( "RandomUtil" ) protected randomUtil : RandomUtil ,
@inject ( "EventOutputHolder" ) protected eventOutputHolder : EventOutputHolder ,
@inject ( "TimeUtil" ) protected timeUtil : TimeUtil ,
@inject ( "SaveServer" ) protected saveServer : SaveServer ,
@inject ( "DatabaseServer" ) protected databaseServer : DatabaseServer ,
@inject ( "ItemHelper" ) protected itemHelper : ItemHelper ,
@inject ( "ProfileHelper" ) protected profileHelper : ProfileHelper ,
@inject ( "DialogueHelper" ) protected dialogueHelper : DialogueHelper ,
2023-10-10 11:03:20 +00:00
@inject ( "TraderHelper" ) protected traderHelper : TraderHelper ,
2023-03-03 15:23:46 +00:00
@inject ( "PaymentService" ) protected paymentService : PaymentService ,
@inject ( "InsuranceService" ) protected insuranceService : InsuranceService ,
2023-10-10 11:03:20 +00:00
@inject ( "MailSendService" ) protected mailSendService : MailSendService ,
2023-03-03 15:23:46 +00:00
@inject ( "ConfigServer" ) protected configServer : ConfigServer
)
{
this . insuranceConfig = this . configServer . getConfig ( ConfigTypes . INSURANCE ) ;
}
/ * *
2023-10-10 11:03:20 +00:00
* Process insurance items of all profiles prior to being given back to the player through the mail service .
*
* @returns void
* /
2023-03-03 15:23:46 +00:00
public processReturn ( ) : void
{
2023-10-10 11:03:20 +00:00
// Process each installed profile.
2023-03-03 15:23:46 +00:00
for ( const sessionID in this . saveServer . getProfiles ( ) )
{
2023-10-10 11:03:20 +00:00
this . processReturnByProfile ( sessionID ) ;
}
}
2023-03-03 15:23:46 +00:00
2023-10-10 11:03:20 +00:00
/ * *
* Process insurance items of a single profile prior to being given back to the player through the mail service .
*
* @returns void
* /
public processReturnByProfile ( sessionID : string ) : void
{
// Filter out items that don't need to be processed yet.
const insuranceDetails = this . filterInsuredItems ( sessionID ) ;
2023-03-03 15:23:46 +00:00
2023-10-10 11:03:20 +00:00
// Skip profile if no insured items to process
if ( insuranceDetails . length === 0 )
{
return ;
}
2023-03-03 15:23:46 +00:00
2023-10-10 11:03:20 +00:00
this . processInsuredItems ( insuranceDetails , sessionID ) ;
}
2023-03-03 15:23:46 +00:00
2023-10-10 11:03:20 +00:00
/ * *
* Get all insured items that are ready to be processed in a specific profile .
*
* @param sessionID Session ID of the profile to check .
* @param time The time to check ready status against . Current time by default .
* @returns All insured items that are ready to be processed .
* /
protected filterInsuredItems ( sessionID : string , time? : number ) : Insurance [ ]
{
// Use the current time by default.
if ( ! time )
{
time = this . timeUtil . getTimestamp ( ) ;
}
2023-03-03 15:23:46 +00:00
2023-10-10 11:03:20 +00:00
const profileInsuranceDetails = this . saveServer . getProfile ( sessionID ) . insurance ;
this . logger . debug ( ` Found ${ profileInsuranceDetails . length } insurance packages in profile ${ sessionID } ` ) ;
2023-03-03 15:23:46 +00:00
2023-10-10 11:03:20 +00:00
return profileInsuranceDetails . filter ( ( insured ) = > time >= insured . scheduledTime ) ;
}
/ * *
* This method orchestrates the processing of insured items in a profile .
*
* @param insuranceDetails The insured items to process .
* @param sessionID The session ID that should receive the processed items .
* @returns void
* /
protected processInsuredItems ( insuranceDetails : Insurance [ ] , sessionID : string ) : void
{
this . logger . debug ( ` Processing ${ insuranceDetails . length } insurance packages, which include ${ insuranceDetails . map ( ins = > ins . items . length ) . reduce ( ( acc , len ) = > acc + len , 0 ) } items, for profile ${ sessionID } ` ) ;
2023-03-03 15:23:46 +00:00
2023-10-10 11:03:20 +00:00
// We start from the end of the array and move towards the beginning, removing elements as we go. This way, the
// indices of the elements that have not been processed yet do not change, which ensures deletions never miss.
for ( let i = insuranceDetails . length - 1 ; i >= 0 ; i -- )
{
const insured = insuranceDetails [ i ] ;
// Find items that should be deleted from the insured items.
const itemsToDelete = this . findItemsToDelete ( insured ) ;
// Actually remove them.
this . removeItemsFromInsurance ( insured , itemsToDelete ) ;
// Send the mail to the player.
this . sendMail ( sessionID , insured , insured . items . length === 0 ) ;
// Remove the insurance package from the profile now that it's been fully processed.
this . saveServer . getProfile ( sessionID ) . insurance . splice ( i , 1 ) ;
}
}
/ * *
* Build an array of items to delete from the insured items .
*
* This method orchestrates several steps :
* - Filters items based on their presence in the database and their raid moddability .
* - Sorts base and independent child items to consider for deletion .
* - Groups child items by their parent for later evaluation .
* - Evaluates grouped child items to decide which should be deleted , based on their value and a random roll .
*
* @param insured - The insured items to build a removal array from .
* @returns An array of IDs representing items that should be deleted .
* /
protected findItemsToDelete ( insured : Insurance ) : string [ ]
{
const toDelete : string [ ] = [ ] ;
const childrenGroupedByParent = new Map < string , Item [ ] > ( ) ;
insured . items . forEach ( insuredItem = >
{
const itemDbDetails = this . itemHelper . getItem ( insuredItem . _tpl ) ;
// Use the _tpl property from the parent item to get the parent item details
const parentItem = insured . items . find ( item = > item . _id === insuredItem . parentId ) ;
const parentItemDbDetailsArray = parentItem ? this . itemHelper . getItem ( parentItem . _tpl ) : null ;
const parentItemDbDetails = parentItemDbDetailsArray ? parentItemDbDetailsArray [ 1 ] : null ;
// Filter out items not in the database or not raid moddable.
if ( ! this . filterByRaidModdability ( insuredItem , parentItemDbDetails , itemDbDetails ) ) return ;
// Check for base or independent child items.
if ( this . isBaseOrIndependentChild ( insuredItem ) )
{
// Find child IDs if the item is a parent.
const itemWithChildren = this . itemHelper . findAndReturnChildrenByItems ( insured . items , insuredItem . _id ) ;
// Make a roll to decide if this item should be deleted, and if so, add it and its children to the deletion list.
if ( this . makeRollAndMarkForDeletion ( insuredItem , insured . traderId , toDelete ) )
2023-03-03 15:23:46 +00:00
{
2023-10-10 11:03:20 +00:00
toDelete . push ( . . . itemWithChildren ) ;
2023-03-03 15:23:46 +00:00
}
2023-10-10 11:03:20 +00:00
}
else if ( insuredItem . parentId )
{
// This is a child item equipped to a parent... Group this child item by its parent.
this . groupChildrenByParent ( insuredItem , childrenGroupedByParent ) ;
}
} ) ;
// Iterate through each group of children and sort and filter them for deletion.
childrenGroupedByParent . forEach ( ( children ) = >
{
this . sortAndFilterChildren ( children , insured . traderId , toDelete ) ;
} ) ;
this . logger . debug ( ` Marked ${ toDelete . length } items for deletion from insurance. ` ) ;
return toDelete ;
}
2023-03-03 15:23:46 +00:00
2023-10-10 11:03:20 +00:00
/ * *
* Filters an item based on its existence in the database , raid moddability , and slot requirements .
*
* @param item The item to be filtered .
* @param parentItemDbDetails The database details of the parent item , or null if the item has no parent .
* @param itemDbDetails A tuple where the first element is a boolean indicating if the item exists in the database ,
* and the second element is the item details if it does .
* @returns true if the item exists in the database and neither of the following conditions are met :
* - The item has the RaidModdable property set to false .
* - The item is attached to a required slot in its parent item .
* Otherwise , returns false .
* /
protected filterByRaidModdability ( item : Item , parentItemDbDetails : ITemplateItem | null , itemDbDetails : [ boolean , ITemplateItem ] ) : boolean
{
// Check for RaidModdable property.
const isNotRaidModdable = itemDbDetails [ 1 ] ? . _props ? . RaidModdable === false ;
// Check for Slots in parent item details.
let isRequiredSlot = false ;
if ( parentItemDbDetails ? . _props ? . Slots )
{
// Check if a Slot in parent details matches the slotId of the current item and is marked as required
isRequiredSlot = parentItemDbDetails . _props . Slots . some ( slot = > slot . _name === item . slotId && slot . _required ) ;
}
return itemDbDetails [ 0 ] && ! ( isNotRaidModdable || isRequiredSlot ) ;
}
/ * *
* Determines if an item is either a base item or a child item that is not equipped to its parent .
*
* @param item The item to check .
* @returns true if the item is a base or an independent child item , otherwise false .
* /
protected isBaseOrIndependentChild ( item : Item ) : boolean
{
return item . slotId === "hideout" || item . slotId === "main" || ! isNaN ( Number ( item . slotId ) ) ;
}
/ * *
* Makes a roll to determine if a given item should be deleted . If the roll is successful , the item ' s ID is added
* to the ` toDelete ` array .
*
* @param item The item for which the roll is made .
* @param traderId The ID of the trader to consider in the rollForItemDelete method .
* @param toDelete The array accumulating the IDs of items to be deleted .
* @returns true if the item is marked for deletion , otherwise false .
* /
protected makeRollAndMarkForDeletion ( item : Item , traderId : string , toDelete : string [ ] ) : boolean
{
if ( this . rollForItemDelete ( item , traderId , toDelete ) )
{
toDelete . push ( item . _id ) ;
return true ;
}
return false ;
}
/ * *
* Groups child items by their parent IDs in a Map data structure .
*
* @param item The child item to be grouped by its parent .
* @param childrenGroupedByParent The Map that holds arrays of children items grouped by their parent IDs .
* @returns void
* /
protected groupChildrenByParent ( item : Item , childrenGroupedByParent : Map < string , Item [ ] > ) : void
{
if ( ! childrenGroupedByParent . has ( item . parentId ! ) )
{
childrenGroupedByParent . set ( item . parentId ! , [ ] ) ;
}
childrenGroupedByParent . get ( item . parentId ! ) ? . push ( item ) ;
}
2023-03-03 15:23:46 +00:00
2023-10-10 11:03:20 +00:00
/ * *
* Sorts the array of children items in descending order by their maximum price . For each child , a roll is made to
* determine if it should be deleted . The method then deletes the most valuable children based on the number of
* successful rolls made .
*
* @param children The array of children items to sort and filter .
* @param traderId The ID of the trader to consider in the rollForItemDelete method .
* @param toDelete The array that accumulates the IDs of the items to be deleted .
* @returns void
* /
protected sortAndFilterChildren ( children : Item [ ] , traderId : string , toDelete : string [ ] ) : void
{
// Sort the children by their max price in descending order.
children . sort ( ( a , b ) = > this . itemHelper . getItemMaxPrice ( b . _tpl ) - this . itemHelper . getItemMaxPrice ( a . _tpl ) ) ;
// Count the number of successful rolls.
let successfulRolls = 0 ;
for ( const child of children )
{
if ( this . rollForItemDelete ( child , traderId , toDelete ) )
{
successfulRolls ++ ;
2023-03-03 15:23:46 +00:00
}
2023-10-10 11:03:20 +00:00
}
// Delete the most valuable children based on the number of successful rolls.
const mostValuableChildrenToDelete = children . slice ( 0 , successfulRolls ) . map ( child = > child . _id ) ;
toDelete . push ( . . . mostValuableChildrenToDelete ) ;
}
2023-03-03 15:23:46 +00:00
2023-10-10 11:03:20 +00:00
/ * *
* Remove items from the insured items that should not be returned to the player .
*
* @param insured The insured items to process .
* @param toDelete The items that should be deleted .
* @returns void
* /
protected removeItemsFromInsurance ( insured : Insurance , toDelete : string [ ] ) : void
{
insured . items = insured . items . filter ( item = > ! toDelete . includes ( item . _id ) ) ;
}
/ * *
* Handle sending the insurance message to the user that potentially contains the valid insurance items .
*
* @param sessionID The session ID that should receive the insurance message .
* @param insurance The context of insurance to use .
* @param noItems Whether or not there are any items to return to the player .
* @returns void
* /
protected sendMail ( sessionID : string , insurance : Insurance , noItems : boolean ) : void
{
// After all of the item filtering that we've done, if there are no items remaining, the insurance has
// successfully "failed" to return anything and an appropriate message should be sent to the player.
if ( noItems )
{
const insuranceFailedTemplates = this . databaseServer . getTables ( ) . traders [ insurance . traderId ] . dialogue . insuranceFailed ;
insurance . messageContent . templateId = this . randomUtil . getArrayValue ( insuranceFailedTemplates ) ;
2023-03-03 15:23:46 +00:00
}
2023-10-10 11:03:20 +00:00
// Send the insurance message
this . mailSendService . sendLocalisedNpcMessageToPlayer (
sessionID ,
this . traderHelper . getTraderById ( insurance . traderId ) ,
MessageType . INSURANCE_RETURN ,
insurance . messageContent . templateId ,
insurance . items ,
insurance . messageContent . maxStorageTime ,
insurance . messageContent . systemData
) ;
2023-03-03 15:23:46 +00:00
}
2023-05-19 17:39:26 +01:00
/ * *
2023-10-10 11:03:20 +00:00
* Determines whether a valid insured item should be removed from the player ' s inventory based on a random roll and
* trader - specific return chance .
*
* @param insuredItem The insured item being evaluated for removal .
* @param traderId The ID of the trader who insured the item .
* @param itemsBeingDeleted List of items that are already slated for removal .
* @returns true if the insured item should be removed from inventory , false otherwise .
2023-05-19 17:39:26 +01:00
* /
2023-10-10 11:03:20 +00:00
protected rollForItemDelete ( insuredItem : Item , traderId : string , itemsBeingDeleted : string [ ] ) : boolean
2023-05-19 17:39:26 +01:00
{
2023-10-10 11:03:20 +00:00
const maxRoll = 9999 ;
const conversionFactor = 100 ;
const returnChance = this . randomUtil . getInt ( 0 , maxRoll ) / conversionFactor ;
2023-05-19 17:39:26 +01:00
const traderReturnChance = this . insuranceConfig . returnChancePercent [ traderId ] ;
2023-10-10 11:03:20 +00:00
const exceedsTraderReturnChance = returnChance >= traderReturnChance ;
const isItemAlreadyBeingDeleted = itemsBeingDeleted . includes ( insuredItem . _id ) ;
2023-05-19 17:39:26 +01:00
2023-10-10 11:03:20 +00:00
return exceedsTraderReturnChance && ! isItemAlreadyBeingDeleted ;
2023-05-19 17:39:26 +01:00
}
2023-03-03 15:23:46 +00:00
/ * *
2023-07-15 10:56:00 +01:00
* Handle Insure event
2023-03-03 15:23:46 +00:00
* Add insurance to an item
2023-10-10 11:03:20 +00:00
*
2023-03-03 15:23:46 +00:00
* @param pmcData Player profile
* @param body Insurance request
* @param sessionID Session id
* @returns IItemEventRouterResponse object to send to client
* /
public insure ( pmcData : IPmcData , body : IInsureRequestData , sessionID : string ) : IItemEventRouterResponse
{
let output = this . eventOutputHolder . getOutput ( sessionID ) ;
const itemsToPay = [ ] ;
const inventoryItemsHash = { } ;
for ( const item of pmcData . Inventory . items )
{
inventoryItemsHash [ item . _id ] = item ;
}
// get the price of all items
for ( const key of body . items )
{
itemsToPay . push ( {
id : inventoryItemsHash [ key ] . _id ,
count : Math.round ( this . insuranceService . getPremium ( pmcData , inventoryItemsHash [ key ] , body . tid ) )
} ) ;
}
const options : IProcessBuyTradeRequestData = {
// eslint-disable-next-line @typescript-eslint/naming-convention
scheme_items : itemsToPay ,
tid : body.tid ,
Action : "" ,
type : "" ,
// eslint-disable-next-line @typescript-eslint/naming-convention
item_id : "" ,
count : 0 ,
// eslint-disable-next-line @typescript-eslint/naming-convention
scheme_id : 0
} ;
// pay for the item insurance
output = this . paymentService . payMoney ( pmcData , options , sessionID , output ) ;
if ( output . warnings . length > 0 )
{
return output ;
}
// add items to InsuredItems list once money has been paid
for ( const key of body . items )
{
pmcData . InsuredItems . push ( {
tid : body.tid ,
itemId : inventoryItemsHash [ key ] . _id
} ) ;
}
return output ;
}
/ * *
2023-07-15 10:56:00 +01:00
* Handle client / insurance / items / list / cost
2023-03-03 15:23:46 +00:00
* Calculate insurance cost
2023-10-10 11:03:20 +00:00
*
* @param request request object
2023-03-03 15:23:46 +00:00
* @param sessionID session id
* @returns IGetInsuranceCostResponseData object to send to client
* /
2023-10-10 11:03:20 +00:00
public cost ( request : IGetInsuranceCostRequestData , sessionID : string ) : IGetInsuranceCostResponseData
2023-03-03 15:23:46 +00:00
{
const output : IGetInsuranceCostResponseData = { } ;
const pmcData = this . profileHelper . getPmcProfile ( sessionID ) ;
const inventoryItemsHash : Record < string , Item > = { } ;
for ( const item of pmcData . Inventory . items )
{
inventoryItemsHash [ item . _id ] = item ;
}
2023-10-10 11:03:20 +00:00
// Loop over each trader in request
for ( const trader of request . traders )
2023-03-03 15:23:46 +00:00
{
2023-10-10 11:03:20 +00:00
const items : Record < string , number > = { } ;
2023-03-03 15:23:46 +00:00
2023-10-10 11:03:20 +00:00
for ( const itemId of request . items )
2023-03-03 15:23:46 +00:00
{
2023-10-10 11:03:20 +00:00
// Ensure hash has item in it
if ( ! inventoryItemsHash [ itemId ] )
{
this . logger . debug ( ` Item with id: ${ itemId } missing from player inventory, skipping ` ) ;
continue ;
}
items [ inventoryItemsHash [ itemId ] . _tpl ] = Math . round ( this . insuranceService . getPremium ( pmcData , inventoryItemsHash [ itemId ] , trader ) ) ;
2023-03-03 15:23:46 +00:00
}
output [ trader ] = items ;
}
return output ;
}
}