ProfileFixerService Refactor (!391)

Refactor to remove legacy code that bloats the `ProfileFixerService` class. Most of which is old profile porting code, some being old profile fixes. Organizes code in the class so that public members are at the top (as they should be). Finally break out some code into their own methods so they're not in the primary method for the class `checkForAndFixPmcProfileIssues`.

I have tested this with a developer profile, an EOD profile and a Standard profile. I've encountered no issues in my own testing.

Co-authored-by: Cj <161484149+CJ-SPT@users.noreply.github.com>
Reviewed-on: https://dev.sp-tarkov.com/SPT/Server/pulls/391
Co-authored-by: Cj <cj@noreply.dev.sp-tarkov.com>
Co-committed-by: Cj <cj@noreply.dev.sp-tarkov.com>
This commit is contained in:
Cj 2024-08-08 17:09:52 +00:00 committed by chomp
parent 2fbcee22bd
commit 967dc15564
3 changed files with 145 additions and 867 deletions

View File

@ -180,16 +180,7 @@ export class GameController {
this.splitBotWavesIntoSingleWaves();
}
this.profileFixerService.removeLegacyScavCaseProductionCrafts(pmcProfile);
this.profileFixerService.addMissingHideoutAreasToProfile(fullProfile);
if (pmcProfile.Inventory) {
// MUST occur prior to `profileFixerService.checkForAndFixPmcProfileIssues()`
this.profileFixerService.fixIncorrectAidValue(fullProfile);
this.profileFixerService.migrateStatsToNewStructure(fullProfile);
this.sendPraporGiftsToNewProfiles(pmcProfile);
this.profileFixerService.checkForOrphanedModdedItems(sessionID, fullProfile);
@ -197,15 +188,9 @@ export class GameController {
this.profileFixerService.checkForAndFixPmcProfileIssues(pmcProfile);
this.profileFixerService.addMissingSptVersionTagToProfile(fullProfile);
if (pmcProfile.Hideout) {
this.profileFixerService.addMissingHideoutBonusesToProfile(pmcProfile);
this.profileFixerService.addMissingUpgradesPropertyToHideout(pmcProfile);
this.hideoutHelper.setHideoutImprovementsToCompleted(pmcProfile);
this.hideoutHelper.unlockHideoutWallInProfile(pmcProfile);
this.profileFixerService.addMissingIdsToBonuses(pmcProfile);
this.profileFixerService.fixBitcoinProductionTime(pmcProfile);
}
this.logProfileDetails(fullProfile);

View File

@ -186,7 +186,6 @@ export class ProfileController {
};
this.profileFixerService.checkForAndFixPmcProfileIssues(profileDetails.characters.pmc);
this.profileFixerService.addMissingHideoutBonusesToProfile(profileDetails.characters.pmc);
this.saveServer.addProfile(profileDetails);

View File

@ -4,16 +4,12 @@ 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, IHideoutImprovement, IQuestStatus } from "@spt/models/eft/common/tables/IBotBase";
import { 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 { QuestStatus } from "@spt/models/enums/QuestStatus";
import { Traders } from "@spt/models/enums/Traders";
import { ICoreConfig } from "@spt/models/spt/config/ICoreConfig";
import { IRagfairConfig } from "@spt/models/spt/config/IRagfairConfig";
import { ILogger } from "@spt/models/spt/utils/ILogger";
@ -59,315 +55,121 @@ export class ProfileFixerService {
public checkForAndFixPmcProfileIssues(pmcProfile: IPmcData): void {
this.removeDanglingConditionCounters(pmcProfile);
this.removeDanglingTaskConditionCounters(pmcProfile);
this.addMissingRepeatableQuestsProperty(pmcProfile);
this.addLighthouseKeeperIfMissing(pmcProfile);
this.addUnlockedInfoObjectIfMissing(pmcProfile);
this.removeOrphanedQuests(pmcProfile);
if (pmcProfile.Inventory) {
this.addHideoutAreaStashes(pmcProfile);
}
if (pmcProfile.Hideout) {
const globals = this.databaseService.getGlobals();
this.migrateImprovements(pmcProfile);
this.addMissingBonusesProperty(pmcProfile);
this.addMissingWallImprovements(pmcProfile);
this.addMissingHideoutWallAreas(pmcProfile);
this.addMissingGunStandContainerImprovements(pmcProfile);
this.addMissingHallOfFameContainerImprovements(pmcProfile);
this.ensureGunStandLevelsMatch(pmcProfile);
this.removeResourcesFromSlotsInHideoutWithoutLocationIndexValue(pmcProfile);
this.reorderHideoutAreasWithResouceInputs(pmcProfile);
if (
pmcProfile.Hideout.Areas.find((x) => x.type === HideoutAreas.GENERATOR).slots.length <
6 + globals.config.SkillsSettings.HideoutManagement.EliteSlots.Generator.Slots
) {
this.logger.debug("Updating generator area slots to a size of 6 + hideout management skill");
this.addEmptyObjectsToHideoutAreaSlots(
HideoutAreas.GENERATOR,
6 + globals.config.SkillsSettings.HideoutManagement.EliteSlots.Generator.Slots,
pmcProfile,
);
}
if (
pmcProfile.Hideout.Areas.find((x) => x.type === HideoutAreas.WATER_COLLECTOR).slots.length <
1 + globals.config.SkillsSettings.HideoutManagement.EliteSlots.WaterCollector.Slots
) {
this.logger.debug("Updating water collector area slots to a size of 1 + hideout management skill");
this.addEmptyObjectsToHideoutAreaSlots(
HideoutAreas.WATER_COLLECTOR,
1 + globals.config.SkillsSettings.HideoutManagement.EliteSlots.WaterCollector.Slots,
pmcProfile,
);
}
if (
pmcProfile.Hideout.Areas.find((x) => x.type === HideoutAreas.AIR_FILTERING).slots.length <
3 + globals.config.SkillsSettings.HideoutManagement.EliteSlots.AirFilteringUnit.Slots
) {
this.logger.debug("Updating air filter area slots to a size of 3 + hideout management skill");
this.addEmptyObjectsToHideoutAreaSlots(
HideoutAreas.AIR_FILTERING,
3 + globals.config.SkillsSettings.HideoutManagement.EliteSlots.AirFilteringUnit.Slots,
pmcProfile,
);
}
// BTC Farm doesnt have extra slots for hideout management, but we still check for modded stuff!!
if (
pmcProfile.Hideout.Areas.find((x) => x.type === HideoutAreas.BITCOIN_FARM).slots.length <
50 + globals.config.SkillsSettings.HideoutManagement.EliteSlots.BitcoinFarm.Slots
) {
this.logger.debug("Updating bitcoin farm area slots to a size of 50 + hideout management skill");
this.addEmptyObjectsToHideoutAreaSlots(
HideoutAreas.BITCOIN_FARM,
50 + globals.config.SkillsSettings.HideoutManagement.EliteSlots.BitcoinFarm.Slots,
pmcProfile,
);
}
this.addHideoutEliteSlots(pmcProfile);
}
if (pmcProfile.Skills) {
this.checkForSkillsOverMaxLevel(pmcProfile);
}
this.fixNullTraderSalesSums(pmcProfile);
this.fixNullTraderNextResupply(pmcProfile);
this.updateProfileQuestDataValues(pmcProfile);
}
/**
* Find issues in the scav profile data that may cause issues
* @param scavProfile profile to check and fix
*/
public checkForAndFixScavProfileIssues(scavProfile: IPmcData): void {
this.updateProfileQuestDataValues(scavProfile);
}
public checkForAndFixScavProfileIssues(scavProfile: IPmcData): void {}
/**
* Check for and cap profile skills at 5100.
* @param pmcProfile profile to check and fix
* Attempt to fix common item issues that corrupt profiles
* @param pmcProfile Profile to check items of
*/
protected checkForSkillsOverMaxLevel(pmcProfile: IPmcData): void {
const skills = pmcProfile.Skills.Common;
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);
for (const skill of skills) {
if (skill.Progress > 5100) {
skill.Progress = 5100;
return acc;
}, {});
for (const key in itemMapping) {
// Only one item for this id, not a dupe
if (itemMapping[key].length === 1) {
continue;
}
}
}
protected addMissingGunStandContainerImprovements(pmcProfile: IPmcData): void {
const weaponStandArea = pmcProfile.Hideout.Areas.find((x) => x.type === HideoutAreas.WEAPON_STAND);
if (!weaponStandArea || weaponStandArea.level === 0) {
// No stand in profile or its level 0, skip
return;
}
const hideout = this.databaseService.getHideout();
const hideoutStandAreaDb = hideout.areas.find((area) => area.type === HideoutAreas.WEAPON_STAND);
const hideoutStandSecondaryAreaDb = hideout.areas.find((x) => x.parentArea === hideoutStandAreaDb._id);
const stageCurrentAt = hideoutStandAreaDb.stages[weaponStandArea.level];
const hideoutStandStashId = pmcProfile.Inventory.hideoutAreaStashes[HideoutAreas.WEAPON_STAND];
const hideoutSecondaryStashId = pmcProfile.Inventory.hideoutAreaStashes[HideoutAreas.WEAPON_STAND_SECONDARY];
// `hideoutAreaStashes` empty but profile has built gun stand
if (!hideoutStandStashId && stageCurrentAt) {
// Value is missing, add it
pmcProfile.Inventory.hideoutAreaStashes[HideoutAreas.WEAPON_STAND] = hideoutStandAreaDb._id;
pmcProfile.Inventory.hideoutAreaStashes[HideoutAreas.WEAPON_STAND_SECONDARY] =
hideoutStandSecondaryAreaDb._id;
// Add stash item to profile
const gunStandStashItem = pmcProfile.Inventory.items.find((item) => item._id === hideoutStandAreaDb._id);
if (gunStandStashItem) {
gunStandStashItem._tpl = stageCurrentAt.container;
this.logger.debug(
`Updated existing gun stand inventory stash: ${gunStandStashItem._id} tpl to ${stageCurrentAt.container}`,
);
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 {
pmcProfile.Inventory.items.push({ _id: hideoutStandAreaDb._id, _tpl: stageCurrentAt.container });
this.logger.debug(
`Added missing gun stand inventory stash: ${hideoutStandAreaDb._id} tpl to ${stageCurrentAt.container}`,
);
}
// Add secondary stash item to profile
const gunStandStashSecondaryItem = pmcProfile.Inventory.items.find(
(item) => item._id === hideoutStandSecondaryAreaDb._id,
);
if (gunStandStashItem) {
gunStandStashSecondaryItem._tpl = stageCurrentAt.container;
this.logger.debug(
`Updated gun stand existing inventory secondary stash: ${gunStandStashSecondaryItem._id} tpl to ${stageCurrentAt.container}`,
);
} else {
pmcProfile.Inventory.items.push({
_id: hideoutStandSecondaryAreaDb._id,
_tpl: stageCurrentAt.container,
});
this.logger.debug(
`Added missing gun stand inventory secondary stash: ${hideoutStandSecondaryAreaDb._id} tpl to ${stageCurrentAt.container}`,
);
}
return;
}
let stashItem = pmcProfile.Inventory.items?.find((x) => x._id === hideoutStandAreaDb._id);
if (!stashItem) {
// Stand inventory stash item doesnt exist, add it
pmcProfile.Inventory.items.push({ _id: hideoutStandAreaDb._id, _tpl: stageCurrentAt.container });
stashItem = pmcProfile.Inventory.items?.find((x) => x._id === hideoutStandAreaDb._id);
}
// `hideoutAreaStashes` has value related stash inventory items tpl doesnt match what's expected
if (hideoutStandStashId && stashItem._tpl !== stageCurrentAt.container) {
this.logger.debug(
`primary Stash tpl was: ${stashItem._tpl}, but should be ${stageCurrentAt.container}, updating`,
);
// The id inside the profile does not match what the hideout db value is, out of sync, adjust
stashItem._tpl = stageCurrentAt.container;
}
let stashSecondaryItem = pmcProfile.Inventory.items?.find((x) => x._id === hideoutStandSecondaryAreaDb._id);
if (!stashSecondaryItem) {
// Stand inventory stash item doesnt exist, add it
pmcProfile.Inventory.items.push({ _id: hideoutStandSecondaryAreaDb._id, _tpl: stageCurrentAt.container });
stashSecondaryItem = pmcProfile.Inventory.items?.find((x) => x._id === hideoutStandSecondaryAreaDb._id);
}
// `hideoutAreaStashes` has value related stash inventory items tpl doesnt match what's expected
if (hideoutSecondaryStashId && stashSecondaryItem?._tpl !== stageCurrentAt.container) {
this.logger.debug(
`Secondary stash tpl was: ${stashSecondaryItem?._tpl}, but should be ${stageCurrentAt.container}, updating`,
);
// The id inside the profile does not match what the hideout db value is, out of sync, adjust
stashSecondaryItem._tpl = stageCurrentAt.container;
}
}
protected addMissingHallOfFameContainerImprovements(pmcProfile: IPmcData): void {
const placeOfFameArea = pmcProfile.Hideout.Areas.find((x) => x.type === HideoutAreas.PLACE_OF_FAME);
if (!placeOfFameArea || placeOfFameArea.level === 0) {
// No place of fame in profile or its level 0, skip
return;
}
const placeOfFameAreaDb = this.databaseService
.getHideout()
.areas.find((area) => area.type === HideoutAreas.PLACE_OF_FAME);
if (!placeOfFameAreaDb) {
return;
}
const stageCurrentlyAt = placeOfFameAreaDb.stages[placeOfFameArea.level];
const placeOfFameStashId = pmcProfile.Inventory.hideoutAreaStashes[HideoutAreas.PLACE_OF_FAME];
// `hideoutAreaStashes` empty but profile has built gun stand
if (!placeOfFameStashId && stageCurrentlyAt) {
// Value is missing, add it
pmcProfile.Inventory.hideoutAreaStashes[HideoutAreas.PLACE_OF_FAME] = placeOfFameAreaDb._id;
// Add stash item to profile
const placeOfFameStashItem = pmcProfile.Inventory.items.find((item) => item._id === placeOfFameAreaDb._id);
if (placeOfFameStashItem) {
placeOfFameStashItem._tpl = stageCurrentlyAt.container;
this.logger.debug(
`Updated existing place of fame inventory stash: ${placeOfFameStashItem._id} tpl to ${stageCurrentlyAt.container}`,
);
} else {
pmcProfile.Inventory.items.push({ _id: placeOfFameAreaDb._id, _tpl: stageCurrentlyAt.container });
this.logger.debug(
`Added missing place of fame inventory stash: ${placeOfFameAreaDb._id} tpl to ${stageCurrentlyAt.container}`,
);
}
return;
}
let stashItem = pmcProfile.Inventory.items?.find((x) => x._id === placeOfFameAreaDb._id);
if (!stashItem) {
// Stand inventory stash item doesnt exist, add it
pmcProfile.Inventory.items.push({ _id: placeOfFameAreaDb._id, _tpl: stageCurrentlyAt.container });
stashItem = pmcProfile.Inventory.items?.find((x) => x._id === placeOfFameAreaDb._id);
}
// `hideoutAreaStashes` has value related stash inventory items tpl doesnt match what's expected
if (placeOfFameStashId && stashItem._tpl !== stageCurrentlyAt.container) {
this.logger.debug(
`primary Stash tpl was: ${stashItem?._tpl}, but should be ${stageCurrentlyAt.container}, updating`,
);
// The id inside the profile does not match what the hideout db value is, out of sync, adjust
stashItem._tpl = stageCurrentlyAt.container;
}
}
protected ensureGunStandLevelsMatch(pmcProfile: IPmcData): void {
// only proceed if stand is level 1 or above
const gunStandParent = pmcProfile.Hideout.Areas.find((x) => x.type === HideoutAreas.WEAPON_STAND);
if (gunStandParent && gunStandParent.level > 0) {
const gunStandChild = pmcProfile.Hideout.Areas.find((x) => x.type === HideoutAreas.WEAPON_STAND_SECONDARY);
if (gunStandChild && gunStandParent.level !== gunStandChild.level) {
this.logger.success("Upgraded gun stand levels to match");
gunStandChild.level = gunStandParent.level;
// 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}`);
}
}
}
}
protected addHideoutAreaStashes(pmcProfile: IPmcData): void {
if (!pmcProfile?.Inventory?.hideoutAreaStashes) {
this.logger.debug("Added missing hideoutAreaStashes to inventory");
pmcProfile.Inventory.hideoutAreaStashes = {};
}
}
// 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;
}
protected addMissingHideoutWallAreas(pmcProfile: IPmcData): void {
if (!pmcProfile.Hideout.Areas.some((x) => x.type === HideoutAreas.WEAPON_STAND)) {
pmcProfile.Hideout.Areas.push({
type: 24,
level: 0,
active: true,
passiveBonusesEnabled: true,
completeTime: 0,
constructing: false,
slots: [],
lastRecipe: "",
});
// 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;
}
}
if (!pmcProfile.Hideout.Areas.some((x) => x.type === HideoutAreas.WEAPON_STAND_SECONDARY)) {
pmcProfile.Hideout.Areas.push({
type: 25,
level: 0,
active: true,
passiveBonusesEnabled: true,
completeTime: 0,
constructing: false,
slots: [],
lastRecipe: "",
});
}
}
// Iterate over clothing
const customizationDb = this.databaseService.getTemplates().customization;
const customizationDbArray = Object.values(customizationDb);
const playerIsUsec = pmcProfile.Info.Side.toLowerCase() === "usec";
/**
* Add tag to profile to indicate when it was made
* @param fullProfile
*/
public addMissingSptVersionTagToProfile(fullProfile: ISptProfile): void {
if (!fullProfile.spt) {
this.logger.debug("Adding spt object to profile");
fullProfile.spt = {
version: this.watermark.getVersionTag(),
receivedGifts: [],
freeRepeatableRefreshUsedCount: {},
};
// 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;
}
}
@ -377,39 +179,15 @@ export class ProfileFixerService {
* @param pmcProfile profile to remove old counters from
*/
public removeDanglingConditionCounters(pmcProfile: IPmcData): void {
if (pmcProfile.TaskConditionCounters) {
for (const counterId in pmcProfile.TaskConditionCounters) {
const counter = pmcProfile.TaskConditionCounters[counterId];
if (!counter.sourceId) {
delete pmcProfile.TaskConditionCounters[counterId];
}
}
}
}
public addLighthouseKeeperIfMissing(pmcProfile: IPmcData): void {
if (!pmcProfile.TradersInfo) {
if (!pmcProfile.TaskConditionCounters) {
return;
}
// only add if other traders exist, means this is pre-patch 13 profile
if (!pmcProfile.TradersInfo[Traders.LIGHTHOUSEKEEPER] && Object.keys(pmcProfile.TradersInfo).length > 0) {
this.logger.warning("Added missing Lighthouse keeper trader to pmc profile");
pmcProfile.TradersInfo[Traders.LIGHTHOUSEKEEPER] = {
unlocked: false,
disabled: false,
salesSum: 0,
standing: 0.2,
loyaltyLevel: 1,
nextResupply: this.timeUtil.getTimestamp() + 3600, // now + 1 hour
};
}
}
protected addUnlockedInfoObjectIfMissing(pmcProfile: IPmcData): void {
if (!pmcProfile.UnlockedInfo) {
this.logger.debug("Adding UnlockedInfo object to profile");
pmcProfile.UnlockedInfo = { unlockedProductionRecipe: [] };
for (const counterId in pmcProfile.TaskConditionCounters) {
const counter = pmcProfile.TaskConditionCounters[counterId];
if (!counter.sourceId) {
delete pmcProfile.TaskConditionCounters[counterId];
}
}
}
@ -463,195 +241,67 @@ export class ProfileFixerService {
return activeQuests;
}
protected fixNullTraderSalesSums(pmcProfile: IPmcData): void {
for (const traderId in pmcProfile.TradersInfo) {
const trader = pmcProfile.TradersInfo[traderId];
if (trader?.salesSum === undefined) {
this.logger.warning(`trader ${traderId} has a undefined salesSum value, resetting to 0`);
trader.salesSum = 0;
}
}
}
protected addMissingBonusesProperty(pmcProfile: IPmcData): void {
if (typeof pmcProfile.Bonuses === "undefined") {
pmcProfile.Bonuses = [];
this.logger.debug("Missing Bonuses property added to profile");
}
}
/**
* Adjust profile quest status and statusTimers object values
* quest.status is numeric e.g. 2
* quest.statusTimers keys are numeric as strings e.g. "2"
* @param profile profile to update
* After removing mods that add quests, the quest panel will break without removing these
* @param pmcProfile Profile to remove dead quests from
*/
protected updateProfileQuestDataValues(profile: IPmcData): void {
if (!profile.Quests) {
return;
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);
}
const fixes: Record<any, number> = {};
const timerFixes: Record<string, number> = {};
const questsToDelete: IQuestStatus[] = [];
const fullProfile = this.profileHelper.getFullProfile(profile.sessionId);
const isDevProfile = fullProfile?.info.edition.toLowerCase() === "spt developer";
for (const quest of profile.Quests) {
// Old profiles had quests with a bad status of 0 (invalid) added to profile, remove them
// E.g. compensation for damage showing before standing check was added to getClientQuests()
if (quest.status === 0 && quest.availableAfter === 0 && !isDevProfile) {
questsToDelete.push(quest);
continue;
}
if (quest.status && Number.isNaN(Number.parseInt(<string>(<unknown>quest.status)))) {
fixes[quest.status] = (fixes[quest.status] ?? 0) + 1;
const newQuestStatus = QuestStatus[quest.status];
quest.status = <QuestStatus>(<unknown>newQuestStatus);
}
for (const statusTimer in quest.statusTimers) {
if (Number.isNaN(Number.parseInt(statusTimer))) {
timerFixes[statusTimer] = (timerFixes[statusTimer] ?? 0) + 1;
const newKey = QuestStatus[statusTimer];
quest.statusTimers[newKey] = quest.statusTimers[statusTimer];
delete quest.statusTimers[statusTimer];
}
}
}
for (const questToDelete of questsToDelete) {
profile.Quests.splice(profile.Quests.indexOf(questToDelete), 1);
}
if (Object.keys(fixes).length > 0) {
this.logger.debug(
`Updated quests values: ${Object.entries(fixes)
.map(([k, v]) => `(${k}: ${v} times)`)
.join(", ")}`,
);
}
if (Object.keys(timerFixes).length > 0) {
this.logger.debug(
`Updated statusTimers values: ${Object.entries(timerFixes)
.map(([k, v]) => `(${k}: ${v} times)`)
.join(", ")}`,
);
}
}
protected addMissingRepeatableQuestsProperty(pmcProfile: IPmcData): void {
if (pmcProfile.RepeatableQuests) {
let repeatablesCompatible = true;
for (const currentRepeatable of pmcProfile.RepeatableQuests) {
if (
!(
currentRepeatable.changeRequirement &&
currentRepeatable.activeQuests.every(
(x) => typeof x.changeCost !== "undefined" && typeof x.changeStandingCost !== "undefined",
)
)
) {
repeatablesCompatible = false;
break;
}
}
if (!repeatablesCompatible) {
pmcProfile.RepeatableQuests = [];
this.logger.debug("Missing RepeatableQuests property added to profile");
}
} else {
pmcProfile.RepeatableQuests = [];
}
}
/**
* Some profiles have hideout maxed and therefore no improvements
* @param pmcProfile Profile to add improvement data to
*/
protected addMissingWallImprovements(pmcProfile: IPmcData): void {
const profileWallArea = pmcProfile.Hideout.Areas.find((x) => x.type === HideoutAreas.EMERGENCY_WALL);
const wallDb = this.databaseService.getHideout().areas.find((x) => x.type === HideoutAreas.EMERGENCY_WALL);
if (profileWallArea.level > 0) {
for (let i = 0; i < profileWallArea.level; i++) {
// Get wall stage from db
const wallStageDb = wallDb.stages[i];
if (wallStageDb.improvements.length === 0) {
// No improvements, skip
continue;
}
for (const improvement of wallStageDb.improvements) {
// Don't overwrite existing improvement
if (pmcProfile.Hideout.Improvement[improvement.id]) {
continue;
}
pmcProfile.Hideout.Improvement[improvement.id] = {
completed: true,
improveCompleteTimestamp: this.timeUtil.getTimestamp() + i, // add some variability
};
this.logger.debug(`Added wall improvement ${improvement.id} to profile`);
}
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");
}
}
}
/**
* A new property was added to slot items "locationIndex", if this is missing, the hideout slot item must be removed
* @param pmcProfile Profile to find and remove slots from
* 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 removeResourcesFromSlotsInHideoutWithoutLocationIndexValue(pmcProfile: IPmcData): void {
for (const area of pmcProfile.Hideout.Areas) {
// Skip areas with no resource slots
if (area.slots.length === 0) {
continue;
}
protected addHideoutEliteSlots(pmcProfile: IPmcData): void {
const globals = this.databaseService.getGlobals();
// Only slots with location index
area.slots = area.slots.filter((x) => "locationIndex" in x);
const genSlots = pmcProfile.Hideout.Areas.find((x) => x.type === HideoutAreas.GENERATOR).slots.length;
const extraGenSlots = globals.config.SkillsSettings.HideoutManagement.EliteSlots.Generator.Slots;
// Only slots that:
// Have an item property and it has at least one item in it
// Or
// Have no item property
area.slots = area.slots.filter((x) => ("item" in x && (x.item?.length ?? 0) > 0) || !("item" in x));
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);
}
}
/**
* Hideout slots need to be in a specific order, locationIndex in ascending order
* @param pmcProfile profile to edit
*/
protected reorderHideoutAreasWithResouceInputs(pmcProfile: IPmcData): void {
const areasToCheck = [
HideoutAreas.AIR_FILTERING,
HideoutAreas.GENERATOR,
HideoutAreas.BITCOIN_FARM,
HideoutAreas.WATER_COLLECTOR,
];
const waterCollSlots = pmcProfile.Hideout.Areas.find((x) => x.type === HideoutAreas.WATER_COLLECTOR).slots
.length;
const extraWaterCollSlots = globals.config.SkillsSettings.HideoutManagement.EliteSlots.WaterCollector.Slots;
for (const areaId of areasToCheck) {
const area = pmcProfile.Hideout.Areas.find((area) => area.type === areaId);
if (!area) {
this.logger.debug(`unable to sort: ${area.type} (${areaId}) slots, no area found`);
continue;
}
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);
}
if (!area.slots || area.slots.length === 0) {
this.logger.debug(`unable to sort ${areaId} slots, no slots found`);
continue;
}
const filterSlots = pmcProfile.Hideout.Areas.find((x) => x.type === HideoutAreas.AIR_FILTERING).slots.length;
const extraFilterSlots = globals.config.SkillsSettings.HideoutManagement.EliteSlots.AirFilteringUnit.Slots;
area.slots = area.slots.sort((a, b) => {
return a.locationIndex > b.locationIndex ? 1 : -1;
});
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);
}
}
@ -680,84 +330,19 @@ export class ProfileFixerService {
}
/**
* 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
* Check for and cap profile skills at 5100.
* @param pmcProfile profile to check and fix
*/
public addMissingHideoutBonusesToProfile(pmcProfile: IPmcData): void {
const profileHideoutAreas = pmcProfile.Hideout.Areas;
const profileBonuses = pmcProfile.Bonuses;
const dbHideoutAreas = this.databaseService.getHideout().areas;
protected checkForSkillsOverMaxLevel(pmcProfile: IPmcData): void {
const skills = pmcProfile.Skills.Common;
for (const area of profileHideoutAreas) {
const areaType = area.type;
const level = area.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[area.type]
} but no bonus found, adding ${bonus.type}`,
);
this.hideoutHelper.applyPlayerUpgradesBonuses(pmcProfile, bonus);
}
}
for (const skill of skills) {
if (skill.Progress > 5100) {
skill.Progress = 5100;
}
}
}
/**
* @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);
}
/**
* Checks profile inventiory for items that do not exist inside the items db
* @param sessionId Session id
@ -976,295 +561,4 @@ export class ProfileFixerService {
return false;
}
/**
* 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;
}
}
/**
* Add `Improvements` object to hideout if missing - added in eft 13.0.21469
* @param pmcProfile profile to update
*/
public addMissingUpgradesPropertyToHideout(pmcProfile: IPmcData): void {
if (!pmcProfile.Hideout.Improvement) {
pmcProfile.Hideout.Improvement = {};
}
}
/**
* Iterate over associated profile template and check all hideout areas exist, add if not
* @param fullProfile Profile to update
*/
public addMissingHideoutAreasToProfile(fullProfile: ISptProfile): void {
const pmcProfile = fullProfile.characters.pmc;
// No profile, probably new account being created
if (!pmcProfile?.Hideout) {
return;
}
const profileTemplates = this.databaseService.getTemplates().profiles[fullProfile.info.edition];
if (!profileTemplates) {
return;
}
const profileTemplate = profileTemplates[pmcProfile.Info.Side.toLowerCase()];
if (!profileTemplate) {
return;
}
// Get all areas from templates/profiles.json
for (const area of profileTemplate.character.Hideout.Areas) {
if (!pmcProfile.Hideout.Areas.some((x) => x.type === area.type)) {
pmcProfile.Hideout.Areas.push(area);
this.logger.debug(`Added missing hideout area ${area.type} to profile`);
}
}
}
/**
* These used to be used for storing scav case rewards, rewards are now generated on pickup
* @param pmcProfile Profile to update
*/
public removeLegacyScavCaseProductionCrafts(pmcProfile: IPmcData): void {
for (const prodKey in pmcProfile.Hideout?.Production) {
if (prodKey.startsWith("ScavCase")) {
delete pmcProfile.Hideout.Production[prodKey];
}
}
}
/**
* 3.7.0 moved AIDs to be numeric, old profiles need to be migrated
* We store the old AID value in new field `sessionId`
* @param fullProfile Profile to update
*/
public fixIncorrectAidValue(fullProfile: ISptProfile): void {
// Not a number, regenerate
// biome-ignore lint/suspicious/noGlobalIsNan: <value can be a valid string, Number.IsNaN() would ignore it>
if (isNaN(fullProfile.characters.pmc.aid) || !fullProfile.info.aid) {
fullProfile.characters.pmc.sessionId = <string>(<unknown>fullProfile.characters.pmc.aid);
fullProfile.characters.pmc.aid = this.hashUtil.generateAccountId();
fullProfile.characters.scav.sessionId = <string>(<unknown>fullProfile.characters.pmc.sessionId);
fullProfile.characters.scav.aid = fullProfile.characters.pmc.aid;
fullProfile.info.aid = fullProfile.characters.pmc.aid;
this.logger.info(
`Migrated AccountId from: ${fullProfile.characters.pmc.sessionId} to: ${fullProfile.characters.pmc.aid}`,
);
}
}
/**
* Bsg nested `stats` into a sub object called 'eft'
* @param fullProfile Profile to check for and migrate stats data
*/
public migrateStatsToNewStructure(fullProfile: ISptProfile): void {
// Data is in old structure, migrate
if ("OverallCounters" in fullProfile.characters.pmc.Stats) {
this.logger.debug("Migrating stats object into new structure");
const statsCopy = this.cloner.clone(fullProfile.characters.pmc.Stats);
// Clear stats object
delete fullProfile.characters.pmc.Stats.Eft;
fullProfile.characters.pmc.Stats.Eft = <any>(<unknown>statsCopy);
}
}
/**
* 26126 (7th August) requires bonuses to have an ID, these were not included in the default profile presets
* @param pmcProfile Profile to add missing IDs to
*/
public addMissingIdsToBonuses(pmcProfile: IPmcData): void {
let foundBonus = false;
for (const bonus of pmcProfile.Bonuses) {
if (bonus.id) {
// Exists already, skip
continue;
}
// Bonus lacks id, find matching hideout area / stage / bonus
for (const area of this.databaseService.getHideout().areas) {
// TODO: skip if no stages
for (const stageIndex in area.stages) {
const stageInfo = area.stages[stageIndex];
const matchingBonus = stageInfo.bonuses.find(
(x) => x.templateId === bonus.templateId && x.type === bonus.type,
);
if (matchingBonus) {
// Add id to bonus, flag bonus as found and exit stage loop
bonus.id = matchingBonus.id;
this.logger.debug(`Added missing Id: ${bonus.id} to bonus: ${bonus.type}`);
foundBonus = true;
break;
}
}
// We've found the bonus we're after, break out of area loop
if (foundBonus) {
foundBonus = false;
break;
}
}
}
}
/**
* 3.8.0 utilized the wrong ProductionTime for bitcoin, fix it if it's found
*/
public fixBitcoinProductionTime(pmcProfile: IPmcData): void {
const btcProd = pmcProfile.Hideout?.Production[HideoutHelper.bitcoinFarm];
if (btcProd) {
btcProd.ProductionTime = this.hideoutHelper.getAdjustedCraftTimeWithSkills(
pmcProfile,
HideoutHelper.bitcoinProductionId,
);
}
}
/**
* At some point the property name was changed,migrate data across to new name
* @param pmcProfile Profile to migrate improvements in
*/
protected migrateImprovements(pmcProfile: IPmcData): void {
if ("Improvements" in pmcProfile.Hideout) {
const improvements = pmcProfile.Hideout.Improvements as Record<string, IHideoutImprovement>;
pmcProfile.Hideout.Improvement = this.cloner.clone(improvements);
delete pmcProfile.Hideout.Improvements;
this.logger.success("Successfully migrated hideout Improvements data to new location, deleted old data");
}
}
/**
* 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 someone has run a mod from pre-3.8.0, it results in an invalid `nextResupply` value
* Resolve this by setting the nextResupply to 0 if it's undefined
*/
protected fixNullTraderNextResupply(pmcProfile: IPmcData): void {
for (const [traderId, trader] of Object.entries(pmcProfile.TradersInfo)) {
if (trader?.nextResupply === undefined) {
this.logger.warning(`trader ${traderId} has a undefined nextResupply value, resetting to 0`);
trader.nextResupply = 0;
}
}
}
}