Server/project/src/routers/EventOutputHolder.ts

214 lines
8.5 KiB
TypeScript
Raw Normal View History

import { ProfileHelper } from "@spt/helpers/ProfileHelper";
import { IPmcData } from "@spt/models/eft/common/IPmcData";
import {
IHideoutImprovement,
IMoneyTransferLimits,
IProductive,
ITraderInfo,
} from "@spt/models/eft/common/tables/IBotBase";
import { IProfileChange, TraderData } from "@spt/models/eft/itemEvent/IItemEventRouterBase";
import { IItemEventRouterResponse } from "@spt/models/eft/itemEvent/IItemEventRouterResponse";
import { TimeUtil } from "@spt/utils/TimeUtil";
import { ICloner } from "@spt/utils/cloners/ICloner";
import { inject, injectable } from "tsyringe";
2023-03-03 16:23:46 +01:00
@injectable()
export class EventOutputHolder {
/**
* What has client been informed of this game session
* Key = sessionId, then second key is prod id
*/
protected clientActiveSessionStorage: Record<string, Record<string, { clientInformed: boolean }>> = {};
protected outputStore: Record<string, IItemEventRouterResponse> = {};
2023-03-03 16:23:46 +01:00
constructor(
@inject("ProfileHelper") protected profileHelper: ProfileHelper,
@inject("TimeUtil") protected timeUtil: TimeUtil,
@inject("PrimaryCloner") protected cloner: ICloner,
) {}
2023-03-03 16:23:46 +01:00
public getOutput(sessionID: string): IItemEventRouterResponse {
if (!this.outputStore[sessionID]?.profileChanges[sessionID]) {
2023-03-03 16:23:46 +01:00
this.resetOutput(sessionID);
}
return this.outputStore[sessionID];
2023-03-03 16:23:46 +01:00
}
/**
* Reset the response object to a default state
* Occurs prior to event being handled by server
* @param sessionID Players id
*/
public resetOutput(sessionID: string): void {
2023-03-03 16:23:46 +01:00
const pmcData: IPmcData = this.profileHelper.getPmcProfile(sessionID);
this.outputStore[sessionID] = { warnings: [], profileChanges: {} };
this.outputStore[sessionID].profileChanges[sessionID] = {
2023-03-03 16:23:46 +01:00
_id: sessionID,
experience: pmcData.Info.Experience,
quests: [],
ragFairOffers: [],
weaponBuilds: [],
equipmentBuilds: [],
items: { new: [], change: [], del: [] },
2023-03-03 16:23:46 +01:00
production: {},
improvements: {},
skills: { Common: [], Mastering: [], Points: 0 },
health: this.cloner.clone(pmcData.Health),
2023-03-03 16:23:46 +01:00
traderRelations: {},
// changedHideoutStashes: {},
2023-03-03 16:23:46 +01:00
recipeUnlocked: {},
questsStatus: [],
2023-03-03 16:23:46 +01:00
};
}
/**
* Update output object with most recent values from player profile
* @param sessionId Session id
*/
public updateOutputProperties(sessionId: string): void {
2023-03-03 16:23:46 +01:00
const pmcData: IPmcData = this.profileHelper.getPmcProfile(sessionId);
const profileChanges: IProfileChange = this.outputStore[sessionId].profileChanges[sessionId];
2023-03-03 16:23:46 +01:00
profileChanges.experience = pmcData.Info.Experience;
profileChanges.health = this.cloner.clone(pmcData.Health);
profileChanges.skills.Common = this.cloner.clone(pmcData.Skills.Common); // Always send skills for Item event route response
profileChanges.skills.Mastering = this.cloner.clone(pmcData.Skills.Mastering);
// Clone productions to ensure we preseve the profile jsons data
profileChanges.production = this.getProductionsFromProfileAndFlagComplete(
this.cloner.clone(pmcData.Hideout.Production),
sessionId,
);
profileChanges.improvements = this.cloner.clone(this.getImprovementsFromProfileAndFlagComplete(pmcData));
profileChanges.traderRelations = this.constructTraderRelations(pmcData.TradersInfo);
this.resetMoneyTransferLimit(pmcData.moneyTransferLimitData);
profileChanges.moneyTransferLimitData = pmcData.moneyTransferLimitData;
// Fixes container craft from water collector not resetting after collection + removed completed normal crafts
this.cleanUpCompleteCraftsInProfile(pmcData.Hideout.Production);
}
protected resetMoneyTransferLimit(limit: IMoneyTransferLimits) {
if (limit.nextResetTime < this.timeUtil.getTimestamp()) {
limit.nextResetTime += limit.resetInterval;
limit.remainingLimit = limit.totalLimit;
}
}
/**
* Convert the internal trader data object into an object we can send to the client
* @param traderData server data for traders
* @returns dict of trader id + TraderData
*/
protected constructTraderRelations(traderData: Record<string, ITraderInfo>): Record<string, TraderData> {
const result: Record<string, TraderData> = {};
for (const traderId in traderData) {
const baseData = traderData[traderId];
result[traderId] = {
salesSum: baseData.salesSum,
disabled: baseData.disabled,
loyalty: baseData.loyaltyLevel,
standing: baseData.standing,
unlocked: baseData.unlocked,
};
}
return result;
2023-03-03 16:23:46 +01:00
}
2023-03-03 16:23:46 +01:00
/**
* Return all hideout Improvements from player profile, adjust completed Improvements' completed property to be true
* @param pmcData Player profile
* @returns dictionary of hideout improvements
*/
protected getImprovementsFromProfileAndFlagComplete(pmcData: IPmcData): Record<string, IHideoutImprovement> {
for (const improvementKey in pmcData.Hideout.Improvements) {
const improvement = pmcData.Hideout.Improvements[improvementKey];
2023-03-03 16:23:46 +01:00
// Skip completed
if (improvement.completed) {
2023-03-03 16:23:46 +01:00
continue;
}
if (improvement.improveCompleteTimestamp < this.timeUtil.getTimestamp()) {
2023-03-03 16:23:46 +01:00
improvement.completed = true;
}
}
return pmcData.Hideout.Improvements;
2023-03-03 16:23:46 +01:00
}
/**
* Return productions from player profile except those completed crafts the client has already seen
2023-03-03 16:23:46 +01:00
* @param pmcData Player profile
* @returns dictionary of hideout productions
*/
protected getProductionsFromProfileAndFlagComplete(
productions: Record<string, IProductive>,
sessionId: string,
): Record<string, IProductive> | undefined {
for (const productionKey in productions) {
2023-03-03 16:23:46 +01:00
const production = productions[productionKey];
if (!production) {
// Could be cancelled production, skip item to save processing
continue;
}
// Complete and is Continuous e.g. water collector
if (production.sptIsComplete && production.sptIsContinuous) {
continue;
}
// Skip completed
if (!production.inProgress) {
2023-03-03 16:23:46 +01:00
continue;
}
// Client informed of craft, remove from data returned
let storageForSessionId = this.clientActiveSessionStorage[sessionId];
if (typeof storageForSessionId === "undefined") {
this.clientActiveSessionStorage[sessionId] = {};
storageForSessionId = this.clientActiveSessionStorage[sessionId];
}
// Ensure we don't inform client of production again
if (storageForSessionId[productionKey]?.clientInformed) {
2023-03-03 16:23:46 +01:00
delete productions[productionKey];
continue;
}
// Flag started craft as having been seen by client so it won't happen subsequent times
if (production.Progress > 0 && !storageForSessionId[productionKey]?.clientInformed) {
storageForSessionId[productionKey] = { clientInformed: true };
}
2023-03-03 16:23:46 +01:00
}
// Return undefined if there's no crafts to send to client to match live behaviour
return Object.keys(productions).length > 0 ? productions : undefined;
2023-03-03 16:23:46 +01:00
}
/**
* Required as continuous productions don't reset and stay at 100% completion but client thinks it hasn't started
* @param productions Productions in a profile
*/
protected cleanUpCompleteCraftsInProfile(productions: Record<string, IProductive>): void {
for (const productionKey in productions) {
const production = productions[productionKey];
if (production?.sptIsComplete && production?.sptIsContinuous) {
// Water collector / Bitcoin etc
production.sptIsComplete = false;
production.Progress = 0;
production.StartTimestamp = this.timeUtil.getTimestamp().toString();
} else if (!production?.inProgress) {
// Normal completed craft, delete
delete productions[productionKey];
}
}
}
2023-03-03 16:23:46 +01:00
}