Merge branch '3.8.2-DEV' into eslint-stylistic

This commit is contained in:
Refringe 2024-05-08 01:25:50 +00:00
commit a73a2a76e6
19 changed files with 241 additions and 380 deletions

View File

@ -17,5 +17,7 @@
"patron_in_weapon" "patron_in_weapon"
], ],
"returnTimeOverrideSeconds": 0, "returnTimeOverrideSeconds": 0,
"runIntervalSeconds": 600 "runIntervalSeconds": 600,
"minAttachmentRoublePriceToBeTaken": 2000,
"chanceNoAttachmentsTakenPercent": 10
} }

View File

@ -2495,7 +2495,8 @@
"Gizzy", "Gizzy",
"LuckyCharmT", "LuckyCharmT",
"Rena-chan", "Rena-chan",
"HB" "HB",
"John Pork"
], ],
"generation": { "generation": {
"items": { "items": {

View File

@ -2492,7 +2492,8 @@
"Gizzy", "Gizzy",
"LuckyCharmT", "LuckyCharmT",
"Rena-chan", "Rena-chan",
"HB" "HB",
"John Pork"
], ],
"generation": { "generation": {
"items": { "items": {

View File

@ -3,6 +3,7 @@ import { inject, injectable } from "tsyringe";
import { DialogueController } from "@spt-aki/controllers/DialogueController"; import { DialogueController } from "@spt-aki/controllers/DialogueController";
import { OnUpdate } from "@spt-aki/di/OnUpdate"; import { OnUpdate } from "@spt-aki/di/OnUpdate";
import { IEmptyRequestData } from "@spt-aki/models/eft/common/IEmptyRequestData"; import { IEmptyRequestData } from "@spt-aki/models/eft/common/IEmptyRequestData";
import { IUIDRequestData } from "@spt-aki/models/eft/common/request/IUIDRequestData";
import { import {
IAcceptFriendRequestData, IAcceptFriendRequestData,
ICancelFriendRequestData, ICancelFriendRequestData,
@ -247,14 +248,14 @@ export class DialogueCallbacks implements OnUpdate
/** Handle client/friend/ignore/set */ /** Handle client/friend/ignore/set */
// eslint-disable-next-line @typescript-eslint/no-unused-vars // 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(); return this.httpResponse.nullResponse();
} }
/** Handle client/friend/ignore/remove */ /** Handle client/friend/ignore/remove */
// eslint-disable-next-line @typescript-eslint/no-unused-vars // 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(); return this.httpResponse.nullResponse();
} }

View File

@ -3,6 +3,7 @@ import { inject, injectable } from "tsyringe";
import { GameController } from "@spt-aki/controllers/GameController"; import { GameController } from "@spt-aki/controllers/GameController";
import { OnLoad } from "@spt-aki/di/OnLoad"; import { OnLoad } from "@spt-aki/di/OnLoad";
import { IEmptyRequestData } from "@spt-aki/models/eft/common/IEmptyRequestData"; 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 { ICheckVersionResponse } from "@spt-aki/models/eft/game/ICheckVersionResponse";
import { ICurrentGroupResponse } from "@spt-aki/models/eft/game/ICurrentGroupResponse"; import { ICurrentGroupResponse } from "@spt-aki/models/eft/game/ICurrentGroupResponse";
import { IGameConfigResponse } from "@spt-aki/models/eft/game/IGameConfigResponse"; 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 { IGameStartResponse } from "@spt-aki/models/eft/game/IGameStartResponse";
import { IGetRaidTimeRequest } from "@spt-aki/models/eft/game/IGetRaidTimeRequest"; import { IGetRaidTimeRequest } from "@spt-aki/models/eft/game/IGetRaidTimeRequest";
import { IGetRaidTimeResponse } from "@spt-aki/models/eft/game/IGetRaidTimeResponse"; 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 { IServerDetails } from "@spt-aki/models/eft/game/IServerDetails";
import { IVersionValidateRequestData } from "@spt-aki/models/eft/game/IVersionValidateRequestData"; import { IVersionValidateRequestData } from "@spt-aki/models/eft/game/IVersionValidateRequestData";
import { IGetBodyResponseData } from "@spt-aki/models/eft/httpResponse/IGetBodyResponseData"; 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() }); 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(); return this.httpResponse.nullResponse();
} }

View File

@ -3,9 +3,9 @@ import { inject, injectable } from "tsyringe";
import { NotifierController } from "@spt-aki/controllers/NotifierController"; import { NotifierController } from "@spt-aki/controllers/NotifierController";
import { HttpServerHelper } from "@spt-aki/helpers/HttpServerHelper"; import { HttpServerHelper } from "@spt-aki/helpers/HttpServerHelper";
import { IEmptyRequestData } from "@spt-aki/models/eft/common/IEmptyRequestData"; 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 { IGetBodyResponseData } from "@spt-aki/models/eft/httpResponse/IGetBodyResponseData";
import { INotifierChannel } from "@spt-aki/models/eft/notifier/INotifier"; 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 { ISelectProfileResponse } from "@spt-aki/models/eft/notifier/ISelectProfileResponse";
import { HttpResponseUtil } from "@spt-aki/utils/HttpResponseUtil"; import { HttpResponseUtil } from "@spt-aki/utils/HttpResponseUtil";
import { JsonUtil } from "@spt-aki/utils/JsonUtil"; import { JsonUtil } from "@spt-aki/utils/JsonUtil";
@ -68,7 +68,7 @@ export class NotifierCallbacks
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
public selectProfile( public selectProfile(
url: string, url: string,
info: ISelectProfileRequestData, info: IUIDRequestData,
sessionID: string, sessionID: string,
): IGetBodyResponseData<ISelectProfileResponse> ): IGetBodyResponseData<ISelectProfileResponse>
{ {

View File

@ -4,6 +4,7 @@ import { DialogueHelper } from "@spt-aki/helpers/DialogueHelper";
import { ItemHelper } from "@spt-aki/helpers/ItemHelper"; import { ItemHelper } from "@spt-aki/helpers/ItemHelper";
import { ProfileHelper } from "@spt-aki/helpers/ProfileHelper"; import { ProfileHelper } from "@spt-aki/helpers/ProfileHelper";
import { TraderHelper } from "@spt-aki/helpers/TraderHelper"; import { TraderHelper } from "@spt-aki/helpers/TraderHelper";
import { WeightedRandomHelper } from "@spt-aki/helpers/WeightedRandomHelper";
import { IPmcData } from "@spt-aki/models/eft/common/IPmcData"; import { IPmcData } from "@spt-aki/models/eft/common/IPmcData";
import { Item } from "@spt-aki/models/eft/common/tables/IItem"; import { Item } from "@spt-aki/models/eft/common/tables/IItem";
import { IGetInsuranceCostRequestData } from "@spt-aki/models/eft/insurance/IGetInsuranceCostRequestData"; 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 { PaymentService } from "@spt-aki/services/PaymentService";
import { RagfairPriceService } from "@spt-aki/services/RagfairPriceService"; import { RagfairPriceService } from "@spt-aki/services/RagfairPriceService";
import { HashUtil } from "@spt-aki/utils/HashUtil"; import { HashUtil } from "@spt-aki/utils/HashUtil";
import { JsonUtil } from "@spt-aki/utils/JsonUtil";
import { MathUtil } from "@spt-aki/utils/MathUtil"; 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"; import { TimeUtil } from "@spt-aki/utils/TimeUtil";
@injectable() @injectable()
@ -39,6 +41,7 @@ export class InsuranceController
@inject("WinstonLogger") protected logger: ILogger, @inject("WinstonLogger") protected logger: ILogger,
@inject("RandomUtil") protected randomUtil: RandomUtil, @inject("RandomUtil") protected randomUtil: RandomUtil,
@inject("MathUtil") protected mathUtil: MathUtil, @inject("MathUtil") protected mathUtil: MathUtil,
@inject("JsonUtil") protected jsonUtil: JsonUtil,
@inject("HashUtil") protected hashUtil: HashUtil, @inject("HashUtil") protected hashUtil: HashUtil,
@inject("EventOutputHolder") protected eventOutputHolder: EventOutputHolder, @inject("EventOutputHolder") protected eventOutputHolder: EventOutputHolder,
@inject("TimeUtil") protected timeUtil: TimeUtil, @inject("TimeUtil") protected timeUtil: TimeUtil,
@ -47,6 +50,7 @@ export class InsuranceController
@inject("ItemHelper") protected itemHelper: ItemHelper, @inject("ItemHelper") protected itemHelper: ItemHelper,
@inject("ProfileHelper") protected profileHelper: ProfileHelper, @inject("ProfileHelper") protected profileHelper: ProfileHelper,
@inject("DialogueHelper") protected dialogueHelper: DialogueHelper, @inject("DialogueHelper") protected dialogueHelper: DialogueHelper,
@inject("WeightedRandomHelper") protected weightedRandomHelper: WeightedRandomHelper,
@inject("TraderHelper") protected traderHelper: TraderHelper, @inject("TraderHelper") protected traderHelper: TraderHelper,
@inject("PaymentService") protected paymentService: PaymentService, @inject("PaymentService") protected paymentService: PaymentService,
@inject("InsuranceService") protected insuranceService: InsuranceService, @inject("InsuranceService") protected insuranceService: InsuranceService,
@ -448,83 +452,100 @@ export class InsuranceController
*/ */
protected processAttachmentByParent(attachments: Item[], traderId: string, toDelete: Set<string>): void protected processAttachmentByParent(attachments: Item[], traderId: string, toDelete: Set<string>): void
{ {
const sortedAttachments = this.sortAttachmentsByPrice(attachments); // Create dict of item ids + their flea/handbook price (highest is chosen)
this.logAttachmentsDetails(sortedAttachments); const weightedAttachmentByPrice = this.weightAttachmentsByPrice(attachments);
const successfulRolls = this.countSuccessfulRolls(sortedAttachments, traderId); // Get how many attachments we want to pull off parent
this.logger.debug(`Number of attachments to be deleted: ${successfulRolls}`); 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<string, number>(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}`);
} }
/** protected logAttachmentsBeingRemoved(
* Sorts the attachment items by their dynamic price in descending order. attachmentIdsToRemove: string[],
* attachments: Item[],
* @param attachments The array of attachments items. attachmentPrices: Record<string, number>,
* @returns An array of items enriched with their max price and common locale-name. ): void
*/
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
{ {
let index = 1; 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++; index++;
} }
} }
/** protected weightAttachmentsByPrice(attachments: Item[]): Record<string, number>
* 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
{ {
const rolls = Array.from({ length: attachments.length }, () => this.rollForDelete(traderId)); const result: Record<string, number> = {};
return rolls.filter(Boolean).length;
// 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. * 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 attachments The array of attachment items. * @param traderId Trader attachment insured against
* @param successfulRolls The number of successful rolls. * @returns Attachment count to remove
* @param toDelete The array that accumulates the IDs of the items to be deleted.
*/ */
protected attachmentDeletionByValue( protected getAttachmentCountToRemove(weightedAttachmentByPrice: Record<string, number>, traderId: string): number
attachments: EnrichedItem[],
successfulRolls: number,
toDelete: Set<string>,
): void
{ {
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); return removeCount;
if (valuableChild) }
for (const attachmentId of Object.keys(weightedAttachmentByPrice))
{
// Below min price to be taken, skip
if (weightedAttachmentByPrice[attachmentId] < this.insuranceConfig.minAttachmentRoublePriceToBeTaken)
{ {
const { name, dynamicPrice } = valuableChild; continue;
this.logger.debug(`Marked attachment "${name}" for removal - Dyanmic Price: ${dynamicPrice}`); }
toDelete.add(attachmentsId);
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. * trader-specific return chance.
* *
* @param traderId The ID of the trader who insured the item. * @param traderId The ID of the trader who insured the item.

View File

@ -1,6 +1,7 @@
import { inject, injectable } from "tsyringe"; import { inject, injectable } from "tsyringe";
import { ItemHelper } from "@spt-aki/helpers/ItemHelper"; 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 { ITemplateItem } from "@spt-aki/models/eft/common/tables/ITemplateItem";
import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes"; import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes";
import { IPmcConfig } from "@spt-aki/models/spt/config/IPmcConfig"; import { IPmcConfig } from "@spt-aki/models/spt/config/IPmcConfig";
@ -22,6 +23,8 @@ export class PMCLootGenerator
protected backpackLootPool: Record<string, number> = {}; protected backpackLootPool: Record<string, number> = {};
protected pmcConfig: IPmcConfig; protected pmcConfig: IPmcConfig;
protected roubleTpl = "5449016a4bdc2d6f028b456f";
constructor( constructor(
@inject("ItemHelper") protected itemHelper: ItemHelper, @inject("ItemHelper") protected itemHelper: ItemHelper,
@inject("DatabaseServer") protected databaseServer: DatabaseServer, @inject("DatabaseServer") protected databaseServer: DatabaseServer,
@ -29,6 +32,7 @@ export class PMCLootGenerator
@inject("ItemFilterService") protected itemFilterService: ItemFilterService, @inject("ItemFilterService") protected itemFilterService: ItemFilterService,
@inject("RagfairPriceService") protected ragfairPriceService: RagfairPriceService, @inject("RagfairPriceService") protected ragfairPriceService: RagfairPriceService,
@inject("SeasonalEventService") protected seasonalEventService: SeasonalEventService, @inject("SeasonalEventService") protected seasonalEventService: SeasonalEventService,
@inject("WeightedRandomHelper") protected weightedRandomHelper: WeightedRandomHelper,
) )
{ {
this.pmcConfig = this.configServer.getConfig(ConfigTypes.PMC); this.pmcConfig = this.configServer.getConfig(ConfigTypes.PMC);
@ -74,7 +78,7 @@ export class PMCLootGenerator
else else
{ {
// Set price of item as its weight // 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; this.pocketLootPool[itemToAdd._id] = price;
} }
} }
@ -87,7 +91,7 @@ export class PMCLootGenerator
this.pocketLootPool[key] = Math.round((1 / this.pocketLootPool[key]) * highestPrice); this.pocketLootPool[key] = Math.round((1 / this.pocketLootPool[key]) * highestPrice);
} }
this.reduceWeightValues(this.pocketLootPool); this.weightedRandomHelper.reduceWeightValues(this.pocketLootPool);
} }
return this.pocketLootPool; return this.pocketLootPool;
@ -132,7 +136,7 @@ export class PMCLootGenerator
else else
{ {
// Set price of item as its weight // 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; this.vestLootPool[itemToAdd._id] = price;
} }
} }
@ -145,7 +149,7 @@ export class PMCLootGenerator
this.vestLootPool[key] = Math.round((1 / this.vestLootPool[key]) * highestPrice); this.vestLootPool[key] = Math.round((1 / this.vestLootPool[key]) * highestPrice);
} }
this.reduceWeightValues(this.vestLootPool); this.weightedRandomHelper.reduceWeightValues(this.vestLootPool);
} }
return this.vestLootPool; return this.vestLootPool;
@ -200,7 +204,7 @@ export class PMCLootGenerator
else else
{ {
// Set price of item as its weight // 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; this.backpackLootPool[itemToAdd._id] = price;
} }
} }
@ -213,71 +217,9 @@ export class PMCLootGenerator
this.backpackLootPool[key] = Math.round((1 / this.backpackLootPool[key]) * highestPrice); this.backpackLootPool[key] = Math.round((1 / this.backpackLootPool[key]) * highestPrice);
} }
this.reduceWeightValues(this.backpackLootPool); this.weightedRandomHelper.reduceWeightValues(this.backpackLootPool);
} }
return 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<string, number>): 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;
}
} }

View File

@ -146,16 +146,17 @@ export class GiveSptCommand implements ISptCommand
commandHandler, commandHandler,
`An error occurred while trying to use localized text. Locale will be defaulted to 'en'.`, `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"; 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() const closestItemsMatchedByName = this.itemHelper.getItems()
.filter((i) => this.isItemAllowed(i)) .filter((i) => this.isItemAllowed(i))
.map((i) => localizedGlobal[`${i?._id} Name`]?.toLowerCase()) .map((i) => localizedGlobal[`${i?._id} Name`]?.toLowerCase() ?? i._props.Name)
.filter((i) => i !== undefined) .filter((i) => i !== undefined && i !== "")
.map(i => ({match: stringSimilarity(item.toLocaleLowerCase(), i.toLocaleLowerCase()), itemName: i})) .map(i => ({match: stringSimilarity(item.toLocaleLowerCase(), i.toLocaleLowerCase()), itemName: i}))
.sort((a1, a2) => a2.match - a1.match); .sort((a1, a2) => a2.match - a1.match);
@ -186,7 +187,7 @@ export class GiveSptCommand implements ISptCommand
const tplId = isItemName const tplId = isItemName
? this.itemHelper.getItems() ? this.itemHelper.getItems()
.filter((i) => this.isItemAllowed(i)) .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; : item;
const checkedItem = this.itemHelper.getItem(tplId); const checkedItem = this.itemHelper.getItem(tplId);
@ -202,9 +203,12 @@ export class GiveSptCommand implements ISptCommand
const itemsToSend: Item[] = []; const itemsToSend: Item[] = [];
if ( if (
this.itemHelper.isOfBaseclass(checkedItem[1]._id, BaseClasses.WEAPON) (this.itemHelper.isOfBaseclass(checkedItem[1]._id, BaseClasses.WEAPON)
|| this.itemHelper.isOfBaseclass(checkedItem[1]._id, BaseClasses.ARMOR) || this.itemHelper.isOfBaseclass(checkedItem[1]._id, BaseClasses.ARMOR)
|| this.itemHelper.isOfBaseclass(checkedItem[1]._id, BaseClasses.VEST) || 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); const preset = this.presetHelper.getDefaultPreset(checkedItem[1]._id);

View File

@ -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<string, number>): 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;
}
} }

View File

@ -0,0 +1,4 @@
export interface IUIDRequestData
{
uid: string;
}

View File

@ -1,4 +0,0 @@
export interface IReportNicknameRequestData
{
uid: string;
}

View File

@ -1,4 +0,0 @@
export interface ISelectProfileRequestData
{
uid: string;
}

View File

@ -1,7 +1,7 @@
import { IEmptyRequestData } from "@spt-aki/models/eft/common/IEmptyRequestData"; 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 { IGetBodyResponseData } from "@spt-aki/models/eft/httpResponse/IGetBodyResponseData";
import { INotifierChannel } from "@spt-aki/models/eft/notifier/INotifier"; import { INotifierChannel } from "@spt-aki/models/eft/notifier/INotifier";
import { ISelectProfileRequestData } from "@spt-aki/models/eft/notifier/ISelectProfileRequestData";
export interface INotifierCallbacks export interface INotifierCallbacks
{ {
@ -18,6 +18,6 @@ export interface INotifierCallbacks
info: IEmptyRequestData, info: IEmptyRequestData,
sessionID: string, sessionID: string,
): IGetBodyResponseData<INotifierChannel>; ): IGetBodyResponseData<INotifierChannel>;
selectProfile(url: string, info: ISelectProfileRequestData, sessionID: string): IGetBodyResponseData<any>; selectProfile(url: string, info: IUIDRequestData, sessionID: string): IGetBodyResponseData<any>;
notify(url: string, info: any, sessionID: string): string; notify(url: string, info: any, sessionID: string): string;
} }

View File

@ -15,4 +15,8 @@ export interface IInsuranceConfig extends IBaseConfig
returnTimeOverrideSeconds: number; returnTimeOverrideSeconds: number;
/** How often server should process insurance in seconds */ /** How often server should process insurance in seconds */
runIntervalSeconds: number; 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;
} }

View File

@ -22,7 +22,7 @@ export class BundleSerializer extends Serializer
{ {
this.logger.info(`[BUNDLE]: ${req.url}`); 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); const bundle = this.bundleLoader.getBundle(key);
this.httpFileUtil.sendFile(resp, `${bundle.modpath}/bundles/${bundle.filename}`); this.httpFileUtil.sendFile(resp, `${bundle.modpath}/bundles/${bundle.filename}`);

View File

@ -897,24 +897,27 @@ export class ProfileFixerService
} }
} }
// Remove invalid builds from weapon, equipment and magazine build lists if (fullProfile.userbuilds)
const weaponBuilds = fullProfile.userbuilds?.weaponBuilds || [];
fullProfile.userbuilds.weaponBuilds = weaponBuilds.filter((weaponBuild) =>
{ {
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 || []; const equipmentBuilds = fullProfile.userbuilds?.equipmentBuilds || [];
fullProfile.userbuilds.equipmentBuilds = equipmentBuilds.filter((equipmentBuild) => fullProfile.userbuilds.equipmentBuilds = equipmentBuilds.filter((equipmentBuild) =>
{ {
return !this.shouldRemoveWeaponEquipmentBuild("equipment", equipmentBuild, itemsDb); return !this.shouldRemoveWeaponEquipmentBuild("equipment", equipmentBuild, itemsDb);
}); });
const magazineBuilds = fullProfile.userbuilds?.magazineBuilds || []; const magazineBuilds = fullProfile.userbuilds?.magazineBuilds || [];
fullProfile.userbuilds.magazineBuilds = magazineBuilds.filter((magazineBuild) => fullProfile.userbuilds.magazineBuilds = magazineBuilds.filter((magazineBuild) =>
{ {
return !this.shouldRemoveMagazineBuild(magazineBuild, itemsDb); return !this.shouldRemoveMagazineBuild(magazineBuild, itemsDb);
}); });
}
// Iterate over dialogs, looking for messages with items not found in item db, remove message if item found // Iterate over dialogs, looking for messages with items not found in item db, remove message if item found
for (const dialogId in fullProfile.dialogues) for (const dialogId in fullProfile.dialogues)

View File

@ -253,9 +253,9 @@ export class RagfairPriceService implements OnLoad
} }
/** /**
* @param itemTemplateId * @param itemTemplateId items tpl value
* @param desiredCurrency * @param desiredCurrency Currency to return result in
* @param item * @param item Item object (used for weapon presets)
* @param offerItems * @param offerItems
* @param isPackOffer * @param isPackOffer
* @returns * @returns

View File

@ -928,7 +928,7 @@ describe("InsuranceController", () =>
describe("processAttachmentByParent", () => 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 insured = insuranceFixture[0];
const itemsMap = insuranceController.itemHelper.generateItemsMap(insured.items); const itemsMap = insuranceController.itemHelper.generateItemsMap(insured.items);
@ -941,49 +941,24 @@ describe("InsuranceController", () =>
const toDelete = new Set<string>(); const toDelete = new Set<string>();
// Mock helper methods. // Mock helper methods.
const mockSortAttachmentsByPrice = vi.spyOn(insuranceController, "sortAttachmentsByPrice"); const weightAttachmentsByPrice = vi.spyOn(insuranceController, "weightAttachmentsByPrice");
const mockCountSuccessfulRolls = vi.spyOn(insuranceController, "countSuccessfulRolls").mockReturnValue(4); const getAttachmentCountToRemove = vi.spyOn(insuranceController, "getAttachmentCountToRemove")
const mockAttachmentDeletionByValue = vi.spyOn(insuranceController, "attachmentDeletionByValue"); .mockReturnValue(4);
const logAttachmentsBeingRemoved = vi.spyOn(insuranceController, "logAttachmentsBeingRemoved");
// Execute the method. // Execute the method.
insuranceController.processAttachmentByParent(attachments, insured.traderId, toDelete); insuranceController.processAttachmentByParent(attachments, insured.traderId, toDelete);
// Verify that helper methods are called. // Verify that helper methods are called.
expect(mockSortAttachmentsByPrice).toHaveBeenCalledWith(attachments); expect(weightAttachmentsByPrice).toHaveBeenCalledWith(attachments);
expect(mockCountSuccessfulRolls).toHaveBeenCalled(); expect(getAttachmentCountToRemove).toHaveBeenCalled();
expect(mockAttachmentDeletionByValue).toHaveBeenCalled(); expect(logAttachmentsBeingRemoved).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<string>();
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}`);
}); });
}); });
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 insured = insuranceFixture[0];
const itemsMap = insuranceController.itemHelper.generateItemsMap(insured.items); const itemsMap = insuranceController.itemHelper.generateItemsMap(insured.items);
@ -995,20 +970,12 @@ describe("InsuranceController", () =>
const attachments = parentAttachmentsMap.entries().next().value; const attachments = parentAttachmentsMap.entries().next().value;
const attachmentCount = attachments.length; const attachmentCount = attachments.length;
// Execute the method. const result = insuranceController.getAttachmentCountToRemove(attachments, insured.traderId);
const sortedAttachments = insuranceController.sortAttachmentsByPrice(attachments);
// Verify the length of the sorted attachments array is unchanged expect(result).lessThanOrEqual(attachmentCount);
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);
}
}); });
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 insured = insuranceFixture[0];
const itemsMap = insuranceController.itemHelper.generateItemsMap(insured.items); const itemsMap = insuranceController.itemHelper.generateItemsMap(insured.items);
@ -1018,133 +985,36 @@ describe("InsuranceController", () =>
itemsMap, itemsMap,
); );
const attachments = parentAttachmentsMap.entries().next().value; const attachments = parentAttachmentsMap.entries().next().value;
insuranceController.insuranceConfig.chanceNoAttachmentsTakenPercent = 100;
// Set the dynamicPrice of the first attachment to null. const result = insuranceController.getAttachmentCountToRemove(attachments, insured.traderId);
vi.spyOn(insuranceController.ragfairPriceService, "getDynamicItemPrice").mockReturnValue(666)
.mockReturnValueOnce(null);
// 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); 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 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 weightedAttachments = insuranceController.weightAttachmentsByPrice(attachments);
const mockRollForDelete = vi.spyOn(insuranceController, "rollForDelete"); 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); 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 insured = insuranceFixture[0];
const itemsMap = insuranceController.itemHelper.generateItemsMap(insured.items); const itemsMap = insuranceController.itemHelper.generateItemsMap(insured.items);
@ -1155,57 +1025,11 @@ describe("InsuranceController", () =>
); );
const attachments = parentAttachmentsMap.values().next().value; const attachments = parentAttachmentsMap.values().next().value;
const successfulRolls = 2; vi.spyOn(insuranceController.ragfairPriceService, "getDynamicItemPrice").mockReturnValue(1);
const toDelete = new Set<string>();
// Execute the method. const result = insuranceController.weightAttachmentsByPrice(attachments);
insuranceController.attachmentDeletionByValue(attachments, successfulRolls, toDelete); expect(Object.keys(result).length).toBe(2);
expect(Object.values(result)).toStrictEqual([1, 1]);
// 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<string>();
// 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<string>();
// Execute the method.
insuranceController.attachmentDeletionByValue(attachments, successfulRolls, toDelete);
// Should be empty.
expect(toDelete.size).toBeLessThan(successfulRolls);
expect(toDelete.size).toEqual(attachments.length);
}); });
}); });