diff --git a/project/assets/configs/insurance.json b/project/assets/configs/insurance.json index 9a57739f..145aaa34 100644 --- a/project/assets/configs/insurance.json +++ b/project/assets/configs/insurance.json @@ -17,5 +17,7 @@ "patron_in_weapon" ], "returnTimeOverrideSeconds": 0, - "runIntervalSeconds": 600 + "runIntervalSeconds": 600, + "minAttachmentRoublePriceToBeTaken": 2000, + "chanceNoAttachmentsTakenPercent": 10 } \ No newline at end of file diff --git a/project/assets/database/bots/types/bear.json b/project/assets/database/bots/types/bear.json index d215d661..4cdf0ae9 100644 --- a/project/assets/database/bots/types/bear.json +++ b/project/assets/database/bots/types/bear.json @@ -2495,7 +2495,8 @@ "Gizzy", "LuckyCharmT", "Rena-chan", - "HB" + "HB", + "John Pork" ], "generation": { "items": { diff --git a/project/assets/database/bots/types/usec.json b/project/assets/database/bots/types/usec.json index 5c58bd78..d80f409e 100644 --- a/project/assets/database/bots/types/usec.json +++ b/project/assets/database/bots/types/usec.json @@ -2492,7 +2492,8 @@ "Gizzy", "LuckyCharmT", "Rena-chan", - "HB" + "HB", + "John Pork" ], "generation": { "items": { diff --git a/project/src/callbacks/DialogueCallbacks.ts b/project/src/callbacks/DialogueCallbacks.ts index 5b0e620b..39997d30 100644 --- a/project/src/callbacks/DialogueCallbacks.ts +++ b/project/src/callbacks/DialogueCallbacks.ts @@ -3,6 +3,7 @@ import { inject, injectable } from "tsyringe"; import { DialogueController } from "@spt-aki/controllers/DialogueController"; import { OnUpdate } from "@spt-aki/di/OnUpdate"; import { IEmptyRequestData } from "@spt-aki/models/eft/common/IEmptyRequestData"; +import { IUIDRequestData } from "@spt-aki/models/eft/common/request/IUIDRequestData"; import { IAcceptFriendRequestData, ICancelFriendRequestData, @@ -247,14 +248,14 @@ export class DialogueCallbacks implements OnUpdate /** Handle client/friend/ignore/set */ // eslint-disable-next-line @typescript-eslint/no-unused-vars - public ignoreFriend(url: string, request: { uid: string; }, sessionID: string): INullResponseData + public ignoreFriend(url: string, request: IUIDRequestData, sessionID: string): INullResponseData { return this.httpResponse.nullResponse(); } /** Handle client/friend/ignore/remove */ // eslint-disable-next-line @typescript-eslint/no-unused-vars - public unIgnoreFriend(url: string, request: { uid: string; }, sessionID: string): INullResponseData + public unIgnoreFriend(url: string, request: IUIDRequestData, sessionID: string): INullResponseData { return this.httpResponse.nullResponse(); } diff --git a/project/src/callbacks/GameCallbacks.ts b/project/src/callbacks/GameCallbacks.ts index 2a6cb781..695a118e 100644 --- a/project/src/callbacks/GameCallbacks.ts +++ b/project/src/callbacks/GameCallbacks.ts @@ -3,6 +3,7 @@ import { inject, injectable } from "tsyringe"; import { GameController } from "@spt-aki/controllers/GameController"; import { OnLoad } from "@spt-aki/di/OnLoad"; import { IEmptyRequestData } from "@spt-aki/models/eft/common/IEmptyRequestData"; +import { IUIDRequestData } from "@spt-aki/models/eft/common/request/IUIDRequestData"; import { ICheckVersionResponse } from "@spt-aki/models/eft/game/ICheckVersionResponse"; import { ICurrentGroupResponse } from "@spt-aki/models/eft/game/ICurrentGroupResponse"; import { IGameConfigResponse } from "@spt-aki/models/eft/game/IGameConfigResponse"; @@ -12,7 +13,6 @@ import { IGameLogoutResponseData } from "@spt-aki/models/eft/game/IGameLogoutRes import { IGameStartResponse } from "@spt-aki/models/eft/game/IGameStartResponse"; import { IGetRaidTimeRequest } from "@spt-aki/models/eft/game/IGetRaidTimeRequest"; import { IGetRaidTimeResponse } from "@spt-aki/models/eft/game/IGetRaidTimeResponse"; -import { IReportNicknameRequestData } from "@spt-aki/models/eft/game/IReportNicknameRequestData"; import { IServerDetails } from "@spt-aki/models/eft/game/IServerDetails"; import { IVersionValidateRequestData } from "@spt-aki/models/eft/game/IVersionValidateRequestData"; import { IGetBodyResponseData } from "@spt-aki/models/eft/httpResponse/IGetBodyResponseData"; @@ -145,7 +145,7 @@ export class GameCallbacks implements OnLoad return this.httpResponse.noBody({ Version: this.watermark.getInGameVersionLabel() }); } - public reportNickname(url: string, info: IReportNicknameRequestData, sessionID: string): INullResponseData + public reportNickname(url: string, info: IUIDRequestData, sessionID: string): INullResponseData { return this.httpResponse.nullResponse(); } diff --git a/project/src/callbacks/NotifierCallbacks.ts b/project/src/callbacks/NotifierCallbacks.ts index 595cde68..2734dfff 100644 --- a/project/src/callbacks/NotifierCallbacks.ts +++ b/project/src/callbacks/NotifierCallbacks.ts @@ -3,9 +3,9 @@ import { inject, injectable } from "tsyringe"; import { NotifierController } from "@spt-aki/controllers/NotifierController"; import { HttpServerHelper } from "@spt-aki/helpers/HttpServerHelper"; import { IEmptyRequestData } from "@spt-aki/models/eft/common/IEmptyRequestData"; +import { IUIDRequestData } from "@spt-aki/models/eft/common/request/IUIDRequestData"; import { IGetBodyResponseData } from "@spt-aki/models/eft/httpResponse/IGetBodyResponseData"; import { INotifierChannel } from "@spt-aki/models/eft/notifier/INotifier"; -import { ISelectProfileRequestData } from "@spt-aki/models/eft/notifier/ISelectProfileRequestData"; import { ISelectProfileResponse } from "@spt-aki/models/eft/notifier/ISelectProfileResponse"; import { HttpResponseUtil } from "@spt-aki/utils/HttpResponseUtil"; import { JsonUtil } from "@spt-aki/utils/JsonUtil"; @@ -68,7 +68,7 @@ export class NotifierCallbacks // eslint-disable-next-line @typescript-eslint/no-unused-vars public selectProfile( url: string, - info: ISelectProfileRequestData, + info: IUIDRequestData, sessionID: string, ): IGetBodyResponseData { diff --git a/project/src/controllers/InsuranceController.ts b/project/src/controllers/InsuranceController.ts index 7bb44a3e..572deacf 100644 --- a/project/src/controllers/InsuranceController.ts +++ b/project/src/controllers/InsuranceController.ts @@ -4,6 +4,7 @@ import { DialogueHelper } from "@spt-aki/helpers/DialogueHelper"; import { ItemHelper } from "@spt-aki/helpers/ItemHelper"; import { ProfileHelper } from "@spt-aki/helpers/ProfileHelper"; import { TraderHelper } from "@spt-aki/helpers/TraderHelper"; +import { WeightedRandomHelper } from "@spt-aki/helpers/WeightedRandomHelper"; import { IPmcData } from "@spt-aki/models/eft/common/IPmcData"; import { Item } from "@spt-aki/models/eft/common/tables/IItem"; import { IGetInsuranceCostRequestData } from "@spt-aki/models/eft/insurance/IGetInsuranceCostRequestData"; @@ -25,8 +26,9 @@ import { MailSendService } from "@spt-aki/services/MailSendService"; import { PaymentService } from "@spt-aki/services/PaymentService"; import { RagfairPriceService } from "@spt-aki/services/RagfairPriceService"; import { HashUtil } from "@spt-aki/utils/HashUtil"; +import { JsonUtil } from "@spt-aki/utils/JsonUtil"; import { MathUtil } from "@spt-aki/utils/MathUtil"; -import { RandomUtil } from "@spt-aki/utils/RandomUtil"; +import { ProbabilityObject, ProbabilityObjectArray, RandomUtil } from "@spt-aki/utils/RandomUtil"; import { TimeUtil } from "@spt-aki/utils/TimeUtil"; @injectable() @@ -39,6 +41,7 @@ export class InsuranceController @inject("WinstonLogger") protected logger: ILogger, @inject("RandomUtil") protected randomUtil: RandomUtil, @inject("MathUtil") protected mathUtil: MathUtil, + @inject("JsonUtil") protected jsonUtil: JsonUtil, @inject("HashUtil") protected hashUtil: HashUtil, @inject("EventOutputHolder") protected eventOutputHolder: EventOutputHolder, @inject("TimeUtil") protected timeUtil: TimeUtil, @@ -47,6 +50,7 @@ export class InsuranceController @inject("ItemHelper") protected itemHelper: ItemHelper, @inject("ProfileHelper") protected profileHelper: ProfileHelper, @inject("DialogueHelper") protected dialogueHelper: DialogueHelper, + @inject("WeightedRandomHelper") protected weightedRandomHelper: WeightedRandomHelper, @inject("TraderHelper") protected traderHelper: TraderHelper, @inject("PaymentService") protected paymentService: PaymentService, @inject("InsuranceService") protected insuranceService: InsuranceService, @@ -448,83 +452,100 @@ export class InsuranceController */ protected processAttachmentByParent(attachments: Item[], traderId: string, toDelete: Set): void { - const sortedAttachments = this.sortAttachmentsByPrice(attachments); - this.logAttachmentsDetails(sortedAttachments); + // Create dict of item ids + their flea/handbook price (highest is chosen) + const weightedAttachmentByPrice = this.weightAttachmentsByPrice(attachments); - const successfulRolls = this.countSuccessfulRolls(sortedAttachments, traderId); - this.logger.debug(`Number of attachments to be deleted: ${successfulRolls}`); + // Get how many attachments we want to pull off parent + const countOfAttachmentsToRemove = this.getAttachmentCountToRemove(weightedAttachmentByPrice, traderId); - this.attachmentDeletionByValue(sortedAttachments, successfulRolls, toDelete); + // Create prob array and add all attachments with rouble price as the weight + const attachmentsProbabilityArray = new ProbabilityObjectArray(this.mathUtil, this.jsonUtil); + for (const attachmentTpl of Object.keys(weightedAttachmentByPrice)) + { + attachmentsProbabilityArray.push( + new ProbabilityObject(attachmentTpl, weightedAttachmentByPrice[attachmentTpl]), + ); + } + + // Draw x attachments from weighted array to remove from parent, remove from pool after being picked + const attachmentIdsToRemove = attachmentsProbabilityArray.draw(countOfAttachmentsToRemove, false); + for (const attachmentId of attachmentIdsToRemove) + { + toDelete.add(attachmentId); + } + + this.logAttachmentsBeingRemoved(attachmentIdsToRemove, attachments, weightedAttachmentByPrice); + + this.logger.debug(`Number of attachments to be deleted: ${attachmentIdsToRemove.length}`); } - /** - * Sorts the attachment items by their dynamic price in descending order. - * - * @param attachments The array of attachments items. - * @returns An array of items enriched with their max price and common locale-name. - */ - protected sortAttachmentsByPrice(attachments: Item[]): EnrichedItem[] - { - return attachments.map((item) => ({ - ...item, - name: this.itemHelper.getItemName(item._tpl), - dynamicPrice: this.ragfairPriceService.getDynamicItemPrice(item._tpl, this.roubleTpl, item, null, false), - })).sort((a, b) => b.dynamicPrice - a.dynamicPrice); - } - - /** - * Logs the details of each attachment item. - * - * @param attachments The array of attachment items. - */ - protected logAttachmentsDetails(attachments: EnrichedItem[]): void + protected logAttachmentsBeingRemoved( + attachmentIdsToRemove: string[], + attachments: Item[], + attachmentPrices: Record, + ): void { let index = 1; - for (const attachment of attachments) + for (const attachmentId of attachmentIdsToRemove) { - this.logger.debug(`Attachment ${index}: "${attachment.name}" - Price: ${attachment.dynamicPrice}`); + this.logger.debug( + `Attachment ${index} Id: ${attachmentId} Tpl: ${ + attachments.find((x) => x._id === attachmentId)?._tpl + } - Price: ${attachmentPrices[attachmentId]}`, + ); index++; } } - /** - * Counts the number of successful rolls for the attachment items. - * - * @param attachments The array of attachment items. - * @param traderId The ID of the trader that has insured these attachments. - * @returns The number of successful rolls. - */ - protected countSuccessfulRolls(attachments: Item[], traderId: string): number + protected weightAttachmentsByPrice(attachments: Item[]): Record { - const rolls = Array.from({ length: attachments.length }, () => this.rollForDelete(traderId)); - return rolls.filter(Boolean).length; + const result: Record = {}; + + // Get a dictionary of item tpls + their rouble price + for (const attachment of attachments) + { + const price = this.ragfairPriceService.getDynamicItemPrice(attachment._tpl, this.roubleTpl); + if (price) + { + result[attachment._id] = Math.round(price); + } + } + + this.weightedRandomHelper.reduceWeightValues(result); + + return result; } /** - * Marks the most valuable attachments for deletion based on the number of successful rolls made. - * - * @param attachments The array of attachment items. - * @param successfulRolls The number of successful rolls. - * @param toDelete The array that accumulates the IDs of the items to be deleted. + * Get count of items to remove from weapon (take into account trader + price of attachment) + * @param weightedAttachmentByPrice Dict of item Tpls and thier rouble price + * @param traderId Trader attachment insured against + * @returns Attachment count to remove */ - protected attachmentDeletionByValue( - attachments: EnrichedItem[], - successfulRolls: number, - toDelete: Set, - ): void + protected getAttachmentCountToRemove(weightedAttachmentByPrice: Record, traderId: string): number { - const valuableToDelete = attachments.slice(0, successfulRolls).map(({ _id }) => _id); + let removeCount = 0; - for (const attachmentsId of valuableToDelete) + if (this.randomUtil.getChance100(this.insuranceConfig.chanceNoAttachmentsTakenPercent)) { - const valuableChild = attachments.find(({ _id }) => _id === attachmentsId); - if (valuableChild) + return removeCount; + } + + for (const attachmentId of Object.keys(weightedAttachmentByPrice)) + { + // Below min price to be taken, skip + if (weightedAttachmentByPrice[attachmentId] < this.insuranceConfig.minAttachmentRoublePriceToBeTaken) { - const { name, dynamicPrice } = valuableChild; - this.logger.debug(`Marked attachment "${name}" for removal - Dyanmic Price: ${dynamicPrice}`); - toDelete.add(attachmentsId); + continue; + } + + if (this.rollForDelete(traderId)) + { + removeCount++; } } + + return removeCount; } /** @@ -588,7 +609,7 @@ export class InsuranceController } /** - * Determines whether a insured item should be removed from the player's inventory based on a random roll and + * Determines whether an insured item should be removed from the player's inventory based on a random roll and * trader-specific return chance. * * @param traderId The ID of the trader who insured the item. diff --git a/project/src/generators/PMCLootGenerator.ts b/project/src/generators/PMCLootGenerator.ts index b4f1b765..68a16950 100644 --- a/project/src/generators/PMCLootGenerator.ts +++ b/project/src/generators/PMCLootGenerator.ts @@ -1,6 +1,7 @@ import { inject, injectable } from "tsyringe"; import { ItemHelper } from "@spt-aki/helpers/ItemHelper"; +import { WeightedRandomHelper } from "@spt-aki/helpers/WeightedRandomHelper"; import { ITemplateItem } from "@spt-aki/models/eft/common/tables/ITemplateItem"; import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes"; import { IPmcConfig } from "@spt-aki/models/spt/config/IPmcConfig"; @@ -22,6 +23,8 @@ export class PMCLootGenerator protected backpackLootPool: Record = {}; protected pmcConfig: IPmcConfig; + protected roubleTpl = "5449016a4bdc2d6f028b456f"; + constructor( @inject("ItemHelper") protected itemHelper: ItemHelper, @inject("DatabaseServer") protected databaseServer: DatabaseServer, @@ -29,6 +32,7 @@ export class PMCLootGenerator @inject("ItemFilterService") protected itemFilterService: ItemFilterService, @inject("RagfairPriceService") protected ragfairPriceService: RagfairPriceService, @inject("SeasonalEventService") protected seasonalEventService: SeasonalEventService, + @inject("WeightedRandomHelper") protected weightedRandomHelper: WeightedRandomHelper, ) { this.pmcConfig = this.configServer.getConfig(ConfigTypes.PMC); @@ -74,7 +78,7 @@ export class PMCLootGenerator else { // Set price of item as its weight - const price = this.ragfairPriceService.getFleaPriceForItem(itemToAdd._id); + const price = this.ragfairPriceService.getDynamicItemPrice(itemToAdd._id, this.roubleTpl); this.pocketLootPool[itemToAdd._id] = price; } } @@ -87,7 +91,7 @@ export class PMCLootGenerator this.pocketLootPool[key] = Math.round((1 / this.pocketLootPool[key]) * highestPrice); } - this.reduceWeightValues(this.pocketLootPool); + this.weightedRandomHelper.reduceWeightValues(this.pocketLootPool); } return this.pocketLootPool; @@ -132,7 +136,7 @@ export class PMCLootGenerator else { // Set price of item as its weight - const price = this.ragfairPriceService.getFleaPriceForItem(itemToAdd._id); + const price = this.ragfairPriceService.getDynamicItemPrice(itemToAdd._id, this.roubleTpl); this.vestLootPool[itemToAdd._id] = price; } } @@ -145,7 +149,7 @@ export class PMCLootGenerator this.vestLootPool[key] = Math.round((1 / this.vestLootPool[key]) * highestPrice); } - this.reduceWeightValues(this.vestLootPool); + this.weightedRandomHelper.reduceWeightValues(this.vestLootPool); } return this.vestLootPool; @@ -200,7 +204,7 @@ export class PMCLootGenerator else { // Set price of item as its weight - const price = this.ragfairPriceService.getFleaPriceForItem(itemToAdd._id); + const price = this.ragfairPriceService.getDynamicItemPrice(itemToAdd._id, this.roubleTpl); this.backpackLootPool[itemToAdd._id] = price; } } @@ -213,71 +217,9 @@ export class PMCLootGenerator this.backpackLootPool[key] = Math.round((1 / this.backpackLootPool[key]) * highestPrice); } - this.reduceWeightValues(this.backpackLootPool); + this.weightedRandomHelper.reduceWeightValues(this.backpackLootPool); } return this.backpackLootPool; } - - /** - * Find the greated common divisor of all weights and use it on the passed in dictionary - * @param weightedDict - */ - protected reduceWeightValues(weightedDict: Record): void - { - // No values, nothing to reduce - if (Object.keys(weightedDict).length === 0) - { - return; - } - - // Only one value, set to 1 and exit - if (Object.keys(weightedDict).length === 1) - { - const key = Object.keys(weightedDict)[0]; - weightedDict[key] = 1; - return; - } - - const weights = Object.values(weightedDict).slice(); - const commonDivisor = this.commonDivisor(weights); - - // No point in dividing by 1 - if (commonDivisor === 1) - { - return; - } - - for (const key in weightedDict) - { - if (Object.hasOwn(weightedDict, key)) - { - weightedDict[key] /= commonDivisor; - } - } - } - - protected commonDivisor(numbers: number[]): number - { - let result = numbers[0]; - for (let i = 1; i < numbers.length; i++) - { - result = this.gcd(result, numbers[i]); - } - - return result; - } - - protected gcd(a: number, b: number): number - { - let x = a; - let y = b; - while (y !== 0) - { - const temp = y; - y = x % y; - x = temp; - } - return x; - } } diff --git a/project/src/helpers/Dialogue/Commando/SptCommands/GiveCommand/GiveSptCommand.ts b/project/src/helpers/Dialogue/Commando/SptCommands/GiveCommand/GiveSptCommand.ts index 99d48a74..29560329 100644 --- a/project/src/helpers/Dialogue/Commando/SptCommands/GiveCommand/GiveSptCommand.ts +++ b/project/src/helpers/Dialogue/Commando/SptCommands/GiveCommand/GiveSptCommand.ts @@ -146,16 +146,17 @@ export class GiveSptCommand implements ISptCommand commandHandler, `An error occurred while trying to use localized text. Locale will be defaulted to 'en'.`, ); - this.logger.error(e); + this.logger.warning(e); locale = "en"; } - const localizedGlobal = this.databaseServer.getTables().locales.global[locale]; + const localizedGlobal = this.databaseServer.getTables().locales.global[locale] ?? + this.databaseServer.getTables().locales.global.en; const closestItemsMatchedByName = this.itemHelper.getItems() .filter((i) => this.isItemAllowed(i)) - .map((i) => localizedGlobal[`${i?._id} Name`]?.toLowerCase()) - .filter((i) => i !== undefined) + .map((i) => localizedGlobal[`${i?._id} Name`]?.toLowerCase() ?? i._props.Name) + .filter((i) => i !== undefined && i !== "") .map(i => ({match: stringSimilarity(item.toLocaleLowerCase(), i.toLocaleLowerCase()), itemName: i})) .sort((a1, a2) => a2.match - a1.match); @@ -186,7 +187,7 @@ export class GiveSptCommand implements ISptCommand const tplId = isItemName ? this.itemHelper.getItems() .filter((i) => this.isItemAllowed(i)) - .find((i) => this.databaseServer.getTables().locales.global[locale][`${i?._id} Name`]?.toLowerCase() === item)._id + .find((i) => (this.databaseServer.getTables().locales.global[locale][`${i?._id} Name`]?.toLowerCase() ?? i._props.Name) === item)._id : item; const checkedItem = this.itemHelper.getItem(tplId); @@ -202,9 +203,12 @@ export class GiveSptCommand implements ISptCommand const itemsToSend: Item[] = []; if ( - this.itemHelper.isOfBaseclass(checkedItem[1]._id, BaseClasses.WEAPON) - || this.itemHelper.isOfBaseclass(checkedItem[1]._id, BaseClasses.ARMOR) - || this.itemHelper.isOfBaseclass(checkedItem[1]._id, BaseClasses.VEST) + (this.itemHelper.isOfBaseclass(checkedItem[1]._id, BaseClasses.WEAPON) + || this.itemHelper.isOfBaseclass(checkedItem[1]._id, BaseClasses.ARMOR) + || this.itemHelper.isOfBaseclass(checkedItem[1]._id, BaseClasses.VEST)) + && !["62178c4d4ecf221597654e3d", "6217726288ed9f0845317459", "624c0b3340357b5f566e8766"].includes( + checkedItem[1]._id, + ) // edge case for handheld flares ) { const preset = this.presetHelper.getDefaultPreset(checkedItem[1]._id); diff --git a/project/src/helpers/WeightedRandomHelper.ts b/project/src/helpers/WeightedRandomHelper.ts index 61049b2d..f145da59 100644 --- a/project/src/helpers/WeightedRandomHelper.ts +++ b/project/src/helpers/WeightedRandomHelper.ts @@ -92,4 +92,66 @@ export class WeightedRandomHelper } } } + + /** + * Find the greated common divisor of all weights and use it on the passed in dictionary + * @param weightedDict values to reduce + */ + public reduceWeightValues(weightedDict: Record): void + { + // No values, nothing to reduce + if (Object.keys(weightedDict).length === 0) + { + return; + } + + // Only one value, set to 1 and exit + if (Object.keys(weightedDict).length === 1) + { + const key = Object.keys(weightedDict)[0]; + weightedDict[key] = 1; + return; + } + + const weights = Object.values(weightedDict).slice(); + const commonDivisor = this.commonDivisor(weights); + + // No point in dividing by 1 + if (commonDivisor === 1) + { + return; + } + + for (const key in weightedDict) + { + if (Object.hasOwn(weightedDict, key)) + { + weightedDict[key] /= commonDivisor; + } + } + } + + protected commonDivisor(numbers: number[]): number + { + let result = numbers[0]; + for (let i = 1; i < numbers.length; i++) + { + result = this.gcd(result, numbers[i]); + } + + return result; + } + + protected gcd(a: number, b: number): number + { + let x = a; + let y = b; + while (y !== 0) + { + const temp = y; + y = x % y; + x = temp; + } + return x; + } } diff --git a/project/src/models/eft/common/request/IUIDRequestData.ts b/project/src/models/eft/common/request/IUIDRequestData.ts new file mode 100644 index 00000000..9c248b22 --- /dev/null +++ b/project/src/models/eft/common/request/IUIDRequestData.ts @@ -0,0 +1,4 @@ +export interface IUIDRequestData +{ + uid: string; +} diff --git a/project/src/models/eft/game/IReportNicknameRequestData.ts b/project/src/models/eft/game/IReportNicknameRequestData.ts deleted file mode 100644 index bbce9814..00000000 --- a/project/src/models/eft/game/IReportNicknameRequestData.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface IReportNicknameRequestData -{ - uid: string; -} diff --git a/project/src/models/eft/notifier/ISelectProfileRequestData.ts b/project/src/models/eft/notifier/ISelectProfileRequestData.ts deleted file mode 100644 index d5d8b98b..00000000 --- a/project/src/models/eft/notifier/ISelectProfileRequestData.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface ISelectProfileRequestData -{ - uid: string; -} diff --git a/project/src/models/spt/callbacks/INotifierCallbacks.ts b/project/src/models/spt/callbacks/INotifierCallbacks.ts index 1dd6a6c0..28406946 100644 --- a/project/src/models/spt/callbacks/INotifierCallbacks.ts +++ b/project/src/models/spt/callbacks/INotifierCallbacks.ts @@ -1,7 +1,7 @@ import { IEmptyRequestData } from "@spt-aki/models/eft/common/IEmptyRequestData"; +import { IUIDRequestData } from "@spt-aki/models/eft/common/request/IUIDRequestData"; import { IGetBodyResponseData } from "@spt-aki/models/eft/httpResponse/IGetBodyResponseData"; import { INotifierChannel } from "@spt-aki/models/eft/notifier/INotifier"; -import { ISelectProfileRequestData } from "@spt-aki/models/eft/notifier/ISelectProfileRequestData"; export interface INotifierCallbacks { @@ -18,6 +18,6 @@ export interface INotifierCallbacks info: IEmptyRequestData, sessionID: string, ): IGetBodyResponseData; - selectProfile(url: string, info: ISelectProfileRequestData, sessionID: string): IGetBodyResponseData; + selectProfile(url: string, info: IUIDRequestData, sessionID: string): IGetBodyResponseData; notify(url: string, info: any, sessionID: string): string; } diff --git a/project/src/models/spt/config/IInsuranceConfig.ts b/project/src/models/spt/config/IInsuranceConfig.ts index 925280aa..40855ae1 100644 --- a/project/src/models/spt/config/IInsuranceConfig.ts +++ b/project/src/models/spt/config/IInsuranceConfig.ts @@ -15,4 +15,8 @@ export interface IInsuranceConfig extends IBaseConfig returnTimeOverrideSeconds: number; /** How often server should process insurance in seconds */ runIntervalSeconds: number; + // Lowest rouble price for an attachment to be allowed to be taken + minAttachmentRoublePriceToBeTaken: number; + // Chance out of 100% no attachments from a parent are taken + chanceNoAttachmentsTakenPercent: number; } diff --git a/project/src/routers/serializers/BundleSerializer.ts b/project/src/routers/serializers/BundleSerializer.ts index df8c8866..a12c51e3 100644 --- a/project/src/routers/serializers/BundleSerializer.ts +++ b/project/src/routers/serializers/BundleSerializer.ts @@ -22,7 +22,7 @@ export class BundleSerializer extends Serializer { this.logger.info(`[BUNDLE]: ${req.url}`); - const key = req.url.split("/bundle/")[1]; + const key = decodeURI(req.url.split("/bundle/")[1]); const bundle = this.bundleLoader.getBundle(key); this.httpFileUtil.sendFile(resp, `${bundle.modpath}/bundles/${bundle.filename}`); diff --git a/project/src/services/ProfileFixerService.ts b/project/src/services/ProfileFixerService.ts index 28205ee0..d07b025b 100644 --- a/project/src/services/ProfileFixerService.ts +++ b/project/src/services/ProfileFixerService.ts @@ -897,24 +897,27 @@ export class ProfileFixerService } } - // Remove invalid builds from weapon, equipment and magazine build lists - const weaponBuilds = fullProfile.userbuilds?.weaponBuilds || []; - fullProfile.userbuilds.weaponBuilds = weaponBuilds.filter((weaponBuild) => + if (fullProfile.userbuilds) { - return !this.shouldRemoveWeaponEquipmentBuild("weapon", weaponBuild, itemsDb); - }); + // Remove invalid builds from weapon, equipment and magazine build lists + const weaponBuilds = fullProfile.userbuilds?.weaponBuilds || []; + fullProfile.userbuilds.weaponBuilds = weaponBuilds.filter((weaponBuild) => + { + return !this.shouldRemoveWeaponEquipmentBuild("weapon", weaponBuild, itemsDb); + }); - const equipmentBuilds = fullProfile.userbuilds?.equipmentBuilds || []; - fullProfile.userbuilds.equipmentBuilds = equipmentBuilds.filter((equipmentBuild) => - { - return !this.shouldRemoveWeaponEquipmentBuild("equipment", equipmentBuild, itemsDb); - }); + const equipmentBuilds = fullProfile.userbuilds?.equipmentBuilds || []; + fullProfile.userbuilds.equipmentBuilds = equipmentBuilds.filter((equipmentBuild) => + { + return !this.shouldRemoveWeaponEquipmentBuild("equipment", equipmentBuild, itemsDb); + }); - const magazineBuilds = fullProfile.userbuilds?.magazineBuilds || []; - fullProfile.userbuilds.magazineBuilds = magazineBuilds.filter((magazineBuild) => - { - return !this.shouldRemoveMagazineBuild(magazineBuild, itemsDb); - }); + const magazineBuilds = fullProfile.userbuilds?.magazineBuilds || []; + fullProfile.userbuilds.magazineBuilds = magazineBuilds.filter((magazineBuild) => + { + return !this.shouldRemoveMagazineBuild(magazineBuild, itemsDb); + }); + } // Iterate over dialogs, looking for messages with items not found in item db, remove message if item found for (const dialogId in fullProfile.dialogues) diff --git a/project/src/services/RagfairPriceService.ts b/project/src/services/RagfairPriceService.ts index 6c3e578b..6f1d4adb 100644 --- a/project/src/services/RagfairPriceService.ts +++ b/project/src/services/RagfairPriceService.ts @@ -253,9 +253,9 @@ export class RagfairPriceService implements OnLoad } /** - * @param itemTemplateId - * @param desiredCurrency - * @param item + * @param itemTemplateId items tpl value + * @param desiredCurrency Currency to return result in + * @param item Item object (used for weapon presets) * @param offerItems * @param isPackOffer * @returns diff --git a/project/tests/controllers/InsuranceController.test.ts b/project/tests/controllers/InsuranceController.test.ts index 7b1fc23b..c36911d7 100644 --- a/project/tests/controllers/InsuranceController.test.ts +++ b/project/tests/controllers/InsuranceController.test.ts @@ -928,7 +928,7 @@ describe("InsuranceController", () => describe("processAttachmentByParent", () => { - it("should handle sorting, rolling, and deleting attachments by calling helper methods", () => + it("should handle weighing and counting of attachments by calling helper methods", () => { const insured = insuranceFixture[0]; const itemsMap = insuranceController.itemHelper.generateItemsMap(insured.items); @@ -941,49 +941,24 @@ describe("InsuranceController", () => const toDelete = new Set(); // Mock helper methods. - const mockSortAttachmentsByPrice = vi.spyOn(insuranceController, "sortAttachmentsByPrice"); - const mockCountSuccessfulRolls = vi.spyOn(insuranceController, "countSuccessfulRolls").mockReturnValue(4); - const mockAttachmentDeletionByValue = vi.spyOn(insuranceController, "attachmentDeletionByValue"); + const weightAttachmentsByPrice = vi.spyOn(insuranceController, "weightAttachmentsByPrice"); + const getAttachmentCountToRemove = vi.spyOn(insuranceController, "getAttachmentCountToRemove") + .mockReturnValue(4); + const logAttachmentsBeingRemoved = vi.spyOn(insuranceController, "logAttachmentsBeingRemoved"); // Execute the method. insuranceController.processAttachmentByParent(attachments, insured.traderId, toDelete); // Verify that helper methods are called. - expect(mockSortAttachmentsByPrice).toHaveBeenCalledWith(attachments); - expect(mockCountSuccessfulRolls).toHaveBeenCalled(); - expect(mockAttachmentDeletionByValue).toHaveBeenCalled(); - }); - - it("should log attachment details and number of attachments to be deleted", () => - { - const insured = insuranceFixture[0]; - const itemsMap = insuranceController.itemHelper.generateItemsMap(insured.items); - const parentAttachmentsMap = insuranceController.populateParentAttachmentsMap( - insuranceController.hashUtil.generate(), - insured, - itemsMap, - ); - const attachments = parentAttachmentsMap.entries().next().value; - const toDelete = new Set(); - const successfulRolls = 4; - - // Mock helper methods. - const mockLogAttachmentsDetails = vi.spyOn(insuranceController, "logAttachmentsDetails"); - vi.spyOn(insuranceController, "countSuccessfulRolls").mockReturnValue(successfulRolls); - const mockLoggerDebug = vi.spyOn(insuranceController.logger, "debug").mockImplementation(vi.fn()); - - // Execute the method. - insuranceController.processAttachmentByParent(attachments, insured.traderId, toDelete); - - // Verify that the logs were called/written. - expect(mockLogAttachmentsDetails).toBeCalled(); - expect(mockLoggerDebug).toHaveBeenCalledWith(`Number of attachments to be deleted: ${successfulRolls}`); + expect(weightAttachmentsByPrice).toHaveBeenCalledWith(attachments); + expect(getAttachmentCountToRemove).toHaveBeenCalled(); + expect(logAttachmentsBeingRemoved).toHaveBeenCalled(); }); }); - describe("sortAttachmentsByPrice", () => + describe("getAttachmentCountToRemove", () => { - it("should sort the attachments array by dynamicPrice in descending order", () => + it("should handle returning a count of attachments that should be removed that is below the total attachment count", () => { const insured = insuranceFixture[0]; const itemsMap = insuranceController.itemHelper.generateItemsMap(insured.items); @@ -995,20 +970,12 @@ describe("InsuranceController", () => const attachments = parentAttachmentsMap.entries().next().value; const attachmentCount = attachments.length; - // Execute the method. - const sortedAttachments = insuranceController.sortAttachmentsByPrice(attachments); + const result = insuranceController.getAttachmentCountToRemove(attachments, insured.traderId); - // Verify the length of the sorted attachments array is unchanged - expect(sortedAttachments.length).toBe(attachmentCount); - - // Verify that the attachments are sorted by dynamicPrice in descending order - for (let i = 1; i < sortedAttachments.length; i++) - { - expect(sortedAttachments[i - 1].dynamicPrice).toBeGreaterThanOrEqual(sortedAttachments[i].dynamicPrice); - } + expect(result).lessThanOrEqual(attachmentCount); }); - it("should place attachments with null dynamicPrice at the bottom of the sorted list", () => + it("should handle returning 0 when chanceNoAttachmentsTakenPercent is 100%", () => { const insured = insuranceFixture[0]; const itemsMap = insuranceController.itemHelper.generateItemsMap(insured.items); @@ -1018,133 +985,36 @@ describe("InsuranceController", () => itemsMap, ); const attachments = parentAttachmentsMap.entries().next().value; + insuranceController.insuranceConfig.chanceNoAttachmentsTakenPercent = 100; - // Set the dynamicPrice of the first attachment to null. - vi.spyOn(insuranceController.ragfairPriceService, "getDynamicItemPrice").mockReturnValue(666) - .mockReturnValueOnce(null); + const result = insuranceController.getAttachmentCountToRemove(attachments, insured.traderId); - // Execute the method. - const sortedAttachments = insuranceController.sortAttachmentsByPrice(attachments); - - // Verify that the attachments with null dynamicPrice are at the bottom of the list - const nullPriceAttachments = sortedAttachments.slice(-1); - for (const attachment of nullPriceAttachments) - { - expect(attachment.dynamicPrice).toBeNull(); - } - - // Verify that the rest of the attachments are sorted by dynamicPrice in descending order - for (let i = 1; i < sortedAttachments.length - 2; i++) - { - expect(sortedAttachments[i - 1].dynamicPrice).toBeGreaterThanOrEqual(sortedAttachments[i].dynamicPrice); - } - }); - }); - - describe("logAttachmentsDetails", () => - { - it("should log details for each attachment", () => - { - const attachments = [{ _id: "item1", name: "Item 1", dynamicPrice: 100 }, { - _id: "item2", - name: "Item 2", - dynamicPrice: 200, - }]; - - // Mock the logger.debug function. - const loggerDebugSpy = vi.spyOn(insuranceController.logger, "debug"); - - // Execute the method. - insuranceController.logAttachmentsDetails(attachments); - - // Verify that logger.debug was called correctly. - expect(loggerDebugSpy).toHaveBeenCalledTimes(2); - expect(loggerDebugSpy).toHaveBeenNthCalledWith(1, "Attachment 1: \"Item 1\" - Price: 100"); - expect(loggerDebugSpy).toHaveBeenNthCalledWith(2, "Attachment 2: \"Item 2\" - Price: 200"); - }); - - it("should not log anything when there are no attachments", () => - { - const attachments = []; - - // Mock the logger.debug function. - const loggerDebugSpy = vi.spyOn(insuranceController.logger, "debug"); - - // Execute the method. - insuranceController.logAttachmentsDetails(attachments); - - // Verify that logger.debug was called correctly. - expect(loggerDebugSpy).not.toHaveBeenCalled(); - }); - }); - - describe("countSuccessfulRolls", () => - { - it("should count the number of successful rolls made in the rollForDelete method", () => - { - const insured = insuranceFixture[0]; - const itemsMap = insuranceController.itemHelper.generateItemsMap(insured.items); - const parentAttachmentsMap = insuranceController.populateParentAttachmentsMap( - insuranceController.hashUtil.generate(), - insured, - itemsMap, - ); - const attachments = parentAttachmentsMap.values().next().value; - - // Mock rollForDelete to return true for the first two attachments. - const mockRollForDelete = vi.spyOn(insuranceController, "rollForDelete").mockReturnValue(false) - .mockReturnValueOnce(true).mockReturnValueOnce(true); - - // Execute the method. - const result = insuranceController.countSuccessfulRolls(attachments, insured.traderId); - - // Verify that two successful rolls were counted. - expect(mockRollForDelete).toHaveBeenCalledTimes(attachments.length); - expect(result).toBe(2); - }); - - it("should return zero if no successful rolls were made in the rollForDelete method", () => - { - const insured = insuranceFixture[0]; - const itemsMap = insuranceController.itemHelper.generateItemsMap(insured.items); - const parentAttachmentsMap = insuranceController.populateParentAttachmentsMap( - insuranceController.hashUtil.generate(), - insured, - itemsMap, - ); - const attachments = parentAttachmentsMap.values().next().value; - - // Mock rollForDelete to return false. - const mockRollForDelete = vi.spyOn(insuranceController, "rollForDelete").mockReturnValue(false); - - // Execute the method. - const result = insuranceController.countSuccessfulRolls(attachments, insured.traderId); - - // Verify that zero successful rolls were counted. - expect(mockRollForDelete).toHaveBeenCalledTimes(attachments.length); expect(result).toBe(0); }); - it("should return zero if there are no attachments", () => + it("should handle returning 0 when all attachments are below configured threshold price", () => { const insured = insuranceFixture[0]; - const attachments = []; + const itemsMap = insuranceController.itemHelper.generateItemsMap(insured.items); + const parentAttachmentsMap = insuranceController.populateParentAttachmentsMap( + insuranceController.hashUtil.generate(), + insured, + itemsMap, + ); + const attachments = parentAttachmentsMap.values().next().value; + insuranceController.insuranceConfig.minAttachmentRoublePriceToBeTaken = 2; + vi.spyOn(insuranceController.ragfairPriceService, "getDynamicItemPrice").mockReturnValue(1); - // Spy on rollForDelete to ensure it is not called. - const mockRollForDelete = vi.spyOn(insuranceController, "rollForDelete"); + const weightedAttachments = insuranceController.weightAttachmentsByPrice(attachments); + const result = insuranceController.getAttachmentCountToRemove(weightedAttachments, insured.traderId); - // Execute the method. - const result = insuranceController.countSuccessfulRolls(attachments, insured.traderId); - - // Verify that zero successful rolls were returned. - expect(mockRollForDelete).not.toHaveBeenCalled(); expect(result).toBe(0); }); }); - describe("attachmentDeletionByValue", () => + describe("weightAttachmentsByPrice", () => { - it("should add the correct number of attachments to the toDelete set", () => + it("Should create a dictionary of 2 items with weights of 1 for each", () => { const insured = insuranceFixture[0]; const itemsMap = insuranceController.itemHelper.generateItemsMap(insured.items); @@ -1155,57 +1025,11 @@ describe("InsuranceController", () => ); const attachments = parentAttachmentsMap.values().next().value; - const successfulRolls = 2; - const toDelete = new Set(); + vi.spyOn(insuranceController.ragfairPriceService, "getDynamicItemPrice").mockReturnValue(1); - // Execute the method. - insuranceController.attachmentDeletionByValue(attachments, successfulRolls, toDelete); - - // Should add the first two valuable attachments to the toDelete set. - expect(toDelete.size).toEqual(successfulRolls); - }); - - it("should not add any attachments to toDelete if successfulRolls is zero", () => - { - const insured = insuranceFixture[0]; - const itemsMap = insuranceController.itemHelper.generateItemsMap(insured.items); - const parentAttachmentsMap = insuranceController.populateParentAttachmentsMap( - insuranceController.hashUtil.generate(), - insured, - itemsMap, - ); - const attachments = parentAttachmentsMap.values().next().value; - - const successfulRolls = 0; - const toDelete = new Set(); - - // Execute the method. - insuranceController.attachmentDeletionByValue(attachments, successfulRolls, toDelete); - - // Should be empty. - expect(toDelete.size).toEqual(successfulRolls); - }); - - it("should add all attachments to toDelete if successfulRolls is greater than the number of attachments", () => - { - const insured = insuranceFixture[0]; - const itemsMap = insuranceController.itemHelper.generateItemsMap(insured.items); - const parentAttachmentsMap = insuranceController.populateParentAttachmentsMap( - insuranceController.hashUtil.generate(), - insured, - itemsMap, - ); - const attachments = parentAttachmentsMap.values().next().value; - - const successfulRolls = 999; - const toDelete = new Set(); - - // Execute the method. - insuranceController.attachmentDeletionByValue(attachments, successfulRolls, toDelete); - - // Should be empty. - expect(toDelete.size).toBeLessThan(successfulRolls); - expect(toDelete.size).toEqual(attachments.length); + const result = insuranceController.weightAttachmentsByPrice(attachments); + expect(Object.keys(result).length).toBe(2); + expect(Object.values(result)).toStrictEqual([1, 1]); }); });