From ae7fddb96e6b89f65ed59bb7b69d7261608f7b9c Mon Sep 17 00:00:00 2001 From: Chomp Date: Thu, 9 Jan 2025 15:58:11 +0000 Subject: [PATCH] Initial implementation of Prestige system Refactor of Create profile code into own service Updated `addHideoutCustomisationLock` to use enums for parameters + refactored logic Removed redundant `HideoutCustomizationGen` script --- project/src/callbacks/PrestigeCallbacks.ts | 2 +- project/src/controllers/PrestigeController.ts | 134 ++++++- project/src/controllers/ProfileController.ts | 339 +--------------- project/src/di/Container.ts | 4 + project/src/helpers/ProfileHelper.ts | 92 +++-- project/src/helpers/QuestRewardHelper.ts | 7 +- .../common/tables/ICustomisationStorage.ts | 30 +- .../src/models/eft/common/tables/IPrestige.ts | 2 +- .../eft/profile/IProfileCreateRequestData.ts | 1 + project/src/models/enums/QuestRewardType.ts | 2 + project/src/services/CreateProfileService.ts | 361 ++++++++++++++++++ .../src/services/LocationLifecycleService.ts | 3 +- .../HideoutCustomisationGen.ts | 131 ------- 13 files changed, 607 insertions(+), 501 deletions(-) create mode 100644 project/src/services/CreateProfileService.ts delete mode 100644 project/src/tools/HideoutCustomisation/HideoutCustomisationGen.ts diff --git a/project/src/callbacks/PrestigeCallbacks.ts b/project/src/callbacks/PrestigeCallbacks.ts index 913b5c8b..cb0f93fa 100644 --- a/project/src/callbacks/PrestigeCallbacks.ts +++ b/project/src/callbacks/PrestigeCallbacks.ts @@ -22,7 +22,7 @@ export class PrestigeCallbacks { } /** Handle client/prestige/obtain */ - public obtainPrestige(url: string, info: IObtainPrestigeRequest, sessionID: string): INullResponseData { + public obtainPrestige(url: string, info: IObtainPrestigeRequest[], sessionID: string): INullResponseData { this.prestigeController.obtainPrestige(sessionID, info); return this.httpResponse.nullResponse(); diff --git a/project/src/controllers/PrestigeController.ts b/project/src/controllers/PrestigeController.ts index 9a5a5ae6..b817f83d 100644 --- a/project/src/controllers/PrestigeController.ts +++ b/project/src/controllers/PrestigeController.ts @@ -1,14 +1,23 @@ import { PlayerScavGenerator } from "@spt/generators/PlayerScavGenerator"; import { DialogueHelper } from "@spt/helpers/DialogueHelper"; +import { InventoryHelper } from "@spt/helpers/InventoryHelper"; import type { ItemHelper } from "@spt/helpers/ItemHelper"; import { ProfileHelper } from "@spt/helpers/ProfileHelper"; import { QuestHelper } from "@spt/helpers/QuestHelper"; import { TraderHelper } from "@spt/helpers/TraderHelper"; +import { CustomisationSource } from "@spt/models/eft/common/tables/ICustomisationStorage"; import { IPrestige } from "@spt/models/eft/common/tables/IPrestige"; +import { IQuestReward } from "@spt/models/eft/common/tables/IQuest"; +import { IAddItemDirectRequest } from "@spt/models/eft/inventory/IAddItemDirectRequest"; +import { IAddItemsDirectRequest } from "@spt/models/eft/inventory/IAddItemsDirectRequest"; import { IObtainPrestigeRequest } from "@spt/models/eft/prestige/IObtainPrestigeRequest"; +import { IProfileCreateRequestData } from "@spt/models/eft/profile/IProfileCreateRequestData"; +import { ISptProfile } from "@spt/models/eft/profile/ISptProfile"; +import { SkillTypes } from "@spt/models/enums/SkillTypes"; import type { ILogger } from "@spt/models/spt/utils/ILogger"; import { EventOutputHolder } from "@spt/routers/EventOutputHolder"; import { SaveServer } from "@spt/servers/SaveServer"; +import { CreateProfileService } from "@spt/services/CreateProfileService"; import { DatabaseService } from "@spt/services/DatabaseService"; import { LocalisationService } from "@spt/services/LocalisationService"; import { MailSendService } from "@spt/services/MailSendService"; @@ -32,12 +41,14 @@ export class PrestigeController { @inject("ItemHelper") protected itemHelper: ItemHelper, @inject("ProfileFixerService") protected profileFixerService: ProfileFixerService, @inject("LocalisationService") protected localisationService: LocalisationService, + @inject("CreateProfileService") protected createProfileService: CreateProfileService, @inject("SeasonalEventService") protected seasonalEventService: SeasonalEventService, @inject("MailSendService") protected mailSendService: MailSendService, @inject("PlayerScavGenerator") protected playerScavGenerator: PlayerScavGenerator, @inject("EventOutputHolder") protected eventOutputHolder: EventOutputHolder, @inject("TraderHelper") protected traderHelper: TraderHelper, @inject("DialogueHelper") protected dialogueHelper: DialogueHelper, + @inject("InventoryHelper") protected inventoryHelper: InventoryHelper, @inject("QuestHelper") protected questHelper: QuestHelper, @inject("ProfileHelper") protected profileHelper: ProfileHelper, ) {} @@ -52,12 +63,125 @@ export class PrestigeController { /** * Handle /client/prestige/obtain */ - public obtainPrestige(sessionID: string, request: IObtainPrestigeRequest): void { + public obtainPrestige(sessionId: string, request: IObtainPrestigeRequest[]): void { // TODO - // Reset profile back to default from template - // Take items passed in from request and add to inventory - // Set prestige level in profile + // DONE Reset profile back to default from template + // DONE Set prestige level in profile + // DONE Copy skills + // DONE Take items passed in from request and add to inventory // Update dogtags to prestige type - // Iterate over prestige.json rewards and add to profile + // DONE Iterate over prestige.json rewards and add to profile + // DONE add achievement + + const prePrestigeProfileClone = this.cloner.clone(this.profileHelper.getFullProfile(sessionId)); + const prePrestigePmc = prePrestigeProfileClone.characters.pmc; + const createRequest: IProfileCreateRequestData = { + side: prePrestigePmc.Info.Side, + nickname: prePrestigePmc.Info.Nickname, + headId: prePrestigePmc.Customization.Head, + voiceId: Object.values(this.databaseService.getTemplates().customization).find( + (customisation) => customisation._name === prePrestigePmc.Info.Voice, + )._id, + sptForcePrestigeLevel: prePrestigeProfileClone.characters.pmc.Info.PrestigeLevel + 1, // Current + 1 + }; + + // Reset profile + this.createProfileService.createProfile(sessionId, createRequest); + + // Get freshly reset profile ready for editing + const newProfile = this.profileHelper.getFullProfile(sessionId); + + // Skill copy + const commonSKillsToCopy = prePrestigePmc.Skills.Common; + for (const skillToCopy of commonSKillsToCopy) { + // Set progress to max level 20 + skillToCopy.Progress = Math.min(skillToCopy.Progress, 2000); + const existingSkill = newProfile.characters.pmc.Skills.Common.find((skill) => skill.Id === skillToCopy.Id); + if (existingSkill) { + existingSkill.Progress = skillToCopy.Progress; + } else { + newProfile.characters.pmc.Skills.Common.push(skillToCopy); + } + } + + const masteringSkillsToCopy = prePrestigePmc.Skills.Mastering; + for (const skillToCopy of masteringSkillsToCopy) { + // Set progress to max level 20 + skillToCopy.Progress = Math.min(skillToCopy.Progress, 2000); + const existingSkill = newProfile.characters.pmc.Skills.Mastering.find( + (skill) => skill.Id === skillToCopy.Id, + ); + if (existingSkill) { + existingSkill.Progress = skillToCopy.Progress; + } else { + newProfile.characters.pmc.Skills.Mastering.push(skillToCopy); + } + } + + const indexToGet = Math.min(createRequest.sptForcePrestigeLevel - 1, 1); // Index starts at 0 + const rewards = this.databaseService.getTemplates().prestige.elements[indexToGet].rewards; + this.addPrestigeRewardsToProfile(sessionId, newProfile, rewards); + + // Copy transferred items + for (const transferRequest of request) { + const item = prePrestigePmc.Inventory.items.find((item) => item._id === transferRequest.id); + const addItemRequest: IAddItemDirectRequest = { + itemWithModsToAdd: [item], + foundInRaid: item.upd?.SpawnedInSession, + useSortingTable: false, + callback: null, + }; + this.inventoryHelper.addItemToStash( + sessionId, + addItemRequest, + newProfile.characters.pmc, + this.eventOutputHolder.getOutput(sessionId), + ); + } + + // Add "Prestigious" achievement + if (!newProfile.achievements["676091c0f457869a94017a23"]) { + newProfile.achievements["676091c0f457869a94017a23"] = this.timeUtil.getTimestamp(); + } + } + + protected addPrestigeRewardsToProfile(sessionId: string, newProfile: ISptProfile, rewards: IQuestReward[]) { + for (const reward of rewards) { + switch (reward.type) { + case "CustomizationDirect": { + this.profileHelper.addHideoutCustomisationUnlock(newProfile, reward, CustomisationSource.PRESTIGE); + break; + } + case "Skill": + this.profileHelper.addSkillPointsToPlayer( + newProfile.characters.pmc, + reward.target as SkillTypes, + reward.value as number, + ); + break; + case "Item": { + const addItemRequest: IAddItemDirectRequest = { + itemWithModsToAdd: reward.items, + foundInRaid: reward.items[0]?.upd?.SpawnedInSession, + useSortingTable: false, + callback: null, + }; + this.inventoryHelper.addItemToStash( + sessionId, + addItemRequest, + newProfile.characters.pmc, + this.eventOutputHolder.getOutput(sessionId), + ); + break; + } + // case "ExtraDailyQuest": { + // // todo + // break; + // } + default: + this.logger.error(`Unhandled prestige reward type: ${reward.type}`); + break; + } + } } } diff --git a/project/src/controllers/ProfileController.ts b/project/src/controllers/ProfileController.ts index ee9a132b..8b6b539d 100644 --- a/project/src/controllers/ProfileController.ts +++ b/project/src/controllers/ProfileController.ts @@ -1,13 +1,6 @@ import { PlayerScavGenerator } from "@spt/generators/PlayerScavGenerator"; -import { DialogueHelper } from "@spt/helpers/DialogueHelper"; -import { ItemHelper } from "@spt/helpers/ItemHelper"; import { ProfileHelper } from "@spt/helpers/ProfileHelper"; -import { QuestHelper } from "@spt/helpers/QuestHelper"; -import { QuestRewardHelper } from "@spt/helpers/QuestRewardHelper"; -import { TraderHelper } from "@spt/helpers/TraderHelper"; import { IPmcData } from "@spt/models/eft/common/IPmcData"; -import { ITemplateSide } from "@spt/models/eft/common/tables/IProfileTemplate"; -import { IItemEventRouterResponse } from "@spt/models/eft/itemEvent/IItemEventRouterResponse"; import { IMiniProfile } from "@spt/models/eft/launcher/IMiniProfile"; import { IGetProfileStatusResponseData } from "@spt/models/eft/profile/GetProfileStatusResponseData"; import { IGetOtherProfileRequest } from "@spt/models/eft/profile/IGetOtherProfileRequest"; @@ -18,22 +11,11 @@ import { IProfileChangeVoiceRequestData } from "@spt/models/eft/profile/IProfile import { IProfileCreateRequestData } from "@spt/models/eft/profile/IProfileCreateRequestData"; import { ISearchFriendRequestData } from "@spt/models/eft/profile/ISearchFriendRequestData"; import { ISearchFriendResponse } from "@spt/models/eft/profile/ISearchFriendResponse"; -import { IInraid, ISptProfile, IVitality } from "@spt/models/eft/profile/ISptProfile"; import { IValidateNicknameRequestData } from "@spt/models/eft/profile/IValidateNicknameRequestData"; -import { GameEditions } from "@spt/models/enums/GameEditions"; -import { ItemTpl } from "@spt/models/enums/ItemTpl"; -import { MessageType } from "@spt/models/enums/MessageType"; -import { QuestStatus } from "@spt/models/enums/QuestStatus"; import type { ILogger } from "@spt/models/spt/utils/ILogger"; -import { EventOutputHolder } from "@spt/routers/EventOutputHolder"; import { SaveServer } from "@spt/servers/SaveServer"; +import { CreateProfileService } from "@spt/services/CreateProfileService"; import { DatabaseService } from "@spt/services/DatabaseService"; -import { LocalisationService } from "@spt/services/LocalisationService"; -import { MailSendService } from "@spt/services/MailSendService"; -import { ProfileFixerService } from "@spt/services/ProfileFixerService"; -import { SeasonalEventService } from "@spt/services/SeasonalEventService"; -import { HashUtil } from "@spt/utils/HashUtil"; -import { TimeUtil } from "@spt/utils/TimeUtil"; import type { ICloner } from "@spt/utils/cloners/ICloner"; import { inject, injectable } from "tsyringe"; @@ -41,22 +23,11 @@ import { inject, injectable } from "tsyringe"; export class ProfileController { constructor( @inject("PrimaryLogger") protected logger: ILogger, - @inject("HashUtil") protected hashUtil: HashUtil, @inject("PrimaryCloner") protected cloner: ICloner, - @inject("TimeUtil") protected timeUtil: TimeUtil, @inject("SaveServer") protected saveServer: SaveServer, @inject("DatabaseService") protected databaseService: DatabaseService, - @inject("ItemHelper") protected itemHelper: ItemHelper, - @inject("ProfileFixerService") protected profileFixerService: ProfileFixerService, - @inject("LocalisationService") protected localisationService: LocalisationService, - @inject("SeasonalEventService") protected seasonalEventService: SeasonalEventService, - @inject("MailSendService") protected mailSendService: MailSendService, + @inject("CreateProfileService") protected createProfileService: CreateProfileService, @inject("PlayerScavGenerator") protected playerScavGenerator: PlayerScavGenerator, - @inject("EventOutputHolder") protected eventOutputHolder: EventOutputHolder, - @inject("TraderHelper") protected traderHelper: TraderHelper, - @inject("DialogueHelper") protected dialogueHelper: DialogueHelper, - @inject("QuestHelper") protected questHelper: QuestHelper, - @inject("QuestRewardHelper") protected questRewardHelper: QuestRewardHelper, @inject("ProfileHelper") protected profileHelper: ProfileHelper, ) {} @@ -129,311 +100,7 @@ export class ProfileController { * @returns Profiles _id value */ public createProfile(info: IProfileCreateRequestData, sessionID: string): string { - const account = this.saveServer.getProfile(sessionID).info; - const profileTemplateClone: ITemplateSide = this.cloner.clone( - this.databaseService.getProfiles()[account.edition][info.side.toLowerCase()], - ); - const pmcData = profileTemplateClone.character; - - // Delete existing profile - this.deleteProfileBySessionId(sessionID); - - // PMC - pmcData._id = account.id; - pmcData.aid = account.aid; - pmcData.savage = account.scavId; - pmcData.sessionId = sessionID; - pmcData.Info.Nickname = info.nickname; - pmcData.Info.LowerNickname = account.username.toLowerCase(); - pmcData.Info.RegistrationDate = this.timeUtil.getTimestamp(); - pmcData.Info.Voice = this.databaseService.getCustomization()[info.voiceId]._name; - pmcData.Stats = this.profileHelper.getDefaultCounters(); - pmcData.Info.NeedWipeOptions = []; - pmcData.Customization.Head = info.headId; - pmcData.Health.UpdateTime = this.timeUtil.getTimestamp(); - pmcData.Quests = []; - pmcData.Hideout.Seed = this.timeUtil.getTimestamp() + 8 * 60 * 60 * 24 * 365; // 8 years in future why? who knows, we saw it in live - pmcData.RepeatableQuests = []; - pmcData.CarExtractCounts = {}; - pmcData.CoopExtractCounts = {}; - pmcData.Achievements = {}; - - this.updateInventoryEquipmentId(pmcData); - - if (!pmcData.UnlockedInfo) { - pmcData.UnlockedInfo = { unlockedProductionRecipe: [] }; - } - - // Add required items to pmc stash - this.addMissingInternalContainersToProfile(pmcData); - - // Change item IDs to be unique - pmcData.Inventory.items = this.itemHelper.replaceIDs( - pmcData.Inventory.items, - pmcData, - undefined, - pmcData.Inventory.fastPanel, - ); - - // Create profile - const profileDetails: ISptProfile = { - info: account, - characters: { pmc: pmcData, scav: {} as IPmcData }, - suits: profileTemplateClone.suits, - userbuilds: profileTemplateClone.userbuilds, - dialogues: profileTemplateClone.dialogues, - spt: this.profileHelper.getDefaultSptDataObject(), - vitality: {} as IVitality, - inraid: {} as IInraid, - insurance: [], - traderPurchases: {}, - achievements: {}, - friends: [], - customisationUnlocks: [], - }; - - this.addCustomisationUnlocksToProfile(profileDetails); - - this.profileFixerService.checkForAndFixPmcProfileIssues(profileDetails.characters.pmc); - - this.saveServer.addProfile(profileDetails); - - if (profileTemplateClone.trader.setQuestsAvailableForStart) { - this.questHelper.addAllQuestsToProfile(profileDetails.characters.pmc, [QuestStatus.AvailableForStart]); - } - - // Profile is flagged as wanting quests set to ready to hand in and collect rewards - if (profileTemplateClone.trader.setQuestsAvailableForFinish) { - this.questHelper.addAllQuestsToProfile(profileDetails.characters.pmc, [ - QuestStatus.AvailableForStart, - QuestStatus.Started, - QuestStatus.AvailableForFinish, - ]); - - // Make unused response so applyQuestReward works - const response = this.eventOutputHolder.getOutput(sessionID); - - // Add rewards for starting quests to profile - this.givePlayerStartingQuestRewards(profileDetails, sessionID, response); - } - - this.resetAllTradersInProfile(sessionID); - - this.saveServer.getProfile(sessionID).characters.scav = this.generatePlayerScav(sessionID); - - // Store minimal profile and reload it - this.saveServer.saveProfile(sessionID); - this.saveServer.loadProfile(sessionID); - - // Completed account creation - this.saveServer.getProfile(sessionID).info.wipe = false; - this.saveServer.saveProfile(sessionID); - - return pmcData._id; - } - - protected addCustomisationUnlocksToProfile(fullProfile: ISptProfile) { - // Some game versions have additional dogtag variants, add them - switch (this.getGameEdition(fullProfile)) { - case GameEditions.EDGE_OF_DARKNESS: - // Gets EoD tags - fullProfile.customisationUnlocks.push({ - id: "6746fd09bafff85008048838", - source: "default", - type: "dogTag", - }); - - fullProfile.customisationUnlocks.push({ - id: "67471938bafff850080488b7", - source: "default", - type: "dogTag", - }); - - break; - case GameEditions.UNHEARD: - // Gets EoD+Unheard tags - fullProfile.customisationUnlocks.push({ - id: "6746fd09bafff85008048838", - source: "default", - type: "dogTag", - }); - - fullProfile.customisationUnlocks.push({ - id: "67471938bafff850080488b7", - source: "default", - type: "dogTag", - }); - - fullProfile.customisationUnlocks.push({ - id: "67471928d17d6431550563b5", - source: "default", - type: "dogTag", - }); - - fullProfile.customisationUnlocks.push({ - id: "6747193f170146228c0d2226", - source: "default", - type: "dogTag", - }); - break; - } - - const pretigeLevel = fullProfile?.characters?.pmc?.Info?.PrestigeLevel; - if (pretigeLevel) { - if (pretigeLevel >= 1) { - fullProfile.customisationUnlocks.push({ - id: "674dbf593bee1152d407f005", - source: "default", - type: "dogTag", - }); - } - - if (pretigeLevel >= 2) { - fullProfile.customisationUnlocks.push({ - id: "675dcfea7ae1a8792107ca99", - source: "default", - type: "dogTag", - }); - } - } - } - - protected getGameEdition(profile: ISptProfile): string { - const edition = profile.characters?.pmc?.Info?.GameVersion; - if (!edition) { - // Edge case - profile not created yet, fall back to what launcher has set - const launcherEdition = profile.info.edition; - switch (launcherEdition.toLowerCase()) { - case "edge of darkness": - return GameEditions.EDGE_OF_DARKNESS; - case "unheard": - return GameEditions.UNHEARD; - default: - return GameEditions.STANDARD; - } - } - - return edition; - } - - /** - * make profiles pmcData.Inventory.equipment unique - * @param pmcData Profile to update - */ - protected updateInventoryEquipmentId(pmcData: IPmcData): void { - const oldEquipmentId = pmcData.Inventory.equipment; - pmcData.Inventory.equipment = this.hashUtil.generate(); - - for (const item of pmcData.Inventory.items) { - if (item.parentId === oldEquipmentId) { - item.parentId = pmcData.Inventory.equipment; - - continue; - } - - if (item._id === oldEquipmentId) { - item._id = pmcData.Inventory.equipment; - } - } - } - - /** - * Ensure a profile has the necessary internal containers e.g. questRaidItems / sortingTable - * DOES NOT check that stash exists - * @param pmcData Profile to check - */ - protected addMissingInternalContainersToProfile(pmcData: IPmcData): void { - if (!pmcData.Inventory.items.find((item) => item._id === pmcData.Inventory.hideoutCustomizationStashId)) { - pmcData.Inventory.items.push({ - _id: pmcData.Inventory.hideoutCustomizationStashId, - _tpl: ItemTpl.HIDEOUTAREACONTAINER_CUSTOMIZATION, - }); - } - - if (!pmcData.Inventory.items.find((item) => item._id === pmcData.Inventory.sortingTable)) { - pmcData.Inventory.items.push({ - _id: pmcData.Inventory.sortingTable, - _tpl: ItemTpl.SORTINGTABLE_SORTING_TABLE, - }); - } - - if (!pmcData.Inventory.items.find((item) => item._id === pmcData.Inventory.questStashItems)) { - pmcData.Inventory.items.push({ - _id: pmcData.Inventory.questStashItems, - _tpl: ItemTpl.STASH_QUESTOFFLINE, - }); - } - - if (!pmcData.Inventory.items.find((item) => item._id === pmcData.Inventory.questRaidItems)) { - pmcData.Inventory.items.push({ - _id: pmcData.Inventory.questRaidItems, - _tpl: ItemTpl.STASH_QUESTRAID, - }); - } - } - - /** - * Delete a profile - * @param sessionID Id of profile to delete - */ - protected deleteProfileBySessionId(sessionID: string): void { - if (sessionID in this.saveServer.getProfiles()) { - this.saveServer.deleteProfileById(sessionID); - } else { - this.logger.warning( - this.localisationService.getText("profile-unable_to_find_profile_by_id_cannot_delete", sessionID), - ); - } - } - - /** - * Iterate over all quests in player profile, inspect rewards for the quests current state (accepted/completed) - * and send rewards to them in mail - * @param profileDetails Player profile - * @param sessionID Session id - * @param response Event router response - */ - protected givePlayerStartingQuestRewards( - profileDetails: ISptProfile, - sessionID: string, - response: IItemEventRouterResponse, - ): void { - for (const quest of profileDetails.characters.pmc.Quests) { - const questFromDb = this.questHelper.getQuestFromDb(quest.qid, profileDetails.characters.pmc); - - // Get messageId of text to send to player as text message in game - // Copy of code from QuestController.acceptQuest() - const messageId = this.questHelper.getMessageIdForQuestStart( - questFromDb.startedMessageText, - questFromDb.description, - ); - const itemRewards = this.questRewardHelper.applyQuestReward( - profileDetails.characters.pmc, - quest.qid, - QuestStatus.Started, - sessionID, - response, - ); - - this.mailSendService.sendLocalisedNpcMessageToPlayer( - sessionID, - this.traderHelper.getTraderById(questFromDb.traderId), - MessageType.QUEST_START, - messageId, - itemRewards, - this.timeUtil.getHoursAsSeconds(100), - ); - } - } - - /** - * For each trader reset their state to what a level 1 player would see - * @param sessionId Session id of profile to reset - */ - protected resetAllTradersInProfile(sessionId: string): void { - for (const traderId in this.databaseService.getTraders()) { - this.traderHelper.resetTrader(sessionId, traderId); - } + return this.createProfileService.createProfile(sessionID, info); } /** diff --git a/project/src/di/Container.ts b/project/src/di/Container.ts index 7a735297..65064fe8 100644 --- a/project/src/di/Container.ts +++ b/project/src/di/Container.ts @@ -209,6 +209,7 @@ import { BotLootCacheService } from "@spt/services/BotLootCacheService"; import { BotNameService } from "@spt/services/BotNameService"; import { BotWeaponModLimitService } from "@spt/services/BotWeaponModLimitService"; import { CircleOfCultistService } from "@spt/services/CircleOfCultistService"; +import { CreateProfileService } from "@spt/services/CreateProfileService"; import { CustomLocationWaveService } from "@spt/services/CustomLocationWaveService"; import { DatabaseService } from "@spt/services/DatabaseService"; import { FenceService } from "@spt/services/FenceService"; @@ -820,6 +821,9 @@ export class Container { depContainer.register("PostDbLoadService", PostDbLoadService, { lifecycle: Lifecycle.Singleton, }); + depContainer.register("CreateProfileService", CreateProfileService, { + lifecycle: Lifecycle.Singleton, + }); } private static registerServers(depContainer: DependencyContainer): void { diff --git a/project/src/helpers/ProfileHelper.ts b/project/src/helpers/ProfileHelper.ts index a446413a..a28ef764 100644 --- a/project/src/helpers/ProfileHelper.ts +++ b/project/src/helpers/ProfileHelper.ts @@ -1,7 +1,11 @@ import { ItemHelper } from "@spt/helpers/ItemHelper"; import { IPmcData } from "@spt/models/eft/common/IPmcData"; import { BanType, Common, ICounterKeyValue, IStats } from "@spt/models/eft/common/tables/IBotBase"; -import { ICustomisationStorage } from "@spt/models/eft/common/tables/ICustomisationStorage"; +import { + CustomisationSource, + CustomisationType, + ICustomisationStorage, +} from "@spt/models/eft/common/tables/ICustomisationStorage"; import { IItem } from "@spt/models/eft/common/tables/IItem"; import { IQuestReward } from "@spt/models/eft/common/tables/IQuest"; import { ISearchFriendResponse } from "@spt/models/eft/profile/ISearchFriendResponse"; @@ -582,8 +586,8 @@ export class ProfileHelper { // Reward found, add to profile fullProfile.customisationUnlocks.push({ id: customizationDirectReward.target, - source: "achievement", - type: customisationDataDb.type, + source: CustomisationSource.ACHIEVEMENT, + type: customisationDataDb.type as CustomisationType, }); } @@ -651,33 +655,75 @@ export class ProfileHelper { * @param reward reward given to player with customisation data * @param source Source of reward, e.g. "unlockedInGame" for quests and "achievement" for achievements */ - public addHideoutCustomisationUnlock(fullProfile: ISptProfile, reward: IQuestReward, source: string): void { - // Get matching db data for reward - const hideoutCustomisationDb = this.databaseService - .getHideout() - .customisation.globals.find((customisation) => customisation.itemId === reward.target); - if (!hideoutCustomisationDb) { - this.logger.warning( - `Unable to add hideout customisaiton reward: ${reward.target} to profile: ${fullProfile.info.id} as matching object cannot be found in hideout/customisation.json`, - ); - - return; - } - + public addHideoutCustomisationUnlock( + fullProfile: ISptProfile, + reward: IQuestReward, + source: CustomisationSource, + ): void { fullProfile.customisationUnlocks ||= []; - if (fullProfile.customisationUnlocks?.some((unlock) => unlock.id === hideoutCustomisationDb.id)) { + if (fullProfile.customisationUnlocks?.some((unlock) => unlock.id === reward.target)) { this.logger.warning( `Profile: ${fullProfile.info.id} already has hideout customisaiton reward: ${reward.target}, skipping`, ); return; } - const rewardToStore: ICustomisationStorage = { - id: hideoutCustomisationDb.itemId, - source: source, - type: hideoutCustomisationDb.type, - }; + const customisationTemplateDb = this.databaseService.getTemplates().customization; + const matchingCustomisation = customisationTemplateDb[reward.target]; - fullProfile.customisationUnlocks.push(rewardToStore); + if (matchingCustomisation) { + const rewardToStore: ICustomisationStorage = { + id: reward.target, + source: source, + type: null, + }; + switch (matchingCustomisation._parent) { + case "675ff48ce8d2356707079617": { + // MannequinPose + rewardToStore.type = CustomisationType.MANNEQUIN_POSE; + break; + } + case "6751848eba5968fd800a01d6": { + // Gestures + rewardToStore.type = CustomisationType.GESTURE; + break; + } + case "67373f170eca6e03ab0d5391": { + // Floor + rewardToStore.type = CustomisationType.FLOOR; + break; + } + case "6746fafabafff8500804880e": { + // DogTags + rewardToStore.type = CustomisationType.DOG_TAG; + break; + } + case "673b3f595bf6b605c90fcdc2": { + // Ceiling + rewardToStore.type = CustomisationType.CEILING; + break; + } + case "67373f1e5a5ee73f2a081baf": { + // Wall + rewardToStore.type = CustomisationType.WALL; + break; + } + default: + this.logger.error( + `Unhandled customisation unlock type: ${matchingCustomisation._parent} not added to profile`, + ); + return; + } + + fullProfile.customisationUnlocks.push(rewardToStore); + } + + // const rewardToStore: ICustomisationStorage = { + // id: matchingHideoutCustomisation.itemId, + // source: source, + // type: matchingHideoutCustomisation.type as CustomisationType, + // }; + + // fullProfile.customisationUnlocks.push(rewardToStore); } } diff --git a/project/src/helpers/QuestRewardHelper.ts b/project/src/helpers/QuestRewardHelper.ts index 88102c01..aaf0f390 100644 --- a/project/src/helpers/QuestRewardHelper.ts +++ b/project/src/helpers/QuestRewardHelper.ts @@ -4,6 +4,7 @@ import { PresetHelper } from "@spt/helpers/PresetHelper"; import { ProfileHelper } from "@spt/helpers/ProfileHelper"; import { TraderHelper } from "@spt/helpers/TraderHelper"; import { IPmcData } from "@spt/models/eft/common/IPmcData"; +import { CustomisationSource } from "@spt/models/eft/common/tables/ICustomisationStorage"; import { IItem } from "@spt/models/eft/common/tables/IItem"; import { IQuest, IQuestReward } from "@spt/models/eft/common/tables/IQuest"; import { IHideoutProduction } from "@spt/models/eft/hideout/IHideoutProduction"; @@ -131,7 +132,11 @@ export class QuestRewardHelper { this.profileHelper.replaceProfilePocketTpl(pmcProfile, reward.target); break; case QuestRewardType.CUSTOMIZATION_DIRECT: - this.profileHelper.addHideoutCustomisationUnlock(fullProfile, reward, "unlockedInGame"); + this.profileHelper.addHideoutCustomisationUnlock( + fullProfile, + reward, + CustomisationSource.UNLOCKED_IN_GAME, + ); break; default: this.logger.error( diff --git a/project/src/models/eft/common/tables/ICustomisationStorage.ts b/project/src/models/eft/common/tables/ICustomisationStorage.ts index b3297988..f5f7cf45 100644 --- a/project/src/models/eft/common/tables/ICustomisationStorage.ts +++ b/project/src/models/eft/common/tables/ICustomisationStorage.ts @@ -1,5 +1,31 @@ export interface ICustomisationStorage { id: string; // Customiastion.json/itemId - source: string; - type: string; + source: CustomisationSource; + type: CustomisationType; +} + +export enum CustomisationType { + SUITE = "suite", + DOG_TAG = "dogTag", + HEAD = "head", + VOICE = "voice", + GESTURE = "gesture", + ENVIRONMENT = "environment", + WALL = "wall", + FLOOR = "floor", + CEILING = "ceiling", + LIGHT = "light", + SHOOTING_RANGE_MARK = "shootingRangeMark", + CAT = "cat", + MANNEQUIN_POSE = "mannequinPose", +} + +export enum CustomisationSource { + QUEST = "quest", + PRESTIGE = "prestige", + ACHIEVEMENT = "achievement", + UNLOCKED_IN_GAME = "unlockedInGame", + PAID = "paid", + DROP = "drop", + DEFAULT = "default", } diff --git a/project/src/models/eft/common/tables/IPrestige.ts b/project/src/models/eft/common/tables/IPrestige.ts index 260b1de6..35fd8c6c 100644 --- a/project/src/models/eft/common/tables/IPrestige.ts +++ b/project/src/models/eft/common/tables/IPrestige.ts @@ -1,7 +1,7 @@ import type { IQuestCondition, IQuestReward } from "./IQuest"; export interface IPrestige { - elements: IPretigeElement; + elements: IPretigeElement[]; } export interface IPretigeElement { diff --git a/project/src/models/eft/profile/IProfileCreateRequestData.ts b/project/src/models/eft/profile/IProfileCreateRequestData.ts index 93cc656f..84088c71 100644 --- a/project/src/models/eft/profile/IProfileCreateRequestData.ts +++ b/project/src/models/eft/profile/IProfileCreateRequestData.ts @@ -3,4 +3,5 @@ export interface IProfileCreateRequestData { nickname: string; headId: string; voiceId: string; + sptForcePrestigeLevel?: number; } diff --git a/project/src/models/enums/QuestRewardType.ts b/project/src/models/enums/QuestRewardType.ts index 0a970bd8..f8f9671c 100644 --- a/project/src/models/enums/QuestRewardType.ts +++ b/project/src/models/enums/QuestRewardType.ts @@ -12,4 +12,6 @@ export enum QuestRewardType { ACHIEVEMENT = "Achievement", POCKETS = "Pockets", CUSTOMIZATION_DIRECT = "CustomizationDirect", + CUSTOMIZATION_OFFER = "CustomizationOffer", + EXTRA_DAILY_QUEST = "ExtraDailyQuest", } diff --git a/project/src/services/CreateProfileService.ts b/project/src/services/CreateProfileService.ts new file mode 100644 index 00000000..2d5d499e --- /dev/null +++ b/project/src/services/CreateProfileService.ts @@ -0,0 +1,361 @@ +import { PlayerScavGenerator } from "@spt/generators/PlayerScavGenerator"; +import { ItemHelper } from "@spt/helpers/ItemHelper"; +import { ProfileHelper } from "@spt/helpers/ProfileHelper"; +import { QuestHelper } from "@spt/helpers/QuestHelper"; +import { QuestRewardHelper } from "@spt/helpers/QuestRewardHelper"; +import { TraderHelper } from "@spt/helpers/TraderHelper"; +import { IPmcData } from "@spt/models/eft/common/IPmcData"; +import { CustomisationSource, CustomisationType } from "@spt/models/eft/common/tables/ICustomisationStorage"; +import { ITemplateSide } from "@spt/models/eft/common/tables/IProfileTemplate"; +import { IItemEventRouterResponse } from "@spt/models/eft/itemEvent/IItemEventRouterResponse"; +import { IProfileCreateRequestData } from "@spt/models/eft/profile/IProfileCreateRequestData"; +import { IInraid, ISptProfile, IVitality } from "@spt/models/eft/profile/ISptProfile"; +import { GameEditions } from "@spt/models/enums/GameEditions"; +import { ItemTpl } from "@spt/models/enums/ItemTpl"; +import { MessageType } from "@spt/models/enums/MessageType"; +import { QuestStatus } from "@spt/models/enums/QuestStatus"; +import type { ILogger } from "@spt/models/spt/utils/ILogger"; +import { EventOutputHolder } from "@spt/routers/EventOutputHolder"; +import { SaveServer } from "@spt/servers/SaveServer"; +import { DatabaseService } from "@spt/services/DatabaseService"; +import { LocalisationService } from "@spt/services/LocalisationService"; +import { MailSendService } from "@spt/services/MailSendService"; +import { ProfileFixerService } from "@spt/services/ProfileFixerService"; +import { HashUtil } from "@spt/utils/HashUtil"; +import { TimeUtil } from "@spt/utils/TimeUtil"; +import type { ICloner } from "@spt/utils/cloners/ICloner"; +import { inject, injectable } from "tsyringe"; + +@injectable() +export class CreateProfileService { + constructor( + @inject("PrimaryLogger") protected logger: ILogger, + @inject("HashUtil") protected hashUtil: HashUtil, + @inject("TimeUtil") protected timeUtil: TimeUtil, + @inject("SaveServer") protected saveServer: SaveServer, + @inject("DatabaseService") protected databaseService: DatabaseService, + @inject("ProfileFixerService") protected profileFixerService: ProfileFixerService, + @inject("ItemHelper") protected itemHelper: ItemHelper, + @inject("QuestHelper") protected questHelper: QuestHelper, + @inject("ProfileHelper") protected profileHelper: ProfileHelper, + @inject("TraderHelper") protected traderHelper: TraderHelper, + @inject("LocalisationService") protected localisationService: LocalisationService, + @inject("MailSendService") protected mailSendService: MailSendService, + @inject("PlayerScavGenerator") protected playerScavGenerator: PlayerScavGenerator, + @inject("QuestRewardHelper") protected questRewardHelper: QuestRewardHelper, + @inject("PrimaryCloner") protected cloner: ICloner, + @inject("EventOutputHolder") protected eventOutputHolder: EventOutputHolder, + ) {} + + public createProfile(sessionID: string, info: IProfileCreateRequestData): string { + const account = this.saveServer.getProfile(sessionID).info; + const profileTemplateClone: ITemplateSide = this.cloner.clone( + this.databaseService.getProfiles()[account.edition][info.side.toLowerCase()], + ); + const pmcData = profileTemplateClone.character; + + // Delete existing profile + this.deleteProfileBySessionId(sessionID); + + // PMC + pmcData._id = account.id; + pmcData.aid = account.aid; + pmcData.savage = account.scavId; + pmcData.sessionId = sessionID; + pmcData.Info.Nickname = info.nickname; + pmcData.Info.LowerNickname = account.username.toLowerCase(); + pmcData.Info.RegistrationDate = this.timeUtil.getTimestamp(); + pmcData.Info.Voice = this.databaseService.getCustomization()[info.voiceId]._name; + pmcData.Stats = this.profileHelper.getDefaultCounters(); + pmcData.Info.NeedWipeOptions = []; + pmcData.Customization.Head = info.headId; + pmcData.Health.UpdateTime = this.timeUtil.getTimestamp(); + pmcData.Quests = []; + pmcData.Hideout.Seed = this.timeUtil.getTimestamp() + 8 * 60 * 60 * 24 * 365; // 8 years in future why? who knows, we saw it in live + pmcData.RepeatableQuests = []; + pmcData.CarExtractCounts = {}; + pmcData.CoopExtractCounts = {}; + pmcData.Achievements = {}; + + if (typeof info.sptForcePrestigeLevel === "number") { + pmcData.Info.PrestigeLevel = info.sptForcePrestigeLevel; + } + + this.updateInventoryEquipmentId(pmcData); + + if (!pmcData.UnlockedInfo) { + pmcData.UnlockedInfo = { unlockedProductionRecipe: [] }; + } + + // Add required items to pmc stash + this.addMissingInternalContainersToProfile(pmcData); + + // Change item IDs to be unique + pmcData.Inventory.items = this.itemHelper.replaceIDs( + pmcData.Inventory.items, + pmcData, + undefined, + pmcData.Inventory.fastPanel, + ); + + // Create profile + const profileDetails: ISptProfile = { + info: account, + characters: { pmc: pmcData, scav: {} as IPmcData }, + suits: profileTemplateClone.suits, + userbuilds: profileTemplateClone.userbuilds, + dialogues: profileTemplateClone.dialogues, + spt: this.profileHelper.getDefaultSptDataObject(), + vitality: {} as IVitality, + inraid: {} as IInraid, + insurance: [], + traderPurchases: {}, + achievements: {}, + friends: [], + customisationUnlocks: [], + }; + + this.addCustomisationUnlocksToProfile(profileDetails); + + this.profileFixerService.checkForAndFixPmcProfileIssues(profileDetails.characters.pmc); + + this.saveServer.addProfile(profileDetails); + + if (profileTemplateClone.trader.setQuestsAvailableForStart) { + this.questHelper.addAllQuestsToProfile(profileDetails.characters.pmc, [QuestStatus.AvailableForStart]); + } + + // Profile is flagged as wanting quests set to ready to hand in and collect rewards + if (profileTemplateClone.trader.setQuestsAvailableForFinish) { + this.questHelper.addAllQuestsToProfile(profileDetails.characters.pmc, [ + QuestStatus.AvailableForStart, + QuestStatus.Started, + QuestStatus.AvailableForFinish, + ]); + + // Make unused response so applyQuestReward works + const response = this.eventOutputHolder.getOutput(sessionID); + + // Add rewards for starting quests to profile + this.givePlayerStartingQuestRewards(profileDetails, sessionID, response); + } + + this.resetAllTradersInProfile(sessionID); + + this.saveServer.getProfile(sessionID).characters.scav = this.playerScavGenerator.generate(sessionID); + + // Store minimal profile and reload it + this.saveServer.saveProfile(sessionID); + this.saveServer.loadProfile(sessionID); + + // Completed account creation + this.saveServer.getProfile(sessionID).info.wipe = false; + this.saveServer.saveProfile(sessionID); + + return pmcData._id; + } + + /** + * Delete a profile + * @param sessionID Id of profile to delete + */ + protected deleteProfileBySessionId(sessionID: string): void { + if (sessionID in this.saveServer.getProfiles()) { + this.saveServer.deleteProfileById(sessionID); + } else { + this.logger.warning( + this.localisationService.getText("profile-unable_to_find_profile_by_id_cannot_delete", sessionID), + ); + } + } + + /** + * make profiles pmcData.Inventory.equipment unique + * @param pmcData Profile to update + */ + protected updateInventoryEquipmentId(pmcData: IPmcData): void { + const oldEquipmentId = pmcData.Inventory.equipment; + pmcData.Inventory.equipment = this.hashUtil.generate(); + + for (const item of pmcData.Inventory.items) { + if (item.parentId === oldEquipmentId) { + item.parentId = pmcData.Inventory.equipment; + + continue; + } + + if (item._id === oldEquipmentId) { + item._id = pmcData.Inventory.equipment; + } + } + } + + /** + * For each trader reset their state to what a level 1 player would see + * @param sessionId Session id of profile to reset + */ + protected resetAllTradersInProfile(sessionId: string): void { + for (const traderId in this.databaseService.getTraders()) { + this.traderHelper.resetTrader(sessionId, traderId); + } + } + + /** + * Ensure a profile has the necessary internal containers e.g. questRaidItems / sortingTable + * DOES NOT check that stash exists + * @param pmcData Profile to check + */ + protected addMissingInternalContainersToProfile(pmcData: IPmcData): void { + if (!pmcData.Inventory.items.find((item) => item._id === pmcData.Inventory.hideoutCustomizationStashId)) { + pmcData.Inventory.items.push({ + _id: pmcData.Inventory.hideoutCustomizationStashId, + _tpl: ItemTpl.HIDEOUTAREACONTAINER_CUSTOMIZATION, + }); + } + + if (!pmcData.Inventory.items.find((item) => item._id === pmcData.Inventory.sortingTable)) { + pmcData.Inventory.items.push({ + _id: pmcData.Inventory.sortingTable, + _tpl: ItemTpl.SORTINGTABLE_SORTING_TABLE, + }); + } + + if (!pmcData.Inventory.items.find((item) => item._id === pmcData.Inventory.questStashItems)) { + pmcData.Inventory.items.push({ + _id: pmcData.Inventory.questStashItems, + _tpl: ItemTpl.STASH_QUESTOFFLINE, + }); + } + + if (!pmcData.Inventory.items.find((item) => item._id === pmcData.Inventory.questRaidItems)) { + pmcData.Inventory.items.push({ + _id: pmcData.Inventory.questRaidItems, + _tpl: ItemTpl.STASH_QUESTRAID, + }); + } + } + + protected addCustomisationUnlocksToProfile(fullProfile: ISptProfile) { + // Some game versions have additional dogtag variants, add them + switch (this.getGameEdition(fullProfile)) { + case GameEditions.EDGE_OF_DARKNESS: + // Gets EoD tags + fullProfile.customisationUnlocks.push({ + id: "6746fd09bafff85008048838", + source: CustomisationSource.DEFAULT, + type: CustomisationType.DOG_TAG, + }); + + fullProfile.customisationUnlocks.push({ + id: "67471938bafff850080488b7", + source: CustomisationSource.DEFAULT, + type: CustomisationType.DOG_TAG, + }); + + break; + case GameEditions.UNHEARD: + // Gets EoD+Unheard tags + fullProfile.customisationUnlocks.push({ + id: "6746fd09bafff85008048838", + source: CustomisationSource.DEFAULT, + type: CustomisationType.DOG_TAG, + }); + + fullProfile.customisationUnlocks.push({ + id: "67471938bafff850080488b7", + source: CustomisationSource.DEFAULT, + type: CustomisationType.DOG_TAG, + }); + + fullProfile.customisationUnlocks.push({ + id: "67471928d17d6431550563b5", + source: CustomisationSource.DEFAULT, + type: CustomisationType.DOG_TAG, + }); + + fullProfile.customisationUnlocks.push({ + id: "6747193f170146228c0d2226", + source: CustomisationSource.DEFAULT, + type: CustomisationType.DOG_TAG, + }); + break; + } + + const pretigeLevel = fullProfile?.characters?.pmc?.Info?.PrestigeLevel; + if (pretigeLevel) { + if (pretigeLevel >= 1) { + fullProfile.customisationUnlocks.push({ + id: "674dbf593bee1152d407f005", + source: CustomisationSource.DEFAULT, + type: CustomisationType.DOG_TAG, + }); + } + + if (pretigeLevel >= 2) { + fullProfile.customisationUnlocks.push({ + id: "675dcfea7ae1a8792107ca99", + source: CustomisationSource.DEFAULT, + type: CustomisationType.DOG_TAG, + }); + } + } + } + + protected getGameEdition(profile: ISptProfile): string { + const edition = profile.characters?.pmc?.Info?.GameVersion; + if (!edition) { + // Edge case - profile not created yet, fall back to what launcher has set + const launcherEdition = profile.info.edition; + switch (launcherEdition.toLowerCase()) { + case "edge of darkness": + return GameEditions.EDGE_OF_DARKNESS; + case "unheard": + return GameEditions.UNHEARD; + default: + return GameEditions.STANDARD; + } + } + + return edition; + } + + /** + * Iterate over all quests in player profile, inspect rewards for the quests current state (accepted/completed) + * and send rewards to them in mail + * @param profileDetails Player profile + * @param sessionID Session id + * @param response Event router response + */ + protected givePlayerStartingQuestRewards( + profileDetails: ISptProfile, + sessionID: string, + response: IItemEventRouterResponse, + ): void { + for (const quest of profileDetails.characters.pmc.Quests) { + const questFromDb = this.questHelper.getQuestFromDb(quest.qid, profileDetails.characters.pmc); + + // Get messageId of text to send to player as text message in game + // Copy of code from QuestController.acceptQuest() + const messageId = this.questHelper.getMessageIdForQuestStart( + questFromDb.startedMessageText, + questFromDb.description, + ); + const itemRewards = this.questRewardHelper.applyQuestReward( + profileDetails.characters.pmc, + quest.qid, + QuestStatus.Started, + sessionID, + response, + ); + + this.mailSendService.sendLocalisedNpcMessageToPlayer( + sessionID, + this.traderHelper.getTraderById(questFromDb.traderId), + MessageType.QUEST_START, + messageId, + itemRewards, + this.timeUtil.getHoursAsSeconds(100), + ); + } + } +} diff --git a/project/src/services/LocationLifecycleService.ts b/project/src/services/LocationLifecycleService.ts index 38a04805..49578916 100644 --- a/project/src/services/LocationLifecycleService.ts +++ b/project/src/services/LocationLifecycleService.ts @@ -11,6 +11,7 @@ import { TraderHelper } from "@spt/helpers/TraderHelper"; import { ILocationBase } from "@spt/models/eft/common/ILocationBase"; import { IPmcData } from "@spt/models/eft/common/IPmcData"; import { Common, IQuestStatus, ITraderInfo } from "@spt/models/eft/common/tables/IBotBase"; +import { CustomisationSource } from "@spt/models/eft/common/tables/ICustomisationStorage"; import { IItem } from "@spt/models/eft/common/tables/IItem"; import { IEndLocalRaidRequestData, @@ -762,7 +763,7 @@ export class LocationLifecycleService { // Insert customisations into profile for (const reward of customisationRewards) { - this.profileHelper.addHideoutCustomisationUnlock(fullProfile, reward, "achievement"); + this.profileHelper.addHideoutCustomisationUnlock(fullProfile, reward, CustomisationSource.ACHIEVEMENT); } } diff --git a/project/src/tools/HideoutCustomisation/HideoutCustomisationGen.ts b/project/src/tools/HideoutCustomisation/HideoutCustomisationGen.ts deleted file mode 100644 index 2fc53de9..00000000 --- a/project/src/tools/HideoutCustomisation/HideoutCustomisationGen.ts +++ /dev/null @@ -1,131 +0,0 @@ -/** - * Hydrate customisationStorage.json with data scraped together from other sources - * - * Usage: - * - Run this script using npm: `npm run gen:customisationstorage` - * - */ -import { dirname, join, resolve } from "node:path"; -import { OnLoad } from "@spt/di/OnLoad"; -import { IQuestReward } from "@spt/models/eft/common/tables/IQuest"; -import type { ILogger } from "@spt/models/spt/utils/ILogger"; -import { DatabaseServer } from "@spt/servers/DatabaseServer"; -import { FileSystem } from "@spt/utils/FileSystem"; -import { inject, injectAll, injectable } from "tsyringe"; - -@injectable() -export class HideoutCustomisationGen { - private questCustomisationReward: Record = {}; - private achievementCustomisationReward: Record = {}; - - constructor( - @inject("DatabaseServer") protected databaseServer: DatabaseServer, - @inject("PrimaryLogger") protected logger: ILogger, - @inject("FileSystem") protected fileSystem: FileSystem, - @injectAll("OnLoad") protected onLoadComponents: OnLoad[], - ) {} - - async run(): Promise { - // Load all of the onload components, this gives us access to most of SPTs injections - for (const onLoad of this.onLoadComponents) { - await onLoad.onLoad(); - } - - // Build up our dataset - this.buildQuestCustomisationList(); - this.buildAchievementRewardCustomisationList(); - this.updateCustomisationStorage(); - - // Dump the new data to disk - const currentDir = dirname(__filename); - const projectDir = resolve(currentDir, "..", "..", ".."); - const templatesDir = join(projectDir, "assets", "database", "templates"); - const customisationStorageOutPath = join(templatesDir, "customisationStorage.json"); - await this.fileSystem.write( - customisationStorageOutPath, - JSON.stringify(this.databaseServer.getTables().templates?.customisationStorage, null, 2), - ); - } - - private updateCustomisationStorage(): void { - const customisationStoageDb = this.databaseServer.getTables().templates?.customisationStorage; - if (!customisationStoageDb) { - // no customisation storage in templates, nothing to do - return; - } - for (const globalCustomisationDb of this.databaseServer.getTables().hideout?.customisation.globals) { - // Look for customisations that have a quest unlock condition - const questOrAchievementRequirement = globalCustomisationDb.conditions.find((condition) => - ["Quest", "Block"].includes(condition.conditionType), - ); - - if (!questOrAchievementRequirement) { - // Customisation doesnt have a requirement, skip - continue; - } - - if (customisationStoageDb.some((custStorageItem) => custStorageItem.id === globalCustomisationDb.id)) { - // Exists already in output destination file, skip - continue; - } - - const matchingQuest = this.questCustomisationReward[questOrAchievementRequirement.target as string]; - const matchingAchievement = - this.achievementCustomisationReward[questOrAchievementRequirement.target as string]; - - let source = null; - if (matchingQuest) { - source = "unlockedInGame"; - } else if (matchingAchievement) { - source = "achievement"; - } - if (!source) { - this.logger.error( - `Found customisation to add but unable to establish source. Id: ${globalCustomisationDb.id} type: ${globalCustomisationDb.type}`, - ); - continue; - } - - this.logger.success( - `Adding Id: ${globalCustomisationDb.id} Source: ${source} type: ${globalCustomisationDb.type}`, - ); - customisationStoageDb.push({ - id: globalCustomisationDb.id, - source: source, - type: globalCustomisationDb.type, - }); - } - } - - // Build a dictionary of all quests with a `CustomizationDirect` reward - private buildQuestCustomisationList(): void { - for (const quest of Object.values(this.databaseServer.getTables().templates.quests)) { - const allRewards: IQuestReward[] = [ - ...quest.rewards.Fail, - ...quest.rewards.Success, - ...quest.rewards.Started, - ]; - const customisationDirectRewards = allRewards.filter((reward) => reward.type === "CustomizationDirect"); - for (const directReward of customisationDirectRewards) { - if (!this.questCustomisationReward[quest._id]) { - this.questCustomisationReward[quest._id] = []; - } - this.questCustomisationReward[quest._id].push(directReward); - } - } - } - - // Build a dictionary of all achievements with a `CustomizationDirect` reward - private buildAchievementRewardCustomisationList(): void { - for (const achievement of Object.values(this.databaseServer.getTables().templates?.achievements)) { - const allRewards: IQuestReward[] = Object.values(achievement.rewards); - const customisationDirectRewards = allRewards.filter((reward) => reward.type === "CustomizationDirect"); - for (const directReward of customisationDirectRewards) { - if (!this.achievementCustomisationReward[achievement.id]) { - this.achievementCustomisationReward[achievement.id] = []; - } - this.achievementCustomisationReward[achievement.id].push(directReward); - } - } - } -}