From 8b1a3158cb9fc3dab0690c8d5d5c43564b3c1ffd Mon Sep 17 00:00:00 2001 From: DrakiaXYZ <565558+TheDgtl@users.noreply.github.com> Date: Sat, 30 Nov 2024 10:08:51 -0800 Subject: [PATCH] Add quest production unlocks to the PMC Profile fixer service --- project/src/helpers/QuestHelper.ts | 45 ++++++++----- project/src/services/ProfileFixerService.ts | 72 +++++++++++++++++++++ 2 files changed, 102 insertions(+), 15 deletions(-) diff --git a/project/src/helpers/QuestHelper.ts b/project/src/helpers/QuestHelper.ts index 3a6adf9d..c1292ba1 100644 --- a/project/src/helpers/QuestHelper.ts +++ b/project/src/helpers/QuestHelper.ts @@ -10,6 +10,7 @@ import { IPmcData } from "@spt/models/eft/common/IPmcData"; import { Common, IQuestStatus } from "@spt/models/eft/common/tables/IBotBase"; import { IItem } from "@spt/models/eft/common/tables/IItem"; import { IQuest, IQuestCondition, IQuestReward } from "@spt/models/eft/common/tables/IQuest"; +import { IHideoutProduction } from "@spt/models/eft/hideout/IHideoutProduction"; import { IItemEventRouterResponse } from "@spt/models/eft/itemEvent/IItemEventRouterResponse"; import { IAcceptQuestRequestData } from "@spt/models/eft/quests/IAcceptQuestRequestData"; import { ICompleteQuestRequestData } from "@spt/models/eft/quests/ICompleteQuestRequestData"; @@ -1034,6 +1035,34 @@ export class QuestHelper { sessionID: string, response: IItemEventRouterResponse, ): void { + const matchingProductions = this.getRewardProductionMatch(craftUnlockReward, questDetails); + if (matchingProductions.length !== 1) { + this.logger.error( + this.localisationService.getText("quest-unable_to_find_matching_hideout_production", { + questName: questDetails.QuestName, + matchCount: matchingProductions.length, + }), + ); + + return; + } + + // Add above match to pmc profile + client response + const matchingCraftId = matchingProductions[0]._id; + pmcData.UnlockedInfo.unlockedProductionRecipe.push(matchingCraftId); + response.profileChanges[sessionID].recipeUnlocked[matchingCraftId] = true; + } + + /** + * Find hideout craft id for the specified quest reward + * @param craftUnlockReward + * @param questDetails + * @returns + */ + public getRewardProductionMatch( + craftUnlockReward: IQuestReward, + questDetails: IQuest, + ): IHideoutProduction[] { // Get hideout crafts and find those that match by areatype/required level/end product tpl - hope for just one match const craftingRecipes = this.databaseService.getHideout().production.recipes; @@ -1055,23 +1084,9 @@ export class QuestHelper { matchingProductions = matchingProductions.filter((prod) => prod.requirements.some((requirement) => requirement.questId === questDetails._id), ); - - if (matchingProductions.length !== 1) { - this.logger.error( - this.localisationService.getText("quest-unable_to_find_matching_hideout_production", { - questName: questDetails.QuestName, - matchCount: matchingProductions.length, - }), - ); - - return; - } } - // Add above match to pmc profile + client response - const matchingCraftId = matchingProductions[0]._id; - pmcData.UnlockedInfo.unlockedProductionRecipe.push(matchingCraftId); - response.profileChanges[sessionID].recipeUnlocked[matchingCraftId] = true; + return matchingProductions; } /** diff --git a/project/src/services/ProfileFixerService.ts b/project/src/services/ProfileFixerService.ts index 4e4d87f6..1b396d9b 100644 --- a/project/src/services/ProfileFixerService.ts +++ b/project/src/services/ProfileFixerService.ts @@ -2,9 +2,11 @@ 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 { QuestHelper } from "@spt/helpers/QuestHelper"; import { TraderHelper } from "@spt/helpers/TraderHelper"; import { IPmcData } from "@spt/models/eft/common/IPmcData"; import { IBonus, IHideoutSlot } from "@spt/models/eft/common/tables/IBotBase"; +import { IQuest, IQuestReward } from "@spt/models/eft/common/tables/IQuest"; import { IPmcDataRepeatableQuest, IRepeatableQuest } from "@spt/models/eft/common/tables/IRepeatableQuests"; import { ITemplateItem } from "@spt/models/eft/common/tables/ITemplateItem"; import { IStageBonus } from "@spt/models/eft/hideout/IHideoutArea"; @@ -12,6 +14,8 @@ import { IEquipmentBuild, IMagazineBuild, ISptProfile, IWeaponBuild } from "@spt import { BonusType } from "@spt/models/enums/BonusType"; import { ConfigTypes } from "@spt/models/enums/ConfigTypes"; import { HideoutAreas } from "@spt/models/enums/HideoutAreas"; +import { QuestRewardType } from "@spt/models/enums/QuestRewardType"; +import { QuestStatus } from "@spt/models/enums/QuestStatus"; import { ICoreConfig } from "@spt/models/spt/config/ICoreConfig"; import { IRagfairConfig } from "@spt/models/spt/config/IRagfairConfig"; import { ILogger } from "@spt/models/spt/utils/ILogger"; @@ -45,6 +49,7 @@ export class ProfileFixerService { @inject("HashUtil") protected hashUtil: HashUtil, @inject("ConfigServer") protected configServer: ConfigServer, @inject("PrimaryCloner") protected cloner: ICloner, + @inject("QuestHelper") protected questHelper: QuestHelper, ) { this.coreConfig = this.configServer.getConfig(ConfigTypes.CORE); this.ragfairConfig = this.configServer.getConfig(ConfigTypes.RAGFAIR); @@ -58,6 +63,7 @@ export class ProfileFixerService { this.removeDanglingConditionCounters(pmcProfile); this.removeDanglingTaskConditionCounters(pmcProfile); this.removeOrphanedQuests(pmcProfile); + this.verifyQuestProductionUnlocks(pmcProfile); if (pmcProfile.Hideout) { this.addHideoutEliteSlots(pmcProfile); @@ -264,6 +270,72 @@ export class ProfileFixerService { } } + /** + * Verify that all quest production unlocks have been applied to the PMC Profile + * @param pmcProfile The profile to validate quest productions for + */ + protected verifyQuestProductionUnlocks(pmcProfile: IPmcData): void { + const start = performance.now(); + + const quests = this.databaseService.getQuests(); + const profileQuests = pmcProfile.Quests; + + for (const profileQuest of profileQuests) + { + const quest = quests[profileQuest.qid]; + + // For started or successful quests, check for unlocks in the `Started` rewards + if (profileQuest.status == QuestStatus.Started || profileQuest.status == QuestStatus.Success) + { + const productionRewards = quest.rewards.Started?.filter(reward => reward.type == QuestRewardType.PRODUCTIONS_SCHEME); + productionRewards?.forEach(reward => this.verifyQuestProductionUnlock(pmcProfile, reward, quest)); + } + + // For successful quests, check for unlocks in the `Success` rewards + if (profileQuest.status == QuestStatus.Success) + { + const productionRewards = quest.rewards.Success?.filter(reward => reward.type == QuestRewardType.PRODUCTIONS_SCHEME); + productionRewards?.forEach(reward => this.verifyQuestProductionUnlock(pmcProfile, reward, quest)); + } + } + + const validateTime = performance.now() - start + this.logger.debug(`Quest Production Unlock validation took: ${validateTime.toFixed(2)}ms`); + } + + /** + * Validate that the given profile has the given quest reward production scheme unlocked, and add it if not + * @param pmcProfile Profile to check + * @param productionUnlockReward The quest reward to validate + * @param questDetails The quest the reward belongs to + * @returns + */ + protected verifyQuestProductionUnlock( + pmcProfile: IPmcData, + productionUnlockReward: IQuestReward, + questDetails: IQuest + ): void { + const matchingProductions = this.questHelper.getRewardProductionMatch(productionUnlockReward, questDetails); + if (matchingProductions.length !== 1) { + this.logger.error( + this.localisationService.getText("quest-unable_to_find_matching_hideout_production", { + questName: questDetails.QuestName, + matchCount: matchingProductions.length, + }), + ); + + return; + } + + // Add above match to pmc profile + const matchingProductionId = matchingProductions[0]._id; + if (!pmcProfile.UnlockedInfo.unlockedProductionRecipe.includes(matchingProductionId)) + { + pmcProfile.UnlockedInfo.unlockedProductionRecipe.push(matchingProductionId); + this.logger.debug(`Added production ${matchingProductionId} to unlocked production recipes for ${questDetails.QuestName}`); + } + } + /** * 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.