Server/project/src/controllers/ProfileController.ts

477 lines
18 KiB
TypeScript
Raw Normal View History

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 { 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 { GetProfileStatusResponseData } from "@spt/models/eft/profile/GetProfileStatusResponseData";
import { IGetOtherProfileRequest } from "@spt/models/eft/profile/IGetOtherProfileRequest";
import { IGetOtherProfileResponse } from "@spt/models/eft/profile/IGetOtherProfileResponse";
import { IGetProfileSettingsRequest } from "@spt/models/eft/profile/IGetProfileSettingsRequest";
import { IProfileChangeNicknameRequestData } from "@spt/models/eft/profile/IProfileChangeNicknameRequestData";
import { IProfileChangeVoiceRequestData } from "@spt/models/eft/profile/IProfileChangeVoiceRequestData";
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 { ISptProfile, Inraid, Vitality } from "@spt/models/eft/profile/ISptProfile";
import { IValidateNicknameRequestData } from "@spt/models/eft/profile/IValidateNicknameRequestData";
import { ItemTpl } from "@spt/models/enums/ItemTpl";
import { MessageType } from "@spt/models/enums/MessageType";
import { QuestStatus } from "@spt/models/enums/QuestStatus";
import { 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 { SeasonalEventService } from "@spt/services/SeasonalEventService";
import { HashUtil } from "@spt/utils/HashUtil";
import { TimeUtil } from "@spt/utils/TimeUtil";
import { ICloner } from "@spt/utils/cloners/ICloner";
import { inject, injectable } from "tsyringe";
2023-03-03 15:23:46 +00:00
@injectable()
export class ProfileController {
2023-03-03 15:23:46 +00:00
constructor(
@inject("PrimaryLogger") protected logger: ILogger,
2023-03-03 15:23:46 +00:00
@inject("HashUtil") protected hashUtil: HashUtil,
@inject("PrimaryCloner") protected cloner: ICloner,
2023-03-03 15:23:46 +00:00
@inject("TimeUtil") protected timeUtil: TimeUtil,
@inject("SaveServer") protected saveServer: SaveServer,
@inject("DatabaseService") protected databaseService: DatabaseService,
2023-03-03 15:23:46 +00:00
@inject("ItemHelper") protected itemHelper: ItemHelper,
@inject("ProfileFixerService") protected profileFixerService: ProfileFixerService,
2023-07-19 13:16:45 +01:00
@inject("LocalisationService") protected localisationService: LocalisationService,
@inject("SeasonalEventService") protected seasonalEventService: SeasonalEventService,
@inject("MailSendService") protected mailSendService: MailSendService,
2023-03-03 15:23:46 +00:00
@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,
2023-11-15 20:35:05 -05:00
@inject("ProfileHelper") protected profileHelper: ProfileHelper,
) {}
2023-03-03 15:23:46 +00:00
2023-07-15 11:00:35 +01:00
/**
* Handle /launcher/profiles
*/
public getMiniProfiles(): IMiniProfile[] {
const allProfiles = Object.keys(this.saveServer.getProfiles());
2023-03-03 15:23:46 +00:00
return allProfiles.map((sessionId) => this.getMiniProfile(sessionId));
2023-03-03 15:23:46 +00:00
}
2023-07-15 11:00:35 +01:00
/**
* Handle launcher/profile/info
*/
public getMiniProfile(sessionID: string): IMiniProfile {
2023-03-03 15:23:46 +00:00
const profile = this.saveServer.getProfile(sessionID);
if (!profile || !profile.characters) {
throw new Error(`Unable to find character data for id: ${sessionID}. Profile may be corrupt`);
}
2023-03-03 15:23:46 +00:00
const pmc = profile.characters.pmc;
const maxlvl = this.profileHelper.getMaxLevel();
2023-03-03 15:23:46 +00:00
// Player hasn't completed profile creation process, send defaults
if (!pmc?.Info?.Level) {
2023-03-03 15:23:46 +00:00
return {
2024-07-06 13:39:56 +01:00
username: profile.info?.username ?? "",
nickname: "unknown",
side: "unknown",
currlvl: 0,
currexp: 0,
prevexp: 0,
nextlvl: 0,
maxlvl: maxlvl,
edition: profile.info?.edition ?? "",
profileId: profile.info?.id ?? "",
sptData: this.profileHelper.getDefaultSptDataObject(),
2023-03-03 15:23:46 +00:00
};
}
const currlvl = pmc.Info.Level;
const nextlvl = this.profileHelper.getExperience(currlvl + 1);
return {
username: profile.info.username,
nickname: pmc.Info.Nickname,
side: pmc.Info.Side,
currlvl: pmc.Info.Level,
currexp: pmc.Info.Experience ?? 0,
prevexp: currlvl === 0 ? 0 : this.profileHelper.getExperience(currlvl),
nextlvl: nextlvl,
maxlvl: maxlvl,
edition: profile.info?.edition ?? "",
profileId: profile.info?.id ?? "",
sptData: profile.spt,
2023-03-03 15:23:46 +00:00
};
}
2023-07-15 11:00:35 +01:00
/**
* Handle client/game/profile/list
*/
public getCompleteProfile(sessionID: string): IPmcData[] {
2023-03-03 15:23:46 +00:00
return this.profileHelper.getCompleteProfile(sessionID);
}
2023-07-15 11:00:35 +01:00
/**
* Handle client/game/profile/create
2023-12-20 00:17:27 +00:00
* @param info Client reqeust object
* @param sessionID Player id
* @returns Profiles _id value
2023-07-15 11:00:35 +01:00
*/
public createProfile(info: IProfileCreateRequestData, sessionID: string): string {
2023-03-03 15:23:46 +00:00
const account = this.saveServer.getProfile(sessionID).info;
const profileTemplate: ITemplateSide = this.cloner.clone(
this.databaseService.getProfiles()[account.edition][info.side.toLowerCase()],
);
const pmcData = profileTemplate.character;
2023-03-03 15:23:46 +00:00
// Delete existing profile
this.deleteProfileBySessionId(sessionID);
2023-03-03 15:23:46 +00:00
2023-07-15 11:00:35 +01:00
// PMC
pmcData._id = account.id;
pmcData.aid = account.aid;
pmcData.savage = account.scavId;
pmcData.sessionId = sessionID;
pmcData.Info.Nickname = account.username;
pmcData.Info.LowerNickname = account.username.toLowerCase();
2023-03-03 15:23:46 +00:00
pmcData.Info.RegistrationDate = this.timeUtil.getTimestamp();
pmcData.Info.Voice = this.databaseService.getCustomization()[info.voiceId]._name;
2023-03-03 15:23:46 +00:00
pmcData.Stats = this.profileHelper.getDefaultCounters();
pmcData.Info.NeedWipeOptions = [];
2023-03-03 15:23:46 +00:00
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
2023-03-03 15:23:46 +00:00
pmcData.RepeatableQuests = [];
pmcData.CarExtractCounts = {};
pmcData.CoopExtractCounts = {};
pmcData.Achievements = {};
2023-03-03 15:23:46 +00:00
this.updateInventoryEquipmentId(pmcData);
if (!pmcData.UnlockedInfo) {
pmcData.UnlockedInfo = { unlockedProductionRecipe: [] };
}
// Change item IDs to be unique
2023-11-15 20:35:05 -05:00
pmcData.Inventory.items = this.itemHelper.replaceIDs(
pmcData.Inventory.items,
pmcData,
undefined,
2023-11-15 20:35:05 -05:00
pmcData.Inventory.fastPanel,
);
pmcData.Inventory.hideoutAreaStashes = {};
2023-03-03 15:23:46 +00:00
// Create profile
const profileDetails: ISptProfile = {
2023-03-03 15:23:46 +00:00
info: account,
2023-11-15 20:35:05 -05:00
characters: { pmc: pmcData, scav: {} as IPmcData },
suits: profileTemplate.suits,
userbuilds: profileTemplate.userbuilds,
dialogues: profileTemplate.dialogues,
spt: this.profileHelper.getDefaultSptDataObject(),
2023-03-03 15:23:46 +00:00
vitality: {} as Vitality,
inraid: {} as Inraid,
insurance: [],
2023-11-15 20:35:05 -05:00
traderPurchases: {},
achievements: {},
2023-03-03 15:23:46 +00:00
};
this.profileFixerService.checkForAndFixPmcProfileIssues(profileDetails.characters.pmc);
this.saveServer.addProfile(profileDetails);
if (profileTemplate.trader.setQuestsAvailableForStart) {
2023-03-03 15:23:46 +00:00
this.questHelper.addAllQuestsToProfile(profileDetails.characters.pmc, [QuestStatus.AvailableForStart]);
}
// Profile is flagged as wanting quests set to ready to hand in and collect rewards
if (profileTemplate.trader.setQuestsAvailableForFinish) {
2023-11-15 20:35:05 -05:00
this.questHelper.addAllQuestsToProfile(profileDetails.characters.pmc, [
QuestStatus.AvailableForStart,
QuestStatus.Started,
QuestStatus.AvailableForFinish,
]);
2023-03-03 15:23:46 +00:00
// Make unused response so applyQuestReward works
const response = this.eventOutputHolder.getOutput(sessionID);
// Add rewards for starting quests to profile
this.givePlayerStartingQuestRewards(profileDetails, sessionID, response);
2023-03-03 15:23:46 +00:00
}
this.resetAllTradersInProfile(sessionID);
2023-03-03 15:23:46 +00:00
this.saveServer.getProfile(sessionID).characters.scav = this.generatePlayerScav(sessionID);
2023-07-15 11:00:35 +01:00
// Store minimal profile and reload it
2023-03-03 15:23:46 +00:00
this.saveServer.saveProfile(sessionID);
this.saveServer.loadProfile(sessionID);
2023-07-15 11:00:35 +01:00
// Completed account creation
2023-03-03 15:23:46 +00:00
this.saveServer.getProfile(sessionID).info.wipe = false;
this.saveServer.saveProfile(sessionID);
2023-12-20 00:17:27 +00:00
// Requires to enable seasonal changes after creating fresh profile
if (this.seasonalEventService.isAutomaticEventDetectionEnabled()) {
this.seasonalEventService.enableSeasonalEvents(sessionID);
}
2023-12-20 00:17:27 +00:00
return pmcData._id;
2023-03-03 15:23:46 +00:00
}
/**
* 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;
}
}
}
/**
* 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 {
2023-11-15 20:35:05 -05:00
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
*/
2023-11-15 20:35:05 -05:00
protected givePlayerStartingQuestRewards(
profileDetails: ISptProfile,
2023-11-15 20:35:05 -05:00
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()
2023-11-15 20:35:05 -05:00
const messageId = this.questHelper.getMessageIdForQuestStart(
questFromDb.startedMessageText,
questFromDb.description,
);
const itemRewards = this.questHelper.applyQuestReward(
profileDetails.characters.pmc,
quest.qid,
QuestStatus.Started,
sessionID,
response,
);
this.mailSendService.sendLocalisedNpcMessageToPlayer(
sessionID,
this.traderHelper.getTraderById(questFromDb.traderId),
MessageType.QUEST_START,
messageId,
itemRewards,
2023-11-15 20:35:05 -05:00
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);
}
}
2023-03-03 15:23:46 +00:00
/**
* Generate a player scav object
2023-07-15 11:00:35 +01:00
* PMC profile MUST exist first before pscav can be generated
2023-11-15 20:35:05 -05:00
* @param sessionID
2023-03-03 15:23:46 +00:00
* @returns IPmcData object
*/
public generatePlayerScav(sessionID: string): IPmcData {
2023-03-03 15:23:46 +00:00
return this.playerScavGenerator.generate(sessionID);
}
2023-07-15 11:00:35 +01:00
/**
* Handle client/game/profile/nickname/validate
*/
public validateNickname(info: IValidateNicknameRequestData, sessionID: string): string {
if (info.nickname.length < 3) {
2023-03-03 15:23:46 +00:00
return "tooshort";
}
if (this.profileHelper.isNicknameTaken(info, sessionID)) {
2023-03-03 15:23:46 +00:00
return "taken";
}
return "OK";
}
2023-07-15 11:00:35 +01:00
/**
* Handle client/game/profile/nickname/change event
* Client allows player to adjust their profile name
*/
public changeNickname(info: IProfileChangeNicknameRequestData, sessionID: string): string {
2023-03-03 15:23:46 +00:00
const output = this.validateNickname(info, sessionID);
if (output === "OK") {
2023-03-03 15:23:46 +00:00
const pmcData = this.profileHelper.getPmcProfile(sessionID);
pmcData.Info.Nickname = info.nickname;
pmcData.Info.LowerNickname = info.nickname.toLowerCase();
}
return output;
}
2023-07-15 11:00:35 +01:00
/**
* Handle client/game/profile/voice/change event
*/
public changeVoice(info: IProfileChangeVoiceRequestData, sessionID: string): void {
2023-03-03 15:23:46 +00:00
const pmcData = this.profileHelper.getPmcProfile(sessionID);
pmcData.Info.Voice = info.voice;
}
2023-07-15 11:00:35 +01:00
/**
* Handle client/game/profile/search
*/
public getFriends(info: ISearchFriendRequestData, sessionID: string): ISearchFriendResponse[] {
const profile = this.saveServer.getProfile(sessionID);
// return some of the current player info for now
return [
{
_id: profile.characters.pmc._id,
aid: profile.characters.pmc.aid,
Info: {
Nickname: info.nickname,
Side: "Bear",
Level: 1,
MemberCategory: profile.characters.pmc.Info.MemberCategory,
},
},
];
2023-03-03 15:23:46 +00:00
}
/**
* Handle client/profile/status
*/
public getProfileStatus(sessionId: string): GetProfileStatusResponseData {
const account = this.saveServer.getProfile(sessionId).info;
const response: GetProfileStatusResponseData = {
maxPveCountExceeded: false,
profiles: [
{ profileid: account.scavId, profileToken: undefined, status: "Free", sid: "", ip: "", port: 0 },
{
profileid: account.id,
profileToken: undefined,
status: "Free",
sid: "",
ip: "",
port: 0,
},
],
};
return response;
}
public getOtherProfile(sessionId: string, request: IGetOtherProfileRequest): IGetOtherProfileResponse {
const player = this.profileHelper.getFullProfile(sessionId);
const playerPmc = player.characters.pmc;
const playerScav = player.characters.scav;
// return player for now
return {
id: playerPmc._id,
aid: playerPmc.aid,
2024-01-06 13:40:00 +00:00
info: {
nickname: playerPmc.Info.Nickname,
side: playerPmc.Info.Side,
experience: playerPmc.Info.Experience,
memberCategory: playerPmc.Info.MemberCategory,
bannedState: playerPmc.Info.BannedState,
bannedUntil: playerPmc.Info.BannedUntil,
registrationDate: playerPmc.Info.RegistrationDate,
2024-01-06 13:40:00 +00:00
},
customization: {
head: playerPmc.Customization.Head,
body: playerPmc.Customization.Body,
feet: playerPmc.Customization.Feet,
hands: playerPmc.Customization.Hands,
2024-01-06 13:40:00 +00:00
},
skills: playerPmc.Skills,
2024-01-06 13:40:00 +00:00
equipment: {
// Default inventory tpl
Id: playerPmc.Inventory.items.find((item) => item._tpl === ItemTpl.INVENTORY_DEFAULT)._id,
Items: playerPmc.Inventory.items,
2024-01-06 13:40:00 +00:00
},
achievements: playerPmc.Achievements,
2024-01-10 13:53:26 +00:00
favoriteItems: playerPmc.Inventory.favoriteItems ?? [],
2024-01-06 13:40:00 +00:00
pmcStats: {
eft: {
totalInGameTime: playerPmc.Stats.Eft.TotalInGameTime,
overAllCounters: playerPmc.Stats.Eft.OverallCounters,
},
2024-01-06 13:40:00 +00:00
},
scavStats: {
eft: {
totalInGameTime: playerScav.Stats.Eft.TotalInGameTime,
overAllCounters: playerScav.Stats.Eft.OverallCounters,
},
},
};
}
/**
* Handle client/profile/settings
*/
public setChosenProfileIcon(sessionId: string, request: IGetProfileSettingsRequest): boolean {
const profileToUpdate = this.profileHelper.getPmcProfile(sessionId);
if (!profileToUpdate) {
return false;
}
if (request.memberCategory !== null) {
profileToUpdate.Info.SelectedMemberCategory = request.memberCategory;
}
if (request.squadInviteRestriction !== null) {
profileToUpdate.Info.SquadInviteRestriction = request.squadInviteRestriction;
}
return true;
}
2023-11-15 20:35:05 -05:00
}