654 lines
29 KiB
TypeScript
654 lines
29 KiB
TypeScript
import { HideoutHelper } from "@spt/helpers/HideoutHelper";
|
|
import { InventoryHelper } from "@spt/helpers/InventoryHelper";
|
|
import { ItemHelper } from "@spt/helpers/ItemHelper";
|
|
import { ProfileHelper } from "@spt/helpers/ProfileHelper";
|
|
import { TraderHelper } from "@spt/helpers/TraderHelper";
|
|
import { IPmcData } from "@spt/models/eft/common/IPmcData";
|
|
import { Bonus, HideoutSlot } from "@spt/models/eft/common/tables/IBotBase";
|
|
import { IPmcDataRepeatableQuest, IRepeatableQuest } from "@spt/models/eft/common/tables/IRepeatableQuests";
|
|
import { ITemplateItem } from "@spt/models/eft/common/tables/ITemplateItem";
|
|
import { StageBonus } from "@spt/models/eft/hideout/IHideoutArea";
|
|
import { IEquipmentBuild, IMagazineBuild, ISptProfile, IWeaponBuild } from "@spt/models/eft/profile/ISptProfile";
|
|
import { BonusType } from "@spt/models/enums/BonusType";
|
|
import { ConfigTypes } from "@spt/models/enums/ConfigTypes";
|
|
import { HideoutAreas } from "@spt/models/enums/HideoutAreas";
|
|
import { ICoreConfig } from "@spt/models/spt/config/ICoreConfig";
|
|
import { IRagfairConfig } from "@spt/models/spt/config/IRagfairConfig";
|
|
import { ILogger } from "@spt/models/spt/utils/ILogger";
|
|
import { ConfigServer } from "@spt/servers/ConfigServer";
|
|
import { DatabaseService } from "@spt/services/DatabaseService";
|
|
import { LocalisationService } from "@spt/services/LocalisationService";
|
|
import { HashUtil } from "@spt/utils/HashUtil";
|
|
import { JsonUtil } from "@spt/utils/JsonUtil";
|
|
import { TimeUtil } from "@spt/utils/TimeUtil";
|
|
import { Watermark } from "@spt/utils/Watermark";
|
|
import { ICloner } from "@spt/utils/cloners/ICloner";
|
|
import { inject, injectable } from "tsyringe";
|
|
|
|
@injectable()
|
|
export class ProfileFixerService {
|
|
protected coreConfig: ICoreConfig;
|
|
protected ragfairConfig: IRagfairConfig;
|
|
|
|
constructor(
|
|
@inject("PrimaryLogger") protected logger: ILogger,
|
|
@inject("Watermark") protected watermark: Watermark,
|
|
@inject("DatabaseService") protected databaseService: DatabaseService,
|
|
@inject("HideoutHelper") protected hideoutHelper: HideoutHelper,
|
|
@inject("InventoryHelper") protected inventoryHelper: InventoryHelper,
|
|
@inject("TraderHelper") protected traderHelper: TraderHelper,
|
|
@inject("ProfileHelper") protected profileHelper: ProfileHelper,
|
|
@inject("ItemHelper") protected itemHelper: ItemHelper,
|
|
@inject("LocalisationService") protected localisationService: LocalisationService,
|
|
@inject("TimeUtil") protected timeUtil: TimeUtil,
|
|
@inject("JsonUtil") protected jsonUtil: JsonUtil,
|
|
@inject("HashUtil") protected hashUtil: HashUtil,
|
|
@inject("ConfigServer") protected configServer: ConfigServer,
|
|
@inject("PrimaryCloner") protected cloner: ICloner,
|
|
) {
|
|
this.coreConfig = this.configServer.getConfig(ConfigTypes.CORE);
|
|
this.ragfairConfig = this.configServer.getConfig(ConfigTypes.RAGFAIR);
|
|
}
|
|
|
|
/**
|
|
* Find issues in the pmc profile data that may cause issues and fix them
|
|
* @param pmcProfile profile to check and fix
|
|
*/
|
|
public checkForAndFixPmcProfileIssues(pmcProfile: IPmcData): void {
|
|
this.removeDanglingConditionCounters(pmcProfile);
|
|
this.removeDanglingTaskConditionCounters(pmcProfile);
|
|
this.removeOrphanedQuests(pmcProfile);
|
|
|
|
if (pmcProfile.Hideout) {
|
|
this.addHideoutEliteSlots(pmcProfile);
|
|
}
|
|
|
|
if (pmcProfile.Skills) {
|
|
this.checkForSkillsOverMaxLevel(pmcProfile);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Find issues in the scav profile data that may cause issues
|
|
* @param scavProfile profile to check and fix
|
|
*/
|
|
public checkForAndFixScavProfileIssues(scavProfile: IPmcData): void {}
|
|
|
|
/**
|
|
* Attempt to fix common item issues that corrupt profiles
|
|
* @param pmcProfile Profile to check items of
|
|
*/
|
|
public fixProfileBreakingInventoryItemIssues(pmcProfile: IPmcData): void {
|
|
// Create a mapping of all inventory items, keyed by _id value
|
|
const itemMapping = pmcProfile.Inventory.items.reduce((acc, curr) => {
|
|
acc[curr._id] = acc[curr._id] || [];
|
|
acc[curr._id].push(curr);
|
|
|
|
return acc;
|
|
}, {});
|
|
|
|
for (const key in itemMapping) {
|
|
// Only one item for this id, not a dupe
|
|
if (itemMapping[key].length === 1) {
|
|
continue;
|
|
}
|
|
|
|
this.logger.warning(`${itemMapping[key].length - 1} duplicate(s) found for item: ${key}`);
|
|
const itemAJson = this.jsonUtil.serialize(itemMapping[key][0]);
|
|
const itemBJson = this.jsonUtil.serialize(itemMapping[key][1]);
|
|
if (itemAJson === itemBJson) {
|
|
// Both items match, we can safely delete one
|
|
const indexOfItemToRemove = pmcProfile.Inventory.items.findIndex((x) => x._id === key);
|
|
pmcProfile.Inventory.items.splice(indexOfItemToRemove, 1);
|
|
this.logger.warning(`Deleted duplicate item: ${key}`);
|
|
} else {
|
|
// Items are different, replace ID with unique value
|
|
// Only replace ID if items have no children, we dont want orphaned children
|
|
const itemsHaveChildren = pmcProfile.Inventory.items.some((x) => x.parentId === key);
|
|
if (!itemsHaveChildren) {
|
|
const itemToAdjust = pmcProfile.Inventory.items.find((x) => x._id === key);
|
|
itemToAdjust._id = this.hashUtil.generate();
|
|
this.logger.warning(`Replace duplicate item Id: ${key} with ${itemToAdjust._id}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Iterate over all inventory items
|
|
for (const item of pmcProfile.Inventory.items.filter((x) => x.slotId)) {
|
|
if (!item.upd) {
|
|
// Ignore items without a upd object
|
|
continue;
|
|
}
|
|
|
|
// Check items with a tag that contains non alphanumeric characters
|
|
const regxp = /([/w"\\'])/g;
|
|
if (item.upd.Tag?.Name && regxp.test(item.upd.Tag?.Name)) {
|
|
this.logger.warning(`Fixed item: ${item._id}s Tag value, removed invalid characters`);
|
|
item.upd.Tag.Name = item.upd.Tag.Name.replace(regxp, "");
|
|
}
|
|
|
|
// Check items with StackObjectsCount (undefined)
|
|
if (item.upd?.StackObjectsCount === undefined) {
|
|
this.logger.warning(`Fixed item: ${item._id}s undefined StackObjectsCount value, now set to 1`);
|
|
item.upd.StackObjectsCount = 1;
|
|
}
|
|
}
|
|
|
|
// Iterate over clothing
|
|
const customizationDb = this.databaseService.getTemplates().customization;
|
|
const customizationDbArray = Object.values(customizationDb);
|
|
const playerIsUsec = pmcProfile.Info.Side.toLowerCase() === "usec";
|
|
|
|
// Check Head
|
|
if (!customizationDb[pmcProfile.Customization.Head]) {
|
|
const defaultHead = playerIsUsec
|
|
? customizationDbArray.find((x) => x._name === "DefaultUsecHead")
|
|
: customizationDbArray.find((x) => x._name === "DefaultBearHead");
|
|
pmcProfile.Customization.Head = defaultHead._id;
|
|
}
|
|
|
|
// check Body
|
|
if (!customizationDb[pmcProfile.Customization.Body]) {
|
|
const defaultBody =
|
|
pmcProfile.Info.Side.toLowerCase() === "usec"
|
|
? customizationDbArray.find((x) => x._name === "DefaultUsecBody")
|
|
: customizationDbArray.find((x) => x._name === "DefaultBearBody");
|
|
pmcProfile.Customization.Body = defaultBody._id;
|
|
}
|
|
|
|
// check Hands
|
|
if (!customizationDb[pmcProfile.Customization.Hands]) {
|
|
const defaultHands =
|
|
pmcProfile.Info.Side.toLowerCase() === "usec"
|
|
? customizationDbArray.find((x) => x._name === "DefaultUsecHands")
|
|
: customizationDbArray.find((x) => x._name === "DefaultBearHands");
|
|
pmcProfile.Customization.Hands = defaultHands._id;
|
|
}
|
|
|
|
// check Hands
|
|
if (!customizationDb[pmcProfile.Customization.Feet]) {
|
|
const defaultFeet =
|
|
pmcProfile.Info.Side.toLowerCase() === "usec"
|
|
? customizationDbArray.find((x) => x._name === "DefaultUsecFeet")
|
|
: customizationDbArray.find((x) => x._name === "DefaultBearFeet");
|
|
pmcProfile.Customization.Feet = defaultFeet._id;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* TODO - make this non-public - currently used by RepeatableQuestController
|
|
* Remove unused condition counters
|
|
* @param pmcProfile profile to remove old counters from
|
|
*/
|
|
public removeDanglingConditionCounters(pmcProfile: IPmcData): void {
|
|
if (!pmcProfile.TaskConditionCounters) {
|
|
return;
|
|
}
|
|
|
|
for (const counterId in pmcProfile.TaskConditionCounters) {
|
|
const counter = pmcProfile.TaskConditionCounters[counterId];
|
|
if (!counter.sourceId) {
|
|
delete pmcProfile.TaskConditionCounters[counterId];
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Repeatable quests leave behind TaskConditionCounter objects that make the profile bloat with time, remove them
|
|
* @param pmcProfile Player profile to check
|
|
*/
|
|
protected removeDanglingTaskConditionCounters(pmcProfile: IPmcData): void {
|
|
if (pmcProfile.TaskConditionCounters) {
|
|
const taskConditionKeysToRemove: string[] = [];
|
|
const activeRepeatableQuests = this.getActiveRepeatableQuests(pmcProfile.RepeatableQuests);
|
|
const achievements = this.databaseService.getAchievements();
|
|
|
|
// Loop over TaskConditionCounters objects and add once we want to remove to counterKeysToRemove
|
|
for (const [key, taskConditionCounter] of Object.entries(pmcProfile.TaskConditionCounters)) {
|
|
// Only check if profile has repeatable quests
|
|
if (pmcProfile.RepeatableQuests && activeRepeatableQuests.length > 0) {
|
|
const existsInActiveRepeatableQuests = activeRepeatableQuests.some(
|
|
(quest) => quest._id === taskConditionCounter.sourceId,
|
|
);
|
|
const existsInQuests = pmcProfile.Quests.some(
|
|
(quest) => quest.qid === taskConditionCounter.sourceId,
|
|
);
|
|
const isAchievementTracker = achievements.some(
|
|
(quest) => quest.id === taskConditionCounter.sourceId,
|
|
);
|
|
|
|
// If task conditions id is neither in activeQuests, quests or achievements - it's stale and should be cleaned up
|
|
if (!(existsInActiveRepeatableQuests || existsInQuests || isAchievementTracker)) {
|
|
taskConditionKeysToRemove.push(key);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const counterKeyToRemove of taskConditionKeysToRemove) {
|
|
this.logger.debug(`Removed ${counterKeyToRemove} TaskConditionCounter object`);
|
|
delete pmcProfile.TaskConditionCounters[counterKeyToRemove];
|
|
}
|
|
}
|
|
}
|
|
|
|
protected getActiveRepeatableQuests(repeatableQuests: IPmcDataRepeatableQuest[]): IRepeatableQuest[] {
|
|
let activeQuests: IRepeatableQuest[] = [];
|
|
for (const repeatableQuest of repeatableQuests) {
|
|
if (repeatableQuest.activeQuests.length > 0) {
|
|
// daily/weekly collection has active quests in them, add to array and return
|
|
activeQuests = activeQuests.concat(repeatableQuest.activeQuests);
|
|
}
|
|
}
|
|
|
|
return activeQuests;
|
|
}
|
|
|
|
/**
|
|
* After removing mods that add quests, the quest panel will break without removing these
|
|
* @param pmcProfile Profile to remove dead quests from
|
|
*/
|
|
protected removeOrphanedQuests(pmcProfile: IPmcData): void {
|
|
const quests = this.databaseService.getQuests();
|
|
const profileQuests = pmcProfile.Quests;
|
|
|
|
const repeatableQuests: IRepeatableQuest[] = [];
|
|
for (const repeatableQuestType of pmcProfile.RepeatableQuests) {
|
|
repeatableQuests.push(...repeatableQuestType.activeQuests);
|
|
}
|
|
|
|
for (let i = profileQuests.length - 1; i >= 0; i--) {
|
|
if (!(quests[profileQuests[i].qid] || repeatableQuests.some((x) => x._id === profileQuests[i].qid))) {
|
|
profileQuests.splice(i, 1);
|
|
this.logger.success("Successfully removed orphaned quest that doesnt exist in our quest data");
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* If the profile has elite Hideout Managment skill, add the additional slots from globals
|
|
* NOTE: This seems redundant, but we will leave it here just incase.
|
|
* @param pmcProfile profile to add slots to
|
|
*/
|
|
protected addHideoutEliteSlots(pmcProfile: IPmcData): void {
|
|
const globals = this.databaseService.getGlobals();
|
|
|
|
const genSlots = pmcProfile.Hideout.Areas.find((x) => x.type === HideoutAreas.GENERATOR).slots.length;
|
|
const extraGenSlots = globals.config.SkillsSettings.HideoutManagement.EliteSlots.Generator.Slots;
|
|
|
|
if (genSlots < 6 + extraGenSlots) {
|
|
this.logger.debug("Updating generator area slots to a size of 6 + hideout management skill");
|
|
this.addEmptyObjectsToHideoutAreaSlots(HideoutAreas.GENERATOR, 6 + extraGenSlots, pmcProfile);
|
|
}
|
|
|
|
const waterCollSlots = pmcProfile.Hideout.Areas.find((x) => x.type === HideoutAreas.WATER_COLLECTOR).slots
|
|
.length;
|
|
const extraWaterCollSlots = globals.config.SkillsSettings.HideoutManagement.EliteSlots.WaterCollector.Slots;
|
|
|
|
if (waterCollSlots < 1 + extraWaterCollSlots) {
|
|
this.logger.debug("Updating water collector area slots to a size of 1 + hideout management skill");
|
|
this.addEmptyObjectsToHideoutAreaSlots(HideoutAreas.WATER_COLLECTOR, 1 + extraWaterCollSlots, pmcProfile);
|
|
}
|
|
|
|
const filterSlots = pmcProfile.Hideout.Areas.find((x) => x.type === HideoutAreas.AIR_FILTERING).slots.length;
|
|
const extraFilterSlots = globals.config.SkillsSettings.HideoutManagement.EliteSlots.AirFilteringUnit.Slots;
|
|
|
|
if (filterSlots < 3 + extraFilterSlots) {
|
|
this.logger.debug("Updating air filter area slots to a size of 3 + hideout management skill");
|
|
this.addEmptyObjectsToHideoutAreaSlots(HideoutAreas.AIR_FILTERING, 3 + extraFilterSlots, pmcProfile);
|
|
}
|
|
|
|
const btcFarmSlots = pmcProfile.Hideout.Areas.find((x) => x.type === HideoutAreas.BITCOIN_FARM).slots.length;
|
|
const extraBtcSlots = globals.config.SkillsSettings.HideoutManagement.EliteSlots.BitcoinFarm.Slots;
|
|
|
|
// BTC Farm doesnt have extra slots for hideout management, but we still check for modded stuff!!
|
|
if (btcFarmSlots < 50 + extraBtcSlots) {
|
|
this.logger.debug("Updating bitcoin farm area slots to a size of 50 + hideout management skill");
|
|
this.addEmptyObjectsToHideoutAreaSlots(HideoutAreas.BITCOIN_FARM, 50 + extraBtcSlots, pmcProfile);
|
|
}
|
|
|
|
const cultistAreaSlots = pmcProfile.Hideout.Areas.find((x) => x.type === HideoutAreas.CIRCLE_OF_CULTISTS).slots
|
|
.length;
|
|
if (cultistAreaSlots < 1) {
|
|
this.logger.debug("Updating cultist area slots to a size of 1");
|
|
this.addEmptyObjectsToHideoutAreaSlots(HideoutAreas.CIRCLE_OF_CULTISTS, 1, pmcProfile);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* add in objects equal to the number of slots
|
|
* @param areaType area to check
|
|
* @param pmcProfile profile to update
|
|
*/
|
|
protected addEmptyObjectsToHideoutAreaSlots(
|
|
areaType: HideoutAreas,
|
|
emptyItemCount: number,
|
|
pmcProfile: IPmcData,
|
|
): void {
|
|
const area = pmcProfile.Hideout.Areas.find((x) => x.type === areaType);
|
|
area.slots = this.addObjectsToArray(emptyItemCount, area.slots);
|
|
}
|
|
|
|
protected addObjectsToArray(count: number, slots: HideoutSlot[]): HideoutSlot[] {
|
|
for (let i = 0; i < count; i++) {
|
|
if (!slots.some((x) => x.locationIndex === i)) {
|
|
slots.push({ locationIndex: i });
|
|
}
|
|
}
|
|
|
|
return slots;
|
|
}
|
|
|
|
/**
|
|
* Check for and cap profile skills at 5100.
|
|
* @param pmcProfile profile to check and fix
|
|
*/
|
|
protected checkForSkillsOverMaxLevel(pmcProfile: IPmcData): void {
|
|
const skills = pmcProfile.Skills.Common;
|
|
|
|
for (const skill of skills) {
|
|
if (skill.Progress > 5100) {
|
|
skill.Progress = 5100;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks profile inventiory for items that do not exist inside the items db
|
|
* @param sessionId Session id
|
|
* @param pmcProfile Profile to check inventory of
|
|
*/
|
|
public checkForOrphanedModdedItems(sessionId: string, fullProfile: ISptProfile): void {
|
|
const itemsDb = this.databaseService.getItems();
|
|
const pmcProfile = fullProfile.characters.pmc;
|
|
|
|
// Get items placed in root of stash
|
|
// TODO: extend to other areas / sub items
|
|
const inventoryItemsToCheck = pmcProfile.Inventory.items.filter((item) =>
|
|
["hideout", "main"].includes(item.slotId ?? ""),
|
|
);
|
|
if (inventoryItemsToCheck) {
|
|
// Check each item in inventory to ensure item exists in itemdb
|
|
for (const item of inventoryItemsToCheck) {
|
|
if (!itemsDb[item._tpl]) {
|
|
this.logger.error(this.localisationService.getText("fixer-mod_item_found", item._tpl));
|
|
|
|
if (this.coreConfig.fixes.removeModItemsFromProfile) {
|
|
this.logger.success(
|
|
`Deleting item from inventory and insurance with id: ${item._id} tpl: ${item._tpl}`,
|
|
);
|
|
|
|
// Also deletes from insured array
|
|
this.inventoryHelper.removeItem(pmcProfile, item._id, sessionId);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (fullProfile.userbuilds) {
|
|
// 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 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) {
|
|
const dialog = fullProfile.dialogues[dialogId];
|
|
if (!dialog?.messages) {
|
|
continue; // Skip dialog with no messages
|
|
}
|
|
|
|
// Iterate over all messages in dialog
|
|
for (const [_, message] of Object.entries(dialog.messages)) {
|
|
if (!message.items?.data) {
|
|
continue; // Skip message with no items
|
|
}
|
|
|
|
// Fix message with no items but have the flags to indicate items to collect
|
|
if (message.items.data.length === 0 && message.hasRewards) {
|
|
message.hasRewards = false;
|
|
message.rewardCollected = true;
|
|
continue;
|
|
}
|
|
|
|
// Iterate over all items in message
|
|
for (const item of message.items.data) {
|
|
// Check item exists in itemsDb
|
|
if (!itemsDb[item._tpl]) {
|
|
this.logger.error(this.localisationService.getText("fixer-mod_item_found", item._tpl));
|
|
|
|
if (this.coreConfig.fixes.removeModItemsFromProfile) {
|
|
dialog.messages.splice(
|
|
dialog.messages.findIndex((x) => x._id === message._id),
|
|
1,
|
|
);
|
|
this.logger.warning(
|
|
`Item: ${item._tpl} has resulted in the deletion of message: ${message._id} from dialog ${dialogId}`,
|
|
);
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const clothing = this.databaseService.getTemplates().customization;
|
|
for (const [_, suitId] of Object.entries(fullProfile.suits)) {
|
|
if (!clothing[suitId]) {
|
|
this.logger.error(this.localisationService.getText("fixer-clothing_item_found", suitId));
|
|
if (this.coreConfig.fixes.removeModItemsFromProfile) {
|
|
fullProfile.suits.splice(fullProfile.suits.indexOf(suitId), 1);
|
|
this.logger.warning(`Non-default suit purchase: ${suitId} removed from profile`);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const repeatable of fullProfile.characters.pmc.RepeatableQuests ?? []) {
|
|
for (const [_, activeQuest] of Object.entries(repeatable.activeQuests ?? [])) {
|
|
if (!this.traderHelper.traderEnumHasValue(activeQuest.traderId)) {
|
|
this.logger.error(this.localisationService.getText("fixer-trader_found", activeQuest.traderId));
|
|
if (this.coreConfig.fixes.removeModItemsFromProfile) {
|
|
this.logger.warning(
|
|
`Non-default quest: ${activeQuest._id} from trader: ${activeQuest.traderId} removed from RepeatableQuests list in profile`,
|
|
);
|
|
repeatable.activeQuests.splice(
|
|
repeatable.activeQuests.findIndex((x) => x._id === activeQuest._id),
|
|
1,
|
|
);
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
for (const successReward of activeQuest.rewards.Success) {
|
|
if (successReward.type === "Item") {
|
|
for (const rewardItem of successReward.items) {
|
|
if (!itemsDb[rewardItem._tpl]) {
|
|
this.logger.error(
|
|
this.localisationService.getText("fixer-mod_item_found", rewardItem._tpl),
|
|
);
|
|
if (this.coreConfig.fixes.removeModItemsFromProfile) {
|
|
this.logger.warning(
|
|
`Non-default quest: ${activeQuest._id} from trader: ${activeQuest.traderId} removed from RepeatableQuests list in profile`,
|
|
);
|
|
repeatable.activeQuests.splice(
|
|
repeatable.activeQuests.findIndex((x) => x._id === activeQuest._id),
|
|
1,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const traderId in fullProfile.traderPurchases) {
|
|
if (!this.traderHelper.traderEnumHasValue(traderId)) {
|
|
this.logger.error(this.localisationService.getText("fixer-trader_found", traderId));
|
|
if (this.coreConfig.fixes.removeModItemsFromProfile) {
|
|
this.logger.warning(`Non-default trader: ${traderId} removed from traderPurchases list in profile`);
|
|
delete fullProfile.traderPurchases[traderId];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param buildType The type of build, used for logging only
|
|
* @param build The build to check for invalid items
|
|
* @param itemsDb The items database to use for item lookup
|
|
* @returns True if the build should be removed from the build list, false otherwise
|
|
*/
|
|
protected shouldRemoveWeaponEquipmentBuild(
|
|
buildType: string,
|
|
build: IWeaponBuild | IEquipmentBuild,
|
|
itemsDb: Record<string, ITemplateItem>,
|
|
): boolean {
|
|
for (const item of build.Items) {
|
|
// Check item exists in itemsDb
|
|
if (!itemsDb[item._tpl]) {
|
|
this.logger.error(this.localisationService.getText("fixer-mod_item_found", item._tpl));
|
|
|
|
if (this.coreConfig.fixes.removeModItemsFromProfile) {
|
|
this.logger.warning(
|
|
`Item: ${item._tpl} has resulted in the deletion of ${buildType} build: ${build.Name}`,
|
|
);
|
|
|
|
return true;
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param magazineBuild The magazine build to check for validity
|
|
* @param itemsDb The items database to use for item lookup
|
|
* @returns True if the build should be removed from the build list, false otherwise
|
|
*/
|
|
protected shouldRemoveMagazineBuild(
|
|
magazineBuild: IMagazineBuild,
|
|
itemsDb: Record<string, ITemplateItem>,
|
|
): boolean {
|
|
for (const item of magazineBuild.Items) {
|
|
// Magazine builds can have undefined items in them, skip those
|
|
if (!item) {
|
|
continue;
|
|
}
|
|
|
|
// Check item exists in itemsDb
|
|
if (!itemsDb[item.TemplateId]) {
|
|
this.logger.error(this.localisationService.getText("fixer-mod_item_found", item.TemplateId));
|
|
|
|
if (this.coreConfig.fixes.removeModItemsFromProfile) {
|
|
this.logger.warning(
|
|
`Item: ${item.TemplateId} has resulted in the deletion of magazine build: ${magazineBuild.Name}`,
|
|
);
|
|
|
|
return true;
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* REQUIRED for dev profiles
|
|
* Iterate over players hideout areas and find what's build, look for missing bonuses those areas give and add them if missing
|
|
* @param pmcProfile Profile to update
|
|
*/
|
|
public addMissingHideoutBonusesToProfile(pmcProfile: IPmcData): void {
|
|
const profileHideoutAreas = pmcProfile.Hideout.Areas;
|
|
const profileBonuses = pmcProfile.Bonuses;
|
|
const dbHideoutAreas = this.databaseService.getHideout().areas;
|
|
|
|
for (const profileArea of profileHideoutAreas) {
|
|
const areaType = profileArea.type;
|
|
const level = profileArea.level;
|
|
|
|
if (level === 0) {
|
|
continue;
|
|
}
|
|
|
|
// Get array of hideout area upgrade levels to check for bonuses
|
|
// Zero indexed
|
|
const areaLevelsToCheck: number[] = [];
|
|
for (let index = 0; index < level + 1; index++) {
|
|
areaLevelsToCheck.push(index);
|
|
}
|
|
|
|
// Iterate over area levels, check for bonuses, add if needed
|
|
const dbArea = dbHideoutAreas.find((x) => x.type === areaType);
|
|
if (!dbArea) {
|
|
continue;
|
|
}
|
|
|
|
for (const level of areaLevelsToCheck) {
|
|
// Get areas level bonuses from db
|
|
const levelBonuses = dbArea.stages[level]?.bonuses;
|
|
if (!levelBonuses || levelBonuses.length === 0) {
|
|
continue;
|
|
}
|
|
|
|
// Iterate over each bonus for the areas level
|
|
for (const bonus of levelBonuses) {
|
|
// Check if profile has bonus
|
|
const profileBonus = this.getBonusFromProfile(profileBonuses, bonus);
|
|
if (!profileBonus) {
|
|
// no bonus, add to profile
|
|
this.logger.debug(
|
|
`Profile has level ${level} area ${
|
|
HideoutAreas[profileArea.type]
|
|
} but no bonus found, adding ${bonus.type}`,
|
|
);
|
|
this.hideoutHelper.applyPlayerUpgradesBonuses(pmcProfile, bonus);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param profileBonuses bonuses from profile
|
|
* @param bonus bonus to find
|
|
* @returns matching bonus
|
|
*/
|
|
protected getBonusFromProfile(profileBonuses: Bonus[], bonus: StageBonus): Bonus | undefined {
|
|
// match by id first, used by "TextBonus" bonuses
|
|
if (bonus.id) {
|
|
return profileBonuses.find((x) => x.id === bonus.id);
|
|
}
|
|
|
|
if (bonus.type === BonusType.STASH_SIZE) {
|
|
return profileBonuses.find((x) => x.type === bonus.type && x.templateId === bonus.templateId);
|
|
}
|
|
|
|
if (bonus.type === BonusType.ADDITIONAL_SLOTS) {
|
|
return profileBonuses.find(
|
|
(x) => x.type === bonus.type && x.value === bonus.value && x.visible === bonus.visible,
|
|
);
|
|
}
|
|
|
|
return profileBonuses.find((x) => x.type === bonus.type && x.value === bonus.value);
|
|
}
|
|
}
|