0
0
mirror of https://github.com/sp-tarkov/server.git synced 2025-02-13 09:50:43 -05:00

Merge branch '4.0.0-DEV' into json-validation-module

This commit is contained in:
Chomp 2025-01-09 15:59:27 +00:00 committed by GitHub
commit 4b04b5d110
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 607 additions and 501 deletions

View File

@ -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();

View File

@ -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;
}
}
}
}

View File

@ -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);
}
/**

View File

@ -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", PostDbLoadService, {
lifecycle: Lifecycle.Singleton,
});
depContainer.register<CreateProfileService>("CreateProfileService", CreateProfileService, {
lifecycle: Lifecycle.Singleton,
});
}
private static registerServers(depContainer: DependencyContainer): void {

View File

@ -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 customisationTemplateDb = this.databaseService.getTemplates().customization;
const matchingCustomisation = customisationTemplateDb[reward.target];
if (matchingCustomisation) {
const rewardToStore: ICustomisationStorage = {
id: hideoutCustomisationDb.itemId,
id: reward.target,
source: source,
type: hideoutCustomisationDb.type,
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);
}
}

View File

@ -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(

View File

@ -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",
}

View File

@ -1,7 +1,7 @@
import type { IQuestCondition, IQuestReward } from "./IQuest";
export interface IPrestige {
elements: IPretigeElement;
elements: IPretigeElement[];
}
export interface IPretigeElement {

View File

@ -3,4 +3,5 @@ export interface IProfileCreateRequestData {
nickname: string;
headId: string;
voiceId: string;
sptForcePrestigeLevel?: number;
}

View File

@ -12,4 +12,6 @@ export enum QuestRewardType {
ACHIEVEMENT = "Achievement",
POCKETS = "Pockets",
CUSTOMIZATION_DIRECT = "CustomizationDirect",
CUSTOMIZATION_OFFER = "CustomizationOffer",
EXTRA_DAILY_QUEST = "ExtraDailyQuest",
}

View File

@ -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),
);
}
}
}

View File

@ -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);
}
}

View File

@ -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<string, IQuestReward[]> = {};
private achievementCustomisationReward: Record<string, IQuestReward[]> = {};
constructor(
@inject("DatabaseServer") protected databaseServer: DatabaseServer,
@inject("PrimaryLogger") protected logger: ILogger,
@inject("FileSystem") protected fileSystem: FileSystem,
@injectAll("OnLoad") protected onLoadComponents: OnLoad[],
) {}
async run(): Promise<void> {
// 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);
}
}
}
}