2022-10-20 03:18:35 +02:00
/ *
* The Peacock Project - a HITMAN server replacement .
2023-01-23 19:37:33 +01:00
* Copyright ( C ) 2021 - 2023 The Peacock Project Team
2022-10-20 03:18:35 +02:00
*
* 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 { Response , Router } from "express"
import {
2023-02-14 04:14:16 +01:00
contractCreationTutorialId ,
2023-03-24 14:19:01 +01:00
DEFAULT_MASTERY_MAXLEVEL ,
2022-10-20 03:18:35 +02:00
gameDifficulty ,
2023-03-18 18:35:26 +01:00
getMaxProfileLevel ,
2023-04-21 14:17:21 +02:00
isSuit ,
2022-10-20 03:18:35 +02:00
PEACOCKVERSTRING ,
2022-12-31 03:06:09 +01:00
unlockOrderComparer ,
2022-10-20 03:18:35 +02:00
uuidRegex ,
} from "./utils"
import { contractSessions , getSession } from "./eventHandler"
import { missionEnd } from "./scoreHandler"
import { getConfig , getVersionedConfig } from "./configSwizzleManager"
import {
contractIdToHitObject ,
controller ,
peacockRecentEscalations ,
} from "./controller"
import { makeCampaigns } from "./menus/campaigns"
2023-01-07 03:15:14 +01:00
import {
createLocationsData ,
destinationsMenu ,
getDestinationCompletion ,
} from "./menus/destinations"
2022-10-20 03:18:35 +02:00
import type {
2023-04-08 02:13:05 +02:00
ChallengeCategoryCompletion ,
2022-10-20 03:18:35 +02:00
CommonSelectScreenConfig ,
2023-04-02 06:22:26 +02:00
ContractSearchResult ,
2023-03-06 19:17:22 +01:00
GameVersion ,
2022-10-20 03:18:35 +02:00
HitsCategoryCategory ,
IHit ,
2022-11-10 18:13:32 +01:00
MissionManifest ,
2022-10-20 03:18:35 +02:00
PeacockLocationsData ,
2023-01-20 22:00:20 +01:00
PlayerProfileView ,
2022-10-20 03:18:35 +02:00
RequestWithJwt ,
SafehouseCategory ,
SceneConfig ,
Unlockable ,
UserCentricContract ,
} from "./types/types"
2023-04-14 04:13:16 +02:00
import { no2016 } from "./contracts/escalations/escalationService"
2022-10-20 03:18:35 +02:00
import {
complications ,
generateCompletionData ,
generateUserCentric ,
2023-04-11 18:10:24 +02:00
getSubLocationByName ,
2022-10-20 03:18:35 +02:00
} from "./contracts/dataGen"
import { log , LogLevel } from "./loggingInterop"
2023-03-06 19:17:22 +01:00
import {
contractsModeHome ,
officialSearchContract ,
} from "./contracts/contractsModeRouting"
2022-10-20 03:18:35 +02:00
import random from "random"
import { getUserData } from "./databaseHandler"
import {
2023-03-06 18:12:52 +01:00
createMainOpportunityTile ,
2022-10-20 03:18:35 +02:00
createPlayNextTile ,
getSeasonId ,
orderedMissions ,
2023-02-24 00:16:38 +01:00
orderedPZMissions ,
2022-10-20 03:18:35 +02:00
} from "./menus/playnext"
import { randomUUID } from "crypto"
import { planningView } from "./menus/planning"
2023-03-26 20:30:11 +02:00
import {
deleteMultiple ,
directRoute ,
withLookupDialog ,
} from "./menus/favoriteContracts"
2022-10-20 03:18:35 +02:00
import { swapToBrowsingMenusStatus } from "./discordRp"
import axios from "axios"
import { getFlag } from "./flags"
import { fakePlayerRegistry } from "./profileHandler"
import { createInventory } from "./inventory"
import { missionsInLocations } from "./contracts/missionsInLocation"
import { json as jsonMiddleware } from "body-parser"
import { hitsCategoryService } from "./contracts/hitsCategoryService"
import {
GetCompletionDataForLocationQuery ,
MasteryUnlockableQuery ,
MissionEndRequestQuery ,
StashpointQuery ,
} from "./types/gameSchemas"
import assert from "assert"
2023-03-24 14:19:01 +01:00
import { SniperLoadoutConfig } from "./menus/sniper"
2022-10-20 03:18:35 +02:00
2022-11-24 04:11:22 +01:00
export const preMenuDataRouter = Router ( )
2022-10-20 03:18:35 +02:00
const menuDataRouter = Router ( )
// /profiles/page/
function dashEscalations ( req : RequestWithJwt , res : Response ) {
const contracts : UserCentricContract [ ] = [ ]
for ( const groupId of peacockRecentEscalations ) {
const userCentric = generateUserCentric (
2023-04-14 04:13:16 +02:00
controller . resolveContract ( groupId , true ) ! ,
2022-10-20 03:18:35 +02:00
req . jwt . unique_name ,
req . gameVersion ,
)
if ( ! userCentric ) {
continue
}
contracts . push ( userCentric )
}
res . json ( {
template : null ,
data : {
Item : {
Id : req.params.id ,
Type : "ContractList" ,
Title : "ContractList" ,
Date : new Date ( ) . toISOString ( ) ,
Data : contracts.length > 0 ? contracts : null ,
} ,
} ,
} )
}
menuDataRouter . get (
"/dashboard/Dashboard_Category_Escalation/:subscriptionId/:type/:id" ,
dashEscalations ,
)
menuDataRouter . get (
"/dashboard/Dashboard_Category_Escalation/:subscriptionId/:type/:id/:mode" ,
dashEscalations ,
)
2023-01-29 03:59:26 +01:00
2023-01-14 08:12:08 +01:00
menuDataRouter . get (
"/ChallengeLocation" ,
( req : RequestWithJwt < { locationId : string } > , res ) = > {
2023-04-02 06:22:26 +02:00
if ( typeof req . query . locationId !== "string" ) {
res . status ( 400 ) . send ( "Invalid locationId" )
return
}
2023-04-02 06:10:14 +02:00
2023-01-14 08:12:08 +01:00
const location = getVersionedConfig < PeacockLocationsData > (
"LocationsData" ,
req . gameVersion ,
true ,
) . children [ req . query . locationId ]
2023-01-29 03:59:26 +01:00
2023-01-14 08:12:08 +01:00
res . json ( {
template : getVersionedConfig (
"ChallengeLocationTemplate" ,
req . gameVersion ,
false ,
) ,
data : {
Name : location.DisplayNameLocKey ,
Location : location ,
Children :
controller . challengeService . getChallengeDataForLocation (
req . query . locationId ,
req . gameVersion ,
req . jwt . unique_name ,
) ,
} ,
} )
} ,
)
2022-10-20 03:18:35 +02:00
menuDataRouter . get ( "/Hub" , ( req : RequestWithJwt , res ) = > {
swapToBrowsingMenusStatus ( req . gameVersion )
const userdata = getUserData ( req . jwt . unique_name , req . gameVersion )
const theTemplate =
req . gameVersion === "h3"
? null
: req . gameVersion === "h2"
? null
: req . gameVersion === "scpc"
? getConfig ( "FrankensteinHubTemplate" , false )
: getConfig ( "LegacyHubTemplate" , false )
if ( req . gameVersion === "scpc" ) {
req . gameVersion = "h1"
}
const contractCreationTutorial = controller . resolveContract (
2023-02-14 04:14:16 +01:00
contractCreationTutorialId ,
2022-10-20 03:18:35 +02:00
) !
2023-01-14 08:12:08 +01:00
const locations = getVersionedConfig < PeacockLocationsData > (
"LocationsData" ,
req . gameVersion ,
true ,
)
2023-04-14 00:49:31 +02:00
const career =
req . 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 : {
2023-01-14 08:12:08 +01:00
Children : [ ] ,
Name : "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE" ,
Location :
locations . parents [ "LOCATION_PARENT_ICA_FACILITY" ] ,
2023-04-14 00:49:31 +02:00
} ,
}
2023-01-29 03:59:26 +01:00
2023-04-19 00:35:46 +02:00
const masteryData = [ ]
2023-01-14 08:12:08 +01:00
for ( const parent in locations . parents ) {
career [ parent ] = {
Children : [ ] ,
Location : locations.parents [ parent ] ,
Name : locations.parents [ parent ] . DisplayNameLocKey ,
}
2023-04-19 00:35:46 +02:00
if (
controller . masteryService . getMasteryDataForDestination (
parent ,
req . gameVersion ,
req . jwt . unique_name ,
) . length
) {
const completionData =
controller . masteryService . getLocationCompletion (
parent ,
parent ,
req . gameVersion ,
req . jwt . unique_name ,
)
masteryData . push ( {
CompletionData : completionData ,
. . . ( req . gameVersion === "h1"
? {
Data : {
normal : {
CompletionData : completionData ,
} ,
// pro1 is currently a copy of normal completion as it is not implemented
pro1 : {
CompletionData : completionData ,
} ,
} ,
}
: { } ) ,
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 ,
} )
}
2023-01-14 08:12:08 +01:00
}
2023-01-29 03:59:26 +01:00
2023-01-14 08:12:08 +01:00
for ( const child in locations . children ) {
if (
child === "LOCATION_ICA_FACILITY_ARRIVAL" ||
2023-01-29 16:34:30 +01:00
child === "LOCATION_HOKKAIDO_SHIM_MAMUSHI" ||
2023-04-08 02:13:05 +02:00
child . includes ( "SNUG_" )
2023-01-14 08:12:08 +01:00
) {
continue
}
2023-01-29 03:59:26 +01:00
2023-01-14 08:12:08 +01:00
const parent = locations . children [ child ] . Properties . ParentLocation
const location = locations . children [ child ]
const challenges = controller . challengeService . getChallengesForLocation (
child ,
req . gameVersion ,
)
const challengeCompletion =
controller . challengeService . countTotalNCompletedChallenges (
challenges ,
req . jwt . unique_name ,
req . gameVersion ,
)
2023-01-26 16:15:27 +01:00
career [ parent ] ? . Children . push ( {
2023-01-14 08:12:08 +01:00
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 ,
req . jwt . unique_name ,
req . gameVersion ,
) ,
} )
}
2022-10-20 03:18:35 +02:00
res . json ( {
template : theTemplate ,
data : {
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 :
req . gameVersion === "h3"
? destinationsMenu ( req )
: req . gameVersion === "h2"
? getConfig ( "H2DestinationsData" , false )
: getConfig ( "LegacyDestinations" , false ) ,
CreateContractTutorial : generateUserCentric (
contractCreationTutorial ,
req . jwt . unique_name ,
req . gameVersion ,
) ,
2023-02-05 16:56:08 +01:00
LocationsData : createLocationsData ( req . gameVersion , true ) ,
2023-01-14 08:12:08 +01:00
ProfileData : {
ChallengeData : {
Children : Object.values ( career ) ,
} ,
2023-04-19 00:35:46 +02:00
MasteryData : masteryData ,
2023-01-14 08:12:08 +01:00
} ,
2022-10-20 03:18:35 +02:00
StoryData : makeCampaigns ( req . gameVersion , req . jwt . unique_name ) ,
FilterData : getVersionedConfig (
"FilterData" ,
req . gameVersion ,
false ,
) ,
StoreData : getVersionedConfig ( "StoreData" , req . 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 ,
2023-03-18 18:35:26 +01:00
MaxLevel : getMaxProfileLevel ( req . gameVersion ) ,
2022-10-20 03:18:35 +02:00
} ,
} ,
} )
} )
menuDataRouter . get ( "/SafehouseCategory" , ( req : RequestWithJwt , res ) = > {
const exts = getUserData ( req . jwt . unique_name , req . gameVersion ) . Extensions
const inventory = createInventory (
req . jwt . unique_name ,
req . gameVersion ,
exts . entP ,
)
const safehouseData = {
template :
req . gameVersion === "h1"
? getConfig ( "LegacySafehouseTemplate" , false )
: null ,
data : {
Category : "_root" ,
SubCategories : [ ] ,
IsLeaf : false ,
Data : null ,
} as SafehouseCategory ,
}
for ( const item of inventory ) {
if ( req . query . type ) {
// if type is specified in query
if ( item . Unlockable . Type !== req . query . type ) {
continue // skip all items that are not that type
}
if (
req . query . subtype &&
item . Unlockable . Subtype !== req . 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 === "agencypickup"
) {
continue // these types should not be displayed when not asked for
}
if (
item . Unlockable . Subtype === "disguise" &&
req . gameVersion === "h3"
) {
continue // I don't want to put this in that elif statement
}
let category = safehouseData . data . SubCategories . find (
( cat ) = > cat . Category === item . Unlockable . Type ,
)
let subcategory
if ( ! category ) {
category = {
Category : item.Unlockable.Type ,
SubCategories : [ ] ,
IsLeaf : false ,
Data : null ,
}
safehouseData . data . 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 . data . SubCategories . entries ( ) ) {
if ( category . SubCategories . length === 1 ) {
// if category only has one subcategory
safehouseData . data . SubCategories [ id ] = category . SubCategories [ 0 ] // flatten it
safehouseData . data . SubCategories [ id ] . Category = category . Category // but keep the top category's name
}
}
if ( safehouseData . data . SubCategories . length === 1 ) {
// if root has only one subcategory
safehouseData . data = safehouseData . data . SubCategories [ 0 ] // flatten it
}
res . json ( safehouseData )
} )
2023-03-20 17:24:21 +01:00
menuDataRouter . get ( "/report" , ( req : RequestWithJwt , res ) = > {
res . json ( {
template : getVersionedConfig ( "ReportTemplate" , req . gameVersion , false ) ,
data : {
Reasons : [
{ Id : 0 , Title : "UI_MENU_REPORT_REASON_OFFENSIVE" } ,
{ Id : 1 , Title : "UI_MENU_REPORT_REASON_BUGGY" } ,
] ,
} ,
} )
} )
2022-10-20 03:18:35 +02:00
menuDataRouter . get (
"/stashpoint" ,
( req : RequestWithJwt < StashpointQuery > , res ) = > {
// Note: this is handled differently for 2016
// /stashpoint?contractid=e5b6ccf4-1f29-4ec6-bfb8-2e9b78882c85&slotid=4&slotname=gear4&stashpoint=&allowlargeitems=true&allowcontainers=true
// /stashpoint?contractid=c1d015b4-be08-4e44-808e-ada0f387656f&slotid=3&slotname=disguise3&stashpoint=&allowlargeitems=true&allowcontainers=true
// /stashpoint?contractid=&slotid=3&slotname=disguise&stashpoint=&allowlargeitems=true&allowcontainers=false
// /stashpoint?contractid=5b5f8aa4-ecb4-4a0a-9aff-98aa1de43dcc&slotid=6&slotname=stashpoint6&stashpoint=28b03709-d1f0-4388-b207-f03611eafb64&allowlargeitems=true&allowcontainers=false
const stashData : {
template : unknown
data ? : {
SlotId? : string | number
LoadoutItemsData? : unknown
UserCentric? : UserCentricContract
ShowSlotName? : string | number
}
} = {
template : getVersionedConfig (
"StashpointTemplate" ,
req . gameVersion === "h1" ? "h1" : "h3" ,
false ,
) ,
}
2023-04-02 06:22:26 +02:00
if (
typeof req . query . slotname !== "string" ||
! ( req . query . slotid ? ? undefined )
) {
res . status ( 400 ) . send ( "invalid slot data" )
return
}
2022-10-20 03:18:35 +02:00
const userData = getUserData ( req . jwt . unique_name , req . gameVersion )
2023-04-11 18:10:24 +02:00
let contractData : MissionManifest | undefined = undefined
if ( req . query . contractid ) {
contractData = controller . resolveContract ( req . query . contractid )
}
2022-10-20 03:18:35 +02:00
const inventory = createInventory (
req . jwt . unique_name ,
req . gameVersion ,
userData . Extensions . entP ,
2023-04-11 18:10:24 +02:00
getSubLocationByName (
2023-04-12 01:59:39 +02:00
contractData ? . Metadata . Location ,
2023-04-11 18:10:24 +02:00
req . gameVersion ,
) ,
2022-10-20 03:18:35 +02:00
)
if ( req . query . slotname . endsWith ( req . query . slotid ! . toString ( ) ) ) {
req . query . slotname = req . query . slotname . slice (
0 ,
- req . query . slotid ! . toString ( ) . length ,
) // weird
}
stashData . data = {
SlotId : req.query.slotid ,
LoadoutItemsData : {
SlotId : req.query.slotid ,
Items : inventory
. filter ( ( item ) = > {
if (
req . query . slotname === "gear" &&
2022-11-10 18:13:32 +01:00
contractData ? . Peacock ? . noGear === true
2022-10-20 03:18:35 +02:00
) {
return false
}
if (
req . query . slotname === "concealedweapon" &&
2022-11-10 18:13:32 +01:00
contractData ? . Peacock ? . noCarriedWeapon === true
2022-10-20 03:18:35 +02:00
) {
return false
}
if (
item . Unlockable . Subtype === "disguise" &&
req . gameVersion === "h3"
) {
return false
}
return (
item . Unlockable . Properties . LoadoutSlot && // only display items
( ! req . query . slotname ||
( ( uuidRegex . test ( req . query . slotid as string ) || // container
req . query . slotname === "stashpoint" ) && // stashpoint
item . Unlockable . Properties . LoadoutSlot !==
"disguise" ) || // container or stashpoint => display all items
item . Unlockable . Properties . LoadoutSlot ===
req . query . slotname ) && // else: display items for requested slot
( req . query . allowcontainers === "true" ||
! item . Unlockable . Properties . IsContainer ) &&
( req . query . allowlargeitems === "true" ||
item . Unlockable . Properties . LoadoutSlot !==
"carriedweapon" )
) // 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 : req.query.slotid ,
SlotName : null ,
} ) ) ,
Page : 0 ,
HasMore : false ,
HasMoreLeft : false ,
HasMoreRight : false ,
OptionalData : {
stashpoint : req.query.stashpoint || "" ,
AllowLargeItems : req.query.allowlargeitems ,
AllowContainers : req.query.allowcontainers , //?? true
} ,
} ,
ShowSlotName : req.query.slotname ,
}
if ( contractData ) {
stashData . data . UserCentric = generateUserCentric (
contractData ,
req . jwt . unique_name ,
req . gameVersion ,
)
}
res . json ( stashData )
} ,
)
menuDataRouter . get (
"/missionrewards" ,
( req : RequestWithJwt < { contractSessionId : string } > , res ) = > {
const { contractId } = getSession ( req . jwt . unique_name )
2023-04-14 04:13:16 +02:00
const contractData = controller . resolveContract ( contractId , true )
2022-10-20 03:18:35 +02:00
2023-01-29 03:59:26 +01:00
const userData = getUserData ( req . jwt . unique_name , req . gameVersion )
2022-10-20 03:18:35 +02:00
res . json ( {
template : {
controller : "group" ,
id : "mission_rewards" ,
selectable : false ,
pressable : false ,
children : [
{
view : "menu3.MissionRewardPage" ,
selectable : false ,
pressable : false ,
data : {
$setup : {
"$set Drops" : {
"$each $.Drops" : "$item $.Unlockable" ,
} ,
$in : "$" ,
} ,
} ,
} ,
] ,
} ,
data : {
LevelInfo : [
0 , 6000 , 12000 , 18000 , 24000 , 30000 , 36000 , 42000 , 48000 ,
54000 , 60000 , 66000 , 72000 , 78000 , 84000 , 90000 , 96000 ,
102000 , 108000 , 114000 ,
] ,
XP : 0 ,
Level : 1 ,
Completion : 0 ,
XPGain : 0 ,
Challenges : Object.values (
controller . challengeService . getChallengesForContract (
2023-04-14 04:13:16 +02:00
contractId ,
2022-10-20 03:18:35 +02:00
req . gameVersion ,
2023-04-21 19:48:56 +02:00
req . jwt . unique_name ,
2023-04-05 21:02:25 +02:00
// TODO: Should a difficulty be passed here?
2022-10-20 03:18:35 +02:00
) ,
)
. flat ( )
// FIXME: This behaviour may not be accurate to original server
2023-01-29 03:59:26 +01:00
. filter ( ( challengeData ) = >
controller . challengeService . fastGetIsCompleted (
userData ,
challengeData . Id ,
) ,
2022-10-20 03:18:35 +02:00
)
. map ( ( challengeData ) = >
controller . challengeService . compileRegistryChallengeTreeData (
challengeData ,
2022-12-12 22:38:55 +01:00
controller . challengeService . getPersistentChallengeProgression (
2022-10-20 03:18:35 +02:00
req . jwt . unique_name ,
challengeData . Id ,
req . gameVersion ,
) ,
req . gameVersion ,
req . jwt . unique_name ,
) ,
) ,
Drops : [ ] ,
ContractCompletionBonus : 0 ,
GroupCompletionBonus : 0 ,
LocationHideProgression : true ,
Difficulty : "normal" , // FIXME: is this right?
CompletionData : generateCompletionData (
contractData . Metadata . Location ,
req . jwt . unique_name ,
req . gameVersion ,
) ,
} ,
} )
} ,
)
menuDataRouter . get (
"/scoreoverview" ,
async ( req : RequestWithJwt < MissionEndRequestQuery > , res ) = > {
const resJsonFunc = res . json
res . json = function fakeJsonBind ( input ) {
return resJsonFunc . call ( this , {
template : getConfig ( "scoreoverviewtemplate" , false ) ,
data : input.data.ScoreOverview ,
} )
}
2022-11-13 12:30:41 +01:00
await innerMissionEnd ( req , res )
2022-10-20 03:18:35 +02:00
} ,
)
menuDataRouter . get ( "/Planning" , planningView )
menuDataRouter . get (
"/selectagencypickup" ,
( req : RequestWithJwt < { contractId : string } > , res ) = > {
const pickupData = getConfig < SceneConfig > ( "AgencyPickups" , false )
const selectagencypickup = {
template : getVersionedConfig (
"SelectAgencyPickupTemplate" ,
req . gameVersion ,
false ,
) ,
} as CommonSelectScreenConfig
const exts = getUserData (
req . jwt . unique_name ,
req . gameVersion ,
) . Extensions
const inventory = createInventory (
req . jwt . unique_name ,
req . gameVersion ,
exts . entP ,
)
const contractData = controller . resolveContract ( req . query . contractId )
if ( ! contractData ) {
log (
LogLevel . WARN ,
` Unknown contract on SAP: ${ req . query . contractId } ` ,
)
return res . status ( 404 ) . end ( )
}
const scenePath = contractData . Metadata . ScenePath . toLowerCase ( )
log (
LogLevel . DEBUG ,
` Looking up details for contract - Id: ${ req . query . contractId } ( ${ scenePath } ) ` ,
)
if ( ! Object . prototype . hasOwnProperty . call ( pickupData , scenePath ) ) {
log (
LogLevel . ERROR ,
` Could not find AgencyPickup data for ${ scenePath } ! This may cause an unhandled promise rejection. ` ,
)
}
if ( contractData . Peacock ? . noAgencyPickupsActive === true ) {
selectagencypickup . data = {
Unlocked : [ ] ,
Contract : contractData ,
OrderedUnlocks : [ ] ,
UserCentric : generateUserCentric (
contractData ,
req . jwt . unique_name ,
req . gameVersion ,
) ,
}
res . json ( selectagencypickup )
return
}
const pickupsInScene = pickupData [ scenePath ]
const unlockedAgencyPickups = inventory
. filter ( ( item ) = > item . Unlockable . Type === "agencypickup" )
. filter (
( item ) = >
item . Unlockable . Properties . Difficulty ===
contractData . Metadata . Difficulty ,
)
. map ( ( i ) = > i . Unlockable )
. filter ( ( unlockable ) = > unlockable . Properties . RepositoryId )
selectagencypickup . data = {
Unlocked : unlockedAgencyPickups.map (
( unlockable ) = > unlockable . Properties . RepositoryId ! ,
) ,
Contract : contractData ,
OrderedUnlocks : unlockedAgencyPickups
. filter ( ( unlockable ) = >
pickupsInScene . includes ( unlockable . Properties . RepositoryId ) ,
)
2022-12-31 03:06:09 +01:00
. sort ( unlockOrderComparer ) ,
2022-10-20 03:18:35 +02:00
UserCentric : generateUserCentric (
contractData ,
req . jwt . unique_name ,
req . gameVersion ,
) ,
}
res . json ( selectagencypickup )
} ,
)
menuDataRouter . get (
"/selectentrance" ,
( req : RequestWithJwt < { contractId : string } > , res ) = > {
const entranceData = getConfig < SceneConfig > ( "Entrances" , false )
const selectEntrance : CommonSelectScreenConfig = {
template : getVersionedConfig (
"SelectEntranceTemplate" ,
req . gameVersion ,
true ,
) ,
}
const exts = getUserData (
req . jwt . unique_name ,
req . gameVersion ,
) . Extensions
const inventory = createInventory (
req . jwt . unique_name ,
req . gameVersion ,
exts . entP ,
)
const contractData = controller . resolveContract ( req . query . contractId )
if ( ! contractData ) {
log ( LogLevel . WARN , ` Unknown contract: ${ req . query . contractId } ` )
return res . status ( 404 ) . end ( )
}
const scenePath = contractData . Metadata . ScenePath . toLowerCase ( )
log (
LogLevel . DEBUG ,
` Looking up details for contract - Id: ${ req . query . contractId } ( ${ scenePath } ) ` ,
)
if ( ! Object . prototype . hasOwnProperty . call ( entranceData , scenePath ) ) {
log (
LogLevel . ERROR ,
` Could not find Entrance data for ${ scenePath } ! This may cause an unhandled promise rejection. ` ,
)
}
const entrancesInScene = entranceData [ scenePath ]
const unlockedEntrances = inventory
. filter ( ( item ) = > item . Unlockable . Subtype === "startinglocation" )
. filter (
( item ) = >
item . Unlockable . Properties . Difficulty ===
contractData . Metadata . Difficulty ,
)
. map ( ( i ) = > i . Unlockable )
. filter ( ( unlockable ) = > unlockable . Properties . RepositoryId )
selectEntrance . data = {
Unlocked : unlockedEntrances.map (
( unlockable ) = > unlockable . Properties . RepositoryId ! ,
) ,
Contract : contractData ,
OrderedUnlocks : unlockedEntrances
. filter ( ( unlockable ) = >
entrancesInScene . includes (
unlockable . Properties . RepositoryId ! ,
) ,
)
2022-12-31 03:06:09 +01:00
. sort ( unlockOrderComparer ) ,
2022-10-20 03:18:35 +02:00
UserCentric : generateUserCentric (
contractData ,
req . jwt . unique_name ,
req . gameVersion ,
) ,
}
res . json ( selectEntrance )
} ,
)
menuDataRouter . get ( "/missionendready" , async ( req , res ) = > {
const sessionDetails = contractSessions . get (
req . query . contractSessionId as string ,
)
const retryCount = 1 + Number ( req . query . retryCount ) || 0
if ( ! sessionDetails ? . timerEnd ) {
// not ready
// wait some time proportional to the amount of retries
await new Promise < void > ( ( resolve ) = >
setTimeout ( ( ) = > resolve ( undefined ) , retryCount * 100 ) ,
)
res . json ( {
template : getConfig ( "MissionEndNotReadyTemplate" , false ) ,
data : {
contractSessionId : req.query.contractSessionId ,
missionEndReady : false ,
retryCount : retryCount ,
} ,
} )
} else {
// ready
res . json ( {
template : getConfig ( "MissionEndReadyTemplate" , false ) ,
data : {
contractSessionId : req.query.contractSessionId ,
missionEndReady : true ,
retryCount : retryCount ,
} ,
} )
}
} )
2022-11-13 12:30:41 +01:00
menuDataRouter . get ( "/missionend" , innerMissionEnd )
2022-10-20 03:18:35 +02:00
2022-11-13 12:30:41 +01:00
menuDataRouter . get ( "/scoreoverviewandunlocks" , innerMissionEnd )
async function innerMissionEnd (
req : RequestWithJwt < MissionEndRequestQuery > ,
res : Response ,
) : Promise < void > {
const result = controller . hooks . getMissionEnd . call ( req , res )
if ( result ) {
return
}
await missionEnd ( req , res )
}
2022-10-20 03:18:35 +02:00
menuDataRouter . get (
"/Destination" ,
( req : RequestWithJwt < { locationId : string ; difficulty? : string } > , res ) = > {
const LOCATION = req . query . locationId
2023-01-07 03:15:14 +01:00
const locData = getVersionedConfig < PeacockLocationsData > (
"LocationsData" ,
req . gameVersion ,
false ,
)
const locationData = locData . parents [ LOCATION ]
2023-04-19 00:35:46 +02:00
const masteryData =
controller . masteryService . getMasteryDataForDestination (
req . query . locationId ,
req . gameVersion ,
req . jwt . unique_name ,
)
2023-01-07 03:15:14 +01:00
2022-10-20 03:18:35 +02:00
const response = {
template :
req . gameVersion === "h1"
? getConfig ( "LegacyDestinationTemplate" , false )
: null ,
data : {
Location : { } ,
MissionData : {
2023-04-08 02:13:05 +02:00
. . . getDestinationCompletion ( locationData , undefined , req ) ,
2023-01-07 03:15:14 +01:00
. . . { SubLocationMissionsData : [ ] } ,
2022-10-20 03:18:35 +02:00
} ,
ChallengeData : {
Children :
controller . challengeService . getChallengeDataForDestination (
req . query . locationId ,
req . gameVersion ,
req . jwt . unique_name ,
) ,
} ,
2023-01-20 22:00:20 +01:00
MasteryData :
2023-04-19 00:35:46 +02:00
// No pro1 mastery, in an ideal world we'd pass normal as 0 and pro1 as 1
req . gameVersion === "h1" ? masteryData [ 0 ] : masteryData ,
2022-10-20 03:18:35 +02:00
DifficultyData : undefined ,
} ,
}
if ( req . gameVersion === "h1" ) {
response . data . DifficultyData = {
AvailableDifficultyModes : [
{
Name : "normal" ,
Available : true ,
} ,
{
Name : "pro1" ,
Available : true ,
} ,
] ,
Difficulty : req.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 . data . Location = locationData
if ( req . query . difficulty === "pro1" ) {
log ( LogLevel . DEBUG , "Adjusting for legacy-pro1." )
const obj = {
Location : locationData ,
SubLocation : locationData ,
Missions : [ controller . missionsInLocations . pro1 [ LOCATION ] ] . map (
( id ) = >
contractIdToHitObject (
id ,
req . gameVersion ,
req . jwt . unique_name ,
) ,
) ,
SarajevoSixMissions : [ ] ,
ElusiveMissions : [ ] ,
EscalationMissions : [ ] ,
SniperMissions : [ ] ,
PlaceholderMissions : [ ] ,
CampaignMissions : [ ] ,
CompletionData : generateCompletionData (
sublocationsData [ 0 ] . Id ,
req . jwt . unique_name ,
req . gameVersion ,
) ,
}
response . data . MissionData . SubLocationMissionsData . push ( obj )
res . json ( response )
return
}
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 [ ] = [
2023-04-14 08:01:53 +02:00
. . . ( req . gameVersion === "h1" && e . Id === "LOCATION_ICA_FACILITY"
2023-04-14 04:13:16 +02:00
? controller . missionsInLocations . escalations [
"LOCATION_ICA_FACILITY_SHIP"
]
: [ ] ) ,
2022-10-20 03:18:35 +02:00
. . . new Set < string > (
controller . missionsInLocations . escalations [ e . Id ] || [ ] ,
) ,
]
for ( const escalation of allUniqueEscalations ) {
2023-04-14 04:13:16 +02:00
if ( req . gameVersion === "h1" && no2016 . includes ( escalation ) )
continue
const details = contractIdToHitObject (
2022-10-20 03:18:35 +02:00
escalation ,
req . gameVersion ,
2023-04-14 04:13:16 +02:00
req . jwt . unique_name ,
2022-10-20 03:18:35 +02:00
)
if ( details ) {
escalations . push ( details )
}
}
const sniperMissions : IHit [ ] = [ ]
for ( const sniperMission of controller . missionsInLocations . sniper [
e . Id
] ? ? [ ] ) {
sniperMissions . push (
contractIdToHitObject (
sniperMission ,
req . gameVersion ,
req . jwt . unique_name ,
) ,
)
}
const obj = {
Location : locationData ,
SubLocation : e ,
Missions : [ ] ,
SarajevoSixMissions : [ ] ,
ElusiveMissions : [ ] ,
EscalationMissions : escalations ,
SniperMissions : sniperMissions ,
PlaceholderMissions : [ ] ,
CampaignMissions : [ ] ,
CompletionData : generateCompletionData (
e . Id ,
req . jwt . unique_name ,
req . gameVersion ,
) ,
}
const types = [
. . . [
[ undefined , "Missions" ] ,
[ "elusive" , "ElusiveMissions" ] ,
] ,
. . . ( req . gameVersion === "h1" ||
( req . gameVersion === "h3" &&
missionsInLocations . sarajevo [ "h3enabled" ] )
? [ [ "sarajevo" , "SarajevoSixMissions" ] ]
: [ ] ) ,
]
for ( const t of types ) {
let theMissions = ! 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 (
req . 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 !== undefined ) {
; ( theMissions as string [ ] )
. filter (
// removes snow festival on h1, traditions on non-h3
( m ) = >
m &&
! (
req . gameVersion === "h1" &&
m === "c414a084-a7b9-43ce-b6ca-590620acd87e"
) &&
! (
req . gameVersion !== "h3" &&
m === "90c291f6-7ac3-46de-99b2-082e38fccb24"
) ,
)
. forEach ( ( c ) = > {
const mission = contractIdToHitObject (
c ,
req . gameVersion ,
req . jwt . unique_name ,
)
obj [ t [ 1 ] ] . push ( mission )
} )
}
}
response . data . MissionData . SubLocationMissionsData . push ( obj )
}
res . json ( response )
} ,
)
2023-03-06 19:17:22 +01:00
async function lookupContractPublicId (
publicid : string ,
userId : string ,
gameVersion : GameVersion ,
) {
const publicIdRegex = /\d{12}/
2022-10-20 03:18:35 +02:00
2023-03-06 19:17:22 +01:00
while ( publicid . includes ( "-" ) ) {
publicid = publicid . replace ( "-" , "" )
}
2022-10-20 03:18:35 +02:00
2023-03-06 19:17:22 +01:00
if ( ! publicIdRegex . test ( publicid ) ) {
return {
PublicId : publicid ,
ErrorReason : "notfound" ,
2022-10-20 03:18:35 +02:00
}
2023-03-06 19:17:22 +01:00
}
2022-10-20 03:18:35 +02:00
2023-03-06 19:17:22 +01:00
const contract = await controller . contractByPubId (
publicid ,
userId ,
gameVersion ,
)
if ( ! contract ) {
return {
PublicId : publicid ,
ErrorReason : "notfound" ,
2022-10-20 03:18:35 +02:00
}
2023-03-06 19:17:22 +01:00
}
2022-10-20 03:18:35 +02:00
2023-03-06 19:17:22 +01:00
const location = (
getVersionedConfig (
"allunlockables" ,
gameVersion ,
false ,
) as readonly Unlockable [ ]
) . find ( ( entry ) = > entry . Id === contract . Metadata . Location )
2022-10-20 03:18:35 +02:00
2023-03-06 19:17:22 +01:00
return {
Contract : contract ,
Location : location ,
UserCentricContract : generateUserCentric ( contract , userId , gameVersion ) ,
}
}
menuDataRouter . get (
"/LookupContractPublicId" ,
async ( req : RequestWithJwt < { publicid : string } > , res ) = > {
2023-04-02 06:22:26 +02:00
if ( ! req . query . publicid || typeof req . query . publicid !== "string" ) {
return res . status ( 400 ) . send ( "no/invalid public id specified!" )
2022-10-20 03:18:35 +02:00
}
2023-03-06 19:17:22 +01:00
res . json ( {
template : getVersionedConfig (
"LookupContractByIdTemplate" ,
2022-10-20 03:18:35 +02:00
req . gameVersion ,
false ,
2023-03-06 19:17:22 +01:00
) ,
data : await lookupContractPublicId (
req . query . publicid ,
req . jwt . unique_name ,
req . gameVersion ,
) ,
2022-10-20 03:18:35 +02:00
} )
} ,
)
menuDataRouter . get (
"/HitsCategory" ,
2023-03-06 19:17:22 +01:00
async (
req : RequestWithJwt < { type : string ; page? : number | string } > ,
res ,
) = > {
2022-10-20 03:18:35 +02:00
const category = req . query . type
const response : {
template : unknown
data? : HitsCategoryCategory
} = {
template :
req . gameVersion === "h1"
? getConfig ( "LegacyHitsCategoryTemplate" , false )
: null ,
data : undefined ,
}
let pageNumber = req . query . page || 0
if ( typeof pageNumber === "string" ) {
pageNumber = parseInt ( pageNumber , 10 )
}
pageNumber = pageNumber < 0 ? 0 : pageNumber
2023-03-06 19:17:22 +01:00
response . data = await hitsCategoryService . paginateHitsCategory (
2022-10-20 03:18:35 +02:00
category ,
pageNumber as number ,
req . gameVersion ,
req . jwt . unique_name ,
)
res . json ( response )
} ,
)
menuDataRouter . get (
"/PlayNext" ,
( req : RequestWithJwt < { contractId : string } > , res ) = > {
if ( ! req . query . contractId ) {
res . status ( 400 ) . send ( "no contract id!" )
return
}
const cats = [ ]
//#region Main story missions
2023-02-24 00:16:38 +01:00
const currentIdIndex = orderedMissions . indexOf ( req . query . contractId )
2022-10-20 03:18:35 +02:00
if (
currentIdIndex !== - 1 &&
currentIdIndex !== orderedMissions . length - 1
) {
const nextMissionId = orderedMissions [ currentIdIndex + 1 ]
const nextSeasonId = getSeasonId ( currentIdIndex + 1 )
let shouldContinue = true
// nextSeasonId > gameVersion's integer
if ( parseInt ( nextSeasonId ) > parseInt ( req . gameVersion [ 1 ] ) ) {
shouldContinue = false
}
if ( shouldContinue ) {
cats . push (
createPlayNextTile (
req . jwt . unique_name ,
nextMissionId ,
req . gameVersion ,
{
CampaignName : ` UI_SEASON_ ${ nextSeasonId } ` ,
} ,
) ,
)
}
2023-03-06 18:12:52 +01:00
cats . push ( createMainOpportunityTile ( req . query . contractId ) )
2022-10-20 03:18:35 +02:00
}
//#endregion
2023-02-24 00:16:38 +01:00
//#region PZ missions
const pzIdIndex = orderedPZMissions . indexOf ( req . query . contractId )
if ( pzIdIndex !== - 1 && pzIdIndex !== orderedPZMissions . length - 1 ) {
const nextMissionId = orderedPZMissions [ pzIdIndex + 1 ]
cats . push (
createPlayNextTile (
req . jwt . unique_name ,
nextMissionId ,
req . gameVersion ,
{
CampaignName : "UI_CONTRACT_CAMPAIGN_WHITE_SPIDER_TITLE" ,
ParentCampaignName : "UI_MENU_PAGE_SIDE_MISSIONS_TITLE" ,
} ,
) ,
)
}
//#endregion
//#region Atlantide
if ( req . query . contractId === "f1ba328f-e3dd-4ef8-bb26-0363499fdd95" ) {
const nextMissionId = "0b616e62-af0c-495b-82e3-b778e82b5912"
cats . push (
createPlayNextTile (
req . jwt . unique_name ,
nextMissionId ,
req . gameVersion ,
{
CampaignName : "UI_MENU_PAGE_SPECIAL_ASSIGNMENTS_TITLE" ,
ParentCampaignName : "UI_MENU_PAGE_SIDE_MISSIONS_TITLE" ,
} ,
) ,
)
}
//#endregion
2022-10-20 03:18:35 +02:00
//#region Plugin missions
const pluginData = controller . hooks . getNextCampaignMission . call (
req . query . contractId ,
req . gameVersion ,
)
if ( pluginData ) {
2023-04-21 14:17:21 +02:00
if ( pluginData . overrideIndex !== undefined ) {
cats [ pluginData . overrideIndex ] = createPlayNextTile (
2022-10-20 03:18:35 +02:00
req . jwt . unique_name ,
pluginData . nextContractId ,
req . gameVersion ,
pluginData . campaignDetails ,
2023-04-21 14:17:21 +02:00
)
} else {
cats . push (
createPlayNextTile (
req . jwt . unique_name ,
pluginData . nextContractId ,
req . gameVersion ,
pluginData . campaignDetails ,
) ,
)
}
2022-10-20 03:18:35 +02:00
}
//#endregion
res . json ( {
template : getConfig ( "PlayNextTemplate" , false ) ,
data : {
Categories : cats ,
ProfileId : req.jwt.unique_name ,
} ,
} )
} ,
)
menuDataRouter . get ( "/LeaderboardsView" , ( req , res ) = > {
res . json ( {
template : getConfig ( "LeaderboardsViewTemplate" , false ) ,
data : {
LeaderboardUrl : "leaderboardentries" ,
LeaderboardType : "singleplayer" ,
} ,
} )
} )
const leaderboardEntries = async (
req : RequestWithJwt < { contractid : string ; difficultyLevel? : string } > ,
res : Response ,
) = > {
let difficulty = "unset"
const parsedDifficulty = parseInt ( req . query ? . difficultyLevel || "0" )
if ( parsedDifficulty === gameDifficulty . casual ) {
difficulty = "casual"
}
if ( parsedDifficulty === gameDifficulty . normal ) {
difficulty = "normal"
}
if ( parsedDifficulty === gameDifficulty . master ) {
difficulty = "master"
}
const response = {
template : getConfig ( "LeaderboardEntriesTemplate" , false ) ,
data : {
Entries : [ ] as ApiLeaderboardEntry [ ] ,
Contract : controller.resolveContract ( req . query . contractid ) ,
Page : 0 ,
HasMore : false ,
LeaderboardType : "singleplayer" ,
} ,
}
type ApiLeaderboardEntry = {
LeaderboardData : {
Player : {
displayName : string
}
}
gameVersion : {
id : number
name : string
}
platformId : string
platform : {
id : number
name : string
}
}
const entries = (
await axios . post < ApiLeaderboardEntry [ ] > (
` ${ getFlag ( "leaderboardsHost" ) } /leaderboards/entries/ ${
req . query . contractid
} ` ,
{
gameVersion : req.gameVersion ,
difficulty ,
platform : req.jwt.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 . data . Entries = entries
res . json ( response )
}
menuDataRouter . get ( "/LeaderboardEntries" , leaderboardEntries )
menuDataRouter . get (
"/DebriefingLeaderboards" ,
async (
req : RequestWithJwt < { contractid : string ; difficulty? : string } > ,
res ,
) = > {
const debriefingLeaderboardsTemplate = getConfig (
"DebriefingLeaderboardsTemplate" ,
false ,
)
const resJsonFunc = res . json
res . json = function ( input ) {
return resJsonFunc . call ( this , {
template : debriefingLeaderboardsTemplate ,
data : input.data ,
} )
}
await leaderboardEntries ( req , res )
} ,
)
menuDataRouter . get ( "/Contracts" , contractsModeHome )
2022-11-24 04:11:22 +01:00
preMenuDataRouter . get (
2022-10-20 03:18:35 +02:00
"/contractcreation/planning" ,
(
req : RequestWithJwt < { contractCreationIdOverwrite : string } > ,
res ,
next ,
) = > {
const createContractPlanningTemplate = getConfig (
"CreateContractPlanningTemplate" ,
false ,
)
req . url = "/Planning"
req . query . contractid = req . query . contractCreationIdOverwrite
req . query . resetescalation = "false"
const originalJsonFunc = res . json
res . json = function ( originalData ) {
const d = originalData . data
// create contract planning isn't supposed to have the following properties
for ( const key of [
"ElusiveContractState" ,
"UserCentric" ,
"UserContract" ,
"UnlockedEntrances" ,
"UnlockedAgencyPickups" ,
"Objectives" ,
"CharacterLoadoutData" ,
"ChallengeData" ,
"Currency" ,
"PaymentDetails" ,
"OpportunityData" ,
"PlayerProfileXpData" ,
] ) {
d [ key ] = undefined
}
return originalJsonFunc . call ( this , {
template : createContractPlanningTemplate ,
data : d ,
} )
}
next ( "router" )
} ,
)
menuDataRouter . get ( "/contractsearchpage" , ( req : RequestWithJwt , res ) = > {
const createContractTutorial = controller . resolveContract (
2023-02-14 04:14:16 +01:00
contractCreationTutorialId ,
2022-10-20 03:18:35 +02:00
)
res . json ( {
template : getVersionedConfig (
"ContractSearchPageTemplate" ,
req . gameVersion ,
false ,
) ,
data : {
CreateContractTutorial : generateUserCentric (
createContractTutorial ! ,
req . jwt . unique_name ,
req . gameVersion ,
) ,
2023-02-05 16:56:08 +01:00
LocationsData : createLocationsData ( req . gameVersion , true ) ,
2022-10-20 03:18:35 +02:00
FilterData : getVersionedConfig (
"FilterData" ,
req . gameVersion ,
false ,
) ,
} ,
} )
} )
menuDataRouter . post (
"/ContractSearch" ,
jsonMiddleware ( ) ,
async ( req : RequestWithJwt < { sorting? : unknown } , string [ ] > , res ) = > {
const specialContracts : string [ ] = [ ]
await controller . hooks . getSearchResults . callAsync (
req . body ,
specialContracts ,
)
2023-04-02 06:22:26 +02:00
let searchResult : ContractSearchResult
2022-10-20 03:18:35 +02:00
2023-03-06 19:17:22 +01:00
if ( specialContracts . length > 0 ) {
// Handled by a plugin
2022-10-20 03:18:35 +02:00
2023-03-06 19:17:22 +01:00
const contracts : { UserCentricContract : UserCentricContract } [ ] = [ ]
2022-10-20 03:18:35 +02:00
2023-03-06 19:17:22 +01:00
for ( const contract of specialContracts ) {
const userCentric = generateUserCentric (
controller . resolveContract ( contract ) ,
req . jwt . unique_name ,
req . gameVersion ,
)
2022-10-20 03:18:35 +02:00
2023-03-06 19:17:22 +01:00
if ( ! userCentric ) {
log (
LogLevel . ERROR ,
"UC is null! (contract not registered?)" ,
)
continue
}
contracts . push ( {
UserCentricContract : userCentric ,
} )
}
searchResult = {
2022-10-20 03:18:35 +02:00
Data : {
Contracts : contracts ,
TotalCount : contracts.length ,
Page : 0 ,
ErrorReason : "" ,
HasPrevious : false ,
HasMore : false ,
} ,
2023-03-06 19:17:22 +01:00
}
} else {
// No plugins handle this. Getting search results from official
searchResult = await officialSearchContract (
req . jwt . unique_name ,
req . gameVersion ,
req . body ,
0 ,
)
}
res . json ( {
template : getVersionedConfig (
"ContractSearchResponseTemplate" ,
req . gameVersion ,
false ,
) ,
data : searchResult ,
} )
} ,
)
menuDataRouter . post (
"/ContractSearchPaginate" ,
jsonMiddleware ( ) ,
async ( req : RequestWithJwt < { page : number } , string [ ] > , res ) = > {
res . json ( {
template : getConfig ( "ContractSearchPaginateTemplate" , false ) ,
data : await officialSearchContract (
req . jwt . unique_name ,
req . gameVersion ,
req . body ,
req . query . page ,
) ,
2022-10-20 03:18:35 +02:00
} )
} ,
)
2023-01-04 17:36:22 +01:00
menuDataRouter . get (
"/DebriefingChallenges" ,
( req : RequestWithJwt < { contractId : string } > , res ) = > {
res . json ( {
template : getConfig ( "DebriefingChallengesTemplate" , false ) ,
data : {
ChallengeData : {
Children :
controller . challengeService . getChallengeTreeForContract (
req . query . contractId ,
req . gameVersion ,
req . jwt . unique_name ,
) ,
} ,
} ,
} )
} ,
)
2022-10-20 03:18:35 +02:00
menuDataRouter . get ( "/contractcreation/create" , ( req : RequestWithJwt , res ) = > {
let cUuid = randomUUID ( )
const createContractReturnTemplate = getConfig (
"CreateContractReturnTemplate" ,
false ,
)
// if for some reason the id is already in use, generate a new one
// the math says this is like a one in a billion chance though, I think
while ( controller . resolveContract ( cUuid ) ) {
cUuid = randomUUID ( )
}
const sesh = getSession ( req . jwt . unique_name )
const one = "1"
const two = ` ${ random . int ( 10 , 99 ) } `
const three = ` ${ random . int ( 1 _000_000 , 9 _999_999 ) } `
const four = ` ${ random . int ( 10 , 99 ) } `
const contractId = [ one , two , three , four ] . join ( "-" )
const joined = [ one , two , three , four ] . join ( "" )
// See my comment in contractRouting.ts about the Math.ceil call
const timeLimit = Math . ceil (
( sesh . timerEnd as number ) - ( sesh . timerStart as number ) ,
)
const timeSeconds = timeLimit % 60
const timeMinutes = Math . trunc ( timeLimit / 60 ) % 60
const timeHours = Math . trunc ( timeLimit / 3600 )
const timeLimitStr = ` ${
timeHours ? ` ${ timeHours } : ` : ""
} $ { ` 0 ${ timeMinutes } ` . slice ( - 2 ) } : $ { ` 0 ${ timeSeconds } ` . slice ( - 2 ) } `
res . json ( {
template : createContractReturnTemplate ,
data : {
Contract : {
Title : {
$loc : {
key : "UI_CONTRACTS_UGC_TITLE" ,
data : [ contractId ] ,
} ,
} ,
Description : "UI_CONTRACTS_UGC_DESCRIPTION" ,
Targets : Array.from ( sesh . kills )
. filter ( ( kill ) = >
sesh . markedTargets . has ( kill . _RepositoryId ) ,
)
. map ( ( km ) = > {
return {
RepositoryId : km._RepositoryId ,
Selected : true ,
Weapon : {
RepositoryId : km.KillItemRepositoryId ,
KillMethodBroad : km.KillMethodBroad ,
KillMethodStrict : km.KillMethodStrict ,
RequiredKillMethodType : 3 ,
} ,
Outfit : {
RepositoryId : km.OutfitRepoId ,
Required : true ,
2023-04-14 04:13:16 +02:00
IsHitmanSuit : isSuit ( km . OutfitRepoId ) ,
2022-10-20 03:18:35 +02:00
} ,
}
} ) ,
ContractConditions : complications ( timeLimitStr ) ,
2023-02-14 04:14:16 +01:00
PublishingDisabled :
sesh . contractId === contractCreationTutorialId ,
2022-10-20 03:18:35 +02:00
Creator : req.jwt.unique_name ,
ContractId : cUuid ,
ContractPublicId : joined ,
} ,
} ,
} )
} )
const createLoadSaveMiddleware =
( menuTemplate : string ) = >
( req : RequestWithJwt < { sessionIds? : string } , string [ ] > , res : Response ) = > {
const template = getVersionedConfig (
menuTemplate ,
req . gameVersion ,
false ,
)
const doneContracts : string [ ] = [ ]
const response = {
template ,
data : {
Contracts : [ ] as UserCentricContract [ ] ,
PaymentEligiblity : { } ,
} ,
}
for ( const e of req . body ) {
if ( e && ! doneContracts . includes ( e ) ) {
doneContracts . push ( e )
const contract = controller . resolveContract ( e )
if ( ! contract ) {
log ( LogLevel . WARN , ` Unknown contract in L/S: ${ e } ` )
continue
}
response . data . Contracts . push (
generateUserCentric (
contract ,
req . jwt . unique_name ,
req . gameVersion ,
) ! ,
)
}
}
if ( req . gameVersion === "h1" ) {
for ( const e of req . body ) {
if ( e ) {
response . data . PaymentEligiblity [ e ] = false
}
}
} else {
for ( const sessionId of req . query . sessionIds ? . split ( "," ) || [ ] ) {
response . data . PaymentEligiblity [ sessionId ] = false
}
}
res . json ( response )
}
menuDataRouter . post (
"/Load" ,
jsonMiddleware ( ) ,
createLoadSaveMiddleware ( "LoadMenuTemplate" ) ,
)
menuDataRouter . post (
"/Save" ,
jsonMiddleware ( ) ,
createLoadSaveMiddleware ( "SaveMenuTemplate" ) ,
)
2023-01-20 22:00:20 +01:00
menuDataRouter . get ( "/PlayerProfile" , ( req : RequestWithJwt , res ) = > {
const playerProfilePage = getConfig < PlayerProfileView > (
"PlayerProfilePage" ,
true ,
)
2023-04-08 02:13:05 +02:00
const locationData = getVersionedConfig < PeacockLocationsData > (
"LocationsData" ,
req . gameVersion ,
false ,
)
playerProfilePage . data . SubLocationData = [ ]
for ( const subLocationKey in locationData . children ) {
//Ewww...
if (
subLocationKey === "LOCATION_ICA_FACILITY_ARRIVAL" ||
subLocationKey === "LOCATION_HOKKAIDO_SHIM_MAMUSHI" ||
subLocationKey . includes ( "SNUG_" )
) {
continue
}
const subLocation = locationData . children [ subLocationKey ]
const parentLocation =
locationData . parents [ subLocation . Properties . ParentLocation ]
const completionData = generateCompletionData (
subLocation . Id ,
req . jwt . unique_name ,
req . gameVersion ,
)
//TODO: Make getDestinationCompletion do something like this.
const challenges = controller . challengeService . getChallengesForLocation (
subLocation . Id ,
req . gameVersion ,
)
const challengeCategoryCompletion : ChallengeCategoryCompletion [ ] = [ ]
for ( const challengeGroup in challenges ) {
const challengeCompletion =
controller . challengeService . countTotalNCompletedChallenges (
{
challengeGroup : challenges [ challengeGroup ] ,
} ,
req . jwt . unique_name ,
req . gameVersion ,
)
challengeCategoryCompletion . push ( {
Name : challenges [ challengeGroup ] [ 0 ] . CategoryName ,
. . . challengeCompletion ,
} )
}
const destinationCompletion = getDestinationCompletion (
parentLocation ,
subLocation ,
req ,
)
playerProfilePage . data . SubLocationData . push ( {
ParentLocation : parentLocation ,
Location : subLocation ,
CompletionData : completionData ,
ChallengeCategoryCompletion : challengeCategoryCompletion ,
ChallengeCompletion : destinationCompletion.ChallengeCompletion ,
OpportunityStatistics : destinationCompletion.OpportunityStatistics ,
LocationCompletionPercent :
destinationCompletion . LocationCompletionPercent ,
} )
}
2023-01-20 22:00:20 +01:00
const userProfile = getUserData ( req . jwt . unique_name , req . gameVersion )
playerProfilePage . data . PlayerProfileXp . Total =
userProfile . Extensions . progression . PlayerProfileXP . Total
playerProfilePage . data . PlayerProfileXp . Level =
userProfile . Extensions . progression . PlayerProfileXP . ProfileLevel
2022-10-20 03:18:35 +02:00
2023-03-18 22:53:14 +01:00
const subLocationMap = new Map (
userProfile . Extensions . progression . PlayerProfileXP . Sublocations . map (
( obj ) = > [ obj . Location , obj ] ,
) ,
)
playerProfilePage . data . PlayerProfileXp . Seasons . forEach ( ( e ) = >
e . Locations . forEach ( ( f ) = > {
const subLocationData = subLocationMap . get ( f . LocationId )
f . Xp = subLocationData ? . Xp || 0
f . ActionXp = subLocationData ? . ActionXp || 0
if ( f . LocationProgression ) {
f . LocationProgression . Level =
userProfile . Extensions . progression . Locations [
f . LocationId . toLocaleLowerCase ( )
] ? . Level || 1
}
} ) ,
)
2022-10-20 03:18:35 +02:00
res . json ( playerProfilePage )
} )
menuDataRouter . get (
// who at IOI decided this was a good route name???!
"/LookupContractDialogAddOrDeleteFromPlaylist" ,
withLookupDialog ,
)
menuDataRouter . get (
// this one is sane Kappa
2023-03-26 20:30:11 +02:00
"/contractplaylist/addordelete/:contractId" ,
2022-10-20 03:18:35 +02:00
directRoute ,
)
2023-03-26 20:30:11 +02:00
menuDataRouter . post (
"/contractplaylist/deletemultiple" ,
jsonMiddleware ( ) ,
deleteMultiple ,
)
2022-10-20 03:18:35 +02:00
menuDataRouter . get ( "/GetPlayerProfileXpData" , ( req : RequestWithJwt , res ) = > {
const userData = getUserData ( req . jwt . unique_name , req . gameVersion )
res . json ( {
template : null ,
data : {
PlayerProfileXpData : {
XP : userData.Extensions.progression.PlayerProfileXP.Total ,
Level : userData.Extensions.progression.PlayerProfileXP
. ProfileLevel ,
2023-03-18 18:35:26 +01:00
MaxLevel : getMaxProfileLevel ( req . gameVersion ) ,
2022-10-20 03:18:35 +02:00
} ,
} ,
} )
} )
menuDataRouter . get (
"/GetMasteryCompletionDataForLocation" ,
( req : RequestWithJwt < GetCompletionDataForLocationQuery > , res ) = > {
res . json (
generateCompletionData (
req . query . locationId ,
req . jwt . unique_name ,
req . gameVersion ,
) ,
)
} ,
)
menuDataRouter . get (
"/MasteryUnlockable" ,
( req : RequestWithJwt < MasteryUnlockableQuery > , res ) = > {
let masteryUnlockTemplate = getConfig (
"MasteryUnlockablesTemplate" ,
false ,
)
const location = ( ( ) = > {
switch ( req . query . unlockableId ? . split ( "_" ) . slice ( 0 , 3 ) . join ( "_" ) ) {
case "FIREARMS_SC_HERO" :
return "LOCATION_AUSTRIA"
case "FIREARMS_SC_SEAGULL" :
return "LOCATION_SALTY_SEAGULL"
case "FIREARMS_SC_FALCON" :
return "LOCATION_CAGED_FALCON"
default :
assert . fail ( "fell through switch (bad query?)" )
}
} ) ( )
2023-03-24 14:19:01 +01:00
let sniperLoadout = getConfig < SniperLoadoutConfig > (
"SniperLoadouts" ,
false ,
) [ location ] [ req . query . unlockableId ]
2022-10-20 03:18:35 +02:00
if ( req . gameVersion === "scpc" ) {
masteryUnlockTemplate = JSON . parse (
JSON . stringify ( masteryUnlockTemplate ) . replace (
/UI_MENU_PAGE_MASTERY_LEVEL_SHORT+/g ,
"UI_MENU_PAGE_MASTERY_LEVEL" ,
) ,
)
2023-03-24 14:19:01 +01:00
sniperLoadout = JSON . parse (
JSON . stringify ( sniperLoadout ) . replace ( /hawk\/+/g , "" ) ,
2022-10-20 03:18:35 +02:00
)
}
2023-03-24 14:19:01 +01:00
const unlockables = sniperLoadout . Unlockable
2022-10-20 03:18:35 +02:00
res . json ( {
template : masteryUnlockTemplate ,
data : {
2023-03-24 14:19:01 +01:00
CompletionData : controller.masteryService.getFirearmCompletion (
req . query . unlockableId ,
sniperLoadout . MainUnlockable . Properties . Name ,
2022-10-20 03:18:35 +02:00
req . jwt . unique_name ,
req . gameVersion ,
) ,
2023-03-24 14:19:01 +01:00
Drops : unlockables.map ( ( unlockable ) = > ( {
IsLevelMarker : false ,
Unlockable : unlockable ,
Level :
unlockable . Properties . UnlockOrder ? ?
DEFAULT_MASTERY_MAXLEVEL ,
// TODO: Everything is unlocked. Change this when adding sniper progression
IsLocked : false ,
TypeLocaKey : "UI_MENU_PAGE_MASTERY_UNLOCKABLE_NAME_weapon" ,
} ) ) ,
Unlockable : sniperLoadout.MainUnlockable ,
2022-10-20 03:18:35 +02:00
} ,
} )
} ,
)
2023-01-20 22:00:20 +01:00
menuDataRouter . get (
"/MasteryDataForLocation" ,
( req : RequestWithJwt < { locationId : string } > , res ) = > {
res . json (
controller . masteryService . getMasteryDataForLocation (
req . query . locationId ,
req . gameVersion ,
req . jwt . unique_name ,
) ,
)
} ,
)
2022-10-20 03:18:35 +02:00
export { menuDataRouter }