0
0
mirror of https://github.com/sp-tarkov/server.git synced 2025-02-13 09:10:43 -05:00
server/project/src/controllers/GameController.ts

528 lines
23 KiB
TypeScript
Raw Normal View History

import { ApplicationContext } from "@spt/context/ApplicationContext";
import { ContextVariableType } from "@spt/context/ContextVariableType";
import { HideoutHelper } from "@spt/helpers/HideoutHelper";
import { HttpServerHelper } from "@spt/helpers/HttpServerHelper";
import { InventoryHelper } from "@spt/helpers/InventoryHelper";
import { ProfileHelper } from "@spt/helpers/ProfileHelper";
import { PreSptModLoader } from "@spt/loaders/PreSptModLoader";
import { IEmptyRequestData } from "@spt/models/eft/common/IEmptyRequestData";
import { IPmcData } from "@spt/models/eft/common/IPmcData";
import { IBodyPartHealth } from "@spt/models/eft/common/tables/IBotBase";
import { ICheckVersionResponse } from "@spt/models/eft/game/ICheckVersionResponse";
import { ICurrentGroupResponse } from "@spt/models/eft/game/ICurrentGroupResponse";
import { IGameConfigResponse } from "@spt/models/eft/game/IGameConfigResponse";
import { IGameKeepAliveResponse } from "@spt/models/eft/game/IGameKeepAliveResponse";
import { IGameModeRequestData } from "@spt/models/eft/game/IGameModeRequestData";
import { ESessionMode } from "@spt/models/eft/game/IGameModeResponse";
import { IGetRaidTimeRequest } from "@spt/models/eft/game/IGetRaidTimeRequest";
import { IGetRaidTimeResponse } from "@spt/models/eft/game/IGetRaidTimeResponse";
import { IServerDetails } from "@spt/models/eft/game/IServerDetails";
import { ISptProfile } from "@spt/models/eft/profile/ISptProfile";
import { BonusType } from "@spt/models/enums/BonusType";
import { ConfigTypes } from "@spt/models/enums/ConfigTypes";
import { HideoutAreas } from "@spt/models/enums/HideoutAreas";
import { SkillTypes } from "@spt/models/enums/SkillTypes";
import { IBotConfig } from "@spt/models/spt/config/IBotConfig";
import { ICoreConfig } from "@spt/models/spt/config/ICoreConfig";
import { IHideoutConfig } from "@spt/models/spt/config/IHideoutConfig";
import { IHttpConfig } from "@spt/models/spt/config/IHttpConfig";
import { IRagfairConfig } from "@spt/models/spt/config/IRagfairConfig";
import { ILogger } from "@spt/models/spt/utils/ILogger";
import { ConfigServer } from "@spt/servers/ConfigServer";
import { CustomLocationWaveService } from "@spt/services/CustomLocationWaveService";
import { DatabaseService } from "@spt/services/DatabaseService";
import { GiftService } from "@spt/services/GiftService";
import { ItemBaseClassService } from "@spt/services/ItemBaseClassService";
import { LocalisationService } from "@spt/services/LocalisationService";
import { OpenZoneService } from "@spt/services/OpenZoneService";
import { PostDbLoadService } from "@spt/services/PostDbLoadService";
import { ProfileActivityService } from "@spt/services/ProfileActivityService";
import { ProfileFixerService } from "@spt/services/ProfileFixerService";
import { RaidTimeAdjustmentService } from "@spt/services/RaidTimeAdjustmentService";
import { SeasonalEventService } from "@spt/services/SeasonalEventService";
import { HashUtil } from "@spt/utils/HashUtil";
import { RandomUtil } from "@spt/utils/RandomUtil";
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 GameController {
2023-03-03 15:23:46 +00:00
protected httpConfig: IHttpConfig;
protected coreConfig: ICoreConfig;
protected ragfairConfig: IRagfairConfig;
protected hideoutConfig: IHideoutConfig;
protected botConfig: IBotConfig;
2023-03-03 15:23:46 +00:00
constructor(
@inject("PrimaryLogger") protected logger: ILogger,
@inject("DatabaseService") protected databaseService: DatabaseService,
2023-03-03 15:23:46 +00:00
@inject("TimeUtil") protected timeUtil: TimeUtil,
@inject("HashUtil") protected hashUtil: HashUtil,
@inject("PreSptModLoader") protected preSptModLoader: PreSptModLoader,
2023-03-03 15:23:46 +00:00
@inject("HttpServerHelper") protected httpServerHelper: HttpServerHelper,
@inject("InventoryHelper") protected inventoryHelper: InventoryHelper,
@inject("RandomUtil") protected randomUtil: RandomUtil,
2023-03-03 15:23:46 +00:00
@inject("HideoutHelper") protected hideoutHelper: HideoutHelper,
@inject("ProfileHelper") protected profileHelper: ProfileHelper,
@inject("ProfileFixerService") protected profileFixerService: ProfileFixerService,
@inject("LocalisationService") protected localisationService: LocalisationService,
@inject("PostDbLoadService") protected postDbLoadService: PostDbLoadService,
2023-03-03 15:23:46 +00:00
@inject("CustomLocationWaveService") protected customLocationWaveService: CustomLocationWaveService,
@inject("OpenZoneService") protected openZoneService: OpenZoneService,
@inject("SeasonalEventService") protected seasonalEventService: SeasonalEventService,
@inject("ItemBaseClassService") protected itemBaseClassService: ItemBaseClassService,
@inject("GiftService") protected giftService: GiftService,
@inject("RaidTimeAdjustmentService") protected raidTimeAdjustmentService: RaidTimeAdjustmentService,
@inject("ProfileActivityService") protected profileActivityService: ProfileActivityService,
2023-03-03 15:23:46 +00:00
@inject("ApplicationContext") protected applicationContext: ApplicationContext,
2023-11-15 20:35:05 -05:00
@inject("ConfigServer") protected configServer: ConfigServer,
@inject("PrimaryCloner") protected cloner: ICloner,
) {
2023-03-03 15:23:46 +00:00
this.httpConfig = this.configServer.getConfig(ConfigTypes.HTTP);
this.coreConfig = this.configServer.getConfig(ConfigTypes.CORE);
this.ragfairConfig = this.configServer.getConfig(ConfigTypes.RAGFAIR);
this.hideoutConfig = this.configServer.getConfig(ConfigTypes.HIDEOUT);
this.botConfig = this.configServer.getConfig(ConfigTypes.BOT);
}
public load(): void {
this.postDbLoadService.performPostDbLoadActions();
2023-03-03 15:23:46 +00:00
}
/**
* Handle client/game/start
*/
public gameStart(_url: string, _info: IEmptyRequestData, sessionID: string, startTimeStampMS: number): void {
// Store client start time in app context
this.applicationContext.addValue(
ContextVariableType.CLIENT_START_TIMESTAMP,
`${sessionID}_${startTimeStampMS}`,
);
this.profileActivityService.setActivityTimestamp(sessionID);
2023-11-10 16:49:29 -05:00
// repeatableQuests are stored by in profile.Quests due to the responses of the client (e.g. Quests in
// offraidData). Since we don't want to clutter the Quests list, we need to remove all completed (failed or
// successful) repeatable quests. We also have to remove the Counters from the repeatableQuests
if (sessionID) {
2023-03-03 15:23:46 +00:00
const fullProfile = this.profileHelper.getFullProfile(sessionID);
if (fullProfile.info.wipe) {
// Don't bother doing any fixes, we're resetting profile
return;
}
if (typeof fullProfile.spt.migrations === "undefined") {
fullProfile.spt.migrations = {};
}
//3.9 migrations
if (fullProfile.spt.version.includes("3.9.") && !fullProfile.spt.migrations["39x"]) {
// Check every item has a valid mongoid
this.inventoryHelper.validateInventoryUsesMonogoIds(fullProfile.characters.pmc.Inventory.items);
this.migrate39xProfile(fullProfile);
// Flag as migrated
fullProfile.spt.migrations["39x"] = this.timeUtil.getTimestamp();
this.logger.success(`Migration of 3.9.x profile: ${fullProfile.info.username} completed successfully`);
}
if (Array.isArray(fullProfile.characters.pmc.WishList)) {
2024-07-05 15:06:43 +01:00
fullProfile.characters.pmc.WishList = {};
}
if (Array.isArray(fullProfile.characters.scav.WishList)) {
2024-07-05 15:06:43 +01:00
fullProfile.characters.scav.WishList = {};
}
this.logger.debug(`Started game with sessionId: ${sessionID} ${fullProfile.info.username}`);
2023-03-03 15:23:46 +00:00
const pmcProfile = fullProfile.characters.pmc;
2023-05-30 13:58:02 +01:00
if (this.coreConfig.fixes.fixProfileBreakingInventoryItemIssues) {
this.profileFixerService.fixProfileBreakingInventoryItemIssues(pmcProfile);
}
if (pmcProfile.Health) {
2023-03-03 15:23:46 +00:00
this.updateProfileHealthValues(pmcProfile);
}
if (pmcProfile.Inventory) {
this.sendPraporGiftsToNewProfiles(pmcProfile);
this.profileFixerService.checkForOrphanedModdedItems(sessionID, fullProfile);
}
2023-03-03 15:23:46 +00:00
this.profileFixerService.checkForAndFixPmcProfileIssues(pmcProfile);
if (pmcProfile.Hideout) {
this.profileFixerService.addMissingHideoutBonusesToProfile(pmcProfile);
2023-03-03 15:23:46 +00:00
this.hideoutHelper.setHideoutImprovementsToCompleted(pmcProfile);
this.hideoutHelper.unlockHideoutWallInProfile(pmcProfile);
}
2023-11-15 20:35:05 -05:00
2023-03-03 15:23:46 +00:00
this.logProfileDetails(fullProfile);
this.saveActiveModsToProfile(fullProfile);
if (pmcProfile.Info) {
2023-03-03 15:23:46 +00:00
this.addPlayerToPMCNames(pmcProfile);
this.checkForAndRemoveUndefinedDialogs(fullProfile);
2023-03-03 15:23:46 +00:00
}
if (pmcProfile?.Skills?.Common) {
this.warnOnActiveBotReloadSkill(pmcProfile);
}
this.seasonalEventService.givePlayerSeasonalGifts(sessionID);
}
}
protected migrate39xProfile(fullProfile: ISptProfile) {
// Karma & Favorite items
if (typeof fullProfile.characters.pmc.karmaValue === "undefined") {
this.logger.warning("Migration: Added karma value of 0.2 to profile");
fullProfile.characters.pmc.karmaValue = 0.2;
// Reset the PMC's favorite items, as the previous data was incorrect.
this.logger.warning("Migration: Emptied out favoriteItems array on profile.");
fullProfile.characters.pmc.Inventory.favoriteItems = [];
}
// Remove wall debuffs
const wallAreaDb = this.databaseService
.getHideout()
.areas.find((area) => area.type === HideoutAreas.EMERGENCY_WALL);
this.hideoutHelper.removeHideoutWallBuffsAndDebuffs(wallAreaDb, fullProfile.characters.pmc);
// Equipment area
const equipmentArea = fullProfile.characters.pmc.Hideout.Areas.find(
(area) => area.type === HideoutAreas.EQUIPMENT_PRESETS_STAND,
);
if (!equipmentArea) {
this.logger.warning("Migration: Added equipment preset stand hideout area to profile, level 0");
fullProfile.characters.pmc.Hideout.Areas.push({
active: true,
completeTime: 0,
constructing: false,
lastRecipe: "",
level: 0,
passiveBonusesEnabled: true,
slots: [],
type: HideoutAreas.EQUIPMENT_PRESETS_STAND,
});
}
// Cultist circle area
const circleArea = fullProfile.characters.pmc.Hideout.Areas.find(
(area) => area.type === HideoutAreas.CIRCLE_OF_CULTISTS,
);
if (!circleArea) {
this.logger.warning("Migration: Added cultist circle hideout area to profile, level 0");
fullProfile.characters.pmc.Hideout.Areas.push({
active: true,
completeTime: 0,
constructing: false,
lastRecipe: "",
level: 0,
passiveBonusesEnabled: true,
slots: [],
type: HideoutAreas.CIRCLE_OF_CULTISTS,
});
}
2024-09-02 11:25:30 +01:00
// Hideout Improvement property changed name
if ((fullProfile.characters.pmc.Hideout as any).Improvement) {
fullProfile.characters.pmc.Hideout.Improvements = (fullProfile.characters.pmc.Hideout as any).Improvement;
delete (fullProfile.characters.pmc.Hideout as any).Improvement;
this.logger.warning(`Migration: Moved Hideout Improvement data to new property 'Improvements'`);
}
}
/**
* Handle client/game/config
*/
public getGameConfig(sessionID: string): IGameConfigResponse {
2023-07-24 16:38:28 +01:00
const profile = this.profileHelper.getPmcProfile(sessionID);
const gameTime =
profile.Stats?.Eft.OverallCounters.Items?.find(
(counter) => counter.Key.includes("LifeTime") && counter.Key.includes("Pmc"),
)?.Value ?? 0;
2023-07-24 16:38:28 +01:00
const config: IGameConfigResponse = {
languages: this.databaseService.getLocales().languages,
ndaFree: false,
reportAvailable: false,
twitchEventMember: false,
lang: "en",
aid: profile.aid,
taxonomy: 6,
activeProfileId: sessionID,
backend: {
Lobby: this.httpServerHelper.getBackendUrl(),
Trading: this.httpServerHelper.getBackendUrl(),
Messaging: this.httpServerHelper.getBackendUrl(),
Main: this.httpServerHelper.getBackendUrl(),
2023-11-15 20:35:05 -05:00
RagFair: this.httpServerHelper.getBackendUrl(),
},
useProtobuf: false,
utc_time: new Date().getTime() / 1000,
2023-12-21 22:12:55 +00:00
totalInGame: gameTime,
};
return config;
}
/**
* Handle client/game/mode
*/
public getGameMode(sessionID: string, info: IGameModeRequestData): any {
return { gameMode: ESessionMode.PVE, backendUrl: this.httpServerHelper.getBackendUrl() };
}
/**
* Handle client/server/list
*/
public getServer(sessionId: string): IServerDetails[] {
return [{ ip: this.httpConfig.backendIp, port: Number.parseInt(this.httpConfig.backendPort) }];
}
/**
* Handle client/match/group/current
*/
public getCurrentGroup(sessionId: string): ICurrentGroupResponse {
2023-11-15 20:35:05 -05:00
return { squad: [] };
}
/**
* Handle client/checkVersion
*/
public getValidGameVersion(sessionId: string): ICheckVersionResponse {
2023-11-15 20:35:05 -05:00
return { isvalid: true, latestVersion: this.coreConfig.compatibleTarkovVersion };
}
/**
* Handle client/game/keepalive
*/
public getKeepAlive(sessionId: string): IGameKeepAliveResponse {
this.profileActivityService.setActivityTimestamp(sessionId);
return { msg: "OK", utc_time: new Date().getTime() / 1000 };
}
/**
* Handle singleplayer/settings/getRaidTime
*/
public getRaidTime(sessionId: string, request: IGetRaidTimeRequest): IGetRaidTimeResponse {
// Set interval times to in-raid value
this.ragfairConfig.runIntervalSeconds = this.ragfairConfig.runIntervalValues.inRaid;
this.hideoutConfig.runIntervalSeconds = this.hideoutConfig.runIntervalValues.inRaid;
return this.raidTimeAdjustmentService.getRaidAdjustments(sessionId, request);
}
/**
* Players set botReload to a high value and don't expect the crazy fast reload speeds, give them a warn about it
* @param pmcProfile Player profile
*/
protected warnOnActiveBotReloadSkill(pmcProfile: IPmcData): void {
const botReloadSkill = this.profileHelper.getSkillFromProfile(pmcProfile, SkillTypes.BOT_RELOAD);
if (botReloadSkill?.Progress > 0) {
this.logger.warning(this.localisationService.getText("server_start_player_active_botreload_skill"));
2023-03-03 15:23:46 +00:00
}
}
/**
* When player logs in, iterate over all active effects and reduce timer
2024-02-03 19:47:39 +00:00
* @param pmcProfile Profile to adjust values for
2023-03-03 15:23:46 +00:00
*/
protected updateProfileHealthValues(pmcProfile: IPmcData): void {
2023-03-03 15:23:46 +00:00
const healthLastUpdated = pmcProfile.Health.UpdateTime;
const currentTimeStamp = this.timeUtil.getTimestamp();
const diffSeconds = currentTimeStamp - healthLastUpdated;
2024-02-03 19:47:39 +00:00
// Last update is in past
if (healthLastUpdated < currentTimeStamp) {
2023-03-03 15:23:46 +00:00
// Base values
let energyRegenPerHour = 60;
let hydrationRegenPerHour = 60;
let hpRegenPerHour = 456.6;
// Set new values, whatever is smallest
energyRegenPerHour += pmcProfile.Bonuses.filter(
(bonus) => bonus.type === BonusType.ENERGY_REGENERATION,
).reduce((sum, curr) => sum + (curr.value ?? 0), 0);
hydrationRegenPerHour += pmcProfile.Bonuses.filter(
(bonus) => bonus.type === BonusType.HYDRATION_REGENERATION,
).reduce((sum, curr) => sum + (curr.value ?? 0), 0);
hpRegenPerHour += pmcProfile.Bonuses.filter((bonus) => bonus.type === BonusType.HEALTH_REGENERATION).reduce(
(sum, curr) => sum + (curr.value ?? 0),
2023-11-15 20:35:05 -05:00
0,
);
2023-03-03 15:23:46 +00:00
// Player has energy deficit
if (pmcProfile.Health.Energy.Current !== pmcProfile.Health.Energy.Maximum) {
2023-03-03 15:23:46 +00:00
// Set new value, whatever is smallest
2023-11-15 20:35:05 -05:00
pmcProfile.Health.Energy.Current += Math.round(energyRegenPerHour * (diffSeconds / 3600));
if (pmcProfile.Health.Energy.Current > pmcProfile.Health.Energy.Maximum) {
2023-03-03 15:23:46 +00:00
pmcProfile.Health.Energy.Current = pmcProfile.Health.Energy.Maximum;
}
}
2024-02-03 19:47:39 +00:00
// Player has hydration deficit
if (pmcProfile.Health.Hydration.Current !== pmcProfile.Health.Hydration.Maximum) {
2023-11-15 20:35:05 -05:00
pmcProfile.Health.Hydration.Current += Math.round(hydrationRegenPerHour * (diffSeconds / 3600));
if (pmcProfile.Health.Hydration.Current > pmcProfile.Health.Hydration.Maximum) {
2023-03-03 15:23:46 +00:00
pmcProfile.Health.Hydration.Current = pmcProfile.Health.Hydration.Maximum;
}
}
// Check all body parts
for (const bodyPartKey in pmcProfile.Health.BodyParts) {
const bodyPart = pmcProfile.Health.BodyParts[bodyPartKey] as IBodyPartHealth;
2023-11-15 20:35:05 -05:00
2023-03-03 15:23:46 +00:00
// Check part hp
if (bodyPart.Health.Current < bodyPart.Health.Maximum) {
2023-11-15 20:35:05 -05:00
bodyPart.Health.Current += Math.round(hpRegenPerHour * (diffSeconds / 3600));
2023-03-03 15:23:46 +00:00
}
if (bodyPart.Health.Current > bodyPart.Health.Maximum) {
2023-03-03 15:23:46 +00:00
bodyPart.Health.Current = bodyPart.Health.Maximum;
}
2023-11-15 20:35:05 -05:00
2023-03-03 15:23:46 +00:00
// Look for effects
if (Object.keys(bodyPart.Effects ?? {}).length > 0) {
for (const effectKey in bodyPart.Effects) {
2024-02-03 19:47:39 +00:00
// remove effects below 1, .e.g. bleeds at -1
if (bodyPart.Effects[effectKey].Time < 1) {
// More than 30 mins has passed
if (diffSeconds > 1800) {
delete bodyPart.Effects[effectKey];
}
2023-03-03 15:23:46 +00:00
continue;
}
// Decrement effect time value by difference between current time and time health was last updated
2023-03-03 15:23:46 +00:00
bodyPart.Effects[effectKey].Time -= diffSeconds;
if (bodyPart.Effects[effectKey].Time < 1) {
2023-03-03 15:23:46 +00:00
// effect time was sub 1, set floor it can be
bodyPart.Effects[effectKey].Time = 1;
}
}
}
}
// Update both values as they've both been updated
2023-03-03 15:23:46 +00:00
pmcProfile.Health.UpdateTime = currentTimeStamp;
}
}
/**
* Send starting gifts to profile after x days
* @param pmcProfile Profile to add gifts to
*/
protected sendPraporGiftsToNewProfiles(pmcProfile: IPmcData): void {
const timeStampProfileCreated = pmcProfile.Info.RegistrationDate;
const oneDaySeconds = this.timeUtil.getHoursAsSeconds(24);
const currentTimeStamp = this.timeUtil.getTimestamp();
// One day post-profile creation
if (currentTimeStamp > timeStampProfileCreated + oneDaySeconds) {
this.giftService.sendPraporStartingGift(pmcProfile.sessionId, 1);
}
// Two day post-profile creation
if (currentTimeStamp > timeStampProfileCreated + oneDaySeconds * 2) {
this.giftService.sendPraporStartingGift(pmcProfile.sessionId, 2);
}
}
2023-03-03 15:23:46 +00:00
/**
* Get a list of installed mods and save their details to the profile being used
* @param fullProfile Profile to add mod details to
*/
protected saveActiveModsToProfile(fullProfile: ISptProfile): void {
2023-03-03 15:23:46 +00:00
// Add empty mod array if undefined
if (!fullProfile.spt.mods) {
fullProfile.spt.mods = [];
2023-03-03 15:23:46 +00:00
}
// Get active mods
const activeMods = this.preSptModLoader.getImportedModDetails();
for (const modKey in activeMods) {
2023-03-03 15:23:46 +00:00
const modDetails = activeMods[modKey];
2023-11-15 20:35:05 -05:00
if (
fullProfile.spt.mods.some(
(mod) =>
mod.author === modDetails.author &&
mod.name === modDetails.name &&
mod.version === modDetails.version,
2023-11-15 20:35:05 -05:00
)
) {
2023-03-03 15:23:46 +00:00
// Exists already, skip
continue;
}
fullProfile.spt.mods.push({
2023-03-03 15:23:46 +00:00
author: modDetails.author,
dateAdded: Date.now(),
name: modDetails.name,
2023-11-15 20:35:05 -05:00
version: modDetails.version,
url: modDetails.url,
2023-03-03 15:23:46 +00:00
});
}
}
/**
* Add the logged in players name to PMC name pool
* @param pmcProfile Profile of player to get name from
2023-03-03 15:23:46 +00:00
*/
protected addPlayerToPMCNames(pmcProfile: IPmcData): void {
2023-03-03 15:23:46 +00:00
const playerName = pmcProfile.Info.Nickname;
if (playerName) {
const bots = this.databaseService.getBots().types;
2023-03-03 15:23:46 +00:00
// Official names can only be 15 chars in length
if (playerName.length > this.botConfig.botNameLengthLimit) {
return;
}
// Skip if player name exists already
if (bots.bear?.firstName.some((x) => x === playerName)) {
return;
}
if (bots.bear) {
bots.bear.firstName.push(playerName);
2023-03-03 15:23:46 +00:00
}
2023-11-15 20:35:05 -05:00
if (bots.usec) {
bots.usec.firstName.push(playerName);
2023-11-15 20:35:05 -05:00
}
2023-03-03 15:23:46 +00:00
}
}
/**
* Check for a dialog with the key 'undefined', and remove it
* @param fullProfile Profile to check for dialog in
*/
protected checkForAndRemoveUndefinedDialogs(fullProfile: ISptProfile): void {
const undefinedDialog = fullProfile.dialogues.undefined;
if (undefinedDialog) {
delete fullProfile.dialogues.undefined;
}
}
protected logProfileDetails(fullProfile: ISptProfile): void {
this.logger.debug(`Profile made with: ${fullProfile.spt.version}`);
this.logger.debug(
`Server version: ${globalThis.G_SPTVERSION || this.coreConfig.sptVersion} ${globalThis.G_COMMIT}`,
);
2023-03-03 15:23:46 +00:00
this.logger.debug(`Debug enabled: ${globalThis.G_DEBUG_CONFIGURATION}`);
this.logger.debug(`Mods enabled: ${globalThis.G_MODS_ENABLED}`);
}
2023-11-15 20:35:05 -05:00
}