mirror of
https://github.com/thepeacockproject/Peacock
synced 2025-03-27 11:12:44 +01:00

Fixed Freelancer to not have payout when the mission failed Fixed score screen from crashing when the current location has no mastery drops
864 lines
27 KiB
TypeScript
864 lines
27 KiB
TypeScript
/*
|
|
* The Peacock Project - a HITMAN server replacement.
|
|
* Copyright (C) 2021-2023 The Peacock Project Team
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
import { Router } from "express"
|
|
import {
|
|
ClientToServerEvent,
|
|
ContractProgressionData,
|
|
ContractSession,
|
|
GameVersion,
|
|
MissionManifestObjective,
|
|
PeacockCameraStatus,
|
|
PushMessage,
|
|
RatingKill,
|
|
RequestWithJwt,
|
|
S2CEventWithTimestamp,
|
|
Seconds,
|
|
ServerToClientEvent,
|
|
} from "./types/types"
|
|
import { contractTypes, extractToken, gameDifficulty, ServerVer } from "./utils"
|
|
import { json as jsonMiddleware } from "body-parser"
|
|
import { log, LogLevel } from "./loggingInterop"
|
|
import { getUserData, writeUserData } from "./databaseHandler"
|
|
import { controller } from "./controller"
|
|
import { swapToLocationStatus } from "./discordRp"
|
|
import { randomUUID } from "crypto"
|
|
import { liveSplitManager } from "./livesplit/liveSplitManager"
|
|
import { handleMultiplayerEvent } from "./multiplayer/multiplayerService"
|
|
import { handleEvent } from "@peacockproject/statemachine-parser"
|
|
import { encodePushMessage } from "./multiplayer/multiplayerUtils"
|
|
import {
|
|
ActorTaggedC2SEvent,
|
|
AmbientChangedC2SEvent,
|
|
BodyHiddenC2SEvent,
|
|
ContractStartC2SEvent,
|
|
Evergreen_Payout_DataC2SEvent,
|
|
HeroSpawn_LocationC2SEvent,
|
|
ItemDroppedC2SEvent,
|
|
ItemPickedUpC2SEvent,
|
|
KillC2SEvent,
|
|
MurderedBodySeenC2SEvent,
|
|
ObjectiveCompletedC2SEvent,
|
|
OpportunityEventsC2SEvent,
|
|
PacifyC2SEvent,
|
|
SecuritySystemRecorderC2SEvent,
|
|
SetpiecesC2SEvent,
|
|
SpottedC2SEvent,
|
|
WitnessesC2SEvent,
|
|
} from "./types/events"
|
|
import picocolors from "picocolors"
|
|
import { setCpd } from "./evergreen"
|
|
|
|
const eventRouter = Router()
|
|
|
|
// /authentication/api/userchannel/EventsService/
|
|
|
|
const eventQueue = new Map<string, S2CEventWithTimestamp[]>()
|
|
const pushMessageQueue = new Map<string, PushMessage[]>()
|
|
|
|
/**
|
|
* Enqueue a server to client push message.
|
|
* It will be sent back the next time the client calls `SaveAndSynchronizeEvents4`.
|
|
*
|
|
* @param userId The push message's target user.
|
|
* @param message The raw push message to send.
|
|
* @see enqueueEvent
|
|
* @author grappigegovert
|
|
*/
|
|
export function enqueuePushMessage(userId: string, message: unknown): void {
|
|
let userQueue
|
|
const time = process.hrtime.bigint()
|
|
|
|
if ((userQueue = pushMessageQueue.get(userId))) {
|
|
userQueue.push({
|
|
time,
|
|
message: encodePushMessage(time, message),
|
|
})
|
|
} else {
|
|
userQueue = [
|
|
{
|
|
time,
|
|
message: encodePushMessage(time, message),
|
|
},
|
|
]
|
|
pushMessageQueue.set(userId, userQueue)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Register a listener for an objective. Allows server-side tracking of the objective's state as events come in.
|
|
*
|
|
* @param session The contract session.
|
|
* @param objective The objective object.
|
|
* @author Reece Dunham
|
|
*/
|
|
export function registerObjectiveListener(
|
|
session: ContractSession,
|
|
objective: MissionManifestObjective,
|
|
): void {
|
|
if (!objective.Definition) {
|
|
return
|
|
}
|
|
|
|
let context = objective.Definition.Context || {}
|
|
let state = "Start"
|
|
|
|
session.objectiveDefinitions.set(objective.Id, objective.Definition)
|
|
|
|
const immediate = handleEvent(
|
|
// @ts-expect-error Type issue, needs to be corrected in sm-p.
|
|
objective.Definition,
|
|
context,
|
|
{},
|
|
{
|
|
eventName: "-",
|
|
currentState: state,
|
|
},
|
|
)
|
|
|
|
if (immediate.state) {
|
|
state = immediate.state
|
|
}
|
|
|
|
if (immediate.context) {
|
|
context = immediate.context
|
|
}
|
|
|
|
session.objectiveContexts.set(objective.Id, context)
|
|
session.objectiveStates.set(objective.Id, state)
|
|
}
|
|
|
|
/**
|
|
* Enqueue a server to client event.
|
|
* It will be sent back the next time the client calls `SaveAndSynchronizeEvents4`.
|
|
*
|
|
* @param userId The event's target user.
|
|
* @param event The event to send.
|
|
* @see enqueuePushMessage
|
|
* @author grappigegovert
|
|
*/
|
|
export function enqueueEvent(userId: string, event: ServerToClientEvent): void {
|
|
let userQueue: S2CEventWithTimestamp[] | undefined
|
|
const time = process.hrtime.bigint().toString()
|
|
event.CreatedAt = new Date().toISOString().slice(0, -1)
|
|
event.Token = time.toString()
|
|
|
|
if ((userQueue = eventQueue.get(userId))) {
|
|
userQueue.push({
|
|
time,
|
|
event,
|
|
})
|
|
} else {
|
|
userQueue = [
|
|
{
|
|
time,
|
|
event,
|
|
},
|
|
]
|
|
eventQueue.set(userId, userQueue)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Like the game's internal enum for EDeathContext.
|
|
*
|
|
* @see https://github.com/OrfeasZ/ZHMModSDK/blob/ba9512092a37d3b1f4de047bdd6acf15a7b9ac7c/ZHMModSDK/Include/Glacier/Enums.h#L6158
|
|
*/
|
|
export const enum EDeathContext {
|
|
eDC_UNDEFINED = 0,
|
|
eDC_NOT_HERO = 1,
|
|
eDC_HIDDEN = 2,
|
|
eDC_ACCIDENT = 3,
|
|
eDC_MURDER = 4,
|
|
}
|
|
|
|
export const contractSessions = new Map<string, ContractSession>()
|
|
const userIdToTempSession = new Map<string, string>()
|
|
|
|
/**
|
|
* Get the current state of an objective.
|
|
*
|
|
* @param sessionId The session ID.
|
|
* @param objectiveId The objective ID.
|
|
*/
|
|
export function getCurrentState(
|
|
sessionId: string,
|
|
objectiveId: string,
|
|
): string | undefined {
|
|
// Note: after the double-layered maps are merged into the session object, this should be rewritten.
|
|
const session = contractSessions.get(sessionId)
|
|
|
|
if (!session) {
|
|
return "Start"
|
|
}
|
|
|
|
return session.objectiveStates.get(objectiveId)
|
|
}
|
|
|
|
/**
|
|
* Creates a new contract session.
|
|
*
|
|
* @param sessionId The ID for the session.
|
|
* @param contractId The ID of the contract the session is for.
|
|
* @param userId The ID of the user playing the session.
|
|
* @param difficulty The difficulty of the game.
|
|
* @param gameVersion The game version.
|
|
* @param doScoring If true, this will be treated like a normal session. If false, this session will not be scored/put on the leaderboards. This should be false if we don't have full session details, e.g. if this is a save from the official servers loaded on Peacock.
|
|
*/
|
|
export function newSession(
|
|
sessionId: string,
|
|
contractId: string,
|
|
userId: string,
|
|
difficulty: number,
|
|
gameVersion: GameVersion,
|
|
doScoring = true,
|
|
): void {
|
|
const timestamp = new Date()
|
|
|
|
const contract = controller.resolveContract(contractId)
|
|
|
|
if (!contract) {
|
|
log(LogLevel.ERROR, `Failed to load ${contractId}`)
|
|
throw new Error("no ct")
|
|
}
|
|
|
|
if (difficulty === 0 && contractTypes.includes(contract.Metadata.Type)) {
|
|
log(
|
|
LogLevel.DEBUG,
|
|
`Difficulty not set for user created contract ${contractId}, setting to 2`,
|
|
)
|
|
difficulty = 2
|
|
}
|
|
|
|
swapToLocationStatus(
|
|
contract.Metadata.ScenePath,
|
|
contract.Metadata.Type,
|
|
contract.Data.Bricks || [],
|
|
)
|
|
|
|
contractSessions.set(sessionId, {
|
|
gameVersion,
|
|
sessionStart: timestamp,
|
|
lastUpdate: timestamp,
|
|
contractId,
|
|
userId: userId,
|
|
timerStart: 0,
|
|
timerEnd: 0,
|
|
duration: 0,
|
|
crowdNpcKills: 0,
|
|
targetKills: new Set(),
|
|
npcKills: new Set(),
|
|
bodiesHidden: new Set(),
|
|
pacifications: new Set(),
|
|
disguisesUsed: new Set(),
|
|
disguisesRuined: new Set(),
|
|
spottedBy: new Set(),
|
|
witnesses: new Set(),
|
|
bodiesFoundBy: new Set(),
|
|
legacyHasBodyBeenFound: false,
|
|
killsNoticedBy: new Set(),
|
|
completedObjectives: new Set(),
|
|
failedObjectives: new Set(),
|
|
recording: PeacockCameraStatus.NotSpotted,
|
|
lastAccident: 0,
|
|
lastKill: {},
|
|
kills: new Set(),
|
|
compat: doScoring,
|
|
markedTargets: new Set(),
|
|
currentDisguise: "4fc9396e-2619-4e66-a51e-2bd366230da7", // sig suit
|
|
difficulty,
|
|
objectiveContexts: new Map(),
|
|
objectiveStates: new Map(),
|
|
objectiveDefinitions: new Map(),
|
|
ghost: {
|
|
deaths: 0,
|
|
unnoticedKills: 0,
|
|
Opponents: [],
|
|
OpponentScore: 0,
|
|
Score: 0,
|
|
IsDraw: false,
|
|
IsWinner: false,
|
|
timerEnd: null,
|
|
},
|
|
challengeContexts: {},
|
|
})
|
|
userIdToTempSession.set(userId, sessionId)
|
|
|
|
controller.challengeService.startContract(
|
|
userId,
|
|
sessionId,
|
|
contractSessions.get(sessionId)!,
|
|
)
|
|
}
|
|
|
|
eventRouter.post(
|
|
"/SaveAndSynchronizeEvents4",
|
|
extractToken,
|
|
jsonMiddleware({ limit: "10Mb" }),
|
|
(
|
|
req: RequestWithJwt<
|
|
unknown,
|
|
{
|
|
lastPushDt: number | string
|
|
lastEventTicks: number | string
|
|
userId?: string
|
|
values?: []
|
|
}
|
|
>,
|
|
res,
|
|
) => {
|
|
if (req.body.userId !== req.jwt.unique_name) {
|
|
res.status(403).send() // Trying to save events for other user
|
|
return
|
|
}
|
|
|
|
if (!Array.isArray(req.body.values)) {
|
|
res.status(400).end() // malformed request
|
|
return
|
|
}
|
|
|
|
const savedTokens = req.body.values.length
|
|
? saveEvents(req.body.userId, req.body.values, req)
|
|
: null
|
|
|
|
let userQueue: S2CEventWithTimestamp[] | undefined
|
|
let newEvents: ServerToClientEvent[] | null = null
|
|
|
|
// events: (server -> client)
|
|
if ((userQueue = eventQueue.get(req.jwt.unique_name))) {
|
|
userQueue = userQueue.filter(
|
|
(item) => item.time > req.body.lastEventTicks,
|
|
)
|
|
eventQueue.set(req.jwt.unique_name, userQueue)
|
|
|
|
newEvents = Array.from(userQueue, (item) => item.event)
|
|
}
|
|
|
|
// push messages: (server -> client)
|
|
let userPushQueue: PushMessage[] | undefined
|
|
let pushMessages: string[] | null = null
|
|
|
|
if ((userPushQueue = pushMessageQueue.get(req.jwt.unique_name))) {
|
|
userPushQueue = userPushQueue.filter(
|
|
(item) => item.time > req.body.lastPushDt,
|
|
)
|
|
pushMessageQueue.set(req.jwt.unique_name, userPushQueue)
|
|
|
|
pushMessages = Array.from(userPushQueue, (item) => item.message)
|
|
}
|
|
|
|
res.json({
|
|
SavedTokens: savedTokens,
|
|
NewEvents: newEvents || null,
|
|
NextPoll: 10.0,
|
|
PushMessages: pushMessages || null,
|
|
})
|
|
},
|
|
)
|
|
|
|
eventRouter.post(
|
|
"/SaveEvents2",
|
|
extractToken,
|
|
jsonMiddleware({ limit: "10Mb" }),
|
|
(req: RequestWithJwt, res) => {
|
|
if (req.jwt.unique_name !== req.body.userId) {
|
|
res.status(403).send() // Trying to save events for other user
|
|
return
|
|
}
|
|
|
|
res.json(saveEvents(req.body.userId, req.body.values, req))
|
|
},
|
|
)
|
|
|
|
/**
|
|
* Gets the active session's ID for the specified user (by their ID).
|
|
*
|
|
* @param uId The user's ID.
|
|
* @returns The ID for the user's active session.
|
|
* @author Reece Dunham
|
|
*/
|
|
export function getActiveSessionIdForUser(uId: string): string | undefined {
|
|
return userIdToTempSession.get(uId)
|
|
}
|
|
|
|
/**
|
|
* Gets the active session for the specified user (by their ID).
|
|
*
|
|
* @param uId The user's ID.
|
|
* @returns The user's active contract session.
|
|
* @author Reece Dunham
|
|
*/
|
|
export function getSession(uId: string): ContractSession | undefined {
|
|
const currentSession = getActiveSessionIdForUser(uId)
|
|
|
|
if (!currentSession) {
|
|
return undefined
|
|
}
|
|
|
|
return contractSessions.get(currentSession)
|
|
}
|
|
|
|
function contractFailed(
|
|
event: ClientToServerEvent,
|
|
session: ContractSession,
|
|
): void {
|
|
session.markedTargets.clear()
|
|
|
|
const json = controller.resolveContract(session.contractId)!
|
|
|
|
let realName: string
|
|
|
|
if (json.Metadata.Type === "creation") {
|
|
realName =
|
|
event.Value === "Contract ended manually: OnRestartLevel"
|
|
? "GameRestart"
|
|
: `ContractFailed:${event.Value}`
|
|
} else {
|
|
realName = `ContractFailed:${event.Value}`
|
|
}
|
|
|
|
// if still in cutscene, end mission with 0 time pass -- this will get converted to minimum time within split manager
|
|
if (session.timerStart !== 0) {
|
|
// @ts-expect-error TypeScript still hates dates
|
|
const timeTotal: Seconds = session.timerEnd - session.timerStart
|
|
liveSplitManager.failMission(timeTotal)
|
|
} else {
|
|
liveSplitManager.failMission(0)
|
|
}
|
|
|
|
enqueueEvent(session.userId, {
|
|
CreatedAt: new Date().toISOString(),
|
|
Token: process.hrtime.bigint().toString(),
|
|
Id: randomUUID(),
|
|
Name: "SegmentClosing",
|
|
UserId: session.userId,
|
|
ContractId: session.contractId,
|
|
SessionId: null,
|
|
ContractSessionId: event.ContractSessionId,
|
|
Timestamp: 0.0,
|
|
Value: {
|
|
SegmentIndex: 0,
|
|
LastEventName: "ContractFailed",
|
|
LastEventTime: (session.lastUpdate as Date).toISOString(),
|
|
CloseType: realName,
|
|
},
|
|
Origin: "ContractSessionService",
|
|
Version: ServerVer,
|
|
IsReplicated: false,
|
|
})
|
|
}
|
|
|
|
function saveEvents(
|
|
userId: string,
|
|
events: ClientToServerEvent[],
|
|
req: RequestWithJwt<unknown, unknown>,
|
|
): string[] {
|
|
const response: string[] = []
|
|
const processed: string[] = []
|
|
const userData = getUserData(req.jwt.unique_name, req.gameVersion)
|
|
events.forEach((event) => {
|
|
let session = contractSessions.get(event.ContractSessionId)
|
|
|
|
if (!session) {
|
|
log(
|
|
LogLevel.WARN,
|
|
"Creating a fake session to avoid problems... scoring will not work!",
|
|
)
|
|
|
|
newSession(
|
|
event.ContractSessionId,
|
|
event.ContractId,
|
|
req.jwt.unique_name,
|
|
gameDifficulty.normal,
|
|
req.gameVersion,
|
|
false,
|
|
)
|
|
|
|
session = contractSessions.get(event.ContractSessionId)
|
|
}
|
|
|
|
if (
|
|
!session ||
|
|
session.contractId !== event.ContractId ||
|
|
session.userId !== userId
|
|
) {
|
|
if (PEACOCK_DEV) {
|
|
log(LogLevel.DEBUG, "No session or session user ID mismatch!")
|
|
console.debug(session)
|
|
console.debug(event)
|
|
}
|
|
|
|
return // session does not exist or contractid/userid doesn't match
|
|
}
|
|
|
|
session.duration = event.Timestamp
|
|
session.lastUpdate = new Date()
|
|
|
|
const contract = controller.resolveContract(session.contractId)
|
|
const contractType = contract?.Metadata?.Type?.toLowerCase()
|
|
|
|
// @ts-expect-error Issue with request type mismatch.
|
|
controller.hooks.newEvent.call(event, req, session)
|
|
|
|
for (const objectiveId of session.objectiveStates.keys()) {
|
|
try {
|
|
const objectiveDefinition =
|
|
session.objectiveDefinitions.get(objectiveId)
|
|
const objectiveState = session.objectiveStates.get(objectiveId)
|
|
const objectiveContext =
|
|
session.objectiveContexts.get(objectiveId)
|
|
|
|
const val = handleEvent(
|
|
objectiveDefinition as never,
|
|
objectiveContext,
|
|
event.Value,
|
|
{
|
|
eventName: event.Name,
|
|
currentState: objectiveState,
|
|
timestamp: event.Timestamp,
|
|
},
|
|
)
|
|
|
|
if (val.state === "Failure") {
|
|
if (PEACOCK_DEV && contractType !== "evergreen") {
|
|
log(LogLevel.DEBUG, `Objective failed: ${objectiveId}`)
|
|
}
|
|
|
|
session.failedObjectives.add(objectiveId)
|
|
}
|
|
|
|
if (val.context) {
|
|
session.objectiveContexts.set(objectiveId, val.context)
|
|
session.objectiveStates.set(objectiveId, val.state)
|
|
}
|
|
} catch (e) {
|
|
log(
|
|
LogLevel.ERROR,
|
|
"An error occurred while tracing C2S events, please report this!",
|
|
)
|
|
log(LogLevel.ERROR, e)
|
|
log(LogLevel.ERROR, e.stack)
|
|
}
|
|
}
|
|
|
|
controller.challengeService.onContractEvent(
|
|
event,
|
|
event.ContractSessionId,
|
|
session,
|
|
)
|
|
|
|
if (event.Name.startsWith("ScoringScreenEndState_")) {
|
|
session.evergreen.scoringScreenEndState = event.Name
|
|
|
|
processed.push(event.Name)
|
|
response.push(process.hrtime.bigint().toString())
|
|
|
|
return
|
|
}
|
|
|
|
// these events are important but may be fired after the timer is over
|
|
const canGetAfterTimerOver = [
|
|
"ContractEnd",
|
|
"ObjectiveCompleted",
|
|
"CpdSet",
|
|
"MissionFailed_Event",
|
|
]
|
|
|
|
if (
|
|
!canGetAfterTimerOver.includes(event.Name) &&
|
|
session.timerEnd !== 0 &&
|
|
event.Timestamp > session.timerEnd
|
|
) {
|
|
// Do not handle events that occur after exiting the level
|
|
response.push(process.hrtime.bigint().toString())
|
|
return
|
|
}
|
|
|
|
if (handleMultiplayerEvent(event, session)) {
|
|
processed.push(event.Name)
|
|
response.push(process.hrtime.bigint().toString())
|
|
|
|
return
|
|
}
|
|
|
|
switch (event.Name) {
|
|
case "HeroSpawn_Location":
|
|
liveSplitManager.missionIntentResolved(
|
|
event.ContractId,
|
|
(<HeroSpawn_LocationC2SEvent>event).Value.RepositoryId,
|
|
)
|
|
break
|
|
case "Kill": {
|
|
const killValue = (event as KillC2SEvent).Value
|
|
|
|
if (session.lastKill.timestamp === event.Timestamp) {
|
|
session.lastKill.repositoryIds?.push(killValue.RepositoryId)
|
|
} else {
|
|
session.lastKill = {
|
|
timestamp: event.Timestamp,
|
|
repositoryIds: [killValue.RepositoryId],
|
|
}
|
|
}
|
|
|
|
if (killValue.KillContext === EDeathContext.eDC_NOT_HERO) {
|
|
// this is not 47, so we keep silent assassin
|
|
log(
|
|
LogLevel.DEBUG,
|
|
`${killValue.RepositoryId} eliminated, 47 not responsible`,
|
|
)
|
|
response.push(process.hrtime.bigint().toString())
|
|
return
|
|
}
|
|
|
|
log(
|
|
LogLevel.DEBUG,
|
|
`Actor ${killValue.RepositoryId} eliminated.`,
|
|
)
|
|
|
|
if (killValue.IsTarget || contractType === "creation") {
|
|
const kill: RatingKill = {
|
|
KillClass: killValue.KillClass,
|
|
KillMethodBroad: killValue.KillMethodBroad,
|
|
KillItemCategory: killValue.KillItemCategory,
|
|
IsHeadshot: killValue.IsHeadshot,
|
|
KillMethodStrict: killValue.KillMethodStrict,
|
|
KillItemRepositoryId: killValue.KillItemRepositoryId,
|
|
_RepositoryId: killValue.RepositoryId,
|
|
OutfitRepoId: session.currentDisguise,
|
|
}
|
|
|
|
session.kills.add(kill)
|
|
|
|
session.targetKills.add(killValue.RepositoryId)
|
|
} else {
|
|
session.npcKills.add(killValue.RepositoryId)
|
|
}
|
|
break
|
|
}
|
|
case "CrowdNPC_Died":
|
|
session.crowdNpcKills += 1
|
|
break
|
|
case "Pacify":
|
|
session.pacifications.add(
|
|
(<PacifyC2SEvent>event).Value.RepositoryId,
|
|
)
|
|
break
|
|
case "BodyHidden":
|
|
session.bodiesHidden.add(
|
|
(<BodyHiddenC2SEvent>event).Value.RepositoryId,
|
|
)
|
|
break
|
|
case "BodyFound":
|
|
if (req.gameVersion === "h1") {
|
|
session.legacyHasBodyBeenFound = true
|
|
}
|
|
break
|
|
case "Disguise":
|
|
log(LogLevel.DEBUG, `Now disguised: ${event.Value as string}`)
|
|
session.currentDisguise = event.Value as string
|
|
session.disguisesUsed.add(event.Value as string)
|
|
break
|
|
case "ContractStart": {
|
|
const disguise = (<ContractStartC2SEvent>event).Value.Disguise
|
|
|
|
session.currentDisguise = disguise
|
|
session.disguisesUsed.add(disguise)
|
|
liveSplitManager.startMission(
|
|
session.contractId,
|
|
req.gameVersion,
|
|
req.jwt.unique_name,
|
|
)
|
|
break
|
|
}
|
|
case "DisguiseBlown":
|
|
session.disguisesRuined.add(event.Value as string)
|
|
break
|
|
case "BrokenDisguiseCleared":
|
|
session.disguisesRuined.delete(event.Value as string)
|
|
break
|
|
case "Spotted":
|
|
for (const actor of (event as SpottedC2SEvent).Value) {
|
|
session.spottedBy.add(actor)
|
|
}
|
|
break
|
|
case "Witnesses":
|
|
for (const actor of (event as WitnessesC2SEvent).Value) {
|
|
session.witnesses.add(actor)
|
|
}
|
|
break
|
|
case "SecuritySystemRecorder": {
|
|
const eventValue = (<SecuritySystemRecorderC2SEvent>event).Value
|
|
if (
|
|
eventValue.event === "spotted" &&
|
|
session.recording !== PeacockCameraStatus.Erased
|
|
) {
|
|
session.recording = PeacockCameraStatus.Spotted
|
|
} else if (
|
|
eventValue.event === "destroyed" ||
|
|
eventValue.event === "erased"
|
|
) {
|
|
session.recording = PeacockCameraStatus.Erased
|
|
}
|
|
break
|
|
}
|
|
case "IntroCutEnd":
|
|
if (!session.timerStart) {
|
|
session.timerStart = event.Timestamp
|
|
log(
|
|
LogLevel.DEBUG,
|
|
`Mission started at: ${session.timerStart}`,
|
|
)
|
|
}
|
|
break
|
|
case "exit_gate":
|
|
session.timerEnd = event.Timestamp
|
|
log(LogLevel.DEBUG, `Mission ended at: ${session.timerEnd}`)
|
|
break
|
|
case "ContractEnd":
|
|
if (!session.timerEnd) {
|
|
session.timerEnd = event.Timestamp
|
|
log(LogLevel.DEBUG, `Mission ended at: ${session.timerEnd}`)
|
|
}
|
|
break
|
|
case "ObjectiveCompleted":
|
|
session.completedObjectives.add(
|
|
(<ObjectiveCompletedC2SEvent>event).Value.Id,
|
|
)
|
|
break
|
|
case "AccidentBodyFound":
|
|
session.lastAccident = event.Timestamp
|
|
break
|
|
case "MurderedBodySeen":
|
|
if (
|
|
(event.Timestamp as unknown as number) !==
|
|
session.lastAccident
|
|
) {
|
|
session.bodiesFoundBy.add(
|
|
(<MurderedBodySeenC2SEvent>event).Value.Witness,
|
|
)
|
|
if (event.Timestamp === session.lastKill.timestamp) {
|
|
session.killsNoticedBy.add(
|
|
(<MurderedBodySeenC2SEvent>event).Value.Witness,
|
|
)
|
|
}
|
|
}
|
|
break
|
|
case "ActorTagged": {
|
|
const val = (<ActorTaggedC2SEvent>event).Value
|
|
|
|
if (!val.Tagged) {
|
|
session.markedTargets.delete(val.RepositoryId)
|
|
} else if (val.Tagged) {
|
|
session.markedTargets.add(val.RepositoryId)
|
|
}
|
|
break
|
|
}
|
|
case "StartingSuit":
|
|
session.currentDisguise = event.Value as string
|
|
break
|
|
case "ContractFailed":
|
|
session.timerEnd = event.Timestamp
|
|
contractFailed(event, session)
|
|
break
|
|
case "OpportunityEvents": {
|
|
const val = (<OpportunityEventsC2SEvent>event).Value
|
|
const opportunities = userData.Extensions.opportunityprogression
|
|
if (val.Event === "Completed") {
|
|
opportunities[val.RepositoryId] = true
|
|
}
|
|
writeUserData(req.jwt.unique_name, req.gameVersion)
|
|
break
|
|
}
|
|
// Evergreen
|
|
case "CpdSet":
|
|
setCpd(
|
|
event.Value as ContractProgressionData,
|
|
userId,
|
|
contract.Metadata.CpdId,
|
|
)
|
|
break
|
|
case "Evergreen_Payout_Data":
|
|
session.evergreen.payout = (<Evergreen_Payout_DataC2SEvent>(
|
|
event
|
|
)).Value.Total_Payout
|
|
break
|
|
case "MissionFailed_Event":
|
|
if (session.evergreen) {
|
|
session.evergreen.failed = true
|
|
}
|
|
break
|
|
// Sinkhole events we don't care about
|
|
case "ItemPickedUp":
|
|
log(
|
|
LogLevel.INFO,
|
|
`Picked up item with repository ID: ${
|
|
(<ItemPickedUpC2SEvent>event).Value.RepositoryId
|
|
}`,
|
|
)
|
|
break
|
|
case "setpieces":
|
|
log(
|
|
LogLevel.DEBUG,
|
|
`Setpiece: ${
|
|
(<SetpiecesC2SEvent>event).Value.RepositoryId
|
|
}`,
|
|
)
|
|
break
|
|
case "ItemDropped":
|
|
log(
|
|
LogLevel.DEBUG,
|
|
`Item dropped: ${
|
|
(<ItemDroppedC2SEvent>event).Value.RepositoryId
|
|
}`,
|
|
)
|
|
break
|
|
case "AmbientChanged":
|
|
log(
|
|
LogLevel.DEBUG,
|
|
`Ambient switched to ${
|
|
(<AmbientChangedC2SEvent>event).Value.AmbientValue
|
|
}`,
|
|
)
|
|
break
|
|
case "Hero_Health":
|
|
case "NPC_Distracted":
|
|
case "ShotsHit":
|
|
case "FirstNonHeadshot":
|
|
case "FirstMissedShot":
|
|
default:
|
|
// no-op on our part
|
|
break
|
|
}
|
|
|
|
processed.push(event.Name)
|
|
|
|
response.push(process.hrtime.bigint().toString())
|
|
})
|
|
|
|
if (PEACOCK_DEV && processed.length > 0) {
|
|
log(
|
|
LogLevel.DEBUG,
|
|
`Event summary: ${picocolors.gray(processed.join(", "))}`,
|
|
)
|
|
}
|
|
|
|
return response
|
|
}
|
|
|
|
export { eventRouter }
|