Server/project/src/controllers/MatchController.ts
2023-11-08 19:52:56 +00:00

324 lines
12 KiB
TypeScript

import { inject, injectable } from "tsyringe";
import { ApplicationContext } from "@spt-aki/context/ApplicationContext";
import { ContextVariableType } from "@spt-aki/context/ContextVariableType";
import { LootGenerator } from "@spt-aki/generators/LootGenerator";
import { ProfileHelper } from "@spt-aki/helpers/ProfileHelper";
import { TraderHelper } from "@spt-aki/helpers/TraderHelper";
import { IPmcData } from "@spt-aki/models/eft/common/IPmcData";
import { Item } from "@spt-aki/models/eft/common/tables/IItem";
import { ICreateGroupRequestData } from "@spt-aki/models/eft/match/ICreateGroupRequestData";
import { IEndOfflineRaidRequestData } from "@spt-aki/models/eft/match/IEndOfflineRaidRequestData";
import { IGetGroupStatusRequestData } from "@spt-aki/models/eft/match/IGetGroupStatusRequestData";
import { IGetProfileRequestData } from "@spt-aki/models/eft/match/IGetProfileRequestData";
import { IGetRaidConfigurationRequestData } from "@spt-aki/models/eft/match/IGetRaidConfigurationRequestData";
import { IJoinMatchRequestData } from "@spt-aki/models/eft/match/IJoinMatchRequestData";
import { IJoinMatchResult } from "@spt-aki/models/eft/match/IJoinMatchResult";
import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes";
import { MessageType } from "@spt-aki/models/enums/MessageType";
import { Traders } from "@spt-aki/models/enums/Traders";
import { IInRaidConfig } from "@spt-aki/models/spt/config/IInRaidConfig";
import { IMatchConfig } from "@spt-aki/models/spt/config/IMatchConfig";
import { IPmcConfig } from "@spt-aki/models/spt/config/IPmcConfig";
import { ITraderConfig } from "@spt-aki/models/spt/config/ITraderConfig";
import { ILogger } from "@spt-aki/models/spt/utils/ILogger";
import { ConfigServer } from "@spt-aki/servers/ConfigServer";
import { SaveServer } from "@spt-aki/servers/SaveServer";
import { BotGenerationCacheService } from "@spt-aki/services/BotGenerationCacheService";
import { BotLootCacheService } from "@spt-aki/services/BotLootCacheService";
import { MailSendService } from "@spt-aki/services/MailSendService";
import { MatchLocationService } from "@spt-aki/services/MatchLocationService";
import { ProfileSnapshotService } from "@spt-aki/services/ProfileSnapshotService";
import { HashUtil } from "@spt-aki/utils/HashUtil";
import { RandomUtil } from "@spt-aki/utils/RandomUtil";
import { TimeUtil } from "@spt-aki/utils/TimeUtil";
@injectable()
export class MatchController
{
protected matchConfig: IMatchConfig;
protected inraidConfig: IInRaidConfig;
protected traderConfig: ITraderConfig;
protected pmcConfig: IPmcConfig;
constructor(
@inject("WinstonLogger") protected logger: ILogger,
@inject("SaveServer") protected saveServer: SaveServer,
@inject("TimeUtil") protected timeUtil: TimeUtil,
@inject("RandomUtil") protected randomUtil: RandomUtil,
@inject("HashUtil") protected hashUtil: HashUtil,
@inject("ProfileHelper") protected profileHelper: ProfileHelper,
@inject("MatchLocationService") protected matchLocationService: MatchLocationService,
@inject("TraderHelper") protected traderHelper: TraderHelper,
@inject("BotLootCacheService") protected botLootCacheService: BotLootCacheService,
@inject("ConfigServer") protected configServer: ConfigServer,
@inject("ProfileSnapshotService") protected profileSnapshotService: ProfileSnapshotService,
@inject("BotGenerationCacheService") protected botGenerationCacheService: BotGenerationCacheService,
@inject("MailSendService") protected mailSendService: MailSendService,
@inject("LootGenerator") protected lootGenerator: LootGenerator,
@inject("ApplicationContext") protected applicationContext: ApplicationContext
)
{
this.matchConfig = this.configServer.getConfig(ConfigTypes.MATCH);
this.inraidConfig = this.configServer.getConfig(ConfigTypes.IN_RAID);
this.traderConfig = this.configServer.getConfig(ConfigTypes.TRADER);
this.pmcConfig = this.configServer.getConfig(ConfigTypes.PMC);
}
public getEnabled(): boolean
{
return this.matchConfig.enabled;
}
/** Handle raid/profile/list */
public getProfile(info: IGetProfileRequestData): IPmcData[]
{
if (info.profileId.includes("pmcAID"))
{
return this.profileHelper.getCompleteProfile(info.profileId.replace("pmcAID", "AID"));
}
if (info.profileId.includes("scavAID"))
{
return this.profileHelper.getCompleteProfile(info.profileId.replace("scavAID", "AID"));
}
return [];
}
/** Handle client/match/group/create */
public createGroup(sessionID: string, info: ICreateGroupRequestData): any
{
return this.matchLocationService.createGroup(sessionID, info);
}
/** Handle client/match/group/delete */
public deleteGroup(info: any): void
{
this.matchLocationService.deleteGroup(info);
}
/** Handle match/group/start_game */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public joinMatch(info: IJoinMatchRequestData, sessionId: string): IJoinMatchResult
{
const output: IJoinMatchResult = {
maxPveCountExceeded: false,
profiles: []
};
// get list of players joining into the match
output.profiles.push({
profileid: "TODO",
profileToken: "TODO",
status: "MatchWait",
sid: "",
ip: "",
port: 0,
version: "live",
location: "TODO get location",
raidMode: "Online",
mode: "deathmatch",
shortid: null,
// eslint-disable-next-line @typescript-eslint/naming-convention
additional_info: null
});
return output;
}
/** Handle client/match/group/status */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public getGroupStatus(info: IGetGroupStatusRequestData): any
{
return {
players: [],
maxPveCountExceeded: false
};
}
/**
* Handle /client/raid/configuration
* @param request Raid config request
* @param sessionID Session id
*/
public startOfflineRaid(request: IGetRaidConfigurationRequestData, sessionID: string): void
{
// Store request data for access during bot generation
this.applicationContext.addValue(ContextVariableType.RAID_CONFIGURATION, request);
//TODO: add code to strip PMC of equipment now they've started the raid
// Set pmcs to difficulty set in pre-raid screen if override in bot config isnt enabled
if (!this.pmcConfig.useDifficultyOverride)
{
this.pmcConfig.difficulty = this.convertDifficultyDropdownIntoBotDifficulty(request.wavesSettings.botDifficulty);
}
// Store the profile as-is for later use on the post-raid exp screen
const currentProfile = this.saveServer.getProfile(sessionID);
this.profileSnapshotService.storeProfileSnapshot(sessionID, currentProfile);
}
/**
* Convert a difficulty value from pre-raid screen to a bot difficulty
* @param botDifficulty dropdown difficulty value
* @returns bot difficulty
*/
protected convertDifficultyDropdownIntoBotDifficulty(botDifficulty: string): string
{
// Edge case medium - must be altered
if (botDifficulty.toLowerCase() === "medium")
{
return "normal";
}
return botDifficulty;
}
/** Handle client/match/offline/end */
public endOfflineRaid(info: IEndOfflineRaidRequestData, sessionId: string): void
{
const pmcData: IPmcData = this.profileHelper.getPmcProfile(sessionId);
const extractName = info.exitName;
// Save time spent in raid
pmcData.Stats.Eft.TotalInGameTime += info.raidSeconds;
// Clean up cached bots now raid is over
this.botGenerationCacheService.clearStoredBots();
// Clear bot loot cache
this.botLootCacheService.clearCache();
if (this.extractWasViaCar(extractName))
{
this.handleCarExtract(extractName, pmcData, sessionId);
}
if (extractName && this.extractWasViaCoop(extractName) && this.traderConfig.fence.coopExtractGift.sendGift)
{
this.sendCoopTakenFenceMessage(sessionId);
}
}
/**
* Did player take a COOP extract
* @param extractName Name of extract player took
* @returns True if coop extract
*/
protected extractWasViaCoop(extractName: string): boolean
{
// No extract name, not a coop extract
if (!extractName)
{
return false;
}
return (this.inraidConfig.coopExtracts.includes(extractName.trim()));
}
protected sendCoopTakenFenceMessage(sessionId: string): void
{
// Generate reward for taking coop extract
const loot = this.lootGenerator.createRandomLoot(this.traderConfig.fence.coopExtractGift);
const mailableLoot: Item[] = [];
const parentId = this.hashUtil.generate();
for (const item of loot)
{
mailableLoot.push(
{
_id: item.id,
_tpl: item.tpl,
slotId: "main",
parentId: parentId,
upd: {
StackObjectsCount: item.stackCount,
SpawnedInSession: true
}
}
);
}
// Send message from fence giving player reward generated above
this.mailSendService.sendLocalisedNpcMessageToPlayer(
sessionId,
this.traderHelper.getTraderById(Traders.FENCE),
MessageType.MESSAGE_WITH_ITEMS,
this.randomUtil.getArrayValue(this.traderConfig.fence.coopExtractGift.messageLocaleIds),
mailableLoot,
this.timeUtil.getHoursAsSeconds(this.traderConfig.fence.coopExtractGift.giftExpiryHours)
);
}
/**
* Was extract by car
* @param extractName name of extract
* @returns true if car extract
*/
protected extractWasViaCar(extractName: string): boolean
{
// exit name is null on death
if (!extractName)
{
return false;
}
if (extractName.toLowerCase().includes("v-ex"))
{
return true;
}
return this.inraidConfig.carExtracts.includes(extractName.trim());
}
/**
* Handle when a player extracts using a car - Add rep to fence
* @param extractName name of the extract used
* @param pmcData Player profile
* @param sessionId Session id
*/
protected handleCarExtract(extractName: string, pmcData: IPmcData, sessionId: string): void
{
// Ensure key exists for extract
if (!(extractName in pmcData.CarExtractCounts))
{
pmcData.CarExtractCounts[extractName] = 0;
}
// Increment extract count value
pmcData.CarExtractCounts[extractName] += 1;
const fenceId: string = Traders.FENCE;
this.updateFenceStandingInProfile(pmcData, fenceId, extractName);
this.traderHelper.lvlUp(fenceId, pmcData);
pmcData.TradersInfo[fenceId].loyaltyLevel = Math.max(pmcData.TradersInfo[fenceId].loyaltyLevel, 1);
this.logger.debug(`Car extract: ${extractName} used, total times taken: ${pmcData.CarExtractCounts[extractName]}`);
}
/**
* Update players fence trader standing value in profile
* @param pmcData Player profile
* @param fenceId Id of fence trader
* @param extractName Name of extract used
*/
protected updateFenceStandingInProfile(pmcData: IPmcData, fenceId: string, extractName: string): void
{
let fenceStanding = Number(pmcData.TradersInfo[fenceId].standing);
// Not exact replica of Live behaviour
// Simplified for now, no real reason to do the whole (unconfirmed) extra 0.01 standing per day regeneration mechanic
const baseGain: number = this.inraidConfig.carExtractBaseStandingGain;
const extractCount: number = pmcData.CarExtractCounts[extractName];
fenceStanding += Math.max(baseGain / extractCount, 0.01);
// Ensure fence loyalty level is not above/below the range -7 - 15
const newFenceStanding = Math.min(Math.max(fenceStanding, -7), 15);
this.logger.debug(`Old vs new fence standing: ${pmcData.TradersInfo[fenceId].standing}, ${newFenceStanding}`);
pmcData.TradersInfo[fenceId].standing = newFenceStanding;
}
}