Skip to content

Commit e5067b0

Browse files
authored
Add quest production unlocks to the PMC Profile fixer service (#959)
Resolves an issue where sometimes quest production unlocks get removed from the profile, resulting in users being locked out of crafting stations due to uncollectable productions
2 parents 063d1ea + 8b1a315 commit e5067b0

File tree

2 files changed

+102
-15
lines changed

2 files changed

+102
-15
lines changed

project/src/helpers/QuestHelper.ts

+30-15
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { IPmcData } from "@spt/models/eft/common/IPmcData";
1010
import { Common, IQuestStatus } from "@spt/models/eft/common/tables/IBotBase";
1111
import { IItem } from "@spt/models/eft/common/tables/IItem";
1212
import { IQuest, IQuestCondition, IQuestReward } from "@spt/models/eft/common/tables/IQuest";
13+
import { IHideoutProduction } from "@spt/models/eft/hideout/IHideoutProduction";
1314
import { IItemEventRouterResponse } from "@spt/models/eft/itemEvent/IItemEventRouterResponse";
1415
import { IAcceptQuestRequestData } from "@spt/models/eft/quests/IAcceptQuestRequestData";
1516
import { ICompleteQuestRequestData } from "@spt/models/eft/quests/ICompleteQuestRequestData";
@@ -1034,6 +1035,34 @@ export class QuestHelper {
10341035
sessionID: string,
10351036
response: IItemEventRouterResponse,
10361037
): void {
1038+
const matchingProductions = this.getRewardProductionMatch(craftUnlockReward, questDetails);
1039+
if (matchingProductions.length !== 1) {
1040+
this.logger.error(
1041+
this.localisationService.getText("quest-unable_to_find_matching_hideout_production", {
1042+
questName: questDetails.QuestName,
1043+
matchCount: matchingProductions.length,
1044+
}),
1045+
);
1046+
1047+
return;
1048+
}
1049+
1050+
// Add above match to pmc profile + client response
1051+
const matchingCraftId = matchingProductions[0]._id;
1052+
pmcData.UnlockedInfo.unlockedProductionRecipe.push(matchingCraftId);
1053+
response.profileChanges[sessionID].recipeUnlocked[matchingCraftId] = true;
1054+
}
1055+
1056+
/**
1057+
* Find hideout craft id for the specified quest reward
1058+
* @param craftUnlockReward
1059+
* @param questDetails
1060+
* @returns
1061+
*/
1062+
public getRewardProductionMatch(
1063+
craftUnlockReward: IQuestReward,
1064+
questDetails: IQuest,
1065+
): IHideoutProduction[] {
10371066
// Get hideout crafts and find those that match by areatype/required level/end product tpl - hope for just one match
10381067
const craftingRecipes = this.databaseService.getHideout().production.recipes;
10391068

@@ -1055,23 +1084,9 @@ export class QuestHelper {
10551084
matchingProductions = matchingProductions.filter((prod) =>
10561085
prod.requirements.some((requirement) => requirement.questId === questDetails._id),
10571086
);
1058-
1059-
if (matchingProductions.length !== 1) {
1060-
this.logger.error(
1061-
this.localisationService.getText("quest-unable_to_find_matching_hideout_production", {
1062-
questName: questDetails.QuestName,
1063-
matchCount: matchingProductions.length,
1064-
}),
1065-
);
1066-
1067-
return;
1068-
}
10691087
}
10701088

1071-
// Add above match to pmc profile + client response
1072-
const matchingCraftId = matchingProductions[0]._id;
1073-
pmcData.UnlockedInfo.unlockedProductionRecipe.push(matchingCraftId);
1074-
response.profileChanges[sessionID].recipeUnlocked[matchingCraftId] = true;
1089+
return matchingProductions;
10751090
}
10761091

10771092
/**

project/src/services/ProfileFixerService.ts

+72
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,20 @@ import { HideoutHelper } from "@spt/helpers/HideoutHelper";
22
import { InventoryHelper } from "@spt/helpers/InventoryHelper";
33
import { ItemHelper } from "@spt/helpers/ItemHelper";
44
import { ProfileHelper } from "@spt/helpers/ProfileHelper";
5+
import { QuestHelper } from "@spt/helpers/QuestHelper";
56
import { TraderHelper } from "@spt/helpers/TraderHelper";
67
import { IPmcData } from "@spt/models/eft/common/IPmcData";
78
import { IBonus, IHideoutSlot } from "@spt/models/eft/common/tables/IBotBase";
9+
import { IQuest, IQuestReward } from "@spt/models/eft/common/tables/IQuest";
810
import { IPmcDataRepeatableQuest, IRepeatableQuest } from "@spt/models/eft/common/tables/IRepeatableQuests";
911
import { ITemplateItem } from "@spt/models/eft/common/tables/ITemplateItem";
1012
import { IStageBonus } from "@spt/models/eft/hideout/IHideoutArea";
1113
import { IEquipmentBuild, IMagazineBuild, ISptProfile, IWeaponBuild } from "@spt/models/eft/profile/ISptProfile";
1214
import { BonusType } from "@spt/models/enums/BonusType";
1315
import { ConfigTypes } from "@spt/models/enums/ConfigTypes";
1416
import { HideoutAreas } from "@spt/models/enums/HideoutAreas";
17+
import { QuestRewardType } from "@spt/models/enums/QuestRewardType";
18+
import { QuestStatus } from "@spt/models/enums/QuestStatus";
1519
import { ICoreConfig } from "@spt/models/spt/config/ICoreConfig";
1620
import { IRagfairConfig } from "@spt/models/spt/config/IRagfairConfig";
1721
import { ILogger } from "@spt/models/spt/utils/ILogger";
@@ -45,6 +49,7 @@ export class ProfileFixerService {
4549
@inject("HashUtil") protected hashUtil: HashUtil,
4650
@inject("ConfigServer") protected configServer: ConfigServer,
4751
@inject("PrimaryCloner") protected cloner: ICloner,
52+
@inject("QuestHelper") protected questHelper: QuestHelper,
4853
) {
4954
this.coreConfig = this.configServer.getConfig(ConfigTypes.CORE);
5055
this.ragfairConfig = this.configServer.getConfig(ConfigTypes.RAGFAIR);
@@ -58,6 +63,7 @@ export class ProfileFixerService {
5863
this.removeDanglingConditionCounters(pmcProfile);
5964
this.removeDanglingTaskConditionCounters(pmcProfile);
6065
this.removeOrphanedQuests(pmcProfile);
66+
this.verifyQuestProductionUnlocks(pmcProfile);
6167

6268
if (pmcProfile.Hideout) {
6369
this.addHideoutEliteSlots(pmcProfile);
@@ -264,6 +270,72 @@ export class ProfileFixerService {
264270
}
265271
}
266272

273+
/**
274+
* Verify that all quest production unlocks have been applied to the PMC Profile
275+
* @param pmcProfile The profile to validate quest productions for
276+
*/
277+
protected verifyQuestProductionUnlocks(pmcProfile: IPmcData): void {
278+
const start = performance.now();
279+
280+
const quests = this.databaseService.getQuests();
281+
const profileQuests = pmcProfile.Quests;
282+
283+
for (const profileQuest of profileQuests)
284+
{
285+
const quest = quests[profileQuest.qid];
286+
287+
// For started or successful quests, check for unlocks in the `Started` rewards
288+
if (profileQuest.status == QuestStatus.Started || profileQuest.status == QuestStatus.Success)
289+
{
290+
const productionRewards = quest.rewards.Started?.filter(reward => reward.type == QuestRewardType.PRODUCTIONS_SCHEME);
291+
productionRewards?.forEach(reward => this.verifyQuestProductionUnlock(pmcProfile, reward, quest));
292+
}
293+
294+
// For successful quests, check for unlocks in the `Success` rewards
295+
if (profileQuest.status == QuestStatus.Success)
296+
{
297+
const productionRewards = quest.rewards.Success?.filter(reward => reward.type == QuestRewardType.PRODUCTIONS_SCHEME);
298+
productionRewards?.forEach(reward => this.verifyQuestProductionUnlock(pmcProfile, reward, quest));
299+
}
300+
}
301+
302+
const validateTime = performance.now() - start
303+
this.logger.debug(`Quest Production Unlock validation took: ${validateTime.toFixed(2)}ms`);
304+
}
305+
306+
/**
307+
* Validate that the given profile has the given quest reward production scheme unlocked, and add it if not
308+
* @param pmcProfile Profile to check
309+
* @param productionUnlockReward The quest reward to validate
310+
* @param questDetails The quest the reward belongs to
311+
* @returns
312+
*/
313+
protected verifyQuestProductionUnlock(
314+
pmcProfile: IPmcData,
315+
productionUnlockReward: IQuestReward,
316+
questDetails: IQuest
317+
): void {
318+
const matchingProductions = this.questHelper.getRewardProductionMatch(productionUnlockReward, questDetails);
319+
if (matchingProductions.length !== 1) {
320+
this.logger.error(
321+
this.localisationService.getText("quest-unable_to_find_matching_hideout_production", {
322+
questName: questDetails.QuestName,
323+
matchCount: matchingProductions.length,
324+
}),
325+
);
326+
327+
return;
328+
}
329+
330+
// Add above match to pmc profile
331+
const matchingProductionId = matchingProductions[0]._id;
332+
if (!pmcProfile.UnlockedInfo.unlockedProductionRecipe.includes(matchingProductionId))
333+
{
334+
pmcProfile.UnlockedInfo.unlockedProductionRecipe.push(matchingProductionId);
335+
this.logger.debug(`Added production ${matchingProductionId} to unlocked production recipes for ${questDetails.QuestName}`);
336+
}
337+
}
338+
267339
/**
268340
* If the profile has elite Hideout Managment skill, add the additional slots from globals
269341
* NOTE: This seems redundant, but we will leave it here just incase.

0 commit comments

Comments
 (0)