mirror of
https://github.com/thepeacockproject/Peacock
synced 2024-11-16 11:03:30 +01:00
Separate routing logic and business logic (#352)
This commit is contained in:
parent
abd9ada63b
commit
0e66d69505
@ -2,6 +2,7 @@
|
||||
<dictionary name="reece">
|
||||
<words>
|
||||
<w>ascensionist</w>
|
||||
<w>atlantide</w>
|
||||
<w>bosco</w>
|
||||
<w>calluna</w>
|
||||
<w>cereus</w>
|
||||
|
@ -1,43 +0,0 @@
|
||||
/*
|
||||
* 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 { json as jsonMiddleware } from "body-parser"
|
||||
|
||||
const legacyEventRouter = Router()
|
||||
|
||||
legacyEventRouter.post(
|
||||
"/SaveAndSynchronizeEvents3",
|
||||
jsonMiddleware({ limit: "10Mb" }),
|
||||
(req, res, next) => {
|
||||
// call /SaveAndSynchronizeEvents4 but add/remove dummy pushMessages
|
||||
req.url = "/SaveAndSynchronizeEvents4"
|
||||
req.body.lastPushDt = "0"
|
||||
|
||||
const originalJsonFunc = res.json
|
||||
|
||||
res.json = function (originalData) {
|
||||
delete originalData.PushMessages
|
||||
return originalJsonFunc.call(this, originalData)
|
||||
}
|
||||
|
||||
next()
|
||||
},
|
||||
)
|
||||
|
||||
export { legacyEventRouter }
|
@ -19,227 +19,13 @@
|
||||
import { Router } from "express"
|
||||
import { RequestWithJwt } from "../types/types"
|
||||
import { getConfig } from "../configSwizzleManager"
|
||||
import { getDefaultSuitFor, uuidRegex } from "../utils"
|
||||
import { json as jsonMiddleware } from "body-parser"
|
||||
import { controller } from "../controller"
|
||||
import {
|
||||
generateUserCentric,
|
||||
getParentLocationByName,
|
||||
getSubLocationByName,
|
||||
} from "../contracts/dataGen"
|
||||
import { getUserData } from "../databaseHandler"
|
||||
import { log, LogLevel } from "../loggingInterop"
|
||||
import { createInventory, getUnlockableById } from "../inventory"
|
||||
import { getFlag } from "../flags"
|
||||
import { loadouts } from "../loadouts"
|
||||
import { StashpointQueryH2016, StashpointSlotName } from "../types/gameSchemas"
|
||||
import { getParentLocationByName } from "../contracts/dataGen"
|
||||
|
||||
const legacyMenuDataRouter = Router()
|
||||
|
||||
legacyMenuDataRouter.get(
|
||||
"/stashpoint",
|
||||
(req: RequestWithJwt<StashpointQueryH2016>, res) => {
|
||||
if (!uuidRegex.test(req.query.contractid)) {
|
||||
res.status(400).send("contract id was not a uuid")
|
||||
return
|
||||
}
|
||||
|
||||
if (typeof req.query.slotname !== "string") {
|
||||
res.status(400).send("invalid slot data")
|
||||
return
|
||||
}
|
||||
|
||||
const contractData = controller.resolveContract(req.query.contractid)
|
||||
|
||||
if (!contractData) {
|
||||
res.status(404).send("contract not found")
|
||||
return
|
||||
}
|
||||
|
||||
const loadoutSlots: StashpointSlotName[] = [
|
||||
"carriedweapon",
|
||||
"carrieditem",
|
||||
"concealedweapon",
|
||||
"disguise",
|
||||
"gear",
|
||||
"gear",
|
||||
"stashpoint",
|
||||
]
|
||||
|
||||
if (loadoutSlots.includes(req.query.slotname.slice(0, -1))) {
|
||||
req.query.slotid = req.query.slotname.slice(0, -1)
|
||||
} else {
|
||||
log(
|
||||
LogLevel.ERROR,
|
||||
`Unknown slotname in legacy stashpoint: ${req.query.slotname}`,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const userProfile = getUserData(req.jwt.unique_name, req.gameVersion)
|
||||
|
||||
const sublocation = getSubLocationByName(
|
||||
contractData.Metadata.Location,
|
||||
req.gameVersion,
|
||||
)
|
||||
|
||||
const inventory = createInventory(
|
||||
req.jwt.unique_name,
|
||||
req.gameVersion,
|
||||
sublocation,
|
||||
)
|
||||
|
||||
const userCentricContract = generateUserCentric(
|
||||
contractData,
|
||||
req.jwt.unique_name,
|
||||
"h1",
|
||||
)
|
||||
|
||||
const defaultLoadout = {
|
||||
2: "FIREARMS_HERO_PISTOL_TACTICAL_001_SU_SKIN01",
|
||||
3: getDefaultSuitFor(sublocation),
|
||||
4: "TOKEN_FIBERWIRE",
|
||||
5: "PROP_TOOL_COIN",
|
||||
}
|
||||
|
||||
const getLoadoutItem = (id: number) => {
|
||||
if (getFlag("loadoutSaving") === "LEGACY") {
|
||||
const dl = userProfile.Extensions.defaultloadout
|
||||
|
||||
if (!dl) {
|
||||
return defaultLoadout[id]
|
||||
}
|
||||
|
||||
const forLocation = (userProfile.Extensions.defaultloadout ||
|
||||
{})[sublocation?.Properties?.ParentLocation]
|
||||
|
||||
if (!forLocation) {
|
||||
return defaultLoadout[id]
|
||||
}
|
||||
|
||||
return forLocation[id]
|
||||
} else {
|
||||
let dl = loadouts.getLoadoutFor("h1")
|
||||
|
||||
if (!dl) {
|
||||
dl = loadouts.createDefault("h1")
|
||||
}
|
||||
|
||||
const forLocation =
|
||||
dl.data[sublocation?.Properties?.ParentLocation]
|
||||
|
||||
if (!forLocation) {
|
||||
return defaultLoadout[id]
|
||||
}
|
||||
|
||||
return forLocation[id]
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
template: getConfig("LegacyStashpointTemplate", false),
|
||||
data: {
|
||||
ContractId: req.query.contractid,
|
||||
// the game actually only needs the loadoutdata from the requested slotid, but this is what IOI servers do
|
||||
LoadoutData: [...loadoutSlots.entries()].map(
|
||||
([slotid, slotname]) => ({
|
||||
SlotName: slotname,
|
||||
SlotId: slotid.toString(),
|
||||
Items: inventory
|
||||
.filter((item) => {
|
||||
return (
|
||||
item.Unlockable.Properties.LoadoutSlot && // only display items
|
||||
(item.Unlockable.Properties.LoadoutSlot ===
|
||||
slotname || // display items for requested slot
|
||||
(slotname === "stashpoint" && // else: if stashpoint
|
||||
item.Unlockable.Properties
|
||||
.LoadoutSlot !== "disguise")) && // => display all non-disguise items
|
||||
(req.query.allowlargeitems === "true" ||
|
||||
item.Unlockable.Properties.ItemSize === // regular gear slot or hidden stash => small item
|
||||
"ITEMSIZE_SMALL" ||
|
||||
(!item.Unlockable.Properties.ItemSize &&
|
||||
item.Unlockable.Properties
|
||||
.LoadoutSlot !== // use old logic if itemsize is not set
|
||||
"carriedweapon")) &&
|
||||
item.Unlockable.Type !==
|
||||
"challengemultipler" &&
|
||||
!item.Unlockable.Properties.InclusionData
|
||||
) // not sure about this one
|
||||
})
|
||||
.map((item) => ({
|
||||
Item: item,
|
||||
ItemDetails: {
|
||||
Capabilities: [],
|
||||
StatList: item.Unlockable.Properties
|
||||
.Gameplay
|
||||
? Object.entries(
|
||||
item.Unlockable.Properties
|
||||
.Gameplay,
|
||||
).map(([key, value]) => ({
|
||||
Name: key,
|
||||
Ratio: value,
|
||||
}))
|
||||
: [],
|
||||
PropertyTexts: [],
|
||||
},
|
||||
SlotId: slotid.toString(),
|
||||
SlotName: slotname,
|
||||
})),
|
||||
Page: 0,
|
||||
Recommended: getLoadoutItem(slotid)
|
||||
? {
|
||||
item: getUnlockableById(
|
||||
getLoadoutItem(slotid),
|
||||
req.gameVersion,
|
||||
),
|
||||
type: loadoutSlots[slotid],
|
||||
owned: true,
|
||||
}
|
||||
: null,
|
||||
HasMore: false,
|
||||
HasMoreLeft: false,
|
||||
HasMoreRight: false,
|
||||
OptionalData:
|
||||
slotid === 6
|
||||
? {
|
||||
stashpoint: req.query.stashpoint,
|
||||
AllowLargeItems:
|
||||
req.query.allowlargeitems ||
|
||||
!req.query.stashpoint,
|
||||
}
|
||||
: {},
|
||||
}),
|
||||
),
|
||||
Contract: userCentricContract.Contract,
|
||||
ShowSlotName: req.query.slotname,
|
||||
UserCentric: userCentricContract,
|
||||
},
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
legacyMenuDataRouter.get("/Safehouse", (req: RequestWithJwt, res, next) => {
|
||||
const template = getConfig("LegacySafehouseTemplate", false)
|
||||
|
||||
// call /SafehouseCategory but rewrite the result a bit
|
||||
req.url = `/SafehouseCategory?page=0&type=${req.query.type}&subtype=`
|
||||
const originalJsonFunc = res.json
|
||||
|
||||
res.json = function json(originalData) {
|
||||
return originalJsonFunc.call(this, {
|
||||
template,
|
||||
data: {
|
||||
SafehouseData: originalData.data,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
legacyMenuDataRouter.get(
|
||||
"/debriefingchallenges",
|
||||
jsonMiddleware(),
|
||||
(
|
||||
req: RequestWithJwt<{ contractSessionId: string; contractId: string }>,
|
||||
res,
|
||||
@ -269,7 +55,6 @@ legacyMenuDataRouter.get(
|
||||
|
||||
legacyMenuDataRouter.get(
|
||||
"/MasteryLocation",
|
||||
jsonMiddleware(),
|
||||
(req: RequestWithJwt<{ locationId: string; difficulty: string }>, res) => {
|
||||
const masteryData =
|
||||
controller.masteryService.getMasteryDataForDestination(
|
||||
|
@ -16,13 +16,10 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import serveStatic from "serve-static"
|
||||
import { Router } from "express"
|
||||
import { join } from "path"
|
||||
import md5File from "md5-file"
|
||||
import { readFile } from "atomically"
|
||||
import { imageFetchingMiddleware } from "../menus/imageHandler"
|
||||
import { MenuSystemDatabase } from "../menus/menuSystem"
|
||||
|
||||
const legacyMenuSystemRouter = Router()
|
||||
|
||||
@ -46,12 +43,4 @@ legacyMenuSystemRouter.get(
|
||||
},
|
||||
)
|
||||
|
||||
legacyMenuSystemRouter.use(MenuSystemDatabase.configMiddleware)
|
||||
|
||||
legacyMenuSystemRouter.use(
|
||||
"/images/",
|
||||
serveStatic("images", { fallthrough: true }),
|
||||
imageFetchingMiddleware,
|
||||
)
|
||||
|
||||
export { legacyMenuSystemRouter }
|
||||
|
@ -26,10 +26,8 @@ import { getConfig } from "../configSwizzleManager"
|
||||
|
||||
import { Router } from "express"
|
||||
import { controller } from "../controller"
|
||||
import { getPlatformEntitlements } from "../platformEntitlements"
|
||||
import { json as jsonMiddleware } from "body-parser"
|
||||
import { uuidRegex } from "../utils"
|
||||
import { menuSystemDatabase } from "../menus/menuSystem"
|
||||
import { compileRuntimeChallenge } from "../candle/challengeHelpers"
|
||||
import { LegacyGetProgressionBody } from "../types/gameSchemas"
|
||||
|
||||
@ -37,23 +35,6 @@ const legacyProfileRouter = Router()
|
||||
|
||||
// /authentication/api/userchannel/
|
||||
|
||||
legacyProfileRouter.post(
|
||||
"/ProfileService/GetPlatformEntitlements",
|
||||
jsonMiddleware(),
|
||||
getPlatformEntitlements,
|
||||
)
|
||||
|
||||
legacyProfileRouter.post(
|
||||
"/AuthenticationService/GetBlobOfflineCacheDatabaseDiff",
|
||||
(req: RequestWithJwt, res) => {
|
||||
const configs: string[] = []
|
||||
|
||||
menuSystemDatabase.hooks.getDatabaseDiff.call(configs, req.gameVersion)
|
||||
|
||||
res.json(configs)
|
||||
},
|
||||
)
|
||||
|
||||
legacyProfileRouter.post(
|
||||
"/ChallengesService/GetActiveChallenges",
|
||||
jsonMiddleware(),
|
||||
|
@ -28,6 +28,7 @@ import { SavedChallengeGroup } from "../types/challenges"
|
||||
import { controller } from "../controller"
|
||||
import { gameDifficulty, isSniperLocation } from "../utils"
|
||||
|
||||
// TODO: unused?
|
||||
export function compileScoringChallenge(
|
||||
challenge: RegistryChallenge,
|
||||
): CompiledChallengeRewardData {
|
||||
@ -68,7 +69,7 @@ export function compileRuntimeChallenge(
|
||||
}
|
||||
|
||||
export enum ChallengeFilterType {
|
||||
// Note that this option will include global elusives and escalations challenges.
|
||||
/** Note that this option will include global elusive and escalations challenges. */
|
||||
None = "None",
|
||||
Contract = "Contract",
|
||||
/** Only used for the CAREER -> CHALLENGES page */
|
||||
|
@ -150,6 +150,8 @@ export class MasteryService {
|
||||
)[0]
|
||||
}
|
||||
|
||||
// TODO: what do we want to do with this? We should prob remove the template part
|
||||
// to make this like the other routes, and more testable.
|
||||
getMasteryDataForLocation(
|
||||
locationId: string,
|
||||
gameVersion: GameVersion,
|
||||
|
@ -280,7 +280,7 @@ export function getVersionedConfig<T = unknown>(
|
||||
if (
|
||||
// is this scpc, do we have a scpc config?
|
||||
gameVersion === "scpc" &&
|
||||
Object.prototype.hasOwnProperty.call(configs, `Scpc${config}`)
|
||||
Object.hasOwn(configs, `Scpc${config}`)
|
||||
) {
|
||||
h1Prefix = "Scpc"
|
||||
} else {
|
||||
@ -291,10 +291,7 @@ export function getVersionedConfig<T = unknown>(
|
||||
}
|
||||
|
||||
// if this is H2, but we don't have a h2 specific config, fall back to h3
|
||||
if (
|
||||
gameVersion === "h2" &&
|
||||
!Object.prototype.hasOwnProperty.call(configs, `H2${config}`)
|
||||
) {
|
||||
if (gameVersion === "h2" && !Object.hasOwn(configs, `H2${config}`)) {
|
||||
return getConfig(config, clone)
|
||||
}
|
||||
|
||||
|
@ -140,12 +140,7 @@ contractRoutingRouter.post(
|
||||
contractData.Data.GameChangerReferences || []
|
||||
|
||||
for (const gameChangerId of contractData.Data.GameChangers) {
|
||||
if (
|
||||
!Object.prototype.hasOwnProperty.call(
|
||||
gameChangerData,
|
||||
gameChangerId,
|
||||
)
|
||||
) {
|
||||
if (!gameChangerData[gameChangerId]) {
|
||||
log(
|
||||
LogLevel.ERROR,
|
||||
`GetForPlay has detected a missing GameChanger: ${gameChangerId}! This is a bug.`,
|
||||
@ -287,12 +282,7 @@ contractRoutingRouter.post(
|
||||
|
||||
req.body.creationData.ContractConditionIds.forEach(
|
||||
(contractConditionId) => {
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(
|
||||
gameChangerData,
|
||||
contractConditionId,
|
||||
)
|
||||
) {
|
||||
if (gameChangerData[contractConditionId]) {
|
||||
gamechangers.push(contractConditionId)
|
||||
} else if (
|
||||
contractConditionId ===
|
||||
|
116
components/contracts/leaderboards.ts
Normal file
116
components/contracts/leaderboards.ts
Normal file
@ -0,0 +1,116 @@
|
||||
/*
|
||||
* 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 { gameDifficulty, PEACOCKVERSTRING } from "../utils"
|
||||
import { controller } from "../controller"
|
||||
import axios from "axios"
|
||||
import { getFlag } from "../flags"
|
||||
import { fakePlayerRegistry } from "../profileHandler"
|
||||
import { GameVersion, JwtData, MissionManifest } from "../types/types"
|
||||
|
||||
type ApiLeaderboardEntry = {
|
||||
LeaderboardData: {
|
||||
Player: {
|
||||
displayName: string
|
||||
}
|
||||
}
|
||||
gameVersion: {
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
platformId: string
|
||||
platform: {
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
}
|
||||
|
||||
type GameFacingLeaderboardData = {
|
||||
Entries: ApiLeaderboardEntry[]
|
||||
Contract: MissionManifest
|
||||
Page: number
|
||||
HasMore: boolean
|
||||
LeaderboardType: string
|
||||
}
|
||||
|
||||
export async function getLeaderboardEntries(
|
||||
contractId: string,
|
||||
platform: JwtData["platform"],
|
||||
gameVersion: GameVersion,
|
||||
difficultyLevel?: string,
|
||||
): Promise<GameFacingLeaderboardData> {
|
||||
let difficulty = "unset"
|
||||
|
||||
const parsedDifficulty = parseInt(difficultyLevel || "0")
|
||||
|
||||
if (parsedDifficulty === gameDifficulty.casual) {
|
||||
difficulty = "casual"
|
||||
}
|
||||
|
||||
if (parsedDifficulty === gameDifficulty.normal) {
|
||||
difficulty = "normal"
|
||||
}
|
||||
|
||||
if (parsedDifficulty === gameDifficulty.master) {
|
||||
difficulty = "master"
|
||||
}
|
||||
|
||||
const response: GameFacingLeaderboardData = {
|
||||
Entries: [],
|
||||
Contract: controller.resolveContract(contractId),
|
||||
Page: 0,
|
||||
HasMore: false,
|
||||
LeaderboardType: "singleplayer",
|
||||
}
|
||||
|
||||
const host = getFlag("leaderboardsHost") as string
|
||||
|
||||
const entries = (
|
||||
await axios.post<ApiLeaderboardEntry[]>(
|
||||
`${host}/leaderboards/entries/${contractId}`,
|
||||
{
|
||||
gameVersion,
|
||||
difficulty,
|
||||
platform,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"Peacock-Version": PEACOCKVERSTRING,
|
||||
},
|
||||
},
|
||||
)
|
||||
).data
|
||||
|
||||
const ids: readonly string[] = entries.map((te) =>
|
||||
fakePlayerRegistry.index(
|
||||
te.LeaderboardData.Player.displayName,
|
||||
te.platform.name,
|
||||
te.platformId,
|
||||
),
|
||||
)
|
||||
|
||||
entries.forEach((entry, index) => {
|
||||
// @ts-expect-error Remapping on different types
|
||||
entry.LeaderboardData.Player = ids[index]
|
||||
return entry
|
||||
})
|
||||
|
||||
response.Entries = entries
|
||||
|
||||
return response
|
||||
}
|
@ -188,12 +188,7 @@ function createPeacockRequire(pluginName: string): NodeRequire {
|
||||
* @param specifier The requested module.
|
||||
*/
|
||||
const peacockRequire: NodeRequire = (specifier: string) => {
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(
|
||||
generatedPeacockRequireTable,
|
||||
specifier,
|
||||
)
|
||||
) {
|
||||
if (generatedPeacockRequireTable[specifier]) {
|
||||
return generatedPeacockRequireTable[specifier]
|
||||
}
|
||||
|
||||
@ -312,7 +307,10 @@ export class Controller {
|
||||
newEvent: SyncHook<
|
||||
[
|
||||
/** event */ ClientToServerEvent,
|
||||
/** request */ RequestWithJwt,
|
||||
/** details */ {
|
||||
gameVersion: GameVersion
|
||||
userId: string
|
||||
},
|
||||
/** session */ ContractSession,
|
||||
]
|
||||
>
|
||||
|
@ -150,12 +150,7 @@ export function scenePathToRpAsset(
|
||||
)
|
||||
|
||||
for (const brick of bricks) {
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(
|
||||
brickAssetsMap,
|
||||
brick.toLowerCase(),
|
||||
)
|
||||
) {
|
||||
if (brickAssetsMap[brick.toLowerCase()]) {
|
||||
return brickAssetsMap[brick.toLowerCase()]!
|
||||
}
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ import {
|
||||
Seconds,
|
||||
ServerToClientEvent,
|
||||
} from "./types/types"
|
||||
import { contractTypes, extractToken, gameDifficulty, ServerVer } from "./utils"
|
||||
import { contractTypes, gameDifficulty, ServerVer } from "./utils"
|
||||
import { json as jsonMiddleware } from "body-parser"
|
||||
import { log, LogLevel } from "./loggingInterop"
|
||||
import { getUserData, writeUserData } from "./databaseHandler"
|
||||
@ -372,18 +372,74 @@ export function newSession(
|
||||
controller.challengeService.startContract(contractSessions.get(sessionId)!)
|
||||
}
|
||||
|
||||
export type SSE3Response = {
|
||||
SavedTokens: string[]
|
||||
NewEvents: ServerToClientEvent[]
|
||||
NextPoll: number
|
||||
}
|
||||
|
||||
export type SSE4Response = SSE3Response & {
|
||||
PushMessages: string[]
|
||||
}
|
||||
|
||||
export function saveAndSyncEvents(
|
||||
version: 3 | 4,
|
||||
userId: string,
|
||||
gameVersion: GameVersion,
|
||||
lastEventTicks: number | string,
|
||||
values: ClientToServerEvent[],
|
||||
lastPushDt?: number | string,
|
||||
): SSE3Response | SSE4Response {
|
||||
const savedTokens = values.length
|
||||
? saveEvents(userId, values, gameVersion)
|
||||
: null
|
||||
|
||||
let userQueue: S2CEventWithTimestamp[] | undefined
|
||||
let newEvents: ServerToClientEvent[] | null = null
|
||||
|
||||
// events: (server -> client)
|
||||
if ((userQueue = eventQueue.get(userId))) {
|
||||
userQueue = userQueue.filter((item) => item.time > lastEventTicks)
|
||||
eventQueue.set(userId, userQueue)
|
||||
|
||||
newEvents = Array.from(userQueue, (item) => item.event)
|
||||
}
|
||||
|
||||
// push messages: (server -> client)
|
||||
let userPushQueue: PushMessage[] | undefined
|
||||
let pushMessages: string[] | undefined
|
||||
|
||||
if ((userPushQueue = pushMessageQueue.get(userId))) {
|
||||
userPushQueue = userPushQueue.filter((item) => item.time > lastPushDt)
|
||||
pushMessageQueue.set(userId, userPushQueue)
|
||||
|
||||
pushMessages = Array.from(userPushQueue, (item) => item.message)
|
||||
}
|
||||
|
||||
const sse3Response: SSE3Response = {
|
||||
SavedTokens: savedTokens,
|
||||
NewEvents: newEvents || null,
|
||||
NextPoll: 10.0,
|
||||
}
|
||||
|
||||
return version === 3
|
||||
? sse3Response
|
||||
: {
|
||||
...sse3Response,
|
||||
PushMessages: pushMessages || null,
|
||||
}
|
||||
}
|
||||
|
||||
eventRouter.post(
|
||||
"/SaveAndSynchronizeEvents4",
|
||||
extractToken,
|
||||
"/SaveAndSynchronizeEvents3",
|
||||
jsonMiddleware({ limit: "10Mb" }),
|
||||
(
|
||||
req: RequestWithJwt<
|
||||
unknown,
|
||||
{
|
||||
lastPushDt: number | string
|
||||
lastEventTicks: number | string
|
||||
userId?: string
|
||||
values?: []
|
||||
userId: string
|
||||
values: ClientToServerEvent[]
|
||||
}
|
||||
>,
|
||||
res,
|
||||
@ -398,48 +454,58 @@ eventRouter.post(
|
||||
return
|
||||
}
|
||||
|
||||
const savedTokens = req.body.values.length
|
||||
? saveEvents(req.body.userId, req.body.values, req)
|
||||
: null
|
||||
res.json(
|
||||
saveAndSyncEvents(
|
||||
3,
|
||||
req.jwt.unique_name,
|
||||
req.gameVersion,
|
||||
req.body.lastEventTicks,
|
||||
req.body.values,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
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)
|
||||
eventRouter.post(
|
||||
"/SaveAndSynchronizeEvents4",
|
||||
jsonMiddleware({ limit: "10Mb" }),
|
||||
(
|
||||
req: RequestWithJwt<
|
||||
unknown,
|
||||
{
|
||||
lastPushDt: number | string
|
||||
lastEventTicks: number | string
|
||||
userId: string
|
||||
values: ClientToServerEvent[]
|
||||
}
|
||||
>,
|
||||
res,
|
||||
) => {
|
||||
if (req.body.userId !== req.jwt.unique_name) {
|
||||
res.status(403).send() // Trying to save events for other user
|
||||
return
|
||||
}
|
||||
|
||||
// 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)
|
||||
if (!Array.isArray(req.body.values)) {
|
||||
res.status(400).end() // malformed request
|
||||
return
|
||||
}
|
||||
|
||||
res.json({
|
||||
SavedTokens: savedTokens,
|
||||
NewEvents: newEvents || null,
|
||||
NextPoll: 10.0,
|
||||
PushMessages: pushMessages || null,
|
||||
})
|
||||
res.json(
|
||||
saveAndSyncEvents(
|
||||
4,
|
||||
req.jwt.unique_name,
|
||||
req.gameVersion,
|
||||
req.body.lastEventTicks,
|
||||
req.body.values,
|
||||
req.body.lastPushDt,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
eventRouter.post(
|
||||
"/SaveEvents2",
|
||||
extractToken,
|
||||
jsonMiddleware({ limit: "10Mb" }),
|
||||
(req: RequestWithJwt, res) => {
|
||||
if (req.jwt.unique_name !== req.body.userId) {
|
||||
@ -447,7 +513,7 @@ eventRouter.post(
|
||||
return
|
||||
}
|
||||
|
||||
res.json(saveEvents(req.body.userId, req.body.values, req))
|
||||
res.json(saveEvents(req.body.userId, req.body.values, req.gameVersion))
|
||||
},
|
||||
)
|
||||
|
||||
@ -574,11 +640,11 @@ function contractFailed(
|
||||
function saveEvents(
|
||||
userId: string,
|
||||
events: ClientToServerEvent[],
|
||||
req: RequestWithJwt<unknown, unknown>,
|
||||
gameVersion: GameVersion,
|
||||
): string[] {
|
||||
const response: string[] = []
|
||||
const processed: string[] = []
|
||||
const userData = getUserData(req.jwt.unique_name, req.gameVersion)
|
||||
const userData = getUserData(userId, gameVersion)
|
||||
events.forEach((event) => {
|
||||
let session = contractSessions.get(event.ContractSessionId)
|
||||
|
||||
@ -591,9 +657,9 @@ function saveEvents(
|
||||
newSession(
|
||||
event.ContractSessionId,
|
||||
event.ContractId,
|
||||
req.jwt.unique_name,
|
||||
userId,
|
||||
gameDifficulty.normal,
|
||||
req.gameVersion,
|
||||
gameVersion,
|
||||
false,
|
||||
)
|
||||
|
||||
@ -620,8 +686,16 @@ function saveEvents(
|
||||
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)
|
||||
controller.hooks.newEvent.call(
|
||||
event,
|
||||
// to avoid breakage, we pass details as an object instead of the request
|
||||
// since we no longer have access to that
|
||||
{
|
||||
gameVersion,
|
||||
userId,
|
||||
},
|
||||
session,
|
||||
)
|
||||
|
||||
for (const objectiveId of session.objectiveStates.keys()) {
|
||||
try {
|
||||
@ -795,7 +869,7 @@ function saveEvents(
|
||||
)
|
||||
break
|
||||
case "BodyFound":
|
||||
if (req.gameVersion === "h1") {
|
||||
if (gameVersion === "h1") {
|
||||
session.legacyHasBodyBeenFound = true
|
||||
}
|
||||
|
||||
@ -812,8 +886,8 @@ function saveEvents(
|
||||
session.disguisesUsed.add(disguise)
|
||||
liveSplitManager.startMission(
|
||||
session.contractId,
|
||||
req.gameVersion,
|
||||
req.jwt.unique_name,
|
||||
gameVersion,
|
||||
userId,
|
||||
)
|
||||
break
|
||||
}
|
||||
@ -924,7 +998,7 @@ function saveEvents(
|
||||
opportunities[val.RepositoryId] = true
|
||||
}
|
||||
|
||||
writeUserData(req.jwt.unique_name, req.gameVersion)
|
||||
writeUserData(userId, gameVersion)
|
||||
break
|
||||
}
|
||||
case "AreaDiscovered":
|
||||
|
@ -42,7 +42,6 @@ import * as smfSupport from "./smfSupport"
|
||||
import * as utils from "./utils"
|
||||
import * as webFeatures from "./webFeatures"
|
||||
import * as legacyContractHandler from "./2016/legacyContractHandler"
|
||||
import * as legacyEventRouter from "./2016/legacyEventRouter"
|
||||
import * as legacyMenuData from "./2016/legacyMenuData"
|
||||
import * as legacyMenuSystem from "./2016/legacyMenuSystem"
|
||||
import * as legacyProfileRouter from "./2016/legacyProfileRouter"
|
||||
@ -56,6 +55,7 @@ import * as dataGen from "./contracts/dataGen"
|
||||
import * as elusiveTargetArcades from "./contracts/elusiveTargetArcades"
|
||||
import * as elusiveTargets from "./contracts/elusiveTargets"
|
||||
import * as hitsCategoryService from "./contracts/hitsCategoryService"
|
||||
import * as leaderboards from "./contracts/leaderboards"
|
||||
import * as missionsInLocation from "./contracts/missionsInLocation"
|
||||
import * as reportRouting from "./contracts/reportRouting"
|
||||
import * as client from "./discord/client"
|
||||
@ -65,24 +65,18 @@ import * as liveSplitManager from "./livesplit/liveSplitManager"
|
||||
import * as campaigns from "./menus/campaigns"
|
||||
import * as destinations from "./menus/destinations"
|
||||
import * as favoriteContracts from "./menus/favoriteContracts"
|
||||
import * as hub from "./menus/hub"
|
||||
import * as imageHandler from "./menus/imageHandler"
|
||||
import * as menuSystem from "./menus/menuSystem"
|
||||
import * as planning from "./menus/planning"
|
||||
import * as playnext from "./menus/playnext"
|
||||
import * as sniper from "./menus/sniper"
|
||||
import * as stashpoints from "./menus/stashpoints"
|
||||
import * as multiplayerMenuData from "./multiplayer/multiplayerMenuData"
|
||||
import * as multiplayerService from "./multiplayer/multiplayerService"
|
||||
import * as multiplayerUtils from "./multiplayer/multiplayerUtils"
|
||||
import * as contextListeners from "./statemachines/contextListeners"
|
||||
import * as contractCreation from "./statemachines/contractCreation"
|
||||
import * as challenges from "./types/challenges"
|
||||
import * as events from "./types/events"
|
||||
import * as gameSchemas from "./types/gameSchemas"
|
||||
import * as livesplit from "./types/livesplit"
|
||||
import * as mastery from "./types/mastery"
|
||||
import * as score from "./types/score"
|
||||
import * as scoring from "./types/scoring"
|
||||
import * as types from "./types/types"
|
||||
import * as escalationService from "./contracts/escalations/escalationService"
|
||||
|
||||
export default {
|
||||
@ -142,10 +136,6 @@ export default {
|
||||
__esModule: true,
|
||||
...legacyContractHandler,
|
||||
},
|
||||
"@peacockproject/core/2016/legacyEventRouter": {
|
||||
__esModule: true,
|
||||
...legacyEventRouter,
|
||||
},
|
||||
"@peacockproject/core/2016/legacyMenuData": {
|
||||
__esModule: true,
|
||||
...legacyMenuData,
|
||||
@ -195,6 +185,10 @@ export default {
|
||||
__esModule: true,
|
||||
...hitsCategoryService,
|
||||
},
|
||||
"@peacockproject/core/contracts/leaderboards": {
|
||||
__esModule: true,
|
||||
...leaderboards,
|
||||
},
|
||||
"@peacockproject/core/contracts/missionsInLocation": {
|
||||
__esModule: true,
|
||||
...missionsInLocation,
|
||||
@ -222,6 +216,7 @@ export default {
|
||||
__esModule: true,
|
||||
...favoriteContracts,
|
||||
},
|
||||
"@peacockproject/core/menus/hub": { __esModule: true, ...hub },
|
||||
"@peacockproject/core/menus/imageHandler": {
|
||||
__esModule: true,
|
||||
...imageHandler,
|
||||
@ -233,6 +228,10 @@ export default {
|
||||
"@peacockproject/core/menus/planning": { __esModule: true, ...planning },
|
||||
"@peacockproject/core/menus/playnext": { __esModule: true, ...playnext },
|
||||
"@peacockproject/core/menus/sniper": { __esModule: true, ...sniper },
|
||||
"@peacockproject/core/menus/stashpoints": {
|
||||
__esModule: true,
|
||||
...stashpoints,
|
||||
},
|
||||
"@peacockproject/core/multiplayer/multiplayerMenuData": {
|
||||
__esModule: true,
|
||||
...multiplayerMenuData,
|
||||
@ -253,20 +252,6 @@ export default {
|
||||
__esModule: true,
|
||||
...contractCreation,
|
||||
},
|
||||
"@peacockproject/core/types/challenges": {
|
||||
__esModule: true,
|
||||
...challenges,
|
||||
},
|
||||
"@peacockproject/core/types/events": { __esModule: true, ...events },
|
||||
"@peacockproject/core/types/gameSchemas": {
|
||||
__esModule: true,
|
||||
...gameSchemas,
|
||||
},
|
||||
"@peacockproject/core/types/livesplit": { __esModule: true, ...livesplit },
|
||||
"@peacockproject/core/types/mastery": { __esModule: true, ...mastery },
|
||||
"@peacockproject/core/types/score": { __esModule: true, ...score },
|
||||
"@peacockproject/core/types/scoring": { __esModule: true, ...scoring },
|
||||
"@peacockproject/core/types/types": { __esModule: true, ...types },
|
||||
"@peacockproject/core/contracts/escalations/escalationService": {
|
||||
__esModule: true,
|
||||
...escalationService,
|
||||
|
@ -59,9 +59,8 @@ import {
|
||||
import { eventRouter } from "./eventHandler"
|
||||
import { contractRoutingRouter } from "./contracts/contractRouting"
|
||||
import { profileRouter } from "./profileHandler"
|
||||
import { menuDataRouter, preMenuDataRouter } from "./menuData"
|
||||
import { menuDataRouter } from "./menuData"
|
||||
import { menuSystemPreRouter, menuSystemRouter } from "./menus/menuSystem"
|
||||
import { legacyEventRouter } from "./2016/legacyEventRouter"
|
||||
import { legacyMenuSystemRouter } from "./2016/legacyMenuSystem"
|
||||
import { _theLastYardbirdScpc, controller } from "./controller"
|
||||
import {
|
||||
@ -140,7 +139,7 @@ if (getFlag("developmentLogRequests")) {
|
||||
|
||||
app.use("/_wf", webFeaturesRouter)
|
||||
|
||||
app.get("/", (req: Request, res) => {
|
||||
app.get("/", (_: Request, res) => {
|
||||
if (PEACOCK_DEV) {
|
||||
res.contentType("text/html")
|
||||
res.send(
|
||||
@ -240,21 +239,19 @@ app.post(
|
||||
"/api/metrics/*",
|
||||
jsonMiddleware({ limit: "10Mb" }),
|
||||
(req: RequestWithJwt<never, S2CEventWithTimestamp[]>, res) => {
|
||||
req.body.forEach((event) => {
|
||||
for (const event of req.body) {
|
||||
controller.hooks.newMetricsEvent.call(event, req)
|
||||
})
|
||||
}
|
||||
|
||||
res.send()
|
||||
},
|
||||
)
|
||||
|
||||
app.use("/oauth/token", urlencoded())
|
||||
|
||||
app.post("/oauth/token", (req: RequestWithJwt, res) =>
|
||||
app.post("/oauth/token", urlencoded(), (req: RequestWithJwt, res) =>
|
||||
handleOauthToken(req, res),
|
||||
)
|
||||
|
||||
app.get("/files/onlineconfig.json", (req, res) => {
|
||||
app.get("/files/onlineconfig.json", (_, res) => {
|
||||
res.set("Content-Type", "application/octet-stream")
|
||||
res.send(getConfig("OnlineConfig", false))
|
||||
})
|
||||
@ -270,9 +267,7 @@ app.use(
|
||||
req.serverVersion = req.params.serverVersion
|
||||
req.gameVersion = req.serverVersion.startsWith("8")
|
||||
? "h3"
|
||||
: req.serverVersion.startsWith("7") &&
|
||||
// prettier-ignore
|
||||
req.serverVersion !== "7.3.0"
|
||||
: req.serverVersion.startsWith("7")
|
||||
? // prettier-ignore
|
||||
"h2"
|
||||
: // prettier-ignore
|
||||
@ -426,10 +421,6 @@ app.post(
|
||||
const legacyRouter = Router()
|
||||
const primaryRouter = Router()
|
||||
|
||||
legacyRouter.use(
|
||||
"/authentication/api/userchannel/EventsService/",
|
||||
legacyEventRouter,
|
||||
)
|
||||
legacyRouter.use("/resources-(\\d+-\\d+)/", legacyMenuSystemRouter)
|
||||
legacyRouter.use("/authentication/api/userchannel/", legacyProfileRouter)
|
||||
legacyRouter.use("/profiles/page/", legacyMenuDataRouter)
|
||||
@ -456,7 +447,6 @@ primaryRouter.use(
|
||||
reportRouter,
|
||||
)
|
||||
primaryRouter.use("/authentication/api/userchannel/", profileRouter)
|
||||
primaryRouter.use("/profiles/page/", preMenuDataRouter)
|
||||
primaryRouter.use("/profiles/page", multiplayerMenuDataRouter)
|
||||
primaryRouter.use("/profiles/page/", menuDataRouter)
|
||||
primaryRouter.use("/resources-(\\d+-\\d+)/", menuSystemPreRouter)
|
||||
@ -464,7 +454,7 @@ primaryRouter.use("/resources-(\\d+-\\d+)/", menuSystemRouter)
|
||||
|
||||
app.use(
|
||||
Router()
|
||||
.use((req: RequestWithJwt, res, next) => {
|
||||
.use((req: RequestWithJwt, _, next) => {
|
||||
if (req.shouldCease) {
|
||||
return next("router")
|
||||
}
|
||||
@ -477,7 +467,7 @@ app.use(
|
||||
})
|
||||
.use(legacyRouter),
|
||||
Router()
|
||||
.use((req: RequestWithJwt, res, next) => {
|
||||
.use((req: RequestWithJwt, _, next) => {
|
||||
if (req.shouldCease) {
|
||||
return next("router")
|
||||
}
|
||||
@ -636,9 +626,7 @@ program
|
||||
.description("packs an input file into a Challenge Resource Package")
|
||||
.action((input, options: { output: string }) => {
|
||||
const outputPath =
|
||||
options.output !== ""
|
||||
? options.output
|
||||
: input.replace(/\.[^/\\.]+$/, ".crp")
|
||||
options.output || input.replace(/\.[^/\\.]+$/, ".crp")
|
||||
|
||||
writeFileSync(
|
||||
outputPath,
|
||||
@ -655,9 +643,7 @@ program
|
||||
.description("unpacks a Challenge Resource Package")
|
||||
.action((input, options: { output: string }) => {
|
||||
const outputPath =
|
||||
options.output !== ""
|
||||
? options.output
|
||||
: input.replace(/\.[^/\\.]+$/, ".json")
|
||||
options.output || input.replace(/\.[^/\\.]+$/, ".json")
|
||||
|
||||
writeFileSync(outputPath, JSON.stringify(unpack(readFileSync(input))))
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -21,16 +21,22 @@ import type {
|
||||
CompletionData,
|
||||
GameLocationsData,
|
||||
GameVersion,
|
||||
IHit,
|
||||
JwtData,
|
||||
MissionStory,
|
||||
OpportunityStatistics,
|
||||
PeacockLocationsData,
|
||||
RequestWithJwt,
|
||||
Unlockable,
|
||||
} from "../types/types"
|
||||
import { controller } from "../controller"
|
||||
import { contractIdToHitObject, controller } from "../controller"
|
||||
import { generateCompletionData } from "../contracts/dataGen"
|
||||
import { getUserData } from "../databaseHandler"
|
||||
import { ChallengeFilterType } from "../candle/challengeHelpers"
|
||||
import { GetDestinationQuery } from "../types/gameSchemas"
|
||||
import { createInventory } from "../inventory"
|
||||
import { log, LogLevel } from "../loggingInterop"
|
||||
import { no2016 } from "../contracts/escalations/escalationService"
|
||||
import { missionsInLocations } from "../contracts/missionsInLocation"
|
||||
|
||||
type GameFacingDestination = {
|
||||
ChallengeCompletion: {
|
||||
@ -52,24 +58,26 @@ type GameFacingDestination = {
|
||||
}
|
||||
}
|
||||
}
|
||||
const missionStories = getConfig<Record<string, MissionStory>>(
|
||||
"MissionStories",
|
||||
false,
|
||||
)
|
||||
|
||||
export function getDestinationCompletion(
|
||||
parent: Unlockable,
|
||||
child: Unlockable | undefined,
|
||||
req: RequestWithJwt,
|
||||
gameVersion: GameVersion,
|
||||
jwt: JwtData,
|
||||
) {
|
||||
const userData = getUserData(req.jwt.unique_name, req.gameVersion)
|
||||
const missionStories = getConfig<Record<string, MissionStory>>(
|
||||
"MissionStories",
|
||||
false,
|
||||
)
|
||||
|
||||
const userData = getUserData(jwt.unique_name, gameVersion)
|
||||
const challenges = controller.challengeService.getGroupedChallengeLists(
|
||||
{
|
||||
type: ChallengeFilterType.ParentLocation,
|
||||
parent: parent.Id,
|
||||
},
|
||||
parent.Id,
|
||||
req.gameVersion,
|
||||
gameVersion,
|
||||
)
|
||||
|
||||
const opportunities = Object.values(missionStories)
|
||||
@ -90,7 +98,7 @@ export function getDestinationCompletion(
|
||||
controller.challengeService.countTotalNCompletedChallenges(
|
||||
challenges,
|
||||
userData.Id,
|
||||
req.gameVersion,
|
||||
gameVersion,
|
||||
)
|
||||
|
||||
return {
|
||||
@ -138,11 +146,20 @@ export function getCompletionPercent(
|
||||
: (100 * totalCompleted) / totalCompletables
|
||||
}
|
||||
|
||||
export function destinationsMenu(req: RequestWithJwt): GameFacingDestination[] {
|
||||
/**
|
||||
* Get the list of destinations used by the `/profiles/page/Destinations` endpoint.
|
||||
*
|
||||
* @param gameVersion
|
||||
* @param jwt
|
||||
*/
|
||||
export function getAllGameDestinations(
|
||||
gameVersion: GameVersion,
|
||||
jwt: JwtData,
|
||||
): GameFacingDestination[] {
|
||||
const result: GameFacingDestination[] = []
|
||||
const locations = getVersionedConfig<PeacockLocationsData>(
|
||||
"LocationsData",
|
||||
req.gameVersion,
|
||||
gameVersion,
|
||||
true,
|
||||
)
|
||||
|
||||
@ -152,22 +169,22 @@ export function destinationsMenu(req: RequestWithJwt): GameFacingDestination[] {
|
||||
"UI_LOCATION_PARENT_" + destination.substring(16) + "_NAME"
|
||||
|
||||
const template: GameFacingDestination = {
|
||||
...getDestinationCompletion(parent, undefined, req),
|
||||
...getDestinationCompletion(parent, undefined, gameVersion, jwt),
|
||||
...{
|
||||
CompletionData: generateCompletionData(
|
||||
destination,
|
||||
req.jwt.unique_name,
|
||||
req.gameVersion,
|
||||
jwt.unique_name,
|
||||
gameVersion,
|
||||
),
|
||||
Data:
|
||||
req.gameVersion === "h1"
|
||||
gameVersion === "h1"
|
||||
? {
|
||||
normal: {
|
||||
ChallengeCompletion: undefined,
|
||||
CompletionData: generateCompletionData(
|
||||
destination,
|
||||
req.jwt.unique_name,
|
||||
req.gameVersion,
|
||||
jwt.unique_name,
|
||||
gameVersion,
|
||||
"mission",
|
||||
"normal",
|
||||
),
|
||||
@ -176,8 +193,8 @@ export function destinationsMenu(req: RequestWithJwt): GameFacingDestination[] {
|
||||
ChallengeCompletion: undefined,
|
||||
CompletionData: generateCompletionData(
|
||||
destination,
|
||||
req.jwt.unique_name,
|
||||
req.gameVersion,
|
||||
jwt.unique_name,
|
||||
gameVersion,
|
||||
"mission",
|
||||
"pro1",
|
||||
),
|
||||
@ -190,7 +207,7 @@ export function destinationsMenu(req: RequestWithJwt): GameFacingDestination[] {
|
||||
// TODO: THIS IS NOT CORRECT FOR 2016!
|
||||
// There are different challenges for normal and pro1 in 2016, right now, we do not support this.
|
||||
// We're just reusing this for now.
|
||||
if (req.gameVersion === "h1") {
|
||||
if (gameVersion === "h1") {
|
||||
template.Data.normal.ChallengeCompletion =
|
||||
template.ChallengeCompletion
|
||||
template.Data.pro1.ChallengeCompletion =
|
||||
@ -270,3 +287,233 @@ export function createLocationsData(
|
||||
|
||||
return finalData
|
||||
}
|
||||
|
||||
// TODO: this is a mess, write docs and type explicitly
|
||||
export function getDestination(
|
||||
query: GetDestinationQuery,
|
||||
gameVersion: GameVersion,
|
||||
jwt: JwtData,
|
||||
) {
|
||||
const LOCATION = query.locationId
|
||||
|
||||
const locData = getVersionedConfig<PeacockLocationsData>(
|
||||
"LocationsData",
|
||||
gameVersion,
|
||||
false,
|
||||
)
|
||||
|
||||
const locationData = locData.parents[LOCATION]
|
||||
const masteryData = controller.masteryService.getMasteryDataForDestination(
|
||||
query.locationId,
|
||||
gameVersion,
|
||||
jwt.unique_name,
|
||||
query.difficulty,
|
||||
)
|
||||
|
||||
const response = {
|
||||
Location: {},
|
||||
MissionData: {
|
||||
...getDestinationCompletion(
|
||||
locationData,
|
||||
undefined,
|
||||
gameVersion,
|
||||
jwt,
|
||||
),
|
||||
...{ SubLocationMissionsData: [] },
|
||||
},
|
||||
ChallengeData: {
|
||||
Children:
|
||||
controller.challengeService.getChallengeDataForDestination(
|
||||
query.locationId,
|
||||
gameVersion,
|
||||
jwt.unique_name,
|
||||
),
|
||||
},
|
||||
MasteryData:
|
||||
LOCATION !== "LOCATION_PARENT_ICA_FACILITY"
|
||||
? gameVersion === "h1"
|
||||
? masteryData[0]
|
||||
: masteryData
|
||||
: {},
|
||||
DifficultyData: undefined,
|
||||
}
|
||||
|
||||
if (gameVersion === "h1" && LOCATION !== "LOCATION_PARENT_ICA_FACILITY") {
|
||||
const inventory = createInventory(jwt.unique_name, gameVersion)
|
||||
|
||||
response.DifficultyData = {
|
||||
AvailableDifficultyModes: [
|
||||
{
|
||||
Name: "normal",
|
||||
Available: true,
|
||||
},
|
||||
{
|
||||
Name: "pro1",
|
||||
Available: inventory.some(
|
||||
(e) =>
|
||||
e.Unlockable.Id ===
|
||||
locationData.Properties.DifficultyUnlock.pro1,
|
||||
),
|
||||
},
|
||||
],
|
||||
Difficulty: query.difficulty,
|
||||
LocationId: LOCATION,
|
||||
}
|
||||
}
|
||||
|
||||
if (PEACOCK_DEV) {
|
||||
log(LogLevel.DEBUG, `Looking up locations details for ${LOCATION}.`)
|
||||
}
|
||||
|
||||
const sublocationsData = Object.values(locData.children).filter(
|
||||
(subLocation) => subLocation.Properties.ParentLocation === LOCATION,
|
||||
)
|
||||
|
||||
response.Location = locationData
|
||||
|
||||
if (query.difficulty === "pro1") {
|
||||
const obj = {
|
||||
Location: locationData,
|
||||
SubLocation: locationData,
|
||||
Missions: [controller.missionsInLocations.pro1[LOCATION]].map(
|
||||
(id) => contractIdToHitObject(id, gameVersion, jwt.unique_name),
|
||||
),
|
||||
SarajevoSixMissions: [],
|
||||
ElusiveMissions: [],
|
||||
EscalationMissions: [],
|
||||
SniperMissions: [],
|
||||
PlaceholderMissions: [],
|
||||
CampaignMissions: [],
|
||||
CompletionData: generateCompletionData(
|
||||
sublocationsData[0].Id,
|
||||
jwt.unique_name,
|
||||
gameVersion,
|
||||
),
|
||||
}
|
||||
|
||||
response.MissionData.SubLocationMissionsData.push(obj)
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
for (const e of sublocationsData) {
|
||||
log(LogLevel.DEBUG, `Looking up sublocation details for ${e.Id}`)
|
||||
|
||||
const escalations: IHit[] = []
|
||||
|
||||
// every unique escalation from the sublocation
|
||||
const allUniqueEscalations: string[] = [
|
||||
...(gameVersion === "h1" && e.Id === "LOCATION_ICA_FACILITY"
|
||||
? controller.missionsInLocations.escalations[
|
||||
"LOCATION_ICA_FACILITY_SHIP"
|
||||
]
|
||||
: []),
|
||||
...new Set<string>(
|
||||
controller.missionsInLocations.escalations[e.Id] || [],
|
||||
),
|
||||
]
|
||||
|
||||
for (const escalation of allUniqueEscalations) {
|
||||
if (gameVersion === "h1" && no2016.includes(escalation)) continue
|
||||
|
||||
const details = contractIdToHitObject(
|
||||
escalation,
|
||||
gameVersion,
|
||||
jwt.unique_name,
|
||||
)
|
||||
|
||||
if (details) {
|
||||
escalations.push(details)
|
||||
}
|
||||
}
|
||||
|
||||
const sniperMissions: IHit[] = []
|
||||
|
||||
for (const sniperMission of controller.missionsInLocations.sniper[
|
||||
e.Id
|
||||
] ?? []) {
|
||||
sniperMissions.push(
|
||||
contractIdToHitObject(
|
||||
sniperMission,
|
||||
gameVersion,
|
||||
jwt.unique_name,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
const obj = {
|
||||
Location: locationData,
|
||||
SubLocation: e,
|
||||
Missions: [],
|
||||
SarajevoSixMissions: [],
|
||||
ElusiveMissions: [],
|
||||
EscalationMissions: escalations,
|
||||
SniperMissions: sniperMissions,
|
||||
PlaceholderMissions: [],
|
||||
CampaignMissions: [],
|
||||
CompletionData: generateCompletionData(
|
||||
e.Id,
|
||||
jwt.unique_name,
|
||||
gameVersion,
|
||||
),
|
||||
}
|
||||
|
||||
const types = [
|
||||
...[
|
||||
[undefined, "Missions"],
|
||||
["elusive", "ElusiveMissions"],
|
||||
],
|
||||
...((gameVersion === "h1" &&
|
||||
missionsInLocations.sarajevo["h2016enabled"]) ||
|
||||
gameVersion === "h3"
|
||||
? [["sarajevo", "SarajevoSixMissions"]]
|
||||
: []),
|
||||
]
|
||||
|
||||
for (const t of types) {
|
||||
let theMissions: string[] | undefined = !t[0] // no specific type
|
||||
? controller.missionsInLocations[e.Id]
|
||||
: controller.missionsInLocations[t[0]][e.Id]
|
||||
|
||||
// edge case: ica facility in h1 was only 1 sublocation, so we merge
|
||||
// these into a single array
|
||||
if (
|
||||
gameVersion === "h1" &&
|
||||
!t[0] &&
|
||||
LOCATION === "LOCATION_PARENT_ICA_FACILITY"
|
||||
) {
|
||||
theMissions = [
|
||||
...controller.missionsInLocations
|
||||
.LOCATION_ICA_FACILITY_ARRIVAL,
|
||||
...controller.missionsInLocations
|
||||
.LOCATION_ICA_FACILITY_SHIP,
|
||||
...controller.missionsInLocations.LOCATION_ICA_FACILITY,
|
||||
]
|
||||
}
|
||||
|
||||
if (theMissions) {
|
||||
for (const c of theMissions.filter(
|
||||
// removes snow festival on h1
|
||||
(m) =>
|
||||
m &&
|
||||
!(
|
||||
gameVersion === "h1" &&
|
||||
m === "c414a084-a7b9-43ce-b6ca-590620acd87e"
|
||||
),
|
||||
)) {
|
||||
const mission = contractIdToHitObject(
|
||||
c,
|
||||
gameVersion,
|
||||
jwt.unique_name,
|
||||
)
|
||||
|
||||
obj[t[1]].push(mission)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
response.MissionData.SubLocationMissionsData.push(obj)
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
211
components/menus/hub.ts
Normal file
211
components/menus/hub.ts
Normal file
@ -0,0 +1,211 @@
|
||||
/*
|
||||
* 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 type { GameVersion, JwtData, PeacockLocationsData } from "../types/types"
|
||||
import { swapToBrowsingMenusStatus } from "../discordRp"
|
||||
import { getUserData } from "../databaseHandler"
|
||||
import { controller } from "../controller"
|
||||
import { contractCreationTutorialId, getMaxProfileLevel } from "../utils"
|
||||
import { getVersionedConfig } from "../configSwizzleManager"
|
||||
import {
|
||||
generateCompletionData,
|
||||
generateUserCentric,
|
||||
} from "../contracts/dataGen"
|
||||
import { createLocationsData, getAllGameDestinations } from "./destinations"
|
||||
import { makeCampaigns } from "./campaigns"
|
||||
|
||||
export function getHubData(gameVersion: GameVersion, jwt: JwtData) {
|
||||
swapToBrowsingMenusStatus(gameVersion)
|
||||
|
||||
const userdata = getUserData(jwt.unique_name, gameVersion)
|
||||
|
||||
const contractCreationTutorial =
|
||||
gameVersion !== "scpc"
|
||||
? controller.resolveContract(contractCreationTutorialId)!
|
||||
: undefined
|
||||
|
||||
const locations = getVersionedConfig<PeacockLocationsData>(
|
||||
"LocationsData",
|
||||
gameVersion,
|
||||
true,
|
||||
)
|
||||
const career =
|
||||
gameVersion === "h3"
|
||||
? {}
|
||||
: {
|
||||
// TODO: Add data on elusive challenges. They are only shown on the Career->Challenges page for H1 and H2. They are not supported by Peacock as of v6.0.0.
|
||||
ELUSIVES_UNSUPPORTED: {
|
||||
Children: [],
|
||||
Name: "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
|
||||
Location:
|
||||
locations.parents["LOCATION_PARENT_ICA_FACILITY"],
|
||||
},
|
||||
}
|
||||
|
||||
const masteryData = []
|
||||
|
||||
for (const parent in locations.parents) {
|
||||
career[parent] = {
|
||||
Children: [],
|
||||
Location: locations.parents[parent],
|
||||
Name: locations.parents[parent].DisplayNameLocKey,
|
||||
}
|
||||
|
||||
// Exclude ICA Facility from showing in the Career -> Mastery page
|
||||
if (parent === "LOCATION_PARENT_ICA_FACILITY") continue
|
||||
|
||||
if (
|
||||
controller.masteryService.getMasteryDataForDestination(
|
||||
parent,
|
||||
gameVersion,
|
||||
jwt.unique_name,
|
||||
).length
|
||||
) {
|
||||
const completionData =
|
||||
controller.masteryService.getLocationCompletion(
|
||||
parent,
|
||||
parent,
|
||||
gameVersion,
|
||||
jwt.unique_name,
|
||||
parent.includes("SNUG") ? "evergreen" : "mission",
|
||||
gameVersion === "h1" ? "normal" : undefined,
|
||||
)
|
||||
|
||||
masteryData.push({
|
||||
CompletionData: completionData,
|
||||
...(gameVersion === "h1"
|
||||
? {
|
||||
Data: {
|
||||
normal: {
|
||||
CompletionData: completionData,
|
||||
},
|
||||
pro1: {
|
||||
CompletionData:
|
||||
controller.masteryService.getLocationCompletion(
|
||||
parent,
|
||||
parent,
|
||||
gameVersion,
|
||||
jwt.unique_name,
|
||||
parent.includes("SNUG")
|
||||
? "evergreen"
|
||||
: "mission",
|
||||
"pro1",
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
Id: locations.parents[parent].Id,
|
||||
Image: locations.parents[parent].Properties.Icon,
|
||||
IsLocked: locations.parents[parent].Properties.IsLocked,
|
||||
Location: locations.parents[parent],
|
||||
RequiredResources:
|
||||
locations.parents[parent].Properties.RequiredResources,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for (const child in locations.children) {
|
||||
if (
|
||||
child === "LOCATION_ICA_FACILITY_ARRIVAL" ||
|
||||
child.includes("SNUG_")
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
const parent = locations.children[child].Properties.ParentLocation
|
||||
const location = locations.children[child]
|
||||
const challenges = controller.challengeService.getChallengesForLocation(
|
||||
child,
|
||||
gameVersion,
|
||||
)
|
||||
const challengeCompletion =
|
||||
controller.challengeService.countTotalNCompletedChallenges(
|
||||
challenges,
|
||||
jwt.unique_name,
|
||||
gameVersion,
|
||||
)
|
||||
|
||||
career[parent]?.Children.push({
|
||||
IsLocked: location.Properties.IsLocked,
|
||||
Name: location.DisplayNameLocKey,
|
||||
Image: location.Properties.Icon,
|
||||
Icon: location.Type, // should be "location" for all locations
|
||||
CompletedChallengesCount:
|
||||
challengeCompletion.CompletedChallengesCount,
|
||||
ChallengesCount: challengeCompletion.ChallengesCount,
|
||||
CategoryId: child,
|
||||
Description: `UI_${child}_PRIMARY_DESC`,
|
||||
Location: location,
|
||||
ImageLocked: location.Properties.LockedIcon,
|
||||
RequiredResources: location.Properties.RequiredResources,
|
||||
IsPack: false, // should be false for all locations
|
||||
CompletionData: generateCompletionData(
|
||||
child,
|
||||
jwt.unique_name,
|
||||
gameVersion,
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
ServerTile: {
|
||||
title: "The Peacock Project",
|
||||
image: "images/contracts/novikov_and_magolis/tile.jpg",
|
||||
icon: "story",
|
||||
url: "",
|
||||
select: {
|
||||
header: "Playing on a Peacock instance",
|
||||
title: "The Peacock Project",
|
||||
icon: "story",
|
||||
},
|
||||
},
|
||||
DashboardData: [],
|
||||
DestinationsData: getAllGameDestinations(gameVersion, jwt),
|
||||
CreateContractTutorial: generateUserCentric(
|
||||
contractCreationTutorial,
|
||||
jwt.unique_name,
|
||||
gameVersion,
|
||||
),
|
||||
LocationsData: createLocationsData(gameVersion, true),
|
||||
ProfileData: {
|
||||
ChallengeData: {
|
||||
Children: Object.values(career),
|
||||
},
|
||||
MasteryData: masteryData,
|
||||
},
|
||||
StoryData: makeCampaigns(gameVersion, jwt.unique_name),
|
||||
FilterData: getVersionedConfig("FilterData", gameVersion, false),
|
||||
StoreData: getVersionedConfig("StoreData", gameVersion, false),
|
||||
IOIAccountStatus: {
|
||||
IsConfirmed: true,
|
||||
LinkedEmail: "mail@example.com",
|
||||
IOIAccountId: "00000000-0000-0000-0000-000000000000",
|
||||
IOIAccountBaseUrl: "https://account.ioi.dk",
|
||||
},
|
||||
FinishedFinalTest: true,
|
||||
Currency: {
|
||||
Balance: 0,
|
||||
},
|
||||
PlayerProfileXpData: {
|
||||
XP: userdata.Extensions.progression.PlayerProfileXP.Total,
|
||||
Level: userdata.Extensions.progression.PlayerProfileXP.ProfileLevel,
|
||||
MaxLevel: getMaxProfileLevel(gameVersion),
|
||||
},
|
||||
}
|
||||
}
|
@ -16,7 +16,17 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type { MissionStory, RequestWithJwt, SceneConfig } from "../types/types"
|
||||
import type {
|
||||
CompiledChallengeTreeCategory,
|
||||
GameVersion,
|
||||
JwtData,
|
||||
MissionManifest,
|
||||
MissionStory,
|
||||
SceneConfig,
|
||||
Unlockable,
|
||||
UserCentricContract,
|
||||
UserProfile,
|
||||
} from "../types/types"
|
||||
import { log, LogLevel } from "../loggingInterop"
|
||||
import { _legacyBull, _theLastYardbirdScpc, controller } from "../controller"
|
||||
import {
|
||||
@ -40,33 +50,79 @@ import {
|
||||
unlockOrderComparer,
|
||||
} from "../utils"
|
||||
|
||||
import type { Response } from "express"
|
||||
import { createInventory, getUnlockableById } from "../inventory"
|
||||
import { createSniperLoadouts } from "./sniper"
|
||||
import { getFlag } from "../flags"
|
||||
import { loadouts } from "../loadouts"
|
||||
import { resolveProfiles } from "../profileHandler"
|
||||
import { PlanningQuery } from "../types/gameSchemas"
|
||||
import { userAuths } from "../officialServerAuth"
|
||||
|
||||
export async function planningView(
|
||||
req: RequestWithJwt<PlanningQuery>,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
if (!req.query.contractid || !req.query.resetescalation) {
|
||||
res.status(400).send("invalid query")
|
||||
return
|
||||
}
|
||||
export type PlanningError = { error: boolean }
|
||||
|
||||
export type PlanningGroupData = {
|
||||
GroupId: string | undefined
|
||||
GroupTitle: string | undefined
|
||||
CompletedLevels: number | undefined
|
||||
Completed: boolean | undefined
|
||||
TotalLevels: number | undefined
|
||||
BestScore: number | undefined
|
||||
BestPlayer: string | undefined
|
||||
BestLevel: number | undefined
|
||||
}
|
||||
|
||||
export type GamePlanningData = {
|
||||
Contract: MissionManifest
|
||||
ElusiveContractState?: "not_completed" | string
|
||||
UserCentric?: UserCentricContract
|
||||
IsFirstInGroup: boolean
|
||||
Creator: UserProfile
|
||||
UserContract?: boolean
|
||||
UnlockedEntrances?: string[]
|
||||
UnlockedAgencyPickups?: string[]
|
||||
Objectives?: unknown
|
||||
GroupData?: PlanningGroupData
|
||||
Entrances: Unlockable[]
|
||||
Location: Unlockable
|
||||
LoadoutData: unknown
|
||||
LimitedLoadoutUnlockLevel: number
|
||||
CharacterLoadoutData?: unknown | null
|
||||
ChallengeData?: {
|
||||
Children: CompiledChallengeTreeCategory[]
|
||||
}
|
||||
Currency?: {
|
||||
Balance: number
|
||||
}
|
||||
PaymentDetails?: {
|
||||
Currency: "Merces" | string
|
||||
Amount: number | null
|
||||
MaximumDeduction: number | null
|
||||
Bonuses: null
|
||||
Expenses: null
|
||||
Entrance: null
|
||||
Pickup: null
|
||||
SideMission: null
|
||||
}
|
||||
OpportunityData?: unknown[]
|
||||
PlayerProfileXpData?: {
|
||||
XP: number
|
||||
Level: number
|
||||
MaxLevel: number
|
||||
}
|
||||
}
|
||||
|
||||
export async function getPlanningData(
|
||||
contractId: string,
|
||||
resetEscalation: boolean,
|
||||
jwt: JwtData,
|
||||
gameVersion: GameVersion,
|
||||
): Promise<PlanningError | GamePlanningData> {
|
||||
const entranceData = getConfig<SceneConfig>("Entrances", false)
|
||||
const missionStories = getConfig<Record<string, MissionStory>>(
|
||||
"MissionStories",
|
||||
false,
|
||||
)
|
||||
|
||||
const userData = getUserData(req.jwt.unique_name, req.gameVersion)
|
||||
|
||||
const isForReset = req.query.resetescalation === "true"
|
||||
const userData = getUserData(jwt.unique_name, gameVersion)
|
||||
|
||||
for (const ms in userData.Extensions.opportunityprogression) {
|
||||
if (Object.keys(missionStories).includes(ms)) {
|
||||
@ -75,26 +131,25 @@ export async function planningView(
|
||||
}
|
||||
|
||||
let contractData =
|
||||
req.gameVersion === "h1" &&
|
||||
req.query.contractid === "42bac555-bbb9-429d-a8ce-f1ffdf94211c"
|
||||
gameVersion === "h1" &&
|
||||
contractId === "42bac555-bbb9-429d-a8ce-f1ffdf94211c"
|
||||
? _legacyBull
|
||||
: req.query.contractid === "ff9f46cf-00bd-4c12-b887-eac491c3a96d"
|
||||
: contractId === "ff9f46cf-00bd-4c12-b887-eac491c3a96d"
|
||||
? _theLastYardbirdScpc
|
||||
: controller.resolveContract(req.query.contractid)
|
||||
: controller.resolveContract(contractId)
|
||||
|
||||
if (isForReset) {
|
||||
if (resetEscalation) {
|
||||
const escalationGroupId =
|
||||
contractData.Metadata.InGroup ?? contractData.Metadata.Id
|
||||
|
||||
resetUserEscalationProgress(userData, escalationGroupId)
|
||||
|
||||
writeUserData(req.jwt.unique_name, req.gameVersion)
|
||||
writeUserData(jwt.unique_name, gameVersion)
|
||||
|
||||
// now reassign properties and continue
|
||||
req.query.contractid =
|
||||
controller.escalationMappings.get(escalationGroupId)["1"]
|
||||
contractId = controller.escalationMappings.get(escalationGroupId)["1"]
|
||||
|
||||
contractData = controller.resolveContract(req.query.contractid)
|
||||
contractData = controller.resolveContract(contractId)
|
||||
}
|
||||
|
||||
if (!contractData) {
|
||||
@ -104,15 +159,13 @@ export async function planningView(
|
||||
// E.g. the user adds a contract to favorites, then deletes the files, then tries to load the contract again.
|
||||
log(
|
||||
LogLevel.WARN,
|
||||
`Trying to download contract ${req.query.contractid} due to it not found locally.`,
|
||||
`Trying to download contract ${contractId} due to it not found locally.`,
|
||||
)
|
||||
const user = userAuths.get(req.jwt.unique_name)
|
||||
const user = userAuths.get(jwt.unique_name)
|
||||
const resp = await user._useService(
|
||||
`https://${getRemoteService(
|
||||
req.gameVersion,
|
||||
)}.hitman.io/profiles/page/Planning?contractid=${
|
||||
req.query.contractid
|
||||
}&resetescalation=false&forcecurrentcontract=false&errorhandling=false`,
|
||||
gameVersion,
|
||||
)}.hitman.io/profiles/page/Planning?contractid=${contractId}&resetescalation=false&forcecurrentcontract=false&errorhandling=false`,
|
||||
true,
|
||||
)
|
||||
|
||||
@ -121,20 +174,19 @@ export async function planningView(
|
||||
}
|
||||
|
||||
if (!contractData) {
|
||||
log(LogLevel.ERROR, `Not found: ${req.query.contractid}, .`)
|
||||
res.status(400).send("no ct")
|
||||
return
|
||||
log(LogLevel.ERROR, `Not found: ${contractId}, .`)
|
||||
return { error: true }
|
||||
}
|
||||
|
||||
const groupData = {
|
||||
GroupId: undefined as string | undefined,
|
||||
GroupTitle: undefined as string | undefined,
|
||||
CompletedLevels: undefined as number | undefined,
|
||||
Completed: undefined as boolean | undefined,
|
||||
TotalLevels: undefined as number | undefined,
|
||||
BestScore: undefined as number | undefined,
|
||||
BestPlayer: undefined as string | undefined,
|
||||
BestLevel: undefined as number | undefined,
|
||||
const groupData: PlanningGroupData = {
|
||||
GroupId: undefined,
|
||||
GroupTitle: undefined,
|
||||
CompletedLevels: undefined,
|
||||
Completed: undefined,
|
||||
TotalLevels: undefined,
|
||||
BestScore: undefined,
|
||||
BestPlayer: undefined,
|
||||
BestLevel: undefined,
|
||||
}
|
||||
|
||||
const escalation = escalationTypes.includes(contractData.Metadata.Type)
|
||||
@ -171,9 +223,8 @@ export async function planningView(
|
||||
}
|
||||
|
||||
if (!contractData) {
|
||||
log(LogLevel.WARN, `Unknown contract: ${req.query.contractid}`)
|
||||
res.status(404).send("contract not found!")
|
||||
return
|
||||
log(LogLevel.WARN, `Unknown contract: ${contractId}`)
|
||||
return { error: true }
|
||||
}
|
||||
|
||||
const creatorProfile = (
|
||||
@ -182,7 +233,7 @@ export async function planningView(
|
||||
contractData.Metadata.CreatorUserId || "",
|
||||
"fadb923c-e6bb-4283-a537-eb4d1150262e",
|
||||
],
|
||||
req.gameVersion,
|
||||
gameVersion,
|
||||
)
|
||||
)[0]
|
||||
|
||||
@ -193,12 +244,9 @@ export async function planningView(
|
||||
`Looking up details for contract - Location:${contractData.Metadata.Location} (${scenePath})`,
|
||||
)
|
||||
|
||||
const sublocation = getSubLocationFromContract(
|
||||
contractData,
|
||||
req.gameVersion,
|
||||
)
|
||||
const sublocation = getSubLocationFromContract(contractData, gameVersion)
|
||||
|
||||
if (!Object.prototype.hasOwnProperty.call(entranceData, scenePath)) {
|
||||
if (!entranceData[scenePath]) {
|
||||
log(
|
||||
LogLevel.ERROR,
|
||||
`Could not find Entrance data for ${scenePath} (loc Planning)! This may cause an unhandled promise rejection.`,
|
||||
@ -207,11 +255,7 @@ export async function planningView(
|
||||
|
||||
const entrancesInScene = entranceData[scenePath]
|
||||
|
||||
const typedInv = createInventory(
|
||||
req.jwt.unique_name,
|
||||
req.gameVersion,
|
||||
sublocation,
|
||||
)
|
||||
const typedInv = createInventory(jwt.unique_name, gameVersion, sublocation)
|
||||
|
||||
const unlockedEntrances = typedInv
|
||||
.filter((item) => item.Unlockable.Type === "access")
|
||||
@ -229,10 +273,10 @@ export async function planningView(
|
||||
|
||||
// Default loadout
|
||||
|
||||
let currentLoadout = loadouts.getLoadoutFor(req.gameVersion)
|
||||
let currentLoadout = loadouts.getLoadoutFor(gameVersion)
|
||||
|
||||
if (!currentLoadout) {
|
||||
currentLoadout = loadouts.createDefault(req.gameVersion)
|
||||
currentLoadout = loadouts.createDefault(gameVersion)
|
||||
}
|
||||
|
||||
let pistol = "FIREARMS_HERO_PISTOL_TACTICAL_ICA_19"
|
||||
@ -242,8 +286,6 @@ export async function planningView(
|
||||
let briefcaseProp: string | undefined = undefined
|
||||
let briefcaseId: string | undefined = undefined
|
||||
|
||||
const hasOwn = Object.prototype.hasOwnProperty.bind(currentLoadout.data)
|
||||
|
||||
const dlForLocation =
|
||||
getFlag("loadoutSaving") === "LEGACY"
|
||||
? // older default loadout setting (per-person)
|
||||
@ -251,8 +293,10 @@ export async function planningView(
|
||||
contractData.Metadata.Location
|
||||
]
|
||||
: // new loadout profiles system
|
||||
hasOwn(contractData.Metadata.Location) &&
|
||||
currentLoadout.data[contractData.Metadata.Location]
|
||||
Object.hasOwn(
|
||||
currentLoadout.data,
|
||||
contractData.Metadata.Location,
|
||||
) && currentLoadout.data[contractData.Metadata.Location]
|
||||
|
||||
if (dlForLocation) {
|
||||
pistol = dlForLocation["2"]
|
||||
@ -275,21 +319,21 @@ export async function planningView(
|
||||
|
||||
const userCentric = generateUserCentric(
|
||||
contractData,
|
||||
req.jwt.unique_name,
|
||||
req.gameVersion,
|
||||
jwt.unique_name,
|
||||
gameVersion,
|
||||
)
|
||||
|
||||
const sniperLoadouts = createSniperLoadouts(
|
||||
req.jwt.unique_name,
|
||||
req.gameVersion,
|
||||
jwt.unique_name,
|
||||
gameVersion,
|
||||
contractData,
|
||||
)
|
||||
|
||||
if (req.gameVersion === "scpc") {
|
||||
sniperLoadouts.forEach((loadout) => {
|
||||
if (gameVersion === "scpc") {
|
||||
for (const loadout of sniperLoadouts) {
|
||||
loadout["LoadoutData"] = loadout["Loadout"]["LoadoutData"]
|
||||
delete loadout["Loadout"]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let loadoutSlots = [
|
||||
@ -368,6 +412,7 @@ export async function planningView(
|
||||
|
||||
/**
|
||||
* Handles loadout lock for Miami and Hokkaido
|
||||
* TODO: migrate this to use the actual game data.
|
||||
*/
|
||||
const limitedLoadoutUnlockLevelMap = {
|
||||
LOCATION_MIAMI: 2,
|
||||
@ -380,19 +425,19 @@ export async function planningView(
|
||||
getFlag("enableMasteryProgression")
|
||||
) {
|
||||
const loadoutUnlockable = getUnlockableById(
|
||||
req.gameVersion === "h1"
|
||||
gameVersion === "h1"
|
||||
? sublocation?.Properties?.NormalLoadoutUnlock[
|
||||
contractData.Metadata.Difficulty ?? "normal"
|
||||
]
|
||||
: sublocation?.Properties?.NormalLoadoutUnlock,
|
||||
req.gameVersion,
|
||||
gameVersion,
|
||||
)
|
||||
|
||||
if (loadoutUnlockable) {
|
||||
const loadoutMasteryData =
|
||||
controller.masteryService.getMasteryForUnlockable(
|
||||
loadoutUnlockable,
|
||||
req.gameVersion,
|
||||
gameVersion,
|
||||
)
|
||||
|
||||
const locationProgression =
|
||||
@ -412,109 +457,96 @@ export async function planningView(
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
template:
|
||||
req.gameVersion === "h1"
|
||||
? getConfig("LegacyPlanningTemplate", false)
|
||||
: req.gameVersion === "scpc"
|
||||
? getConfig("FrankensteinPlanningTemplate", false)
|
||||
: null,
|
||||
data: {
|
||||
Contract: contractData,
|
||||
ElusiveContractState: "not_completed",
|
||||
UserCentric: userCentric,
|
||||
IsFirstInGroup: escalation ? groupData.CompletedLevels === 0 : true,
|
||||
Creator: creatorProfile,
|
||||
UserContract: creatorProfile.DevId !== "IOI",
|
||||
UnlockedEntrances:
|
||||
contractData.Metadata.Type === "sniper"
|
||||
? null
|
||||
: typedInv
|
||||
.filter(
|
||||
(item) =>
|
||||
item.Unlockable.Subtype ===
|
||||
"startinglocation",
|
||||
)
|
||||
.filter(
|
||||
(item) =>
|
||||
item.Unlockable.Properties.Difficulty ===
|
||||
contractData.Metadata.Difficulty,
|
||||
)
|
||||
.map((i) => i.Unlockable.Properties.RepositoryId)
|
||||
.filter((id) => id),
|
||||
UnlockedAgencyPickups:
|
||||
contractData.Metadata.Type === "sniper"
|
||||
? null
|
||||
: typedInv
|
||||
.filter(
|
||||
(item) => item.Unlockable.Type === "agencypickup",
|
||||
)
|
||||
.filter(
|
||||
(item) =>
|
||||
item.Unlockable.Properties.Difficulty ===
|
||||
contractData.Metadata.Difficulty,
|
||||
)
|
||||
.map((i) => i.Unlockable.Properties.RepositoryId)
|
||||
.filter((id) => id),
|
||||
Objectives: mapObjectives(
|
||||
contractData.Data.Objectives,
|
||||
contractData.Data.GameChangers || [],
|
||||
contractData.Metadata.GroupObjectiveDisplayOrder || [],
|
||||
contractData.Metadata.IsEvergreenSafehouse,
|
||||
return {
|
||||
Contract: contractData,
|
||||
ElusiveContractState: "not_completed",
|
||||
UserCentric: userCentric,
|
||||
IsFirstInGroup: escalation ? groupData.CompletedLevels === 0 : true,
|
||||
Creator: creatorProfile,
|
||||
UserContract: creatorProfile.DevId !== "IOI",
|
||||
UnlockedEntrances:
|
||||
contractData.Metadata.Type === "sniper"
|
||||
? null
|
||||
: typedInv
|
||||
.filter(
|
||||
(item) =>
|
||||
item.Unlockable.Subtype === "startinglocation",
|
||||
)
|
||||
.filter(
|
||||
(item) =>
|
||||
item.Unlockable.Properties.Difficulty ===
|
||||
contractData.Metadata.Difficulty,
|
||||
)
|
||||
.map((i) => i.Unlockable.Properties.RepositoryId)
|
||||
.filter((id) => id),
|
||||
UnlockedAgencyPickups:
|
||||
contractData.Metadata.Type === "sniper"
|
||||
? null
|
||||
: typedInv
|
||||
.filter((item) => item.Unlockable.Type === "agencypickup")
|
||||
.filter(
|
||||
(item) =>
|
||||
item.Unlockable.Properties.Difficulty ===
|
||||
contractData.Metadata.Difficulty,
|
||||
)
|
||||
.map((i) => i.Unlockable.Properties.RepositoryId)
|
||||
.filter((id) => id),
|
||||
Objectives: mapObjectives(
|
||||
contractData.Data.Objectives,
|
||||
contractData.Data.GameChangers || [],
|
||||
contractData.Metadata.GroupObjectiveDisplayOrder || [],
|
||||
contractData.Metadata.IsEvergreenSafehouse,
|
||||
),
|
||||
GroupData: groupData,
|
||||
Entrances:
|
||||
contractData.Metadata.Type === "sniper"
|
||||
? null
|
||||
: unlockedEntrances
|
||||
.filter((unlockable) =>
|
||||
entrancesInScene.includes(
|
||||
unlockable.Properties.RepositoryId,
|
||||
),
|
||||
)
|
||||
.filter(
|
||||
(unlockable) =>
|
||||
unlockable.Properties.Difficulty ===
|
||||
contractData.Metadata.Difficulty,
|
||||
)
|
||||
.sort(unlockOrderComparer),
|
||||
Location: sublocation,
|
||||
LoadoutData:
|
||||
contractData.Metadata.Type === "sniper" ? null : loadoutSlots,
|
||||
LimitedLoadoutUnlockLevel:
|
||||
limitedLoadoutUnlockLevelMap[sublocation.Id] ?? 0,
|
||||
CharacterLoadoutData:
|
||||
sniperLoadouts.length !== 0 ? sniperLoadouts : null,
|
||||
ChallengeData: {
|
||||
Children: controller.challengeService.getChallengeTreeForContract(
|
||||
contractId,
|
||||
gameVersion,
|
||||
jwt.unique_name,
|
||||
),
|
||||
GroupData: groupData,
|
||||
Entrances:
|
||||
contractData.Metadata.Type === "sniper"
|
||||
? null
|
||||
: unlockedEntrances
|
||||
.filter((unlockable) =>
|
||||
entrancesInScene.includes(
|
||||
unlockable.Properties.RepositoryId,
|
||||
),
|
||||
)
|
||||
.filter(
|
||||
(unlockable) =>
|
||||
unlockable.Properties.Difficulty ===
|
||||
contractData.Metadata.Difficulty,
|
||||
)
|
||||
.sort(unlockOrderComparer),
|
||||
Location: sublocation,
|
||||
LoadoutData:
|
||||
contractData.Metadata.Type === "sniper" ? null : loadoutSlots,
|
||||
LimitedLoadoutUnlockLevel:
|
||||
limitedLoadoutUnlockLevelMap[sublocation.Id] ?? 0,
|
||||
CharacterLoadoutData:
|
||||
sniperLoadouts.length !== 0 ? sniperLoadouts : null,
|
||||
ChallengeData: {
|
||||
Children:
|
||||
controller.challengeService.getChallengeTreeForContract(
|
||||
req.query.contractid,
|
||||
req.gameVersion,
|
||||
req.jwt.unique_name,
|
||||
),
|
||||
},
|
||||
Currency: {
|
||||
Balance: 0,
|
||||
},
|
||||
PaymentDetails: {
|
||||
Currency: "Merces",
|
||||
Amount: 0,
|
||||
MaximumDeduction: 85,
|
||||
Bonuses: null,
|
||||
Expenses: null,
|
||||
Entrance: null,
|
||||
Pickup: null,
|
||||
SideMission: null,
|
||||
},
|
||||
OpportunityData: (contractData.Metadata.Opportunities || [])
|
||||
.map((value) => missionStories[value])
|
||||
.filter(Boolean),
|
||||
PlayerProfileXpData: {
|
||||
XP: userData.Extensions.progression.PlayerProfileXP.Total,
|
||||
Level: userData.Extensions.progression.PlayerProfileXP
|
||||
.ProfileLevel,
|
||||
MaxLevel: getMaxProfileLevel(req.gameVersion),
|
||||
},
|
||||
},
|
||||
})
|
||||
Currency: {
|
||||
Balance: 0,
|
||||
},
|
||||
PaymentDetails: {
|
||||
Currency: "Merces",
|
||||
Amount: 0,
|
||||
MaximumDeduction: 85,
|
||||
Bonuses: null,
|
||||
Expenses: null,
|
||||
Entrance: null,
|
||||
Pickup: null,
|
||||
SideMission: null,
|
||||
},
|
||||
OpportunityData: (contractData.Metadata.Opportunities || [])
|
||||
.map((value) => missionStories[value])
|
||||
.filter(Boolean),
|
||||
PlayerProfileXpData: {
|
||||
XP: userData.Extensions.progression.PlayerProfileXP.Total,
|
||||
Level: userData.Extensions.progression.PlayerProfileXP.ProfileLevel,
|
||||
MaxLevel: getMaxProfileLevel(gameVersion),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -21,11 +21,16 @@ import { generateUserCentric } from "../contracts/dataGen"
|
||||
import { controller } from "../controller"
|
||||
import type {
|
||||
GameVersion,
|
||||
JwtData,
|
||||
MissionStory,
|
||||
PlayNextCampaignDetails,
|
||||
UserCentricContract,
|
||||
} from "../types/types"
|
||||
|
||||
export const orderedMissions: string[] = [
|
||||
/**
|
||||
* Main story campaign ordered mission IDs.
|
||||
*/
|
||||
export const orderedMainCampaignMissions: string[] = [
|
||||
"00000000-0000-0000-0000-000000000200",
|
||||
"00000000-0000-0000-0000-000000000600",
|
||||
"00000000-0000-0000-0000-000000000400",
|
||||
@ -48,6 +53,9 @@ export const orderedMissions: string[] = [
|
||||
"a3e19d55-64a6-4282-bb3c-d18c3f3e6e29",
|
||||
]
|
||||
|
||||
/**
|
||||
* Ordered Patient Zero campaign mission IDs.
|
||||
*/
|
||||
export const orderedPZMissions: string[] = [
|
||||
"024b6964-a3bb-4457-b085-08f9a7dc7fb7",
|
||||
"7e3f758a-2435-42de-93bd-d8f0b72c63a4",
|
||||
@ -55,6 +63,9 @@ export const orderedPZMissions: string[] = [
|
||||
"a2befcec-7799-4987-9215-6a152cb6a320",
|
||||
]
|
||||
|
||||
/**
|
||||
* Ordered sniper campaign mission IDs.
|
||||
*/
|
||||
export const sniperMissionIds: string[] = [
|
||||
"ff9f46cf-00bd-4c12-b887-eac491c3a96d",
|
||||
"00e57709-e049-44c9-a2c3-7655e19884fb",
|
||||
@ -62,13 +73,9 @@ export const sniperMissionIds: string[] = [
|
||||
]
|
||||
|
||||
/**
|
||||
* Gets the ID for a season.
|
||||
*
|
||||
* @param index The index in orderedMissions.
|
||||
* @returns The season's ID. ("1", "2", or "3")
|
||||
* @see orderedMissions
|
||||
* Gets the ID for a season based on the main mission index.
|
||||
*/
|
||||
export function getSeasonId(index: number): string {
|
||||
function getSeasonId(index: number): string {
|
||||
if (index <= 5) {
|
||||
return "1"
|
||||
}
|
||||
@ -81,7 +88,7 @@ export function getSeasonId(index: number): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a tile for play next given a contract ID and other details.
|
||||
* Generates a "Continue Story" tile for play next given a contract ID and other details.
|
||||
*
|
||||
* @param userId The user's ID.
|
||||
* @param contractId The next contract ID.
|
||||
@ -89,12 +96,12 @@ export function getSeasonId(index: number): string {
|
||||
* @param campaignInfo The campaign information.
|
||||
* @returns The tile object.
|
||||
*/
|
||||
export function createPlayNextTile(
|
||||
export function createPlayNextMission(
|
||||
userId: string,
|
||||
contractId: string,
|
||||
gameVersion: GameVersion,
|
||||
campaignInfo: PlayNextCampaignDetails,
|
||||
) {
|
||||
): PlayNextCategory {
|
||||
return {
|
||||
CategoryType: "NextMission",
|
||||
CategoryName: "UI_PLAYNEXT_CONTINUE_STORY_TITLE",
|
||||
@ -117,13 +124,32 @@ export function createPlayNextTile(
|
||||
}
|
||||
}
|
||||
|
||||
export type PlayNextCategory = {
|
||||
CategoryType: "NextMission" | "MainOpportunity" | "MenuPage"
|
||||
CategoryName: string
|
||||
Items: {
|
||||
ItemType: null | unknown
|
||||
ContentType: "Contract" | "Opportunity" | "MenuPage"
|
||||
Content: {
|
||||
ContractId?: string
|
||||
RepositoryId?: string
|
||||
Name?: string
|
||||
UserCentricContract?: UserCentricContract
|
||||
CampaignInfo?: PlayNextCampaignDetails
|
||||
}
|
||||
CategoryType: "NextMission" | "MainOpportunity" | "MenuPage"
|
||||
}[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates tiles for recommended mission stories given a contract ID.
|
||||
*
|
||||
* @param contractId The contract ID.
|
||||
* @returns The tile object.
|
||||
*/
|
||||
export function createMainOpportunityTile(contractId: string) {
|
||||
export function createMainOpportunityTile(
|
||||
contractId: string,
|
||||
): PlayNextCategory {
|
||||
const contractData = controller.resolveContract(contractId)
|
||||
|
||||
const missionStories = getConfig<Record<string, MissionStory>>(
|
||||
@ -153,7 +179,7 @@ export function createMainOpportunityTile(contractId: string) {
|
||||
* @param menuPages An array of menu page IDs.
|
||||
* @returns The tile object
|
||||
*/
|
||||
export function createMenuPageTile(...menuPages: string[]) {
|
||||
export function createMenuPageTile(...menuPages: string[]): PlayNextCategory {
|
||||
// This is all based on what sniper does, not sure if any others have it.
|
||||
return {
|
||||
CategoryType: "MenuPage",
|
||||
@ -168,3 +194,105 @@ export function createMenuPageTile(...menuPages: string[]) {
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
export type GameFacingPlayNextData = {
|
||||
Categories: PlayNextCategory[]
|
||||
ProfileId: string
|
||||
}
|
||||
|
||||
export function getGamePlayNextData(
|
||||
contractId: string,
|
||||
jwt: JwtData,
|
||||
gameVersion: GameVersion,
|
||||
): GameFacingPlayNextData {
|
||||
const cats: PlayNextCategory[] = []
|
||||
|
||||
const currentIdIndex = orderedMainCampaignMissions.indexOf(contractId)
|
||||
|
||||
if (
|
||||
currentIdIndex !== -1 &&
|
||||
currentIdIndex !== orderedMainCampaignMissions.length - 1
|
||||
) {
|
||||
const nextMissionId = orderedMainCampaignMissions[currentIdIndex + 1]
|
||||
const nextSeasonId = getSeasonId(currentIdIndex + 1)
|
||||
|
||||
let shouldContinue = true
|
||||
|
||||
// nextSeasonId > gameVersion's integer
|
||||
if (parseInt(nextSeasonId) > parseInt(gameVersion[1])) {
|
||||
shouldContinue = false
|
||||
}
|
||||
|
||||
if (shouldContinue) {
|
||||
cats.push(
|
||||
createPlayNextMission(
|
||||
jwt.unique_name,
|
||||
nextMissionId,
|
||||
gameVersion,
|
||||
{
|
||||
CampaignName: `UI_SEASON_${nextSeasonId}`,
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
cats.push(createMainOpportunityTile(contractId))
|
||||
}
|
||||
|
||||
const pzIdIndex = orderedPZMissions.indexOf(contractId)
|
||||
|
||||
if (pzIdIndex !== -1 && pzIdIndex !== orderedPZMissions.length - 1) {
|
||||
const nextMissionId = orderedPZMissions[pzIdIndex + 1]
|
||||
cats.push(
|
||||
createPlayNextMission(jwt.unique_name, nextMissionId, gameVersion, {
|
||||
CampaignName: "UI_CONTRACT_CAMPAIGN_WHITE_SPIDER_TITLE",
|
||||
ParentCampaignName: "UI_MENU_PAGE_SIDE_MISSIONS_TITLE",
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// Atlantide mini-campaign (Miami Cottonmouth -> Skunk Gartersnake)
|
||||
if (contractId === "f1ba328f-e3dd-4ef8-bb26-0363499fdd95") {
|
||||
const nextMissionId = "0b616e62-af0c-495b-82e3-b778e82b5912"
|
||||
cats.push(
|
||||
createPlayNextMission(jwt.unique_name, nextMissionId, gameVersion, {
|
||||
CampaignName: "UI_MENU_PAGE_SPECIAL_ASSIGNMENTS_TITLE",
|
||||
ParentCampaignName: "UI_MENU_PAGE_SIDE_MISSIONS_TITLE",
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
if (sniperMissionIds.includes(contractId)) {
|
||||
cats.push(createMenuPageTile("sniper"))
|
||||
}
|
||||
|
||||
const pluginData = controller.hooks.getNextCampaignMission.call(
|
||||
contractId,
|
||||
gameVersion,
|
||||
)
|
||||
|
||||
if (pluginData) {
|
||||
if (pluginData.overrideIndex !== undefined) {
|
||||
cats[pluginData.overrideIndex] = createPlayNextMission(
|
||||
jwt.unique_name,
|
||||
pluginData.nextContractId,
|
||||
gameVersion,
|
||||
pluginData.campaignDetails,
|
||||
)
|
||||
} else {
|
||||
cats.push(
|
||||
createPlayNextMission(
|
||||
jwt.unique_name,
|
||||
pluginData.nextContractId,
|
||||
gameVersion,
|
||||
pluginData.campaignDetails,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
Categories: cats,
|
||||
ProfileId: jwt.unique_name,
|
||||
}
|
||||
}
|
||||
|
469
components/menus/stashpoints.ts
Normal file
469
components/menus/stashpoints.ts
Normal file
@ -0,0 +1,469 @@
|
||||
/*
|
||||
* 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 { createInventory, getUnlockableById, InventoryItem } from "../inventory"
|
||||
import type {
|
||||
GameVersion,
|
||||
JwtData,
|
||||
MissionManifest,
|
||||
SafehouseCategory,
|
||||
UserCentricContract,
|
||||
} from "../types/types"
|
||||
import {
|
||||
SafehouseCategoryQuery,
|
||||
StashpointQuery,
|
||||
StashpointQueryH2016,
|
||||
StashpointSlotName,
|
||||
} from "../types/gameSchemas"
|
||||
import { getDefaultSuitFor, uuidRegex } from "../utils"
|
||||
import { controller } from "../controller"
|
||||
import { generateUserCentric, getSubLocationByName } from "../contracts/dataGen"
|
||||
import { log, LogLevel } from "../loggingInterop"
|
||||
import { getUserData } from "../databaseHandler"
|
||||
import { getFlag } from "../flags"
|
||||
import { loadouts } from "../loadouts"
|
||||
|
||||
/**
|
||||
* Algorithm to get the stashpoint items data for H2 and H3.
|
||||
*
|
||||
* @param inventory The user's inventory.
|
||||
* @param query The input query for the stashpoint.
|
||||
* @param gameVersion
|
||||
* @param contractData The optional contract data.
|
||||
*/
|
||||
export function getModernStashItemsData(
|
||||
inventory: InventoryItem[],
|
||||
query: StashpointQuery,
|
||||
gameVersion: GameVersion,
|
||||
contractData: MissionManifest | undefined,
|
||||
) {
|
||||
return inventory
|
||||
.filter((item) => {
|
||||
if (
|
||||
(query.slotname === "gear" &&
|
||||
contractData?.Peacock?.noGear === true) ||
|
||||
(query.slotname === "concealedweapon" &&
|
||||
contractData?.Peacock?.noCarriedWeapon === true)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (
|
||||
item.Unlockable.Subtype === "disguise" &&
|
||||
gameVersion === "h3"
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
return (
|
||||
item.Unlockable.Properties.LoadoutSlot && // only display items
|
||||
(!query.slotname ||
|
||||
((uuidRegex.test(query.slotid as string) || // container
|
||||
query.slotname === "stashpoint") && // stashpoint
|
||||
item.Unlockable.Properties.LoadoutSlot !==
|
||||
"disguise") || // container or stashpoint => display all items
|
||||
item.Unlockable.Properties.LoadoutSlot ===
|
||||
query.slotname) && // else: display items for requested slot
|
||||
(query.allowcontainers === "true" ||
|
||||
!item.Unlockable.Properties.IsContainer) &&
|
||||
(query.allowlargeitems === "true" ||
|
||||
item.Unlockable.Properties.ItemSize === // regular gear slot or hidden stash => small item
|
||||
"ITEMSIZE_SMALL" ||
|
||||
(!item.Unlockable.Properties.ItemSize &&
|
||||
item.Unlockable.Properties.LoadoutSlot !== // use old logic if itemsize is not set
|
||||
"carriedweapon")) &&
|
||||
item.Unlockable.Type !== "challengemultiplier" &&
|
||||
!item.Unlockable.Properties.InclusionData
|
||||
) // not sure about this one
|
||||
})
|
||||
.map((item) => ({
|
||||
Item: item,
|
||||
ItemDetails: {
|
||||
Capabilities: [],
|
||||
StatList: item.Unlockable.Properties.Gameplay
|
||||
? Object.entries(item.Unlockable.Properties.Gameplay).map(
|
||||
([key, value]) => ({
|
||||
Name: key,
|
||||
Ratio: value,
|
||||
}),
|
||||
)
|
||||
: [],
|
||||
PropertyTexts: [],
|
||||
},
|
||||
SlotId: query.slotid,
|
||||
SlotName: null,
|
||||
}))
|
||||
}
|
||||
|
||||
export type ModernStashData = {
|
||||
SlotId: string | number
|
||||
LoadoutItemsData: unknown
|
||||
UserCentric?: UserCentricContract
|
||||
ShowSlotName: string | number
|
||||
}
|
||||
|
||||
/**
|
||||
* Algorithm to get the stashpoint data for H2 and H3.
|
||||
*
|
||||
* @param query The stashpoint query.
|
||||
* @param userId
|
||||
* @param gameVersion
|
||||
* @returns undefined if the query is invalid, or the stash data.
|
||||
*/
|
||||
export function getModernStashData(
|
||||
query: StashpointQuery,
|
||||
userId: string,
|
||||
gameVersion: GameVersion,
|
||||
): ModernStashData {
|
||||
let contractData: MissionManifest | undefined = undefined
|
||||
|
||||
if (query.contractid) {
|
||||
contractData = controller.resolveContract(query.contractid)
|
||||
}
|
||||
|
||||
const inventory = createInventory(
|
||||
userId,
|
||||
gameVersion,
|
||||
getSubLocationByName(contractData?.Metadata.Location, gameVersion),
|
||||
)
|
||||
|
||||
if (query.slotname.endsWith(query.slotid!.toString())) {
|
||||
query.slotname = query.slotname.slice(
|
||||
0,
|
||||
-query.slotid!.toString().length,
|
||||
) // weird
|
||||
}
|
||||
|
||||
const stashData: ModernStashData = {
|
||||
SlotId: query.slotid,
|
||||
LoadoutItemsData: {
|
||||
SlotId: query.slotid,
|
||||
Items: getModernStashItemsData(
|
||||
inventory,
|
||||
query,
|
||||
gameVersion,
|
||||
contractData,
|
||||
),
|
||||
Page: 0,
|
||||
HasMore: false,
|
||||
HasMoreLeft: false,
|
||||
HasMoreRight: false,
|
||||
OptionalData: {
|
||||
stashpoint: query.stashpoint || "",
|
||||
AllowLargeItems: query.allowlargeitems,
|
||||
AllowContainers: query.allowcontainers, // ?? true
|
||||
},
|
||||
},
|
||||
ShowSlotName: query.slotname,
|
||||
}
|
||||
|
||||
if (contractData) {
|
||||
stashData.UserCentric = generateUserCentric(
|
||||
contractData,
|
||||
userId,
|
||||
gameVersion,
|
||||
)
|
||||
}
|
||||
|
||||
return stashData
|
||||
}
|
||||
|
||||
/**
|
||||
* Algorithm to get the stashpoint items data for H2016.
|
||||
*
|
||||
* @param inventory The user's inventory.
|
||||
* @param slotname The slot name.
|
||||
* @param query
|
||||
* @param slotid The slot id.
|
||||
*/
|
||||
export function getLegacyStashItems(
|
||||
inventory: InventoryItem[],
|
||||
slotname: StashpointSlotName,
|
||||
query: StashpointQueryH2016,
|
||||
slotid: number,
|
||||
) {
|
||||
return inventory
|
||||
.filter((item) => {
|
||||
return (
|
||||
item.Unlockable.Properties.LoadoutSlot && // only display items
|
||||
(item.Unlockable.Properties.LoadoutSlot === slotname || // display items for requested slot
|
||||
(slotname === "stashpoint" && // else: if stashpoint
|
||||
item.Unlockable.Properties.LoadoutSlot !==
|
||||
"disguise")) && // => display all non-disguise items
|
||||
(query.allowlargeitems === "true" ||
|
||||
item.Unlockable.Properties.ItemSize === // regular gear slot or hidden stash => small item
|
||||
"ITEMSIZE_SMALL" ||
|
||||
(!item.Unlockable.Properties.ItemSize &&
|
||||
item.Unlockable.Properties.LoadoutSlot !== // use old logic if itemsize is not set
|
||||
"carriedweapon")) &&
|
||||
item.Unlockable.Type !== "challengemultipler" &&
|
||||
!item.Unlockable.Properties.InclusionData
|
||||
) // not sure about this one
|
||||
})
|
||||
.map((item) => ({
|
||||
Item: item,
|
||||
ItemDetails: {
|
||||
Capabilities: [],
|
||||
StatList: item.Unlockable.Properties.Gameplay
|
||||
? Object.entries(item.Unlockable.Properties.Gameplay).map(
|
||||
([key, value]) => ({
|
||||
Name: key,
|
||||
Ratio: value,
|
||||
}),
|
||||
)
|
||||
: [],
|
||||
PropertyTexts: [],
|
||||
},
|
||||
SlotId: slotid.toString(),
|
||||
SlotName: slotname,
|
||||
}))
|
||||
}
|
||||
|
||||
const loadoutSlots: StashpointSlotName[] = [
|
||||
"carriedweapon",
|
||||
"carrieditem",
|
||||
"concealedweapon",
|
||||
"disguise",
|
||||
"gear",
|
||||
"gear",
|
||||
"stashpoint",
|
||||
]
|
||||
|
||||
/**
|
||||
* Algorithm to get the stashpoint data for H2016.
|
||||
*
|
||||
* @param query The stashpoint query.
|
||||
* @param userId
|
||||
* @param gameVersion
|
||||
*/
|
||||
export function getLegacyStashData(
|
||||
query: StashpointQueryH2016,
|
||||
userId: string,
|
||||
gameVersion: GameVersion,
|
||||
) {
|
||||
const contractData = controller.resolveContract(query.contractid)
|
||||
|
||||
if (!contractData) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (!loadoutSlots.includes(query.slotname.slice(0, -1))) {
|
||||
log(
|
||||
LogLevel.ERROR,
|
||||
`Unknown slotname in legacy stashpoint: ${query.slotname}`,
|
||||
)
|
||||
return undefined
|
||||
}
|
||||
|
||||
const userProfile = getUserData(userId, gameVersion)
|
||||
|
||||
const sublocation = getSubLocationByName(
|
||||
contractData.Metadata.Location,
|
||||
gameVersion,
|
||||
)
|
||||
|
||||
const inventory = createInventory(userId, gameVersion, sublocation)
|
||||
|
||||
const userCentricContract = generateUserCentric(
|
||||
contractData,
|
||||
userId,
|
||||
gameVersion,
|
||||
)
|
||||
|
||||
const defaultLoadout = {
|
||||
2: "FIREARMS_HERO_PISTOL_TACTICAL_001_SU_SKIN01",
|
||||
3: getDefaultSuitFor(sublocation),
|
||||
4: "TOKEN_FIBERWIRE",
|
||||
5: "PROP_TOOL_COIN",
|
||||
}
|
||||
|
||||
const getLoadoutItem = (id: number) => {
|
||||
if (getFlag("loadoutSaving") === "LEGACY") {
|
||||
const dl = userProfile.Extensions.defaultloadout
|
||||
|
||||
if (!dl) {
|
||||
return defaultLoadout[id]
|
||||
}
|
||||
|
||||
const forLocation = (userProfile.Extensions.defaultloadout || {})[
|
||||
sublocation?.Properties?.ParentLocation
|
||||
]
|
||||
|
||||
return (forLocation || defaultLoadout)[id]
|
||||
} else {
|
||||
let dl = loadouts.getLoadoutFor("h1")
|
||||
|
||||
if (!dl) {
|
||||
dl = loadouts.createDefault("h1")
|
||||
}
|
||||
|
||||
const forLocation = dl.data[sublocation?.Properties?.ParentLocation]
|
||||
|
||||
return (forLocation || defaultLoadout)[id]
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ContractId: query.contractid,
|
||||
// the game actually only needs the loadoutdata from the requested slotid, but this is what IOI servers do
|
||||
LoadoutData: [...loadoutSlots.entries()].map(([slotid, slotname]) => ({
|
||||
SlotName: slotname,
|
||||
SlotId: slotid.toString(),
|
||||
Items: getLegacyStashItems(inventory, slotname, query, slotid),
|
||||
Page: 0,
|
||||
Recommended: getLoadoutItem(slotid)
|
||||
? {
|
||||
item: getUnlockableById(
|
||||
getLoadoutItem(slotid),
|
||||
gameVersion,
|
||||
),
|
||||
type: loadoutSlots[slotid],
|
||||
owned: true,
|
||||
}
|
||||
: null,
|
||||
HasMore: false,
|
||||
HasMoreLeft: false,
|
||||
HasMoreRight: false,
|
||||
OptionalData:
|
||||
slotid === 6
|
||||
? {
|
||||
stashpoint: query.stashpoint,
|
||||
AllowLargeItems:
|
||||
query.allowlargeitems || !query.stashpoint,
|
||||
}
|
||||
: {},
|
||||
})),
|
||||
Contract: userCentricContract.Contract,
|
||||
ShowSlotName: query.slotname,
|
||||
UserCentric: userCentricContract,
|
||||
}
|
||||
}
|
||||
|
||||
export function getSafehouseCategory(
|
||||
query: SafehouseCategoryQuery,
|
||||
gameVersion: GameVersion,
|
||||
jwt: JwtData,
|
||||
) {
|
||||
const inventory = createInventory(jwt.unique_name, gameVersion)
|
||||
|
||||
let safehouseData: SafehouseCategory = {
|
||||
Category: "_root",
|
||||
SubCategories: [],
|
||||
IsLeaf: false,
|
||||
Data: null,
|
||||
}
|
||||
|
||||
for (const item of inventory) {
|
||||
if (query.type) {
|
||||
// if type is specified in query
|
||||
if (item.Unlockable.Type !== query.type) {
|
||||
continue // skip all items that are not that type
|
||||
}
|
||||
|
||||
if (query.subtype && item.Unlockable.Subtype !== query.subtype) {
|
||||
// if subtype is specified
|
||||
continue // skip all items that are not that subtype
|
||||
}
|
||||
} else if (
|
||||
item.Unlockable.Type === "access" ||
|
||||
item.Unlockable.Type === "location" ||
|
||||
item.Unlockable.Type === "package" ||
|
||||
item.Unlockable.Type === "loadoutunlock" ||
|
||||
item.Unlockable.Type === "difficultyunlock" ||
|
||||
item.Unlockable.Type === "agencypickup" ||
|
||||
item.Unlockable.Type === "challengemultiplier"
|
||||
) {
|
||||
continue // these types should not be displayed when not asked for
|
||||
} else if (item.Unlockable.Properties.InclusionData) {
|
||||
// Only sniper unlockables have inclusion data, don't show them
|
||||
continue
|
||||
}
|
||||
|
||||
if (item.Unlockable.Subtype === "disguise" && gameVersion === "h3") {
|
||||
continue // I don't want to put this in that elif statement
|
||||
}
|
||||
|
||||
let category = safehouseData.SubCategories.find(
|
||||
(cat) => cat.Category === item.Unlockable.Type,
|
||||
)
|
||||
let subcategory
|
||||
|
||||
if (!category) {
|
||||
category = {
|
||||
Category: item.Unlockable.Type,
|
||||
SubCategories: [],
|
||||
IsLeaf: false,
|
||||
Data: null,
|
||||
}
|
||||
safehouseData.SubCategories.push(category)
|
||||
}
|
||||
|
||||
subcategory = category.SubCategories.find(
|
||||
(cat) => cat.Category === item.Unlockable.Subtype,
|
||||
)
|
||||
|
||||
if (!subcategory) {
|
||||
subcategory = {
|
||||
Category: item.Unlockable.Subtype,
|
||||
SubCategories: null,
|
||||
IsLeaf: true,
|
||||
Data: {
|
||||
Type: item.Unlockable.Type,
|
||||
SubType: item.Unlockable.Subtype,
|
||||
Items: [],
|
||||
Page: 0,
|
||||
HasMore: false,
|
||||
},
|
||||
}
|
||||
category.SubCategories.push(subcategory)
|
||||
}
|
||||
|
||||
subcategory.Data?.Items.push({
|
||||
Item: item,
|
||||
ItemDetails: {
|
||||
Capabilities: [],
|
||||
StatList: item.Unlockable.Properties.Gameplay
|
||||
? Object.entries(item.Unlockable.Properties.Gameplay).map(
|
||||
([key, value]) => ({
|
||||
Name: key,
|
||||
Ratio: value,
|
||||
}),
|
||||
)
|
||||
: [],
|
||||
PropertyTexts: [],
|
||||
},
|
||||
Type: item.Unlockable.Type,
|
||||
SubType: item.Unlockable.SubType,
|
||||
})
|
||||
}
|
||||
|
||||
for (const [id, category] of safehouseData.SubCategories.entries()) {
|
||||
if (category.SubCategories.length === 1) {
|
||||
// if category only has one subcategory
|
||||
safehouseData.SubCategories[id] = category.SubCategories[0] // flatten it
|
||||
safehouseData.SubCategories[id].Category = category.Category // but keep the top category's name
|
||||
}
|
||||
}
|
||||
|
||||
if (safehouseData.SubCategories.length === 1) {
|
||||
// if root has only one subcategory
|
||||
safehouseData = safehouseData.SubCategories[0] // flatten it
|
||||
}
|
||||
|
||||
return safehouseData
|
||||
}
|
@ -174,14 +174,14 @@ multiplayerRouter.post(
|
||||
// join existing match
|
||||
const match = activeMatches.get(req.body.matchId)!
|
||||
|
||||
match.Players.forEach((playerId) =>
|
||||
for (const playerId of match.Players) {
|
||||
enqueuePushMessage(playerId, {
|
||||
MatchId: req.body.matchId,
|
||||
Type: 1,
|
||||
PlayerId: req.jwt.unique_name,
|
||||
MatchData: null,
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
match.Players.push(req.jwt.unique_name)
|
||||
} else {
|
||||
@ -233,12 +233,12 @@ multiplayerRouter.post(
|
||||
},
|
||||
)
|
||||
|
||||
multiplayerRouter.post("/RegisterToPreset", jsonMiddleware(), (req, res) => {
|
||||
multiplayerRouter.post("/RegisterToPreset", jsonMiddleware(), (_, res) => {
|
||||
// matchmaking
|
||||
// TODO: implement matchmaking
|
||||
// req.body.presetId
|
||||
// req.body.lobbyId (this is just a timestamp?)
|
||||
res.status(500).end()
|
||||
res.status(501).end()
|
||||
})
|
||||
|
||||
export function handleMultiplayerEvent(
|
||||
|
@ -232,12 +232,7 @@ export async function handleOauthToken(
|
||||
userData.EpicId = req.body.epic_userid
|
||||
}
|
||||
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(
|
||||
userData.Extensions,
|
||||
"inventory",
|
||||
)
|
||||
) {
|
||||
if (Object.hasOwn(userData.Extensions, "inventory")) {
|
||||
// @ts-expect-error No longer in the typedefs.
|
||||
delete userData.Extensions.inventory
|
||||
}
|
||||
|
@ -212,12 +212,7 @@ profileRouter.post(
|
||||
const userdata = getUserData(req.jwt.unique_name, req.gameVersion)
|
||||
|
||||
for (const extension in req.body.extensionsData) {
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(
|
||||
req.body.extensionsData,
|
||||
extension,
|
||||
)
|
||||
) {
|
||||
if (Object.hasOwn(req.body.extensionsData, extension)) {
|
||||
userdata.Extensions[extension] =
|
||||
req.body.extensionsData[extension]
|
||||
}
|
||||
|
@ -16,7 +16,6 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type { Response } from "express"
|
||||
import {
|
||||
contractTypes,
|
||||
DEFAULT_MASTERY_MAXLEVEL,
|
||||
@ -39,9 +38,9 @@ import type {
|
||||
ContractSession,
|
||||
GameChanger,
|
||||
GameVersion,
|
||||
JwtData,
|
||||
MissionManifest,
|
||||
MissionManifestObjective,
|
||||
RequestWithJwt,
|
||||
Seconds,
|
||||
} from "./types/types"
|
||||
import {
|
||||
@ -68,7 +67,7 @@ import {
|
||||
MissionEndChallenge,
|
||||
MissionEndDrop,
|
||||
MissionEndEvergreen,
|
||||
MissionEndResponse,
|
||||
MissionEndResult,
|
||||
} from "./types/score"
|
||||
import { MasteryData } from "./types/mastery"
|
||||
import { createInventory, InventoryItem, getUnlockablesById } from "./inventory"
|
||||
@ -511,42 +510,45 @@ export function calculateSniperScore(
|
||||
]
|
||||
}
|
||||
|
||||
export async function missionEnd(
|
||||
req: RequestWithJwt<MissionEndRequestQuery>,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
// TODO: For this entire function, add support for 2016 difficulties
|
||||
// Resolve the contract session
|
||||
if (!req.query.contractSessionId) {
|
||||
res.status(400).end()
|
||||
return
|
||||
}
|
||||
export type MissionEndError = { errorCode: number; error: string }
|
||||
|
||||
const sessionDetails = contractSessions.get(req.query.contractSessionId)
|
||||
export async function getMissionEndData(
|
||||
query: MissionEndRequestQuery,
|
||||
jwt: JwtData,
|
||||
gameVersion: GameVersion,
|
||||
): Promise<MissionEndError | MissionEndResult> {
|
||||
// TODO: For this entire function, add support for 2016 difficulties
|
||||
const sessionDetails = contractSessions.get(query.contractSessionId)
|
||||
|
||||
if (!sessionDetails) {
|
||||
res.status(404).send("contract session not found")
|
||||
return
|
||||
return {
|
||||
errorCode: 404,
|
||||
error: "contract session not found",
|
||||
}
|
||||
}
|
||||
|
||||
if (sessionDetails.userId !== req.jwt.unique_name) {
|
||||
res.status(401).send("requested score for other user's session")
|
||||
return
|
||||
if (sessionDetails.userId !== jwt.unique_name) {
|
||||
return {
|
||||
errorCode: 401,
|
||||
error: "requested score for other user's session",
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve userdata
|
||||
const userData = getUserData(req.jwt.unique_name, req.gameVersion)
|
||||
const userData = getUserData(jwt.unique_name, gameVersion)
|
||||
|
||||
// Resolve contract data
|
||||
const contractData =
|
||||
req.gameVersion === "scpc" &&
|
||||
gameVersion === "scpc" &&
|
||||
sessionDetails.contractId === "ff9f46cf-00bd-4c12-b887-eac491c3a96d"
|
||||
? _theLastYardbirdScpc
|
||||
: controller.resolveContract(sessionDetails.contractId, true)
|
||||
|
||||
if (!contractData) {
|
||||
res.status(404).send("contract not found")
|
||||
return
|
||||
return {
|
||||
errorCode: 404,
|
||||
error: "Contract not found",
|
||||
}
|
||||
}
|
||||
|
||||
// Handle escalation groups
|
||||
@ -559,8 +561,11 @@ export async function missionEnd(
|
||||
LogLevel.ERROR,
|
||||
`Unregistered escalation group ${sessionDetails.contractId}`,
|
||||
)
|
||||
res.status(500).end()
|
||||
return
|
||||
|
||||
return {
|
||||
errorCode: 500,
|
||||
error: "unregistered escalation group",
|
||||
}
|
||||
}
|
||||
|
||||
if (!userData.Extensions.PeacockEscalations[eGroupId]) {
|
||||
@ -598,7 +603,7 @@ export async function missionEnd(
|
||||
|
||||
userData.Extensions.PeacockPlayedContracts[eGroupId] = history
|
||||
|
||||
writeUserData(req.jwt.unique_name, req.gameVersion)
|
||||
writeUserData(jwt.unique_name, gameVersion)
|
||||
} else if (contractTypes.includes(contractData.Metadata.Type)) {
|
||||
// Update the contract in the played list
|
||||
const id = contractData.Metadata.Id
|
||||
@ -612,7 +617,7 @@ export async function missionEnd(
|
||||
Completed: true,
|
||||
}
|
||||
|
||||
writeUserData(req.jwt.unique_name, req.gameVersion)
|
||||
writeUserData(jwt.unique_name, gameVersion)
|
||||
}
|
||||
|
||||
const levelData = controller.resolveContract(
|
||||
@ -623,7 +628,7 @@ export async function missionEnd(
|
||||
// Resolve the id of the parent location
|
||||
const subLocation = getSubLocationByName(
|
||||
levelData.Metadata.Location,
|
||||
req.gameVersion,
|
||||
gameVersion,
|
||||
)
|
||||
|
||||
const locationParentId = subLocation
|
||||
@ -631,8 +636,10 @@ export async function missionEnd(
|
||||
: levelData.Metadata.Location
|
||||
|
||||
if (!locationParentId) {
|
||||
res.status(404).send("location parentid not found")
|
||||
return
|
||||
return {
|
||||
errorCode: 400,
|
||||
error: "location parentid not found",
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve all opportunities for the location
|
||||
@ -652,27 +659,27 @@ export async function missionEnd(
|
||||
parent: locationParentId,
|
||||
},
|
||||
locationParentId,
|
||||
req.gameVersion,
|
||||
gameVersion,
|
||||
)
|
||||
const contractChallenges =
|
||||
controller.challengeService.getChallengesForContract(
|
||||
sessionDetails.contractId,
|
||||
req.gameVersion,
|
||||
req.jwt.unique_name,
|
||||
gameVersion,
|
||||
jwt.unique_name,
|
||||
sessionDetails.difficulty,
|
||||
)
|
||||
const locationChallengeCompletion =
|
||||
controller.challengeService.countTotalNCompletedChallenges(
|
||||
locationChallenges,
|
||||
userData.Id,
|
||||
req.gameVersion,
|
||||
gameVersion,
|
||||
)
|
||||
|
||||
const contractChallengeCompletion =
|
||||
controller.challengeService.countTotalNCompletedChallenges(
|
||||
contractChallenges,
|
||||
userData.Id,
|
||||
req.gameVersion,
|
||||
gameVersion,
|
||||
)
|
||||
|
||||
const locationPercentageComplete = getCompletionPercent(
|
||||
@ -688,7 +695,7 @@ export async function missionEnd(
|
||||
// Calculate XP based on global challenges.
|
||||
const calculateXpResult: CalculateXpResult = calculateGlobalXp(
|
||||
sessionDetails,
|
||||
req.gameVersion,
|
||||
gameVersion,
|
||||
)
|
||||
let justTickedChallenges = 0
|
||||
let totalXpGain = calculateXpResult.xp
|
||||
@ -711,8 +718,7 @@ export async function missionEnd(
|
||||
)
|
||||
})
|
||||
.forEach((challengeData) => {
|
||||
const userId = req.jwt.unique_name
|
||||
const gameVersion = req.gameVersion
|
||||
const userId = jwt.unique_name
|
||||
|
||||
userData.Extensions.ChallengeProgression[challengeData.Id].Ticked =
|
||||
true
|
||||
@ -737,10 +743,10 @@ export async function missionEnd(
|
||||
|
||||
let completionData = generateCompletionData(
|
||||
levelData.Metadata.Location,
|
||||
req.jwt.unique_name,
|
||||
req.gameVersion,
|
||||
jwt.unique_name,
|
||||
gameVersion,
|
||||
contractData.Metadata.Type,
|
||||
req.query.masteryUnlockableId,
|
||||
query.masteryUnlockableId,
|
||||
)
|
||||
|
||||
// Calculate the old location progression based on the current one and process it
|
||||
@ -752,17 +758,17 @@ export async function missionEnd(
|
||||
const newLocationXp = completionData.XP
|
||||
let newLocationLevel = levelForXp(newLocationXp)
|
||||
|
||||
if (!req.query.masteryUnlockableId) {
|
||||
if (!query.masteryUnlockableId) {
|
||||
userData.Extensions.progression.Locations[
|
||||
locationParentId
|
||||
].PreviouslySeenXp = newLocationXp
|
||||
}
|
||||
|
||||
writeUserData(req.jwt.unique_name, req.gameVersion)
|
||||
writeUserData(jwt.unique_name, gameVersion)
|
||||
|
||||
const masteryData = controller.masteryService.getMasteryPackage(
|
||||
locationParentId,
|
||||
req.gameVersion,
|
||||
gameVersion,
|
||||
)
|
||||
|
||||
let maxLevel = 1
|
||||
@ -770,9 +776,9 @@ export async function missionEnd(
|
||||
|
||||
if (masteryData) {
|
||||
maxLevel =
|
||||
(req.query.masteryUnlockableId
|
||||
(query.masteryUnlockableId
|
||||
? masteryData.SubPackages.find(
|
||||
(subPkg) => subPkg.Id === req.query.masteryUnlockableId,
|
||||
(subPkg) => subPkg.Id === query.masteryUnlockableId,
|
||||
).MaxLevel
|
||||
: masteryData.MaxLevel) || DEFAULT_MASTERY_MAXLEVEL
|
||||
|
||||
@ -815,7 +821,7 @@ export async function missionEnd(
|
||||
|
||||
// Calculate score and summary
|
||||
const calculateScoreResult = calculateScore(
|
||||
req.gameVersion,
|
||||
gameVersion,
|
||||
sessionDetails,
|
||||
contractData,
|
||||
timeTotal,
|
||||
@ -923,8 +929,8 @@ export async function missionEnd(
|
||||
|
||||
if (contractData.Metadata.Type === "sniper") {
|
||||
const userInventory = createInventory(
|
||||
req.jwt.unique_name,
|
||||
req.gameVersion,
|
||||
jwt.unique_name,
|
||||
gameVersion,
|
||||
undefined,
|
||||
)
|
||||
|
||||
@ -943,7 +949,7 @@ export async function missionEnd(
|
||||
sessionDetails,
|
||||
userData,
|
||||
locationParentId,
|
||||
req.query.masteryUnlockableId,
|
||||
query.masteryUnlockableId,
|
||||
)
|
||||
|
||||
// Update completion data with latest mastery
|
||||
@ -953,10 +959,10 @@ export async function missionEnd(
|
||||
// Temporarily get completion data for the unlockable
|
||||
completionData = generateCompletionData(
|
||||
levelData.Metadata.Location,
|
||||
req.jwt.unique_name,
|
||||
req.gameVersion,
|
||||
jwt.unique_name,
|
||||
gameVersion,
|
||||
"sniper", // We know the type will be sniper.
|
||||
req.query.masteryUnlockableId,
|
||||
query.masteryUnlockableId,
|
||||
)
|
||||
newLocationLevel = completionData.Level
|
||||
unlockableProgression = {
|
||||
@ -972,16 +978,16 @@ export async function missionEnd(
|
||||
}
|
||||
|
||||
userData.Extensions.progression.Locations[locationParentId][
|
||||
req.query.masteryUnlockableId
|
||||
query.masteryUnlockableId
|
||||
].PreviouslySeenXp = completionData.XP
|
||||
|
||||
writeUserData(req.jwt.unique_name, req.gameVersion)
|
||||
writeUserData(jwt.unique_name, gameVersion)
|
||||
|
||||
// Set the completion data to the location so the end screen formats properly.
|
||||
completionData = generateCompletionData(
|
||||
levelData.Metadata.Location,
|
||||
req.jwt.unique_name,
|
||||
req.gameVersion,
|
||||
jwt.unique_name,
|
||||
gameVersion,
|
||||
)
|
||||
|
||||
// Override the contract score
|
||||
@ -1003,9 +1009,9 @@ export async function missionEnd(
|
||||
const masteryData =
|
||||
controller.masteryService.getMasteryDataForSubPackage(
|
||||
locationParentId,
|
||||
req.query.masteryUnlockableId ?? undefined,
|
||||
req.gameVersion,
|
||||
req.jwt.unique_name,
|
||||
query.masteryUnlockableId ?? undefined,
|
||||
gameVersion,
|
||||
jwt.unique_name,
|
||||
) as MasteryData
|
||||
|
||||
if (masteryData) {
|
||||
@ -1024,10 +1030,7 @@ export async function missionEnd(
|
||||
const challengeDrops: MissionEndDrop[] =
|
||||
calculateXpResult.completedChallenges.reduce((acc, challenge) => {
|
||||
if (challenge?.Drops?.length) {
|
||||
const drops = getUnlockablesById(
|
||||
challenge.Drops,
|
||||
req.gameVersion,
|
||||
)
|
||||
const drops = getUnlockablesById(challenge.Drops, gameVersion)
|
||||
delete challenge.Drops
|
||||
|
||||
for (const drop of drops) {
|
||||
@ -1042,7 +1045,7 @@ export async function missionEnd(
|
||||
}, [])
|
||||
|
||||
// Setup the result
|
||||
const result: MissionEndResponse = {
|
||||
const result: MissionEndResult = {
|
||||
MissionReward: {
|
||||
LocationProgression: {
|
||||
LevelInfo: locationLevelInfo,
|
||||
@ -1082,7 +1085,7 @@ export async function missionEnd(
|
||||
XPGain: 0,
|
||||
ChallengesCompleted: justTickedChallenges,
|
||||
LocationHideProgression: masteryData?.HideProgression || false,
|
||||
ProdileId1: req.jwt.unique_name,
|
||||
ProdileId1: jwt.unique_name,
|
||||
stars: calculateScoreResult.stars,
|
||||
ScoreDetails: {
|
||||
Headlines: calculateScoreResult.scoringHeadlines,
|
||||
@ -1136,11 +1139,11 @@ export async function missionEnd(
|
||||
gameDifficulty: difficultyToString(
|
||||
sessionDetails.difficulty,
|
||||
),
|
||||
gameVersion: req.gameVersion,
|
||||
platform: req.jwt.platform,
|
||||
gameVersion,
|
||||
platform: jwt.platform,
|
||||
username: userData.Gamertag,
|
||||
platformId:
|
||||
req.jwt.platform === "epic"
|
||||
jwt.platform === "epic"
|
||||
? userData.EpicId
|
||||
: userData.SteamId,
|
||||
score: calculateScoreResult.scoreWithBonus,
|
||||
@ -1168,7 +1171,7 @@ export async function missionEnd(
|
||||
SniperChallengeScore: sniperChallengeScore,
|
||||
PlayStyle: result.ScoreOverview.PlayStyle || null,
|
||||
Description: "UI_MENU_SCORE_CONTRACT_COMPLETED",
|
||||
ContractSessionId: req.query.contractSessionId,
|
||||
ContractSessionId: query.contractSessionId,
|
||||
Percentile: {
|
||||
Spread: Array(10).fill(0),
|
||||
Index: 0,
|
||||
@ -1192,11 +1195,5 @@ export async function missionEnd(
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
template:
|
||||
req.gameVersion === "scpc"
|
||||
? getConfig("FrankensteinScoreOverviewTemplate", false)
|
||||
: null,
|
||||
data: result,
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
@ -104,3 +104,51 @@ export type LoadSaveBody = Partial<{
|
||||
difficultyLevel: number
|
||||
contractId: string
|
||||
}>
|
||||
|
||||
/**
|
||||
* Query params that `/profiles/page/Safehouse` gets.
|
||||
* Roughly the same as {@link SafehouseCategoryQuery} but this route is only for H1.
|
||||
*/
|
||||
export type SafehouseQuery = {
|
||||
type?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Query params that `/profiles/page/SafehouseCategory` (used for Career > Inventory and possibly some of the H1 stuff) gets.
|
||||
*/
|
||||
export type SafehouseCategoryQuery = {
|
||||
type?: string
|
||||
subtype?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Query params that `/profiles/page/Destination` gets.
|
||||
*/
|
||||
export type GetDestinationQuery = {
|
||||
locationId: string
|
||||
difficulty?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Query params that `/profiles/page/Leaderboards` gets.
|
||||
*/
|
||||
export type LeaderboardEntriesCommonQuery = {
|
||||
contractid: string
|
||||
difficultyLevel?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Query params that `/profiles/page/DebriefingLeaderboards` gets.
|
||||
* Because ofc it's different. Thanks IOI.
|
||||
*/
|
||||
export type DebriefingLeaderboardsQuery = {
|
||||
contractid: string
|
||||
difficulty?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Query params that `/profiles/page/ChallengeLocation` gets.
|
||||
*/
|
||||
export type ChallengeLocationQuery = {
|
||||
locationId: string
|
||||
}
|
||||
|
@ -103,7 +103,7 @@ export interface MissionEndEvergreenPayout {
|
||||
IsPrestige: boolean
|
||||
}
|
||||
|
||||
export interface MissionEndResponse {
|
||||
export interface MissionEndResult {
|
||||
MissionReward: {
|
||||
LocationProgression: {
|
||||
LevelInfo: number[]
|
||||
|
@ -1141,14 +1141,11 @@ export interface CreateFromParamsBody {
|
||||
}
|
||||
}
|
||||
|
||||
export interface CommonSelectScreenConfig {
|
||||
template: unknown
|
||||
data?: {
|
||||
Unlocked: string[]
|
||||
Contract: MissionManifest
|
||||
OrderedUnlocks: Unlockable[]
|
||||
UserCentric: UserCentricContract
|
||||
}
|
||||
export interface SelectEntranceOrPickupData {
|
||||
Unlocked: string[]
|
||||
Contract: MissionManifest
|
||||
OrderedUnlocks: Unlockable[]
|
||||
UserCentric: UserCentricContract
|
||||
}
|
||||
|
||||
export type CompiledIoiStatemachine = unknown
|
||||
|
@ -161,7 +161,6 @@ export function xpRequiredForLevel(level: number): number {
|
||||
return Math.max(0, (level - 1) * XP_PER_LEVEL)
|
||||
}
|
||||
|
||||
// TODO: Determine some mathematical function
|
||||
export const EVERGREEN_LEVEL_INFO: number[] = [
|
||||
0, 5000, 10000, 17000, 24000, 31000, 38000, 45000, 52000, 61000, 70000,
|
||||
79000, 88000, 97000, 106000, 115000, 124000, 133000, 142000, 154000, 166000,
|
||||
@ -189,11 +188,16 @@ export function evergreenLevelForXp(xp: number): number {
|
||||
return 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the amount of XP needed to reach a mastery level in Evergreen.
|
||||
*
|
||||
* @param level
|
||||
* @returns The XP, as a number.
|
||||
*/
|
||||
export function xpRequiredForEvergreenLevel(level: number): number {
|
||||
return EVERGREEN_LEVEL_INFO[level - 1]
|
||||
}
|
||||
|
||||
// TODO: Determine some mathematical function
|
||||
export const SNIPER_LEVEL_INFO: number[] = [
|
||||
0, 50000, 150000, 500000, 1000000, 1700000, 2500000, 3500000, 5000000,
|
||||
7000000, 9500000, 12500000, 16000000, 20000000, 25000000, 31000000,
|
||||
@ -213,16 +217,17 @@ export function sniperLevelForXp(xp: number): number {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of xp needed to reach a level in sniper missions.
|
||||
* @param level The level in question.
|
||||
* @returns The xp, as a number.
|
||||
* Get the amount of XP needed to reach a mastery level in sniper missions.
|
||||
*
|
||||
* @param level
|
||||
* @returns The XP, as a number.
|
||||
*/
|
||||
export function xpRequiredForSniperLevel(level: number): number {
|
||||
return SNIPER_LEVEL_INFO[level - 1]
|
||||
}
|
||||
|
||||
/**
|
||||
* Clamps the given value between a minimum and maximum value
|
||||
* Clamps the given value between a minimum and maximum value.
|
||||
*/
|
||||
export function clampValue(value: number, min: number, max: number) {
|
||||
return Math.max(min, Math.min(value, max))
|
||||
@ -230,9 +235,10 @@ export function clampValue(value: number, min: number, max: number) {
|
||||
|
||||
/**
|
||||
* Updates a user profile depending on the current version (if any).
|
||||
* @param profile The userprofile to update
|
||||
*
|
||||
* @param profile The user profile to update
|
||||
* @param gameVersion The game version
|
||||
* @returns The updated user profile
|
||||
* @returns The updated user profile.
|
||||
*/
|
||||
function updateUserProfile(
|
||||
profile: UserProfile,
|
||||
@ -350,6 +356,7 @@ function updateUserProfile(
|
||||
|
||||
/**
|
||||
* Returns whether a location is a sniper location. Works for both parent and child locations.
|
||||
*
|
||||
* @param location The location ID string.
|
||||
* @returns A boolean denoting the result.
|
||||
*/
|
||||
@ -382,7 +389,7 @@ export function castUserProfile(
|
||||
"PeacockCompletedEscalations",
|
||||
"CPD",
|
||||
]) {
|
||||
if (!Object.prototype.hasOwnProperty.call(j.Extensions, item)) {
|
||||
if (!Object.hasOwn(j.Extensions, item)) {
|
||||
log(LogLevel.DEBUG, `Err missing property ${item}`)
|
||||
log(
|
||||
LogLevel.WARN,
|
||||
@ -420,7 +427,7 @@ export function castUserProfile(
|
||||
// Fix Extensions.gamepersistentdata.HitsFilterType.
|
||||
// None of the old profiles should have "MyPlaylist".
|
||||
if (
|
||||
!Object.prototype.hasOwnProperty.call(
|
||||
!Object.hasOwn(
|
||||
j.Extensions.gamepersistentdata.HitsFilterType,
|
||||
"MyPlaylist",
|
||||
)
|
||||
@ -504,11 +511,13 @@ export const defaultSuits = {
|
||||
|
||||
/**
|
||||
* Default suits that are attainable via challenges or mastery in this version.
|
||||
*
|
||||
* NOTE: Currently this is hardcoded. To allow for flexibility and extensibility, this should be generated in real-time
|
||||
* using the Drops of challenges and masteries. However, that would require looping through all challenges and masteries
|
||||
* for all default suits, which is slow. This is a trade-off.
|
||||
* @param gameVersion The game version.
|
||||
* @returns The default suits that are attainable via challenges or mastery.
|
||||
*
|
||||
* @param gameVersion The game version.
|
||||
* @returns The default suits that are attainable via challenges or mastery.
|
||||
*/
|
||||
export function attainableDefaults(gameVersion: GameVersion): string[] {
|
||||
return gameVersion === "h1"
|
||||
@ -525,6 +534,7 @@ export function attainableDefaults(gameVersion: GameVersion): string[] {
|
||||
/**
|
||||
* Gets the default suit for a given sub-location and parent location.
|
||||
* Priority is given to the sub-location, then the parent location, then 47's signature suit.
|
||||
*
|
||||
* @param subLocation The sub-location.
|
||||
* @returns The default suit for the given sub-location and parent location.
|
||||
*/
|
||||
@ -591,6 +601,9 @@ export const gameDifficulty = {
|
||||
* Casual mode.
|
||||
*/
|
||||
casual: 1,
|
||||
/**
|
||||
* Alias for {@link casual}.
|
||||
*/
|
||||
easy: 1,
|
||||
/**
|
||||
* Professional (normal) mode.
|
||||
@ -600,6 +613,9 @@ export const gameDifficulty = {
|
||||
* Master mode.
|
||||
*/
|
||||
master: 4,
|
||||
/**
|
||||
* Alias for {@link master}.
|
||||
*/
|
||||
hard: 4,
|
||||
} as const
|
||||
|
||||
|
@ -59,6 +59,7 @@ export async function generateRequireTable() {
|
||||
"index.ts",
|
||||
"generatedPeacockRequireTable.ts",
|
||||
"types/globals.d.ts",
|
||||
"types/*.ts",
|
||||
],
|
||||
})
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user