mirror of
https://github.com/thepeacockproject/Peacock
synced 2025-02-16 16:34:28 +01:00
Enable strict types mode (#362)
Signed-off-by: Reece Dunham <me@rdil.rocks>
This commit is contained in:
parent
0585b35447
commit
5cc69434c6
.eslintrc.jspackage.json
components
2016
candle
configSwizzleManager.tscontracts
contractRouting.tsdataGen.ts
controller.tsdatabaseHandler.tsescalations
hitsCategoryService.tsleaderboards.tsreportRouting.tssessions.tsdiscord
entitlementStrategies.tseventHandler.tsflags.tsgeneratedPeacockRequireTable.tshooksImpl.tsindex.tsinventory.tsloadouts.tsloggingInterop.tsmenuData.tsmenus
campaigns.tsdestinations.tsfavoriteContracts.tshub.tsmenuSystem.tsplanning.tsplayerProfile.tsplaynext.tssniper.tsstashpoints.ts
multiplayer
oauthToken.tsofficialServerAuth.tsplayStyles.tsprofileHandler.tsscoreHandler.tssmfSupport.tsstatemachines
types
utils.tswebFeatures.tspackaging
static
tests
tsconfig.jsonwebui
yarn.lock@ -44,11 +44,13 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
|
"@typescript-eslint/prefer-optional-chain": "warn",
|
||||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||||
"@typescript-eslint/no-extra-semi": "off",
|
"@typescript-eslint/no-extra-semi": "off",
|
||||||
"@typescript-eslint/no-non-null-assertion": "off",
|
"@typescript-eslint/no-non-null-assertion": "off",
|
||||||
"@typescript-eslint/require-await": "warn",
|
"@typescript-eslint/require-await": "warn",
|
||||||
"@typescript-eslint/prefer-ts-expect-error": "error",
|
"@typescript-eslint/prefer-ts-expect-error": "error",
|
||||||
|
"no-nested-ternary": "warn",
|
||||||
eqeqeq: "error",
|
eqeqeq: "error",
|
||||||
"no-duplicate-imports": "warn",
|
"no-duplicate-imports": "warn",
|
||||||
"promise/always-return": "error",
|
"promise/always-return": "error",
|
||||||
|
@ -33,6 +33,7 @@ const legacyContractRouter = Router()
|
|||||||
legacyContractRouter.post(
|
legacyContractRouter.post(
|
||||||
"/GetForPlay",
|
"/GetForPlay",
|
||||||
jsonMiddleware(),
|
jsonMiddleware(),
|
||||||
|
// @ts-expect-error Has jwt props.
|
||||||
(req: RequestWithJwt, res) => {
|
(req: RequestWithJwt, res) => {
|
||||||
if (!uuidRegex.test(req.body.id)) {
|
if (!uuidRegex.test(req.body.id)) {
|
||||||
res.status(400).end()
|
res.status(400).end()
|
||||||
@ -130,6 +131,7 @@ legacyContractRouter.post(
|
|||||||
legacyContractRouter.post(
|
legacyContractRouter.post(
|
||||||
"/Start",
|
"/Start",
|
||||||
jsonMiddleware(),
|
jsonMiddleware(),
|
||||||
|
// @ts-expect-error Has jwt props.
|
||||||
(req: RequestWithJwt, res) => {
|
(req: RequestWithJwt, res) => {
|
||||||
if (req.body.profileId !== req.jwt.unique_name) {
|
if (req.body.profileId !== req.jwt.unique_name) {
|
||||||
res.status(400).end() // requested for different user id
|
res.status(400).end() // requested for different user id
|
||||||
|
@ -24,37 +24,9 @@ import { getParentLocationByName } from "../contracts/dataGen"
|
|||||||
|
|
||||||
const legacyMenuDataRouter = Router()
|
const legacyMenuDataRouter = Router()
|
||||||
|
|
||||||
legacyMenuDataRouter.get(
|
|
||||||
"/debriefingchallenges",
|
|
||||||
(
|
|
||||||
req: RequestWithJwt<{ contractSessionId: string; contractId: string }>,
|
|
||||||
res,
|
|
||||||
) => {
|
|
||||||
if (typeof req.query.contractId !== "string") {
|
|
||||||
res.status(400).send("invalid contractId")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// debriefingchallenges?contractSessionId=00000000000000-00000000-0000-0000-0000-000000000001&contractId=dd906289-7c32-427f-b689-98ae645b407f
|
|
||||||
res.json({
|
|
||||||
template: getConfig("LegacyDebriefingChallengesTemplate", false),
|
|
||||||
data: {
|
|
||||||
ChallengeData: {
|
|
||||||
// FIXME: This may not work correctly; I don't know the actual format so I'm assuming challenge tree
|
|
||||||
Children:
|
|
||||||
controller.challengeService.getChallengeTreeForContract(
|
|
||||||
req.query.contractId,
|
|
||||||
req.gameVersion,
|
|
||||||
req.jwt.unique_name,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
legacyMenuDataRouter.get(
|
legacyMenuDataRouter.get(
|
||||||
"/MasteryLocation",
|
"/MasteryLocation",
|
||||||
|
// @ts-expect-error Has jwt props.
|
||||||
(req: RequestWithJwt<{ locationId: string; difficulty: string }>, res) => {
|
(req: RequestWithJwt<{ locationId: string; difficulty: string }>, res) => {
|
||||||
const masteryData =
|
const masteryData =
|
||||||
controller.masteryService.getMasteryDataForDestination(
|
controller.masteryService.getMasteryDataForDestination(
|
||||||
|
@ -1,46 +0,0 @@
|
|||||||
/*
|
|
||||||
* The Peacock Project - a HITMAN server replacement.
|
|
||||||
* Copyright (C) 2021-2024 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 { join } from "path"
|
|
||||||
import md5File from "md5-file"
|
|
||||||
import { readFile } from "atomically"
|
|
||||||
|
|
||||||
const legacyMenuSystemRouter = Router()
|
|
||||||
|
|
||||||
// /resources-6-74/
|
|
||||||
|
|
||||||
legacyMenuSystemRouter.get(
|
|
||||||
"/dynamic_resources_pc_release_rpkg",
|
|
||||||
async (req, res) => {
|
|
||||||
const filePath = join(
|
|
||||||
PEACOCK_DEV ? process.cwd() : __dirname,
|
|
||||||
"resources",
|
|
||||||
"dynamic_resources_h1.rpkg",
|
|
||||||
)
|
|
||||||
|
|
||||||
const md5 = await md5File(filePath)
|
|
||||||
|
|
||||||
res.set("Content-Type", "application/octet-stream")
|
|
||||||
res.set("Content-MD5", Buffer.from(md5, "hex").toString("base64"))
|
|
||||||
|
|
||||||
res.send(await readFile(filePath))
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
export { legacyMenuSystemRouter }
|
|
@ -38,6 +38,7 @@ const legacyProfileRouter = Router()
|
|||||||
legacyProfileRouter.post(
|
legacyProfileRouter.post(
|
||||||
"/ChallengesService/GetActiveChallenges",
|
"/ChallengesService/GetActiveChallenges",
|
||||||
jsonMiddleware(),
|
jsonMiddleware(),
|
||||||
|
// @ts-expect-error Has jwt props.
|
||||||
(req: RequestWithJwt, res) => {
|
(req: RequestWithJwt, res) => {
|
||||||
if (!uuidRegex.test(req.body.contractId)) {
|
if (!uuidRegex.test(req.body.contractId)) {
|
||||||
return res.status(404).send("invalid contract")
|
return res.status(404).send("invalid contract")
|
||||||
@ -93,7 +94,13 @@ legacyProfileRouter.post(
|
|||||||
legacyProfileRouter.post(
|
legacyProfileRouter.post(
|
||||||
"/ChallengesService/GetProgression",
|
"/ChallengesService/GetProgression",
|
||||||
jsonMiddleware(),
|
jsonMiddleware(),
|
||||||
|
// @ts-expect-error Has jwt props.
|
||||||
(req: RequestWithJwt<never, LegacyGetProgressionBody>, res) => {
|
(req: RequestWithJwt<never, LegacyGetProgressionBody>, res) => {
|
||||||
|
if (!Array.isArray(req.body.challengeids)) {
|
||||||
|
res.status(400).send("invalid body")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const legacyGlobalChallenges = getConfig<CompiledChallengeIngameData[]>(
|
const legacyGlobalChallenges = getConfig<CompiledChallengeIngameData[]>(
|
||||||
"LegacyGlobalChallenges",
|
"LegacyGlobalChallenges",
|
||||||
false,
|
false,
|
||||||
@ -114,10 +121,11 @@ legacyProfileRouter.post(
|
|||||||
MustBeSaved: false,
|
MustBeSaved: false,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
/*
|
|
||||||
for (const challengeId of req.body.challengeids) {
|
for (const challengeId of req.body.challengeids) {
|
||||||
const challenge =
|
const challenge = controller.challengeService.getChallengeById(
|
||||||
controller.challengeService.getChallengeById(challengeId)
|
challengeId,
|
||||||
|
"h1",
|
||||||
|
)
|
||||||
|
|
||||||
if (!challenge) {
|
if (!challenge) {
|
||||||
log(
|
log(
|
||||||
@ -128,7 +136,7 @@ legacyProfileRouter.post(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const progression =
|
const progression =
|
||||||
controller.challengeService.getChallengeProgression(
|
controller.challengeService.getPersistentChallengeProgression(
|
||||||
req.jwt.unique_name,
|
req.jwt.unique_name,
|
||||||
challengeId,
|
challengeId,
|
||||||
req.gameVersion,
|
req.gameVersion,
|
||||||
@ -138,19 +146,16 @@ legacyProfileRouter.post(
|
|||||||
ChallengeId: challengeId,
|
ChallengeId: challengeId,
|
||||||
ProfileId: req.jwt.unique_name,
|
ProfileId: req.jwt.unique_name,
|
||||||
Completed: progression.Completed,
|
Completed: progression.Completed,
|
||||||
|
Ticked: progression.Ticked,
|
||||||
State: progression.State,
|
State: progression.State,
|
||||||
ETag: `W/"datetime'${encodeURIComponent(
|
ETag: `W/"datetime'${encodeURIComponent(
|
||||||
new Date().toISOString(),
|
new Date().toISOString(),
|
||||||
)}'"`,
|
)}'"`,
|
||||||
CompletedAt: progression.CompletedAt,
|
CompletedAt: progression.CompletedAt,
|
||||||
MustBeSaved: false,
|
MustBeSaved: progression.MustBeSaved,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
*/
|
// TODO: HELP! Please DM rdil if you see this
|
||||||
// TODO: atampy broke this - please fix
|
|
||||||
// update(RD) nov 18 '22: fixed but still missing challenges in
|
|
||||||
// 2016 engine (e.g. showstopper is missing 9, 5 of which are the
|
|
||||||
// classics I think, not sure about the other 4)
|
|
||||||
|
|
||||||
res.json(challenges)
|
res.json(challenges)
|
||||||
},
|
},
|
||||||
|
@ -18,7 +18,6 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
ChallengeProgressionData,
|
ChallengeProgressionData,
|
||||||
CompiledChallengeRewardData,
|
|
||||||
CompiledChallengeRuntimeData,
|
CompiledChallengeRuntimeData,
|
||||||
InclusionData,
|
InclusionData,
|
||||||
MissionManifest,
|
MissionManifest,
|
||||||
@ -28,19 +27,6 @@ import { SavedChallengeGroup } from "../types/challenges"
|
|||||||
import { controller } from "../controller"
|
import { controller } from "../controller"
|
||||||
import { gameDifficulty, isSniperLocation } from "../utils"
|
import { gameDifficulty, isSniperLocation } from "../utils"
|
||||||
|
|
||||||
// TODO: unused?
|
|
||||||
export function compileScoringChallenge(
|
|
||||||
challenge: RegistryChallenge,
|
|
||||||
): CompiledChallengeRewardData {
|
|
||||||
return {
|
|
||||||
ChallengeId: challenge.Id,
|
|
||||||
ChallengeName: challenge.Name,
|
|
||||||
ChallengeDescription: challenge.Description,
|
|
||||||
ChallengeImageUrl: challenge.ImageName,
|
|
||||||
XPGain: challenge.Rewards?.MasteryXP || 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function compileRuntimeChallenge(
|
export function compileRuntimeChallenge(
|
||||||
challenge: RegistryChallenge,
|
challenge: RegistryChallenge,
|
||||||
progression: ChallengeProgressionData,
|
progression: ChallengeProgressionData,
|
||||||
@ -106,8 +92,8 @@ export type ChallengeFilterOptions =
|
|||||||
* @returns A boolean as the result.
|
* @returns A boolean as the result.
|
||||||
*/
|
*/
|
||||||
export function inclusionDataCheck(
|
export function inclusionDataCheck(
|
||||||
incData: InclusionData,
|
incData: InclusionData | undefined,
|
||||||
contract: MissionManifest,
|
contract: MissionManifest | undefined,
|
||||||
): boolean {
|
): boolean {
|
||||||
if (!incData) return true
|
if (!incData) return true
|
||||||
if (!contract) return false
|
if (!contract) return false
|
||||||
@ -174,9 +160,9 @@ function isChallengeInContract(
|
|||||||
: {
|
: {
|
||||||
...challenge.InclusionData,
|
...challenge.InclusionData,
|
||||||
ContractTypes:
|
ContractTypes:
|
||||||
challenge.InclusionData.ContractTypes.filter(
|
challenge.InclusionData?.ContractTypes?.filter(
|
||||||
(type) => type !== "tutorial",
|
(type) => type !== "tutorial",
|
||||||
),
|
) || [],
|
||||||
},
|
},
|
||||||
contract,
|
contract,
|
||||||
)
|
)
|
||||||
@ -184,14 +170,15 @@ function isChallengeInContract(
|
|||||||
|
|
||||||
// Is this for the current contract or group contract?
|
// Is this for the current contract or group contract?
|
||||||
const isForContract = (challenge.InclusionData?.ContractIds || []).includes(
|
const isForContract = (challenge.InclusionData?.ContractIds || []).includes(
|
||||||
contract.Metadata.Id,
|
contract?.Metadata.Id || "",
|
||||||
)
|
)
|
||||||
|
|
||||||
// Is this for the current contract type?
|
// Is this for the current contract type?
|
||||||
// As of v6.1.0, this is only used for ET challenges.
|
// As of v6.1.0, this is only used for ET challenges.
|
||||||
|
// We have to resolve the non-group contract, `contract` is the group contract
|
||||||
const isForContractType = (
|
const isForContractType = (
|
||||||
challenge.InclusionData?.ContractTypes || []
|
challenge.InclusionData?.ContractTypes || []
|
||||||
).includes(controller.resolveContract(contractId).Metadata.Type)
|
).includes(controller.resolveContract(contractId)!.Metadata.Type)
|
||||||
|
|
||||||
// Is this a location-wide challenge?
|
// Is this a location-wide challenge?
|
||||||
// "location" is more widely used, but "parentlocation" is used in Ambrose and Berlin, as well as some "Discover XX" challenges.
|
// "location" is more widely used, but "parentlocation" is used in Ambrose and Berlin, as well as some "Discover XX" challenges.
|
||||||
@ -287,7 +274,7 @@ export function filterChallenge(
|
|||||||
*/
|
*/
|
||||||
export function mergeSavedChallengeGroups(
|
export function mergeSavedChallengeGroups(
|
||||||
g1: SavedChallengeGroup,
|
g1: SavedChallengeGroup,
|
||||||
g2: SavedChallengeGroup,
|
g2?: SavedChallengeGroup,
|
||||||
): SavedChallengeGroup {
|
): SavedChallengeGroup {
|
||||||
return {
|
return {
|
||||||
...g1,
|
...g1,
|
||||||
|
@ -49,7 +49,7 @@ import {
|
|||||||
HandleEventOptions,
|
HandleEventOptions,
|
||||||
} from "@peacockproject/statemachine-parser"
|
} from "@peacockproject/statemachine-parser"
|
||||||
import { ChallengeContext, SavedChallengeGroup } from "../types/challenges"
|
import { ChallengeContext, SavedChallengeGroup } from "../types/challenges"
|
||||||
import { fastClone, isSniperLocation } from "../utils"
|
import { fastClone, gameDifficulty, isSniperLocation } from "../utils"
|
||||||
import {
|
import {
|
||||||
ChallengeFilterOptions,
|
ChallengeFilterOptions,
|
||||||
ChallengeFilterType,
|
ChallengeFilterType,
|
||||||
@ -88,12 +88,13 @@ export abstract class ChallengeRegistry {
|
|||||||
* @Key2 The challenge Id.
|
* @Key2 The challenge Id.
|
||||||
* @value A `RegistryChallenge` object.
|
* @value A `RegistryChallenge` object.
|
||||||
*/
|
*/
|
||||||
protected challenges: Map<GameVersion, Map<string, RegistryChallenge>> =
|
protected challenges: Record<GameVersion, Map<string, RegistryChallenge>> =
|
||||||
new Map([
|
{
|
||||||
["h1", new Map()],
|
h1: new Map(),
|
||||||
["h2", new Map()],
|
h2: new Map(),
|
||||||
["h3", new Map()],
|
h3: new Map(),
|
||||||
])
|
scpc: new Map(),
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Key1 Game version.
|
* @Key1 Game version.
|
||||||
@ -101,14 +102,15 @@ export abstract class ChallengeRegistry {
|
|||||||
* @Key3 The group Id.
|
* @Key3 The group Id.
|
||||||
* @Value A `SavedChallengeGroup` object.
|
* @Value A `SavedChallengeGroup` object.
|
||||||
*/
|
*/
|
||||||
protected groups: Map<
|
protected groups: Record<
|
||||||
GameVersion,
|
GameVersion,
|
||||||
Map<string, Map<string, SavedChallengeGroup>>
|
Map<string, Map<string, SavedChallengeGroup>>
|
||||||
> = new Map([
|
> = {
|
||||||
["h1", new Map()],
|
h1: new Map(),
|
||||||
["h2", new Map()],
|
h2: new Map(),
|
||||||
["h3", new Map()],
|
h3: new Map(),
|
||||||
])
|
scpc: new Map(),
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Key1 Game version.
|
* @Key1 Game version.
|
||||||
@ -116,28 +118,30 @@ export abstract class ChallengeRegistry {
|
|||||||
* @Key3 The group Id.
|
* @Key3 The group Id.
|
||||||
* @Value A `Set` of challenge Ids.
|
* @Value A `Set` of challenge Ids.
|
||||||
*/
|
*/
|
||||||
protected groupContents: Map<
|
protected groupContents: Record<
|
||||||
GameVersion,
|
GameVersion,
|
||||||
Map<string, Map<string, Set<string>>>
|
Map<string, Map<string, Set<string>>>
|
||||||
> = new Map([
|
> = {
|
||||||
["h1", new Map()],
|
h1: new Map(),
|
||||||
["h2", new Map()],
|
h2: new Map(),
|
||||||
["h3", new Map()],
|
h3: new Map(),
|
||||||
])
|
scpc: new Map(),
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Key1 Game version.
|
* @Key1 Game version.
|
||||||
* @Key2 The challenge Id.
|
* @Key2 The challenge Id.
|
||||||
* @Value An `array` of challenge Ids that Key2 depends on.
|
* @Value An `array` of challenge Ids that Key2 depends on.
|
||||||
*/
|
*/
|
||||||
protected readonly _dependencyTree: Map<
|
protected readonly _dependencyTree: Record<
|
||||||
GameVersion,
|
GameVersion,
|
||||||
Map<string, readonly string[]>
|
Map<string, readonly string[]>
|
||||||
> = new Map([
|
> = {
|
||||||
["h1", new Map()],
|
h1: new Map(),
|
||||||
["h2", new Map()],
|
h2: new Map(),
|
||||||
["h3", new Map()],
|
h3: new Map(),
|
||||||
])
|
scpc: new Map(),
|
||||||
|
}
|
||||||
|
|
||||||
protected constructor(protected readonly controller: Controller) {}
|
protected constructor(protected readonly controller: Controller) {}
|
||||||
|
|
||||||
@ -147,13 +151,9 @@ export abstract class ChallengeRegistry {
|
|||||||
location: string,
|
location: string,
|
||||||
gameVersion: GameVersion,
|
gameVersion: GameVersion,
|
||||||
): void {
|
): void {
|
||||||
if (!this.groupContents.has(gameVersion)) {
|
const gameChallenges = this.groupContents[gameVersion]
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const gameChallenges = this.groupContents.get(gameVersion)
|
|
||||||
challenge.inGroup = groupId
|
challenge.inGroup = groupId
|
||||||
this.challenges.get(gameVersion)?.set(challenge.Id, challenge)
|
this.challenges[gameVersion].set(challenge.Id, challenge)
|
||||||
|
|
||||||
if (!gameChallenges.has(location)) {
|
if (!gameChallenges.has(location)) {
|
||||||
gameChallenges.set(location, new Map())
|
gameChallenges.set(location, new Map())
|
||||||
@ -176,34 +176,33 @@ export abstract class ChallengeRegistry {
|
|||||||
location: string,
|
location: string,
|
||||||
gameVersion: GameVersion,
|
gameVersion: GameVersion,
|
||||||
): void {
|
): void {
|
||||||
if (!this.groups.has(gameVersion)) {
|
const gameGroups = this.groups[gameVersion]
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const gameGroups = this.groups.get(gameVersion)
|
|
||||||
|
|
||||||
if (!gameGroups.has(location)) {
|
if (!gameGroups.has(location)) {
|
||||||
gameGroups.set(location, new Map())
|
gameGroups.set(location, new Map())
|
||||||
}
|
}
|
||||||
|
|
||||||
gameGroups.get(location).set(group.CategoryId, group)
|
gameGroups.get(location)?.set(group.CategoryId, group)
|
||||||
}
|
}
|
||||||
|
|
||||||
getChallengeById(
|
getChallengeById(
|
||||||
challengeId: string,
|
challengeId: string,
|
||||||
gameVersion: GameVersion,
|
gameVersion: GameVersion,
|
||||||
): RegistryChallenge | undefined {
|
): RegistryChallenge | undefined {
|
||||||
return this.challenges.get(gameVersion)?.get(challengeId)
|
return this.challenges[gameVersion].get(challengeId)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a list of all challenges unlockables
|
* This method retrieves all the unlockables associated with the challenges for a given game version.
|
||||||
|
* It iterates over all the challenges for the specified game version and for each challenge, it checks if there are any unlockables (Drops).
|
||||||
|
* If there are unlockables, it adds them to the accumulator object with the dropId as the key and the challenge Id as the value.
|
||||||
*
|
*
|
||||||
* @todo This is bad, untyped, and undocumented. Fix it.
|
* @param gameVersion - The version of the game for which to retrieve the unlockables.
|
||||||
|
* @returns {Record<string, string>} - An object where each key is an unlockable's id (dropId) and the corresponding value is the id of the challenge that unlocks it.
|
||||||
*/
|
*/
|
||||||
getChallengesUnlockables(gameVersion: GameVersion) {
|
getChallengesUnlockables(gameVersion: GameVersion): Record<string, string> {
|
||||||
return [...this.challenges.get(gameVersion).values()].reduce(
|
return [...this.challenges[gameVersion].values()].reduce(
|
||||||
(acc, challenge) => {
|
(acc: Record<string, string>, challenge) => {
|
||||||
if (challenge?.Drops?.length) {
|
if (challenge?.Drops?.length) {
|
||||||
challenge.Drops.forEach(
|
challenge.Drops.forEach(
|
||||||
(dropId) => (acc[dropId] = challenge.Id),
|
(dropId) => (acc[dropId] = challenge.Id),
|
||||||
@ -228,15 +227,18 @@ export abstract class ChallengeRegistry {
|
|||||||
location: string,
|
location: string,
|
||||||
gameVersion: GameVersion,
|
gameVersion: GameVersion,
|
||||||
): SavedChallengeGroup | undefined {
|
): SavedChallengeGroup | undefined {
|
||||||
if (!this.groups.has(gameVersion)) {
|
const gameGroups = this.groups[gameVersion]
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
const gameGroups = this.groups.get(gameVersion)
|
const mainGroup = gameGroups.get(location)?.get(groupId)
|
||||||
|
|
||||||
if (groupId === "feats" && gameVersion !== "h3") {
|
if (groupId === "feats" && gameVersion !== "h3") {
|
||||||
|
if (!mainGroup) {
|
||||||
|
// emergency bailout - shouldn't happen in practice
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
return mergeSavedChallengeGroups(
|
return mergeSavedChallengeGroups(
|
||||||
gameGroups.get(location)?.get(groupId),
|
mainGroup,
|
||||||
gameGroups.get("GLOBAL_ESCALATION_CHALLENGES")?.get(groupId),
|
gameGroups.get("GLOBAL_ESCALATION_CHALLENGES")?.get(groupId),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -255,8 +257,13 @@ export abstract class ChallengeRegistry {
|
|||||||
|
|
||||||
// Included by default. Filtered later.
|
// Included by default. Filtered later.
|
||||||
if (groupId === "classic" && location !== "GLOBAL_CLASSIC_CHALLENGES") {
|
if (groupId === "classic" && location !== "GLOBAL_CLASSIC_CHALLENGES") {
|
||||||
|
if (!mainGroup) {
|
||||||
|
// emergency bailout - shouldn't happen in practice
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
return mergeSavedChallengeGroups(
|
return mergeSavedChallengeGroups(
|
||||||
gameGroups.get(location)?.get(groupId),
|
mainGroup,
|
||||||
gameGroups.get("GLOBAL_CLASSIC_CHALLENGES")?.get(groupId),
|
gameGroups.get("GLOBAL_CLASSIC_CHALLENGES")?.get(groupId),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -265,13 +272,18 @@ export abstract class ChallengeRegistry {
|
|||||||
groupId === "elusive" &&
|
groupId === "elusive" &&
|
||||||
location !== "GLOBAL_ELUSIVES_CHALLENGES"
|
location !== "GLOBAL_ELUSIVES_CHALLENGES"
|
||||||
) {
|
) {
|
||||||
|
if (!mainGroup) {
|
||||||
|
// emergency bailout - shouldn't happen in practice
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
return mergeSavedChallengeGroups(
|
return mergeSavedChallengeGroups(
|
||||||
gameGroups.get(location)?.get(groupId),
|
mainGroup,
|
||||||
gameGroups.get("GLOBAL_ELUSIVES_CHALLENGES")?.get(groupId),
|
gameGroups.get("GLOBAL_ELUSIVES_CHALLENGES")?.get(groupId),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return gameGroups.get(location)?.get(groupId)
|
return mainGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
public getGroupContentByIdLoc(
|
public getGroupContentByIdLoc(
|
||||||
@ -279,11 +291,7 @@ export abstract class ChallengeRegistry {
|
|||||||
location: string,
|
location: string,
|
||||||
gameVersion: GameVersion,
|
gameVersion: GameVersion,
|
||||||
): Set<string> | undefined {
|
): Set<string> | undefined {
|
||||||
if (!this.groupContents.has(gameVersion)) {
|
const gameChalGC = this.groupContents[gameVersion]
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
const gameChalGC = this.groupContents.get(gameVersion)
|
|
||||||
|
|
||||||
if (groupId === "feats" && gameVersion !== "h3") {
|
if (groupId === "feats" && gameVersion !== "h3") {
|
||||||
return new Set([
|
return new Set([
|
||||||
@ -334,9 +342,16 @@ export abstract class ChallengeRegistry {
|
|||||||
challengeId: string,
|
challengeId: string,
|
||||||
gameVersion: GameVersion,
|
gameVersion: GameVersion,
|
||||||
): readonly string[] {
|
): readonly string[] {
|
||||||
return this._dependencyTree.get(gameVersion)?.get(challengeId) || []
|
return this._dependencyTree[gameVersion].get(challengeId) || []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method checks the heuristics of a challenge.
|
||||||
|
* It parses the context listeners of the challenge and if the challenge has any dependencies (other challenges that need to be completed before this one), it adds them to the dependency tree.
|
||||||
|
*
|
||||||
|
* @param challenge The challenge to check.
|
||||||
|
* @param gameVersion The game version this challenge belongs to.
|
||||||
|
*/
|
||||||
protected checkHeuristics(
|
protected checkHeuristics(
|
||||||
challenge: RegistryChallenge,
|
challenge: RegistryChallenge,
|
||||||
gameVersion: GameVersion,
|
gameVersion: GameVersion,
|
||||||
@ -344,9 +359,10 @@ export abstract class ChallengeRegistry {
|
|||||||
const ctxListeners = ChallengeRegistry._parseContextListeners(challenge)
|
const ctxListeners = ChallengeRegistry._parseContextListeners(challenge)
|
||||||
|
|
||||||
if (ctxListeners.challengeTreeIds.length > 0) {
|
if (ctxListeners.challengeTreeIds.length > 0) {
|
||||||
this._dependencyTree
|
this._dependencyTree[gameVersion].set(
|
||||||
.get(gameVersion)
|
challenge.Id,
|
||||||
?.set(challenge.Id, ctxListeners.challengeTreeIds)
|
ctxListeners.challengeTreeIds,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -392,10 +408,11 @@ export class ChallengeService extends ChallengeRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the challenge needs to be saved in the user's progression data
|
* Check if the challenge needs to be saved in the user's progression data.
|
||||||
* i.e. challenges with scopes being "profile" or "hit".
|
* Challenges with scopes "profile" or "hit".
|
||||||
|
*
|
||||||
* @param challenge The challenge.
|
* @param challenge The challenge.
|
||||||
* @returns Whether the challenge needs to be saved in the user's progression data.
|
* @returns Whether the challenge needs to be saved in the user's progression data.
|
||||||
*/
|
*/
|
||||||
needSaveProgression(challenge: RegistryChallenge): boolean {
|
needSaveProgression(challenge: RegistryChallenge): boolean {
|
||||||
return (
|
return (
|
||||||
@ -512,7 +529,7 @@ export class ChallengeService extends ChallengeRegistry {
|
|||||||
challenges: [string, RegistryChallenge[]][],
|
challenges: [string, RegistryChallenge[]][],
|
||||||
gameVersion: GameVersion,
|
gameVersion: GameVersion,
|
||||||
) {
|
) {
|
||||||
const groups = this.groups.get(gameVersion).get(location)?.keys() ?? []
|
const groups = this.groups[gameVersion].get(location)?.keys() ?? []
|
||||||
|
|
||||||
for (const groupId of groups) {
|
for (const groupId of groups) {
|
||||||
// if this is the global group, skip it.
|
// if this is the global group, skip it.
|
||||||
@ -543,9 +560,9 @@ export class ChallengeService extends ChallengeRegistry {
|
|||||||
return challenge
|
return challenge
|
||||||
}
|
}
|
||||||
|
|
||||||
return filterChallenge(filter, challenge)
|
const res = filterChallenge(filter, challenge)
|
||||||
? challenge
|
|
||||||
: undefined
|
return res ? challenge : undefined
|
||||||
})
|
})
|
||||||
.filter(Boolean) as RegistryChallenge[]
|
.filter(Boolean) as RegistryChallenge[]
|
||||||
|
|
||||||
@ -570,10 +587,6 @@ export class ChallengeService extends ChallengeRegistry {
|
|||||||
): GroupIndexedChallengeLists {
|
): GroupIndexedChallengeLists {
|
||||||
let challenges: [string, RegistryChallenge[]][] = []
|
let challenges: [string, RegistryChallenge[]][] = []
|
||||||
|
|
||||||
if (!this.groups.has(gameVersion)) {
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.getGroupedChallengesByLoc(
|
this.getGroupedChallengesByLoc(
|
||||||
filter,
|
filter,
|
||||||
location,
|
location,
|
||||||
@ -622,25 +635,36 @@ export class ChallengeService extends ChallengeRegistry {
|
|||||||
difficulty = 4,
|
difficulty = 4,
|
||||||
): GroupIndexedChallengeLists {
|
): GroupIndexedChallengeLists {
|
||||||
const userData = getUserData(userId, gameVersion)
|
const userData = getUserData(userId, gameVersion)
|
||||||
const contract = this.controller.resolveContract(contractId, true)
|
const contractGroup = this.controller.resolveContract(contractId, true)
|
||||||
|
|
||||||
const level =
|
if (!contractGroup) {
|
||||||
contract.Metadata.Type === "arcade" &&
|
return {}
|
||||||
contract.Metadata.Id === contractId
|
}
|
||||||
? // contractData, being a group contract, has the same Id as the input id parameter.
|
|
||||||
// This means that we are requesting the challenges for the next level of the group
|
|
||||||
this.controller.resolveContract(
|
|
||||||
contract.Metadata.GroupDefinition.Order[
|
|
||||||
getUserEscalationProgress(userData, contractId) - 1
|
|
||||||
],
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
: this.controller.resolveContract(contractId, false)
|
|
||||||
|
|
||||||
assert.ok(contract)
|
let contract: MissionManifest | undefined
|
||||||
|
|
||||||
|
if (
|
||||||
|
contractGroup.Metadata.Type === "arcade" &&
|
||||||
|
contractGroup.Metadata.Id === contractId
|
||||||
|
) {
|
||||||
|
const currentLevel =
|
||||||
|
contractGroup.Metadata.GroupDefinition?.Order[
|
||||||
|
getUserEscalationProgress(userData, contractId) - 1
|
||||||
|
]
|
||||||
|
|
||||||
|
assert.ok(currentLevel, "expected current level ID in escalation")
|
||||||
|
|
||||||
|
contract = this.controller.resolveContract(currentLevel, false)
|
||||||
|
} else {
|
||||||
|
contract = this.controller.resolveContract(contractId, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!contract) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
const levelParentLocation = getSubLocationFromContract(
|
const levelParentLocation = getSubLocationFromContract(
|
||||||
level,
|
contract,
|
||||||
gameVersion,
|
gameVersion,
|
||||||
)?.Properties.ParentLocation
|
)?.Properties.ParentLocation
|
||||||
|
|
||||||
@ -651,12 +675,12 @@ export class ChallengeService extends ChallengeRegistry {
|
|||||||
type: ChallengeFilterType.Contract,
|
type: ChallengeFilterType.Contract,
|
||||||
contractId: contractId,
|
contractId: contractId,
|
||||||
locationId:
|
locationId:
|
||||||
contract.Metadata.Id ===
|
contractGroup.Metadata.Id ===
|
||||||
"aee6a16f-6525-4d63-a37f-225e293c6118" &&
|
"aee6a16f-6525-4d63-a37f-225e293c6118" &&
|
||||||
gameVersion !== "h1"
|
gameVersion !== "h1"
|
||||||
? "LOCATION_ICA_FACILITY_SHIP"
|
? "LOCATION_ICA_FACILITY_SHIP"
|
||||||
: level.Metadata.Location,
|
: contract.Metadata.Location,
|
||||||
isFeatured: contract.Metadata.Type === "featured",
|
isFeatured: contractGroup.Metadata.Type === "featured",
|
||||||
difficulty,
|
difficulty,
|
||||||
},
|
},
|
||||||
levelParentLocation,
|
levelParentLocation,
|
||||||
@ -676,17 +700,23 @@ export class ChallengeService extends ChallengeRegistry {
|
|||||||
const parent = locations.children[child].Properties.ParentLocation
|
const parent = locations.children[child].Properties.ParentLocation
|
||||||
|
|
||||||
let contracts = isSniperLocation(child)
|
let contracts = isSniperLocation(child)
|
||||||
? this.controller.missionsInLocations.sniper[child]
|
? // @ts-expect-error This is fine - we know it will be there
|
||||||
: (this.controller.missionsInLocations[child] ?? [])
|
this.controller.missionsInLocations.sniper[child]
|
||||||
|
: // @ts-expect-error This is fine - we know it will be there
|
||||||
|
(this.controller.missionsInLocations[child] ?? [])
|
||||||
.concat(
|
.concat(
|
||||||
|
// @ts-expect-error This is fine - we know it will be there
|
||||||
this.controller.missionsInLocations.escalations[child],
|
this.controller.missionsInLocations.escalations[child],
|
||||||
)
|
)
|
||||||
|
// @ts-expect-error This is fine - we know it will be there
|
||||||
.concat(this.controller.missionsInLocations.arcade[child])
|
.concat(this.controller.missionsInLocations.arcade[child])
|
||||||
|
|
||||||
if (!contracts) {
|
if (!contracts) {
|
||||||
contracts = []
|
contracts = []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assert.ok(parent, "expected parent location")
|
||||||
|
|
||||||
return this.getGroupedChallengeLists(
|
return this.getGroupedChallengeLists(
|
||||||
{
|
{
|
||||||
type: ChallengeFilterType.Contracts,
|
type: ChallengeFilterType.Contracts,
|
||||||
@ -712,26 +742,31 @@ export class ChallengeService extends ChallengeRegistry {
|
|||||||
session.difficulty,
|
session.difficulty,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (contractJson.Metadata.Type === "evergreen") {
|
if (contractJson?.Metadata.Type === "evergreen") {
|
||||||
session.evergreen = {
|
session.evergreen = {
|
||||||
payout: 0,
|
payout: 0,
|
||||||
scoringScreenEndState: undefined,
|
scoringScreenEndState: null,
|
||||||
failed: false,
|
failed: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Add this to getChallengesForContract without breaking the rest of Peacock?
|
// TODO: Add this to getChallengesForContract without breaking the rest of Peacock?
|
||||||
challengeGroups["global"] = this.getGroupByIdLoc(
|
challengeGroups["global"] =
|
||||||
"global",
|
this.getGroupByIdLoc(
|
||||||
"GLOBAL",
|
"global",
|
||||||
session.gameVersion,
|
"GLOBAL",
|
||||||
).Challenges.filter((val) =>
|
session.gameVersion,
|
||||||
inclusionDataCheck(val.InclusionData, contractJson),
|
)?.Challenges.filter((val) =>
|
||||||
)
|
inclusionDataCheck(val.InclusionData, contractJson),
|
||||||
|
) || []
|
||||||
|
|
||||||
const profile = getUserData(session.userId, session.gameVersion)
|
const profile = getUserData(session.userId, session.gameVersion)
|
||||||
|
|
||||||
for (const group of Object.keys(challengeGroups)) {
|
for (const group of Object.keys(challengeGroups)) {
|
||||||
|
if (!challengeContexts) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
for (const challenge of challengeGroups[group]) {
|
for (const challenge of challengeGroups[group]) {
|
||||||
challengeContexts[challenge.Id] = {
|
challengeContexts[challenge.Id] = {
|
||||||
context: undefined,
|
context: undefined,
|
||||||
@ -892,7 +927,7 @@ export class ChallengeService extends ChallengeRegistry {
|
|||||||
contractId: string,
|
contractId: string,
|
||||||
gameVersion: GameVersion,
|
gameVersion: GameVersion,
|
||||||
userId: string,
|
userId: string,
|
||||||
difficulty = 4,
|
difficulty = gameDifficulty.master,
|
||||||
): CompiledChallengeTreeCategory[] {
|
): CompiledChallengeTreeCategory[] {
|
||||||
const userData = getUserData(userId, gameVersion)
|
const userData = getUserData(userId, gameVersion)
|
||||||
|
|
||||||
@ -902,18 +937,37 @@ export class ChallengeService extends ChallengeRegistry {
|
|||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
const levelData =
|
let levelData: MissionManifest | undefined
|
||||||
|
|
||||||
|
if (
|
||||||
contractData.Metadata.Type === "arcade" &&
|
contractData.Metadata.Type === "arcade" &&
|
||||||
contractData.Metadata.Id === contractId
|
contractData.Metadata.Id === contractId
|
||||||
? // contractData, being a group contract, has the same Id as the input id parameter.
|
) {
|
||||||
// This means that we are requesting the challenges for the next level of the group
|
const order =
|
||||||
this.controller.resolveContract(
|
contractData.Metadata.GroupDefinition?.Order[
|
||||||
contractData.Metadata.GroupDefinition.Order[
|
getUserEscalationProgress(userData, contractId) - 1
|
||||||
getUserEscalationProgress(userData, contractId) - 1
|
]
|
||||||
],
|
|
||||||
false,
|
if (!order) {
|
||||||
)
|
log(
|
||||||
: this.controller.resolveContract(contractId, false)
|
LogLevel.WARN,
|
||||||
|
`Failed to get escalation order in CTREE [${contractData.Metadata.GroupDefinition?.Order}]`,
|
||||||
|
)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
levelData = this.controller.resolveContract(order, false)
|
||||||
|
} else {
|
||||||
|
levelData = this.controller.resolveContract(contractId, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!levelData) {
|
||||||
|
log(
|
||||||
|
LogLevel.WARN,
|
||||||
|
`Failed to get level data in CTREE [${contractId}]`,
|
||||||
|
)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
const subLocation = getSubLocationFromContract(levelData, gameVersion)
|
const subLocation = getSubLocationFromContract(levelData, gameVersion)
|
||||||
|
|
||||||
@ -1116,80 +1170,89 @@ export class ChallengeService extends ChallengeRegistry {
|
|||||||
gameVersion,
|
gameVersion,
|
||||||
)
|
)
|
||||||
|
|
||||||
return entries.map(([groupId, challenges], index) => {
|
return entries
|
||||||
const groupData = this.getGroupByIdLoc(
|
.map(([groupId, challenges], index) => {
|
||||||
groupId,
|
const groupData = this.getGroupByIdLoc(
|
||||||
location.Properties.ParentLocation ?? location.Id,
|
groupId,
|
||||||
gameVersion,
|
location.Properties.ParentLocation ?? location.Id,
|
||||||
)
|
|
||||||
const challengeProgressionData = challenges.map((challengeData) =>
|
|
||||||
this.getPersistentChallengeProgression(
|
|
||||||
userId,
|
|
||||||
challengeData.Id,
|
|
||||||
gameVersion,
|
gameVersion,
|
||||||
),
|
)
|
||||||
)
|
|
||||||
|
|
||||||
const lastGroup = this.getGroupByIdLoc(
|
if (!groupData) {
|
||||||
Object.keys(challengeLists)[index - 1],
|
return undefined
|
||||||
location.Properties.ParentLocation ?? location.Id,
|
}
|
||||||
gameVersion,
|
|
||||||
)
|
|
||||||
const nextGroup = this.getGroupByIdLoc(
|
|
||||||
Object.keys(challengeLists)[index + 1],
|
|
||||||
location.Properties.ParentLocation ?? location.Id,
|
|
||||||
gameVersion,
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
const challengeProgressionData = challenges.map(
|
||||||
Name: groupData?.Name,
|
(challengeData) =>
|
||||||
Description: groupData?.Description,
|
this.getPersistentChallengeProgression(
|
||||||
Image: groupData?.Image,
|
|
||||||
CategoryId: groupData?.CategoryId,
|
|
||||||
Icon: groupData?.Icon,
|
|
||||||
ChallengesCount: challenges.length,
|
|
||||||
CompletedChallengesCount: challengeProgressionData.filter(
|
|
||||||
(progressionData) => progressionData.Completed,
|
|
||||||
).length,
|
|
||||||
CompletionData: completion,
|
|
||||||
Location: location,
|
|
||||||
IsLocked: location.Properties.IsLocked || false,
|
|
||||||
ImageLocked: location.Properties.LockedIcon || "",
|
|
||||||
RequiredResources: location.Properties.RequiredResources!,
|
|
||||||
SwitchData: {
|
|
||||||
Data: {
|
|
||||||
Challenges: this.mapSwitchChallenges(
|
|
||||||
challenges,
|
|
||||||
userId,
|
userId,
|
||||||
|
challengeData.Id,
|
||||||
gameVersion,
|
gameVersion,
|
||||||
compiler,
|
|
||||||
),
|
),
|
||||||
HasPrevious: index !== 0, // whether we are not at the first group
|
)
|
||||||
HasNext:
|
|
||||||
index !== Object.keys(challengeLists).length - 1, // whether we are not at the final group
|
const lastGroup = this.getGroupByIdLoc(
|
||||||
PreviousCategoryIcon:
|
Object.keys(challengeLists)[index - 1],
|
||||||
index !== 0 ? lastGroup?.Icon : "",
|
location.Properties.ParentLocation ?? location.Id,
|
||||||
NextCategoryIcon:
|
gameVersion,
|
||||||
index !== Object.keys(challengeLists).length - 1
|
)
|
||||||
? nextGroup?.Icon
|
const nextGroup = this.getGroupByIdLoc(
|
||||||
: "",
|
Object.keys(challengeLists)[index + 1],
|
||||||
CategoryData: {
|
location.Properties.ParentLocation ?? location.Id,
|
||||||
Name: groupData.Name,
|
gameVersion,
|
||||||
Image: groupData.Image,
|
)
|
||||||
Icon: groupData.Icon,
|
|
||||||
ChallengesCount: challenges.length,
|
return {
|
||||||
CompletedChallengesCount:
|
Name: groupData.Name,
|
||||||
challengeProgressionData.filter(
|
Description: groupData.Description,
|
||||||
(progressionData) =>
|
Image: groupData.Image,
|
||||||
progressionData.Completed,
|
CategoryId: groupData.CategoryId,
|
||||||
).length,
|
Icon: groupData.Icon,
|
||||||
|
ChallengesCount: challenges.length,
|
||||||
|
CompletedChallengesCount: challengeProgressionData.filter(
|
||||||
|
(progressionData) => progressionData.Completed,
|
||||||
|
).length,
|
||||||
|
CompletionData: completion,
|
||||||
|
Location: location,
|
||||||
|
IsLocked: location.Properties.IsLocked || false,
|
||||||
|
ImageLocked: location.Properties.LockedIcon || "",
|
||||||
|
RequiredResources: location.Properties.RequiredResources!,
|
||||||
|
SwitchData: {
|
||||||
|
Data: {
|
||||||
|
Challenges: this.mapSwitchChallenges(
|
||||||
|
challenges,
|
||||||
|
userId,
|
||||||
|
gameVersion,
|
||||||
|
compiler,
|
||||||
|
),
|
||||||
|
HasPrevious: index !== 0, // whether we are not at the first group
|
||||||
|
HasNext:
|
||||||
|
index !==
|
||||||
|
Object.keys(challengeLists).length - 1, // whether we are not at the final group
|
||||||
|
PreviousCategoryIcon:
|
||||||
|
index !== 0 ? lastGroup?.Icon : "",
|
||||||
|
NextCategoryIcon:
|
||||||
|
index !== Object.keys(challengeLists).length - 1
|
||||||
|
? nextGroup?.Icon
|
||||||
|
: "",
|
||||||
|
CategoryData: {
|
||||||
|
Name: groupData.Name,
|
||||||
|
Image: groupData.Image,
|
||||||
|
Icon: groupData.Icon,
|
||||||
|
ChallengesCount: challenges.length,
|
||||||
|
CompletedChallengesCount:
|
||||||
|
challengeProgressionData.filter(
|
||||||
|
(progressionData) =>
|
||||||
|
progressionData.Completed,
|
||||||
|
).length,
|
||||||
|
},
|
||||||
|
CompletionData: completion,
|
||||||
},
|
},
|
||||||
CompletionData: completion,
|
IsLeaf: true,
|
||||||
},
|
},
|
||||||
IsLeaf: true,
|
}
|
||||||
},
|
})
|
||||||
}
|
.filter(Boolean) as CompiledChallengeTreeCategory[]
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
compileRegistryChallengeTreeData(
|
compileRegistryChallengeTreeData(
|
||||||
@ -1201,7 +1264,7 @@ export class ChallengeService extends ChallengeRegistry {
|
|||||||
): CompiledChallengeTreeData {
|
): CompiledChallengeTreeData {
|
||||||
const drops = challenge.Drops.map((e) =>
|
const drops = challenge.Drops.map((e) =>
|
||||||
getUnlockableById(e, gameVersion),
|
getUnlockableById(e, gameVersion),
|
||||||
).filter(Boolean)
|
).filter(Boolean) as Unlockable[]
|
||||||
|
|
||||||
if (drops.length !== challenge.Drops.length) {
|
if (drops.length !== challenge.Drops.length) {
|
||||||
log(
|
log(
|
||||||
@ -1255,7 +1318,7 @@ export class ChallengeService extends ChallengeRegistry {
|
|||||||
gameVersion: GameVersion,
|
gameVersion: GameVersion,
|
||||||
userId: string,
|
userId: string,
|
||||||
): CompiledChallengeTreeData {
|
): CompiledChallengeTreeData {
|
||||||
let contract: MissionManifest | null
|
let contract: MissionManifest | undefined
|
||||||
|
|
||||||
if (challenge.Type === "contract") {
|
if (challenge.Type === "contract") {
|
||||||
contract = this.controller.resolveContract(
|
contract = this.controller.resolveContract(
|
||||||
@ -1264,39 +1327,40 @@ export class ChallengeService extends ChallengeRegistry {
|
|||||||
|
|
||||||
// This is so we can remove unused data and make it more like official - AF
|
// This is so we can remove unused data and make it more like official - AF
|
||||||
const meta = contract?.Metadata
|
const meta = contract?.Metadata
|
||||||
contract = !contract
|
contract =
|
||||||
? null
|
!contract || !meta
|
||||||
: {
|
? undefined
|
||||||
// The null is for escalations as we cannot currently get groups
|
: {
|
||||||
Data: {
|
// The null is for escalations as we cannot currently get groups
|
||||||
Bricks: contract.Data.Bricks,
|
Data: {
|
||||||
DevOnlyBricks: null,
|
Bricks: contract.Data.Bricks,
|
||||||
GameChangerReferences:
|
DevOnlyBricks: null,
|
||||||
contract.Data.GameChangerReferences || [],
|
GameChangerReferences:
|
||||||
GameChangers: contract.Data.GameChangers || [],
|
contract.Data.GameChangerReferences || [],
|
||||||
GameDifficulties:
|
GameChangers: contract.Data.GameChangers || [],
|
||||||
contract.Data.GameDifficulties || [],
|
GameDifficulties:
|
||||||
},
|
contract.Data.GameDifficulties || [],
|
||||||
Metadata: {
|
},
|
||||||
CreationTimestamp: null,
|
Metadata: {
|
||||||
CreatorUserId: meta.CreatorUserId,
|
CreationTimestamp: null,
|
||||||
DebriefingVideo: meta.DebriefingVideo || "",
|
CreatorUserId: meta.CreatorUserId,
|
||||||
Description: meta.Description,
|
DebriefingVideo: meta.DebriefingVideo || "",
|
||||||
Drops: meta.Drops || null,
|
Description: meta.Description,
|
||||||
Entitlements: meta.Entitlements || [],
|
Drops: meta.Drops || null,
|
||||||
GroupTitle: meta.GroupTitle || "",
|
Entitlements: meta.Entitlements || [],
|
||||||
Id: meta.Id,
|
GroupTitle: meta.GroupTitle || "",
|
||||||
IsPublished: meta.IsPublished || true,
|
Id: meta.Id,
|
||||||
LastUpdate: null,
|
IsPublished: meta.IsPublished || true,
|
||||||
Location: meta.Location,
|
LastUpdate: null,
|
||||||
PublicId: meta.PublicId || "",
|
Location: meta.Location,
|
||||||
ScenePath: meta.ScenePath,
|
PublicId: meta.PublicId || "",
|
||||||
Subtype: meta.Subtype || "",
|
ScenePath: meta.ScenePath,
|
||||||
TileImage: meta.TileImage,
|
Subtype: meta.Subtype || "",
|
||||||
Title: meta.Title,
|
TileImage: meta.TileImage,
|
||||||
Type: meta.Type,
|
Title: meta.Title,
|
||||||
},
|
Type: meta.Type,
|
||||||
}
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -1375,12 +1439,11 @@ export class ChallengeService extends ChallengeRegistry {
|
|||||||
|
|
||||||
if (challengeId === parentId) {
|
if (challengeId === parentId) {
|
||||||
// we're checking the tree of the challenge that was just completed,
|
// we're checking the tree of the challenge that was just completed,
|
||||||
// so we need to skip it, or we'll get an infinite loop and hit
|
// so we need to skip it, or we'll get an infinite loop
|
||||||
// the max call stack size
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const allDeps = this._dependencyTree.get(gameVersion)?.get(challengeId)
|
const allDeps = this._dependencyTree[gameVersion].get(challengeId)
|
||||||
assert.ok(allDeps, `No dep tree for ${challengeId}`)
|
assert.ok(allDeps, `No dep tree for ${challengeId}`)
|
||||||
|
|
||||||
if (!allDeps.includes(parentId)) {
|
if (!allDeps.includes(parentId)) {
|
||||||
@ -1393,10 +1456,17 @@ export class ChallengeService extends ChallengeRegistry {
|
|||||||
|
|
||||||
// Check if the dependency tree is completed now
|
// Check if the dependency tree is completed now
|
||||||
|
|
||||||
const dep = this.getChallengeById(challengeId, gameVersion)
|
const challengeDependency = this.getChallengeById(
|
||||||
|
challengeId,
|
||||||
|
gameVersion,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!challengeDependency) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const { challengeCountData } =
|
const { challengeCountData } =
|
||||||
ChallengeService._parseContextListeners(dep)
|
ChallengeService._parseContextListeners(challengeDependency)
|
||||||
|
|
||||||
// First check for challengecounter, then challengetree
|
// First check for challengecounter, then challengetree
|
||||||
const completed =
|
const completed =
|
||||||
@ -1408,11 +1478,15 @@ export class ChallengeService extends ChallengeRegistry {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const challenge = this.getChallengeById(challengeId, gameVersion)
|
||||||
|
|
||||||
|
assert.ok(challenge, `No challenge for ${challengeId}`)
|
||||||
|
|
||||||
this.onChallengeCompleted(
|
this.onChallengeCompleted(
|
||||||
session,
|
session,
|
||||||
userData.Id,
|
userData.Id,
|
||||||
gameVersion,
|
gameVersion,
|
||||||
this.getChallengeById(challengeId, gameVersion),
|
challenge,
|
||||||
parentId,
|
parentId,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -1464,18 +1538,20 @@ export class ChallengeService extends ChallengeRegistry {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always count the number of completions
|
if (session.challengeContexts) {
|
||||||
if (session.challengeContexts[challenge.Id]) {
|
// Always count the number of completions
|
||||||
session.challengeContexts[challenge.Id].timesCompleted++
|
if (session.challengeContexts[challenge.Id]) {
|
||||||
}
|
session.challengeContexts[challenge.Id].timesCompleted++
|
||||||
|
}
|
||||||
|
|
||||||
// If we have a Definition-scope with a Repeatable, we may want to restart it.
|
// If we have a Definition-scope with a Repeatable, we may want to restart it.
|
||||||
// TODO: Figure out what Base/Delta means. For now if Repeatable is set, we restart the challenge.
|
// TODO: Figure out what Base/Delta means. For now if Repeatable is set, we restart the challenge.
|
||||||
if (
|
if (
|
||||||
challenge.Definition.Repeatable &&
|
challenge.Definition.Repeatable &&
|
||||||
session.challengeContexts[challenge.Id]
|
session.challengeContexts[challenge.Id]
|
||||||
) {
|
) {
|
||||||
session.challengeContexts[challenge.Id].state = "Start"
|
session.challengeContexts[challenge.Id].state = "Start"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
controller.progressionService.grantProfileProgression(
|
controller.progressionService.grantProfileProgression(
|
||||||
@ -1490,7 +1566,7 @@ export class ChallengeService extends ChallengeRegistry {
|
|||||||
this.hooks.onChallengeCompleted.call(userId, challenge, gameVersion)
|
this.hooks.onChallengeCompleted.call(userId, challenge, gameVersion)
|
||||||
|
|
||||||
// Check if completing this challenge also completes any dependency trees depending on it
|
// Check if completing this challenge also completes any dependency trees depending on it
|
||||||
for (const depTreeId of this._dependencyTree.get(gameVersion).keys()) {
|
for (const depTreeId of this._dependencyTree[gameVersion].keys()) {
|
||||||
this.tryToCompleteChallenge(
|
this.tryToCompleteChallenge(
|
||||||
session,
|
session,
|
||||||
depTreeId,
|
depTreeId,
|
||||||
|
@ -21,17 +21,22 @@ import {
|
|||||||
getSubLocationByName,
|
getSubLocationByName,
|
||||||
} from "../contracts/dataGen"
|
} from "../contracts/dataGen"
|
||||||
import { log, LogLevel } from "../loggingInterop"
|
import { log, LogLevel } from "../loggingInterop"
|
||||||
import { getConfig, getVersionedConfig } from "../configSwizzleManager"
|
import { getVersionedConfig } from "../configSwizzleManager"
|
||||||
import { getUserData } from "../databaseHandler"
|
import { getUserData } from "../databaseHandler"
|
||||||
import {
|
import {
|
||||||
|
LocationMasteryData,
|
||||||
MasteryData,
|
MasteryData,
|
||||||
MasteryDataTemplate,
|
|
||||||
MasteryDrop,
|
MasteryDrop,
|
||||||
MasteryPackage,
|
MasteryPackage,
|
||||||
MasteryPackageDrop,
|
MasteryPackageDrop,
|
||||||
UnlockableMasteryData,
|
UnlockableMasteryData,
|
||||||
} from "../types/mastery"
|
} from "../types/mastery"
|
||||||
import { CompletionData, GameVersion, Unlockable } from "../types/types"
|
import {
|
||||||
|
CompletionData,
|
||||||
|
GameVersion,
|
||||||
|
ProgressionData,
|
||||||
|
Unlockable,
|
||||||
|
} from "../types/types"
|
||||||
import {
|
import {
|
||||||
clampValue,
|
clampValue,
|
||||||
DEFAULT_MASTERY_MAXLEVEL,
|
DEFAULT_MASTERY_MAXLEVEL,
|
||||||
@ -42,6 +47,7 @@ import {
|
|||||||
} from "../utils"
|
} from "../utils"
|
||||||
|
|
||||||
import { getUnlockablesById } from "../inventory"
|
import { getUnlockablesById } from "../inventory"
|
||||||
|
import assert from "assert"
|
||||||
|
|
||||||
export class MasteryService {
|
export class MasteryService {
|
||||||
/**
|
/**
|
||||||
@ -150,22 +156,16 @@ export class MasteryService {
|
|||||||
)[0]
|
)[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(
|
getMasteryDataForLocation(
|
||||||
locationId: string,
|
locationId: string,
|
||||||
gameVersion: GameVersion,
|
gameVersion: GameVersion,
|
||||||
userId: string,
|
userId: string,
|
||||||
): MasteryDataTemplate {
|
): LocationMasteryData {
|
||||||
const location: Unlockable =
|
const location =
|
||||||
getSubLocationByName(locationId, gameVersion) ??
|
getSubLocationByName(locationId, gameVersion) ??
|
||||||
getParentLocationByName(locationId, gameVersion)
|
getParentLocationByName(locationId, gameVersion)
|
||||||
|
|
||||||
const masteryDataTemplate: MasteryDataTemplate =
|
assert.ok(location, "cannot get mastery data for unknown location")
|
||||||
getConfig<MasteryDataTemplate>(
|
|
||||||
"MasteryDataForLocationTemplate",
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
|
|
||||||
const masteryData = this.getMasteryData(
|
const masteryData = this.getMasteryData(
|
||||||
location.Properties.ParentLocation ?? location.Id,
|
location.Properties.ParentLocation ?? location.Id,
|
||||||
@ -174,11 +174,8 @@ export class MasteryService {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
template: masteryDataTemplate,
|
Location: location,
|
||||||
data: {
|
MasteryData: masteryData,
|
||||||
Location: location,
|
|
||||||
MasteryData: masteryData,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -202,19 +199,12 @@ export class MasteryService {
|
|||||||
// Get the user profile
|
// Get the user profile
|
||||||
const userProfile = getUserData(userId, gameVersion)
|
const userProfile = getUserData(userId, gameVersion)
|
||||||
|
|
||||||
// @since v7.0.0 this has been commented out as the default profile should
|
const parent =
|
||||||
// have all the required properties - AF
|
userProfile.Extensions.progression.Locations[locationParentId]
|
||||||
/* userProfile.Extensions.progression.Locations[locationParentId] ??= {
|
|
||||||
Xp: 0,
|
|
||||||
Level: 1,
|
|
||||||
PreviouslySeenXp: 0,
|
|
||||||
} */
|
|
||||||
|
|
||||||
const completionData = subPackageId
|
const completionData: ProgressionData = subPackageId
|
||||||
? userProfile.Extensions.progression.Locations[locationParentId][
|
? (parent[subPackageId as keyof typeof parent] as ProgressionData)
|
||||||
subPackageId
|
: (parent as ProgressionData)
|
||||||
]
|
|
||||||
: userProfile.Extensions.progression.Locations[locationParentId]
|
|
||||||
|
|
||||||
const nextLevel: number = clampValue(
|
const nextLevel: number = clampValue(
|
||||||
completionData.Level + 1,
|
completionData.Level + 1,
|
||||||
@ -279,7 +269,7 @@ export class MasteryService {
|
|||||||
"SniperUnlockables",
|
"SniperUnlockables",
|
||||||
gameVersion,
|
gameVersion,
|
||||||
false,
|
false,
|
||||||
).find((unlockable) => unlockable.Id === subPackageId).Properties
|
).find((unlockable) => unlockable.Id === subPackageId)?.Properties
|
||||||
.Name
|
.Name
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
@ -297,11 +287,11 @@ export class MasteryService {
|
|||||||
: xpRequiredForLevel,
|
: xpRequiredForLevel,
|
||||||
subPackageId,
|
subPackageId,
|
||||||
),
|
),
|
||||||
Id: isSniper ? subPackageId : masteryPkg.LocationId,
|
Id: isSniper ? subPackageId! : masteryPkg.LocationId,
|
||||||
SubLocationId: isSniper ? "" : subLocationId,
|
SubLocationId: isSniper ? "" : subLocationId,
|
||||||
HideProgression: masteryPkg.HideProgression || false,
|
HideProgression: masteryPkg.HideProgression || false,
|
||||||
IsLocationProgression: !isSniper,
|
IsLocationProgression: !isSniper,
|
||||||
Name: name,
|
Name: name!,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -347,7 +337,7 @@ export class MasteryService {
|
|||||||
subPackageId?: string,
|
subPackageId?: string,
|
||||||
): MasteryData[] {
|
): MasteryData[] {
|
||||||
// Get the mastery data
|
// Get the mastery data
|
||||||
const masteryPkg: MasteryPackage = this.getMasteryPackage(
|
const masteryPkg: MasteryPackage | undefined = this.getMasteryPackage(
|
||||||
locationParentId,
|
locationParentId,
|
||||||
gameVersion,
|
gameVersion,
|
||||||
)
|
)
|
||||||
@ -389,16 +379,23 @@ export class MasteryService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get all unlockables with matching Ids
|
// Get all unlockables with matching Ids
|
||||||
const unlockableData: Unlockable[] = getUnlockablesById(
|
const unlockableData = getUnlockablesById(
|
||||||
Array.from(dropIdSet),
|
Array.from(dropIdSet),
|
||||||
gameVersion,
|
gameVersion,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Put all unlockabkes in a map for quick lookup
|
// Put all unlockabkes in a map for quick lookup
|
||||||
const unlockableMap = new Map(
|
const mapped: [string, Unlockable][] = unlockableData.map(
|
||||||
unlockableData.map((unlockable) => [unlockable.Id, unlockable]),
|
(unlockable) => {
|
||||||
|
return [unlockable?.Id, unlockable] as unknown as [
|
||||||
|
string,
|
||||||
|
Unlockable,
|
||||||
|
]
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const unlockableMap: Map<string, Unlockable> = new Map(mapped)
|
||||||
|
|
||||||
const masteryData: MasteryData[] = []
|
const masteryData: MasteryData[] = []
|
||||||
|
|
||||||
if (masteryPkg.SubPackages) {
|
if (masteryPkg.SubPackages) {
|
||||||
@ -418,17 +415,19 @@ export class MasteryService {
|
|||||||
subPkg.Id,
|
subPkg.Id,
|
||||||
)
|
)
|
||||||
|
|
||||||
masteryData.push({
|
if (completionData) {
|
||||||
CompletionData: completionData,
|
masteryData.push({
|
||||||
Drops: this.processDrops(
|
CompletionData: completionData,
|
||||||
completionData.Level,
|
Drops: this.processDrops(
|
||||||
subPkg.Drops,
|
completionData.Level,
|
||||||
unlockableMap,
|
subPkg.Drops,
|
||||||
),
|
unlockableMap,
|
||||||
Unlockable: isSniper
|
),
|
||||||
? unlockableMap.get(subPkg.Id)
|
Unlockable: isSniper
|
||||||
: undefined,
|
? unlockableMap.get(subPkg.Id)
|
||||||
})
|
: undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// All sniper locations are subpackages, so we don't need to add "sniper"
|
// All sniper locations are subpackages, so we don't need to add "sniper"
|
||||||
@ -441,14 +440,16 @@ export class MasteryService {
|
|||||||
locationParentId.includes("SNUG") ? "evergreen" : "mission",
|
locationParentId.includes("SNUG") ? "evergreen" : "mission",
|
||||||
)
|
)
|
||||||
|
|
||||||
masteryData.push({
|
if (completionData) {
|
||||||
CompletionData: completionData,
|
masteryData.push({
|
||||||
Drops: this.processDrops(
|
CompletionData: completionData,
|
||||||
completionData.Level,
|
Drops: this.processDrops(
|
||||||
masteryPkg.Drops,
|
completionData.Level,
|
||||||
unlockableMap,
|
masteryPkg.Drops || [],
|
||||||
),
|
unlockableMap,
|
||||||
})
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return masteryData
|
return masteryData
|
||||||
|
@ -19,7 +19,12 @@
|
|||||||
import { getSubLocationByName } from "../contracts/dataGen"
|
import { getSubLocationByName } from "../contracts/dataGen"
|
||||||
import { controller } from "../controller"
|
import { controller } from "../controller"
|
||||||
import { getUnlockablesById, grantDrops } from "../inventory"
|
import { getUnlockablesById, grantDrops } from "../inventory"
|
||||||
import type { ContractSession, UserProfile, GameVersion } from "../types/types"
|
import type {
|
||||||
|
ContractSession,
|
||||||
|
GameVersion,
|
||||||
|
Unlockable,
|
||||||
|
UserProfile,
|
||||||
|
} from "../types/types"
|
||||||
import {
|
import {
|
||||||
clampValue,
|
clampValue,
|
||||||
DEFAULT_MASTERY_MAXLEVEL,
|
DEFAULT_MASTERY_MAXLEVEL,
|
||||||
@ -70,7 +75,9 @@ export class ProgressionService {
|
|||||||
if (dropIds.length > 0) {
|
if (dropIds.length > 0) {
|
||||||
grantDrops(
|
grantDrops(
|
||||||
userProfile.Id,
|
userProfile.Id,
|
||||||
getUnlockablesById(dropIds, contractSession.gameVersion),
|
getUnlockablesById(dropIds, contractSession.gameVersion).filter(
|
||||||
|
Boolean,
|
||||||
|
) as Unlockable[],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,7 +92,8 @@ export class ProgressionService {
|
|||||||
subPkgId?: string,
|
subPkgId?: string,
|
||||||
) {
|
) {
|
||||||
return subPkgId
|
return subPkgId
|
||||||
? userProfile.Extensions.progression.Locations[location][subPkgId]
|
? // @ts-expect-error It is possible to index into an object with a string
|
||||||
|
userProfile.Extensions.progression.Locations[location][subPkgId]
|
||||||
: userProfile.Extensions.progression.Locations[location]
|
: userProfile.Extensions.progression.Locations[location]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -179,25 +187,29 @@ export class ProgressionService {
|
|||||||
if (masteryData) {
|
if (masteryData) {
|
||||||
const previousLevel = locationData.Level
|
const previousLevel = locationData.Level
|
||||||
|
|
||||||
|
let newLocationXp = xpRequiredForLevel(maxLevel)
|
||||||
|
|
||||||
|
if (isEvergreenContract) {
|
||||||
|
newLocationXp = xpRequiredForEvergreenLevel(maxLevel)
|
||||||
|
} else if (sniperUnlockable) {
|
||||||
|
newLocationXp = xpRequiredForSniperLevel(maxLevel)
|
||||||
|
}
|
||||||
|
|
||||||
locationData.Xp = clampValue(
|
locationData.Xp = clampValue(
|
||||||
locationData.Xp + masteryXp + actionXp,
|
locationData.Xp + masteryXp + actionXp,
|
||||||
0,
|
0,
|
||||||
isEvergreenContract
|
newLocationXp,
|
||||||
? xpRequiredForEvergreenLevel(maxLevel)
|
|
||||||
: sniperUnlockable
|
|
||||||
? xpRequiredForSniperLevel(maxLevel)
|
|
||||||
: xpRequiredForLevel(maxLevel),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
locationData.Level = clampValue(
|
let newLocationLevel = levelForXp(newLocationXp)
|
||||||
isEvergreenContract
|
|
||||||
? evergreenLevelForXp(locationData.Xp)
|
if (isEvergreenContract) {
|
||||||
: sniperUnlockable
|
newLocationLevel = evergreenLevelForXp(newLocationXp)
|
||||||
? sniperLevelForXp(locationData.Xp)
|
} else if (sniperUnlockable) {
|
||||||
: levelForXp(locationData.Xp),
|
newLocationLevel = sniperLevelForXp(newLocationXp)
|
||||||
1,
|
}
|
||||||
maxLevel,
|
|
||||||
)
|
locationData.Level = clampValue(newLocationLevel, 1, maxLevel)
|
||||||
|
|
||||||
// If mastery level has gone up, check if there are available drop rewards and award them
|
// If mastery level has gone up, check if there are available drop rewards and award them
|
||||||
if (locationData.Level > previousLevel) {
|
if (locationData.Level > previousLevel) {
|
||||||
@ -205,13 +217,13 @@ export class ProgressionService {
|
|||||||
contractSession.gameVersion,
|
contractSession.gameVersion,
|
||||||
isEvergreenContract,
|
isEvergreenContract,
|
||||||
sniperUnlockable
|
sniperUnlockable
|
||||||
? masteryData.SubPackages.find(
|
? masteryData.SubPackages?.find(
|
||||||
(pkg) => pkg.Id === sniperUnlockable,
|
(pkg) => pkg.Id === sniperUnlockable,
|
||||||
).Drops
|
)?.Drops || []
|
||||||
: masteryData.Drops,
|
: masteryData.Drops || [],
|
||||||
previousLevel,
|
previousLevel,
|
||||||
locationData.Level,
|
locationData.Level,
|
||||||
)
|
).filter(Boolean) as Unlockable[]
|
||||||
grantDrops(userProfile.Id, masteryLocationDrops)
|
grantDrops(userProfile.Id, masteryLocationDrops)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -291,7 +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 this is H2, but we don't have a h2 specific config, fall back to h3
|
||||||
if (gameVersion === "h2" && !Object.hasOwn(configs, `H2${config}`)) {
|
if (gameVersion === "h2" && !configs[`H2${config}`]) {
|
||||||
return getConfig(config, clone)
|
return getConfig(config, clone)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,7 +52,7 @@ import {
|
|||||||
createTimeLimit,
|
createTimeLimit,
|
||||||
TargetCreator,
|
TargetCreator,
|
||||||
} from "../statemachines/contractCreation"
|
} from "../statemachines/contractCreation"
|
||||||
import { createSniperLoadouts } from "../menus/sniper"
|
import { createSniperLoadouts, SniperCharacter } from "../menus/sniper"
|
||||||
import { GetForPlay2Body } from "../types/gameSchemas"
|
import { GetForPlay2Body } from "../types/gameSchemas"
|
||||||
import assert from "assert"
|
import assert from "assert"
|
||||||
import { getUserData } from "../databaseHandler"
|
import { getUserData } from "../databaseHandler"
|
||||||
@ -63,7 +63,8 @@ const contractRoutingRouter = Router()
|
|||||||
contractRoutingRouter.post(
|
contractRoutingRouter.post(
|
||||||
"/GetForPlay2",
|
"/GetForPlay2",
|
||||||
jsonMiddleware(),
|
jsonMiddleware(),
|
||||||
async (req: RequestWithJwt<never, GetForPlay2Body>, res) => {
|
// @ts-expect-error Has jwt props.
|
||||||
|
(req: RequestWithJwt<never, GetForPlay2Body>, res) => {
|
||||||
if (!req.body.id || !uuidRegex.test(req.body.id)) {
|
if (!req.body.id || !uuidRegex.test(req.body.id)) {
|
||||||
res.status(400).end()
|
res.status(400).end()
|
||||||
return // user sent some nasty info
|
return // user sent some nasty info
|
||||||
@ -84,7 +85,8 @@ contractRoutingRouter.post(
|
|||||||
req.jwt.unique_name,
|
req.jwt.unique_name,
|
||||||
req.gameVersion,
|
req.gameVersion,
|
||||||
contractData,
|
contractData,
|
||||||
)
|
) as SniperCharacter[]
|
||||||
|
|
||||||
const loadoutData = {
|
const loadoutData = {
|
||||||
CharacterLoadoutData:
|
CharacterLoadoutData:
|
||||||
sniperloadouts.length !== 0 ? sniperloadouts : null,
|
sniperloadouts.length !== 0 ? sniperloadouts : null,
|
||||||
@ -100,7 +102,7 @@ contractRoutingRouter.post(
|
|||||||
req.gameVersion,
|
req.gameVersion,
|
||||||
)
|
)
|
||||||
: {}),
|
: {}),
|
||||||
...loadoutData,
|
...(loadoutData || {}),
|
||||||
...{
|
...{
|
||||||
OpportunityData: getContractOpportunityData(req, contractData),
|
OpportunityData: getContractOpportunityData(req, contractData),
|
||||||
},
|
},
|
||||||
@ -120,7 +122,7 @@ contractRoutingRouter.post(
|
|||||||
.toString()}-${randomUUID()}`,
|
.toString()}-${randomUUID()}`,
|
||||||
ContractProgressionData: contractData.Metadata
|
ContractProgressionData: contractData.Metadata
|
||||||
.UseContractProgressionData
|
.UseContractProgressionData
|
||||||
? await getCpd(req.jwt.unique_name, contractData.Metadata.CpdId)
|
? getCpd(req.jwt.unique_name, contractData.Metadata.CpdId!)
|
||||||
: null,
|
: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,6 +161,8 @@ contractRoutingRouter.post(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assert.ok(gameChanger.Objectives, "gc has no objectives")
|
||||||
|
|
||||||
contractData.Data.GameChangerReferences.push(gameChanger)
|
contractData.Data.GameChangerReferences.push(gameChanger)
|
||||||
contractData.Data.Bricks = [
|
contractData.Data.Bricks = [
|
||||||
...(contractData.Data.Bricks ?? []),
|
...(contractData.Data.Bricks ?? []),
|
||||||
@ -228,6 +232,7 @@ contractRoutingRouter.post(
|
|||||||
contractRoutingRouter.post(
|
contractRoutingRouter.post(
|
||||||
"/CreateFromParams",
|
"/CreateFromParams",
|
||||||
jsonMiddleware(),
|
jsonMiddleware(),
|
||||||
|
// @ts-expect-error Has jwt props.
|
||||||
async (
|
async (
|
||||||
req: RequestWithJwt<Record<never, never>, CreateFromParamsBody>,
|
req: RequestWithJwt<Record<never, never>, CreateFromParamsBody>,
|
||||||
res,
|
res,
|
||||||
@ -340,13 +345,20 @@ contractRoutingRouter.post(
|
|||||||
contractRoutingRouter.post(
|
contractRoutingRouter.post(
|
||||||
"/GetContractOpportunities",
|
"/GetContractOpportunities",
|
||||||
jsonMiddleware(),
|
jsonMiddleware(),
|
||||||
|
// @ts-expect-error Has jwt props.
|
||||||
(req: RequestWithJwt<never, { contractId: string }>, res) => {
|
(req: RequestWithJwt<never, { contractId: string }>, res) => {
|
||||||
const contract = controller.resolveContract(req.body.contractId)
|
const contract = controller.resolveContract(req.body.contractId)
|
||||||
|
|
||||||
|
if (!contract) {
|
||||||
|
res.status(400).send("contract not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
res.json(getContractOpportunityData(req, contract))
|
res.json(getContractOpportunityData(req, contract))
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
function getContractOpportunityData(
|
export function getContractOpportunityData(
|
||||||
req: RequestWithJwt,
|
req: RequestWithJwt,
|
||||||
contract: MissionManifest,
|
contract: MissionManifest,
|
||||||
): MissionStory[] {
|
): MissionStory[] {
|
||||||
@ -366,6 +378,7 @@ function getContractOpportunityData(
|
|||||||
missionStories[ms].PreviouslyCompleted =
|
missionStories[ms].PreviouslyCompleted =
|
||||||
ms in userData.Extensions.opportunityprogression
|
ms in userData.Extensions.opportunityprogression
|
||||||
const current = fastClone(missionStories[ms])
|
const current = fastClone(missionStories[ms])
|
||||||
|
// @ts-expect-error Deal with it.
|
||||||
delete current.Location
|
delete current.Location
|
||||||
result.push(current)
|
result.push(current)
|
||||||
}
|
}
|
||||||
|
@ -38,6 +38,7 @@ import {
|
|||||||
getUserEscalationProgress,
|
getUserEscalationProgress,
|
||||||
} from "./escalations/escalationService"
|
} from "./escalations/escalationService"
|
||||||
import { translateEntitlements } from "../ownership"
|
import { translateEntitlements } from "../ownership"
|
||||||
|
import assert from "assert"
|
||||||
|
|
||||||
// TODO: In the near future, this file should be cleaned up where possible.
|
// TODO: In the near future, this file should be cleaned up where possible.
|
||||||
|
|
||||||
@ -123,19 +124,28 @@ export function generateCompletionData(
|
|||||||
let difficulty = undefined
|
let difficulty = undefined
|
||||||
|
|
||||||
if (gameVersion === "h1") {
|
if (gameVersion === "h1") {
|
||||||
difficulty = getUserData(userId, gameVersion).Extensions
|
const userData = getUserData(userId, gameVersion)
|
||||||
.gamepersistentdata.menudata.difficulty.destinations[
|
difficulty =
|
||||||
subLocation ? subLocation.Properties?.ParentLocation : subLocationId
|
userData.Extensions.gamepersistentdata.menudata.difficulty
|
||||||
]
|
.destinations[
|
||||||
|
subLocation
|
||||||
|
? subLocation.Properties?.ParentLocation || ""
|
||||||
|
: subLocationId
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
const locationId = subLocation
|
const locationId = subLocation
|
||||||
? subLocation.Properties?.ParentLocation
|
? subLocation.Properties?.ParentLocation
|
||||||
: subLocationId
|
: subLocationId
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
locationId,
|
||||||
|
`Location ID is undefined for ${subLocationId} in ${gameVersion}!`,
|
||||||
|
)
|
||||||
|
|
||||||
const completionData = controller.masteryService.getLocationCompletion(
|
const completionData = controller.masteryService.getLocationCompletion(
|
||||||
locationId,
|
locationId,
|
||||||
subLocation?.Id,
|
subLocationId,
|
||||||
gameVersion,
|
gameVersion,
|
||||||
userId,
|
userId,
|
||||||
contractType,
|
contractType,
|
||||||
@ -153,7 +163,7 @@ export function generateCompletionData(
|
|||||||
Completion: 1.0,
|
Completion: 1.0,
|
||||||
XpLeft: 0,
|
XpLeft: 0,
|
||||||
Id: locationId,
|
Id: locationId,
|
||||||
SubLocationId: subLocation?.Id,
|
SubLocationId: subLocationId,
|
||||||
HideProgression: true,
|
HideProgression: true,
|
||||||
IsLocationProgression: true,
|
IsLocationProgression: true,
|
||||||
Name: null,
|
Name: null,
|
||||||
@ -197,7 +207,7 @@ export function generateUserCentric(
|
|||||||
// fix h1/h2 entitlements
|
// fix h1/h2 entitlements
|
||||||
contractData.Metadata.Entitlements = translateEntitlements(
|
contractData.Metadata.Entitlements = translateEntitlements(
|
||||||
gameVersion,
|
gameVersion,
|
||||||
contractData.Metadata.Entitlements,
|
contractData.Metadata.Entitlements || [],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -210,6 +220,12 @@ export function generateUserCentric(
|
|||||||
gameVersion,
|
gameVersion,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
let lastPlayed: string | undefined = undefined
|
||||||
|
|
||||||
|
if (played[id]?.LastPlayedAt) {
|
||||||
|
lastPlayed = new Date(played[id].LastPlayedAt!).toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
const uc: UserCentricContract = {
|
const uc: UserCentricContract = {
|
||||||
Contract: contractData,
|
Contract: contractData,
|
||||||
Data: {
|
Data: {
|
||||||
@ -222,10 +238,7 @@ export function generateUserCentric(
|
|||||||
LocationHideProgression: completionData.HideProgression,
|
LocationHideProgression: completionData.HideProgression,
|
||||||
ElusiveContractState: "",
|
ElusiveContractState: "",
|
||||||
IsFeatured: false,
|
IsFeatured: false,
|
||||||
LastPlayedAt:
|
LastPlayedAt: lastPlayed,
|
||||||
played[id] === undefined
|
|
||||||
? undefined
|
|
||||||
: new Date(played[id]?.LastPlayedAt).toISOString(),
|
|
||||||
// relevant for contracts
|
// relevant for contracts
|
||||||
// Favorite contracts
|
// Favorite contracts
|
||||||
PlaylistData: {
|
PlaylistData: {
|
||||||
@ -316,6 +329,7 @@ export function mapObjectives(
|
|||||||
gameChangerProps.ObjectivesCategory = (() => {
|
gameChangerProps.ObjectivesCategory = (() => {
|
||||||
let obj: MissionManifestObjective
|
let obj: MissionManifestObjective
|
||||||
|
|
||||||
|
// @ts-expect-error State machines are impossible to type
|
||||||
for (obj of gameChangerProps.Objectives) {
|
for (obj of gameChangerProps.Objectives) {
|
||||||
if (obj.Category === "primary") return "primary"
|
if (obj.Category === "primary") return "primary"
|
||||||
if (obj.Category === "secondary")
|
if (obj.Category === "secondary")
|
||||||
@ -362,11 +376,9 @@ export function mapObjectives(
|
|||||||
objective.OnActive.IfInProgress.Visible === false) ||
|
objective.OnActive.IfInProgress.Visible === false) ||
|
||||||
(objective.OnActive?.IfCompleted &&
|
(objective.OnActive?.IfCompleted &&
|
||||||
objective.OnActive.IfCompleted.Visible === false &&
|
objective.OnActive.IfCompleted.Visible === false &&
|
||||||
objective.Definition &&
|
// @ts-expect-error State machines are impossible to type
|
||||||
objective.Definition.States &&
|
objective.Definition?.States?.Start?.["-"]?.Transition ===
|
||||||
objective.Definition.States.Start &&
|
"Success")
|
||||||
objective.Definition.States.Start["-"] &&
|
|
||||||
objective.Definition.States.Start["-"].Transition === "Success")
|
|
||||||
) {
|
) {
|
||||||
continue // do not show objectives with 'ForceShowOnLoadingScreen: false' or objectives that are not visible on start
|
continue // do not show objectives with 'ForceShowOnLoadingScreen: false' or objectives that are not visible on start
|
||||||
}
|
}
|
||||||
@ -374,8 +386,7 @@ export function mapObjectives(
|
|||||||
if (
|
if (
|
||||||
objective.SuccessEvent &&
|
objective.SuccessEvent &&
|
||||||
objective.SuccessEvent.EventName === "Kill" &&
|
objective.SuccessEvent.EventName === "Kill" &&
|
||||||
objective.SuccessEvent.EventValues &&
|
objective.SuccessEvent.EventValues?.RepositoryId
|
||||||
objective.SuccessEvent.EventValues.RepositoryId
|
|
||||||
) {
|
) {
|
||||||
result.set(objective.Id, {
|
result.set(objective.Id, {
|
||||||
Type: "kill",
|
Type: "kill",
|
||||||
@ -396,6 +407,7 @@ export function mapObjectives(
|
|||||||
objective.Definition?.Context?.Targets &&
|
objective.Definition?.Context?.Targets &&
|
||||||
(objective.Definition.Context.Targets as string[]).length === 1
|
(objective.Definition.Context.Targets as string[]).length === 1
|
||||||
) {
|
) {
|
||||||
|
// @ts-expect-error State machines are impossible to type
|
||||||
id = objective.Definition.Context.Targets[0]
|
id = objective.Definition.Context.Targets[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -437,10 +449,8 @@ export function mapObjectives(
|
|||||||
})
|
})
|
||||||
} else if (
|
} else if (
|
||||||
objective.Type === "statemachine" &&
|
objective.Type === "statemachine" &&
|
||||||
objective.Definition &&
|
(objective.Definition?.Context?.Targets as unknown[])?.length ===
|
||||||
objective.Definition.Context &&
|
1 &&
|
||||||
objective.Definition.Context.Targets &&
|
|
||||||
(objective.Definition.Context.Targets as unknown[]).length === 1 &&
|
|
||||||
objective.HUDTemplate
|
objective.HUDTemplate
|
||||||
) {
|
) {
|
||||||
// This objective will be displayed as a kill objective
|
// This objective will be displayed as a kill objective
|
||||||
@ -457,6 +467,7 @@ export function mapObjectives(
|
|||||||
result.set(objective.Id, {
|
result.set(objective.Id, {
|
||||||
Type: "kill",
|
Type: "kill",
|
||||||
Properties: {
|
Properties: {
|
||||||
|
// @ts-expect-error State machines are impossible to type
|
||||||
Id: objective.Definition.Context.Targets[0],
|
Id: objective.Definition.Context.Targets[0],
|
||||||
Conditions: Conditions,
|
Conditions: Conditions,
|
||||||
},
|
},
|
||||||
|
@ -26,7 +26,6 @@ import type {
|
|||||||
} from "../../types/types"
|
} from "../../types/types"
|
||||||
import { getUserData } from "../../databaseHandler"
|
import { getUserData } from "../../databaseHandler"
|
||||||
import { log, LogLevel } from "../../loggingInterop"
|
import { log, LogLevel } from "../../loggingInterop"
|
||||||
import assert from "assert"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Put a group id in here to hide it from the menus on 2016.
|
* Put a group id in here to hide it from the menus on 2016.
|
||||||
@ -113,19 +112,25 @@ export function getLevelCount(
|
|||||||
* @param userId The current user's ID.
|
* @param userId The current user's ID.
|
||||||
* @param groupContractId The escalation's group contract ID.
|
* @param groupContractId The escalation's group contract ID.
|
||||||
* @param gameVersion The game's version.
|
* @param gameVersion The game's version.
|
||||||
* @returns The escalation play details.
|
* @returns The escalation play details, or an empty object if not applicable.
|
||||||
*/
|
*/
|
||||||
export function getPlayEscalationInfo(
|
export function getPlayEscalationInfo(
|
||||||
userId: string,
|
userId: string,
|
||||||
groupContractId: string,
|
groupContractId: string | undefined | null,
|
||||||
gameVersion: GameVersion,
|
gameVersion: GameVersion,
|
||||||
): EscalationInfo {
|
): EscalationInfo {
|
||||||
|
if (!groupContractId) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
const userData = getUserData(userId, gameVersion)
|
const userData = getUserData(userId, gameVersion)
|
||||||
|
|
||||||
const p = getUserEscalationProgress(userData, groupContractId)
|
const p = getUserEscalationProgress(userData, groupContractId)
|
||||||
const groupCt = controller.escalationMappings.get(groupContractId)
|
const groupCt = controller.escalationMappings.get(groupContractId)
|
||||||
|
|
||||||
assert.ok(groupCt, `No escalation mapping for ${groupContractId}`)
|
if (!groupCt) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
const totalLevelCount = getLevelCount(
|
const totalLevelCount = getLevelCount(
|
||||||
controller.resolveContract(groupContractId),
|
controller.resolveContract(groupContractId),
|
||||||
|
@ -21,6 +21,7 @@ import {
|
|||||||
ContractHistory,
|
ContractHistory,
|
||||||
GameVersion,
|
GameVersion,
|
||||||
HitsCategoryCategory,
|
HitsCategoryCategory,
|
||||||
|
IHit,
|
||||||
} from "../types/types"
|
} from "../types/types"
|
||||||
import {
|
import {
|
||||||
contractIdToHitObject,
|
contractIdToHitObject,
|
||||||
@ -35,6 +36,7 @@ import { log, LogLevel } from "../loggingInterop"
|
|||||||
import { fastClone, getRemoteService } from "../utils"
|
import { fastClone, getRemoteService } from "../utils"
|
||||||
import { orderedETAs } from "./elusiveTargetArcades"
|
import { orderedETAs } from "./elusiveTargetArcades"
|
||||||
import { missionsInLocations } from "./missionsInLocation"
|
import { missionsInLocations } from "./missionsInLocation"
|
||||||
|
import assert from "assert"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The filters supported for HitsCategories.
|
* The filters supported for HitsCategories.
|
||||||
@ -154,11 +156,11 @@ export class HitsCategoryService {
|
|||||||
|
|
||||||
switch (gameVersion) {
|
switch (gameVersion) {
|
||||||
case "h1":
|
case "h1":
|
||||||
if (contract.Metadata.Season === 1)
|
if (contract?.Metadata.Season === 1)
|
||||||
contracts.push(id)
|
contracts.push(id)
|
||||||
break
|
break
|
||||||
case "h2":
|
case "h2":
|
||||||
if (contract.Metadata.Season <= 2)
|
if ((contract?.Metadata.Season || 0) <= 2)
|
||||||
contracts.push(id)
|
contracts.push(id)
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
@ -232,7 +234,7 @@ export class HitsCategoryService {
|
|||||||
.tap(tapName, (contracts, gameVersion) => {
|
.tap(tapName, (contracts, gameVersion) => {
|
||||||
// We need to push Peacock contracts first to work around H2 not
|
// We need to push Peacock contracts first to work around H2 not
|
||||||
// having the "order" property for $arraygroupby. (The game just crashes)
|
// having the "order" property for $arraygroupby. (The game just crashes)
|
||||||
const nEscalations = []
|
const nEscalations: string[] = []
|
||||||
|
|
||||||
for (const escalations of Object.values(
|
for (const escalations of Object.values(
|
||||||
missionsInLocations.escalations,
|
missionsInLocations.escalations,
|
||||||
@ -240,6 +242,10 @@ export class HitsCategoryService {
|
|||||||
for (const id of escalations) {
|
for (const id of escalations) {
|
||||||
const contract = controller.resolveContract(id)
|
const contract = controller.resolveContract(id)
|
||||||
|
|
||||||
|
if (!contract) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
const isPeacock = contract.Metadata.Season === 0
|
const isPeacock = contract.Metadata.Season === 0
|
||||||
const season = isPeacock
|
const season = isPeacock
|
||||||
? contract.Metadata.OriginalSeason
|
? contract.Metadata.OriginalSeason
|
||||||
@ -252,7 +258,7 @@ export class HitsCategoryService {
|
|||||||
if (season === 1) contracts.push(id)
|
if (season === 1) contracts.push(id)
|
||||||
break
|
break
|
||||||
case "h2":
|
case "h2":
|
||||||
if (season <= 2)
|
if ((season || 0) <= 2)
|
||||||
(isPeacock ? contracts : nEscalations).push(
|
(isPeacock ? contracts : nEscalations).push(
|
||||||
id,
|
id,
|
||||||
)
|
)
|
||||||
@ -273,7 +279,7 @@ export class HitsCategoryService {
|
|||||||
pageNumber: number,
|
pageNumber: number,
|
||||||
gameVersion: GameVersion,
|
gameVersion: GameVersion,
|
||||||
userId: string,
|
userId: string,
|
||||||
): Promise<HitsCategoryCategory> {
|
): Promise<HitsCategoryCategory | undefined> {
|
||||||
const remoteService = getRemoteService(gameVersion)
|
const remoteService = getRemoteService(gameVersion)
|
||||||
const user = userAuths.get(userId)
|
const user = userAuths.get(userId)
|
||||||
|
|
||||||
@ -289,10 +295,12 @@ export class HitsCategoryService {
|
|||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
const hits = resp.data.data.Data.Hits
|
const hits = resp.data.data.Data.Hits
|
||||||
preserveContracts(
|
void preserveContracts(
|
||||||
hits.map(
|
hits
|
||||||
(hit) => hit.UserCentricContract.Contract.Metadata.PublicId,
|
.map(
|
||||||
),
|
(hit) => hit.UserCentricContract.Contract.Metadata.PublicId,
|
||||||
|
)
|
||||||
|
.filter(Boolean) as string[],
|
||||||
)
|
)
|
||||||
|
|
||||||
// Fix completion and favorite status for retrieved contracts
|
// Fix completion and favorite status for retrieved contracts
|
||||||
@ -304,7 +312,7 @@ export class HitsCategoryService {
|
|||||||
if (Object.keys(played).includes(hit.Id)) {
|
if (Object.keys(played).includes(hit.Id)) {
|
||||||
// Replace with data stored by Peacock
|
// Replace with data stored by Peacock
|
||||||
hit.UserCentricContract.Data.LastPlayedAt = new Date(
|
hit.UserCentricContract.Data.LastPlayedAt = new Date(
|
||||||
played[hit.Id].LastPlayedAt,
|
played[hit.Id].LastPlayedAt || 0,
|
||||||
).toISOString()
|
).toISOString()
|
||||||
hit.UserCentricContract.Data.Completed =
|
hit.UserCentricContract.Data.Completed =
|
||||||
played[hit.Id].Completed
|
played[hit.Id].Completed
|
||||||
@ -314,8 +322,10 @@ export class HitsCategoryService {
|
|||||||
hit.UserCentricContract.Data.Completed = false
|
hit.UserCentricContract.Data.Completed = false
|
||||||
}
|
}
|
||||||
|
|
||||||
hit.UserCentricContract.Data.PlaylistData.IsAdded =
|
if (hit.UserCentricContract.Data.PlaylistData) {
|
||||||
favorites.includes(hit.Id)
|
hit.UserCentricContract.Data.PlaylistData.IsAdded =
|
||||||
|
favorites.includes(hit.Id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return resp.data.data
|
return resp.data.data
|
||||||
@ -378,13 +388,23 @@ export class HitsCategoryService {
|
|||||||
type: ContractFilter,
|
type: ContractFilter,
|
||||||
category: string,
|
category: string,
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
if (!this.filterSupported.includes(category)) return undefined
|
if (!this.filterSupported.includes(category)) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
const user = getUserData(userId, gameVersion)
|
const user = getUserData(userId, gameVersion)
|
||||||
|
type Cast =
|
||||||
|
keyof typeof user.Extensions.gamepersistentdata.HitsFilterType
|
||||||
|
|
||||||
if (type === "default") {
|
if (type === "default") {
|
||||||
type = user.Extensions.gamepersistentdata.HitsFilterType[category]
|
type =
|
||||||
|
user.Extensions.gamepersistentdata.HitsFilterType[
|
||||||
|
category as Cast
|
||||||
|
]
|
||||||
} else {
|
} else {
|
||||||
user.Extensions.gamepersistentdata.HitsFilterType[category] = type
|
user.Extensions.gamepersistentdata.HitsFilterType[
|
||||||
|
category as Cast
|
||||||
|
] = type
|
||||||
writeUserData(userId, gameVersion)
|
writeUserData(userId, gameVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -408,7 +428,10 @@ export class HitsCategoryService {
|
|||||||
return Object.keys(played)
|
return Object.keys(played)
|
||||||
.filter((id) => this.isContractOfType(played, type, id))
|
.filter((id) => this.isContractOfType(played, type, id))
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
return played[b].LastPlayedAt - played[a].LastPlayedAt
|
return (
|
||||||
|
(played[b].LastPlayedAt || 0) -
|
||||||
|
(played[a].LastPlayedAt || 0)
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -428,9 +451,9 @@ export class HitsCategoryService {
|
|||||||
): boolean {
|
): boolean {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "completed":
|
case "completed":
|
||||||
return (
|
return Boolean(
|
||||||
played[contractId]?.Completed &&
|
played[contractId]?.Completed &&
|
||||||
!played[contractId]?.IsEscalation
|
!played[contractId]?.IsEscalation,
|
||||||
)
|
)
|
||||||
case "failed":
|
case "failed":
|
||||||
return (
|
return (
|
||||||
@ -440,6 +463,8 @@ export class HitsCategoryService {
|
|||||||
)
|
)
|
||||||
case "all":
|
case "all":
|
||||||
return !played[contractId]?.IsEscalation
|
return !played[contractId]?.IsEscalation
|
||||||
|
default:
|
||||||
|
assert.fail("Invalid type passed to isContractOfType")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -457,7 +482,7 @@ export class HitsCategoryService {
|
|||||||
pageNumber: number,
|
pageNumber: number,
|
||||||
gameVersion: GameVersion,
|
gameVersion: GameVersion,
|
||||||
userId: string,
|
userId: string,
|
||||||
): Promise<HitsCategoryCategory> {
|
): Promise<HitsCategoryCategory | undefined> {
|
||||||
if (this.realtimeFetched.includes(categoryName)) {
|
if (this.realtimeFetched.includes(categoryName)) {
|
||||||
return await this.fetchFromOfficial(
|
return await this.fetchFromOfficial(
|
||||||
categoryName,
|
categoryName,
|
||||||
@ -479,7 +504,7 @@ export class HitsCategoryService {
|
|||||||
userId,
|
userId,
|
||||||
filter,
|
filter,
|
||||||
category,
|
category,
|
||||||
)
|
)!
|
||||||
|
|
||||||
const hitsCategory: HitsCategoryCategory = {
|
const hitsCategory: HitsCategoryCategory = {
|
||||||
Category: category,
|
Category: category,
|
||||||
@ -489,7 +514,7 @@ export class HitsCategoryService {
|
|||||||
Page: pageNumber,
|
Page: pageNumber,
|
||||||
HasMore: false,
|
HasMore: false,
|
||||||
},
|
},
|
||||||
CurrentSubType: undefined,
|
CurrentSubType: "",
|
||||||
}
|
}
|
||||||
|
|
||||||
const hook = this.hitsCategories.for(category)
|
const hook = this.hitsCategories.for(category)
|
||||||
@ -500,7 +525,7 @@ export class HitsCategoryService {
|
|||||||
|
|
||||||
const hitObjectList = hits
|
const hitObjectList = hits
|
||||||
.map((id) => contractIdToHitObject(id, gameVersion, userId))
|
.map((id) => contractIdToHitObject(id, gameVersion, userId))
|
||||||
.filter(Boolean)
|
.filter(Boolean) as IHit[]
|
||||||
|
|
||||||
if (!this.paginationExempt.includes(category)) {
|
if (!this.paginationExempt.includes(category)) {
|
||||||
const paginated = paginate(hitObjectList, this.hitsPerPage)
|
const paginated = paginate(hitObjectList, this.hitsPerPage)
|
||||||
|
@ -53,9 +53,15 @@ export async function getLeaderboardEntries(
|
|||||||
platform: JwtData["platform"],
|
platform: JwtData["platform"],
|
||||||
gameVersion: GameVersion,
|
gameVersion: GameVersion,
|
||||||
difficultyLevel?: string,
|
difficultyLevel?: string,
|
||||||
): Promise<GameFacingLeaderboardData> {
|
): Promise<GameFacingLeaderboardData | undefined> {
|
||||||
let difficulty = "unset"
|
let difficulty = "unset"
|
||||||
|
|
||||||
|
const contract = controller.resolveContract(contractId)
|
||||||
|
|
||||||
|
if (!contract) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
const parsedDifficulty = parseInt(difficultyLevel || "0")
|
const parsedDifficulty = parseInt(difficultyLevel || "0")
|
||||||
|
|
||||||
if (parsedDifficulty === gameDifficulty.casual) {
|
if (parsedDifficulty === gameDifficulty.casual) {
|
||||||
@ -72,7 +78,7 @@ export async function getLeaderboardEntries(
|
|||||||
|
|
||||||
const response: GameFacingLeaderboardData = {
|
const response: GameFacingLeaderboardData = {
|
||||||
Entries: [],
|
Entries: [],
|
||||||
Contract: controller.resolveContract(contractId),
|
Contract: contract,
|
||||||
Page: 0,
|
Page: 0,
|
||||||
HasMore: false,
|
HasMore: false,
|
||||||
LeaderboardType: "singleplayer",
|
LeaderboardType: "singleplayer",
|
||||||
|
@ -1,34 +0,0 @@
|
|||||||
/*
|
|
||||||
* The Peacock Project - a HITMAN server replacement.
|
|
||||||
* Copyright (C) 2021-2024 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"
|
|
||||||
import type { RequestWithJwt } from "../types/types"
|
|
||||||
const reportRouter = Router()
|
|
||||||
|
|
||||||
reportRouter.post(
|
|
||||||
"/ReportContract",
|
|
||||||
jsonMiddleware(),
|
|
||||||
(
|
|
||||||
req: RequestWithJwt<never, { contractId: string; reason: number }>,
|
|
||||||
res,
|
|
||||||
) => {
|
|
||||||
res.json({})
|
|
||||||
},
|
|
||||||
)
|
|
||||||
export { reportRouter }
|
|
@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ContractSession } from "./types/types"
|
import { ContractSession } from "../types/types"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Changes a set to an array.
|
* Changes a set to an array.
|
||||||
@ -60,23 +60,28 @@ const SESSION_MAP_PROPS: (keyof ContractSession)[] = [
|
|||||||
* @param session The ContractSession.
|
* @param session The ContractSession.
|
||||||
*/
|
*/
|
||||||
export function serializeSession(session: ContractSession): unknown {
|
export function serializeSession(session: ContractSession): unknown {
|
||||||
const o = {}
|
const o: Partial<ContractSession> = {}
|
||||||
|
|
||||||
|
type K = keyof ContractSession
|
||||||
|
|
||||||
// obj clone
|
// obj clone
|
||||||
for (const key of Object.keys(session)) {
|
for (const key of Object.keys(session)) {
|
||||||
if (session[key] instanceof Map) {
|
if (session[key as K] instanceof Map) {
|
||||||
|
// @ts-expect-error Type mismatch.
|
||||||
o[key] = Array.from(
|
o[key] = Array.from(
|
||||||
(session[key] as Map<string, unknown>).entries(),
|
(session[key as K] as Map<string, unknown>).entries(),
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if (session[key] instanceof Set) {
|
if (session[key as K] instanceof Set) {
|
||||||
|
// @ts-expect-error Type mismatch.
|
||||||
o[key] = normalizeSet(session[key])
|
o[key] = normalizeSet(session[key])
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
o[key] = session[key]
|
// @ts-expect-error Type mismatch.
|
||||||
|
o[key] = session[key as K]
|
||||||
}
|
}
|
||||||
|
|
||||||
return o
|
return o
|
||||||
@ -90,19 +95,22 @@ export function serializeSession(session: ContractSession): unknown {
|
|||||||
export function deserializeSession(
|
export function deserializeSession(
|
||||||
saved: Record<string, unknown>,
|
saved: Record<string, unknown>,
|
||||||
): ContractSession {
|
): ContractSession {
|
||||||
const session = {}
|
const session: Partial<ContractSession> = {}
|
||||||
|
|
||||||
// obj clone
|
// obj clone
|
||||||
for (const key of Object.keys(saved)) {
|
for (const key of Object.keys(saved)) {
|
||||||
|
// @ts-expect-error Type mismatch.
|
||||||
session[key] = saved[key]
|
session[key] = saved[key]
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const collection of SESSION_SET_PROPS) {
|
for (const collection of SESSION_SET_PROPS) {
|
||||||
|
// @ts-expect-error Type mismatch.
|
||||||
session[collection] = new Set(session[collection])
|
session[collection] = new Set(session[collection])
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const map of SESSION_MAP_PROPS) {
|
for (const map of SESSION_MAP_PROPS) {
|
||||||
if (Object.hasOwn(session, map)) {
|
if (Object.hasOwn(session, map)) {
|
||||||
|
// @ts-expect-error Type mismatch.
|
||||||
session[map] = new Map(session[map])
|
session[map] = new Map(session[map])
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -188,8 +188,10 @@ function createPeacockRequire(pluginName: string): NodeRequire {
|
|||||||
* @param specifier The requested module.
|
* @param specifier The requested module.
|
||||||
*/
|
*/
|
||||||
const peacockRequire: NodeRequire = (specifier: string) => {
|
const peacockRequire: NodeRequire = (specifier: string) => {
|
||||||
if (generatedPeacockRequireTable[specifier]) {
|
type T = keyof typeof generatedPeacockRequireTable
|
||||||
return generatedPeacockRequireTable[specifier]
|
|
||||||
|
if (generatedPeacockRequireTable[specifier as T]) {
|
||||||
|
return generatedPeacockRequireTable[specifier as T]
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -237,14 +239,22 @@ export const validateMission = (m: MissionManifest): boolean => {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const prop of ["Id", "Title", "Location", "ScenePath"]) {
|
for (const prop of <(keyof MissionManifest["Metadata"])[]>[
|
||||||
if (!Object.hasOwn(m.Metadata, prop)) {
|
"Id",
|
||||||
|
"Title",
|
||||||
|
"Location",
|
||||||
|
"ScenePath",
|
||||||
|
]) {
|
||||||
|
if (!m.Metadata[prop]) {
|
||||||
log(LogLevel.ERROR, `Contract missing property Metadata.${prop}!`)
|
log(LogLevel.ERROR, `Contract missing property Metadata.${prop}!`)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const prop of ["Objectives", "Bricks"]) {
|
for (const prop of <(keyof MissionManifest["Data"])[]>[
|
||||||
|
"Objectives",
|
||||||
|
"Bricks",
|
||||||
|
]) {
|
||||||
if (!Object.hasOwn(m.Data, prop)) {
|
if (!Object.hasOwn(m.Data, prop)) {
|
||||||
log(LogLevel.ERROR, `Contract missing property Data.${prop}!`)
|
log(LogLevel.ERROR, `Contract missing property Data.${prop}!`)
|
||||||
return false
|
return false
|
||||||
@ -365,11 +375,11 @@ export class Controller {
|
|||||||
*/
|
*/
|
||||||
public fetchedContracts: Map<string, MissionManifest> = new Map()
|
public fetchedContracts: Map<string, MissionManifest> = new Map()
|
||||||
|
|
||||||
public challengeService: ChallengeService
|
public challengeService!: ChallengeService
|
||||||
public masteryService: MasteryService
|
public masteryService!: MasteryService
|
||||||
escalationMappings: Map<string, Record<string, string>> = new Map()
|
escalationMappings: Map<string, Record<string, string>> = new Map()
|
||||||
public progressionService: ProgressionService
|
public progressionService!: ProgressionService
|
||||||
public smf: SMFSupport
|
public smf!: SMFSupport
|
||||||
private _pubIdToContractId: Map<string, string> = new Map()
|
private _pubIdToContractId: Map<string, string> = new Map()
|
||||||
/** Internal elusive target contracts - only accessible during bootstrap. */
|
/** Internal elusive target contracts - only accessible during bootstrap. */
|
||||||
private _internalElusives: MissionManifest[] | undefined
|
private _internalElusives: MissionManifest[] | undefined
|
||||||
@ -444,12 +454,8 @@ export class Controller {
|
|||||||
this.hooks.challengesLoaded.call()
|
this.hooks.challengesLoaded.call()
|
||||||
this.hooks.masteryDataLoaded.call()
|
this.hooks.masteryDataLoaded.call()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log(
|
log(LogLevel.ERROR, `Fatal error with challenge bootstrap`, "boot")
|
||||||
LogLevel.ERROR,
|
log(LogLevel.ERROR, e)
|
||||||
`Fatal error with challenge bootstrap: ${e}`,
|
|
||||||
"boot",
|
|
||||||
)
|
|
||||||
log(LogLevel.ERROR, e.stack)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -461,6 +467,11 @@ export class Controller {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
contract.Metadata.GroupDefinition,
|
||||||
|
"arcade contract has no group definition",
|
||||||
|
)
|
||||||
|
|
||||||
for (const lId of contract.Metadata.GroupDefinition.Order) {
|
for (const lId of contract.Metadata.GroupDefinition.Order) {
|
||||||
const level = this.resolveContract(lId, false)
|
const level = this.resolveContract(lId, false)
|
||||||
|
|
||||||
@ -481,9 +492,10 @@ export class Controller {
|
|||||||
)
|
)
|
||||||
|
|
||||||
for (const location of this.locationsWithETA) {
|
for (const location of this.locationsWithETA) {
|
||||||
this.parentsWithETA.add(
|
const pl = locations.children[location].Properties.ParentLocation
|
||||||
locations.children[location].Properties.ParentLocation,
|
assert.ok(pl, "no parent location")
|
||||||
)
|
|
||||||
|
this.parentsWithETA.add(pl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -509,14 +521,7 @@ export class Controller {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.contracts.has(this._pubIdToContractId.get(pubId)!)) {
|
return this.contracts.get(this._pubIdToContractId.get(pubId)!)
|
||||||
return (
|
|
||||||
this.contracts.get(this._pubIdToContractId.get(pubId)!) ||
|
|
||||||
undefined
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -546,6 +551,10 @@ export class Controller {
|
|||||||
|
|
||||||
private getGroupContract(json: MissionManifest) {
|
private getGroupContract(json: MissionManifest) {
|
||||||
if (escalationTypes.includes(json.Metadata.Type)) {
|
if (escalationTypes.includes(json.Metadata.Type)) {
|
||||||
|
if (!json.Metadata.InGroup) {
|
||||||
|
return json
|
||||||
|
}
|
||||||
|
|
||||||
return this.resolveContract(json.Metadata.InGroup) ?? json
|
return this.resolveContract(json.Metadata.InGroup) ?? json
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -565,7 +574,7 @@ export class Controller {
|
|||||||
* @returns The mission manifest object, or undefined if it wasn't found.
|
* @returns The mission manifest object, or undefined if it wasn't found.
|
||||||
*/
|
*/
|
||||||
public resolveContract(
|
public resolveContract(
|
||||||
id: string,
|
id: string | undefined,
|
||||||
getGroup = false,
|
getGroup = false,
|
||||||
): MissionManifest | undefined {
|
): MissionManifest | undefined {
|
||||||
if (!id) {
|
if (!id) {
|
||||||
@ -650,11 +659,16 @@ export class Controller {
|
|||||||
this.addMission(groupContract)
|
this.addMission(groupContract)
|
||||||
fixedLevels.forEach((level) => this.addMission(level))
|
fixedLevels.forEach((level) => this.addMission(level))
|
||||||
|
|
||||||
this.missionsInLocations.escalations[locationId] ??= []
|
type K = keyof typeof this.missionsInLocations.escalations
|
||||||
|
|
||||||
this.missionsInLocations.escalations[locationId].push(
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
groupContract.Metadata.Id,
|
this.missionsInLocations.escalations[locationId as K] ??= <any>[]
|
||||||
)
|
|
||||||
|
const a = this.missionsInLocations.escalations[
|
||||||
|
locationId as K
|
||||||
|
] as string[]
|
||||||
|
|
||||||
|
a.push(groupContract.Metadata.Id)
|
||||||
|
|
||||||
this.scanForGroups()
|
this.scanForGroups()
|
||||||
}
|
}
|
||||||
@ -758,7 +772,6 @@ export class Controller {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log(LogLevel.ERROR, `Failed to load contract ${i}!`)
|
log(LogLevel.ERROR, `Failed to load contract ${i}!`)
|
||||||
log(LogLevel.ERROR, e.stack)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -1028,7 +1041,6 @@ export class Controller {
|
|||||||
let theExports
|
let theExports
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// eslint-disable-next-line prefer-const
|
|
||||||
theExports = new Script(pluginContents, {
|
theExports = new Script(pluginContents, {
|
||||||
filename: pluginPath,
|
filename: pluginPath,
|
||||||
}).runInContext(context)
|
}).runInContext(context)
|
||||||
@ -1038,7 +1050,6 @@ export class Controller {
|
|||||||
`Error while attempting to queue plugin ${pluginName} for loading!`,
|
`Error while attempting to queue plugin ${pluginName} for loading!`,
|
||||||
)
|
)
|
||||||
log(LogLevel.ERROR, e)
|
log(LogLevel.ERROR, e)
|
||||||
log(LogLevel.ERROR, e.stack)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1057,7 +1068,6 @@ export class Controller {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
log(LogLevel.ERROR, `Error while evaluating plugin ${pluginName}!`)
|
log(LogLevel.ERROR, `Error while evaluating plugin ${pluginName}!`)
|
||||||
log(LogLevel.ERROR, e)
|
log(LogLevel.ERROR, e)
|
||||||
log(LogLevel.ERROR, e.stack)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1185,7 +1195,7 @@ export function contractIdToHitObject(
|
|||||||
"LocationsData",
|
"LocationsData",
|
||||||
gameVersion,
|
gameVersion,
|
||||||
false,
|
false,
|
||||||
).parents[subLocation?.Properties?.ParentLocation]
|
).parents[subLocation?.Properties?.ParentLocation || ""]
|
||||||
|
|
||||||
// failed to find the location, must be from a newer game
|
// failed to find the location, must be from a newer game
|
||||||
if (!subLocation && ["h1", "h2", "scpc"].includes(gameVersion)) {
|
if (!subLocation && ["h1", "h2", "scpc"].includes(gameVersion)) {
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
import { readFile, writeFile } from "atomically"
|
import { readFile, writeFile } from "atomically"
|
||||||
import { join } from "path"
|
import { join } from "path"
|
||||||
import type { ContractSession, GameVersion, UserProfile } from "./types/types"
|
import type { ContractSession, GameVersion, UserProfile } from "./types/types"
|
||||||
import { serializeSession, deserializeSession } from "./sessionSerialization"
|
import { serializeSession, deserializeSession } from "./contracts/sessions"
|
||||||
import { castUserProfile } from "./utils"
|
import { castUserProfile } from "./utils"
|
||||||
import { log, LogLevel } from "./loggingInterop"
|
import { log, LogLevel } from "./loggingInterop"
|
||||||
import { unlink, readdir } from "fs/promises"
|
import { unlink, readdir } from "fs/promises"
|
||||||
|
@ -16,6 +16,10 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// Vendor code - does not need type-checking.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
import EventEmitter from "events"
|
import EventEmitter from "events"
|
||||||
import { clearTimeout, setTimeout } from "timers"
|
import { clearTimeout, setTimeout } from "timers"
|
||||||
import { IPCTransport } from "./ipc"
|
import { IPCTransport } from "./ipc"
|
||||||
|
@ -16,6 +16,10 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// Vendor code - does not need type-checking.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
import net from "net"
|
import net from "net"
|
||||||
import EventEmitter from "events"
|
import EventEmitter from "events"
|
||||||
import axios from "axios"
|
import axios from "axios"
|
||||||
|
@ -67,7 +67,7 @@ export class IOIStrategy extends EntitlementStrategy {
|
|||||||
constructor(gameVersion: GameVersion, private readonly issuerId: string) {
|
constructor(gameVersion: GameVersion, private readonly issuerId: string) {
|
||||||
super()
|
super()
|
||||||
this.issuerId = issuerId
|
this.issuerId = issuerId
|
||||||
this._remoteService = getRemoteService(gameVersion)
|
this._remoteService = getRemoteService(gameVersion)!
|
||||||
}
|
}
|
||||||
|
|
||||||
override async get(userId: string) {
|
override async get(userId: string) {
|
||||||
|
@ -174,8 +174,8 @@ export function setupScoring(
|
|||||||
|
|
||||||
if (name === "scoring") {
|
if (name === "scoring") {
|
||||||
const definition: ManifestScoringDefinition = deepmerge(
|
const definition: ManifestScoringDefinition = deepmerge(
|
||||||
...module.ScoringDefinitions,
|
...(module.ScoringDefinitions || []),
|
||||||
)
|
) as unknown as ManifestScoringDefinition
|
||||||
|
|
||||||
let state = "Start"
|
let state = "Start"
|
||||||
let context = definition.Context
|
let context = definition.Context
|
||||||
@ -200,15 +200,21 @@ export function setupScoring(
|
|||||||
context = immediate.context
|
context = immediate.context
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @ts-expect-error Type issue
|
||||||
scoring.Definition = definition
|
scoring.Definition = definition
|
||||||
|
// @ts-expect-error Type issue
|
||||||
scoring.Context = context
|
scoring.Context = context
|
||||||
|
// @ts-expect-error Type issue
|
||||||
scoring.State = state
|
scoring.State = state
|
||||||
} else {
|
} else {
|
||||||
|
// @ts-expect-error Type issue
|
||||||
scoring.Settings[name] = module
|
scoring.Settings[name] = module
|
||||||
|
// @ts-expect-error Type issue
|
||||||
delete scoring.Settings[name]["Type"]
|
delete scoring.Settings[name]["Type"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @ts-expect-error Type issue
|
||||||
session.scoring = scoring
|
session.scoring = scoring
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -373,13 +379,13 @@ export function newSession(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type SSE3Response = {
|
export type SSE3Response = {
|
||||||
SavedTokens: string[]
|
SavedTokens: string[] | null
|
||||||
NewEvents: ServerToClientEvent[]
|
NewEvents: ServerToClientEvent[] | null
|
||||||
NextPoll: number
|
NextPoll: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SSE4Response = SSE3Response & {
|
export type SSE4Response = SSE3Response & {
|
||||||
PushMessages: string[]
|
PushMessages: string[] | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export function saveAndSyncEvents(
|
export function saveAndSyncEvents(
|
||||||
@ -410,7 +416,7 @@ export function saveAndSyncEvents(
|
|||||||
let pushMessages: string[] | undefined
|
let pushMessages: string[] | undefined
|
||||||
|
|
||||||
if ((userPushQueue = pushMessageQueue.get(userId))) {
|
if ((userPushQueue = pushMessageQueue.get(userId))) {
|
||||||
userPushQueue = userPushQueue.filter((item) => item.time > lastPushDt)
|
userPushQueue = userPushQueue.filter((item) => item.time > lastPushDt!)
|
||||||
pushMessageQueue.set(userId, userPushQueue)
|
pushMessageQueue.set(userId, userPushQueue)
|
||||||
|
|
||||||
pushMessages = Array.from(userPushQueue, (item) => item.message)
|
pushMessages = Array.from(userPushQueue, (item) => item.message)
|
||||||
@ -430,20 +436,21 @@ export function saveAndSyncEvents(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SSE3Body = {
|
||||||
|
lastEventTicks: number | string
|
||||||
|
userId: string
|
||||||
|
values: ClientToServerEvent[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type SSE4Body = SSE3Body & {
|
||||||
|
lastPushDt: number | string
|
||||||
|
}
|
||||||
|
|
||||||
eventRouter.post(
|
eventRouter.post(
|
||||||
"/SaveAndSynchronizeEvents3",
|
"/SaveAndSynchronizeEvents3",
|
||||||
jsonMiddleware({ limit: "10Mb" }),
|
jsonMiddleware({ limit: "10Mb" }),
|
||||||
(
|
// @ts-expect-error Request has jwt props.
|
||||||
req: RequestWithJwt<
|
(req: RequestWithJwt<unknown, SSE3Body>, res) => {
|
||||||
unknown,
|
|
||||||
{
|
|
||||||
lastEventTicks: number | string
|
|
||||||
userId: string
|
|
||||||
values: ClientToServerEvent[]
|
|
||||||
}
|
|
||||||
>,
|
|
||||||
res,
|
|
||||||
) => {
|
|
||||||
if (req.body.userId !== req.jwt.unique_name) {
|
if (req.body.userId !== req.jwt.unique_name) {
|
||||||
res.status(403).send() // Trying to save events for other user
|
res.status(403).send() // Trying to save events for other user
|
||||||
return
|
return
|
||||||
@ -469,18 +476,8 @@ eventRouter.post(
|
|||||||
eventRouter.post(
|
eventRouter.post(
|
||||||
"/SaveAndSynchronizeEvents4",
|
"/SaveAndSynchronizeEvents4",
|
||||||
jsonMiddleware({ limit: "10Mb" }),
|
jsonMiddleware({ limit: "10Mb" }),
|
||||||
(
|
// @ts-expect-error Request has jwt props.
|
||||||
req: RequestWithJwt<
|
(req: RequestWithJwt<unknown, SSE4Body>, res) => {
|
||||||
unknown,
|
|
||||||
{
|
|
||||||
lastPushDt: number | string
|
|
||||||
lastEventTicks: number | string
|
|
||||||
userId: string
|
|
||||||
values: ClientToServerEvent[]
|
|
||||||
}
|
|
||||||
>,
|
|
||||||
res,
|
|
||||||
) => {
|
|
||||||
if (req.body.userId !== req.jwt.unique_name) {
|
if (req.body.userId !== req.jwt.unique_name) {
|
||||||
res.status(403).send() // Trying to save events for other user
|
res.status(403).send() // Trying to save events for other user
|
||||||
return
|
return
|
||||||
@ -507,6 +504,7 @@ eventRouter.post(
|
|||||||
eventRouter.post(
|
eventRouter.post(
|
||||||
"/SaveEvents2",
|
"/SaveEvents2",
|
||||||
jsonMiddleware({ limit: "10Mb" }),
|
jsonMiddleware({ limit: "10Mb" }),
|
||||||
|
// @ts-expect-error Request has jwt props.
|
||||||
(req: RequestWithJwt, res) => {
|
(req: RequestWithJwt, res) => {
|
||||||
if (req.jwt.unique_name !== req.body.userId) {
|
if (req.jwt.unique_name !== req.body.userId) {
|
||||||
res.status(403).send() // Trying to save events for other user
|
res.status(403).send() // Trying to save events for other user
|
||||||
@ -595,7 +593,7 @@ function contractFailed(
|
|||||||
) {
|
) {
|
||||||
if (session.completedObjectives.size === 0) break arcadeFail
|
if (session.completedObjectives.size === 0) break arcadeFail
|
||||||
|
|
||||||
for (const obj of json.Data.Objectives) {
|
for (const obj of json.Data.Objectives || []) {
|
||||||
if (
|
if (
|
||||||
session.completedObjectives.has(obj.Id) &&
|
session.completedObjectives.has(obj.Id) &&
|
||||||
obj.Category === "primary"
|
obj.Category === "primary"
|
||||||
@ -707,7 +705,9 @@ function saveEvents(
|
|||||||
|
|
||||||
const val = handleEvent(
|
const val = handleEvent(
|
||||||
objectiveDefinition as never,
|
objectiveDefinition as never,
|
||||||
objectiveContext,
|
// SMP sucks. Sorry, not sorry.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
objectiveContext as any,
|
||||||
event.Value,
|
event.Value,
|
||||||
{
|
{
|
||||||
eventName: event.Name,
|
eventName: event.Name,
|
||||||
@ -734,7 +734,6 @@ function saveEvents(
|
|||||||
"An error occurred while tracing C2S events, please report this!",
|
"An error occurred while tracing C2S events, please report this!",
|
||||||
)
|
)
|
||||||
log(LogLevel.ERROR, e)
|
log(LogLevel.ERROR, e)
|
||||||
log(LogLevel.ERROR, e.stack)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -744,7 +743,9 @@ function saveEvents(
|
|||||||
|
|
||||||
const val = handleEvent(
|
const val = handleEvent(
|
||||||
session.scoring.Definition as never,
|
session.scoring.Definition as never,
|
||||||
scoringContext,
|
// SMP sucks. Sorry, not sorry.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
scoringContext as any,
|
||||||
event.Value,
|
event.Value,
|
||||||
{
|
{
|
||||||
eventName: event.Name,
|
eventName: event.Name,
|
||||||
@ -762,7 +763,10 @@ function saveEvents(
|
|||||||
|
|
||||||
controller.challengeService.onContractEvent(event, session)
|
controller.challengeService.onContractEvent(event, session)
|
||||||
|
|
||||||
if (event.Name.startsWith("ScoringScreenEndState_")) {
|
if (
|
||||||
|
event.Name.startsWith("ScoringScreenEndState_") &&
|
||||||
|
session.evergreen
|
||||||
|
) {
|
||||||
session.evergreen.scoringScreenEndState = event.Name
|
session.evergreen.scoringScreenEndState = event.Name
|
||||||
|
|
||||||
processed.push(event.Name)
|
processed.push(event.Name)
|
||||||
@ -1010,7 +1014,10 @@ function saveEvents(
|
|||||||
const areaId = (<AreaDiscoveredC2SEvent>event).Value
|
const areaId = (<AreaDiscoveredC2SEvent>event).Value
|
||||||
.RepositoryId
|
.RepositoryId
|
||||||
|
|
||||||
const challengeId = getConfig("AreaMap", false)[areaId]
|
const challengeId = getConfig<Record<string, string>>(
|
||||||
|
"AreaMap",
|
||||||
|
false,
|
||||||
|
)[areaId]
|
||||||
const progress = userData.Extensions.ChallengeProgression
|
const progress = userData.Extensions.ChallengeProgression
|
||||||
|
|
||||||
log(LogLevel.DEBUG, `Area discovered: ${areaId}`)
|
log(LogLevel.DEBUG, `Area discovered: ${areaId}`)
|
||||||
@ -1044,16 +1051,22 @@ function saveEvents(
|
|||||||
break
|
break
|
||||||
// Evergreen
|
// Evergreen
|
||||||
case "CpdSet":
|
case "CpdSet":
|
||||||
setCpd(
|
if (contract?.Metadata.CpdId) {
|
||||||
event.Value as ContractProgressionData,
|
setCpd(
|
||||||
userId,
|
event.Value as ContractProgressionData,
|
||||||
contract.Metadata.CpdId,
|
userId,
|
||||||
)
|
contract.Metadata.CpdId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
break
|
break
|
||||||
case "Evergreen_Payout_Data":
|
case "Evergreen_Payout_Data":
|
||||||
session.evergreen.payout = (<Evergreen_Payout_DataC2SEvent>(
|
if (session.evergreen) {
|
||||||
event
|
session.evergreen.payout = (<Evergreen_Payout_DataC2SEvent>(
|
||||||
)).Value.Total_Payout
|
event
|
||||||
|
)).Value.Total_Payout
|
||||||
|
}
|
||||||
|
|
||||||
break
|
break
|
||||||
case "MissionFailed_Event":
|
case "MissionFailed_Event":
|
||||||
if (session.evergreen) {
|
if (session.evergreen) {
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "fs"
|
import { existsSync, readFileSync, writeFileSync } from "fs"
|
||||||
import type { Flags } from "./types/types"
|
import type { Flags } from "./types/types"
|
||||||
import { log, LogLevel } from "./loggingInterop"
|
import { log, LogLevel } from "./loggingInterop"
|
||||||
import { parse } from "js-ini"
|
import { parse } from "js-ini"
|
||||||
@ -123,7 +123,6 @@ const defaultFlags: Flags = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const OLD_FLAGS_FILE = "flags.json5"
|
|
||||||
const NEW_FLAGS_FILE = "options.ini"
|
const NEW_FLAGS_FILE = "options.ini"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -152,7 +151,10 @@ const makeFlagsIni = (
|
|||||||
Object.keys(defaultFlags)
|
Object.keys(defaultFlags)
|
||||||
.map((flagId) => {
|
.map((flagId) => {
|
||||||
return `; ${defaultFlags[flagId].desc}
|
return `; ${defaultFlags[flagId].desc}
|
||||||
${flagId} = ${_flags[flagId]}`
|
${flagId} = ${
|
||||||
|
// @ts-expect-error You know what, I don't care
|
||||||
|
_flags[flagId]
|
||||||
|
}`
|
||||||
})
|
})
|
||||||
.join("\n\n")
|
.join("\n\n")
|
||||||
|
|
||||||
@ -160,24 +162,11 @@ ${flagId} = ${_flags[flagId]}`
|
|||||||
* Loads all flags.
|
* Loads all flags.
|
||||||
*/
|
*/
|
||||||
export function loadFlags(): void {
|
export function loadFlags(): void {
|
||||||
// somebody please, clean this method up, I hate it
|
|
||||||
if (existsSync(OLD_FLAGS_FILE)) {
|
|
||||||
log(
|
|
||||||
LogLevel.WARN,
|
|
||||||
"The flags file (flags.json5) has been revamped in the latest Peacock version, and we had to remove your settings.",
|
|
||||||
)
|
|
||||||
log(
|
|
||||||
LogLevel.INFO,
|
|
||||||
"You can take a look at the new options.ini file, which includes descriptions and more!",
|
|
||||||
)
|
|
||||||
|
|
||||||
unlinkSync(OLD_FLAGS_FILE)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!existsSync(NEW_FLAGS_FILE)) {
|
if (!existsSync(NEW_FLAGS_FILE)) {
|
||||||
const allTheFlags = {}
|
const allTheFlags = {}
|
||||||
|
|
||||||
Object.keys(defaultFlags).forEach((f) => {
|
Object.keys(defaultFlags).forEach((f) => {
|
||||||
|
// @ts-expect-error You know what, I don't care
|
||||||
allTheFlags[f] = defaultFlags[f].default
|
allTheFlags[f] = defaultFlags[f].default
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -37,13 +37,11 @@ import * as platformEntitlements from "./platformEntitlements"
|
|||||||
import * as playStyles from "./playStyles"
|
import * as playStyles from "./playStyles"
|
||||||
import * as profileHandler from "./profileHandler"
|
import * as profileHandler from "./profileHandler"
|
||||||
import * as scoreHandler from "./scoreHandler"
|
import * as scoreHandler from "./scoreHandler"
|
||||||
import * as sessionSerialization from "./sessionSerialization"
|
|
||||||
import * as smfSupport from "./smfSupport"
|
import * as smfSupport from "./smfSupport"
|
||||||
import * as utils from "./utils"
|
import * as utils from "./utils"
|
||||||
import * as webFeatures from "./webFeatures"
|
import * as webFeatures from "./webFeatures"
|
||||||
import * as legacyContractHandler from "./2016/legacyContractHandler"
|
import * as legacyContractHandler from "./2016/legacyContractHandler"
|
||||||
import * as legacyMenuData from "./2016/legacyMenuData"
|
import * as legacyMenuData from "./2016/legacyMenuData"
|
||||||
import * as legacyMenuSystem from "./2016/legacyMenuSystem"
|
|
||||||
import * as legacyProfileRouter from "./2016/legacyProfileRouter"
|
import * as legacyProfileRouter from "./2016/legacyProfileRouter"
|
||||||
import * as challengeHelpers from "./candle/challengeHelpers"
|
import * as challengeHelpers from "./candle/challengeHelpers"
|
||||||
import * as challengeService from "./candle/challengeService"
|
import * as challengeService from "./candle/challengeService"
|
||||||
@ -57,7 +55,7 @@ import * as elusiveTargets from "./contracts/elusiveTargets"
|
|||||||
import * as hitsCategoryService from "./contracts/hitsCategoryService"
|
import * as hitsCategoryService from "./contracts/hitsCategoryService"
|
||||||
import * as leaderboards from "./contracts/leaderboards"
|
import * as leaderboards from "./contracts/leaderboards"
|
||||||
import * as missionsInLocation from "./contracts/missionsInLocation"
|
import * as missionsInLocation from "./contracts/missionsInLocation"
|
||||||
import * as reportRouting from "./contracts/reportRouting"
|
import * as sessions from "./contracts/sessions"
|
||||||
import * as client from "./discord/client"
|
import * as client from "./discord/client"
|
||||||
import * as ipc from "./discord/ipc"
|
import * as ipc from "./discord/ipc"
|
||||||
import * as liveSplitClient from "./livesplit/liveSplitClient"
|
import * as liveSplitClient from "./livesplit/liveSplitClient"
|
||||||
@ -69,6 +67,7 @@ import * as hub from "./menus/hub"
|
|||||||
import * as imageHandler from "./menus/imageHandler"
|
import * as imageHandler from "./menus/imageHandler"
|
||||||
import * as menuSystem from "./menus/menuSystem"
|
import * as menuSystem from "./menus/menuSystem"
|
||||||
import * as planning from "./menus/planning"
|
import * as planning from "./menus/planning"
|
||||||
|
import * as playerProfile from "./menus/playerProfile"
|
||||||
import * as playnext from "./menus/playnext"
|
import * as playnext from "./menus/playnext"
|
||||||
import * as sniper from "./menus/sniper"
|
import * as sniper from "./menus/sniper"
|
||||||
import * as stashpoints from "./menus/stashpoints"
|
import * as stashpoints from "./menus/stashpoints"
|
||||||
@ -125,10 +124,6 @@ export default {
|
|||||||
...profileHandler,
|
...profileHandler,
|
||||||
},
|
},
|
||||||
"@peacockproject/core/scoreHandler": { __esModule: true, ...scoreHandler },
|
"@peacockproject/core/scoreHandler": { __esModule: true, ...scoreHandler },
|
||||||
"@peacockproject/core/sessionSerialization": {
|
|
||||||
__esModule: true,
|
|
||||||
...sessionSerialization,
|
|
||||||
},
|
|
||||||
"@peacockproject/core/smfSupport": { __esModule: true, ...smfSupport },
|
"@peacockproject/core/smfSupport": { __esModule: true, ...smfSupport },
|
||||||
"@peacockproject/core/utils": { __esModule: true, ...utils },
|
"@peacockproject/core/utils": { __esModule: true, ...utils },
|
||||||
"@peacockproject/core/webFeatures": { __esModule: true, ...webFeatures },
|
"@peacockproject/core/webFeatures": { __esModule: true, ...webFeatures },
|
||||||
@ -140,10 +135,6 @@ export default {
|
|||||||
__esModule: true,
|
__esModule: true,
|
||||||
...legacyMenuData,
|
...legacyMenuData,
|
||||||
},
|
},
|
||||||
"@peacockproject/core/2016/legacyMenuSystem": {
|
|
||||||
__esModule: true,
|
|
||||||
...legacyMenuSystem,
|
|
||||||
},
|
|
||||||
"@peacockproject/core/2016/legacyProfileRouter": {
|
"@peacockproject/core/2016/legacyProfileRouter": {
|
||||||
__esModule: true,
|
__esModule: true,
|
||||||
...legacyProfileRouter,
|
...legacyProfileRouter,
|
||||||
@ -193,9 +184,9 @@ export default {
|
|||||||
__esModule: true,
|
__esModule: true,
|
||||||
...missionsInLocation,
|
...missionsInLocation,
|
||||||
},
|
},
|
||||||
"@peacockproject/core/contracts/reportRouting": {
|
"@peacockproject/core/contracts/sessions": {
|
||||||
__esModule: true,
|
__esModule: true,
|
||||||
...reportRouting,
|
...sessions,
|
||||||
},
|
},
|
||||||
"@peacockproject/core/discord/client": { __esModule: true, ...client },
|
"@peacockproject/core/discord/client": { __esModule: true, ...client },
|
||||||
"@peacockproject/core/discord/ipc": { __esModule: true, ...ipc },
|
"@peacockproject/core/discord/ipc": { __esModule: true, ...ipc },
|
||||||
@ -226,6 +217,10 @@ export default {
|
|||||||
...menuSystem,
|
...menuSystem,
|
||||||
},
|
},
|
||||||
"@peacockproject/core/menus/planning": { __esModule: true, ...planning },
|
"@peacockproject/core/menus/planning": { __esModule: true, ...planning },
|
||||||
|
"@peacockproject/core/menus/playerProfile": {
|
||||||
|
__esModule: true,
|
||||||
|
...playerProfile,
|
||||||
|
},
|
||||||
"@peacockproject/core/menus/playnext": { __esModule: true, ...playnext },
|
"@peacockproject/core/menus/playnext": { __esModule: true, ...playnext },
|
||||||
"@peacockproject/core/menus/sniper": { __esModule: true, ...sniper },
|
"@peacockproject/core/menus/sniper": { __esModule: true, ...sniper },
|
||||||
"@peacockproject/core/menus/stashpoints": {
|
"@peacockproject/core/menus/stashpoints": {
|
||||||
|
@ -89,7 +89,7 @@ export interface Intercept<Params, Return> {
|
|||||||
* @param context The context object. Can be modified.
|
* @param context The context object. Can be modified.
|
||||||
* @param params The parameters that the taps will get. Can be modified.
|
* @param params The parameters that the taps will get. Can be modified.
|
||||||
*/
|
*/
|
||||||
call(context, ...params: AsArray<Params>): void
|
call(context: unknown, ...params: AsArray<Params>): void | Promise<void>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A function called when the hook is tapped. Note that it will not be called when an interceptor is registered, since that doesn't count as a tap.
|
* A function called when the hook is tapped. Note that it will not be called when an interceptor is registered, since that doesn't count as a tap.
|
||||||
@ -108,8 +108,8 @@ export interface Intercept<Params, Return> {
|
|||||||
* @see AsyncSeriesHook
|
* @see AsyncSeriesHook
|
||||||
*/
|
*/
|
||||||
export abstract class BaseImpl<Params, Return = void> {
|
export abstract class BaseImpl<Params, Return = void> {
|
||||||
protected _intercepts: Intercept<Params, Return>[]
|
protected _intercepts!: Intercept<Params, Return>[]
|
||||||
protected _taps: Tap<Params, Return>[]
|
protected _taps!: Tap<Params, Return>[]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register an interceptor.
|
* Register an interceptor.
|
||||||
|
@ -16,8 +16,6 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// noinspection RequiredAttributes
|
|
||||||
|
|
||||||
// load as soon as possible to prevent dependency issues
|
// load as soon as possible to prevent dependency issues
|
||||||
import "./generatedPeacockRequireTable"
|
import "./generatedPeacockRequireTable"
|
||||||
|
|
||||||
@ -26,7 +24,6 @@ import { getFlag, loadFlags } from "./flags"
|
|||||||
|
|
||||||
loadFlags()
|
loadFlags()
|
||||||
|
|
||||||
import { setFlagsFromString } from "v8"
|
|
||||||
import { program } from "commander"
|
import { program } from "commander"
|
||||||
import express, { Request, Router } from "express"
|
import express, { Request, Router } from "express"
|
||||||
import http from "http"
|
import http from "http"
|
||||||
@ -41,7 +38,12 @@ import {
|
|||||||
ServerVer,
|
ServerVer,
|
||||||
} from "./utils"
|
} from "./utils"
|
||||||
import { getConfig } from "./configSwizzleManager"
|
import { getConfig } from "./configSwizzleManager"
|
||||||
import { handleOauthToken } from "./oauthToken"
|
import {
|
||||||
|
error400,
|
||||||
|
error406,
|
||||||
|
handleOAuthToken,
|
||||||
|
OAuthTokenBody,
|
||||||
|
} from "./oauthToken"
|
||||||
import type {
|
import type {
|
||||||
RequestWithJwt,
|
RequestWithJwt,
|
||||||
S2CEventWithTimestamp,
|
S2CEventWithTimestamp,
|
||||||
@ -61,7 +63,6 @@ import { contractRoutingRouter } from "./contracts/contractRouting"
|
|||||||
import { profileRouter } from "./profileHandler"
|
import { profileRouter } from "./profileHandler"
|
||||||
import { menuDataRouter } from "./menuData"
|
import { menuDataRouter } from "./menuData"
|
||||||
import { menuSystemPreRouter, menuSystemRouter } from "./menus/menuSystem"
|
import { menuSystemPreRouter, menuSystemRouter } from "./menus/menuSystem"
|
||||||
import { legacyMenuSystemRouter } from "./2016/legacyMenuSystem"
|
|
||||||
import { _theLastYardbirdScpc, controller } from "./controller"
|
import { _theLastYardbirdScpc, controller } from "./controller"
|
||||||
import {
|
import {
|
||||||
STEAM_NAMESPACE_2016,
|
STEAM_NAMESPACE_2016,
|
||||||
@ -88,10 +89,8 @@ import { multiplayerMenuDataRouter } from "./multiplayer/multiplayerMenuData"
|
|||||||
import { pack, unpack } from "msgpackr"
|
import { pack, unpack } from "msgpackr"
|
||||||
import { liveSplitManager } from "./livesplit/liveSplitManager"
|
import { liveSplitManager } from "./livesplit/liveSplitManager"
|
||||||
import { cheapLoadUserData } from "./databaseHandler"
|
import { cheapLoadUserData } from "./databaseHandler"
|
||||||
import { reportRouter } from "./contracts/reportRouting"
|
|
||||||
|
|
||||||
// welcome to the bleeding edge
|
loadFlags()
|
||||||
setFlagsFromString("--harmony")
|
|
||||||
|
|
||||||
const host = process.env.HOST || "0.0.0.0"
|
const host = process.env.HOST || "0.0.0.0"
|
||||||
const port = process.env.PORT || 80
|
const port = process.env.PORT || 80
|
||||||
@ -145,12 +144,13 @@ app.get("/", (_: Request, res) => {
|
|||||||
res.send(
|
res.send(
|
||||||
'<html lang="en">PEACOCK_DEV active, please run "yarn webui start" to start the web UI on port 3000 and access it there.</html>',
|
'<html lang="en">PEACOCK_DEV active, please run "yarn webui start" to start the web UI on port 3000 and access it there.</html>',
|
||||||
)
|
)
|
||||||
} else {
|
return
|
||||||
const data = readFileSync("webui/dist/index.html").toString()
|
|
||||||
|
|
||||||
res.contentType("text/html")
|
|
||||||
res.send(data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const data = readFileSync("webui/dist/index.html").toString()
|
||||||
|
|
||||||
|
res.contentType("text/html")
|
||||||
|
res.send(data)
|
||||||
})
|
})
|
||||||
|
|
||||||
serveStatic.mime.define({ "application/javascript": ["js"] })
|
serveStatic.mime.define({ "application/javascript": ["js"] })
|
||||||
@ -169,6 +169,7 @@ if (getFlag("loadoutSaving") === "PROFILES") {
|
|||||||
|
|
||||||
app.get(
|
app.get(
|
||||||
"/config/:audience/:serverVersion(\\d+_\\d+_\\d+)",
|
"/config/:audience/:serverVersion(\\d+_\\d+_\\d+)",
|
||||||
|
// @ts-expect-error Has jwt props.
|
||||||
(req: RequestWithJwt<{ issuer: string }>, res) => {
|
(req: RequestWithJwt<{ issuer: string }>, res) => {
|
||||||
const proto = req.protocol
|
const proto = req.protocol
|
||||||
const config = getConfig(
|
const config = getConfig(
|
||||||
@ -177,11 +178,13 @@ app.get(
|
|||||||
) as ServerConnectionConfig
|
) as ServerConnectionConfig
|
||||||
const serverhost = req.get("Host")
|
const serverhost = req.get("Host")
|
||||||
|
|
||||||
config.Versions[0].GAME_VER = req.params.serverVersion.startsWith("8")
|
config.Versions[0].GAME_VER = "6.74.0"
|
||||||
? `${ServerVer._Major}.${ServerVer._Minor}.${ServerVer._Build}`
|
|
||||||
: req.params.serverVersion.startsWith("7")
|
if (req.params.serverVersion.startsWith("8")) {
|
||||||
? "7.17.0"
|
req.params.serverVersion = `${ServerVer._Major}.${ServerVer._Minor}.${ServerVer._Build}`
|
||||||
: "6.74.0"
|
} else if (req.params.serverVersion.startsWith("7")) {
|
||||||
|
req.params.serverVersion = "7.17.0"
|
||||||
|
}
|
||||||
|
|
||||||
if (req.params.serverVersion.startsWith("8")) {
|
if (req.params.serverVersion.startsWith("8")) {
|
||||||
config.Versions[0].SERVER_VER.GlobalAuthentication.RequestedAudience =
|
config.Versions[0].SERVER_VER.GlobalAuthentication.RequestedAudience =
|
||||||
@ -229,7 +232,7 @@ app.get(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
app.get("/files/privacypolicy/hm3/privacypolicy_*.json", (req, res) => {
|
app.get("/files/privacypolicy/hm3/privacypolicy_*.json", (_, res) => {
|
||||||
res.set("Content-Type", "application/octet-stream")
|
res.set("Content-Type", "application/octet-stream")
|
||||||
res.set("x-ms-meta-version", "20181001")
|
res.set("x-ms-meta-version", "20181001")
|
||||||
res.send(getConfig("PrivacyPolicy", false))
|
res.send(getConfig("PrivacyPolicy", false))
|
||||||
@ -238,6 +241,7 @@ app.get("/files/privacypolicy/hm3/privacypolicy_*.json", (req, res) => {
|
|||||||
app.post(
|
app.post(
|
||||||
"/api/metrics/*",
|
"/api/metrics/*",
|
||||||
jsonMiddleware({ limit: "10Mb" }),
|
jsonMiddleware({ limit: "10Mb" }),
|
||||||
|
// @ts-expect-error jwt props.
|
||||||
(req: RequestWithJwt<never, S2CEventWithTimestamp[]>, res) => {
|
(req: RequestWithJwt<never, S2CEventWithTimestamp[]>, res) => {
|
||||||
for (const event of req.body) {
|
for (const event of req.body) {
|
||||||
controller.hooks.newMetricsEvent.call(event, req)
|
controller.hooks.newMetricsEvent.call(event, req)
|
||||||
@ -247,8 +251,26 @@ app.post(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
app.post("/oauth/token", urlencoded(), (req: RequestWithJwt, res) =>
|
app.post(
|
||||||
handleOauthToken(req, res),
|
"/oauth/token",
|
||||||
|
urlencoded(),
|
||||||
|
// @ts-expect-error jwt props.
|
||||||
|
(req: RequestWithJwt<never, OAuthTokenBody>, res) => {
|
||||||
|
handleOAuthToken(req)
|
||||||
|
.then((token) => {
|
||||||
|
if (token === error400) {
|
||||||
|
return res.status(400).send()
|
||||||
|
} else if (token === error406) {
|
||||||
|
return res.status(406).send()
|
||||||
|
} else {
|
||||||
|
return res.json(token)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
log(LogLevel.ERROR, err.message)
|
||||||
|
res.status(500).send()
|
||||||
|
})
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
app.get("/files/onlineconfig.json", (_, res) => {
|
app.get("/files/onlineconfig.json", (_, res) => {
|
||||||
@ -263,15 +285,16 @@ app.use(
|
|||||||
Router()
|
Router()
|
||||||
.use(
|
.use(
|
||||||
"/resources-:serverVersion(\\d+-\\d+)/",
|
"/resources-:serverVersion(\\d+-\\d+)/",
|
||||||
(req: RequestWithJwt, res, next) => {
|
// @ts-expect-error Has jwt props.
|
||||||
|
(req: RequestWithJwt, _, next) => {
|
||||||
req.serverVersion = req.params.serverVersion
|
req.serverVersion = req.params.serverVersion
|
||||||
req.gameVersion = req.serverVersion.startsWith("8")
|
req.gameVersion = "h1"
|
||||||
? "h3"
|
|
||||||
: req.serverVersion.startsWith("7")
|
if (req.serverVersion.startsWith("8")) {
|
||||||
? // prettier-ignore
|
req.gameVersion = "h3"
|
||||||
"h2"
|
} else if (req.serverVersion.startsWith("7")) {
|
||||||
: // prettier-ignore
|
req.gameVersion = "h2"
|
||||||
"h1"
|
}
|
||||||
|
|
||||||
if (req.serverVersion === "7.3.0") {
|
if (req.serverVersion === "7.3.0") {
|
||||||
req.gameVersion = "scpc"
|
req.gameVersion = "scpc"
|
||||||
@ -281,6 +304,7 @@ app.use(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
// we're fine with skipping to the next router if we don't have auth
|
// we're fine with skipping to the next router if we don't have auth
|
||||||
|
// @ts-expect-error Has jwt props.
|
||||||
.use(extractToken, (req: RequestWithJwt, res, next) => {
|
.use(extractToken, (req: RequestWithJwt, res, next) => {
|
||||||
switch (req.jwt?.pis) {
|
switch (req.jwt?.pis) {
|
||||||
case "egp_io_interactive_hitman_the_complete_first_season":
|
case "egp_io_interactive_hitman_the_complete_first_season":
|
||||||
@ -300,11 +324,13 @@ app.use(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
req.gameVersion = req.serverVersion.startsWith("8")
|
req.gameVersion = "h1"
|
||||||
? "h3"
|
|
||||||
: req.serverVersion.startsWith("7")
|
if (req.serverVersion.startsWith("8")) {
|
||||||
? "h2"
|
req.gameVersion = "h3"
|
||||||
: "h1"
|
} else if (req.serverVersion.startsWith("7")) {
|
||||||
|
req.gameVersion = "h2"
|
||||||
|
}
|
||||||
|
|
||||||
if (req.jwt?.aud === "scpc-prod") {
|
if (req.jwt?.aud === "scpc-prod") {
|
||||||
req.gameVersion = "scpc"
|
req.gameVersion = "scpc"
|
||||||
@ -316,6 +342,7 @@ app.use(
|
|||||||
|
|
||||||
app.get(
|
app.get(
|
||||||
"/profiles/page//dashboard//Dashboard_Category_Sniper_Singleplayer/00000000-0000-0000-0000-000000000015/Contract/ff9f46cf-00bd-4c12-b887-eac491c3a96d",
|
"/profiles/page//dashboard//Dashboard_Category_Sniper_Singleplayer/00000000-0000-0000-0000-000000000015/Contract/ff9f46cf-00bd-4c12-b887-eac491c3a96d",
|
||||||
|
// @ts-expect-error jwt props.
|
||||||
(req: RequestWithJwt, res) => {
|
(req: RequestWithJwt, res) => {
|
||||||
res.json({
|
res.json({
|
||||||
template: getConfig("FrankensteinMmSpTemplate", false),
|
template: getConfig("FrankensteinMmSpTemplate", false),
|
||||||
@ -339,6 +366,7 @@ app.get(
|
|||||||
// We handle this for now, but it's not used. For the future though.
|
// We handle this for now, but it's not used. For the future though.
|
||||||
app.get(
|
app.get(
|
||||||
"/profiles/page//dashboard//Dashboard_Category_Sniper_Multiplayer/00000000-0000-0000-0000-000000000015/Contract/ff9f46cf-00bd-4c12-b887-eac491c3a96d",
|
"/profiles/page//dashboard//Dashboard_Category_Sniper_Multiplayer/00000000-0000-0000-0000-000000000015/Contract/ff9f46cf-00bd-4c12-b887-eac491c3a96d",
|
||||||
|
// @ts-expect-error jwt props.
|
||||||
(req: RequestWithJwt, res) => {
|
(req: RequestWithJwt, res) => {
|
||||||
const template = getConfig("FrankensteinMmMpTemplate", false)
|
const template = getConfig("FrankensteinMmMpTemplate", false)
|
||||||
|
|
||||||
@ -371,6 +399,7 @@ app.get(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (PEACOCK_DEV) {
|
if (PEACOCK_DEV) {
|
||||||
|
// @ts-expect-error Has jwt props.
|
||||||
app.use(async (req: RequestWithJwt, _res, next): Promise<void> => {
|
app.use(async (req: RequestWithJwt, _res, next): Promise<void> => {
|
||||||
if (!req.jwt) {
|
if (!req.jwt) {
|
||||||
next()
|
next()
|
||||||
@ -397,6 +426,7 @@ function generateBlobConfig(req: RequestWithJwt) {
|
|||||||
|
|
||||||
app.get(
|
app.get(
|
||||||
"/authentication/api/configuration/Init?*",
|
"/authentication/api/configuration/Init?*",
|
||||||
|
// @ts-expect-error jwt props.
|
||||||
extractToken,
|
extractToken,
|
||||||
(req: RequestWithJwt, res) => {
|
(req: RequestWithJwt, res) => {
|
||||||
// configName=pc-prod&lockedContentDisabled=false&isFreePrologueUser=false&isIntroPackUser=false&isFullExperienceUser=false
|
// configName=pc-prod&lockedContentDisabled=false&isFreePrologueUser=false&isIntroPackUser=false&isFullExperienceUser=false
|
||||||
@ -413,6 +443,7 @@ app.get(
|
|||||||
|
|
||||||
app.post(
|
app.post(
|
||||||
"/authentication/api/userchannel/AuthenticationService/RenewBlobSignature",
|
"/authentication/api/userchannel/AuthenticationService/RenewBlobSignature",
|
||||||
|
// @ts-expect-error jwt props.
|
||||||
(req: RequestWithJwt, res) => {
|
(req: RequestWithJwt, res) => {
|
||||||
res.json(generateBlobConfig(req))
|
res.json(generateBlobConfig(req))
|
||||||
},
|
},
|
||||||
@ -421,7 +452,6 @@ app.post(
|
|||||||
const legacyRouter = Router()
|
const legacyRouter = Router()
|
||||||
const primaryRouter = Router()
|
const primaryRouter = Router()
|
||||||
|
|
||||||
legacyRouter.use("/resources-(\\d+-\\d+)/", legacyMenuSystemRouter)
|
|
||||||
legacyRouter.use("/authentication/api/userchannel/", legacyProfileRouter)
|
legacyRouter.use("/authentication/api/userchannel/", legacyProfileRouter)
|
||||||
legacyRouter.use("/profiles/page/", legacyMenuDataRouter)
|
legacyRouter.use("/profiles/page/", legacyMenuDataRouter)
|
||||||
legacyRouter.use(
|
legacyRouter.use(
|
||||||
@ -442,9 +472,12 @@ primaryRouter.use(
|
|||||||
"/authentication/api/userchannel/ContractsService/",
|
"/authentication/api/userchannel/ContractsService/",
|
||||||
contractRoutingRouter,
|
contractRoutingRouter,
|
||||||
)
|
)
|
||||||
primaryRouter.use(
|
primaryRouter.get(
|
||||||
"/authentication/api/userchannel/ReportingService/",
|
"/authentication/api/userchannel/ReportingService/ReportContract",
|
||||||
reportRouter,
|
(_, res) => {
|
||||||
|
// TODO
|
||||||
|
res.json({})
|
||||||
|
},
|
||||||
)
|
)
|
||||||
primaryRouter.use("/authentication/api/userchannel/", profileRouter)
|
primaryRouter.use("/authentication/api/userchannel/", profileRouter)
|
||||||
primaryRouter.use("/profiles/page", multiplayerMenuDataRouter)
|
primaryRouter.use("/profiles/page", multiplayerMenuDataRouter)
|
||||||
@ -454,6 +487,7 @@ primaryRouter.use("/resources-(\\d+-\\d+)/", menuSystemRouter)
|
|||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
Router()
|
Router()
|
||||||
|
// @ts-expect-error Has jwt props.
|
||||||
.use((req: RequestWithJwt, _, next) => {
|
.use((req: RequestWithJwt, _, next) => {
|
||||||
if (req.shouldCease) {
|
if (req.shouldCease) {
|
||||||
return next("router")
|
return next("router")
|
||||||
@ -467,6 +501,7 @@ app.use(
|
|||||||
})
|
})
|
||||||
.use(legacyRouter),
|
.use(legacyRouter),
|
||||||
Router()
|
Router()
|
||||||
|
// @ts-expect-error Has jwt props.
|
||||||
.use((req: RequestWithJwt, _, next) => {
|
.use((req: RequestWithJwt, _, next) => {
|
||||||
if (req.shouldCease) {
|
if (req.shouldCease) {
|
||||||
return next("router")
|
return next("router")
|
||||||
@ -493,28 +528,15 @@ app.all("*", (req, res) => {
|
|||||||
app.use(errorLoggingMiddleware)
|
app.use(errorLoggingMiddleware)
|
||||||
|
|
||||||
program.description(
|
program.description(
|
||||||
"The Peacock Project is a HITMAN™ World of Assassination Trilogy server built for general use.",
|
"The Peacock Project is a HITMAN™ World of Assassination Trilogy server replacement.",
|
||||||
)
|
)
|
||||||
|
|
||||||
const PEECOCK_ART = picocolors.yellow(`
|
|
||||||
███████████ ██████████ ██████████ █████████ ███████ █████████ █████ ████
|
|
||||||
░░███░░░░░███░░███░░░░░█░░███░░░░░█ ███░░░░░███ ███░░░░░███ ███░░░░░███░░███ ███░
|
|
||||||
░███ ░███ ░███ █ ░ ░███ █ ░ ███ ░░░ ███ ░░███ ███ ░░░ ░███ ███
|
|
||||||
░██████████ ░██████ ░██████ ░███ ░███ ░███░███ ░███████
|
|
||||||
░███░░░░░░ ░███░░█ ░███░░█ ░███ ░███ ░███░███ ░███░░███
|
|
||||||
░███ ░███ ░ █ ░███ ░ █░░███ ███░░███ ███ ░░███ ███ ░███ ░░███
|
|
||||||
█████ ██████████ ██████████ ░░█████████ ░░░███████░ ░░█████████ █████ ░░████
|
|
||||||
░░░░░ ░░░░░░░░░░ ░░░░░░░░░░ ░░░░░░░░░ ░░░░░░░ ░░░░░░░░░ ░░░░░ ░░░░
|
|
||||||
`)
|
|
||||||
|
|
||||||
function startServer(options: { hmr: boolean; pluginDevHost: boolean }): void {
|
function startServer(options: { hmr: boolean; pluginDevHost: boolean }): void {
|
||||||
checkForUpdates()
|
void checkForUpdates()
|
||||||
|
|
||||||
if (!IS_LAUNCHER) {
|
if (!IS_LAUNCHER) {
|
||||||
console.log(
|
console.log(
|
||||||
Math.random() < 0.001
|
picocolors.greenBright(`
|
||||||
? PEECOCK_ART
|
|
||||||
: picocolors.greenBright(`
|
|
||||||
███████████ ██████████ █████████ █████████ ███████ █████████ █████ ████
|
███████████ ██████████ █████████ █████████ ███████ █████████ █████ ████
|
||||||
░░███░░░░░███░░███░░░░░█ ███░░░░░███ ███░░░░░███ ███░░░░░███ ███░░░░░███░░███ ███░
|
░░███░░░░░███░░███░░░░░█ ███░░░░░███ ███░░░░░███ ███░░░░░███ ███░░░░░███░░███ ███░
|
||||||
░███ ░███ ░███ █ ░ ░███ ░███ ███ ░░░ ███ ░░███ ███ ░░░ ░███ ███
|
░███ ░███ ░███ █ ░ ░███ ░███ ███ ░░░ ███ ░░███ ███ ░░░ ░███ ███
|
||||||
@ -576,7 +598,7 @@ function startServer(options: { hmr: boolean; pluginDevHost: boolean }): void {
|
|||||||
if (options.hmr) {
|
if (options.hmr) {
|
||||||
log(LogLevel.DEBUG, "Experimental HMR enabled.")
|
log(LogLevel.DEBUG, "Experimental HMR enabled.")
|
||||||
|
|
||||||
setupHotListener("contracts", () => {
|
void setupHotListener("contracts", () => {
|
||||||
log(LogLevel.INFO, "Detected a change in contracts! Re-indexing...")
|
log(LogLevel.INFO, "Detected a change in contracts! Re-indexing...")
|
||||||
controller.index()
|
controller.index()
|
||||||
})
|
})
|
||||||
@ -584,7 +606,7 @@ function startServer(options: { hmr: boolean; pluginDevHost: boolean }): void {
|
|||||||
|
|
||||||
// once contracts directory is present, we are clear to boot
|
// once contracts directory is present, we are clear to boot
|
||||||
loadouts.init()
|
loadouts.init()
|
||||||
controller.boot(options.pluginDevHost)
|
void controller.boot(options.pluginDevHost)
|
||||||
|
|
||||||
const httpServer = http.createServer(app)
|
const httpServer = http.createServer(app)
|
||||||
|
|
||||||
@ -597,7 +619,7 @@ function startServer(options: { hmr: boolean; pluginDevHost: boolean }): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// initialize livesplit
|
// initialize livesplit
|
||||||
liveSplitManager.init()
|
void liveSplitManager.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
program.option(
|
program.option(
|
||||||
@ -616,9 +638,10 @@ program
|
|||||||
.command("tools")
|
.command("tools")
|
||||||
.description("open the tools UI")
|
.description("open the tools UI")
|
||||||
.action(() => {
|
.action(() => {
|
||||||
toolsMenu()
|
void toolsMenu()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// noinspection RequiredAttributes
|
||||||
program
|
program
|
||||||
.command("pack")
|
.command("pack")
|
||||||
.argument("<input>", "input file to pack")
|
.argument("<input>", "input file to pack")
|
||||||
@ -636,6 +659,7 @@ program
|
|||||||
log(LogLevel.INFO, `Packed "${input}" to "${outputPath}" successfully.`)
|
log(LogLevel.INFO, `Packed "${input}" to "${outputPath}" successfully.`)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// noinspection RequiredAttributes
|
||||||
program
|
program
|
||||||
.command("unpack")
|
.command("unpack")
|
||||||
.argument("<input>", "input file to unpack")
|
.argument("<input>", "input file to unpack")
|
||||||
|
@ -106,7 +106,7 @@ export function clearInventoryCache(): void {
|
|||||||
function filterUnlockedContent(
|
function filterUnlockedContent(
|
||||||
userProfile: UserProfile,
|
userProfile: UserProfile,
|
||||||
packagedUnlocks: Map<string, boolean>,
|
packagedUnlocks: Map<string, boolean>,
|
||||||
challengesUnlockables: object,
|
challengesUnlockables: Record<string, string>,
|
||||||
gameVersion: GameVersion,
|
gameVersion: GameVersion,
|
||||||
) {
|
) {
|
||||||
return function (
|
return function (
|
||||||
@ -114,7 +114,7 @@ function filterUnlockedContent(
|
|||||||
unlockable: Unlockable,
|
unlockable: Unlockable,
|
||||||
) {
|
) {
|
||||||
let unlockableChallengeId: string
|
let unlockableChallengeId: string
|
||||||
let unlockableMasteryData: UnlockableMasteryData
|
let unlockableMasteryData: UnlockableMasteryData | undefined
|
||||||
|
|
||||||
// Handles unlockables that belong to a package or unlocked gear from evergreen
|
// Handles unlockables that belong to a package or unlocked gear from evergreen
|
||||||
if (packagedUnlocks.has(unlockable.Id)) {
|
if (packagedUnlocks.has(unlockable.Id)) {
|
||||||
@ -123,7 +123,7 @@ function filterUnlockedContent(
|
|||||||
|
|
||||||
// Handles packages
|
// Handles packages
|
||||||
else if (unlockable.Type === "package") {
|
else if (unlockable.Type === "package") {
|
||||||
for (const pkgUnlockableId of unlockable.Properties.Unlocks) {
|
for (const pkgUnlockableId of unlockable.Properties.Unlocks || []) {
|
||||||
packagedUnlocks.set(pkgUnlockableId, true)
|
packagedUnlocks.set(pkgUnlockableId, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -199,7 +199,7 @@ function filterUnlockedContent(
|
|||||||
* @returns boolean
|
* @returns boolean
|
||||||
*/
|
*/
|
||||||
function filterAllowedContent(gameVersion: GameVersion, entP: string[]) {
|
function filterAllowedContent(gameVersion: GameVersion, entP: string[]) {
|
||||||
return function (unlockContainer: {
|
return function (unlockContainer?: {
|
||||||
InstanceId: string
|
InstanceId: string
|
||||||
ProfileId: string
|
ProfileId: string
|
||||||
Unlockable: Unlockable
|
Unlockable: Unlockable
|
||||||
@ -474,21 +474,25 @@ function updateWithDefaultSuit(
|
|||||||
profileId: string,
|
profileId: string,
|
||||||
gameVersion: GameVersion,
|
gameVersion: GameVersion,
|
||||||
inv: InventoryItem[],
|
inv: InventoryItem[],
|
||||||
sublocation: Unlockable,
|
sublocation?: Unlockable,
|
||||||
): InventoryItem[] {
|
): InventoryItem[] {
|
||||||
if (sublocation === undefined) {
|
if (!sublocation) {
|
||||||
return inv
|
return inv
|
||||||
}
|
}
|
||||||
|
|
||||||
// We need to add a suit, so need to copy the cache to prevent modifying it.
|
|
||||||
const newInv = [...inv]
|
|
||||||
|
|
||||||
// Yes this is slow. We should organize the unlockables into a { [Id: string]: Unlockable } map.
|
// Yes this is slow. We should organize the unlockables into a { [Id: string]: Unlockable } map.
|
||||||
const locationSuit = getUnlockableById(
|
const locationSuit = getUnlockableById(
|
||||||
getDefaultSuitFor(sublocation),
|
getDefaultSuitFor(sublocation),
|
||||||
gameVersion,
|
gameVersion,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (!locationSuit) {
|
||||||
|
return inv
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need to add a suit, so need to copy the cache to prevent modifying it.
|
||||||
|
const newInv = [...inv]
|
||||||
|
|
||||||
// check if any inventoryItem's unlockable is the default suit for the sublocation
|
// check if any inventoryItem's unlockable is the default suit for the sublocation
|
||||||
if (newInv.every((i) => i.Unlockable.Id !== locationSuit.Id)) {
|
if (newInv.every((i) => i.Unlockable.Id !== locationSuit.Id)) {
|
||||||
// if not, add it
|
// if not, add it
|
||||||
@ -576,7 +580,7 @@ export function createInventory(
|
|||||||
// and location-wide default suits will be given afterwards.
|
// and location-wide default suits will be given afterwards.
|
||||||
const defaults = Object.values(defaultSuits)
|
const defaults = Object.values(defaultSuits)
|
||||||
|
|
||||||
if ((getFlag("getDefaultSuits") as boolean) === false) {
|
if (!getFlag("getDefaultSuits")) {
|
||||||
unlockables = unlockables.filter(
|
unlockables = unlockables.filter(
|
||||||
(u) =>
|
(u) =>
|
||||||
!defaults.includes(u.Id) ||
|
!defaults.includes(u.Id) ||
|
||||||
@ -584,8 +588,7 @@ export function createInventory(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ts-expect-error It cannot be undefined.
|
const filtered = unlockables
|
||||||
const filtered: InventoryItem[] = unlockables
|
|
||||||
.map((unlockable) => {
|
.map((unlockable) => {
|
||||||
if (brokenItems.includes(unlockable.Guid)) {
|
if (brokenItems.includes(unlockable.Guid)) {
|
||||||
return undefined
|
return undefined
|
||||||
@ -601,7 +604,9 @@ export function createInventory(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
// filter again, this time removing legacy unlockables
|
// filter again, this time removing legacy unlockables
|
||||||
.filter(filterAllowedContent(gameVersion, userProfile.Extensions.entP))
|
.filter(
|
||||||
|
filterAllowedContent(gameVersion, userProfile.Extensions.entP),
|
||||||
|
) as InventoryItem[]
|
||||||
|
|
||||||
for (const unlockable of filtered) {
|
for (const unlockable of filtered) {
|
||||||
unlockable!.ProfileId = profileId
|
unlockable!.ProfileId = profileId
|
||||||
@ -627,7 +632,7 @@ export function grantDrops(profileId: string, drops: Unlockable[]): void {
|
|||||||
|
|
||||||
inventoryUserCache.set(profileId, [
|
inventoryUserCache.set(profileId, [
|
||||||
...new Set([
|
...new Set([
|
||||||
...inventoryUserCache.get(profileId),
|
...(inventoryUserCache.get(profileId) || []),
|
||||||
...inventoryItems.filter(
|
...inventoryItems.filter(
|
||||||
(invItem) => invItem.Unlockable.Type !== "evergreenmastery",
|
(invItem) => invItem.Unlockable.Type !== "evergreenmastery",
|
||||||
),
|
),
|
||||||
|
@ -50,14 +50,7 @@ const defaultValue: LoadoutFile = {
|
|||||||
* A class for managing loadouts.
|
* A class for managing loadouts.
|
||||||
*/
|
*/
|
||||||
export class Loadouts {
|
export class Loadouts {
|
||||||
private _loadouts: LoadoutFile
|
private _loadouts!: LoadoutFile
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new instance of the class.
|
|
||||||
*/
|
|
||||||
public constructor() {
|
|
||||||
this._loadouts = undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the loadouts data.
|
* Get the loadouts data.
|
||||||
@ -107,7 +100,10 @@ export class Loadouts {
|
|||||||
// if the selected value is null/undefined or is not length 0 or 21, it's not a valid id
|
// if the selected value is null/undefined or is not length 0 or 21, it's not a valid id
|
||||||
if (
|
if (
|
||||||
!this._loadouts[gameVersion].selected ||
|
!this._loadouts[gameVersion].selected ||
|
||||||
![0, 21].includes(this._loadouts[gameVersion].selected.length)
|
// first condition ensures selected is truthy, but TS doesn't know
|
||||||
|
![0, 21].includes(
|
||||||
|
this._loadouts[gameVersion].selected?.length || -1,
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
dirty = true
|
dirty = true
|
||||||
|
|
||||||
@ -121,7 +117,7 @@ export class Loadouts {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dirty === true) {
|
if (dirty) {
|
||||||
writeFileSync(LOADOUT_PROFILES_FILE, JSON.stringify(this._loadouts))
|
writeFileSync(LOADOUT_PROFILES_FILE, JSON.stringify(this._loadouts))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -217,7 +213,7 @@ loadoutRouter.patch(
|
|||||||
async (
|
async (
|
||||||
req: Request<
|
req: Request<
|
||||||
never,
|
never,
|
||||||
string,
|
string | { error?: string; message?: string },
|
||||||
{ gameVersion: "h1" | "h2" | "h3"; id: string }
|
{ gameVersion: "h1" | "h2" | "h3"; id: string }
|
||||||
>,
|
>,
|
||||||
res,
|
res,
|
||||||
|
@ -16,8 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { NextFunction, Response } from "express"
|
import type { NextFunction, Request, Response } from "express"
|
||||||
import type { RequestWithJwt } from "./types/types"
|
|
||||||
import picocolors from "picocolors"
|
import picocolors from "picocolors"
|
||||||
import winston from "winston"
|
import winston from "winston"
|
||||||
import "winston-daily-rotate-file"
|
import "winston-daily-rotate-file"
|
||||||
@ -135,6 +134,7 @@ if (consoleLogLevel !== LOG_LEVEL_NONE) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const winstonLogLevel = {}
|
const winstonLogLevel = {}
|
||||||
|
// @ts-expect-error Type mismatch.
|
||||||
Object.values(LogLevel).forEach((e, i) => (winstonLogLevel[e] = i))
|
Object.values(LogLevel).forEach((e, i) => (winstonLogLevel[e] = i))
|
||||||
|
|
||||||
const logger = winston.createLogger({
|
const logger = winston.createLogger({
|
||||||
@ -255,13 +255,13 @@ export function log(
|
|||||||
* Express middleware that logs all requests and their details with the info log level.
|
* Express middleware that logs all requests and their details with the info log level.
|
||||||
*
|
*
|
||||||
* @param req The Express request object.
|
* @param req The Express request object.
|
||||||
* @param res The Express response object.
|
* @param _ The Express response object.
|
||||||
* @param next The Express next function.
|
* @param next The Express next function.
|
||||||
* @see LogLevel.INFO
|
* @see LogLevel.INFO
|
||||||
*/
|
*/
|
||||||
export function loggingMiddleware(
|
export function loggingMiddleware(
|
||||||
req: RequestWithJwt,
|
req: Request,
|
||||||
res: Response,
|
_: Response,
|
||||||
next?: NextFunction,
|
next?: NextFunction,
|
||||||
): void {
|
): void {
|
||||||
log(
|
log(
|
||||||
@ -273,7 +273,7 @@ export function loggingMiddleware(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function requestLoggingMiddleware(
|
export function requestLoggingMiddleware(
|
||||||
req: RequestWithJwt,
|
req: Request,
|
||||||
res: Response,
|
res: Response,
|
||||||
next?: NextFunction,
|
next?: NextFunction,
|
||||||
): void {
|
): void {
|
||||||
@ -294,8 +294,8 @@ export function requestLoggingMiddleware(
|
|||||||
|
|
||||||
export function errorLoggingMiddleware(
|
export function errorLoggingMiddleware(
|
||||||
err: Error,
|
err: Error,
|
||||||
req: RequestWithJwt,
|
req: Request,
|
||||||
res: Response,
|
_: Response,
|
||||||
next?: NextFunction,
|
next?: NextFunction,
|
||||||
): void {
|
): void {
|
||||||
const debug = {
|
const debug = {
|
||||||
|
@ -21,7 +21,6 @@ import { Response, Router } from "express"
|
|||||||
import {
|
import {
|
||||||
contractCreationTutorialId,
|
contractCreationTutorialId,
|
||||||
getMaxProfileLevel,
|
getMaxProfileLevel,
|
||||||
isSniperLocation,
|
|
||||||
isSuit,
|
isSuit,
|
||||||
unlockOrderComparer,
|
unlockOrderComparer,
|
||||||
uuidRegex,
|
uuidRegex,
|
||||||
@ -29,22 +28,15 @@ import {
|
|||||||
import { contractSessions, getSession } from "./eventHandler"
|
import { contractSessions, getSession } from "./eventHandler"
|
||||||
import { getConfig, getVersionedConfig } from "./configSwizzleManager"
|
import { getConfig, getVersionedConfig } from "./configSwizzleManager"
|
||||||
import { controller } from "./controller"
|
import { controller } from "./controller"
|
||||||
import {
|
import { createLocationsData, getDestination } from "./menus/destinations"
|
||||||
createLocationsData,
|
|
||||||
getDestination,
|
|
||||||
getDestinationCompletion,
|
|
||||||
} from "./menus/destinations"
|
|
||||||
import type {
|
import type {
|
||||||
ChallengeCategoryCompletion,
|
|
||||||
SelectEntranceOrPickupData,
|
|
||||||
ContractSearchResult,
|
ContractSearchResult,
|
||||||
GameVersion,
|
GameVersion,
|
||||||
HitsCategoryCategory,
|
HitsCategoryCategory,
|
||||||
PeacockLocationsData,
|
PeacockLocationsData,
|
||||||
PlayerProfileView,
|
|
||||||
ProgressionData,
|
|
||||||
RequestWithJwt,
|
RequestWithJwt,
|
||||||
SceneConfig,
|
SceneConfig,
|
||||||
|
SelectEntranceOrPickupData,
|
||||||
UserCentricContract,
|
UserCentricContract,
|
||||||
} from "./types/types"
|
} from "./types/types"
|
||||||
import {
|
import {
|
||||||
@ -79,7 +71,9 @@ import {
|
|||||||
DebriefingLeaderboardsQuery,
|
DebriefingLeaderboardsQuery,
|
||||||
GetCompletionDataForLocationQuery,
|
GetCompletionDataForLocationQuery,
|
||||||
GetDestinationQuery,
|
GetDestinationQuery,
|
||||||
|
GetMasteryCompletionDataForUnlockableQuery,
|
||||||
LeaderboardEntriesCommonQuery,
|
LeaderboardEntriesCommonQuery,
|
||||||
|
LookupContractPublicIdQuery,
|
||||||
MasteryUnlockableQuery,
|
MasteryUnlockableQuery,
|
||||||
MissionEndRequestQuery,
|
MissionEndRequestQuery,
|
||||||
PlanningQuery,
|
PlanningQuery,
|
||||||
@ -97,6 +91,7 @@ import {
|
|||||||
getSafehouseCategory,
|
getSafehouseCategory,
|
||||||
} from "./menus/stashpoints"
|
} from "./menus/stashpoints"
|
||||||
import { getHubData } from "./menus/hub"
|
import { getHubData } from "./menus/hub"
|
||||||
|
import { getPlayerProfileData } from "./menus/playerProfile"
|
||||||
|
|
||||||
const menuDataRouter = Router()
|
const menuDataRouter = Router()
|
||||||
|
|
||||||
@ -104,18 +99,19 @@ const menuDataRouter = Router()
|
|||||||
|
|
||||||
menuDataRouter.get(
|
menuDataRouter.get(
|
||||||
"/ChallengeLocation",
|
"/ChallengeLocation",
|
||||||
|
// @ts-expect-error Jwt props.
|
||||||
(req: RequestWithJwt<ChallengeLocationQuery>, res) => {
|
(req: RequestWithJwt<ChallengeLocationQuery>, res) => {
|
||||||
if (typeof req.query.locationId !== "string") {
|
|
||||||
res.status(400).send("Invalid locationId")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const location = getVersionedConfig<PeacockLocationsData>(
|
const location = getVersionedConfig<PeacockLocationsData>(
|
||||||
"LocationsData",
|
"LocationsData",
|
||||||
req.gameVersion,
|
req.gameVersion,
|
||||||
true,
|
true,
|
||||||
).children[req.query.locationId]
|
).children[req.query.locationId]
|
||||||
|
|
||||||
|
if (!location) {
|
||||||
|
res.status(400).send("Invalid locationId")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
Name: location.DisplayNameLocKey,
|
Name: location.DisplayNameLocKey,
|
||||||
Location: location,
|
Location: location,
|
||||||
@ -137,17 +133,20 @@ menuDataRouter.get(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// @ts-expect-error Jwt props.
|
||||||
menuDataRouter.get("/Hub", (req: RequestWithJwt, res) => {
|
menuDataRouter.get("/Hub", (req: RequestWithJwt, res) => {
|
||||||
const hubInfo = getHubData(req.gameVersion, req.jwt)
|
const hubInfo = getHubData(req.gameVersion, req.jwt.unique_name)
|
||||||
|
|
||||||
const template =
|
let template: unknown
|
||||||
req.gameVersion === "h3"
|
|
||||||
? null
|
if (req.gameVersion === "h3" || req.gameVersion === "h2") {
|
||||||
: req.gameVersion === "h2"
|
template = null
|
||||||
? null
|
} else {
|
||||||
: req.gameVersion === "scpc"
|
template =
|
||||||
? getConfig("FrankensteinHubTemplate", false)
|
req.gameVersion === "scpc"
|
||||||
: getConfig("LegacyHubTemplate", false)
|
? getConfig("FrankensteinHubTemplate", false)
|
||||||
|
: getConfig("LegacyHubTemplate", false)
|
||||||
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
template,
|
template,
|
||||||
@ -155,6 +154,7 @@ menuDataRouter.get("/Hub", (req: RequestWithJwt, res) => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// @ts-expect-error Jwt props.
|
||||||
menuDataRouter.get("/SafehouseCategory", (req: RequestWithJwt, res) => {
|
menuDataRouter.get("/SafehouseCategory", (req: RequestWithJwt, res) => {
|
||||||
res.json({
|
res.json({
|
||||||
template:
|
template:
|
||||||
@ -165,6 +165,7 @@ menuDataRouter.get("/SafehouseCategory", (req: RequestWithJwt, res) => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// @ts-expect-error Jwt props.
|
||||||
menuDataRouter.get("/Safehouse", (req: RequestWithJwt<SafehouseQuery>, res) => {
|
menuDataRouter.get("/Safehouse", (req: RequestWithJwt<SafehouseQuery>, res) => {
|
||||||
const template = getConfig("LegacySafehouseTemplate", false)
|
const template = getConfig("LegacySafehouseTemplate", false)
|
||||||
|
|
||||||
@ -184,6 +185,7 @@ menuDataRouter.get("/Safehouse", (req: RequestWithJwt<SafehouseQuery>, res) => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// @ts-expect-error Jwt props.
|
||||||
menuDataRouter.get("/report", (req: RequestWithJwt, res) => {
|
menuDataRouter.get("/report", (req: RequestWithJwt, res) => {
|
||||||
res.json({
|
res.json({
|
||||||
template: getVersionedConfig("ReportTemplate", req.gameVersion, false),
|
template: getVersionedConfig("ReportTemplate", req.gameVersion, false),
|
||||||
@ -202,6 +204,7 @@ menuDataRouter.get("/report", (req: RequestWithJwt, res) => {
|
|||||||
// /stashpoint?contractid=5b5f8aa4-ecb4-4a0a-9aff-98aa1de43dcc&slotid=6&slotname=stashpoint6&stashpoint=28b03709-d1f0-4388-b207-f03611eafb64&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
|
||||||
menuDataRouter.get(
|
menuDataRouter.get(
|
||||||
"/stashpoint",
|
"/stashpoint",
|
||||||
|
// @ts-expect-error Jwt props.
|
||||||
(req: RequestWithJwt<StashpointQuery | StashpointQueryH2016>, res) => {
|
(req: RequestWithJwt<StashpointQuery | StashpointQueryH2016>, res) => {
|
||||||
function isValidModernQuery(
|
function isValidModernQuery(
|
||||||
query: StashpointQuery | StashpointQueryH2016,
|
query: StashpointQuery | StashpointQueryH2016,
|
||||||
@ -214,7 +217,7 @@ menuDataRouter.get(
|
|||||||
|
|
||||||
if (["h1", "scpc"].includes(req.gameVersion)) {
|
if (["h1", "scpc"].includes(req.gameVersion)) {
|
||||||
// H1 or SCPC
|
// H1 or SCPC
|
||||||
if (!uuidRegex.test(req.query.contractid)) {
|
if (!uuidRegex.test(req.query.contractid!)) {
|
||||||
res.status(400).send("contract id was not a uuid")
|
res.status(400).send("contract id was not a uuid")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -264,15 +267,22 @@ menuDataRouter.get(
|
|||||||
|
|
||||||
menuDataRouter.get(
|
menuDataRouter.get(
|
||||||
"/missionrewards",
|
"/missionrewards",
|
||||||
|
// @ts-expect-error Jwt props.
|
||||||
(
|
(
|
||||||
req: RequestWithJwt<{
|
req: RequestWithJwt<{
|
||||||
contractSessionId: string
|
contractSessionId: string
|
||||||
}>,
|
}>,
|
||||||
res,
|
res,
|
||||||
) => {
|
) => {
|
||||||
const { contractId } = getSession(req.jwt.unique_name)
|
const s = getSession(req.jwt.unique_name)
|
||||||
const contractData = controller.resolveContract(contractId, true)
|
|
||||||
|
|
||||||
|
if (!s) {
|
||||||
|
res.status(400).send("no session")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { contractId } = s
|
||||||
|
const contractData = controller.resolveContract(contractId, true)
|
||||||
const userData = getUserData(req.jwt.unique_name, req.gameVersion)
|
const userData = getUserData(req.jwt.unique_name, req.gameVersion)
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
@ -321,7 +331,7 @@ menuDataRouter.get(
|
|||||||
LocationHideProgression: true,
|
LocationHideProgression: true,
|
||||||
Difficulty: "normal", // FIXME: is this right?
|
Difficulty: "normal", // FIXME: is this right?
|
||||||
CompletionData: generateCompletionData(
|
CompletionData: generateCompletionData(
|
||||||
contractData.Metadata.Location,
|
contractData?.Metadata.Location || "",
|
||||||
req.jwt.unique_name,
|
req.jwt.unique_name,
|
||||||
req.gameVersion,
|
req.gameVersion,
|
||||||
),
|
),
|
||||||
@ -332,6 +342,7 @@ menuDataRouter.get(
|
|||||||
|
|
||||||
menuDataRouter.get(
|
menuDataRouter.get(
|
||||||
"/Planning",
|
"/Planning",
|
||||||
|
// @ts-expect-error Jwt props.
|
||||||
async (req: RequestWithJwt<PlanningQuery>, res) => {
|
async (req: RequestWithJwt<PlanningQuery>, res) => {
|
||||||
if (!req.query.contractid || !req.query.resetescalation) {
|
if (!req.query.contractid || !req.query.resetescalation) {
|
||||||
res.status(400).send("invalid query")
|
res.status(400).send("invalid query")
|
||||||
@ -341,7 +352,7 @@ menuDataRouter.get(
|
|||||||
const planningData = await getPlanningData(
|
const planningData = await getPlanningData(
|
||||||
req.query.contractid,
|
req.query.contractid,
|
||||||
req.query.resetescalation === "true",
|
req.query.resetescalation === "true",
|
||||||
req.jwt,
|
req.jwt.unique_name,
|
||||||
req.gameVersion,
|
req.gameVersion,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -350,13 +361,16 @@ menuDataRouter.get(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let template: unknown | null = null
|
||||||
|
|
||||||
|
if (req.gameVersion === "h1") {
|
||||||
|
template = getConfig("LegacyPlanningTemplate", false)
|
||||||
|
} else if (req.gameVersion === "scpc") {
|
||||||
|
template = getConfig("FrankensteinPlanningTemplate", false)
|
||||||
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
template:
|
template,
|
||||||
req.gameVersion === "h1"
|
|
||||||
? getConfig("LegacyPlanningTemplate", false)
|
|
||||||
: req.gameVersion === "scpc"
|
|
||||||
? getConfig("FrankensteinPlanningTemplate", false)
|
|
||||||
: null,
|
|
||||||
data: planningData,
|
data: planningData,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@ -364,6 +378,7 @@ menuDataRouter.get(
|
|||||||
|
|
||||||
menuDataRouter.get(
|
menuDataRouter.get(
|
||||||
"/selectagencypickup",
|
"/selectagencypickup",
|
||||||
|
// @ts-expect-error Jwt props.
|
||||||
(
|
(
|
||||||
req: RequestWithJwt<{
|
req: RequestWithJwt<{
|
||||||
contractId: string
|
contractId: string
|
||||||
@ -407,7 +422,7 @@ menuDataRouter.get(
|
|||||||
contractData,
|
contractData,
|
||||||
req.jwt.unique_name,
|
req.jwt.unique_name,
|
||||||
req.gameVersion,
|
req.gameVersion,
|
||||||
),
|
)!,
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
@ -440,14 +455,16 @@ menuDataRouter.get(
|
|||||||
Contract: contractData,
|
Contract: contractData,
|
||||||
OrderedUnlocks: unlockedAgencyPickups
|
OrderedUnlocks: unlockedAgencyPickups
|
||||||
.filter((unlockable) =>
|
.filter((unlockable) =>
|
||||||
pickupsInScene.includes(unlockable.Properties.RepositoryId),
|
pickupsInScene.includes(
|
||||||
|
unlockable.Properties.RepositoryId || "",
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.sort(unlockOrderComparer),
|
.sort(unlockOrderComparer),
|
||||||
UserCentric: generateUserCentric(
|
UserCentric: generateUserCentric(
|
||||||
contractData,
|
contractData,
|
||||||
req.jwt.unique_name,
|
req.jwt.unique_name,
|
||||||
req.gameVersion,
|
req.gameVersion,
|
||||||
),
|
)!,
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
@ -463,6 +480,7 @@ menuDataRouter.get(
|
|||||||
|
|
||||||
menuDataRouter.get(
|
menuDataRouter.get(
|
||||||
"/selectentrance",
|
"/selectentrance",
|
||||||
|
// @ts-expect-error Jwt props.
|
||||||
(
|
(
|
||||||
req: RequestWithJwt<{
|
req: RequestWithJwt<{
|
||||||
contractId: string
|
contractId: string
|
||||||
@ -522,7 +540,7 @@ menuDataRouter.get(
|
|||||||
contractData,
|
contractData,
|
||||||
req.jwt.unique_name,
|
req.jwt.unique_name,
|
||||||
req.gameVersion,
|
req.gameVersion,
|
||||||
),
|
)!,
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
@ -580,6 +598,12 @@ const missionEndRequest = async (
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// prototype pollution prevention
|
||||||
|
if (/(__proto__|prototype|constructor)/.test(req.query.contractSessionId)) {
|
||||||
|
res.status(400).send("invalid session id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const missionEndOutput = await getMissionEndData(
|
const missionEndOutput = await getMissionEndData(
|
||||||
req.query,
|
req.query,
|
||||||
req.jwt,
|
req.jwt,
|
||||||
@ -613,21 +637,29 @@ const missionEndRequest = async (
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @ts-expect-error Has jwt props.
|
||||||
menuDataRouter.get("/missionend", missionEndRequest)
|
menuDataRouter.get("/missionend", missionEndRequest)
|
||||||
|
|
||||||
|
// @ts-expect-error Has jwt props.
|
||||||
menuDataRouter.get("/scoreoverviewandunlocks", missionEndRequest)
|
menuDataRouter.get("/scoreoverviewandunlocks", missionEndRequest)
|
||||||
|
|
||||||
|
// @ts-expect-error Has jwt props.
|
||||||
menuDataRouter.get("/scoreoverview", missionEndRequest)
|
menuDataRouter.get("/scoreoverview", missionEndRequest)
|
||||||
|
|
||||||
menuDataRouter.get(
|
menuDataRouter.get(
|
||||||
"/Destination",
|
"/Destination",
|
||||||
|
// @ts-expect-error Jwt props.
|
||||||
(req: RequestWithJwt<GetDestinationQuery>, res) => {
|
(req: RequestWithJwt<GetDestinationQuery>, res) => {
|
||||||
if (!req.query.locationId) {
|
if (!req.query.locationId) {
|
||||||
res.status(400).send("Invalid locationId")
|
res.status(400).send("Invalid locationId")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const destination = getDestination(req.query, req.gameVersion, req.jwt)
|
const destination = getDestination(
|
||||||
|
req.query,
|
||||||
|
req.gameVersion,
|
||||||
|
req.jwt.unique_name,
|
||||||
|
)
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
template:
|
template:
|
||||||
@ -681,13 +713,9 @@ async function lookupContractPublicId(
|
|||||||
|
|
||||||
menuDataRouter.get(
|
menuDataRouter.get(
|
||||||
"/LookupContractPublicId",
|
"/LookupContractPublicId",
|
||||||
async (
|
// @ts-expect-error Has jwt props.
|
||||||
req: RequestWithJwt<{
|
async (req: RequestWithJwt<LookupContractPublicIdQuery>, res) => {
|
||||||
publicid: string
|
if (typeof req.query.publicid !== "string") {
|
||||||
}>,
|
|
||||||
res,
|
|
||||||
) => {
|
|
||||||
if (!req.query.publicid || typeof req.query.publicid !== "string") {
|
|
||||||
return res.status(400).send("no/invalid public id specified!")
|
return res.status(400).send("no/invalid public id specified!")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -708,6 +736,7 @@ menuDataRouter.get(
|
|||||||
|
|
||||||
menuDataRouter.get(
|
menuDataRouter.get(
|
||||||
"/HitsCategory",
|
"/HitsCategory",
|
||||||
|
// @ts-expect-error Has jwt props.
|
||||||
async (
|
async (
|
||||||
req: RequestWithJwt<{
|
req: RequestWithJwt<{
|
||||||
type: string
|
type: string
|
||||||
@ -749,6 +778,7 @@ menuDataRouter.get(
|
|||||||
|
|
||||||
menuDataRouter.get(
|
menuDataRouter.get(
|
||||||
"/PlayNext",
|
"/PlayNext",
|
||||||
|
// @ts-expect-error Has jwt props.
|
||||||
(
|
(
|
||||||
req: RequestWithJwt<{
|
req: RequestWithJwt<{
|
||||||
contractId: string
|
contractId: string
|
||||||
@ -764,14 +794,14 @@ menuDataRouter.get(
|
|||||||
template: getConfig("PlayNextTemplate", false),
|
template: getConfig("PlayNextTemplate", false),
|
||||||
data: getGamePlayNextData(
|
data: getGamePlayNextData(
|
||||||
req.query.contractId,
|
req.query.contractId,
|
||||||
req.jwt,
|
req.jwt.unique_name,
|
||||||
req.gameVersion,
|
req.gameVersion,
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
menuDataRouter.get("/LeaderboardsView", (req, res) => {
|
menuDataRouter.get("/LeaderboardsView", (_, res) => {
|
||||||
res.json({
|
res.json({
|
||||||
template: getConfig("LeaderboardsViewTemplate", false),
|
template: getConfig("LeaderboardsViewTemplate", false),
|
||||||
data: {
|
data: {
|
||||||
@ -783,6 +813,7 @@ menuDataRouter.get("/LeaderboardsView", (req, res) => {
|
|||||||
|
|
||||||
menuDataRouter.get(
|
menuDataRouter.get(
|
||||||
"/LeaderboardEntries",
|
"/LeaderboardEntries",
|
||||||
|
// @ts-expect-error Has jwt props.
|
||||||
async (req: RequestWithJwt<LeaderboardEntriesCommonQuery>, res) => {
|
async (req: RequestWithJwt<LeaderboardEntriesCommonQuery>, res) => {
|
||||||
if (!req.query.contractid) {
|
if (!req.query.contractid) {
|
||||||
res.status(400).send("no contract id!")
|
res.status(400).send("no contract id!")
|
||||||
@ -810,6 +841,7 @@ menuDataRouter.get(
|
|||||||
|
|
||||||
menuDataRouter.get(
|
menuDataRouter.get(
|
||||||
"/DebriefingLeaderboards",
|
"/DebriefingLeaderboards",
|
||||||
|
// @ts-expect-error Has jwt props.
|
||||||
async (req: RequestWithJwt<DebriefingLeaderboardsQuery>, res) => {
|
async (req: RequestWithJwt<DebriefingLeaderboardsQuery>, res) => {
|
||||||
if (!req.query.contractid) {
|
if (!req.query.contractid) {
|
||||||
res.status(400).send("no contract id!")
|
res.status(400).send("no contract id!")
|
||||||
@ -835,10 +867,12 @@ menuDataRouter.get(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// @ts-expect-error Has jwt props.
|
||||||
menuDataRouter.get("/Contracts", contractsModeHome)
|
menuDataRouter.get("/Contracts", contractsModeHome)
|
||||||
|
|
||||||
menuDataRouter.get(
|
menuDataRouter.get(
|
||||||
"/contractcreation/planning",
|
"/contractcreation/planning",
|
||||||
|
// @ts-expect-error Has jwt props.
|
||||||
async (
|
async (
|
||||||
req: RequestWithJwt<{
|
req: RequestWithJwt<{
|
||||||
contractCreationIdOverwrite: string
|
contractCreationIdOverwrite: string
|
||||||
@ -858,7 +892,7 @@ menuDataRouter.get(
|
|||||||
const planningData = await getPlanningData(
|
const planningData = await getPlanningData(
|
||||||
req.query.contractCreationIdOverwrite,
|
req.query.contractCreationIdOverwrite,
|
||||||
false,
|
false,
|
||||||
req.jwt,
|
req.jwt.unique_name,
|
||||||
req.gameVersion,
|
req.gameVersion,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -890,6 +924,7 @@ menuDataRouter.get(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// @ts-expect-error Has jwt props.
|
||||||
menuDataRouter.get("/contractsearchpage", (req: RequestWithJwt, res) => {
|
menuDataRouter.get("/contractsearchpage", (req: RequestWithJwt, res) => {
|
||||||
const createContractTutorial = controller.resolveContract(
|
const createContractTutorial = controller.resolveContract(
|
||||||
contractCreationTutorialId,
|
contractCreationTutorialId,
|
||||||
@ -920,6 +955,7 @@ menuDataRouter.get("/contractsearchpage", (req: RequestWithJwt, res) => {
|
|||||||
menuDataRouter.post(
|
menuDataRouter.post(
|
||||||
"/ContractSearch",
|
"/ContractSearch",
|
||||||
jsonMiddleware(),
|
jsonMiddleware(),
|
||||||
|
// @ts-expect-error Jwt props.
|
||||||
async (
|
async (
|
||||||
req: RequestWithJwt<
|
req: RequestWithJwt<
|
||||||
{
|
{
|
||||||
@ -977,12 +1013,21 @@ menuDataRouter.post(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No plugins handle this. Getting search results from official
|
// No plugins handle this. Getting search results from official
|
||||||
searchResult = await officialSearchContract(
|
searchResult = (await officialSearchContract(
|
||||||
req.jwt.unique_name,
|
req.jwt.unique_name,
|
||||||
req.gameVersion,
|
req.gameVersion,
|
||||||
req.body,
|
req.body,
|
||||||
0,
|
0,
|
||||||
)
|
)) || {
|
||||||
|
Data: {
|
||||||
|
Contracts: [],
|
||||||
|
TotalCount: 0,
|
||||||
|
Page: 0,
|
||||||
|
ErrorReason: "",
|
||||||
|
HasPrevious: false,
|
||||||
|
HasMore: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
@ -999,6 +1044,7 @@ menuDataRouter.post(
|
|||||||
menuDataRouter.post(
|
menuDataRouter.post(
|
||||||
"/ContractSearchPaginate",
|
"/ContractSearchPaginate",
|
||||||
jsonMiddleware(),
|
jsonMiddleware(),
|
||||||
|
// @ts-expect-error Has jwt props.
|
||||||
async (
|
async (
|
||||||
req: RequestWithJwt<
|
req: RequestWithJwt<
|
||||||
{
|
{
|
||||||
@ -1022,14 +1068,26 @@ menuDataRouter.post(
|
|||||||
|
|
||||||
menuDataRouter.get(
|
menuDataRouter.get(
|
||||||
"/DebriefingChallenges",
|
"/DebriefingChallenges",
|
||||||
|
// @ts-expect-error Has jwt props.
|
||||||
(
|
(
|
||||||
req: RequestWithJwt<{
|
req: RequestWithJwt<
|
||||||
contractId: string
|
Partial<{
|
||||||
}>,
|
contractId: string
|
||||||
|
}>
|
||||||
|
>,
|
||||||
res,
|
res,
|
||||||
) => {
|
) => {
|
||||||
|
if (typeof req.query.contractId !== "string") {
|
||||||
|
res.status(400).send("invalid contractId")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
template: getConfig("DebriefingChallengesTemplate", false),
|
template: getVersionedConfig(
|
||||||
|
"DebriefingChallengesTemplate",
|
||||||
|
req.gameVersion,
|
||||||
|
false,
|
||||||
|
),
|
||||||
data: {
|
data: {
|
||||||
ChallengeData: {
|
ChallengeData: {
|
||||||
Children:
|
Children:
|
||||||
@ -1044,6 +1102,7 @@ menuDataRouter.get(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// @ts-expect-error Has jwt props.
|
||||||
menuDataRouter.get("/contractcreation/create", (req: RequestWithJwt, res) => {
|
menuDataRouter.get("/contractcreation/create", (req: RequestWithJwt, res) => {
|
||||||
let cUuid = randomUUID()
|
let cUuid = randomUUID()
|
||||||
const createContractReturnTemplate = getConfig(
|
const createContractReturnTemplate = getConfig(
|
||||||
@ -1059,6 +1118,11 @@ menuDataRouter.get("/contractcreation/create", (req: RequestWithJwt, res) => {
|
|||||||
|
|
||||||
const sesh = getSession(req.jwt.unique_name)
|
const sesh = getSession(req.jwt.unique_name)
|
||||||
|
|
||||||
|
if (!sesh) {
|
||||||
|
res.status(400).send("no session")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const one = "1"
|
const one = "1"
|
||||||
const two = `${random.int(10, 99)}`
|
const two = `${random.int(10, 99)}`
|
||||||
const three = `${random.int(1_000_000, 9_999_999)}`
|
const three = `${random.int(1_000_000, 9_999_999)}`
|
||||||
@ -1091,25 +1155,23 @@ menuDataRouter.get("/contractcreation/create", (req: RequestWithJwt, res) => {
|
|||||||
Description: "UI_CONTRACTS_UGC_DESCRIPTION",
|
Description: "UI_CONTRACTS_UGC_DESCRIPTION",
|
||||||
Targets: Array.from(sesh.kills)
|
Targets: Array.from(sesh.kills)
|
||||||
.filter((kill) =>
|
.filter((kill) =>
|
||||||
sesh.markedTargets.has(kill._RepositoryId),
|
sesh.markedTargets.has(kill._RepositoryId || ""),
|
||||||
)
|
)
|
||||||
.map((km) => {
|
.map((km) => ({
|
||||||
return {
|
RepositoryId: km._RepositoryId,
|
||||||
RepositoryId: km._RepositoryId,
|
Selected: true,
|
||||||
Selected: true,
|
Weapon: {
|
||||||
Weapon: {
|
RepositoryId: km.KillItemRepositoryId,
|
||||||
RepositoryId: km.KillItemRepositoryId,
|
KillMethodBroad: km.KillMethodBroad,
|
||||||
KillMethodBroad: km.KillMethodBroad,
|
KillMethodStrict: km.KillMethodStrict,
|
||||||
KillMethodStrict: km.KillMethodStrict,
|
RequiredKillMethodType: 3,
|
||||||
RequiredKillMethodType: 3,
|
},
|
||||||
},
|
Outfit: {
|
||||||
Outfit: {
|
RepositoryId: km.OutfitRepoId,
|
||||||
RepositoryId: km.OutfitRepoId,
|
Required: true,
|
||||||
Required: true,
|
IsHitmanSuit: isSuit(km.OutfitRepoId),
|
||||||
IsHitmanSuit: isSuit(km.OutfitRepoId),
|
},
|
||||||
},
|
})),
|
||||||
}
|
|
||||||
}),
|
|
||||||
ContractConditions: complications(timeLimitStr),
|
ContractConditions: complications(timeLimitStr),
|
||||||
PublishingDisabled:
|
PublishingDisabled:
|
||||||
sesh.contractId === contractCreationTutorialId,
|
sesh.contractId === contractCreationTutorialId,
|
||||||
@ -1143,7 +1205,7 @@ const createLoadSaveMiddleware =
|
|||||||
template,
|
template,
|
||||||
data: {
|
data: {
|
||||||
Contracts: [] as UserCentricContract[],
|
Contracts: [] as UserCentricContract[],
|
||||||
PaymentEligiblity: {},
|
PaymentEligiblity: {} as Record<string, boolean>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1186,144 +1248,46 @@ const createLoadSaveMiddleware =
|
|||||||
menuDataRouter.post(
|
menuDataRouter.post(
|
||||||
"/Load",
|
"/Load",
|
||||||
jsonMiddleware(),
|
jsonMiddleware(),
|
||||||
|
// @ts-expect-error Has jwt props.
|
||||||
createLoadSaveMiddleware("LoadMenuTemplate"),
|
createLoadSaveMiddleware("LoadMenuTemplate"),
|
||||||
)
|
)
|
||||||
|
|
||||||
menuDataRouter.post(
|
menuDataRouter.post(
|
||||||
"/Save",
|
"/Save",
|
||||||
jsonMiddleware(),
|
jsonMiddleware(),
|
||||||
|
// @ts-expect-error Has jwt props.
|
||||||
createLoadSaveMiddleware("SaveMenuTemplate"),
|
createLoadSaveMiddleware("SaveMenuTemplate"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// @ts-expect-error Has jwt props.
|
||||||
menuDataRouter.get("/PlayerProfile", (req: RequestWithJwt, res) => {
|
menuDataRouter.get("/PlayerProfile", (req: RequestWithJwt, res) => {
|
||||||
const playerProfilePage = getConfig<PlayerProfileView>(
|
res.json({
|
||||||
"PlayerProfilePage",
|
template: null,
|
||||||
true,
|
data: getPlayerProfileData(req.gameVersion, req.jwt.unique_name),
|
||||||
)
|
})
|
||||||
|
|
||||||
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.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.gameVersion,
|
|
||||||
req.jwt,
|
|
||||||
)
|
|
||||||
|
|
||||||
playerProfilePage.data.SubLocationData.push({
|
|
||||||
ParentLocation: parentLocation,
|
|
||||||
Location: subLocation,
|
|
||||||
CompletionData: completionData,
|
|
||||||
ChallengeCategoryCompletion: challengeCategoryCompletion,
|
|
||||||
ChallengeCompletion: destinationCompletion.ChallengeCompletion,
|
|
||||||
OpportunityStatistics: destinationCompletion.OpportunityStatistics,
|
|
||||||
LocationCompletionPercent:
|
|
||||||
destinationCompletion.LocationCompletionPercent,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
const subLocationMap = new Map(
|
|
||||||
userProfile.Extensions.progression.PlayerProfileXP.Sublocations.map(
|
|
||||||
(obj) => [obj.Location, obj],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
for (const e of playerProfilePage.data.PlayerProfileXp.Seasons) {
|
|
||||||
for (const f of e.Locations) {
|
|
||||||
const subLocationData = subLocationMap.get(f.LocationId)
|
|
||||||
|
|
||||||
f.Xp = subLocationData?.Xp || 0
|
|
||||||
f.ActionXp = subLocationData?.ActionXp || 0
|
|
||||||
|
|
||||||
if (f.LocationProgression && !isSniperLocation(f.LocationId)) {
|
|
||||||
// We typecast below as it could be an object for subpackages.
|
|
||||||
// Checks before this ensure it isn't, but TS doesn't realise this.
|
|
||||||
f.LocationProgression.Level =
|
|
||||||
(
|
|
||||||
userProfile.Extensions.progression.Locations[
|
|
||||||
f.LocationId
|
|
||||||
] as ProgressionData
|
|
||||||
).Level || 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json(playerProfilePage)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
menuDataRouter.get(
|
menuDataRouter.get(
|
||||||
// who at IOI decided this was a good route name???!
|
// who at IOI decided this was a good route name???!
|
||||||
"/LookupContractDialogAddOrDeleteFromPlaylist",
|
"/LookupContractDialogAddOrDeleteFromPlaylist",
|
||||||
|
// @ts-expect-error Has jwt props.
|
||||||
withLookupDialog,
|
withLookupDialog,
|
||||||
)
|
)
|
||||||
|
|
||||||
menuDataRouter.get(
|
menuDataRouter.get(
|
||||||
// this one is sane Kappa
|
|
||||||
"/contractplaylist/addordelete/:contractId",
|
"/contractplaylist/addordelete/:contractId",
|
||||||
|
// @ts-expect-error Has jwt props.
|
||||||
directRoute,
|
directRoute,
|
||||||
)
|
)
|
||||||
|
|
||||||
menuDataRouter.post(
|
menuDataRouter.post(
|
||||||
"/contractplaylist/deletemultiple",
|
"/contractplaylist/deletemultiple",
|
||||||
jsonMiddleware(),
|
jsonMiddleware(),
|
||||||
|
// @ts-expect-error Has jwt props.
|
||||||
deleteMultiple,
|
deleteMultiple,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// @ts-expect-error Has jwt props.
|
||||||
menuDataRouter.get("/GetPlayerProfileXpData", (req: RequestWithJwt, res) => {
|
menuDataRouter.get("/GetPlayerProfileXpData", (req: RequestWithJwt, res) => {
|
||||||
const userData = getUserData(req.jwt.unique_name, req.gameVersion)
|
const userData = getUserData(req.jwt.unique_name, req.gameVersion)
|
||||||
|
|
||||||
@ -1342,7 +1306,13 @@ menuDataRouter.get("/GetPlayerProfileXpData", (req: RequestWithJwt, res) => {
|
|||||||
|
|
||||||
menuDataRouter.get(
|
menuDataRouter.get(
|
||||||
"/GetMasteryCompletionDataForLocation",
|
"/GetMasteryCompletionDataForLocation",
|
||||||
|
// @ts-expect-error Has jwt props.
|
||||||
(req: RequestWithJwt<GetCompletionDataForLocationQuery>, res) => {
|
(req: RequestWithJwt<GetCompletionDataForLocationQuery>, res) => {
|
||||||
|
if (!req.query.locationId) {
|
||||||
|
res.status(400).send("no location id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
res.json(
|
res.json(
|
||||||
generateCompletionData(
|
generateCompletionData(
|
||||||
req.query.locationId,
|
req.query.locationId,
|
||||||
@ -1355,6 +1325,7 @@ menuDataRouter.get(
|
|||||||
|
|
||||||
menuDataRouter.get(
|
menuDataRouter.get(
|
||||||
"/MasteryUnlockable",
|
"/MasteryUnlockable",
|
||||||
|
// @ts-expect-error Has jwt props.
|
||||||
(req: RequestWithJwt<MasteryUnlockableQuery>, res) => {
|
(req: RequestWithJwt<MasteryUnlockableQuery>, res) => {
|
||||||
let masteryUnlockTemplate = getConfig(
|
let masteryUnlockTemplate = getConfig(
|
||||||
"MasteryUnlockablesTemplate",
|
"MasteryUnlockablesTemplate",
|
||||||
@ -1402,32 +1373,30 @@ menuDataRouter.get(
|
|||||||
|
|
||||||
menuDataRouter.get(
|
menuDataRouter.get(
|
||||||
"/MasteryDataForLocation",
|
"/MasteryDataForLocation",
|
||||||
|
// @ts-expect-error Has jwt props.
|
||||||
(
|
(
|
||||||
req: RequestWithJwt<{
|
req: RequestWithJwt<{
|
||||||
locationId: string
|
locationId: string
|
||||||
}>,
|
}>,
|
||||||
res,
|
res,
|
||||||
) => {
|
) => {
|
||||||
res.json(
|
res.json({
|
||||||
controller.masteryService.getMasteryDataForLocation(
|
template: getConfig("MasteryDataForLocationTemplate", false),
|
||||||
|
data: controller.masteryService.getMasteryDataForLocation(
|
||||||
req.query.locationId,
|
req.query.locationId,
|
||||||
req.gameVersion,
|
req.gameVersion,
|
||||||
req.jwt.unique_name,
|
req.jwt.unique_name,
|
||||||
),
|
),
|
||||||
)
|
})
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
menuDataRouter.get(
|
menuDataRouter.get(
|
||||||
"/GetMasteryCompletionDataForUnlockable",
|
"/GetMasteryCompletionDataForUnlockable",
|
||||||
(
|
// @ts-expect-error Has jwt props.
|
||||||
req: RequestWithJwt<{
|
(req: RequestWithJwt<GetMasteryCompletionDataForUnlockableQuery>, res) => {
|
||||||
unlockableId: string
|
|
||||||
}>,
|
|
||||||
res,
|
|
||||||
) => {
|
|
||||||
// We make this lookup table to quickly get it, there's no other quick way for it.
|
// We make this lookup table to quickly get it, there's no other quick way for it.
|
||||||
const unlockToLoc = {
|
const unlockToLoc: Record<string, string> = {
|
||||||
FIREARMS_SC_HERO_SNIPER_HM: "LOCATION_PARENT_AUSTRIA",
|
FIREARMS_SC_HERO_SNIPER_HM: "LOCATION_PARENT_AUSTRIA",
|
||||||
FIREARMS_SC_HERO_SNIPER_KNIGHT: "LOCATION_PARENT_AUSTRIA",
|
FIREARMS_SC_HERO_SNIPER_KNIGHT: "LOCATION_PARENT_AUSTRIA",
|
||||||
FIREARMS_SC_HERO_SNIPER_STONE: "LOCATION_PARENT_AUSTRIA",
|
FIREARMS_SC_HERO_SNIPER_STONE: "LOCATION_PARENT_AUSTRIA",
|
||||||
|
@ -21,8 +21,8 @@ import type {
|
|||||||
Campaign,
|
Campaign,
|
||||||
GameVersion,
|
GameVersion,
|
||||||
GenSingleMissionFunc,
|
GenSingleMissionFunc,
|
||||||
ICampaignMission,
|
CampaignMission,
|
||||||
ICampaignVideo,
|
CampaignVideo,
|
||||||
IVideo,
|
IVideo,
|
||||||
StoryData,
|
StoryData,
|
||||||
} from "../types/types"
|
} from "../types/types"
|
||||||
@ -37,7 +37,7 @@ const genSingleMissionFactory = (userId: string): GenSingleMissionFunc => {
|
|||||||
return function genSingleMission(
|
return function genSingleMission(
|
||||||
contractId: string,
|
contractId: string,
|
||||||
gameVersion: GameVersion,
|
gameVersion: GameVersion,
|
||||||
): ICampaignMission {
|
): CampaignMission {
|
||||||
assert.ok(
|
assert.ok(
|
||||||
contractId,
|
contractId,
|
||||||
"Plugin tried to generate mission with no contract ID",
|
"Plugin tried to generate mission with no contract ID",
|
||||||
@ -51,11 +51,12 @@ const genSingleMissionFactory = (userId: string): GenSingleMissionFunc => {
|
|||||||
|
|
||||||
if (!actualContractData) {
|
if (!actualContractData) {
|
||||||
log(LogLevel.ERROR, `Failed to resolve contract ${contractId}!`)
|
log(LogLevel.ERROR, `Failed to resolve contract ${contractId}!`)
|
||||||
|
assert.fail(`Failed to resolve contract ${contractId}! (campaign)`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
Type: "Mission",
|
Type: "Mission",
|
||||||
Data: contractIdToHitObject(contractId, gameVersion, userId),
|
Data: contractIdToHitObject(contractId, gameVersion, userId)!,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -63,7 +64,7 @@ const genSingleMissionFactory = (userId: string): GenSingleMissionFunc => {
|
|||||||
function genSingleVideo(
|
function genSingleVideo(
|
||||||
videoId: string,
|
videoId: string,
|
||||||
gameVersion: GameVersion,
|
gameVersion: GameVersion,
|
||||||
): ICampaignVideo {
|
): CampaignVideo {
|
||||||
const videos = getConfig<Record<string, IVideo>>("Videos", true) // we modify videos so we need to clone this
|
const videos = getConfig<Record<string, IVideo>>("Videos", true) // we modify videos so we need to clone this
|
||||||
const video = videos[videoId]
|
const video = videos[videoId]
|
||||||
|
|
||||||
|
@ -18,16 +18,18 @@
|
|||||||
|
|
||||||
import { getConfig, getVersionedConfig } from "../configSwizzleManager"
|
import { getConfig, getVersionedConfig } from "../configSwizzleManager"
|
||||||
import type {
|
import type {
|
||||||
|
ChallengeCompletion,
|
||||||
|
CompiledChallengeTreeCategory,
|
||||||
CompletionData,
|
CompletionData,
|
||||||
GameLocationsData,
|
GameLocationsData,
|
||||||
GameVersion,
|
GameVersion,
|
||||||
IHit,
|
IHit,
|
||||||
JwtData,
|
|
||||||
MissionStory,
|
MissionStory,
|
||||||
OpportunityStatistics,
|
OpportunityStatistics,
|
||||||
PeacockLocationsData,
|
PeacockLocationsData,
|
||||||
Unlockable,
|
Unlockable,
|
||||||
} from "../types/types"
|
} from "../types/types"
|
||||||
|
import type { MasteryData } from "../types/mastery"
|
||||||
import { contractIdToHitObject, controller } from "../controller"
|
import { contractIdToHitObject, controller } from "../controller"
|
||||||
import { generateCompletionData } from "../contracts/dataGen"
|
import { generateCompletionData } from "../contracts/dataGen"
|
||||||
import { getUserData } from "../databaseHandler"
|
import { getUserData } from "../databaseHandler"
|
||||||
@ -37,6 +39,17 @@ import { createInventory } from "../inventory"
|
|||||||
import { log, LogLevel } from "../loggingInterop"
|
import { log, LogLevel } from "../loggingInterop"
|
||||||
import { no2016 } from "../contracts/escalations/escalationService"
|
import { no2016 } from "../contracts/escalations/escalationService"
|
||||||
import { missionsInLocations } from "../contracts/missionsInLocation"
|
import { missionsInLocations } from "../contracts/missionsInLocation"
|
||||||
|
import assert from "assert"
|
||||||
|
|
||||||
|
type LegacyData = {
|
||||||
|
[difficulty: string]: {
|
||||||
|
ChallengeCompletion: {
|
||||||
|
ChallengesCount: number
|
||||||
|
CompletedChallengesCount: number
|
||||||
|
}
|
||||||
|
CompletionData: CompletionData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type GameFacingDestination = {
|
type GameFacingDestination = {
|
||||||
ChallengeCompletion: {
|
ChallengeCompletion: {
|
||||||
@ -48,14 +61,45 @@ type GameFacingDestination = {
|
|||||||
LocationCompletionPercent: number
|
LocationCompletionPercent: number
|
||||||
Location: Unlockable
|
Location: Unlockable
|
||||||
// H2016 only
|
// H2016 only
|
||||||
Data?: {
|
Data?: LegacyData
|
||||||
[difficulty: string]: {
|
}
|
||||||
ChallengeCompletion: {
|
|
||||||
ChallengesCount: number
|
type LocationMissionData = {
|
||||||
CompletedChallengesCount: number
|
Location: Unlockable
|
||||||
}
|
SubLocation: Unlockable
|
||||||
CompletionData: CompletionData
|
Missions: IHit[]
|
||||||
|
SarajevoSixMissions: IHit[]
|
||||||
|
ElusiveMissions: IHit[]
|
||||||
|
EscalationMissions: IHit[]
|
||||||
|
SniperMissions: IHit[]
|
||||||
|
PlaceholderMissions: IHit[]
|
||||||
|
CampaignMissions: IHit[]
|
||||||
|
CompletionData: CompletionData
|
||||||
|
}
|
||||||
|
|
||||||
|
type GameDestination = {
|
||||||
|
ChallengeData: {
|
||||||
|
Children: CompiledChallengeTreeCategory[]
|
||||||
|
}
|
||||||
|
DifficultyData: {
|
||||||
|
AvailableDifficultyModes: {
|
||||||
|
Name: string
|
||||||
|
Available: boolean
|
||||||
|
}[]
|
||||||
|
Difficulty: string | undefined
|
||||||
|
LocationId: string
|
||||||
|
}
|
||||||
|
Location: Unlockable
|
||||||
|
MasteryData: MasteryData | MasteryData[] | Record<string, never>
|
||||||
|
MissionData: {
|
||||||
|
ChallengeCompletion: ChallengeCompletion
|
||||||
|
Location: Unlockable
|
||||||
|
LocationCompletionPercent: number
|
||||||
|
OpportunityStatistics: {
|
||||||
|
Completed: number
|
||||||
|
Count: number
|
||||||
}
|
}
|
||||||
|
SubLocationMissionsData: LocationMissionData[]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,14 +107,14 @@ export function getDestinationCompletion(
|
|||||||
parent: Unlockable,
|
parent: Unlockable,
|
||||||
child: Unlockable | undefined,
|
child: Unlockable | undefined,
|
||||||
gameVersion: GameVersion,
|
gameVersion: GameVersion,
|
||||||
jwt: JwtData,
|
userId: string,
|
||||||
) {
|
) {
|
||||||
const missionStories = getConfig<Record<string, MissionStory>>(
|
const missionStories = getConfig<Record<string, MissionStory>>(
|
||||||
"MissionStories",
|
"MissionStories",
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
|
|
||||||
const userData = getUserData(jwt.unique_name, gameVersion)
|
const userData = getUserData(userId, gameVersion)
|
||||||
const challenges = controller.challengeService.getGroupedChallengeLists(
|
const challenges = controller.challengeService.getGroupedChallengeLists(
|
||||||
{
|
{
|
||||||
type: ChallengeFilterType.ParentLocation,
|
type: ChallengeFilterType.ParentLocation,
|
||||||
@ -123,21 +167,10 @@ export function getCompletionPercent(
|
|||||||
opportunityDone: number,
|
opportunityDone: number,
|
||||||
opportunityTotal: number,
|
opportunityTotal: number,
|
||||||
): number {
|
): number {
|
||||||
if (challengeDone === undefined) {
|
challengeDone ??= 0
|
||||||
challengeDone = 0
|
challengeTotal ??= 0
|
||||||
}
|
opportunityDone ??= 0
|
||||||
|
opportunityTotal ??= 0
|
||||||
if (challengeTotal === undefined) {
|
|
||||||
challengeTotal = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opportunityDone === undefined) {
|
|
||||||
opportunityDone = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opportunityTotal === undefined) {
|
|
||||||
opportunityTotal = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
const totalCompletables = challengeTotal + opportunityTotal
|
const totalCompletables = challengeTotal + opportunityTotal
|
||||||
const totalCompleted = challengeDone + opportunityDone
|
const totalCompleted = challengeDone + opportunityDone
|
||||||
@ -150,11 +183,11 @@ export function getCompletionPercent(
|
|||||||
* Get the list of destinations used by the `/profiles/page/Destinations` endpoint.
|
* Get the list of destinations used by the `/profiles/page/Destinations` endpoint.
|
||||||
*
|
*
|
||||||
* @param gameVersion
|
* @param gameVersion
|
||||||
* @param jwt
|
* @param userId The user ID.
|
||||||
*/
|
*/
|
||||||
export function getAllGameDestinations(
|
export function getAllGameDestinations(
|
||||||
gameVersion: GameVersion,
|
gameVersion: GameVersion,
|
||||||
jwt: JwtData,
|
userId: string,
|
||||||
): GameFacingDestination[] {
|
): GameFacingDestination[] {
|
||||||
const result: GameFacingDestination[] = []
|
const result: GameFacingDestination[] = []
|
||||||
const locations = getVersionedConfig<PeacockLocationsData>(
|
const locations = getVersionedConfig<PeacockLocationsData>(
|
||||||
@ -169,38 +202,13 @@ export function getAllGameDestinations(
|
|||||||
"UI_LOCATION_PARENT_" + destination.substring(16) + "_NAME"
|
"UI_LOCATION_PARENT_" + destination.substring(16) + "_NAME"
|
||||||
|
|
||||||
const template: GameFacingDestination = {
|
const template: GameFacingDestination = {
|
||||||
...getDestinationCompletion(parent, undefined, gameVersion, jwt),
|
...getDestinationCompletion(parent, undefined, gameVersion, userId),
|
||||||
...{
|
...{
|
||||||
CompletionData: generateCompletionData(
|
CompletionData: generateCompletionData(
|
||||||
destination,
|
destination,
|
||||||
jwt.unique_name,
|
userId,
|
||||||
gameVersion,
|
gameVersion,
|
||||||
),
|
),
|
||||||
Data:
|
|
||||||
gameVersion === "h1"
|
|
||||||
? {
|
|
||||||
normal: {
|
|
||||||
ChallengeCompletion: undefined,
|
|
||||||
CompletionData: generateCompletionData(
|
|
||||||
destination,
|
|
||||||
jwt.unique_name,
|
|
||||||
gameVersion,
|
|
||||||
"mission",
|
|
||||||
"normal",
|
|
||||||
),
|
|
||||||
},
|
|
||||||
pro1: {
|
|
||||||
ChallengeCompletion: undefined,
|
|
||||||
CompletionData: generateCompletionData(
|
|
||||||
destination,
|
|
||||||
jwt.unique_name,
|
|
||||||
gameVersion,
|
|
||||||
"mission",
|
|
||||||
"pro1",
|
|
||||||
),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -208,10 +216,28 @@ export function getAllGameDestinations(
|
|||||||
// There are different challenges for normal and pro1 in 2016, right now, we do not support this.
|
// There are different challenges for normal and pro1 in 2016, right now, we do not support this.
|
||||||
// We're just reusing this for now.
|
// We're just reusing this for now.
|
||||||
if (gameVersion === "h1") {
|
if (gameVersion === "h1") {
|
||||||
template.Data.normal.ChallengeCompletion =
|
template.Data = {
|
||||||
template.ChallengeCompletion
|
normal: {
|
||||||
template.Data.pro1.ChallengeCompletion =
|
ChallengeCompletion: template.ChallengeCompletion,
|
||||||
template.ChallengeCompletion
|
CompletionData: generateCompletionData(
|
||||||
|
destination,
|
||||||
|
userId,
|
||||||
|
gameVersion,
|
||||||
|
"mission",
|
||||||
|
"normal",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
pro1: {
|
||||||
|
ChallengeCompletion: template.ChallengeCompletion,
|
||||||
|
CompletionData: generateCompletionData(
|
||||||
|
destination,
|
||||||
|
userId,
|
||||||
|
gameVersion,
|
||||||
|
"mission",
|
||||||
|
"pro1",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
} satisfies LegacyData
|
||||||
}
|
}
|
||||||
|
|
||||||
result.push(template)
|
result.push(template)
|
||||||
@ -258,10 +284,15 @@ export function createLocationsData(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sublocation = locData.children[sublocationId]
|
const sublocation = locData.children[sublocationId]
|
||||||
|
|
||||||
|
if (!sublocation.Properties.ParentLocation) {
|
||||||
|
assert.fail("sublocation has no parent, that's illegal")
|
||||||
|
}
|
||||||
|
|
||||||
const parentLocation =
|
const parentLocation =
|
||||||
locData.parents[sublocation.Properties.ParentLocation]
|
locData.parents[sublocation.Properties.ParentLocation]
|
||||||
const creationContract = controller.resolveContract(
|
const creationContract = controller.resolveContract(
|
||||||
sublocation.Properties.CreateContractId,
|
sublocation.Properties.CreateContractId!,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!creationContract && excludeIfNoContracts) {
|
if (!creationContract && excludeIfNoContracts) {
|
||||||
@ -288,12 +319,18 @@ export function createLocationsData(
|
|||||||
return finalData
|
return finalData
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: this is a mess, write docs and type explicitly
|
/**
|
||||||
|
* This gets the game-facing data for a destination.
|
||||||
|
*
|
||||||
|
* @param query
|
||||||
|
* @param gameVersion
|
||||||
|
* @param userId
|
||||||
|
*/
|
||||||
export function getDestination(
|
export function getDestination(
|
||||||
query: GetDestinationQuery,
|
query: GetDestinationQuery,
|
||||||
gameVersion: GameVersion,
|
gameVersion: GameVersion,
|
||||||
jwt: JwtData,
|
userId: string,
|
||||||
) {
|
): GameDestination {
|
||||||
const LOCATION = query.locationId
|
const LOCATION = query.locationId
|
||||||
|
|
||||||
const locData = getVersionedConfig<PeacockLocationsData>(
|
const locData = getVersionedConfig<PeacockLocationsData>(
|
||||||
@ -306,18 +343,30 @@ export function getDestination(
|
|||||||
const masteryData = controller.masteryService.getMasteryDataForDestination(
|
const masteryData = controller.masteryService.getMasteryDataForDestination(
|
||||||
query.locationId,
|
query.locationId,
|
||||||
gameVersion,
|
gameVersion,
|
||||||
jwt.unique_name,
|
userId,
|
||||||
query.difficulty,
|
query.difficulty,
|
||||||
)
|
)
|
||||||
|
|
||||||
const response = {
|
let resMasteryData: GameDestination["MasteryData"]
|
||||||
Location: {},
|
|
||||||
|
if (LOCATION !== "LOCATION_PARENT_ICA_FACILITY") {
|
||||||
|
if (gameVersion === "h1") {
|
||||||
|
resMasteryData = masteryData[0]
|
||||||
|
} else {
|
||||||
|
resMasteryData = masteryData
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resMasteryData = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const response: Partial<GameDestination> = {
|
||||||
|
Location: locationData,
|
||||||
MissionData: {
|
MissionData: {
|
||||||
...getDestinationCompletion(
|
...getDestinationCompletion(
|
||||||
locationData,
|
locationData,
|
||||||
undefined,
|
undefined,
|
||||||
gameVersion,
|
gameVersion,
|
||||||
jwt,
|
userId,
|
||||||
),
|
),
|
||||||
...{ SubLocationMissionsData: [] },
|
...{ SubLocationMissionsData: [] },
|
||||||
},
|
},
|
||||||
@ -326,20 +375,14 @@ export function getDestination(
|
|||||||
controller.challengeService.getChallengeDataForDestination(
|
controller.challengeService.getChallengeDataForDestination(
|
||||||
query.locationId,
|
query.locationId,
|
||||||
gameVersion,
|
gameVersion,
|
||||||
jwt.unique_name,
|
userId,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
MasteryData:
|
MasteryData: resMasteryData,
|
||||||
LOCATION !== "LOCATION_PARENT_ICA_FACILITY"
|
|
||||||
? gameVersion === "h1"
|
|
||||||
? masteryData[0]
|
|
||||||
: masteryData
|
|
||||||
: {},
|
|
||||||
DifficultyData: undefined,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (gameVersion === "h1" && LOCATION !== "LOCATION_PARENT_ICA_FACILITY") {
|
if (gameVersion === "h1" && LOCATION !== "LOCATION_PARENT_ICA_FACILITY") {
|
||||||
const inventory = createInventory(jwt.unique_name, gameVersion)
|
const inventory = createInventory(userId, gameVersion)
|
||||||
|
|
||||||
response.DifficultyData = {
|
response.DifficultyData = {
|
||||||
AvailableDifficultyModes: [
|
AvailableDifficultyModes: [
|
||||||
@ -352,7 +395,7 @@ export function getDestination(
|
|||||||
Available: inventory.some(
|
Available: inventory.some(
|
||||||
(e) =>
|
(e) =>
|
||||||
e.Unlockable.Id ===
|
e.Unlockable.Id ===
|
||||||
locationData.Properties.DifficultyUnlock.pro1,
|
locationData.Properties.DifficultyUnlock?.pro1,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -369,15 +412,15 @@ export function getDestination(
|
|||||||
(subLocation) => subLocation.Properties.ParentLocation === LOCATION,
|
(subLocation) => subLocation.Properties.ParentLocation === LOCATION,
|
||||||
)
|
)
|
||||||
|
|
||||||
response.Location = locationData
|
|
||||||
|
|
||||||
if (query.difficulty === "pro1") {
|
if (query.difficulty === "pro1") {
|
||||||
const obj = {
|
type Cast = keyof typeof controller.missionsInLocations.pro1
|
||||||
|
|
||||||
|
const obj: LocationMissionData = {
|
||||||
Location: locationData,
|
Location: locationData,
|
||||||
SubLocation: locationData,
|
SubLocation: locationData,
|
||||||
Missions: [controller.missionsInLocations.pro1[LOCATION]].map(
|
Missions: [controller.missionsInLocations.pro1[LOCATION as Cast]]
|
||||||
(id) => contractIdToHitObject(id, gameVersion, jwt.unique_name),
|
.map((id) => contractIdToHitObject(id, gameVersion, userId))
|
||||||
),
|
.filter(Boolean) as IHit[],
|
||||||
SarajevoSixMissions: [],
|
SarajevoSixMissions: [],
|
||||||
ElusiveMissions: [],
|
ElusiveMissions: [],
|
||||||
EscalationMissions: [],
|
EscalationMissions: [],
|
||||||
@ -386,14 +429,14 @@ export function getDestination(
|
|||||||
CampaignMissions: [],
|
CampaignMissions: [],
|
||||||
CompletionData: generateCompletionData(
|
CompletionData: generateCompletionData(
|
||||||
sublocationsData[0].Id,
|
sublocationsData[0].Id,
|
||||||
jwt.unique_name,
|
userId,
|
||||||
gameVersion,
|
gameVersion,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
response.MissionData.SubLocationMissionsData.push(obj)
|
response.MissionData?.SubLocationMissionsData.push(obj)
|
||||||
|
|
||||||
return response
|
return response as GameDestination
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const e of sublocationsData) {
|
for (const e of sublocationsData) {
|
||||||
@ -401,6 +444,7 @@ export function getDestination(
|
|||||||
|
|
||||||
const escalations: IHit[] = []
|
const escalations: IHit[] = []
|
||||||
|
|
||||||
|
type ECast = keyof typeof controller.missionsInLocations.escalations
|
||||||
// every unique escalation from the sublocation
|
// every unique escalation from the sublocation
|
||||||
const allUniqueEscalations: string[] = [
|
const allUniqueEscalations: string[] = [
|
||||||
...(gameVersion === "h1" && e.Id === "LOCATION_ICA_FACILITY"
|
...(gameVersion === "h1" && e.Id === "LOCATION_ICA_FACILITY"
|
||||||
@ -409,7 +453,7 @@ export function getDestination(
|
|||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
...new Set<string>(
|
...new Set<string>(
|
||||||
controller.missionsInLocations.escalations[e.Id] || [],
|
controller.missionsInLocations.escalations[e.Id as ECast] || [],
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -419,7 +463,7 @@ export function getDestination(
|
|||||||
const details = contractIdToHitObject(
|
const details = contractIdToHitObject(
|
||||||
escalation,
|
escalation,
|
||||||
gameVersion,
|
gameVersion,
|
||||||
jwt.unique_name,
|
userId,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (details) {
|
if (details) {
|
||||||
@ -428,17 +472,18 @@ export function getDestination(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sniperMissions: IHit[] = []
|
const sniperMissions: IHit[] = []
|
||||||
|
type SCast = keyof typeof controller.missionsInLocations.sniper
|
||||||
|
|
||||||
for (const sniperMission of controller.missionsInLocations.sniper[
|
for (const sniperMission of controller.missionsInLocations.sniper[
|
||||||
e.Id
|
e.Id as SCast
|
||||||
] ?? []) {
|
] ?? []) {
|
||||||
sniperMissions.push(
|
const hit = contractIdToHitObject(
|
||||||
contractIdToHitObject(
|
sniperMission,
|
||||||
sniperMission,
|
gameVersion,
|
||||||
gameVersion,
|
userId,
|
||||||
jwt.unique_name,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (hit) sniperMissions.push(hit)
|
||||||
}
|
}
|
||||||
|
|
||||||
const obj = {
|
const obj = {
|
||||||
@ -451,11 +496,7 @@ export function getDestination(
|
|||||||
SniperMissions: sniperMissions,
|
SniperMissions: sniperMissions,
|
||||||
PlaceholderMissions: [],
|
PlaceholderMissions: [],
|
||||||
CampaignMissions: [],
|
CampaignMissions: [],
|
||||||
CompletionData: generateCompletionData(
|
CompletionData: generateCompletionData(e.Id, userId, gameVersion),
|
||||||
e.Id,
|
|
||||||
jwt.unique_name,
|
|
||||||
gameVersion,
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const types = [
|
const types = [
|
||||||
@ -464,6 +505,7 @@ export function getDestination(
|
|||||||
["elusive", "ElusiveMissions"],
|
["elusive", "ElusiveMissions"],
|
||||||
],
|
],
|
||||||
...((gameVersion === "h1" &&
|
...((gameVersion === "h1" &&
|
||||||
|
// @ts-expect-error Hack.
|
||||||
missionsInLocations.sarajevo["h2016enabled"]) ||
|
missionsInLocations.sarajevo["h2016enabled"]) ||
|
||||||
gameVersion === "h3"
|
gameVersion === "h3"
|
||||||
? [["sarajevo", "SarajevoSixMissions"]]
|
? [["sarajevo", "SarajevoSixMissions"]]
|
||||||
@ -472,8 +514,10 @@ export function getDestination(
|
|||||||
|
|
||||||
for (const t of types) {
|
for (const t of types) {
|
||||||
let theMissions: string[] | undefined = !t[0] // no specific type
|
let theMissions: string[] | undefined = !t[0] // no specific type
|
||||||
? controller.missionsInLocations[e.Id]
|
? // @ts-expect-error Yup.
|
||||||
: controller.missionsInLocations[t[0]][e.Id]
|
controller.missionsInLocations[e.Id]
|
||||||
|
: // @ts-expect-error Yup.
|
||||||
|
controller.missionsInLocations[t[0]][e.Id]
|
||||||
|
|
||||||
// edge case: ica facility in h1 was only 1 sublocation, so we merge
|
// edge case: ica facility in h1 was only 1 sublocation, so we merge
|
||||||
// these into a single array
|
// these into a single array
|
||||||
@ -504,16 +548,17 @@ export function getDestination(
|
|||||||
const mission = contractIdToHitObject(
|
const mission = contractIdToHitObject(
|
||||||
c,
|
c,
|
||||||
gameVersion,
|
gameVersion,
|
||||||
jwt.unique_name,
|
userId,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// @ts-expect-error Yup.
|
||||||
obj[t[1]].push(mission)
|
obj[t[1]].push(mission)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
response.MissionData.SubLocationMissionsData.push(obj)
|
response.MissionData?.SubLocationMissionsData.push(obj)
|
||||||
}
|
}
|
||||||
|
|
||||||
return response
|
return response as GameDestination
|
||||||
}
|
}
|
||||||
|
@ -82,7 +82,7 @@ export function withLookupDialog(
|
|||||||
contract,
|
contract,
|
||||||
req.jwt.unique_name,
|
req.jwt.unique_name,
|
||||||
req.gameVersion,
|
req.gameVersion,
|
||||||
),
|
)!,
|
||||||
},
|
},
|
||||||
...(flag && { AddedSuccessfully: true }),
|
...(flag && { AddedSuccessfully: true }),
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,12 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { GameVersion, JwtData, PeacockLocationsData } from "../types/types"
|
import type {
|
||||||
|
CompletionData,
|
||||||
|
GameVersion,
|
||||||
|
PeacockLocationsData,
|
||||||
|
Unlockable,
|
||||||
|
} from "../types/types"
|
||||||
import { swapToBrowsingMenusStatus } from "../discordRp"
|
import { swapToBrowsingMenusStatus } from "../discordRp"
|
||||||
import { getUserData } from "../databaseHandler"
|
import { getUserData } from "../databaseHandler"
|
||||||
import { controller } from "../controller"
|
import { controller } from "../controller"
|
||||||
@ -29,10 +34,32 @@ import {
|
|||||||
import { createLocationsData, getAllGameDestinations } from "./destinations"
|
import { createLocationsData, getAllGameDestinations } from "./destinations"
|
||||||
import { makeCampaigns } from "./campaigns"
|
import { makeCampaigns } from "./campaigns"
|
||||||
|
|
||||||
export function getHubData(gameVersion: GameVersion, jwt: JwtData) {
|
type CareerEntry = {
|
||||||
|
Children: CareerEntryChild[]
|
||||||
|
Name: string
|
||||||
|
Location: Unlockable
|
||||||
|
}
|
||||||
|
|
||||||
|
type CareerEntryChild = {
|
||||||
|
IsLocked: boolean
|
||||||
|
Name: string
|
||||||
|
Image: string
|
||||||
|
Icon: string
|
||||||
|
CompletedChallengesCount: number
|
||||||
|
ChallengesCount: number
|
||||||
|
CategoryId: string
|
||||||
|
Description: string
|
||||||
|
Location: Unlockable
|
||||||
|
ImageLocked: string
|
||||||
|
RequiredResources: string[]
|
||||||
|
IsPack?: boolean
|
||||||
|
CompletionData: CompletionData
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getHubData(gameVersion: GameVersion, userId: string) {
|
||||||
swapToBrowsingMenusStatus(gameVersion)
|
swapToBrowsingMenusStatus(gameVersion)
|
||||||
|
|
||||||
const userdata = getUserData(jwt.unique_name, gameVersion)
|
const userdata = getUserData(userId, gameVersion)
|
||||||
|
|
||||||
const contractCreationTutorial =
|
const contractCreationTutorial =
|
||||||
gameVersion !== "scpc"
|
gameVersion !== "scpc"
|
||||||
@ -44,7 +71,7 @@ export function getHubData(gameVersion: GameVersion, jwt: JwtData) {
|
|||||||
gameVersion,
|
gameVersion,
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
const career =
|
const career: Record<string, CareerEntry> =
|
||||||
gameVersion === "h3"
|
gameVersion === "h3"
|
||||||
? {}
|
? {}
|
||||||
: {
|
: {
|
||||||
@ -73,7 +100,7 @@ export function getHubData(gameVersion: GameVersion, jwt: JwtData) {
|
|||||||
controller.masteryService.getMasteryDataForDestination(
|
controller.masteryService.getMasteryDataForDestination(
|
||||||
parent,
|
parent,
|
||||||
gameVersion,
|
gameVersion,
|
||||||
jwt.unique_name,
|
userId,
|
||||||
).length
|
).length
|
||||||
) {
|
) {
|
||||||
const completionData =
|
const completionData =
|
||||||
@ -81,7 +108,7 @@ export function getHubData(gameVersion: GameVersion, jwt: JwtData) {
|
|||||||
parent,
|
parent,
|
||||||
parent,
|
parent,
|
||||||
gameVersion,
|
gameVersion,
|
||||||
jwt.unique_name,
|
userId,
|
||||||
parent.includes("SNUG") ? "evergreen" : "mission",
|
parent.includes("SNUG") ? "evergreen" : "mission",
|
||||||
gameVersion === "h1" ? "normal" : undefined,
|
gameVersion === "h1" ? "normal" : undefined,
|
||||||
)
|
)
|
||||||
@ -100,7 +127,7 @@ export function getHubData(gameVersion: GameVersion, jwt: JwtData) {
|
|||||||
parent,
|
parent,
|
||||||
parent,
|
parent,
|
||||||
gameVersion,
|
gameVersion,
|
||||||
jwt.unique_name,
|
userId,
|
||||||
parent.includes("SNUG")
|
parent.includes("SNUG")
|
||||||
? "evergreen"
|
? "evergreen"
|
||||||
: "mission",
|
: "mission",
|
||||||
@ -137,14 +164,14 @@ export function getHubData(gameVersion: GameVersion, jwt: JwtData) {
|
|||||||
const challengeCompletion =
|
const challengeCompletion =
|
||||||
controller.challengeService.countTotalNCompletedChallenges(
|
controller.challengeService.countTotalNCompletedChallenges(
|
||||||
challenges,
|
challenges,
|
||||||
jwt.unique_name,
|
userId,
|
||||||
gameVersion,
|
gameVersion,
|
||||||
)
|
)
|
||||||
|
|
||||||
career[parent]?.Children.push({
|
career[parent!]?.Children.push({
|
||||||
IsLocked: location.Properties.IsLocked,
|
IsLocked: Boolean(location.Properties.IsLocked),
|
||||||
Name: location.DisplayNameLocKey,
|
Name: location.DisplayNameLocKey,
|
||||||
Image: location.Properties.Icon,
|
Image: location.Properties.Icon || "",
|
||||||
Icon: location.Type, // should be "location" for all locations
|
Icon: location.Type, // should be "location" for all locations
|
||||||
CompletedChallengesCount:
|
CompletedChallengesCount:
|
||||||
challengeCompletion.CompletedChallengesCount,
|
challengeCompletion.CompletedChallengesCount,
|
||||||
@ -152,14 +179,10 @@ export function getHubData(gameVersion: GameVersion, jwt: JwtData) {
|
|||||||
CategoryId: child,
|
CategoryId: child,
|
||||||
Description: `UI_${child}_PRIMARY_DESC`,
|
Description: `UI_${child}_PRIMARY_DESC`,
|
||||||
Location: location,
|
Location: location,
|
||||||
ImageLocked: location.Properties.LockedIcon,
|
ImageLocked: location.Properties.LockedIcon || "",
|
||||||
RequiredResources: location.Properties.RequiredResources,
|
RequiredResources: location.Properties.RequiredResources || [],
|
||||||
IsPack: false, // should be false for all locations
|
IsPack: false, // should be false for all locations
|
||||||
CompletionData: generateCompletionData(
|
CompletionData: generateCompletionData(child, userId, gameVersion),
|
||||||
child,
|
|
||||||
jwt.unique_name,
|
|
||||||
gameVersion,
|
|
||||||
),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -176,10 +199,10 @@ export function getHubData(gameVersion: GameVersion, jwt: JwtData) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
DashboardData: [],
|
DashboardData: [],
|
||||||
DestinationsData: getAllGameDestinations(gameVersion, jwt),
|
DestinationsData: getAllGameDestinations(gameVersion, userId),
|
||||||
CreateContractTutorial: generateUserCentric(
|
CreateContractTutorial: generateUserCentric(
|
||||||
contractCreationTutorial,
|
contractCreationTutorial,
|
||||||
jwt.unique_name,
|
userId,
|
||||||
gameVersion,
|
gameVersion,
|
||||||
),
|
),
|
||||||
LocationsData: createLocationsData(gameVersion, true),
|
LocationsData: createLocationsData(gameVersion, true),
|
||||||
@ -189,7 +212,7 @@ export function getHubData(gameVersion: GameVersion, jwt: JwtData) {
|
|||||||
},
|
},
|
||||||
MasteryData: masteryData,
|
MasteryData: masteryData,
|
||||||
},
|
},
|
||||||
StoryData: makeCampaigns(gameVersion, jwt.unique_name),
|
StoryData: makeCampaigns(gameVersion, userId),
|
||||||
FilterData: getVersionedConfig("FilterData", gameVersion, false),
|
FilterData: getVersionedConfig("FilterData", gameVersion, false),
|
||||||
StoreData: getVersionedConfig("StoreData", gameVersion, false),
|
StoreData: getVersionedConfig("StoreData", gameVersion, false),
|
||||||
IOIAccountStatus: {
|
IOIAccountStatus: {
|
||||||
|
@ -544,8 +544,11 @@ export const menuSystemDatabase = new MenuSystemDatabase()
|
|||||||
|
|
||||||
menuSystemRouter.get(
|
menuSystemRouter.get(
|
||||||
"/dynamic_resources_pc_release_rpkg",
|
"/dynamic_resources_pc_release_rpkg",
|
||||||
|
// @ts-expect-error No type issue is actually here.
|
||||||
async (req: RequestWithJwt, res) => {
|
async (req: RequestWithJwt, res) => {
|
||||||
const dynamicResourceName = `dynamic_resources_${req.gameVersion}.rpkg`
|
const dynamicResourceName = `dynamic_resources_${
|
||||||
|
req.gameVersion === "scpc" ? "h1" : req.gameVersion
|
||||||
|
}.rpkg`
|
||||||
const dynamicResourcePath = join(
|
const dynamicResourcePath = join(
|
||||||
PEACOCK_DEV ? process.cwd() : __dirname,
|
PEACOCK_DEV ? process.cwd() : __dirname,
|
||||||
"resources",
|
"resources",
|
||||||
@ -565,6 +568,7 @@ menuSystemRouter.get(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// @ts-expect-error No type issue is actually here.
|
||||||
menuSystemRouter.use("/menusystem/", MenuSystemDatabase.configMiddleware)
|
menuSystemRouter.use("/menusystem/", MenuSystemDatabase.configMiddleware)
|
||||||
|
|
||||||
// Miranda Jamison's image path in the repository is escaped for some reason
|
// Miranda Jamison's image path in the repository is escaped for some reason
|
||||||
@ -587,6 +591,7 @@ menuSystemPreRouter.get(
|
|||||||
|
|
||||||
menuSystemRouter.use(
|
menuSystemRouter.use(
|
||||||
"/images/",
|
"/images/",
|
||||||
|
// @ts-expect-error No type issue is actually here.
|
||||||
serveStatic("images", { fallthrough: true }),
|
serveStatic("images", { fallthrough: true }),
|
||||||
imageFetchingMiddleware,
|
imageFetchingMiddleware,
|
||||||
)
|
)
|
||||||
|
@ -19,9 +19,9 @@
|
|||||||
import type {
|
import type {
|
||||||
CompiledChallengeTreeCategory,
|
CompiledChallengeTreeCategory,
|
||||||
GameVersion,
|
GameVersion,
|
||||||
JwtData,
|
|
||||||
MissionManifest,
|
MissionManifest,
|
||||||
MissionStory,
|
MissionStory,
|
||||||
|
ProgressionData,
|
||||||
SceneConfig,
|
SceneConfig,
|
||||||
Unlockable,
|
Unlockable,
|
||||||
UserCentricContract,
|
UserCentricContract,
|
||||||
@ -51,11 +51,12 @@ import {
|
|||||||
} from "../utils"
|
} from "../utils"
|
||||||
|
|
||||||
import { createInventory, getUnlockableById } from "../inventory"
|
import { createInventory, getUnlockableById } from "../inventory"
|
||||||
import { createSniperLoadouts } from "./sniper"
|
import { createSniperLoadouts, SniperCharacter, SniperLoadout } from "./sniper"
|
||||||
import { getFlag } from "../flags"
|
import { getFlag } from "../flags"
|
||||||
import { loadouts } from "../loadouts"
|
import { loadouts } from "../loadouts"
|
||||||
import { resolveProfiles } from "../profileHandler"
|
import { resolveProfiles } from "../profileHandler"
|
||||||
import { userAuths } from "../officialServerAuth"
|
import { userAuths } from "../officialServerAuth"
|
||||||
|
import assert from "assert"
|
||||||
|
|
||||||
export type PlanningError = { error: boolean }
|
export type PlanningError = { error: boolean }
|
||||||
|
|
||||||
@ -77,11 +78,11 @@ export type GamePlanningData = {
|
|||||||
IsFirstInGroup: boolean
|
IsFirstInGroup: boolean
|
||||||
Creator: UserProfile
|
Creator: UserProfile
|
||||||
UserContract?: boolean
|
UserContract?: boolean
|
||||||
UnlockedEntrances?: string[]
|
UnlockedEntrances?: string[] | null
|
||||||
UnlockedAgencyPickups?: string[]
|
UnlockedAgencyPickups?: string[] | null
|
||||||
Objectives?: unknown
|
Objectives?: unknown
|
||||||
GroupData?: PlanningGroupData
|
GroupData?: PlanningGroupData
|
||||||
Entrances: Unlockable[]
|
Entrances: Unlockable[] | null
|
||||||
Location: Unlockable
|
Location: Unlockable
|
||||||
LoadoutData: unknown
|
LoadoutData: unknown
|
||||||
LimitedLoadoutUnlockLevel: number
|
LimitedLoadoutUnlockLevel: number
|
||||||
@ -113,7 +114,7 @@ export type GamePlanningData = {
|
|||||||
export async function getPlanningData(
|
export async function getPlanningData(
|
||||||
contractId: string,
|
contractId: string,
|
||||||
resetEscalation: boolean,
|
resetEscalation: boolean,
|
||||||
jwt: JwtData,
|
userId: string,
|
||||||
gameVersion: GameVersion,
|
gameVersion: GameVersion,
|
||||||
): Promise<PlanningError | GamePlanningData> {
|
): Promise<PlanningError | GamePlanningData> {
|
||||||
const entranceData = getConfig<SceneConfig>("Entrances", false)
|
const entranceData = getConfig<SceneConfig>("Entrances", false)
|
||||||
@ -122,7 +123,7 @@ export async function getPlanningData(
|
|||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
|
|
||||||
const userData = getUserData(jwt.unique_name, gameVersion)
|
const userData = getUserData(userId, gameVersion)
|
||||||
|
|
||||||
for (const ms in userData.Extensions.opportunityprogression) {
|
for (const ms in userData.Extensions.opportunityprogression) {
|
||||||
if (Object.keys(missionStories).includes(ms)) {
|
if (Object.keys(missionStories).includes(ms)) {
|
||||||
@ -130,13 +131,24 @@ export async function getPlanningData(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let contractData =
|
let contractData: MissionManifest | undefined
|
||||||
|
|
||||||
|
if (
|
||||||
gameVersion === "h1" &&
|
gameVersion === "h1" &&
|
||||||
contractId === "42bac555-bbb9-429d-a8ce-f1ffdf94211c"
|
contractId === "42bac555-bbb9-429d-a8ce-f1ffdf94211c"
|
||||||
? _legacyBull
|
) {
|
||||||
: contractId === "ff9f46cf-00bd-4c12-b887-eac491c3a96d"
|
contractData = _legacyBull
|
||||||
? _theLastYardbirdScpc
|
} else if (contractId === "ff9f46cf-00bd-4c12-b887-eac491c3a96d") {
|
||||||
: controller.resolveContract(contractId)
|
contractData = _theLastYardbirdScpc
|
||||||
|
} else {
|
||||||
|
contractData = controller.resolveContract(contractId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!contractData) {
|
||||||
|
return {
|
||||||
|
error: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (resetEscalation) {
|
if (resetEscalation) {
|
||||||
const escalationGroupId =
|
const escalationGroupId =
|
||||||
@ -144,10 +156,20 @@ export async function getPlanningData(
|
|||||||
|
|
||||||
resetUserEscalationProgress(userData, escalationGroupId)
|
resetUserEscalationProgress(userData, escalationGroupId)
|
||||||
|
|
||||||
writeUserData(jwt.unique_name, gameVersion)
|
writeUserData(userId, gameVersion)
|
||||||
|
|
||||||
|
const group = controller.escalationMappings.get(escalationGroupId)
|
||||||
|
|
||||||
|
if (!group) {
|
||||||
|
log(
|
||||||
|
LogLevel.ERROR,
|
||||||
|
`Unknown escalation group: ${escalationGroupId}`,
|
||||||
|
)
|
||||||
|
return { error: true }
|
||||||
|
}
|
||||||
|
|
||||||
// now reassign properties and continue
|
// now reassign properties and continue
|
||||||
contractId = controller.escalationMappings.get(escalationGroupId)["1"]
|
contractId = group["1"]
|
||||||
|
|
||||||
contractData = controller.resolveContract(contractId)
|
contractData = controller.resolveContract(contractId)
|
||||||
}
|
}
|
||||||
@ -161,20 +183,29 @@ export async function getPlanningData(
|
|||||||
LogLevel.WARN,
|
LogLevel.WARN,
|
||||||
`Trying to download contract ${contractId} due to it not found locally.`,
|
`Trying to download contract ${contractId} due to it not found locally.`,
|
||||||
)
|
)
|
||||||
const user = userAuths.get(jwt.unique_name)
|
const user = userAuths.get(userId)
|
||||||
const resp = await user._useService(
|
const resp = await user?._useService(
|
||||||
`https://${getRemoteService(
|
`https://${getRemoteService(
|
||||||
gameVersion,
|
gameVersion,
|
||||||
)}.hitman.io/profiles/page/Planning?contractid=${contractId}&resetescalation=false&forcecurrentcontract=false&errorhandling=false`,
|
)}.hitman.io/profiles/page/Planning?contractid=${contractId}&resetescalation=false&forcecurrentcontract=false&errorhandling=false`,
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
|
|
||||||
contractData = resp.data.data.Contract
|
contractData = resp?.data.data.Contract
|
||||||
|
|
||||||
|
if (!contractData) {
|
||||||
|
log(
|
||||||
|
LogLevel.ERROR,
|
||||||
|
`Official planning lookup no result: ${contractId}`,
|
||||||
|
)
|
||||||
|
return { error: true }
|
||||||
|
}
|
||||||
|
|
||||||
controller.fetchedContracts.set(contractData.Metadata.Id, contractData)
|
controller.fetchedContracts.set(contractData.Metadata.Id, contractData)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!contractData) {
|
if (!contractData) {
|
||||||
log(LogLevel.ERROR, `Not found: ${contractId}, .`)
|
log(LogLevel.ERROR, `Not found: ${contractId}, planning regular.`)
|
||||||
return { error: true }
|
return { error: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -198,6 +229,11 @@ export async function getPlanningData(
|
|||||||
if (escalation) {
|
if (escalation) {
|
||||||
const groupContractData = controller.resolveContract(escalationGroupId)
|
const groupContractData = controller.resolveContract(escalationGroupId)
|
||||||
|
|
||||||
|
if (!groupContractData) {
|
||||||
|
log(LogLevel.ERROR, `Not found: ${contractId}, planning esc group`)
|
||||||
|
return { error: true }
|
||||||
|
}
|
||||||
|
|
||||||
const p = getUserEscalationProgress(userData, escalationGroupId)
|
const p = getUserEscalationProgress(userData, escalationGroupId)
|
||||||
|
|
||||||
const done =
|
const done =
|
||||||
@ -216,9 +252,12 @@ export async function getPlanningData(
|
|||||||
|
|
||||||
// Fix contractData to the data of the level in the group.
|
// Fix contractData to the data of the level in the group.
|
||||||
if (!contractData.Metadata.InGroup) {
|
if (!contractData.Metadata.InGroup) {
|
||||||
contractData = controller.resolveContract(
|
const newLevelId =
|
||||||
contractData.Metadata.GroupDefinition.Order[p - 1],
|
contractData.Metadata.GroupDefinition?.Order[p - 1]
|
||||||
)
|
|
||||||
|
assert(typeof newLevelId === "string", "newLevelId is not a string")
|
||||||
|
|
||||||
|
contractData = controller.resolveContract(newLevelId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -246,16 +285,21 @@ export async function getPlanningData(
|
|||||||
|
|
||||||
const sublocation = getSubLocationFromContract(contractData, gameVersion)
|
const sublocation = getSubLocationFromContract(contractData, gameVersion)
|
||||||
|
|
||||||
|
assert.ok(sublocation, "contract sublocation is null")
|
||||||
|
|
||||||
if (!entranceData[scenePath]) {
|
if (!entranceData[scenePath]) {
|
||||||
log(
|
log(
|
||||||
LogLevel.ERROR,
|
LogLevel.ERROR,
|
||||||
`Could not find Entrance data for ${scenePath} (loc Planning)! This may cause an unhandled promise rejection.`,
|
`Could not find Entrance data for ${scenePath} in planning`,
|
||||||
)
|
)
|
||||||
|
return {
|
||||||
|
error: true,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const entrancesInScene = entranceData[scenePath]
|
const entrancesInScene = entranceData[scenePath]
|
||||||
|
|
||||||
const typedInv = createInventory(jwt.unique_name, gameVersion, sublocation)
|
const typedInv = createInventory(userId, gameVersion, sublocation)
|
||||||
|
|
||||||
const unlockedEntrances = typedInv
|
const unlockedEntrances = typedInv
|
||||||
.filter((item) => item.Unlockable.Type === "access")
|
.filter((item) => item.Unlockable.Type === "access")
|
||||||
@ -267,6 +311,9 @@ export async function getPlanningData(
|
|||||||
LogLevel.ERROR,
|
LogLevel.ERROR,
|
||||||
"No matching entrance data found in planning, this is a bug!",
|
"No matching entrance data found in planning, this is a bug!",
|
||||||
)
|
)
|
||||||
|
return {
|
||||||
|
error: true,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sublocation.DisplayNameLocKey = `UI_${sublocation.Id}_NAME`
|
sublocation.DisplayNameLocKey = `UI_${sublocation.Id}_NAME`
|
||||||
@ -283,7 +330,7 @@ export async function getPlanningData(
|
|||||||
let suit = getDefaultSuitFor(sublocation)
|
let suit = getDefaultSuitFor(sublocation)
|
||||||
let tool1 = "TOKEN_FIBERWIRE"
|
let tool1 = "TOKEN_FIBERWIRE"
|
||||||
let tool2 = "PROP_TOOL_COIN"
|
let tool2 = "PROP_TOOL_COIN"
|
||||||
let briefcaseProp: string | undefined = undefined
|
let briefcaseContainedItemId: string | undefined = undefined
|
||||||
let briefcaseId: string | undefined = undefined
|
let briefcaseId: string | undefined = undefined
|
||||||
|
|
||||||
const dlForLocation =
|
const dlForLocation =
|
||||||
@ -293,16 +340,13 @@ export async function getPlanningData(
|
|||||||
contractData.Metadata.Location
|
contractData.Metadata.Location
|
||||||
]
|
]
|
||||||
: // new loadout profiles system
|
: // new loadout profiles system
|
||||||
Object.hasOwn(
|
currentLoadout.data[contractData.Metadata.Location]
|
||||||
currentLoadout.data,
|
|
||||||
contractData.Metadata.Location,
|
|
||||||
) && currentLoadout.data[contractData.Metadata.Location]
|
|
||||||
|
|
||||||
if (dlForLocation) {
|
if (dlForLocation) {
|
||||||
pistol = dlForLocation["2"]
|
pistol = dlForLocation["2"]!
|
||||||
suit = dlForLocation["3"]
|
suit = dlForLocation["3"]!
|
||||||
tool1 = dlForLocation["4"]
|
tool1 = dlForLocation["4"]!
|
||||||
tool2 = dlForLocation["5"]
|
tool2 = dlForLocation["5"]!
|
||||||
|
|
||||||
for (const key of Object.keys(dlForLocation)) {
|
for (const key of Object.keys(dlForLocation)) {
|
||||||
if (["2", "3", "4", "5"].includes(key)) {
|
if (["2", "3", "4", "5"].includes(key)) {
|
||||||
@ -311,28 +355,30 @@ export async function getPlanningData(
|
|||||||
}
|
}
|
||||||
|
|
||||||
briefcaseId = key
|
briefcaseId = key
|
||||||
briefcaseProp = dlForLocation[key]
|
// @ts-expect-error This will work.
|
||||||
|
briefcaseContainedItemId = dlForLocation[key]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const i = typedInv.find((item) => item.Unlockable.Id === briefcaseProp)
|
const briefcaseContainedItem = typedInv.find(
|
||||||
|
(item) => item.Unlockable.Id === briefcaseContainedItemId,
|
||||||
const userCentric = generateUserCentric(
|
|
||||||
contractData,
|
|
||||||
jwt.unique_name,
|
|
||||||
gameVersion,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const userCentric = generateUserCentric(contractData, userId, gameVersion)
|
||||||
|
|
||||||
const sniperLoadouts = createSniperLoadouts(
|
const sniperLoadouts = createSniperLoadouts(
|
||||||
jwt.unique_name,
|
userId,
|
||||||
gameVersion,
|
gameVersion,
|
||||||
contractData,
|
contractData,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (gameVersion === "scpc") {
|
if (gameVersion === "scpc") {
|
||||||
for (const loadout of sniperLoadouts) {
|
for (const loadout of sniperLoadouts) {
|
||||||
loadout["LoadoutData"] = loadout["Loadout"]["LoadoutData"]
|
const l = loadout as SniperLoadout
|
||||||
delete loadout["Loadout"]
|
l["LoadoutData"] = (loadout as SniperCharacter)["Loadout"][
|
||||||
|
"LoadoutData"
|
||||||
|
]
|
||||||
|
delete (loadout as Partial<SniperCharacter>)["Loadout"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -395,19 +441,20 @@ export async function getPlanningData(
|
|||||||
SlotId: "6",
|
SlotId: "6",
|
||||||
Recommended: null,
|
Recommended: null,
|
||||||
},
|
},
|
||||||
briefcaseId && {
|
briefcaseId &&
|
||||||
SlotName: briefcaseProp,
|
briefcaseContainedItem && {
|
||||||
SlotId: briefcaseId,
|
SlotName: briefcaseContainedItemId,
|
||||||
Recommended: {
|
SlotId: briefcaseId,
|
||||||
item: {
|
Recommended: {
|
||||||
...i,
|
item: {
|
||||||
Properties: {},
|
...briefcaseContainedItem,
|
||||||
|
Properties: {},
|
||||||
|
},
|
||||||
|
type: briefcaseContainedItem.Unlockable.Id,
|
||||||
|
owned: true,
|
||||||
},
|
},
|
||||||
type: i.Unlockable.Id,
|
IsContainer: true,
|
||||||
owned: true,
|
|
||||||
},
|
},
|
||||||
IsContainer: true,
|
|
||||||
},
|
|
||||||
].filter(Boolean)
|
].filter(Boolean)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -426,7 +473,8 @@ export async function getPlanningData(
|
|||||||
) {
|
) {
|
||||||
const loadoutUnlockable = getUnlockableById(
|
const loadoutUnlockable = getUnlockableById(
|
||||||
gameVersion === "h1"
|
gameVersion === "h1"
|
||||||
? sublocation?.Properties?.NormalLoadoutUnlock[
|
? // @ts-expect-error This works.
|
||||||
|
sublocation?.Properties?.NormalLoadoutUnlock[
|
||||||
contractData.Metadata.Difficulty ?? "normal"
|
contractData.Metadata.Difficulty ?? "normal"
|
||||||
]
|
]
|
||||||
: sublocation?.Properties?.NormalLoadoutUnlock,
|
: sublocation?.Properties?.NormalLoadoutUnlock,
|
||||||
@ -440,23 +488,31 @@ export async function getPlanningData(
|
|||||||
gameVersion,
|
gameVersion,
|
||||||
)
|
)
|
||||||
|
|
||||||
const locationProgression =
|
const locationProgression: ProgressionData =
|
||||||
loadoutMasteryData &&
|
loadoutMasteryData?.SubPackageId
|
||||||
(loadoutMasteryData.SubPackageId
|
? // @ts-expect-error This works
|
||||||
? userData.Extensions.progression.Locations[
|
userData.Extensions.progression.Locations[
|
||||||
loadoutMasteryData.Location
|
loadoutMasteryData.Location
|
||||||
][loadoutMasteryData.SubPackageId]
|
][loadoutMasteryData.SubPackageId]
|
||||||
: userData.Extensions.progression.Locations[
|
: userData.Extensions.progression.Locations[
|
||||||
loadoutMasteryData.Location
|
loadoutMasteryData?.Location as unknown as string
|
||||||
])
|
]
|
||||||
|
|
||||||
if (locationProgression.Level < loadoutMasteryData.Level)
|
if (locationProgression.Level < (loadoutMasteryData?.Level || 0)) {
|
||||||
|
type S = {
|
||||||
|
SlotId: string
|
||||||
|
}
|
||||||
loadoutSlots = loadoutSlots.filter(
|
loadoutSlots = loadoutSlots.filter(
|
||||||
(slot) => !["2", "4", "5"].includes(slot.SlotId),
|
(slot) => !["2", "4", "5"].includes((slot as S)?.SlotId),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assert.ok(contractData, "no contract data at final - planning")
|
||||||
|
|
||||||
|
type Cast = keyof typeof limitedLoadoutUnlockLevelMap
|
||||||
|
|
||||||
return {
|
return {
|
||||||
Contract: contractData,
|
Contract: contractData,
|
||||||
ElusiveContractState: "not_completed",
|
ElusiveContractState: "not_completed",
|
||||||
@ -467,7 +523,7 @@ export async function getPlanningData(
|
|||||||
UnlockedEntrances:
|
UnlockedEntrances:
|
||||||
contractData.Metadata.Type === "sniper"
|
contractData.Metadata.Type === "sniper"
|
||||||
? null
|
? null
|
||||||
: typedInv
|
: (typedInv
|
||||||
.filter(
|
.filter(
|
||||||
(item) =>
|
(item) =>
|
||||||
item.Unlockable.Subtype === "startinglocation",
|
item.Unlockable.Subtype === "startinglocation",
|
||||||
@ -475,27 +531,28 @@ export async function getPlanningData(
|
|||||||
.filter(
|
.filter(
|
||||||
(item) =>
|
(item) =>
|
||||||
item.Unlockable.Properties.Difficulty ===
|
item.Unlockable.Properties.Difficulty ===
|
||||||
contractData.Metadata.Difficulty,
|
contractData!.Metadata.Difficulty,
|
||||||
)
|
)
|
||||||
.map((i) => i.Unlockable.Properties.RepositoryId)
|
.map((i) => i.Unlockable.Properties.RepositoryId)
|
||||||
.filter((id) => id),
|
.filter(Boolean) as string[]),
|
||||||
UnlockedAgencyPickups:
|
UnlockedAgencyPickups:
|
||||||
contractData.Metadata.Type === "sniper"
|
contractData.Metadata.Type === "sniper"
|
||||||
? null
|
? null
|
||||||
: typedInv
|
: (typedInv
|
||||||
.filter((item) => item.Unlockable.Type === "agencypickup")
|
.filter((item) => item.Unlockable.Type === "agencypickup")
|
||||||
.filter(
|
.filter(
|
||||||
(item) =>
|
(item) =>
|
||||||
item.Unlockable.Properties.Difficulty ===
|
item.Unlockable.Properties.Difficulty ===
|
||||||
contractData.Metadata.Difficulty,
|
// we already know it's not undefined
|
||||||
|
contractData!.Metadata.Difficulty,
|
||||||
)
|
)
|
||||||
.map((i) => i.Unlockable.Properties.RepositoryId)
|
.map((i) => i.Unlockable.Properties.RepositoryId)
|
||||||
.filter((id) => id),
|
.filter(Boolean) as string[]),
|
||||||
Objectives: mapObjectives(
|
Objectives: mapObjectives(
|
||||||
contractData.Data.Objectives,
|
contractData.Data.Objectives!,
|
||||||
contractData.Data.GameChangers || [],
|
contractData.Data.GameChangers || [],
|
||||||
contractData.Metadata.GroupObjectiveDisplayOrder || [],
|
contractData.Metadata.GroupObjectiveDisplayOrder || [],
|
||||||
contractData.Metadata.IsEvergreenSafehouse,
|
Boolean(contractData.Metadata.IsEvergreenSafehouse),
|
||||||
),
|
),
|
||||||
GroupData: groupData,
|
GroupData: groupData,
|
||||||
Entrances:
|
Entrances:
|
||||||
@ -504,27 +561,28 @@ export async function getPlanningData(
|
|||||||
: unlockedEntrances
|
: unlockedEntrances
|
||||||
.filter((unlockable) =>
|
.filter((unlockable) =>
|
||||||
entrancesInScene.includes(
|
entrancesInScene.includes(
|
||||||
unlockable.Properties.RepositoryId,
|
unlockable.Properties.RepositoryId || "",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.filter(
|
.filter(
|
||||||
(unlockable) =>
|
(unlockable) =>
|
||||||
unlockable.Properties.Difficulty ===
|
unlockable.Properties.Difficulty ===
|
||||||
contractData.Metadata.Difficulty,
|
// we already know it's not undefined
|
||||||
|
contractData!.Metadata.Difficulty,
|
||||||
)
|
)
|
||||||
.sort(unlockOrderComparer),
|
.sort(unlockOrderComparer),
|
||||||
Location: sublocation,
|
Location: sublocation,
|
||||||
LoadoutData:
|
LoadoutData:
|
||||||
contractData.Metadata.Type === "sniper" ? null : loadoutSlots,
|
contractData.Metadata.Type === "sniper" ? null : loadoutSlots,
|
||||||
LimitedLoadoutUnlockLevel:
|
LimitedLoadoutUnlockLevel:
|
||||||
limitedLoadoutUnlockLevelMap[sublocation.Id] ?? 0,
|
limitedLoadoutUnlockLevelMap[sublocation.Id as Cast] ?? 0,
|
||||||
CharacterLoadoutData:
|
CharacterLoadoutData:
|
||||||
sniperLoadouts.length !== 0 ? sniperLoadouts : null,
|
sniperLoadouts.length !== 0 ? sniperLoadouts : null,
|
||||||
ChallengeData: {
|
ChallengeData: {
|
||||||
Children: controller.challengeService.getChallengeTreeForContract(
|
Children: controller.challengeService.getChallengeTreeForContract(
|
||||||
contractId,
|
contractId,
|
||||||
gameVersion,
|
gameVersion,
|
||||||
jwt.unique_name,
|
userId,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
Currency: {
|
Currency: {
|
||||||
|
148
components/menus/playerProfile.ts
Normal file
148
components/menus/playerProfile.ts
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
/*
|
||||||
|
* The Peacock Project - a HITMAN server replacement.
|
||||||
|
* Copyright (C) 2021-2024 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 { getConfig, getVersionedConfig } from "../configSwizzleManager"
|
||||||
|
import type {
|
||||||
|
ChallengeCategoryCompletion,
|
||||||
|
GameVersion,
|
||||||
|
PeacockLocationsData,
|
||||||
|
PlayerProfileView,
|
||||||
|
ProgressionData,
|
||||||
|
} from "../types/types"
|
||||||
|
import { generateCompletionData } from "../contracts/dataGen"
|
||||||
|
import { controller } from "../controller"
|
||||||
|
import { getDestinationCompletion } from "./destinations"
|
||||||
|
import { getUserData } from "../databaseHandler"
|
||||||
|
import { isSniperLocation } from "../utils"
|
||||||
|
|
||||||
|
export function getPlayerProfileData(
|
||||||
|
gameVersion: GameVersion,
|
||||||
|
userId: string,
|
||||||
|
): PlayerProfileView {
|
||||||
|
const playerProfilePage = getConfig<PlayerProfileView>(
|
||||||
|
"PlayerProfilePage",
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
|
||||||
|
const locationData = getVersionedConfig<PeacockLocationsData>(
|
||||||
|
"LocationsData",
|
||||||
|
gameVersion,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
|
||||||
|
playerProfilePage.SubLocationData = []
|
||||||
|
|
||||||
|
for (const subLocationKey in locationData.children) {
|
||||||
|
// Ewww...
|
||||||
|
if (
|
||||||
|
subLocationKey === "LOCATION_ICA_FACILITY_ARRIVAL" ||
|
||||||
|
subLocationKey.includes("SNUG_")
|
||||||
|
) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const subLocation = locationData.children[subLocationKey]
|
||||||
|
const parentLocation =
|
||||||
|
locationData.parents[subLocation.Properties.ParentLocation || ""]
|
||||||
|
|
||||||
|
const completionData = generateCompletionData(
|
||||||
|
subLocation.Id,
|
||||||
|
userId,
|
||||||
|
gameVersion,
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: Make getDestinationCompletion do something like this.
|
||||||
|
const challenges = controller.challengeService.getChallengesForLocation(
|
||||||
|
subLocation.Id,
|
||||||
|
gameVersion,
|
||||||
|
)
|
||||||
|
|
||||||
|
const challengeCategoryCompletion: ChallengeCategoryCompletion[] = []
|
||||||
|
|
||||||
|
for (const challengeGroup in challenges) {
|
||||||
|
const challengeCompletion =
|
||||||
|
controller.challengeService.countTotalNCompletedChallenges(
|
||||||
|
{
|
||||||
|
challengeGroup: challenges[challengeGroup],
|
||||||
|
},
|
||||||
|
userId,
|
||||||
|
gameVersion,
|
||||||
|
)
|
||||||
|
|
||||||
|
challengeCategoryCompletion.push({
|
||||||
|
Name: challenges[challengeGroup][0].CategoryName,
|
||||||
|
...challengeCompletion,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const destinationCompletion = getDestinationCompletion(
|
||||||
|
parentLocation,
|
||||||
|
subLocation,
|
||||||
|
gameVersion,
|
||||||
|
userId,
|
||||||
|
)
|
||||||
|
|
||||||
|
playerProfilePage.SubLocationData.push({
|
||||||
|
ParentLocation: parentLocation,
|
||||||
|
Location: subLocation,
|
||||||
|
CompletionData: completionData,
|
||||||
|
ChallengeCategoryCompletion: challengeCategoryCompletion,
|
||||||
|
ChallengeCompletion: destinationCompletion.ChallengeCompletion,
|
||||||
|
OpportunityStatistics: destinationCompletion.OpportunityStatistics,
|
||||||
|
LocationCompletionPercent:
|
||||||
|
destinationCompletion.LocationCompletionPercent,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const userProfile = getUserData(userId, gameVersion)
|
||||||
|
playerProfilePage.PlayerProfileXp.Total =
|
||||||
|
userProfile.Extensions.progression.PlayerProfileXP.Total
|
||||||
|
playerProfilePage.PlayerProfileXp.Level =
|
||||||
|
userProfile.Extensions.progression.PlayerProfileXP.ProfileLevel
|
||||||
|
|
||||||
|
const subLocationMap = new Map(
|
||||||
|
userProfile.Extensions.progression.PlayerProfileXP.Sublocations.map(
|
||||||
|
(obj) => [obj.Location, obj],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const season of playerProfilePage.PlayerProfileXp.Seasons) {
|
||||||
|
for (const location of season.Locations) {
|
||||||
|
const subLocationData = subLocationMap.get(location.LocationId)
|
||||||
|
|
||||||
|
location.Xp = subLocationData?.Xp || 0
|
||||||
|
location.ActionXp = subLocationData?.ActionXp || 0
|
||||||
|
|
||||||
|
if (
|
||||||
|
location.LocationProgression &&
|
||||||
|
!isSniperLocation(location.LocationId)
|
||||||
|
) {
|
||||||
|
// We typecast below as it could be an object for subpackages.
|
||||||
|
// Checks before this ensure it isn't, but TS doesn't realise this.
|
||||||
|
location.LocationProgression.Level =
|
||||||
|
(
|
||||||
|
userProfile.Extensions.progression.Locations[
|
||||||
|
location.LocationId
|
||||||
|
] as ProgressionData
|
||||||
|
).Level || 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return playerProfilePage
|
||||||
|
}
|
@ -21,11 +21,11 @@ import { generateUserCentric } from "../contracts/dataGen"
|
|||||||
import { controller } from "../controller"
|
import { controller } from "../controller"
|
||||||
import type {
|
import type {
|
||||||
GameVersion,
|
GameVersion,
|
||||||
JwtData,
|
|
||||||
MissionStory,
|
MissionStory,
|
||||||
PlayNextCampaignDetails,
|
PlayNextCampaignDetails,
|
||||||
UserCentricContract,
|
UserCentricContract,
|
||||||
} from "../types/types"
|
} from "../types/types"
|
||||||
|
import assert from "assert"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main story campaign ordered mission IDs.
|
* Main story campaign ordered mission IDs.
|
||||||
@ -157,6 +157,8 @@ export function createMainOpportunityTile(
|
|||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
assert.ok(contractData)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
CategoryType: "MainOpportunity",
|
CategoryType: "MainOpportunity",
|
||||||
CategoryName: "UI_PLAYNEXT_MAINOPPORTUNITY_CATEGORY_NAME",
|
CategoryName: "UI_PLAYNEXT_MAINOPPORTUNITY_CATEGORY_NAME",
|
||||||
@ -202,7 +204,7 @@ export type GameFacingPlayNextData = {
|
|||||||
|
|
||||||
export function getGamePlayNextData(
|
export function getGamePlayNextData(
|
||||||
contractId: string,
|
contractId: string,
|
||||||
jwt: JwtData,
|
userId: string,
|
||||||
gameVersion: GameVersion,
|
gameVersion: GameVersion,
|
||||||
): GameFacingPlayNextData {
|
): GameFacingPlayNextData {
|
||||||
const cats: PlayNextCategory[] = []
|
const cats: PlayNextCategory[] = []
|
||||||
@ -225,14 +227,9 @@ export function getGamePlayNextData(
|
|||||||
|
|
||||||
if (shouldContinue) {
|
if (shouldContinue) {
|
||||||
cats.push(
|
cats.push(
|
||||||
createPlayNextMission(
|
createPlayNextMission(userId, nextMissionId, gameVersion, {
|
||||||
jwt.unique_name,
|
CampaignName: `UI_SEASON_${nextSeasonId}`,
|
||||||
nextMissionId,
|
}),
|
||||||
gameVersion,
|
|
||||||
{
|
|
||||||
CampaignName: `UI_SEASON_${nextSeasonId}`,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -244,7 +241,7 @@ export function getGamePlayNextData(
|
|||||||
if (pzIdIndex !== -1 && pzIdIndex !== orderedPZMissions.length - 1) {
|
if (pzIdIndex !== -1 && pzIdIndex !== orderedPZMissions.length - 1) {
|
||||||
const nextMissionId = orderedPZMissions[pzIdIndex + 1]
|
const nextMissionId = orderedPZMissions[pzIdIndex + 1]
|
||||||
cats.push(
|
cats.push(
|
||||||
createPlayNextMission(jwt.unique_name, nextMissionId, gameVersion, {
|
createPlayNextMission(userId, nextMissionId, gameVersion, {
|
||||||
CampaignName: "UI_CONTRACT_CAMPAIGN_WHITE_SPIDER_TITLE",
|
CampaignName: "UI_CONTRACT_CAMPAIGN_WHITE_SPIDER_TITLE",
|
||||||
ParentCampaignName: "UI_MENU_PAGE_SIDE_MISSIONS_TITLE",
|
ParentCampaignName: "UI_MENU_PAGE_SIDE_MISSIONS_TITLE",
|
||||||
}),
|
}),
|
||||||
@ -255,7 +252,7 @@ export function getGamePlayNextData(
|
|||||||
if (contractId === "f1ba328f-e3dd-4ef8-bb26-0363499fdd95") {
|
if (contractId === "f1ba328f-e3dd-4ef8-bb26-0363499fdd95") {
|
||||||
const nextMissionId = "0b616e62-af0c-495b-82e3-b778e82b5912"
|
const nextMissionId = "0b616e62-af0c-495b-82e3-b778e82b5912"
|
||||||
cats.push(
|
cats.push(
|
||||||
createPlayNextMission(jwt.unique_name, nextMissionId, gameVersion, {
|
createPlayNextMission(userId, nextMissionId, gameVersion, {
|
||||||
CampaignName: "UI_MENU_PAGE_SPECIAL_ASSIGNMENTS_TITLE",
|
CampaignName: "UI_MENU_PAGE_SPECIAL_ASSIGNMENTS_TITLE",
|
||||||
ParentCampaignName: "UI_MENU_PAGE_SIDE_MISSIONS_TITLE",
|
ParentCampaignName: "UI_MENU_PAGE_SIDE_MISSIONS_TITLE",
|
||||||
}),
|
}),
|
||||||
@ -274,7 +271,7 @@ export function getGamePlayNextData(
|
|||||||
if (pluginData) {
|
if (pluginData) {
|
||||||
if (pluginData.overrideIndex !== undefined) {
|
if (pluginData.overrideIndex !== undefined) {
|
||||||
cats[pluginData.overrideIndex] = createPlayNextMission(
|
cats[pluginData.overrideIndex] = createPlayNextMission(
|
||||||
jwt.unique_name,
|
userId,
|
||||||
pluginData.nextContractId,
|
pluginData.nextContractId,
|
||||||
gameVersion,
|
gameVersion,
|
||||||
pluginData.campaignDetails,
|
pluginData.campaignDetails,
|
||||||
@ -282,7 +279,7 @@ export function getGamePlayNextData(
|
|||||||
} else {
|
} else {
|
||||||
cats.push(
|
cats.push(
|
||||||
createPlayNextMission(
|
createPlayNextMission(
|
||||||
jwt.unique_name,
|
userId,
|
||||||
pluginData.nextContractId,
|
pluginData.nextContractId,
|
||||||
gameVersion,
|
gameVersion,
|
||||||
pluginData.campaignDetails,
|
pluginData.campaignDetails,
|
||||||
@ -293,6 +290,6 @@ export function getGamePlayNextData(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
Categories: cats,
|
Categories: cats,
|
||||||
ProfileId: jwt.unique_name,
|
ProfileId: userId,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,8 +17,44 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { controller } from "../controller"
|
import { controller } from "../controller"
|
||||||
import type { GameVersion, MissionManifest } from "../types/types"
|
import type {
|
||||||
|
CompletionData,
|
||||||
|
GameVersion,
|
||||||
|
MissionManifest,
|
||||||
|
} from "../types/types"
|
||||||
import { getSubLocationByName } from "../contracts/dataGen"
|
import { getSubLocationByName } from "../contracts/dataGen"
|
||||||
|
import { InventoryItem } from "../inventory"
|
||||||
|
import assert from "assert"
|
||||||
|
|
||||||
|
export type SniperCharacter = {
|
||||||
|
Id: string
|
||||||
|
Loadout: SniperLoadout
|
||||||
|
CompletionData: CompletionData
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SniperLoadout = {
|
||||||
|
LoadoutData: {
|
||||||
|
SlotId: string
|
||||||
|
SlotName: string
|
||||||
|
Items: {
|
||||||
|
Item: InventoryItem
|
||||||
|
ItemDetails: unknown
|
||||||
|
}[]
|
||||||
|
Page: number
|
||||||
|
Recommended: {
|
||||||
|
item: InventoryItem
|
||||||
|
type: string
|
||||||
|
owned: boolean
|
||||||
|
}
|
||||||
|
HasMore: boolean
|
||||||
|
HasMoreLeft: boolean
|
||||||
|
HasMoreRight: boolean
|
||||||
|
OptionalData: Record<never, never>
|
||||||
|
}[]
|
||||||
|
LimitedLoadoutUnlockLevel: number | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
type Return = (SniperLoadout | SniperCharacter)[]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates the sniper loadouts data for a contract. Returns loadouts for all three
|
* Creates the sniper loadouts data for a contract. Returns loadouts for all three
|
||||||
@ -38,13 +74,15 @@ export function createSniperLoadouts(
|
|||||||
gameVersion: GameVersion,
|
gameVersion: GameVersion,
|
||||||
contractData: MissionManifest,
|
contractData: MissionManifest,
|
||||||
loadoutData = false,
|
loadoutData = false,
|
||||||
) {
|
): Return {
|
||||||
const sniperLoadouts = []
|
const sniperLoadouts: Return = []
|
||||||
const parentLocation = getSubLocationByName(
|
const parentLocation = getSubLocationByName(
|
||||||
contractData.Metadata.Location,
|
contractData.Metadata.Location,
|
||||||
gameVersion,
|
gameVersion,
|
||||||
)?.Properties.ParentLocation
|
)?.Properties.ParentLocation
|
||||||
|
|
||||||
|
assert.ok(parentLocation, "Parent location not found")
|
||||||
|
|
||||||
// This function call is used as it gets all mastery data for the current location
|
// This function call is used as it gets all mastery data for the current location
|
||||||
// which includes all the characters we'll need.
|
// which includes all the characters we'll need.
|
||||||
// We map it by Id for quick lookup.
|
// We map it by Id for quick lookup.
|
||||||
@ -54,85 +92,97 @@ export function createSniperLoadouts(
|
|||||||
.map((data) => [data.CompletionData.Id, data]),
|
.map((data) => [data.CompletionData.Id, data]),
|
||||||
)
|
)
|
||||||
|
|
||||||
if (contractData.Metadata.Type === "sniper") {
|
if (contractData.Metadata.Type !== "sniper") {
|
||||||
for (const charSetup of contractData.Metadata.CharacterSetup) {
|
return sniperLoadouts
|
||||||
for (const character of charSetup.Characters) {
|
}
|
||||||
// Get the mastery data for this character
|
|
||||||
const masteryData = masteryMap.get(
|
|
||||||
character.MandatoryLoadout[0],
|
|
||||||
)
|
|
||||||
|
|
||||||
// Get the unlockable that is currently unlocked
|
assert.ok(
|
||||||
const curUnlockable =
|
contractData.Metadata.CharacterSetup,
|
||||||
masteryData.CompletionData.Level === 1
|
"Contract missing sniper character setup",
|
||||||
? masteryData.Unlockable
|
)
|
||||||
: masteryData.Drops[
|
|
||||||
masteryData.CompletionData.Level - 2
|
|
||||||
].Unlockable
|
|
||||||
|
|
||||||
const data = {
|
for (const charSetup of contractData.Metadata.CharacterSetup) {
|
||||||
Id: character.Id,
|
for (const character of charSetup.Characters) {
|
||||||
Loadout: {
|
// Get the mastery data for this character
|
||||||
LoadoutData: [
|
const masteryData = masteryMap.get(
|
||||||
{
|
character.MandatoryLoadout?.[0] || "",
|
||||||
SlotId: "0",
|
)
|
||||||
SlotName: "carriedweapon",
|
|
||||||
Items: [
|
assert.ok(
|
||||||
{
|
masteryData,
|
||||||
Item: {
|
`Mastery data not found for ${contractData.Metadata.Id}`,
|
||||||
InstanceId: character.Id,
|
)
|
||||||
ProfileId: userId,
|
|
||||||
Unlockable: curUnlockable,
|
// Get the unlockable that is currently unlocked
|
||||||
Properties: {},
|
const curUnlockable =
|
||||||
},
|
masteryData.CompletionData.Level === 1
|
||||||
ItemDetails: {
|
? masteryData.Unlockable
|
||||||
Capabilities: [],
|
: masteryData.Drops[masteryData.CompletionData.Level - 2]
|
||||||
StatList: Object.keys(
|
.Unlockable
|
||||||
curUnlockable.Properties
|
|
||||||
.Gameplay,
|
assert.ok(curUnlockable, "Unlockable not found")
|
||||||
).map((key) => {
|
assert.ok(
|
||||||
return {
|
curUnlockable.Properties.Gameplay,
|
||||||
Name: key,
|
"Unlockable has no gameplay data",
|
||||||
Ratio: curUnlockable
|
)
|
||||||
.Properties.Gameplay[
|
|
||||||
key
|
const data: SniperCharacter = {
|
||||||
],
|
Id: character.Id,
|
||||||
}
|
Loadout: {
|
||||||
}),
|
LoadoutData: [
|
||||||
PropertyTexts: [],
|
{
|
||||||
},
|
SlotId: "0",
|
||||||
},
|
SlotName: "carriedweapon",
|
||||||
],
|
Items: [],
|
||||||
Page: 0,
|
Page: 0,
|
||||||
Recommended: {
|
Recommended: {
|
||||||
item: {
|
item: {
|
||||||
InstanceId: character.Id,
|
InstanceId: character.Id,
|
||||||
ProfileId: userId,
|
ProfileId: userId,
|
||||||
Unlockable: curUnlockable,
|
Unlockable: curUnlockable,
|
||||||
Properties: {},
|
Properties: {},
|
||||||
},
|
|
||||||
type: "carriedweapon",
|
|
||||||
owned: true,
|
|
||||||
},
|
},
|
||||||
HasMore: false,
|
type: "carriedweapon",
|
||||||
HasMoreLeft: false,
|
owned: true,
|
||||||
HasMoreRight: false,
|
|
||||||
OptionalData: {},
|
|
||||||
},
|
},
|
||||||
],
|
HasMore: false,
|
||||||
LimitedLoadoutUnlockLevel: 0 as number | undefined,
|
HasMoreLeft: false,
|
||||||
},
|
HasMoreRight: false,
|
||||||
CompletionData: masteryData?.CompletionData,
|
OptionalData: {},
|
||||||
}
|
},
|
||||||
|
],
|
||||||
if (loadoutData) {
|
LimitedLoadoutUnlockLevel: 0 as number | undefined,
|
||||||
delete data.Loadout.LimitedLoadoutUnlockLevel
|
},
|
||||||
sniperLoadouts.push(data.Loadout)
|
CompletionData: masteryData.CompletionData,
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
sniperLoadouts.push(data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data.Loadout.LoadoutData[0].Items.push({
|
||||||
|
Item: {
|
||||||
|
InstanceId: character.Id,
|
||||||
|
ProfileId: userId,
|
||||||
|
Unlockable: curUnlockable,
|
||||||
|
Properties: {},
|
||||||
|
},
|
||||||
|
ItemDetails: {
|
||||||
|
Capabilities: [],
|
||||||
|
StatList: Object.keys(
|
||||||
|
curUnlockable.Properties.Gameplay,
|
||||||
|
).map((key) => ({
|
||||||
|
Name: key,
|
||||||
|
// @ts-expect-error This will work.
|
||||||
|
Ratio: curUnlockable.Properties.Gameplay[key],
|
||||||
|
})),
|
||||||
|
PropertyTexts: [],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (loadoutData) {
|
||||||
|
delete data.Loadout.LimitedLoadoutUnlockLevel
|
||||||
|
sniperLoadouts.push(data.Loadout)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
sniperLoadouts.push(data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,6 +37,7 @@ import { log, LogLevel } from "../loggingInterop"
|
|||||||
import { getUserData } from "../databaseHandler"
|
import { getUserData } from "../databaseHandler"
|
||||||
import { getFlag } from "../flags"
|
import { getFlag } from "../flags"
|
||||||
import { loadouts } from "../loadouts"
|
import { loadouts } from "../loadouts"
|
||||||
|
import assert from "assert"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Algorithm to get the stashpoint items data for H2 and H3.
|
* Algorithm to get the stashpoint items data for H2 and H3.
|
||||||
@ -139,10 +140,13 @@ export function getModernStashData(
|
|||||||
const inventory = createInventory(
|
const inventory = createInventory(
|
||||||
userId,
|
userId,
|
||||||
gameVersion,
|
gameVersion,
|
||||||
getSubLocationByName(contractData?.Metadata.Location, gameVersion),
|
getSubLocationByName(
|
||||||
|
contractData?.Metadata.Location || "",
|
||||||
|
gameVersion,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
if (query.slotname.endsWith(query.slotid!.toString())) {
|
if (query.slotname?.endsWith(query.slotid!.toString())) {
|
||||||
query.slotname = query.slotname.slice(
|
query.slotname = query.slotname.slice(
|
||||||
0,
|
0,
|
||||||
-query.slotid!.toString().length,
|
-query.slotid!.toString().length,
|
||||||
@ -150,7 +154,7 @@ export function getModernStashData(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const stashData: ModernStashData = {
|
const stashData: ModernStashData = {
|
||||||
SlotId: query.slotid,
|
SlotId: query.slotid!,
|
||||||
LoadoutItemsData: {
|
LoadoutItemsData: {
|
||||||
SlotId: query.slotid,
|
SlotId: query.slotid,
|
||||||
Items: getModernStashItemsData(
|
Items: getModernStashItemsData(
|
||||||
@ -169,7 +173,7 @@ export function getModernStashData(
|
|||||||
AllowContainers: query.allowcontainers, // ?? true
|
AllowContainers: query.allowcontainers, // ?? true
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ShowSlotName: query.slotname,
|
ShowSlotName: query.slotname!,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contractData) {
|
if (contractData) {
|
||||||
@ -256,6 +260,10 @@ export function getLegacyStashData(
|
|||||||
userId: string,
|
userId: string,
|
||||||
gameVersion: GameVersion,
|
gameVersion: GameVersion,
|
||||||
) {
|
) {
|
||||||
|
if (!query.contractid || !query.slotname) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
const contractData = controller.resolveContract(query.contractid)
|
const contractData = controller.resolveContract(query.contractid)
|
||||||
|
|
||||||
if (!contractData) {
|
if (!contractData) {
|
||||||
@ -277,6 +285,8 @@ export function getLegacyStashData(
|
|||||||
gameVersion,
|
gameVersion,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
assert.ok(sublocation, "Sublocation not found")
|
||||||
|
|
||||||
const inventory = createInventory(userId, gameVersion, sublocation)
|
const inventory = createInventory(userId, gameVersion, sublocation)
|
||||||
|
|
||||||
const userCentricContract = generateUserCentric(
|
const userCentricContract = generateUserCentric(
|
||||||
@ -297,14 +307,17 @@ export function getLegacyStashData(
|
|||||||
const dl = userProfile.Extensions.defaultloadout
|
const dl = userProfile.Extensions.defaultloadout
|
||||||
|
|
||||||
if (!dl) {
|
if (!dl) {
|
||||||
return defaultLoadout[id]
|
return defaultLoadout[id as keyof typeof defaultLoadout]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain -- it makes the code 10x less readable
|
||||||
const forLocation = (userProfile.Extensions.defaultloadout || {})[
|
const forLocation = (userProfile.Extensions.defaultloadout || {})[
|
||||||
sublocation?.Properties?.ParentLocation
|
sublocation?.Properties?.ParentLocation || ""
|
||||||
]
|
]
|
||||||
|
|
||||||
return (forLocation || defaultLoadout)[id]
|
return (forLocation || defaultLoadout)[
|
||||||
|
id as keyof typeof defaultLoadout
|
||||||
|
]
|
||||||
} else {
|
} else {
|
||||||
let dl = loadouts.getLoadoutFor("h1")
|
let dl = loadouts.getLoadoutFor("h1")
|
||||||
|
|
||||||
@ -312,7 +325,8 @@ export function getLegacyStashData(
|
|||||||
dl = loadouts.createDefault("h1")
|
dl = loadouts.createDefault("h1")
|
||||||
}
|
}
|
||||||
|
|
||||||
const forLocation = dl.data[sublocation?.Properties?.ParentLocation]
|
const forLocation =
|
||||||
|
dl.data[sublocation?.Properties?.ParentLocation || ""]
|
||||||
|
|
||||||
return (forLocation || defaultLoadout)[id]
|
return (forLocation || defaultLoadout)[id]
|
||||||
}
|
}
|
||||||
@ -329,7 +343,7 @@ export function getLegacyStashData(
|
|||||||
Recommended: getLoadoutItem(slotid)
|
Recommended: getLoadoutItem(slotid)
|
||||||
? {
|
? {
|
||||||
item: getUnlockableById(
|
item: getUnlockableById(
|
||||||
getLoadoutItem(slotid),
|
getLoadoutItem(slotid)!,
|
||||||
gameVersion,
|
gameVersion,
|
||||||
),
|
),
|
||||||
type: loadoutSlots[slotid],
|
type: loadoutSlots[slotid],
|
||||||
@ -348,7 +362,7 @@ export function getLegacyStashData(
|
|||||||
}
|
}
|
||||||
: {},
|
: {},
|
||||||
})),
|
})),
|
||||||
Contract: userCentricContract.Contract,
|
Contract: userCentricContract?.Contract,
|
||||||
ShowSlotName: query.slotname,
|
ShowSlotName: query.slotname,
|
||||||
UserCentric: userCentricContract,
|
UserCentric: userCentricContract,
|
||||||
}
|
}
|
||||||
@ -398,10 +412,10 @@ export function getSafehouseCategory(
|
|||||||
continue // I don't want to put this in that elif statement
|
continue // I don't want to put this in that elif statement
|
||||||
}
|
}
|
||||||
|
|
||||||
let category = safehouseData.SubCategories.find(
|
let category = safehouseData.SubCategories?.find(
|
||||||
(cat) => cat.Category === item.Unlockable.Type,
|
(cat) => cat.Category === item.Unlockable.Type,
|
||||||
)
|
)
|
||||||
let subcategory
|
let subcategory: SafehouseCategory | undefined
|
||||||
|
|
||||||
if (!category) {
|
if (!category) {
|
||||||
category = {
|
category = {
|
||||||
@ -410,16 +424,16 @@ export function getSafehouseCategory(
|
|||||||
IsLeaf: false,
|
IsLeaf: false,
|
||||||
Data: null,
|
Data: null,
|
||||||
}
|
}
|
||||||
safehouseData.SubCategories.push(category)
|
safehouseData.SubCategories?.push(category)
|
||||||
}
|
}
|
||||||
|
|
||||||
subcategory = category.SubCategories.find(
|
subcategory = category.SubCategories?.find(
|
||||||
(cat) => cat.Category === item.Unlockable.Subtype,
|
(cat) => cat.Category === item.Unlockable.Subtype,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!subcategory) {
|
if (!subcategory) {
|
||||||
subcategory = {
|
subcategory = {
|
||||||
Category: item.Unlockable.Subtype,
|
Category: item.Unlockable.Subtype!,
|
||||||
SubCategories: null,
|
SubCategories: null,
|
||||||
IsLeaf: true,
|
IsLeaf: true,
|
||||||
Data: {
|
Data: {
|
||||||
@ -430,13 +444,14 @@ export function getSafehouseCategory(
|
|||||||
HasMore: false,
|
HasMore: false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
category.SubCategories.push(subcategory)
|
category.SubCategories?.push(subcategory!)
|
||||||
}
|
}
|
||||||
|
|
||||||
subcategory.Data?.Items.push({
|
subcategory!.Data?.Items.push({
|
||||||
Item: item,
|
Item: item,
|
||||||
ItemDetails: {
|
ItemDetails: {
|
||||||
Capabilities: [],
|
Capabilities: [],
|
||||||
|
// @ts-expect-error It just works. Types are probably wrong somewhere up the chain.
|
||||||
StatList: item.Unlockable.Properties.Gameplay
|
StatList: item.Unlockable.Properties.Gameplay
|
||||||
? Object.entries(item.Unlockable.Properties.Gameplay).map(
|
? Object.entries(item.Unlockable.Properties.Gameplay).map(
|
||||||
([key, value]) => ({
|
([key, value]) => ({
|
||||||
@ -452,15 +467,15 @@ export function getSafehouseCategory(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [id, category] of safehouseData.SubCategories.entries()) {
|
for (const [id, category] of safehouseData.SubCategories?.entries() || []) {
|
||||||
if (category.SubCategories.length === 1) {
|
if (category.SubCategories?.length === 1) {
|
||||||
// if category only has one subcategory
|
// if category only has one subcategory
|
||||||
safehouseData.SubCategories[id] = category.SubCategories[0] // flatten it
|
safehouseData.SubCategories![id] = category.SubCategories[0] // flatten it
|
||||||
safehouseData.SubCategories[id].Category = category.Category // but keep the top category's name
|
safehouseData.SubCategories![id].Category = category.Category // but keep the top category's name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (safehouseData.SubCategories.length === 1) {
|
if (safehouseData.SubCategories?.length === 1) {
|
||||||
// if root has only one subcategory
|
// if root has only one subcategory
|
||||||
safehouseData = safehouseData.SubCategories[0] // flatten it
|
safehouseData = safehouseData.SubCategories[0] // flatten it
|
||||||
}
|
}
|
||||||
|
@ -39,6 +39,7 @@ export const multiplayerMenuDataRouter = Router()
|
|||||||
|
|
||||||
multiplayerMenuDataRouter.post(
|
multiplayerMenuDataRouter.post(
|
||||||
"/multiplayermatchstatsready",
|
"/multiplayermatchstatsready",
|
||||||
|
// @ts-expect-error Has JWT data.
|
||||||
(req: RequestWithJwt<MissionEndRequestQuery>, res) => {
|
(req: RequestWithJwt<MissionEndRequestQuery>, res) => {
|
||||||
res.json({
|
res.json({
|
||||||
template: null,
|
template: null,
|
||||||
@ -53,8 +54,11 @@ multiplayerMenuDataRouter.post(
|
|||||||
|
|
||||||
multiplayerMenuDataRouter.post(
|
multiplayerMenuDataRouter.post(
|
||||||
"/multiplayermatchstats",
|
"/multiplayermatchstats",
|
||||||
|
// @ts-expect-error Has JWT data.
|
||||||
(req: RequestWithJwt<MultiplayerMatchStatsQuery>, res) => {
|
(req: RequestWithJwt<MultiplayerMatchStatsQuery>, res) => {
|
||||||
const sessionDetails = contractSessions.get(req.query.contractSessionId)
|
const sessionDetails = contractSessions.get(
|
||||||
|
req.query.contractSessionId || "",
|
||||||
|
)
|
||||||
|
|
||||||
if (!sessionDetails) {
|
if (!sessionDetails) {
|
||||||
// contract session not found
|
// contract session not found
|
||||||
@ -90,16 +94,22 @@ multiplayerMenuDataRouter.post(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
interface MultiplayerPresetsQuery {
|
type MultiplayerPresetsQuery = {
|
||||||
gamemode?: string
|
gamemode?: string
|
||||||
disguiseUnlockableId?: string
|
disguiseUnlockableId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
multiplayerMenuDataRouter.get(
|
multiplayerMenuDataRouter.get(
|
||||||
"/multiplayerpresets",
|
"/multiplayerpresets",
|
||||||
|
// @ts-expect-error Has JWT data.
|
||||||
(req: RequestWithJwt<MultiplayerPresetsQuery>, res) => {
|
(req: RequestWithJwt<MultiplayerPresetsQuery>, res) => {
|
||||||
if (req.query.gamemode !== "versus") {
|
if (req.query.gamemode !== "versus") {
|
||||||
res.status(401).send("unknown gamemode")
|
res.status(400).send("unknown gamemode")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.query.disguiseUnlockableId) {
|
||||||
|
res.status(400).send("no disguiseUnlockableId")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,9 +151,16 @@ multiplayerMenuDataRouter.get(
|
|||||||
|
|
||||||
multiplayerMenuDataRouter.get(
|
multiplayerMenuDataRouter.get(
|
||||||
"/multiplayer",
|
"/multiplayer",
|
||||||
|
// @ts-expect-error Has JWT data.
|
||||||
(req: RequestWithJwt<MultiplayerQuery>, res) => {
|
(req: RequestWithJwt<MultiplayerQuery>, res) => {
|
||||||
// /multiplayer?gamemode=versus&disguiseUnlockableId=TOKEN_OUTFIT_ELUSIVE_COMPLETE_15_SUIT
|
// /multiplayer?gamemode=versus&disguiseUnlockableId=TOKEN_OUTFIT_ELUSIVE_COMPLETE_15_SUIT
|
||||||
if (req.query.gamemode !== "versus") {
|
if (req.query.gamemode !== "versus") {
|
||||||
|
res.status(400).send("unknown gamemode")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.query.disguiseUnlockableId) {
|
||||||
|
res.status(400).send("no disguiseUnlockableId")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ import { randomUUID } from "crypto"
|
|||||||
import { getConfig } from "../configSwizzleManager"
|
import { getConfig } from "../configSwizzleManager"
|
||||||
import { generateUserCentric } from "../contracts/dataGen"
|
import { generateUserCentric } from "../contracts/dataGen"
|
||||||
import { controller } from "../controller"
|
import { controller } from "../controller"
|
||||||
import { MatchOverC2SEvent } from "../types/events"
|
import { MatchOverC2SEvent, OpponentsC2sEvent } from "../types/events"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A multiplayer preset.
|
* A multiplayer preset.
|
||||||
@ -89,6 +89,7 @@ const activeMatches: Map<string, MatchData> = new Map()
|
|||||||
multiplayerRouter.post(
|
multiplayerRouter.post(
|
||||||
"/GetRequiredResourcesForPreset",
|
"/GetRequiredResourcesForPreset",
|
||||||
jsonMiddleware(),
|
jsonMiddleware(),
|
||||||
|
// @ts-expect-error Has JWT data.
|
||||||
(req: RequestWithJwt, res) => {
|
(req: RequestWithJwt, res) => {
|
||||||
const allPresets = getConfig<MultiplayerPreset[]>(
|
const allPresets = getConfig<MultiplayerPreset[]>(
|
||||||
"MultiplayerPresets",
|
"MultiplayerPresets",
|
||||||
@ -114,7 +115,7 @@ multiplayerRouter.post(
|
|||||||
req.gameVersion,
|
req.gameVersion,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.filter(Boolean)
|
.filter(Boolean) as UserCentricContract[]
|
||||||
|
|
||||||
res.json(
|
res.json(
|
||||||
userCentrics.map((userCentric: UserCentricContract) => ({
|
userCentrics.map((userCentric: UserCentricContract) => ({
|
||||||
@ -132,6 +133,7 @@ multiplayerRouter.post(
|
|||||||
multiplayerRouter.post(
|
multiplayerRouter.post(
|
||||||
"/RegisterToMatch",
|
"/RegisterToMatch",
|
||||||
jsonMiddleware(),
|
jsonMiddleware(),
|
||||||
|
// @ts-expect-error Has JWT data.
|
||||||
(req: RequestWithJwt, res) => {
|
(req: RequestWithJwt, res) => {
|
||||||
// get a random contract from the list of possible ones in the selected preset
|
// get a random contract from the list of possible ones in the selected preset
|
||||||
const multiplayerPresets = getConfig<MultiplayerPreset[]>(
|
const multiplayerPresets = getConfig<MultiplayerPreset[]>(
|
||||||
@ -212,6 +214,7 @@ multiplayerRouter.post(
|
|||||||
multiplayerRouter.post(
|
multiplayerRouter.post(
|
||||||
"/SetMatchData",
|
"/SetMatchData",
|
||||||
jsonMiddleware(),
|
jsonMiddleware(),
|
||||||
|
// @ts-expect-error Has JWT data.
|
||||||
(req: RequestWithJwt, res) => {
|
(req: RequestWithJwt, res) => {
|
||||||
const match = activeMatches.get(req.body.matchId)
|
const match = activeMatches.get(req.body.matchId)
|
||||||
|
|
||||||
@ -256,9 +259,7 @@ export function handleMultiplayerEvent(
|
|||||||
ghost.unnoticedKills += 1
|
ghost.unnoticedKills += 1
|
||||||
return true
|
return true
|
||||||
case "Opponents": {
|
case "Opponents": {
|
||||||
const value = event.Value as {
|
const value = (event as OpponentsC2sEvent).Value
|
||||||
ConnectedSessions: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
ghost.Opponents = value.ConnectedSessions
|
ghost.Opponents = value.ConnectedSessions
|
||||||
return true
|
return true
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Response } from "express"
|
|
||||||
import { decode, sign } from "jsonwebtoken"
|
import { decode, sign } from "jsonwebtoken"
|
||||||
import { extractToken, uuidRegex } from "./utils"
|
import { extractToken, uuidRegex } from "./utils"
|
||||||
import type { GameVersion, RequestWithJwt, UserProfile } from "./types/types"
|
import type { GameVersion, RequestWithJwt, UserProfile } from "./types/types"
|
||||||
@ -49,10 +48,37 @@ export const JWT_SECRET = PEACOCK_DEV
|
|||||||
? "secret"
|
? "secret"
|
||||||
: randomBytes(32).toString("hex")
|
: randomBytes(32).toString("hex")
|
||||||
|
|
||||||
export async function handleOauthToken(
|
export type OAuthTokenBody = {
|
||||||
req: RequestWithJwt,
|
grant_type: "external_steam" | "external_epic" | "refresh_token"
|
||||||
res: Response,
|
steam_userid?: string
|
||||||
): Promise<void> {
|
epic_userid?: string
|
||||||
|
access_token: string
|
||||||
|
pId?: string
|
||||||
|
locale: string
|
||||||
|
rgn: string
|
||||||
|
gs: string
|
||||||
|
steam_appid: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OAuthTokenResponse = {
|
||||||
|
access_token: string
|
||||||
|
token_type: "bearer" | string
|
||||||
|
expires_in: number
|
||||||
|
refresh_token: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const error400: unique symbol = Symbol("http400")
|
||||||
|
export const error406: unique symbol = Symbol("http406")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the code that handles the OAuth token request.
|
||||||
|
* We cannot do this without a request object because of the refresh token use case.
|
||||||
|
*
|
||||||
|
* @param req The request object.
|
||||||
|
*/
|
||||||
|
export async function handleOAuthToken(
|
||||||
|
req: RequestWithJwt<never, OAuthTokenBody>,
|
||||||
|
): Promise<typeof error400 | typeof error406 | OAuthTokenResponse> {
|
||||||
const isFrankenstein = req.body.gs === "scpc-prod"
|
const isFrankenstein = req.body.gs === "scpc-prod"
|
||||||
|
|
||||||
const signOptions = {
|
const signOptions = {
|
||||||
@ -69,19 +95,17 @@ export async function handleOauthToken(
|
|||||||
external_appid: string
|
external_appid: string
|
||||||
|
|
||||||
if (req.body.grant_type === "external_steam") {
|
if (req.body.grant_type === "external_steam") {
|
||||||
if (!/^\d{1,20}$/.test(req.body.steam_userid)) {
|
if (!/^\d{1,20}$/.test(req.body.steam_userid || "")) {
|
||||||
res.status(400).end() // invalid steam user id
|
return error400 // invalid steam user id
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
external_platform = "steam"
|
external_platform = "steam"
|
||||||
external_userid = req.body.steam_userid
|
external_userid = req.body.steam_userid || ""
|
||||||
external_users_folder = "steamids"
|
external_users_folder = "steamids"
|
||||||
external_appid = req.body.steam_appid
|
external_appid = req.body.steam_appid
|
||||||
} else if (req.body.grant_type === "external_epic") {
|
} else if (req.body.grant_type === "external_epic") {
|
||||||
if (!/^[\da-f]{32}$/.test(req.body.epic_userid)) {
|
if (!/^[\da-f]{32}$/.test(req.body.epic_userid || "")) {
|
||||||
res.status(400).end() // invalid epic user id
|
return error400 // invalid epic user id
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const epic_token = decode(
|
const epic_token = decode(
|
||||||
@ -92,25 +116,24 @@ export async function handleOauthToken(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!epic_token || !(epic_token.appid || epic_token.app)) {
|
if (!epic_token || !(epic_token.appid || epic_token.app)) {
|
||||||
res.status(400).end() // invalid epic access token
|
return error400 // invalid epic access token
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
external_appid = epic_token.appid || epic_token.app
|
external_appid = epic_token.appid || epic_token.app
|
||||||
external_platform = "epic"
|
external_platform = "epic"
|
||||||
external_userid = req.body.epic_userid
|
external_userid = req.body.epic_userid || ""
|
||||||
external_users_folder = "epicids"
|
external_users_folder = "epicids"
|
||||||
} else if (req.body.grant_type === "refresh_token") {
|
} else if (req.body.grant_type === "refresh_token") {
|
||||||
// send back the token from the request (re-signed so the timestamps update)
|
// send back the token from the request (re-signed so the timestamps update)
|
||||||
extractToken(req) // init req.jwt
|
extractToken(req) // init req.jwt
|
||||||
// remove signOptions from existing jwt
|
// remove signOptions from existing jwt
|
||||||
// ts-expect-error Non-optional, we're reassigning.
|
// @ts-expect-error Non-optional, we're reassigning.
|
||||||
delete req.jwt.nbf // notBefore
|
delete req.jwt.nbf // notBefore
|
||||||
// ts-expect-error Non-optional, we're reassigning.
|
// @ts-expect-error Non-optional, we're reassigning.
|
||||||
delete req.jwt.exp // expiresIn
|
delete req.jwt.exp // expiresIn
|
||||||
// ts-expect-error Non-optional, we're reassigning.
|
// @ts-expect-error Non-optional, we're reassigning.
|
||||||
delete req.jwt.iss // issuer
|
delete req.jwt.iss // issuer
|
||||||
// ts-expect-error Non-optional, we're reassigning.
|
// @ts-expect-error Non-optional, we're reassigning.
|
||||||
delete req.jwt.aud // audience
|
delete req.jwt.aud // audience
|
||||||
|
|
||||||
if (!isFrankenstein) {
|
if (!isFrankenstein) {
|
||||||
@ -126,35 +149,33 @@ export async function handleOauthToken(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({
|
return {
|
||||||
access_token: sign(req.jwt, JWT_SECRET, signOptions),
|
access_token: sign(req.jwt, JWT_SECRET, signOptions),
|
||||||
token_type: "bearer",
|
token_type: "bearer",
|
||||||
expires_in: 5000,
|
expires_in: 5000,
|
||||||
refresh_token: randomUUID(),
|
refresh_token: randomUUID(),
|
||||||
})
|
}
|
||||||
|
|
||||||
return
|
|
||||||
} else {
|
} else {
|
||||||
res.status(406).end() // unsupported auth method
|
return error406 // unsupported auth method
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.body.pId && !uuidRegex.test(req.body.pId)) {
|
if (req.body.pId && !uuidRegex.test(req.body.pId)) {
|
||||||
res.status(400).end() // pId is not a GUID
|
return error406 // pId is not a GUID
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const isHitman3 =
|
const isHitman3 =
|
||||||
external_appid === "fghi4567xQOCheZIin0pazB47qGUvZw4" ||
|
external_appid === "fghi4567xQOCheZIin0pazB47qGUvZw4" ||
|
||||||
external_appid === STEAM_NAMESPACE_2021
|
external_appid === STEAM_NAMESPACE_2021
|
||||||
|
|
||||||
const gameVersion: GameVersion = isFrankenstein
|
let gameVersion: GameVersion = "h1"
|
||||||
? "scpc"
|
|
||||||
: isHitman3
|
if (isFrankenstein) {
|
||||||
? "h3"
|
gameVersion = "scpc"
|
||||||
: external_appid === STEAM_NAMESPACE_2018
|
} else if (isHitman3) {
|
||||||
? "h2"
|
gameVersion = "h3"
|
||||||
: "h1"
|
} else if (external_appid === STEAM_NAMESPACE_2018) {
|
||||||
|
gameVersion = "h2"
|
||||||
|
}
|
||||||
|
|
||||||
if (!req.body.pId) {
|
if (!req.body.pId) {
|
||||||
// if no profile id supplied
|
// if no profile id supplied
|
||||||
@ -184,7 +205,8 @@ export async function handleOauthToken(
|
|||||||
await writeExternalUserData(
|
await writeExternalUserData(
|
||||||
external_userid,
|
external_userid,
|
||||||
external_users_folder,
|
external_users_folder,
|
||||||
req.body.pId,
|
// we've already confirmed this will be there, and it's a GUID
|
||||||
|
req.body.pId!,
|
||||||
gameVersion,
|
gameVersion,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -227,9 +249,9 @@ export async function handleOauthToken(
|
|||||||
userData.LinkedAccounts[external_platform] = external_userid
|
userData.LinkedAccounts[external_platform] = external_userid
|
||||||
|
|
||||||
if (external_platform === "steam") {
|
if (external_platform === "steam") {
|
||||||
userData.SteamId = req.body.steam_userid
|
userData.SteamId = req.body.steam_userid!
|
||||||
} else if (external_platform === "epic") {
|
} else if (external_platform === "epic") {
|
||||||
userData.EpicId = req.body.epic_userid
|
userData.EpicId = req.body.epic_userid!
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Object.hasOwn(userData.Extensions, "inventory")) {
|
if (Object.hasOwn(userData.Extensions, "inventory")) {
|
||||||
@ -262,13 +284,13 @@ export async function handleOauthToken(
|
|||||||
if (external_platform === "epic") {
|
if (external_platform === "epic") {
|
||||||
return await new EpicH3Strategy().get(
|
return await new EpicH3Strategy().get(
|
||||||
req.body.access_token,
|
req.body.access_token,
|
||||||
req.body.epic_userid,
|
req.body.epic_userid!,
|
||||||
)
|
)
|
||||||
} else if (external_platform === "steam") {
|
} else if (external_platform === "steam") {
|
||||||
return await new IOIStrategy(
|
return await new IOIStrategy(
|
||||||
gameVersion,
|
gameVersion,
|
||||||
STEAM_NAMESPACE_2021,
|
STEAM_NAMESPACE_2021,
|
||||||
).get(req.body.pId)
|
).get(req.body.pId!)
|
||||||
} else {
|
} else {
|
||||||
log(LogLevel.ERROR, "Unsupported platform.")
|
log(LogLevel.ERROR, "Unsupported platform.")
|
||||||
return []
|
return []
|
||||||
@ -316,10 +338,10 @@ export async function handleOauthToken(
|
|||||||
|
|
||||||
clearInventoryFor(req.body.pId)
|
clearInventoryFor(req.body.pId)
|
||||||
|
|
||||||
res!.json({
|
return {
|
||||||
access_token: sign(userinfo, JWT_SECRET, signOptions),
|
access_token: sign(userinfo, JWT_SECRET, signOptions),
|
||||||
token_type: "bearer",
|
token_type: "bearer",
|
||||||
expires_in: 5000,
|
expires_in: 5000,
|
||||||
refresh_token: randomUUID(),
|
refresh_token: randomUUID(),
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import axios, { AxiosResponse } from "axios"
|
import axios, { AxiosError, AxiosResponse } from "axios"
|
||||||
import type { Request } from "express"
|
import type { Request } from "express"
|
||||||
import { log, LogLevel } from "./loggingInterop"
|
import { log, LogLevel } from "./loggingInterop"
|
||||||
import { handleAxiosError } from "./utils"
|
import { handleAxiosError } from "./utils"
|
||||||
@ -106,7 +106,7 @@ export class OfficialServerAuth {
|
|||||||
this._refreshToken = r.refresh_token
|
this._refreshToken = r.refresh_token
|
||||||
this.initialized = true
|
this.initialized = true
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
handleAxiosError(e)
|
handleAxiosError(e as AxiosError)
|
||||||
|
|
||||||
if (PEACOCK_DEV) {
|
if (PEACOCK_DEV) {
|
||||||
log(
|
log(
|
||||||
|
@ -42,7 +42,7 @@ export function calculatePlaystyle(
|
|||||||
const doneKillMethods: string[] = []
|
const doneKillMethods: string[] = []
|
||||||
const doneAccidents: string[] = []
|
const doneAccidents: string[] = []
|
||||||
|
|
||||||
session.kills.forEach((k) => {
|
session.kills?.forEach((k) => {
|
||||||
if (k.KillClass === "ballistic") {
|
if (k.KillClass === "ballistic") {
|
||||||
if (k.KillItemCategory === "pistol") {
|
if (k.KillItemCategory === "pistol") {
|
||||||
playstylesCopy[1].Score += 6000
|
playstylesCopy[1].Score += 6000
|
||||||
|
@ -29,7 +29,8 @@ import {
|
|||||||
import { json as jsonMiddleware } from "body-parser"
|
import { json as jsonMiddleware } from "body-parser"
|
||||||
import { getPlatformEntitlements } from "./platformEntitlements"
|
import { getPlatformEntitlements } from "./platformEntitlements"
|
||||||
import { contractSessions, newSession } from "./eventHandler"
|
import { contractSessions, newSession } from "./eventHandler"
|
||||||
import type {
|
import {
|
||||||
|
ChallengeProgressionData,
|
||||||
CompiledChallengeIngameData,
|
CompiledChallengeIngameData,
|
||||||
ContractSession,
|
ContractSession,
|
||||||
GameVersion,
|
GameVersion,
|
||||||
@ -57,7 +58,8 @@ import {
|
|||||||
compileRuntimeChallenge,
|
compileRuntimeChallenge,
|
||||||
inclusionDataCheck,
|
inclusionDataCheck,
|
||||||
} from "./candle/challengeHelpers"
|
} from "./candle/challengeHelpers"
|
||||||
import { LoadSaveBody } from "./types/gameSchemas"
|
import { LoadSaveBody, ResolveGamerTagsBody } from "./types/gameSchemas"
|
||||||
|
import assert from "assert"
|
||||||
|
|
||||||
const profileRouter = Router()
|
const profileRouter = Router()
|
||||||
|
|
||||||
@ -109,8 +111,9 @@ export const fakePlayerRegistry: {
|
|||||||
|
|
||||||
profileRouter.post(
|
profileRouter.post(
|
||||||
"/AuthenticationService/GetBlobOfflineCacheDatabaseDiff",
|
"/AuthenticationService/GetBlobOfflineCacheDatabaseDiff",
|
||||||
|
// @ts-expect-error Has jwt props.
|
||||||
(req: RequestWithJwt, res) => {
|
(req: RequestWithJwt, res) => {
|
||||||
const configs = []
|
const configs: string[] = []
|
||||||
|
|
||||||
menuSystemDatabase.hooks.getDatabaseDiff.call(configs, req.gameVersion)
|
menuSystemDatabase.hooks.getDatabaseDiff.call(configs, req.gameVersion)
|
||||||
|
|
||||||
@ -118,19 +121,21 @@ profileRouter.post(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
profileRouter.post("/ProfileService/SetClientEntitlements", (req, res) => {
|
profileRouter.post("/ProfileService/SetClientEntitlements", (_, res) => {
|
||||||
res.json("null")
|
res.json("null")
|
||||||
})
|
})
|
||||||
|
|
||||||
profileRouter.post(
|
profileRouter.post(
|
||||||
"/ProfileService/GetPlatformEntitlements",
|
"/ProfileService/GetPlatformEntitlements",
|
||||||
jsonMiddleware(),
|
jsonMiddleware(),
|
||||||
|
// @ts-expect-error Jwt props.
|
||||||
getPlatformEntitlements,
|
getPlatformEntitlements,
|
||||||
)
|
)
|
||||||
|
|
||||||
profileRouter.post(
|
profileRouter.post(
|
||||||
"/ProfileService/UpdateProfileStats",
|
"/ProfileService/UpdateProfileStats",
|
||||||
jsonMiddleware(),
|
jsonMiddleware(),
|
||||||
|
// @ts-expect-error Has jwt props.
|
||||||
(req: RequestWithJwt, res) => {
|
(req: RequestWithJwt, res) => {
|
||||||
if (req.jwt.unique_name !== req.body.id) {
|
if (req.jwt.unique_name !== req.body.id) {
|
||||||
return res.status(403).end() // data submitted for different profile id
|
return res.status(403).end() // data submitted for different profile id
|
||||||
@ -148,18 +153,19 @@ profileRouter.post(
|
|||||||
|
|
||||||
profileRouter.post(
|
profileRouter.post(
|
||||||
"/ProfileService/SynchronizeOfflineUnlockables",
|
"/ProfileService/SynchronizeOfflineUnlockables",
|
||||||
(req, res) => {
|
(_, res) => {
|
||||||
res.status(204).end()
|
res.status(204).end()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
profileRouter.post("/ProfileService/GetUserConfig", (req, res) => {
|
profileRouter.post("/ProfileService/GetUserConfig", (_, res) => {
|
||||||
res.json({})
|
res.json({})
|
||||||
})
|
})
|
||||||
|
|
||||||
profileRouter.post(
|
profileRouter.post(
|
||||||
"/ProfileService/GetProfile",
|
"/ProfileService/GetProfile",
|
||||||
jsonMiddleware(),
|
jsonMiddleware(),
|
||||||
|
// @ts-expect-error Has jwt props.
|
||||||
(req: RequestWithJwt, res) => {
|
(req: RequestWithJwt, res) => {
|
||||||
if (req.body.id !== req.jwt.unique_name) {
|
if (req.body.id !== req.jwt.unique_name) {
|
||||||
res.status(403).end() // data requested for different profile id
|
res.status(403).end() // data requested for different profile id
|
||||||
@ -173,7 +179,10 @@ profileRouter.post(
|
|||||||
const userdata = getUserData(req.jwt.unique_name, req.gameVersion)
|
const userdata = getUserData(req.jwt.unique_name, req.gameVersion)
|
||||||
const extensions = req.body.extensions.reduce(
|
const extensions = req.body.extensions.reduce(
|
||||||
(acc: object, key: string) => {
|
(acc: object, key: string) => {
|
||||||
if (Object.hasOwn(userdata.Extensions, key)) {
|
if (
|
||||||
|
userdata.Extensions[key as keyof typeof userdata.Extensions]
|
||||||
|
) {
|
||||||
|
// @ts-expect-error Ok.
|
||||||
acc[key] = userdata.Extensions[key]
|
acc[key] = userdata.Extensions[key]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -188,6 +197,7 @@ profileRouter.post(
|
|||||||
|
|
||||||
profileRouter.post(
|
profileRouter.post(
|
||||||
"/UnlockableService/GetInventory",
|
"/UnlockableService/GetInventory",
|
||||||
|
// @ts-expect-error Has jwt props.
|
||||||
(req: RequestWithJwt, res) => {
|
(req: RequestWithJwt, res) => {
|
||||||
res.json(createInventory(req.jwt.unique_name, req.gameVersion))
|
res.json(createInventory(req.jwt.unique_name, req.gameVersion))
|
||||||
},
|
},
|
||||||
@ -196,6 +206,7 @@ profileRouter.post(
|
|||||||
profileRouter.post(
|
profileRouter.post(
|
||||||
"/ProfileService/UpdateExtensions",
|
"/ProfileService/UpdateExtensions",
|
||||||
jsonMiddleware(),
|
jsonMiddleware(),
|
||||||
|
// @ts-expect-error jwt props.
|
||||||
(
|
(
|
||||||
req: RequestWithJwt<
|
req: RequestWithJwt<
|
||||||
Record<string, never>,
|
Record<string, never>,
|
||||||
@ -213,6 +224,7 @@ profileRouter.post(
|
|||||||
|
|
||||||
for (const extension in req.body.extensionsData) {
|
for (const extension in req.body.extensionsData) {
|
||||||
if (Object.hasOwn(req.body.extensionsData, extension)) {
|
if (Object.hasOwn(req.body.extensionsData, extension)) {
|
||||||
|
// @ts-expect-error It's fine.
|
||||||
userdata.Extensions[extension] =
|
userdata.Extensions[extension] =
|
||||||
req.body.extensionsData[extension]
|
req.body.extensionsData[extension]
|
||||||
}
|
}
|
||||||
@ -226,6 +238,7 @@ profileRouter.post(
|
|||||||
profileRouter.post(
|
profileRouter.post(
|
||||||
"/ProfileService/SynchroniseGameStats",
|
"/ProfileService/SynchroniseGameStats",
|
||||||
jsonMiddleware(),
|
jsonMiddleware(),
|
||||||
|
// @ts-expect-error jwt props.
|
||||||
(req: RequestWithJwt, res) => {
|
(req: RequestWithJwt, res) => {
|
||||||
if (req.body.profileId !== req.jwt.unique_name) {
|
if (req.body.profileId !== req.jwt.unique_name) {
|
||||||
// data requested for different profile id
|
// data requested for different profile id
|
||||||
@ -282,32 +295,6 @@ export async function resolveProfiles(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (id === "a38faeaa-5b5b-4d7e-af90-329e98a26652") {
|
|
||||||
log(
|
|
||||||
LogLevel.WARN,
|
|
||||||
"The game tried to resolve the PeacockProject account, which should no longer be used!",
|
|
||||||
)
|
|
||||||
|
|
||||||
return Promise.resolve({
|
|
||||||
Id: "a38faeaa-5b5b-4d7e-af90-329e98a26652",
|
|
||||||
LinkedAccounts: {
|
|
||||||
dev: "PeacockProject",
|
|
||||||
},
|
|
||||||
Extensions: {},
|
|
||||||
ETag: null,
|
|
||||||
Gamertag: "PeacockProject",
|
|
||||||
DevId: "PeacockProject",
|
|
||||||
SteamId: null,
|
|
||||||
StadiaId: null,
|
|
||||||
EpicId: null,
|
|
||||||
NintendoId: null,
|
|
||||||
XboxLiveId: null,
|
|
||||||
PSNAccountId: null,
|
|
||||||
PSNOnlineId: null,
|
|
||||||
Version: LATEST_PROFILE_VERSION,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const fakePlayer = fakePlayerRegistry.getFromId(id)
|
const fakePlayer = fakePlayerRegistry.getFromId(id)
|
||||||
|
|
||||||
if (fakePlayer) {
|
if (fakePlayer) {
|
||||||
@ -350,6 +337,7 @@ export async function resolveProfiles(
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
// @ts-expect-error This whole function is an exception handling clusterfunk and needs to be rewritten.
|
||||||
.map((outcome: PromiseSettledResult<UserProfile>) => {
|
.map((outcome: PromiseSettledResult<UserProfile>) => {
|
||||||
if (outcome.status !== "fulfilled") {
|
if (outcome.status !== "fulfilled") {
|
||||||
if (outcome.reason.code === "ENOENT") {
|
if (outcome.reason.code === "ENOENT") {
|
||||||
@ -387,6 +375,7 @@ export async function resolveProfiles(
|
|||||||
profileRouter.post(
|
profileRouter.post(
|
||||||
"/ProfileService/ResolveProfiles",
|
"/ProfileService/ResolveProfiles",
|
||||||
jsonMiddleware(),
|
jsonMiddleware(),
|
||||||
|
// @ts-expect-error Has jwt props.
|
||||||
async (req: RequestWithJwt, res) => {
|
async (req: RequestWithJwt, res) => {
|
||||||
res.json(await resolveProfiles(req.body.profileIDs, req.gameVersion))
|
res.json(await resolveProfiles(req.body.profileIDs, req.gameVersion))
|
||||||
},
|
},
|
||||||
@ -395,16 +384,22 @@ profileRouter.post(
|
|||||||
profileRouter.post(
|
profileRouter.post(
|
||||||
"/ProfileService/ResolveGamerTags",
|
"/ProfileService/ResolveGamerTags",
|
||||||
jsonMiddleware(),
|
jsonMiddleware(),
|
||||||
async (req: RequestWithJwt, res) => {
|
// @ts-expect-error Has jwt props.
|
||||||
|
async (req: RequestWithJwt<never, ResolveGamerTagsBody>, res) => {
|
||||||
|
if (!Array.isArray(req.body.profileIds)) {
|
||||||
|
res.status(400).send("bad body")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const profiles = (await resolveProfiles(
|
const profiles = (await resolveProfiles(
|
||||||
req.body.profileIds,
|
req.body.profileIds,
|
||||||
req.gameVersion,
|
req.gameVersion,
|
||||||
)) as UserProfile[]
|
)) as UserProfile[]
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
steam: {},
|
steam: {} as Record<string, string>,
|
||||||
epic: {},
|
epic: {} as Record<string, string>,
|
||||||
dev: {},
|
dev: {} as Record<string, string>,
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const profile of profiles) {
|
for (const profile of profiles) {
|
||||||
@ -427,26 +422,27 @@ profileRouter.post(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
profileRouter.post("/ProfileService/GetFriendsCount", (req, res) =>
|
profileRouter.post("/ProfileService/GetFriendsCount", (_, res) => res.send("0"))
|
||||||
res.send("0"),
|
|
||||||
)
|
|
||||||
|
|
||||||
profileRouter.post(
|
profileRouter.post(
|
||||||
"/GamePersistentDataService/GetData",
|
"/GamePersistentDataService/GetData",
|
||||||
jsonMiddleware(),
|
jsonMiddleware(),
|
||||||
|
// @ts-expect-error Has jwt props.
|
||||||
(req: RequestWithJwt, res) => {
|
(req: RequestWithJwt, res) => {
|
||||||
if (req.jwt.unique_name !== req.body.userId) {
|
if (req.jwt.unique_name !== req.body.userId) {
|
||||||
return res.status(403).end()
|
return res.status(403).end()
|
||||||
}
|
}
|
||||||
|
|
||||||
const userdata = getUserData(req.body.userId, req.gameVersion)
|
const userdata = getUserData(req.body.userId, req.gameVersion)
|
||||||
res.json(userdata.Extensions.gamepersistentdata[req.body.key])
|
type Cast = keyof typeof userdata.Extensions.gamepersistentdata
|
||||||
|
res.json(userdata.Extensions.gamepersistentdata[req.body.key as Cast])
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
profileRouter.post(
|
profileRouter.post(
|
||||||
"/GamePersistentDataService/SaveData",
|
"/GamePersistentDataService/SaveData",
|
||||||
jsonMiddleware(),
|
jsonMiddleware(),
|
||||||
|
// @ts-expect-error jwt props.
|
||||||
(req: RequestWithJwt, res) => {
|
(req: RequestWithJwt, res) => {
|
||||||
if (req.jwt.unique_name !== req.body.userId) {
|
if (req.jwt.unique_name !== req.body.userId) {
|
||||||
return res.status(403).end()
|
return res.status(403).end()
|
||||||
@ -454,6 +450,7 @@ profileRouter.post(
|
|||||||
|
|
||||||
const userdata = getUserData(req.body.userId, req.gameVersion)
|
const userdata = getUserData(req.body.userId, req.gameVersion)
|
||||||
|
|
||||||
|
// @ts-expect-error This is fine.
|
||||||
userdata.Extensions.gamepersistentdata[req.body.key] = req.body.data
|
userdata.Extensions.gamepersistentdata[req.body.key] = req.body.data
|
||||||
writeUserData(req.body.userId, req.gameVersion)
|
writeUserData(req.body.userId, req.gameVersion)
|
||||||
|
|
||||||
@ -464,6 +461,7 @@ profileRouter.post(
|
|||||||
profileRouter.post(
|
profileRouter.post(
|
||||||
"/ChallengesService/GetActiveChallengesAndProgression",
|
"/ChallengesService/GetActiveChallengesAndProgression",
|
||||||
jsonMiddleware(),
|
jsonMiddleware(),
|
||||||
|
// @ts-expect-error Has jwt props.
|
||||||
(
|
(
|
||||||
req: RequestWithJwt<
|
req: RequestWithJwt<
|
||||||
Record<string, never>,
|
Record<string, never>,
|
||||||
@ -489,11 +487,14 @@ profileRouter.post(
|
|||||||
return res.json([])
|
return res.json([])
|
||||||
}
|
}
|
||||||
|
|
||||||
let challenges = getVersionedConfig<CompiledChallengeIngameData[]>(
|
type CWP = {
|
||||||
"GlobalChallenges",
|
Challenge: CompiledChallengeIngameData
|
||||||
req.gameVersion,
|
Progression: ChallengeProgressionData | undefined
|
||||||
true,
|
}
|
||||||
)
|
|
||||||
|
let challenges: CWP[] = getVersionedConfig<
|
||||||
|
CompiledChallengeIngameData[]
|
||||||
|
>("GlobalChallenges", req.gameVersion, true)
|
||||||
.filter((val) => inclusionDataCheck(val.InclusionData, json))
|
.filter((val) => inclusionDataCheck(val.InclusionData, json))
|
||||||
.map((item) => ({ Challenge: item, Progression: undefined }))
|
.map((item) => ({ Challenge: item, Progression: undefined }))
|
||||||
|
|
||||||
@ -528,6 +529,7 @@ profileRouter.post(
|
|||||||
challenges.forEach((val) => {
|
challenges.forEach((val) => {
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
if (val.Challenge.Id === "b1a85feb-55af-4707-8271-b3522661c0b1") {
|
if (val.Challenge.Id === "b1a85feb-55af-4707-8271-b3522661c0b1") {
|
||||||
|
// @ts-expect-error State machines impossible to type.
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
val.Challenge.Definition!["States"]["Start"][
|
val.Challenge.Definition!["States"]["Start"][
|
||||||
"CrowdNPC_Died"
|
"CrowdNPC_Died"
|
||||||
@ -580,6 +582,7 @@ profileRouter.post(
|
|||||||
profileRouter.post(
|
profileRouter.post(
|
||||||
"/HubPagesService/GetChallengeTreeFor",
|
"/HubPagesService/GetChallengeTreeFor",
|
||||||
jsonMiddleware(),
|
jsonMiddleware(),
|
||||||
|
// @ts-expect-error Has jwt props.
|
||||||
(req: RequestWithJwt, res) => {
|
(req: RequestWithJwt, res) => {
|
||||||
res.json({
|
res.json({
|
||||||
Data: {
|
Data: {
|
||||||
@ -607,6 +610,7 @@ profileRouter.post(
|
|||||||
profileRouter.post(
|
profileRouter.post(
|
||||||
"/DefaultLoadoutService/Set",
|
"/DefaultLoadoutService/Set",
|
||||||
jsonMiddleware(),
|
jsonMiddleware(),
|
||||||
|
// @ts-expect-error jwt props.
|
||||||
async (req: RequestWithJwt, res) => {
|
async (req: RequestWithJwt, res) => {
|
||||||
if (getFlag("loadoutSaving") === "PROFILES") {
|
if (getFlag("loadoutSaving") === "PROFILES") {
|
||||||
let loadout = loadouts.getLoadoutFor(req.gameVersion)
|
let loadout = loadouts.getLoadoutFor(req.gameVersion)
|
||||||
@ -638,6 +642,7 @@ profileRouter.post(
|
|||||||
profileRouter.post(
|
profileRouter.post(
|
||||||
"/ProfileService/UpdateUserSaveFileTable",
|
"/ProfileService/UpdateUserSaveFileTable",
|
||||||
jsonMiddleware(),
|
jsonMiddleware(),
|
||||||
|
// @ts-expect-error Has jwt props.
|
||||||
async (req: RequestWithJwt<never, UpdateUserSaveFileTableBody>, res) => {
|
async (req: RequestWithJwt<never, UpdateUserSaveFileTableBody>, res) => {
|
||||||
if (req.body.clientSaveFileList.length > 0) {
|
if (req.body.clientSaveFileList.length > 0) {
|
||||||
// We are saving to the SaveFile with the most recent timestamp.
|
// We are saving to the SaveFile with the most recent timestamp.
|
||||||
@ -681,7 +686,7 @@ profileRouter.post(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
function getErrorMessage(error: unknown) {
|
export function getErrorMessage(error: unknown) {
|
||||||
if (error instanceof Error) return error.message
|
if (error instanceof Error) return error.message
|
||||||
return String(error)
|
return String(error)
|
||||||
}
|
}
|
||||||
@ -691,7 +696,82 @@ function getErrorCause(error: unknown) {
|
|||||||
return String(error)
|
return String(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveSession(
|
profileRouter.post(
|
||||||
|
"/ContractSessionsService/Load",
|
||||||
|
jsonMiddleware(),
|
||||||
|
// @ts-expect-error Has jwt props.
|
||||||
|
async (req: RequestWithJwt<never, LoadSaveBody>, res) => {
|
||||||
|
const userData = getUserData(req.jwt.unique_name, req.gameVersion)
|
||||||
|
|
||||||
|
if (
|
||||||
|
!req.body.contractSessionId ||
|
||||||
|
!req.body.saveToken ||
|
||||||
|
!req.body.contractId
|
||||||
|
) {
|
||||||
|
res.status(400).send("bad body")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await loadSession(
|
||||||
|
req.body.contractSessionId,
|
||||||
|
req.body.saveToken,
|
||||||
|
userData,
|
||||||
|
)
|
||||||
|
} catch (e) {
|
||||||
|
log(
|
||||||
|
LogLevel.DEBUG,
|
||||||
|
`Failed to load contract with token = ${
|
||||||
|
req.body.saveToken
|
||||||
|
}, session id = ${req.body.contractSessionId} because ${
|
||||||
|
(e as Error)?.message
|
||||||
|
}`,
|
||||||
|
)
|
||||||
|
log(
|
||||||
|
LogLevel.WARN,
|
||||||
|
"No such save detected! Might be an official servers save.",
|
||||||
|
)
|
||||||
|
|
||||||
|
if (PEACOCK_DEV) {
|
||||||
|
log(
|
||||||
|
LogLevel.DEBUG,
|
||||||
|
`(Save-context: ${req.body.contractSessionId}; ${req.body.saveToken})`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
log(
|
||||||
|
LogLevel.WARN,
|
||||||
|
"Creating a fake session to avoid problems... scoring will not work!",
|
||||||
|
)
|
||||||
|
|
||||||
|
newSession(
|
||||||
|
req.body.contractSessionId,
|
||||||
|
req.body.contractId,
|
||||||
|
req.jwt.unique_name,
|
||||||
|
req.body.difficultyLevel!,
|
||||||
|
req.gameVersion,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
res.send(`"${req.body.contractSessionId}"`)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
profileRouter.post(
|
||||||
|
"/ProfileService/GetSemLinkStatus",
|
||||||
|
jsonMiddleware(),
|
||||||
|
(_, res) => {
|
||||||
|
res.json({
|
||||||
|
IsConfirmed: true,
|
||||||
|
LinkedEmail: "mail@example.com",
|
||||||
|
IOIAccountId: nilUuid,
|
||||||
|
IOIAccountBaseUrl: "https://account.ioi.dk",
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
export async function saveSession(
|
||||||
save: SaveFile,
|
save: SaveFile,
|
||||||
userData: UserProfile,
|
userData: UserProfile,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
@ -747,69 +827,12 @@ async function saveSession(
|
|||||||
log(
|
log(
|
||||||
LogLevel.DEBUG,
|
LogLevel.DEBUG,
|
||||||
`Saved contract to slot ${slot} with token = ${token}, session id = ${sessionId}, start time = ${
|
`Saved contract to slot ${slot} with token = ${token}, session id = ${sessionId}, start time = ${
|
||||||
contractSessions.get(sessionId).timerStart
|
contractSessions.get(sessionId)!.timerStart
|
||||||
}.`,
|
}.`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
profileRouter.post(
|
export async function loadSession(
|
||||||
"/ContractSessionsService/Load",
|
|
||||||
jsonMiddleware(),
|
|
||||||
async (req: RequestWithJwt<never, LoadSaveBody>, res) => {
|
|
||||||
const userData = getUserData(req.jwt.unique_name, req.gameVersion)
|
|
||||||
|
|
||||||
if (
|
|
||||||
!req.body.contractSessionId ||
|
|
||||||
!req.body.saveToken ||
|
|
||||||
!req.body.contractId
|
|
||||||
) {
|
|
||||||
res.status(400).send("bad body")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await loadSession(
|
|
||||||
req.body.contractSessionId,
|
|
||||||
req.body.saveToken,
|
|
||||||
userData,
|
|
||||||
)
|
|
||||||
} catch (e) {
|
|
||||||
log(
|
|
||||||
LogLevel.DEBUG,
|
|
||||||
`Failed to load contract with token = ${req.body.saveToken}, session id = ${req.body.contractSessionId} because ${e.message}`,
|
|
||||||
)
|
|
||||||
log(
|
|
||||||
LogLevel.WARN,
|
|
||||||
"No such save detected! Might be an official servers save.",
|
|
||||||
)
|
|
||||||
|
|
||||||
if (PEACOCK_DEV) {
|
|
||||||
log(
|
|
||||||
LogLevel.DEBUG,
|
|
||||||
`(Save-context: ${req.body.contractSessionId}; ${req.body.saveToken})`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
log(
|
|
||||||
LogLevel.WARN,
|
|
||||||
"Creating a fake session to avoid problems... scoring will not work!",
|
|
||||||
)
|
|
||||||
|
|
||||||
newSession(
|
|
||||||
req.body.contractSessionId,
|
|
||||||
req.body.contractId,
|
|
||||||
req.jwt.unique_name,
|
|
||||||
req.body.difficultyLevel!,
|
|
||||||
req.gameVersion,
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
res.send(`"${req.body.contractSessionId}"`)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
async function loadSession(
|
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
token: string,
|
token: string,
|
||||||
userData: UserProfile,
|
userData: UserProfile,
|
||||||
@ -831,6 +854,8 @@ async function loadSession(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assert.ok(sessionData, "should have session data")
|
||||||
|
|
||||||
// Update challenge progression with the user's latest progression data
|
// Update challenge progression with the user's latest progression data
|
||||||
for (const cid in sessionData.challengeContexts) {
|
for (const cid in sessionData.challengeContexts) {
|
||||||
// Make sure the ChallengeProgression is available, otherwise loading might fail!
|
// Make sure the ChallengeProgression is available, otherwise loading might fail!
|
||||||
@ -846,6 +871,11 @@ async function loadSession(
|
|||||||
sessionData.gameVersion,
|
sessionData.gameVersion,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
challenge,
|
||||||
|
`session has context for unregistered challenge ${cid}`,
|
||||||
|
)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!userData.Extensions.ChallengeProgression[cid].Completed &&
|
!userData.Extensions.ChallengeProgression[cid].Completed &&
|
||||||
controller.challengeService.needSaveProgression(challenge)
|
controller.challengeService.needSaveProgression(challenge)
|
||||||
@ -859,22 +889,9 @@ async function loadSession(
|
|||||||
log(
|
log(
|
||||||
LogLevel.DEBUG,
|
LogLevel.DEBUG,
|
||||||
`Loaded contract with token = ${token}, session id = ${sessionId}, start time = ${
|
`Loaded contract with token = ${token}, session id = ${sessionId}, start time = ${
|
||||||
contractSessions.get(sessionId).timerStart
|
contractSessions.get(sessionId)!.timerStart
|
||||||
}.`,
|
}.`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
profileRouter.post(
|
|
||||||
"/ProfileService/GetSemLinkStatus",
|
|
||||||
jsonMiddleware(),
|
|
||||||
(req, res) => {
|
|
||||||
res.json({
|
|
||||||
IsConfirmed: true,
|
|
||||||
LinkedEmail: "mail@example.com",
|
|
||||||
IOIAccountId: nilUuid,
|
|
||||||
IOIAccountBaseUrl: "https://account.ioi.dk",
|
|
||||||
})
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
export { profileRouter }
|
export { profileRouter }
|
||||||
|
@ -48,7 +48,7 @@ import {
|
|||||||
getLevelCount,
|
getLevelCount,
|
||||||
} from "./contracts/escalations/escalationService"
|
} from "./contracts/escalations/escalationService"
|
||||||
import { getUserData, writeUserData } from "./databaseHandler"
|
import { getUserData, writeUserData } from "./databaseHandler"
|
||||||
import axios from "axios"
|
import axios, { AxiosError } from "axios"
|
||||||
import { getFlag } from "./flags"
|
import { getFlag } from "./flags"
|
||||||
import { log, LogLevel } from "./loggingInterop"
|
import { log, LogLevel } from "./loggingInterop"
|
||||||
import {
|
import {
|
||||||
@ -64,6 +64,7 @@ import {
|
|||||||
CalculateScoreResult,
|
CalculateScoreResult,
|
||||||
CalculateSniperScoreResult,
|
CalculateSniperScoreResult,
|
||||||
CalculateXpResult,
|
CalculateXpResult,
|
||||||
|
ContractScore,
|
||||||
MissionEndChallenge,
|
MissionEndChallenge,
|
||||||
MissionEndDrop,
|
MissionEndDrop,
|
||||||
MissionEndEvergreen,
|
MissionEndEvergreen,
|
||||||
@ -72,6 +73,7 @@ import {
|
|||||||
import { MasteryData } from "./types/mastery"
|
import { MasteryData } from "./types/mastery"
|
||||||
import { createInventory, InventoryItem, getUnlockablesById } from "./inventory"
|
import { createInventory, InventoryItem, getUnlockablesById } from "./inventory"
|
||||||
import { calculatePlaystyle } from "./playStyles"
|
import { calculatePlaystyle } from "./playStyles"
|
||||||
|
import assert from "assert"
|
||||||
|
|
||||||
export function calculateGlobalXp(
|
export function calculateGlobalXp(
|
||||||
contractSession: ContractSession,
|
contractSession: ContractSession,
|
||||||
@ -81,8 +83,10 @@ export function calculateGlobalXp(
|
|||||||
let totalXp = 0
|
let totalXp = 0
|
||||||
|
|
||||||
// TODO: Merge with the non-global challenges?
|
// TODO: Merge with the non-global challenges?
|
||||||
for (const challengeId of Object.keys(contractSession.challengeContexts)) {
|
for (const challengeId of Object.keys(
|
||||||
const data = contractSession.challengeContexts[challengeId]
|
contractSession.challengeContexts || {},
|
||||||
|
)) {
|
||||||
|
const data = contractSession.challengeContexts![challengeId]
|
||||||
|
|
||||||
if (data.timesCompleted <= 0) {
|
if (data.timesCompleted <= 0) {
|
||||||
continue
|
continue
|
||||||
@ -93,7 +97,7 @@ export function calculateGlobalXp(
|
|||||||
gameVersion,
|
gameVersion,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!challenge || !challenge.Xp || !challenge.Tags.includes("global")) {
|
if (!challenge?.Xp || !challenge.Tags.includes("global")) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -181,7 +185,7 @@ export function calculateScore(
|
|||||||
headline: "UI_SCORING_SUMMARY_NO_BODIES_FOUND",
|
headline: "UI_SCORING_SUMMARY_NO_BODIES_FOUND",
|
||||||
bonusId: "NoBodiesFound",
|
bonusId: "NoBodiesFound",
|
||||||
condition:
|
condition:
|
||||||
contractSession.legacyHasBodyBeenFound === false &&
|
!contractSession.legacyHasBodyBeenFound &&
|
||||||
[...contractSession.bodiesFoundBy].every(
|
[...contractSession.bodiesFoundBy].every(
|
||||||
(witness) =>
|
(witness) =>
|
||||||
(gameVersion === "h1"
|
(gameVersion === "h1"
|
||||||
@ -386,7 +390,8 @@ export function calculateSniperScore(
|
|||||||
[480, 35000], // 35000 bonus score at 480 secs (8 min)
|
[480, 35000], // 35000 bonus score at 480 secs (8 min)
|
||||||
[900, 0], // 0 bonus score at 900 secs (15 min)
|
[900, 0], // 0 bonus score at 900 secs (15 min)
|
||||||
]
|
]
|
||||||
let prevsecs: number, prevscore: number
|
let prevsecs: number = 0
|
||||||
|
let prevscore: number = 0
|
||||||
|
|
||||||
for (const [secs, score] of scorePoints) {
|
for (const [secs, score] of scorePoints) {
|
||||||
if (bonusTimeTotal > secs) {
|
if (bonusTimeTotal > secs) {
|
||||||
@ -414,36 +419,46 @@ export function calculateSniperScore(
|
|||||||
scoreTotal: 0,
|
scoreTotal: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseScore = contractSession.scoring.Context["TotalScore"]
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const challengeMultiplier = contractSession.scoring.Settings["challenges"][
|
const baseScore = (contractSession.scoring?.Context as any)["TotalScore"]
|
||||||
|
// @ts-expect-error it's a number
|
||||||
|
const challengeMultiplier = contractSession.scoring?.Settings["challenges"][
|
||||||
"Unlockables"
|
"Unlockables"
|
||||||
].reduce((acc, unlockable) => {
|
].reduce((acc, unlockable) => {
|
||||||
const item = inventory.find((item) => item.Unlockable.Id === unlockable)
|
const item = inventory.find((item) => item.Unlockable.Id === unlockable)
|
||||||
|
|
||||||
if (item) {
|
if (item) {
|
||||||
|
// @ts-expect-error it's a number
|
||||||
return acc + item.Unlockable.Properties["Multiplier"]
|
return acc + item.Unlockable.Properties["Multiplier"]
|
||||||
}
|
}
|
||||||
|
|
||||||
return acc
|
return acc
|
||||||
}, 1.0)
|
}, 1.0)
|
||||||
|
|
||||||
|
assert(
|
||||||
|
typeof challengeMultiplier === "number",
|
||||||
|
"challengeMultiplier is falsey/NaN",
|
||||||
|
)
|
||||||
|
|
||||||
const bulletsMissed = 0 // TODO? not sure if neccessary, the penalty is always 0 for inbuilt contracts
|
const bulletsMissed = 0 // TODO? not sure if neccessary, the penalty is always 0 for inbuilt contracts
|
||||||
const bulletsMissedPenalty =
|
const bulletsMissedPenalty =
|
||||||
bulletsMissed *
|
bulletsMissed *
|
||||||
contractSession.scoring.Settings["bulletsused"]["penalty"]
|
(contractSession.scoring?.Settings["bulletsused"]["penalty"] || 0)
|
||||||
// Get SA status from global SA challenge for contracttype sniper
|
// Get SA status from global SA challenge for contracttype sniper
|
||||||
const silentAssassin =
|
const silentAssassin =
|
||||||
contractSession.challengeContexts[
|
contractSession.challengeContexts?.[
|
||||||
"029c4971-0ddd-47ab-a568-17b007eec04e"
|
"029c4971-0ddd-47ab-a568-17b007eec04e"
|
||||||
].state !== "Failure"
|
].state !== "Failure"
|
||||||
const saBonus = silentAssassin
|
const saBonus = silentAssassin
|
||||||
? contractSession.scoring.Settings["silentassassin"]["score"]
|
? contractSession.scoring?.Settings["silentassassin"]["score"]
|
||||||
: 0
|
: 0
|
||||||
const saMultiplier = silentAssassin
|
const saMultiplier = silentAssassin
|
||||||
? contractSession.scoring.Settings["silentassassin"]["multiplier"]
|
? contractSession.scoring?.Settings["silentassassin"]["multiplier"]
|
||||||
: 1.0
|
: 1.0
|
||||||
|
|
||||||
const subTotalScore = baseScore + timeBonus + saBonus - bulletsMissedPenalty
|
const subTotalScore = baseScore + timeBonus + saBonus - bulletsMissedPenalty
|
||||||
const totalScore = Math.round(
|
const totalScore = Math.round(
|
||||||
|
// @ts-expect-error it's a number
|
||||||
subTotalScore * challengeMultiplier * saMultiplier,
|
subTotalScore * challengeMultiplier * saMultiplier,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -518,7 +533,7 @@ export async function getMissionEndData(
|
|||||||
gameVersion: GameVersion,
|
gameVersion: GameVersion,
|
||||||
): Promise<MissionEndError | MissionEndResult> {
|
): Promise<MissionEndError | MissionEndResult> {
|
||||||
// TODO: For this entire function, add support for 2016 difficulties
|
// TODO: For this entire function, add support for 2016 difficulties
|
||||||
const sessionDetails = contractSessions.get(query.contractSessionId)
|
const sessionDetails = contractSessions.get(query.contractSessionId || "")
|
||||||
|
|
||||||
if (!sessionDetails) {
|
if (!sessionDetails) {
|
||||||
return {
|
return {
|
||||||
@ -625,6 +640,8 @@ export async function getMissionEndData(
|
|||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
assert.ok(levelData, "contract not found")
|
||||||
|
|
||||||
// Resolve the id of the parent location
|
// Resolve the id of the parent location
|
||||||
const subLocation = getSubLocationByName(
|
const subLocation = getSubLocationByName(
|
||||||
levelData.Metadata.Location,
|
levelData.Metadata.Location,
|
||||||
@ -777,9 +794,9 @@ export async function getMissionEndData(
|
|||||||
if (masteryData) {
|
if (masteryData) {
|
||||||
maxLevel =
|
maxLevel =
|
||||||
(query.masteryUnlockableId
|
(query.masteryUnlockableId
|
||||||
? masteryData.SubPackages.find(
|
? masteryData.SubPackages?.find(
|
||||||
(subPkg) => subPkg.Id === query.masteryUnlockableId,
|
(subPkg) => subPkg.Id === query.masteryUnlockableId,
|
||||||
).MaxLevel
|
)?.MaxLevel
|
||||||
: masteryData.MaxLevel) || DEFAULT_MASTERY_MAXLEVEL
|
: masteryData.MaxLevel) || DEFAULT_MASTERY_MAXLEVEL
|
||||||
|
|
||||||
locationLevelInfo = Array.from({ length: maxLevel }, (_, i) => {
|
locationLevelInfo = Array.from({ length: maxLevel }, (_, i) => {
|
||||||
@ -828,7 +845,8 @@ export async function getMissionEndData(
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Evergreen
|
// Evergreen
|
||||||
const evergreenData: MissionEndEvergreen = <MissionEndEvergreen>{
|
const evergreenData: MissionEndEvergreen = {
|
||||||
|
Payout: 0,
|
||||||
PayoutsCompleted: [],
|
PayoutsCompleted: [],
|
||||||
PayoutsFailed: [],
|
PayoutsFailed: [],
|
||||||
}
|
}
|
||||||
@ -846,14 +864,14 @@ export async function getMissionEndData(
|
|||||||
Object.keys(gameChangerProperties).forEach((e) => {
|
Object.keys(gameChangerProperties).forEach((e) => {
|
||||||
const gameChanger = gameChangerProperties[e]
|
const gameChanger = gameChangerProperties[e]
|
||||||
|
|
||||||
const conditionObjective = gameChanger.Objectives.find(
|
const conditionObjective = gameChanger.Objectives?.find(
|
||||||
(e) => e.Category === "condition",
|
(e) => e.Category === "condition",
|
||||||
)
|
)
|
||||||
|
|
||||||
const secondaryObjective = gameChanger.Objectives.find(
|
const secondaryObjective = gameChanger.Objectives?.find(
|
||||||
(e) =>
|
(e) =>
|
||||||
e.Category === "secondary" &&
|
e.Category === "secondary" &&
|
||||||
e.Definition.Context["MyPayout"],
|
e.Definition?.Context?.["MyPayout"],
|
||||||
)
|
)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -862,18 +880,20 @@ export async function getMissionEndData(
|
|||||||
sessionDetails.objectiveStates.get(conditionObjective.Id) ===
|
sessionDetails.objectiveStates.get(conditionObjective.Id) ===
|
||||||
"Success"
|
"Success"
|
||||||
) {
|
) {
|
||||||
|
type P = { MyPayout: string }
|
||||||
|
|
||||||
|
const context = sessionDetails.objectiveContexts.get(
|
||||||
|
secondaryObjective.Id,
|
||||||
|
) as P | undefined
|
||||||
|
|
||||||
const payoutObjective = {
|
const payoutObjective = {
|
||||||
Name: gameChanger.Name,
|
Name: gameChanger.Name,
|
||||||
Payout: parseInt(
|
Payout: parseInt(context?.["MyPayout"] || "0"),
|
||||||
sessionDetails.objectiveContexts.get(
|
|
||||||
secondaryObjective.Id,
|
|
||||||
)["MyPayout"] || 0,
|
|
||||||
),
|
|
||||||
IsPrestige: gameChanger.IsPrestigeObjective || false,
|
IsPrestige: gameChanger.IsPrestigeObjective || false,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!sessionDetails.evergreen.failed &&
|
!sessionDetails.evergreen?.failed &&
|
||||||
sessionDetails.objectiveStates.get(
|
sessionDetails.objectiveStates.get(
|
||||||
secondaryObjective.Id,
|
secondaryObjective.Id,
|
||||||
) === "Success"
|
) === "Success"
|
||||||
@ -888,7 +908,7 @@ export async function getMissionEndData(
|
|||||||
|
|
||||||
evergreenData.Payout = totalPayout
|
evergreenData.Payout = totalPayout
|
||||||
evergreenData.EndStateEventName =
|
evergreenData.EndStateEventName =
|
||||||
sessionDetails.evergreen.scoringScreenEndState
|
sessionDetails.evergreen?.scoringScreenEndState
|
||||||
|
|
||||||
locationLevelInfo = EVERGREEN_LEVEL_INFO
|
locationLevelInfo = EVERGREEN_LEVEL_INFO
|
||||||
|
|
||||||
@ -909,14 +929,14 @@ export async function getMissionEndData(
|
|||||||
calculateScoreResult.silentAssassin = false
|
calculateScoreResult.silentAssassin = false
|
||||||
|
|
||||||
// Overide the calculated score
|
// Overide the calculated score
|
||||||
calculateScoreResult.stars = undefined
|
calculateScoreResult.stars = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sniper
|
// Sniper
|
||||||
let unlockableProgression = undefined
|
let unlockableProgression = undefined
|
||||||
let sniperChallengeScore = undefined
|
let sniperChallengeScore: CalculateSniperScoreResult | undefined = undefined
|
||||||
|
|
||||||
let contractScore = {
|
let contractScore: ContractScore | undefined = {
|
||||||
Total: calculateScoreResult.scoreWithBonus,
|
Total: calculateScoreResult.scoreWithBonus,
|
||||||
AchievedMasteries: calculateScoreResult.achievedMasteries,
|
AchievedMasteries: calculateScoreResult.achievedMasteries,
|
||||||
AwardedBonuses: calculateScoreResult.awardedBonuses,
|
AwardedBonuses: calculateScoreResult.awardedBonuses,
|
||||||
@ -969,7 +989,7 @@ export async function getMissionEndData(
|
|||||||
Id: completionData.Id,
|
Id: completionData.Id,
|
||||||
Level: completionData.Level,
|
Level: completionData.Level,
|
||||||
LevelInfo: locationLevelInfo,
|
LevelInfo: locationLevelInfo,
|
||||||
Name: completionData.Name,
|
Name: completionData.Name!,
|
||||||
XP: completionData.XP,
|
XP: completionData.XP,
|
||||||
XPGain:
|
XPGain:
|
||||||
completionData.Level === completionData.MaxLevel
|
completionData.Level === completionData.MaxLevel
|
||||||
@ -977,8 +997,9 @@ export async function getMissionEndData(
|
|||||||
: sniperScore.FinalScore,
|
: sniperScore.FinalScore,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @ts-expect-error should be fine (allegedly)
|
||||||
userData.Extensions.progression.Locations[locationParentId][
|
userData.Extensions.progression.Locations[locationParentId][
|
||||||
query.masteryUnlockableId
|
query.masteryUnlockableId!
|
||||||
].PreviouslySeenXp = completionData.XP
|
].PreviouslySeenXp = completionData.XP
|
||||||
|
|
||||||
writeUserData(jwt.unique_name, gameVersion)
|
writeUserData(jwt.unique_name, gameVersion)
|
||||||
@ -996,7 +1017,7 @@ export async function getMissionEndData(
|
|||||||
// Override the playstyle
|
// Override the playstyle
|
||||||
playstyle = undefined
|
playstyle = undefined
|
||||||
|
|
||||||
calculateScoreResult.stars = undefined
|
calculateScoreResult.stars = 0
|
||||||
calculateScoreResult.scoringHeadlines = headlines
|
calculateScoreResult.scoringHeadlines = headlines
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1009,7 +1030,7 @@ export async function getMissionEndData(
|
|||||||
const masteryData =
|
const masteryData =
|
||||||
controller.masteryService.getMasteryDataForSubPackage(
|
controller.masteryService.getMasteryDataForSubPackage(
|
||||||
locationParentId,
|
locationParentId,
|
||||||
query.masteryUnlockableId ?? undefined,
|
query.masteryUnlockableId!,
|
||||||
gameVersion,
|
gameVersion,
|
||||||
jwt.unique_name,
|
jwt.unique_name,
|
||||||
) as MasteryData
|
) as MasteryData
|
||||||
@ -1018,31 +1039,39 @@ export async function getMissionEndData(
|
|||||||
masteryDrops = masteryData.Drops.filter(
|
masteryDrops = masteryData.Drops.filter(
|
||||||
(e) =>
|
(e) =>
|
||||||
e.Level > oldLocationLevel && e.Level <= newLocationLevel,
|
e.Level > oldLocationLevel && e.Level <= newLocationLevel,
|
||||||
).map((e) => {
|
).map((e) => ({
|
||||||
return {
|
Unlockable: e.Unlockable,
|
||||||
Unlockable: e.Unlockable,
|
}))
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Challenge Drops
|
// Challenge Drops
|
||||||
const challengeDrops: MissionEndDrop[] =
|
const challengeDrops: MissionEndDrop[] =
|
||||||
calculateXpResult.completedChallenges.reduce((acc, challenge) => {
|
calculateXpResult.completedChallenges.reduce(
|
||||||
if (challenge?.Drops?.length) {
|
(acc: MissionEndDrop[], challenge) => {
|
||||||
const drops = getUnlockablesById(challenge.Drops, gameVersion)
|
if (challenge?.Drops?.length) {
|
||||||
delete challenge.Drops
|
const drops = getUnlockablesById(
|
||||||
|
challenge.Drops,
|
||||||
|
gameVersion,
|
||||||
|
)
|
||||||
|
delete challenge.Drops
|
||||||
|
|
||||||
for (const drop of drops) {
|
for (const drop of drops) {
|
||||||
acc.push({
|
if (!drop) {
|
||||||
Unlockable: drop,
|
continue
|
||||||
SourceChallenge: challenge,
|
}
|
||||||
})
|
|
||||||
|
acc.push({
|
||||||
|
Unlockable: drop,
|
||||||
|
SourceChallenge: challenge,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return acc
|
return acc
|
||||||
}, [])
|
},
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
|
||||||
// Setup the result
|
// Setup the result
|
||||||
const result: MissionEndResult = {
|
const result: MissionEndResult = {
|
||||||
@ -1094,7 +1123,7 @@ export async function getMissionEndData(
|
|||||||
SniperChallengeScore: sniperChallengeScore,
|
SniperChallengeScore: sniperChallengeScore,
|
||||||
SilentAssassin:
|
SilentAssassin:
|
||||||
contractScore?.SilentAssassin ||
|
contractScore?.SilentAssassin ||
|
||||||
sniperChallengeScore?.silentAssassin ||
|
sniperChallengeScore?.SilentAssassin ||
|
||||||
false,
|
false,
|
||||||
// TODO: Use data from the leaderboard?
|
// TODO: Use data from the leaderboard?
|
||||||
NewRank: 1,
|
NewRank: 1,
|
||||||
@ -1112,7 +1141,7 @@ export async function getMissionEndData(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Finalize the response
|
// Finalize the response
|
||||||
if ((getFlag("autoSplitterForceSilentAssassin") as boolean) === true) {
|
if (getFlag("autoSplitterForceSilentAssassin")) {
|
||||||
if (result.ScoreOverview.SilentAssassin) {
|
if (result.ScoreOverview.SilentAssassin) {
|
||||||
await liveSplitManager.completeMission(timeTotal)
|
await liveSplitManager.completeMission(timeTotal)
|
||||||
} else {
|
} else {
|
||||||
@ -1124,7 +1153,7 @@ export async function getMissionEndData(
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
getFlag("leaderboards") === true &&
|
getFlag("leaderboards") === true &&
|
||||||
sessionDetails.compat === true &&
|
sessionDetails.compat &&
|
||||||
contractData.Metadata.Type !== "vsrace" &&
|
contractData.Metadata.Type !== "vsrace" &&
|
||||||
contractData.Metadata.Type !== "evergreen" &&
|
contractData.Metadata.Type !== "evergreen" &&
|
||||||
// Disable sending sniper scores for now
|
// Disable sending sniper scores for now
|
||||||
@ -1187,7 +1216,7 @@ export async function getMissionEndData(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
handleAxiosError(e)
|
handleAxiosError(e as AxiosError)
|
||||||
log(
|
log(
|
||||||
LogLevel.WARN,
|
LogLevel.WARN,
|
||||||
"Failed to commit leaderboards data! Either you or the server may be offline.",
|
"Failed to commit leaderboards data! Either you or the server may be offline.",
|
||||||
|
@ -120,7 +120,9 @@ export class SMFSupport {
|
|||||||
const id = contractData.Metadata.Id
|
const id = contractData.Metadata.Id
|
||||||
const placeBefore = contractData.SMF?.destinations.placeBefore
|
const placeBefore = contractData.SMF?.destinations.placeBefore
|
||||||
const placeAfter = contractData.SMF?.destinations.placeAfter
|
const placeAfter = contractData.SMF?.destinations.placeAfter
|
||||||
|
// @ts-expect-error I know what I'm doing.
|
||||||
const inLocation = (this.controller.missionsInLocations[location] ??
|
const inLocation = (this.controller.missionsInLocations[location] ??
|
||||||
|
// @ts-expect-error I know what I'm doing.
|
||||||
(this.controller.missionsInLocations[location] = [])) as string[]
|
(this.controller.missionsInLocations[location] = [])) as string[]
|
||||||
|
|
||||||
if (placeBefore) {
|
if (placeBefore) {
|
||||||
|
@ -165,7 +165,6 @@ export function parseContextListeners(
|
|||||||
info.challengeCountData.total = test(total, context)
|
info.challengeCountData.total = test(total, context)
|
||||||
|
|
||||||
// Might be counting finished challenges, so need required challenges list. e.g. (SA5, SA12, SA17)
|
// Might be counting finished challenges, so need required challenges list. e.g. (SA5, SA12, SA17)
|
||||||
// todo: maybe not hard-code this?
|
|
||||||
if ((count as string).includes("CompletedChallenges")) {
|
if ((count as string).includes("CompletedChallenges")) {
|
||||||
info.challengeTreeIds.push(
|
info.challengeTreeIds.push(
|
||||||
...test("$.RequiredChallenges", context),
|
...test("$.RequiredChallenges", context),
|
||||||
|
@ -42,7 +42,9 @@ export interface IContractCreationPayload {
|
|||||||
* The target creator API.
|
* The target creator API.
|
||||||
*/
|
*/
|
||||||
export class TargetCreator {
|
export class TargetCreator {
|
||||||
|
// @ts-expect-error TODO: type this
|
||||||
private _targetSm
|
private _targetSm
|
||||||
|
// @ts-expect-error TODO: type this
|
||||||
private _outfitSm
|
private _outfitSm
|
||||||
private _targetConds: undefined | unknown[] = undefined
|
private _targetConds: undefined | unknown[] = undefined
|
||||||
|
|
||||||
|
@ -23,6 +23,7 @@ import {
|
|||||||
InclusionData,
|
InclusionData,
|
||||||
MissionManifestObjective,
|
MissionManifestObjective,
|
||||||
} from "./types"
|
} from "./types"
|
||||||
|
import { gameDifficulty } from "../utils"
|
||||||
|
|
||||||
export interface SavedChallenge {
|
export interface SavedChallenge {
|
||||||
Id: string
|
Id: string
|
||||||
@ -45,7 +46,7 @@ export interface SavedChallenge {
|
|||||||
RuntimeType: "Hit" | string
|
RuntimeType: "Hit" | string
|
||||||
Xp: number
|
Xp: number
|
||||||
XpModifier?: unknown
|
XpModifier?: unknown
|
||||||
DifficultyLevels: string[]
|
DifficultyLevels: (keyof typeof gameDifficulty)[]
|
||||||
Definition: MissionManifestObjective["Definition"] & {
|
Definition: MissionManifestObjective["Definition"] & {
|
||||||
Scope: ContextScopedStorageLocation
|
Scope: ContextScopedStorageLocation
|
||||||
Repeatable?: {
|
Repeatable?: {
|
||||||
@ -93,7 +94,7 @@ export type ProfileChallengeData = {
|
|||||||
|
|
||||||
export type ChallengeContext = {
|
export type ChallengeContext = {
|
||||||
context: unknown
|
context: unknown
|
||||||
state: string
|
state: string | undefined
|
||||||
timers: Timer[]
|
timers: Timer[]
|
||||||
timesCompleted: number
|
timesCompleted: number
|
||||||
}
|
}
|
||||||
|
@ -257,3 +257,7 @@ export type Dart_HitC2SEvent = ClientToServerEvent<{
|
|||||||
export type Evergreen_Payout_DataC2SEvent = ClientToServerEvent<{
|
export type Evergreen_Payout_DataC2SEvent = ClientToServerEvent<{
|
||||||
Total_Payout: number
|
Total_Payout: number
|
||||||
}>
|
}>
|
||||||
|
|
||||||
|
export type OpponentsC2sEvent = ClientToServerEvent<{
|
||||||
|
ConnectedSessions: string[]
|
||||||
|
}>
|
||||||
|
@ -32,7 +32,7 @@ export type StashpointSlotName =
|
|||||||
| string
|
| string
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query that the game sends for the stashpoint route.
|
* Query for `/profiles/page/stashpoint`.
|
||||||
*/
|
*/
|
||||||
export type StashpointQuery = Partial<{
|
export type StashpointQuery = Partial<{
|
||||||
contractid: string
|
contractid: string
|
||||||
@ -47,7 +47,7 @@ export type StashpointQuery = Partial<{
|
|||||||
}>
|
}>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query that the game sends for the stashpoint route in H2016.
|
* Query for `/profiles/page/stashpoint` (H2016 ONLY).
|
||||||
*
|
*
|
||||||
* @see StashpointQuery
|
* @see StashpointQuery
|
||||||
*/
|
*/
|
||||||
@ -94,8 +94,7 @@ export type GetCompletionDataForLocationQuery = Partial<{
|
|||||||
}>
|
}>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Body that the game sends for the
|
* Body for `/authentication/api/userchannel/ContractSessionsService/Load`.
|
||||||
* `/authentication/api/userchannel/ContractSessionsService/Load` route.
|
|
||||||
*/
|
*/
|
||||||
export type LoadSaveBody = Partial<{
|
export type LoadSaveBody = Partial<{
|
||||||
saveToken: string
|
saveToken: string
|
||||||
@ -106,7 +105,7 @@ export type LoadSaveBody = Partial<{
|
|||||||
}>
|
}>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query params that `/profiles/page/Safehouse` gets.
|
* Query for `/profiles/page/Safehouse`.
|
||||||
* Roughly the same as {@link SafehouseCategoryQuery} but this route is only for H1.
|
* Roughly the same as {@link SafehouseCategoryQuery} but this route is only for H1.
|
||||||
*/
|
*/
|
||||||
export type SafehouseQuery = {
|
export type SafehouseQuery = {
|
||||||
@ -114,7 +113,7 @@ export type SafehouseQuery = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query params that `/profiles/page/SafehouseCategory` (used for Career > Inventory and possibly some of the H1 stuff) gets.
|
* Query for `/profiles/page/SafehouseCategory` (used for Career > Inventory and possibly some of the H1 stuff).
|
||||||
*/
|
*/
|
||||||
export type SafehouseCategoryQuery = {
|
export type SafehouseCategoryQuery = {
|
||||||
type?: string
|
type?: string
|
||||||
@ -122,7 +121,7 @@ export type SafehouseCategoryQuery = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query params that `/profiles/page/Destination` gets.
|
* Query for `/profiles/page/Destination`.
|
||||||
*/
|
*/
|
||||||
export type GetDestinationQuery = {
|
export type GetDestinationQuery = {
|
||||||
locationId: string
|
locationId: string
|
||||||
@ -138,7 +137,7 @@ export type LeaderboardEntriesCommonQuery = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query params that `/profiles/page/DebriefingLeaderboards` gets.
|
* Query for `/profiles/page/DebriefingLeaderboards`.
|
||||||
* Because ofc it's different. Thanks IOI.
|
* Because ofc it's different. Thanks IOI.
|
||||||
*/
|
*/
|
||||||
export type DebriefingLeaderboardsQuery = {
|
export type DebriefingLeaderboardsQuery = {
|
||||||
@ -147,8 +146,37 @@ export type DebriefingLeaderboardsQuery = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query params that `/profiles/page/ChallengeLocation` gets.
|
* Query for `/profiles/page/ChallengeLocation`.
|
||||||
*/
|
*/
|
||||||
export type ChallengeLocationQuery = {
|
export type ChallengeLocationQuery = {
|
||||||
locationId: string
|
locationId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Body for `/authentication/api/userchannel/ReportingService/ReportContract`.
|
||||||
|
*/
|
||||||
|
export type ContractReportBody = {
|
||||||
|
contractId: string
|
||||||
|
reason: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query for `/profiles/page/LookupContractPublicId`.
|
||||||
|
*/
|
||||||
|
export type LookupContractPublicIdQuery = {
|
||||||
|
publicid: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Body for `/authentication/api/userchannel/ProfileService/ResolveGamerTags`.
|
||||||
|
*/
|
||||||
|
export type ResolveGamerTagsBody = {
|
||||||
|
profileIds: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query for `/profiles/page/GetMasteryCompletionDataForUnlockable`.
|
||||||
|
*/
|
||||||
|
export type GetMasteryCompletionDataForUnlockableQuery = {
|
||||||
|
unlockableId: string
|
||||||
|
}
|
||||||
|
@ -18,12 +18,9 @@
|
|||||||
|
|
||||||
import { CompletionData, GameVersion, Unlockable } from "./types"
|
import { CompletionData, GameVersion, Unlockable } from "./types"
|
||||||
|
|
||||||
export interface MasteryDataTemplate {
|
export interface LocationMasteryData {
|
||||||
template: unknown
|
Location: Unlockable
|
||||||
data: {
|
MasteryData: MasteryData[]
|
||||||
Location: Unlockable
|
|
||||||
MasteryData: MasteryData[]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MasteryPackageDrop {
|
export interface MasteryPackageDrop {
|
||||||
@ -41,19 +38,27 @@ interface MasterySubPackage {
|
|||||||
* @since v7.0.0
|
* @since v7.0.0
|
||||||
* The Id field has been renamed to LocationId to properly reflect what it is.
|
* The Id field has been renamed to LocationId to properly reflect what it is.
|
||||||
*
|
*
|
||||||
* Mastery packages may have Drops OR SubPackages, never the two.
|
* Mastery packages may have Drops OR SubPackages, never both.
|
||||||
* This is to properly support sniper mastery by integrating it into the current system
|
* This is to properly support sniper mastery by integrating it into the current system
|
||||||
* and mastery on H2016 as it is separated by difficulty.
|
* and mastery on H2016 as it is separated by difficulty.
|
||||||
*
|
*
|
||||||
* Also, a GameVersions array has been added to support multi-version mastery.
|
* Also, a GameVersions array has been added to support multi-version mastery.
|
||||||
*/
|
*/
|
||||||
export interface MasteryPackage {
|
export type MasteryPackage = {
|
||||||
LocationId: string
|
LocationId: string
|
||||||
GameVersions: GameVersion[]
|
GameVersions: GameVersion[]
|
||||||
MaxLevel?: number
|
MaxLevel?: number
|
||||||
HideProgression?: boolean
|
HideProgression?: boolean
|
||||||
Drops?: MasteryPackageDrop[]
|
} & (HasDrop | HasSubPackage)
|
||||||
SubPackages?: MasterySubPackage[]
|
|
||||||
|
type HasDrop = {
|
||||||
|
Drops: MasteryPackageDrop[]
|
||||||
|
SubPackages?: never
|
||||||
|
}
|
||||||
|
|
||||||
|
type HasSubPackage = {
|
||||||
|
Drops?: never
|
||||||
|
SubPackages: MasterySubPackage[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MasteryData {
|
export interface MasteryData {
|
||||||
|
@ -25,12 +25,31 @@ import {
|
|||||||
Unlockable,
|
Unlockable,
|
||||||
} from "./types"
|
} from "./types"
|
||||||
|
|
||||||
export interface CalculateXpResult {
|
export type CalculateXpResult = {
|
||||||
completedChallenges: MissionEndChallenge[]
|
completedChallenges: MissionEndChallenge[]
|
||||||
xp: number
|
xp: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CalculateScoreResult {
|
export type ScoreProgressionStats = {
|
||||||
|
LevelInfo: number[]
|
||||||
|
XP: number
|
||||||
|
Level: number
|
||||||
|
XPGain: number
|
||||||
|
Id?: string
|
||||||
|
Name?: string
|
||||||
|
Completion?: number
|
||||||
|
HideProgression?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ScoreProfileProgressionStats = {
|
||||||
|
LevelInfo: number[]
|
||||||
|
LevelInfoOffset: number
|
||||||
|
XP: number
|
||||||
|
Level: number
|
||||||
|
XPGain: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CalculateScoreResult = {
|
||||||
stars: number
|
stars: number
|
||||||
scoringHeadlines: ScoringHeadline[]
|
scoringHeadlines: ScoringHeadline[]
|
||||||
awardedBonuses: ScoringBonus[]
|
awardedBonuses: ScoringBonus[]
|
||||||
@ -41,7 +60,7 @@ export interface CalculateScoreResult {
|
|||||||
scoreWithBonus: number
|
scoreWithBonus: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CalculateSniperScoreResult {
|
export type CalculateSniperScoreResult = {
|
||||||
FinalScore: number
|
FinalScore: number
|
||||||
BaseScore: number
|
BaseScore: number
|
||||||
TotalChallengeMultiplier: number
|
TotalChallengeMultiplier: number
|
||||||
@ -50,11 +69,11 @@ export interface CalculateSniperScoreResult {
|
|||||||
TimeTaken: number
|
TimeTaken: number
|
||||||
TimeBonus: number
|
TimeBonus: number
|
||||||
SilentAssassin: boolean
|
SilentAssassin: boolean
|
||||||
SilentAssassinBonus: number
|
SilentAssassinBonus: number | undefined
|
||||||
SilentAssassinMultiplier: number
|
SilentAssassinMultiplier: number | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MissionEndChallenge {
|
export type MissionEndChallenge = {
|
||||||
ChallengeId: string
|
ChallengeId: string
|
||||||
ChallengeTags: string[]
|
ChallengeTags: string[]
|
||||||
ChallengeName: string
|
ChallengeName: string
|
||||||
@ -66,7 +85,7 @@ export interface MissionEndChallenge {
|
|||||||
Drops?: string[]
|
Drops?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MissionEndSourceChallenge {
|
export type MissionEndSourceChallenge = {
|
||||||
ChallengeId: string
|
ChallengeId: string
|
||||||
ChallengeTags: string[]
|
ChallengeTags: string[]
|
||||||
ChallengeName: string
|
ChallengeName: string
|
||||||
@ -77,12 +96,12 @@ export interface MissionEndSourceChallenge {
|
|||||||
IsActionReward: boolean
|
IsActionReward: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MissionEndDrop {
|
export type MissionEndDrop = {
|
||||||
Unlockable: Unlockable
|
Unlockable: Unlockable
|
||||||
SourceChallenge?: MissionEndSourceChallenge
|
SourceChallenge?: MissionEndSourceChallenge
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MissionEndAchievedMastery {
|
export type MissionEndAchievedMastery = {
|
||||||
score: number
|
score: number
|
||||||
RatioParts: number
|
RatioParts: number
|
||||||
RatioTotal: number
|
RatioTotal: number
|
||||||
@ -90,47 +109,38 @@ export interface MissionEndAchievedMastery {
|
|||||||
BaseScore: number
|
BaseScore: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MissionEndEvergreen {
|
export type MissionEndEvergreen = {
|
||||||
Payout: number
|
Payout: number
|
||||||
EndStateEventName?: string
|
EndStateEventName?: string | null
|
||||||
PayoutsCompleted: MissionEndEvergreenPayout[]
|
PayoutsCompleted: MissionEndEvergreenPayout[]
|
||||||
PayoutsFailed: MissionEndEvergreenPayout[]
|
PayoutsFailed: MissionEndEvergreenPayout[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MissionEndEvergreenPayout {
|
export type MissionEndEvergreenPayout = {
|
||||||
Name: string
|
Name: string
|
||||||
Payout: number
|
Payout: number
|
||||||
IsPrestige: boolean
|
IsPrestige: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MissionEndResult {
|
export type ContractScore = {
|
||||||
|
Total: number
|
||||||
|
AchievedMasteries: MissionEndAchievedMastery[]
|
||||||
|
AwardedBonuses: ScoringBonus[]
|
||||||
|
TotalNoMultipliers: number
|
||||||
|
TimeUsedSecs: Seconds
|
||||||
|
StarCount: number
|
||||||
|
FailedBonuses: ScoringBonus[]
|
||||||
|
SilentAssassin: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MissionEndResult = {
|
||||||
MissionReward: {
|
MissionReward: {
|
||||||
LocationProgression: {
|
LocationProgression: ScoreProgressionStats
|
||||||
LevelInfo: number[]
|
ProfileProgression: ScoreProfileProgressionStats
|
||||||
XP: number
|
|
||||||
Level: number
|
|
||||||
Completion: number
|
|
||||||
XPGain: number
|
|
||||||
HideProgression: boolean
|
|
||||||
}
|
|
||||||
ProfileProgression: {
|
|
||||||
LevelInfo: number[]
|
|
||||||
LevelInfoOffset: number
|
|
||||||
XP: number
|
|
||||||
Level: number
|
|
||||||
XPGain: number
|
|
||||||
}
|
|
||||||
Challenges: MissionEndChallenge[]
|
Challenges: MissionEndChallenge[]
|
||||||
Drops: MissionEndDrop[]
|
Drops: MissionEndDrop[]
|
||||||
OpportunityRewards: unknown[] // ?
|
OpportunityRewards: unknown[] // ?
|
||||||
UnlockableProgression?: {
|
UnlockableProgression?: ScoreProgressionStats
|
||||||
LevelInfo: number[]
|
|
||||||
XP: number
|
|
||||||
Level: number
|
|
||||||
XPGain: number
|
|
||||||
Id: string
|
|
||||||
Name: string
|
|
||||||
}
|
|
||||||
CompletionData: CompletionData
|
CompletionData: CompletionData
|
||||||
ChallengeCompletion: ChallengeCompletion
|
ChallengeCompletion: ChallengeCompletion
|
||||||
ContractChallengeCompletion: ChallengeCompletion
|
ContractChallengeCompletion: ChallengeCompletion
|
||||||
@ -149,28 +159,8 @@ export interface MissionEndResult {
|
|||||||
ScoreDetails: {
|
ScoreDetails: {
|
||||||
Headlines: ScoringHeadline[]
|
Headlines: ScoringHeadline[]
|
||||||
}
|
}
|
||||||
ContractScore?: {
|
ContractScore?: ContractScore
|
||||||
Total: number
|
SniperChallengeScore?: CalculateSniperScoreResult
|
||||||
AchievedMasteries: MissionEndAchievedMastery[]
|
|
||||||
AwardedBonuses: ScoringBonus[]
|
|
||||||
TotalNoMultipliers: number
|
|
||||||
TimeUsedSecs: Seconds
|
|
||||||
StarCount: number
|
|
||||||
FailedBonuses: ScoringBonus[]
|
|
||||||
SilentAssassin: boolean
|
|
||||||
}
|
|
||||||
SniperChallengeScore?: {
|
|
||||||
FinalScore: number
|
|
||||||
BaseScore: number
|
|
||||||
TotalChallengeMultiplier: number
|
|
||||||
BulletsMissed: number
|
|
||||||
BulletsMissedPenalty: number
|
|
||||||
TimeTaken: number
|
|
||||||
TimeBonus: number
|
|
||||||
SilentAssassin: boolean
|
|
||||||
SilentAssassinBonus: number
|
|
||||||
SilentAssassinMultiplier: number
|
|
||||||
}
|
|
||||||
SilentAssassin: boolean
|
SilentAssassin: boolean
|
||||||
NewRank: number
|
NewRank: number
|
||||||
RankCount: number
|
RankCount: number
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
import type * as core from "express-serve-static-core"
|
import type * as core from "express-serve-static-core"
|
||||||
|
|
||||||
import type { IContractCreationPayload } from "../statemachines/contractCreation"
|
import type { IContractCreationPayload } from "../statemachines/contractCreation"
|
||||||
import type { Request } from "express"
|
import { Request } from "express"
|
||||||
import {
|
import {
|
||||||
ChallengeContext,
|
ChallengeContext,
|
||||||
ProfileChallengeData,
|
ProfileChallengeData,
|
||||||
@ -29,6 +29,7 @@ import { SessionGhostModeDetails } from "../multiplayer/multiplayerService"
|
|||||||
import { IContextListener } from "../statemachines/contextListeners"
|
import { IContextListener } from "../statemachines/contextListeners"
|
||||||
import { ManifestScoringModule, ScoringModule } from "./scoring"
|
import { ManifestScoringModule, ScoringModule } from "./scoring"
|
||||||
import { Timer } from "@peacockproject/statemachine-parser"
|
import { Timer } from "@peacockproject/statemachine-parser"
|
||||||
|
import { InventoryItem } from "../inventory"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A duration or relative point in time expressed in seconds.
|
* A duration or relative point in time expressed in seconds.
|
||||||
@ -274,7 +275,7 @@ export interface ContractSession {
|
|||||||
*/
|
*/
|
||||||
evergreen?: {
|
evergreen?: {
|
||||||
payout: number
|
payout: number
|
||||||
scoringScreenEndState: string
|
scoringScreenEndState: string | null
|
||||||
failed: boolean
|
failed: boolean
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
@ -367,7 +368,7 @@ export interface S2CEventWithTimestamp<EventValue = unknown> {
|
|||||||
* A server to client push message. The message component is encoded JSON.
|
* A server to client push message. The message component is encoded JSON.
|
||||||
*/
|
*/
|
||||||
export interface PushMessage {
|
export interface PushMessage {
|
||||||
time: number | string
|
time: number | string | bigint
|
||||||
message: string
|
message: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -405,33 +406,30 @@ export interface MissionStory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface PlayerProfileView {
|
export interface PlayerProfileView {
|
||||||
template: unknown
|
SubLocationData: {
|
||||||
data: {
|
ParentLocation: Unlockable
|
||||||
SubLocationData: {
|
Location: Unlockable
|
||||||
ParentLocation: Unlockable
|
CompletionData: CompletionData
|
||||||
Location: Unlockable
|
ChallengeCategoryCompletion: ChallengeCategoryCompletion[]
|
||||||
CompletionData: CompletionData
|
ChallengeCompletion: ChallengeCompletion
|
||||||
ChallengeCategoryCompletion: ChallengeCategoryCompletion[]
|
OpportunityStatistics: OpportunityStatistics
|
||||||
ChallengeCompletion: ChallengeCompletion
|
LocationCompletionPercent: number
|
||||||
OpportunityStatistics: OpportunityStatistics
|
}[]
|
||||||
LocationCompletionPercent: number
|
PlayerProfileXp: {
|
||||||
}[]
|
Total: number
|
||||||
PlayerProfileXp: {
|
Level: number
|
||||||
Total: number
|
Seasons: {
|
||||||
Level: number
|
Number: number
|
||||||
Seasons: {
|
Locations: {
|
||||||
Number: number
|
LocationId: string
|
||||||
Locations: {
|
Xp: number
|
||||||
LocationId: string
|
ActionXp: number
|
||||||
Xp: number
|
LocationProgression?: {
|
||||||
ActionXp: number
|
Level: number
|
||||||
LocationProgression?: {
|
MaxLevel: number
|
||||||
Level: number
|
}
|
||||||
MaxLevel: number
|
|
||||||
}
|
|
||||||
}[]
|
|
||||||
}[]
|
}[]
|
||||||
}
|
}[]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -878,7 +876,7 @@ export type ContractGroupDefinition = {
|
|||||||
Order: string[]
|
Order: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EscalationInfo {
|
export type EscalationInfo = {
|
||||||
Type?: MissionType
|
Type?: MissionType
|
||||||
InGroup?: string
|
InGroup?: string
|
||||||
NextContractId?: string
|
NextContractId?: string
|
||||||
@ -893,77 +891,76 @@ export interface EscalationInfo {
|
|||||||
export interface MissionManifestMetadata {
|
export interface MissionManifestMetadata {
|
||||||
Id: string
|
Id: string
|
||||||
Location: string
|
Location: string
|
||||||
IsPublished?: boolean
|
IsPublished?: boolean | null
|
||||||
CreationTimestamp?: string
|
CreationTimestamp?: string | null
|
||||||
CreatorUserId?: string
|
CreatorUserId?: string | null
|
||||||
Title: string
|
Title: string
|
||||||
Description?: string
|
Description?: string | null
|
||||||
BriefingVideo?:
|
BriefingVideo?:
|
||||||
| string
|
| string
|
||||||
| {
|
| {
|
||||||
Mode: string
|
Mode: string
|
||||||
VideoId: string
|
VideoId: string
|
||||||
}[]
|
}[]
|
||||||
DebriefingVideo?: string
|
DebriefingVideo?: string | null
|
||||||
TileImage?:
|
TileImage?:
|
||||||
| string
|
| string
|
||||||
| {
|
| {
|
||||||
Mode: string
|
Mode: string
|
||||||
Image: string
|
Image: string
|
||||||
}[]
|
}[]
|
||||||
CodeName_Hint?: string
|
CodeName_Hint?: string | null
|
||||||
ScenePath: string
|
ScenePath: string
|
||||||
Type: MissionType
|
Type: MissionType
|
||||||
Release?: string | object
|
Release?: string | object | null
|
||||||
RequiredUnlockable?: string
|
RequiredUnlockable?: string | null
|
||||||
Drops?: string[]
|
Drops?: string[] | null
|
||||||
Opportunities?: string[]
|
Opportunities?: string[] | null
|
||||||
OpportunityData?: MissionStory[]
|
OpportunityData?: MissionStory[] | null
|
||||||
Entitlements: string[]
|
Entitlements: string[] | null
|
||||||
LastUpdate?: string
|
LastUpdate?: string | null
|
||||||
PublicId?: string
|
PublicId?: string | null
|
||||||
GroupObjectiveDisplayOrder?: GroupObjectiveDisplayOrderItem[]
|
GroupObjectiveDisplayOrder?: GroupObjectiveDisplayOrderItem[] | null
|
||||||
GameVersion?: string
|
GameVersion?: string | null
|
||||||
ServerVersion?: string
|
ServerVersion?: string | null
|
||||||
AllowNonTargetKills?: boolean
|
AllowNonTargetKills?: boolean | null
|
||||||
Difficulty?: "pro1" | string
|
Difficulty?: "pro1" | string | null
|
||||||
CharacterSetup?: {
|
CharacterSetup?:
|
||||||
Mode: "singleplayer" | "multiplayer" | string
|
| {
|
||||||
Characters: {
|
Mode: "singleplayer" | "multiplayer" | string
|
||||||
Name: string
|
Characters: {
|
||||||
Id: string
|
Name: string
|
||||||
MandatoryLoadout?: string[]
|
Id: string
|
||||||
}[]
|
MandatoryLoadout?: string[]
|
||||||
}[]
|
}[]
|
||||||
CharacterLoadoutData?: {
|
}[]
|
||||||
Id: string
|
| null
|
||||||
Loadout: unknown
|
CharacterLoadoutData?:
|
||||||
CompletionData: CompletionData
|
| {
|
||||||
}[]
|
Id: string
|
||||||
SpawnSelectionType?: "random" | string
|
Loadout: unknown
|
||||||
Gamemodes?: ("versus" | string)[]
|
CompletionData: CompletionData
|
||||||
Enginemodes?: ("singleplayer" | "multiplayer" | string)[]
|
}[]
|
||||||
|
| null
|
||||||
|
SpawnSelectionType?: "random" | string | null
|
||||||
|
Gamemodes?: ("versus" | string)[] | null
|
||||||
|
Enginemodes?: ("singleplayer" | "multiplayer" | string)[] | null
|
||||||
EndConditions?: {
|
EndConditions?: {
|
||||||
PointLimit?: number
|
PointLimit?: number
|
||||||
}
|
} | null
|
||||||
Subtype?: string
|
Subtype?: string | null
|
||||||
GroupTitle?: string
|
GroupTitle?: string | null
|
||||||
TargetExpiration?: number
|
TargetExpiration?: number | null
|
||||||
TargetExpirationReduced?: number
|
TargetExpirationReduced?: number | null
|
||||||
TargetLifeTime?: number
|
TargetLifeTime?: number | null
|
||||||
NonTargetKillPenaltyEnabled?: boolean
|
NonTargetKillPenaltyEnabled?: boolean | null
|
||||||
NoticedTargetStreakPenaltyMax?: number
|
NoticedTargetStreakPenaltyMax?: number | null
|
||||||
IsFeatured?: boolean
|
IsFeatured?: boolean | null
|
||||||
// Begin escalation-exclusive properties
|
// Begin escalation-exclusive properties
|
||||||
InGroup?: string
|
InGroup?: string | null
|
||||||
NextContractId?: string
|
NextContractId?: string | null
|
||||||
GroupDefinition?: ContractGroupDefinition
|
GroupDefinition?: ContractGroupDefinition | null
|
||||||
GroupData?: {
|
GroupData?: EscalationInfo["GroupData"] | null
|
||||||
Level: number
|
|
||||||
TotalLevels: number
|
|
||||||
Completed: boolean
|
|
||||||
FirstContractId: string
|
|
||||||
}
|
|
||||||
// End escalation-exclusive properties
|
// End escalation-exclusive properties
|
||||||
/**
|
/**
|
||||||
* Useless property.
|
* Useless property.
|
||||||
@ -971,19 +968,19 @@ export interface MissionManifestMetadata {
|
|||||||
* @deprecated
|
* @deprecated
|
||||||
*/
|
*/
|
||||||
readonly UserData?: unknown | null
|
readonly UserData?: unknown | null
|
||||||
IsVersus?: boolean
|
IsVersus?: boolean | null
|
||||||
IsEvergreenSafehouse?: boolean
|
IsEvergreenSafehouse?: boolean | null
|
||||||
UseContractProgressionData?: boolean
|
UseContractProgressionData?: boolean | null
|
||||||
CpdId?: string
|
CpdId?: string | null
|
||||||
/**
|
/**
|
||||||
* Custom property used for Elusives (like official's year)
|
* Custom property used for Elusives (like official's year)
|
||||||
* and Escalations (if it's 0, it is a Peacock escalation,
|
* and Escalations (if it's 0, it is a Peacock escalation,
|
||||||
* and OriginalSeason will exist for filtering).
|
* and OriginalSeason will exist for filtering).
|
||||||
*/
|
*/
|
||||||
Season?: number
|
Season?: number | null
|
||||||
OriginalSeason?: number
|
OriginalSeason?: number | null
|
||||||
// Used for sniper scoring
|
// Used for sniper scoring
|
||||||
Modules?: ManifestScoringModule[]
|
Modules?: ManifestScoringModule[] | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GroupObjectiveDisplayOrderItem {
|
export interface GroupObjectiveDisplayOrderItem {
|
||||||
@ -1039,7 +1036,7 @@ export interface MissionManifest {
|
|||||||
EnableExits?: {
|
EnableExits?: {
|
||||||
$eq?: (string | boolean)[]
|
$eq?: (string | boolean)[]
|
||||||
}
|
}
|
||||||
DevOnlyBricks?: string[]
|
DevOnlyBricks?: string[] | null
|
||||||
}
|
}
|
||||||
Metadata: MissionManifestMetadata
|
Metadata: MissionManifestMetadata
|
||||||
readonly UserData?: Record<string, never> | never[]
|
readonly UserData?: Record<string, never> | never[]
|
||||||
@ -1216,7 +1213,7 @@ export interface CompiledChallengeTreeData {
|
|||||||
CategoryName: string
|
CategoryName: string
|
||||||
ChallengeProgress?: ChallengeTreeWaterfallState
|
ChallengeProgress?: ChallengeTreeWaterfallState
|
||||||
Completed: boolean
|
Completed: boolean
|
||||||
CompletionData: CompletionData
|
CompletionData?: CompletionData
|
||||||
Description: string
|
Description: string
|
||||||
// A string array of at most one element ("easy", "normal", or "hard").
|
// A string array of at most one element ("easy", "normal", or "hard").
|
||||||
// If empty, then the challenge should appear in sessions on any difficulty.
|
// If empty, then the challenge should appear in sessions on any difficulty.
|
||||||
@ -1276,6 +1273,7 @@ export interface ChallengeProgressionData {
|
|||||||
ProfileId: string
|
ProfileId: string
|
||||||
Completed: boolean
|
Completed: boolean
|
||||||
Ticked: boolean
|
Ticked: boolean
|
||||||
|
ETag?: string
|
||||||
State: Record<string, unknown>
|
State: Record<string, unknown>
|
||||||
CompletedAt: Date | string | null
|
CompletedAt: Date | string | null
|
||||||
MustBeSaved: boolean
|
MustBeSaved: boolean
|
||||||
@ -1286,14 +1284,6 @@ export interface CompiledChallengeRuntimeData {
|
|||||||
Progression: ChallengeProgressionData
|
Progression: ChallengeProgressionData
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CompiledChallengeRewardData {
|
|
||||||
ChallengeId: string
|
|
||||||
ChallengeName: string
|
|
||||||
ChallengeDescription: string
|
|
||||||
ChallengeImageUrl: string
|
|
||||||
XPGain: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export type LoadoutSavingMechanism = "PROFILES" | "LEGACY"
|
export type LoadoutSavingMechanism = "PROFILES" | "LEGACY"
|
||||||
export type ImageLoadingStrategy = "SAVEASREQUESTED" | "ONLINE" | "OFFLINE"
|
export type ImageLoadingStrategy = "SAVEASREQUESTED" | "ONLINE" | "OFFLINE"
|
||||||
|
|
||||||
@ -1322,7 +1312,7 @@ export interface IHit {
|
|||||||
/**
|
/**
|
||||||
* A video object.
|
* A video object.
|
||||||
*
|
*
|
||||||
* @see ICampaignVideo
|
* @see CampaignVideo
|
||||||
* @see StoryData
|
* @see StoryData
|
||||||
*/
|
*/
|
||||||
export interface IVideo {
|
export interface IVideo {
|
||||||
@ -1346,7 +1336,7 @@ export interface IVideo {
|
|||||||
*
|
*
|
||||||
* @see IHit
|
* @see IHit
|
||||||
*/
|
*/
|
||||||
export type ICampaignMission = {
|
export type CampaignMission = {
|
||||||
Type: "Mission"
|
Type: "Mission"
|
||||||
Data: IHit
|
Data: IHit
|
||||||
}
|
}
|
||||||
@ -1356,7 +1346,7 @@ export type ICampaignMission = {
|
|||||||
*
|
*
|
||||||
* @see IVideo
|
* @see IVideo
|
||||||
*/
|
*/
|
||||||
export type ICampaignVideo = {
|
export type CampaignVideo = {
|
||||||
Type: "Video"
|
Type: "Video"
|
||||||
Data: IVideo
|
Data: IVideo
|
||||||
}
|
}
|
||||||
@ -1373,7 +1363,7 @@ export interface RegistryChallenge extends SavedChallenge {
|
|||||||
/**
|
/**
|
||||||
* An element for the game's story data.
|
* An element for the game's story data.
|
||||||
*/
|
*/
|
||||||
export type StoryData = ICampaignMission | ICampaignVideo
|
export type StoryData = CampaignMission | CampaignVideo
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A campaign object.
|
* A campaign object.
|
||||||
@ -1415,7 +1405,7 @@ export interface Loadout {
|
|||||||
*
|
*
|
||||||
* @see LoadoutFile
|
* @see LoadoutFile
|
||||||
*/
|
*/
|
||||||
export interface LoadoutsGameVersion {
|
export type LoadoutsGameVersion = {
|
||||||
selected: string | null
|
selected: string | null
|
||||||
loadouts: Loadout[]
|
loadouts: Loadout[]
|
||||||
}
|
}
|
||||||
@ -1423,19 +1413,20 @@ export interface LoadoutsGameVersion {
|
|||||||
/**
|
/**
|
||||||
* The top-level format for the loadout profiles storage file.
|
* The top-level format for the loadout profiles storage file.
|
||||||
*/
|
*/
|
||||||
export interface LoadoutFile {
|
export type LoadoutFile = Record<
|
||||||
h1: LoadoutsGameVersion
|
// game version but not scpc
|
||||||
h2: LoadoutsGameVersion
|
Exclude<GameVersion, "scpc">,
|
||||||
h3: LoadoutsGameVersion
|
LoadoutsGameVersion
|
||||||
}
|
>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A function that generates a campaign mission object for use in the campaigns menu.
|
* A function that generates a campaign mission object for use in the campaigns menu.
|
||||||
|
* Will throw if contract is not found.
|
||||||
*/
|
*/
|
||||||
export type GenSingleMissionFunc = (
|
export type GenSingleMissionFunc = (
|
||||||
contractId: string,
|
contractId: string,
|
||||||
gameVersion: GameVersion,
|
gameVersion: GameVersion,
|
||||||
) => ICampaignMission
|
) => CampaignMission
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A function that generates a campaign video object for use in the campaigns menu.
|
* A function that generates a campaign video object for use in the campaigns menu.
|
||||||
@ -1443,7 +1434,7 @@ export type GenSingleMissionFunc = (
|
|||||||
export type GenSingleVideoFunc = (
|
export type GenSingleVideoFunc = (
|
||||||
videoId: string,
|
videoId: string,
|
||||||
gameVersion: GameVersion,
|
gameVersion: GameVersion,
|
||||||
) => ICampaignVideo
|
) => CampaignVideo
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A "hits category" is used to display lists of contracts in-game.
|
* A "hits category" is used to display lists of contracts in-game.
|
||||||
@ -1486,16 +1477,25 @@ export interface PlayNextGetCampaignsHookReturn {
|
|||||||
|
|
||||||
export type SafehouseCategory = {
|
export type SafehouseCategory = {
|
||||||
Category: string
|
Category: string
|
||||||
SubCategories: SafehouseCategory[]
|
SubCategories: SafehouseCategory[] | null
|
||||||
IsLeaf: boolean
|
IsLeaf: boolean
|
||||||
Data: null
|
Data: null | {
|
||||||
}
|
Type: string
|
||||||
|
SubType: string | undefined
|
||||||
export type SniperLoadout = {
|
Items: {
|
||||||
ID: string
|
Item: InventoryItem
|
||||||
InstanceID: string
|
ItemDetails: {
|
||||||
Unlockable: Unlockable[]
|
Capabilities: []
|
||||||
MainUnlockable: Unlockable
|
StatList: {
|
||||||
|
Name: string
|
||||||
|
Ratio: unknown
|
||||||
|
PropertyTexts: []
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
}[]
|
||||||
|
Page: number
|
||||||
|
HasMore: boolean
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -28,7 +28,7 @@ import type {
|
|||||||
Unlockable,
|
Unlockable,
|
||||||
UserProfile,
|
UserProfile,
|
||||||
} from "./types/types"
|
} from "./types/types"
|
||||||
import axios, { AxiosError } from "axios"
|
import { AxiosError } from "axios"
|
||||||
import { log, LogLevel } from "./loggingInterop"
|
import { log, LogLevel } from "./loggingInterop"
|
||||||
import { writeFileSync } from "fs"
|
import { writeFileSync } from "fs"
|
||||||
import { getFlag } from "./flags"
|
import { getFlag } from "./flags"
|
||||||
@ -54,7 +54,7 @@ export const uuidRegex =
|
|||||||
|
|
||||||
export const contractTypes = ["featured", "usercreated"]
|
export const contractTypes = ["featured", "usercreated"]
|
||||||
|
|
||||||
export const versions: GameVersion[] = ["h1", "h2", "h3"]
|
export const versions: Exclude<GameVersion, "scpc">[] = ["h1", "h2", "h3"]
|
||||||
|
|
||||||
export const contractCreationTutorialId = "d7e2607c-6916-48e2-9588-976c7d8998bb"
|
export const contractCreationTutorialId = "d7e2607c-6916-48e2-9588-976c7d8998bb"
|
||||||
|
|
||||||
@ -71,10 +71,10 @@ export async function checkForUpdates(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await axios(
|
const res = await fetch(
|
||||||
"https://backend.rdil.rocks/peacock/latest-version",
|
"https://backend.rdil.rocks/peacock/latest-version",
|
||||||
)
|
)
|
||||||
const current = res.data
|
const current = parseInt(await res.text(), 10)
|
||||||
|
|
||||||
if (PEACOCKVER < 0 && current < -PEACOCKVER) {
|
if (PEACOCKVER < 0 && current < -PEACOCKVER) {
|
||||||
log(
|
log(
|
||||||
@ -94,12 +94,15 @@ export async function checkForUpdates(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRemoteService(gameVersion: GameVersion): string {
|
export function getRemoteService(gameVersion: GameVersion): string | undefined {
|
||||||
return gameVersion === "h3"
|
switch (gameVersion) {
|
||||||
? "hm3-service"
|
case "h3":
|
||||||
: gameVersion === "h2"
|
return "hm3-service"
|
||||||
? "pc2-service"
|
case "h2":
|
||||||
: "pc-service"
|
return "pc2-service"
|
||||||
|
default:
|
||||||
|
return "pc-service"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -306,6 +309,7 @@ function updateUserProfile(
|
|||||||
|
|
||||||
if (gameVersion === "h1") {
|
if (gameVersion === "h1") {
|
||||||
// No sniper locations, but we add normal and pro1
|
// No sniper locations, but we add normal and pro1
|
||||||
|
// @ts-expect-error I know what I'm doing.
|
||||||
obj[newKey] = {
|
obj[newKey] = {
|
||||||
// Data from previous profiles only contains normal and is the default.
|
// Data from previous profiles only contains normal and is the default.
|
||||||
normal: {
|
normal: {
|
||||||
@ -321,8 +325,10 @@ function updateUserProfile(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// We need to update sniper locations.
|
// We need to update sniper locations.
|
||||||
|
// @ts-expect-error I know what I'm doing.
|
||||||
obj[newKey] = sniperLocs[newKey]
|
obj[newKey] = sniperLocs[newKey]
|
||||||
? sniperLocs[newKey].reduce((obj, uId) => {
|
? // @ts-expect-error I know what I'm doing.
|
||||||
|
sniperLocs[newKey].reduce((obj, uId) => {
|
||||||
obj[uId] = {
|
obj[uId] = {
|
||||||
Xp: 0,
|
Xp: 0,
|
||||||
Level: 1,
|
Level: 1,
|
||||||
@ -344,7 +350,7 @@ function updateUserProfile(
|
|||||||
{},
|
{},
|
||||||
)
|
)
|
||||||
|
|
||||||
// ts-expect-error Legacy property.
|
// @ts-expect-error Legacy property.
|
||||||
delete profile.Extensions.progression["Unlockables"]
|
delete profile.Extensions.progression["Unlockables"]
|
||||||
|
|
||||||
profile.Version = 1
|
profile.Version = 1
|
||||||
@ -426,12 +432,7 @@ export function castUserProfile(
|
|||||||
|
|
||||||
// Fix Extensions.gamepersistentdata.HitsFilterType.
|
// Fix Extensions.gamepersistentdata.HitsFilterType.
|
||||||
// None of the old profiles should have "MyPlaylist".
|
// None of the old profiles should have "MyPlaylist".
|
||||||
if (
|
if (j.Extensions.gamepersistentdata.HitsFilterType["MyPlaylist"]) {
|
||||||
!Object.hasOwn(
|
|
||||||
j.Extensions.gamepersistentdata.HitsFilterType,
|
|
||||||
"MyPlaylist",
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
j.Extensions.gamepersistentdata.HitsFilterType = {
|
j.Extensions.gamepersistentdata.HitsFilterType = {
|
||||||
MyHistory: "all",
|
MyHistory: "all",
|
||||||
MyContracts: "all",
|
MyContracts: "all",
|
||||||
@ -520,15 +521,17 @@ export const defaultSuits = {
|
|||||||
* @returns The default suits that are attainable via challenges or mastery.
|
* @returns The default suits that are attainable via challenges or mastery.
|
||||||
*/
|
*/
|
||||||
export function attainableDefaults(gameVersion: GameVersion): string[] {
|
export function attainableDefaults(gameVersion: GameVersion): string[] {
|
||||||
return gameVersion === "h1"
|
if (gameVersion === "h1") {
|
||||||
? []
|
return []
|
||||||
: gameVersion === "h2"
|
} else if (gameVersion === "h2") {
|
||||||
? ["TOKEN_OUTFIT_WET_SUIT"]
|
return ["TOKEN_OUTFIT_WET_SUIT"]
|
||||||
: [
|
} else {
|
||||||
"TOKEN_OUTFIT_GREENLAND_HERO_TRAININGSUIT",
|
return [
|
||||||
"TOKEN_OUTFIT_WET_SUIT",
|
"TOKEN_OUTFIT_GREENLAND_HERO_TRAININGSUIT",
|
||||||
"TOKEN_OUTFIT_HERO_DUGONG_SUIT",
|
"TOKEN_OUTFIT_WET_SUIT",
|
||||||
]
|
"TOKEN_OUTFIT_HERO_DUGONG_SUIT",
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -538,10 +541,11 @@ export function attainableDefaults(gameVersion: GameVersion): string[] {
|
|||||||
* @param subLocation The sub-location.
|
* @param subLocation The sub-location.
|
||||||
* @returns The default suit for the given sub-location and parent location.
|
* @returns The default suit for the given sub-location and parent location.
|
||||||
*/
|
*/
|
||||||
export function getDefaultSuitFor(subLocation: Unlockable): string | undefined {
|
export function getDefaultSuitFor(subLocation: Unlockable): string {
|
||||||
|
type Cast = keyof typeof defaultSuits
|
||||||
return (
|
return (
|
||||||
defaultSuits[subLocation.Id] ||
|
defaultSuits[subLocation.Id as Cast] ||
|
||||||
defaultSuits[subLocation.Properties.ParentLocation] ||
|
defaultSuits[subLocation.Properties.ParentLocation as Cast] ||
|
||||||
"TOKEN_OUTFIT_HITMANSUIT"
|
"TOKEN_OUTFIT_HITMANSUIT"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Request, Response, Router } from "express"
|
import { NextFunction, Request, Response, Router } from "express"
|
||||||
import { getConfig } from "./configSwizzleManager"
|
import { getConfig } from "./configSwizzleManager"
|
||||||
import { readFileSync } from "atomically"
|
import { readFileSync } from "atomically"
|
||||||
import { GameVersion, UserProfile } from "./types/types"
|
import { GameVersion, UserProfile } from "./types/types"
|
||||||
@ -41,6 +41,40 @@ if (PEACOCK_DEV) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CommonRequest<ExtraQuery = Record<never, never>> = Request<
|
||||||
|
unknown,
|
||||||
|
unknown,
|
||||||
|
unknown,
|
||||||
|
{
|
||||||
|
user: string
|
||||||
|
gv: Exclude<GameVersion, "scpc">
|
||||||
|
} & ExtraQuery
|
||||||
|
>
|
||||||
|
|
||||||
|
function commonValidationMiddleware(
|
||||||
|
req: CommonRequest,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction,
|
||||||
|
): void {
|
||||||
|
if (!req.query.gv || !versions.includes(req.query.gv ?? null)) {
|
||||||
|
res.json({
|
||||||
|
success: false,
|
||||||
|
error: "invalid game version",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.query.user || !uuidRegex.test(req.query.user)) {
|
||||||
|
res.json({
|
||||||
|
success: false,
|
||||||
|
error: "The request must contain the uuid of a user.",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
|
||||||
function formErrorMessage(res: Response, message: string): void {
|
function formErrorMessage(res: Response, message: string): void {
|
||||||
res.json({
|
res.json({
|
||||||
success: false,
|
success: false,
|
||||||
@ -48,83 +82,49 @@ function formErrorMessage(res: Response, message: string): void {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
webFeaturesRouter.get("/codenames", (req, res) => {
|
webFeaturesRouter.get("/codenames", (_, res) => {
|
||||||
res.json(getConfig("EscalationCodenames", false))
|
res.json(getConfig("EscalationCodenames", false))
|
||||||
})
|
})
|
||||||
|
|
||||||
webFeaturesRouter.get(
|
webFeaturesRouter.get("/local-users", (req: CommonRequest, res) => {
|
||||||
"/local-users",
|
|
||||||
(req: Request<unknown, unknown, unknown, { gv: GameVersion }>, res) => {
|
|
||||||
if (!req.query.gv || !versions.includes(req.query.gv ?? null)) {
|
|
||||||
res.json([])
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let dir
|
|
||||||
|
|
||||||
if (req.query.gv === "h3") {
|
|
||||||
dir = join("userdata", "users")
|
|
||||||
} else {
|
|
||||||
dir = join("userdata", req.query.gv, "users")
|
|
||||||
}
|
|
||||||
|
|
||||||
const files: string[] = readdirSync(dir).filter(
|
|
||||||
(name) => name !== "lop.json",
|
|
||||||
)
|
|
||||||
|
|
||||||
const result = []
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
const read = JSON.parse(
|
|
||||||
readFileSync(join(dir, file)).toString(),
|
|
||||||
) as UserProfile
|
|
||||||
|
|
||||||
result.push({
|
|
||||||
id: read.Id,
|
|
||||||
name: read.Gamertag,
|
|
||||||
platform: read.EpicId ? "Epic" : "Steam",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json(result)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
function validateUserAndGv(
|
|
||||||
req: Request<unknown, unknown, unknown, { gv: GameVersion; user: string }>,
|
|
||||||
res: Response,
|
|
||||||
): boolean {
|
|
||||||
if (!req.query.gv || !versions.includes(req.query.gv ?? null)) {
|
if (!req.query.gv || !versions.includes(req.query.gv ?? null)) {
|
||||||
formErrorMessage(
|
res.json([])
|
||||||
res,
|
return
|
||||||
'The request must contain a valid game version among "h1", "h2", and "h3".',
|
|
||||||
)
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!req.query.user || !uuidRegex.test(req.query.user)) {
|
let dir
|
||||||
formErrorMessage(res, "The request must contain the uuid of a user.")
|
|
||||||
return false
|
if (req.query.gv === "h3") {
|
||||||
|
dir = join("userdata", "users")
|
||||||
|
} else {
|
||||||
|
dir = join("userdata", req.query.gv, "users")
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
const files: string[] = readdirSync(dir).filter(
|
||||||
}
|
(name) => name !== "lop.json",
|
||||||
|
)
|
||||||
|
|
||||||
|
const result = []
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const read = JSON.parse(
|
||||||
|
readFileSync(join(dir, file)).toString(),
|
||||||
|
) as UserProfile
|
||||||
|
|
||||||
|
result.push({
|
||||||
|
id: read.Id,
|
||||||
|
name: read.Gamertag,
|
||||||
|
platform: read.EpicId ? "Epic" : "Steam",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(result)
|
||||||
|
})
|
||||||
|
|
||||||
webFeaturesRouter.get(
|
webFeaturesRouter.get(
|
||||||
"/modify",
|
"/modify",
|
||||||
async (
|
commonValidationMiddleware,
|
||||||
req: Request<
|
async (req: CommonRequest<{ level: string; id: string }>, res) => {
|
||||||
unknown,
|
|
||||||
unknown,
|
|
||||||
unknown,
|
|
||||||
{ gv: GameVersion; user: string; level: string; id: string }
|
|
||||||
>,
|
|
||||||
res,
|
|
||||||
) => {
|
|
||||||
if (!validateUserAndGv(req, res)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!req.query.level) {
|
if (!req.query.level) {
|
||||||
formErrorMessage(
|
formErrorMessage(
|
||||||
res,
|
res,
|
||||||
@ -158,7 +158,7 @@ webFeaturesRouter.get(
|
|||||||
|
|
||||||
const mapping = controller.escalationMappings.get(req.query.id)
|
const mapping = controller.escalationMappings.get(req.query.id)
|
||||||
|
|
||||||
if (mapping === undefined) {
|
if (!mapping) {
|
||||||
formErrorMessage(res, "Unknown escalation.")
|
formErrorMessage(res, "Unknown escalation.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -198,19 +198,8 @@ webFeaturesRouter.get(
|
|||||||
|
|
||||||
webFeaturesRouter.get(
|
webFeaturesRouter.get(
|
||||||
"/user-progress",
|
"/user-progress",
|
||||||
async (
|
commonValidationMiddleware,
|
||||||
req: Request<
|
async (req: CommonRequest, res) => {
|
||||||
unknown,
|
|
||||||
unknown,
|
|
||||||
unknown,
|
|
||||||
{ gv: GameVersion; user: string }
|
|
||||||
>,
|
|
||||||
res,
|
|
||||||
) => {
|
|
||||||
if (!validateUserAndGv(req, res)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await loadUserData(req.query.user, req.query.gv)
|
await loadUserData(req.query.user, req.query.gv)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
27
package.json
27
package.json
@ -19,7 +19,8 @@
|
|||||||
"webui": "yarn workspace @peacockproject/web-ui",
|
"webui": "yarn workspace @peacockproject/web-ui",
|
||||||
"typedefs": "yarn workspace @peacockproject/core",
|
"typedefs": "yarn workspace @peacockproject/core",
|
||||||
"run-dev": "node packaging/devLoader.mjs",
|
"run-dev": "node packaging/devLoader.mjs",
|
||||||
"extract-challenge-data": "node packaging/extractChallengeData.mjs"
|
"extract-challenge-data": "node packaging/extractChallengeData.mjs",
|
||||||
|
"find-circular": "yarn dlx dpdm components/index.ts --exclude \"(components/types)|(node_modules)\" -T"
|
||||||
},
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"semi": false,
|
"semi": false,
|
||||||
@ -27,7 +28,7 @@
|
|||||||
"trailingComma": "all"
|
"trailingComma": "all"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"body-parser": "npm:@peacockproject/body-parser@npm:2.0.0-peacock.6",
|
"body-parser": "npm:@peacockproject/body-parser@npm:3.0.0-peacock.1",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"http-errors": "patch:http-errors@npm:2.0.0#.yarn/patches/http-errors-npm-2.0.0-3f1c503428.patch",
|
"http-errors": "patch:http-errors@npm:2.0.0#.yarn/patches/http-errors-npm-2.0.0-3f1c503428.patch",
|
||||||
"iconv-lite": "patch:iconv-lite@npm:0.6.3#.yarn/patches/iconv-lite-npm-0.6.3-24b8aae27e.patch",
|
"iconv-lite": "patch:iconv-lite@npm:0.6.3#.yarn/patches/iconv-lite-npm-0.6.3-24b8aae27e.patch",
|
||||||
@ -40,18 +41,18 @@
|
|||||||
"atomically": "^2.0.2",
|
"atomically": "^2.0.2",
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.6.0",
|
||||||
"body-parser": "*",
|
"body-parser": "*",
|
||||||
"clipanion": "^3.2.1",
|
"clipanion": "^4.0.0-rc.3",
|
||||||
"commander": "^11.1.0",
|
"commander": "^11.1.0",
|
||||||
"deepmerge-ts": "^5.1.0",
|
"deepmerge-ts": "^5.1.0",
|
||||||
"esbuild-wasm": "^0.19.5",
|
"esbuild-wasm": "^0.19.12",
|
||||||
"express": "patch:express@npm%3A4.18.2#~/.yarn/patches/express-npm-4.18.2-bb15ff679a.patch",
|
"express": "patch:express@npm%3A4.18.2#~/.yarn/patches/express-npm-4.18.2-bb15ff679a.patch",
|
||||||
"jest-diff": "^29.7.0",
|
"jest-diff": "^29.7.0",
|
||||||
"js-ini": "^1.6.0",
|
"js-ini": "^1.6.0",
|
||||||
"json5": "^2.2.3",
|
"json5": "^2.2.3",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"md5-file": "^5.0.0",
|
"md5-file": "^5.0.0",
|
||||||
"msgpackr": "^1.9.9",
|
"msgpackr": "^1.10.1",
|
||||||
"nanoid": "^5.0.3",
|
"nanoid": "^5.0.4",
|
||||||
"parseurl": "^1.3.3",
|
"parseurl": "^1.3.3",
|
||||||
"picocolors": "patch:picocolors@npm%3A1.0.0#~/.yarn/patches/picocolors-npm-1.0.0-d81e0b1927.patch",
|
"picocolors": "patch:picocolors@npm%3A1.0.0#~/.yarn/patches/picocolors-npm-1.0.0-d81e0b1927.patch",
|
||||||
"progress": "^2.0.3",
|
"progress": "^2.0.3",
|
||||||
@ -71,12 +72,12 @@
|
|||||||
"@types/progress": "^2.0.6",
|
"@types/progress": "^2.0.6",
|
||||||
"@types/prompts": "^2.4.7",
|
"@types/prompts": "^2.4.7",
|
||||||
"@types/send": "^0.17.3",
|
"@types/send": "^0.17.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.10.0",
|
"@typescript-eslint/eslint-plugin": "^6.19.1",
|
||||||
"@typescript-eslint/parser": "^6.10.0",
|
"@typescript-eslint/parser": "^6.19.1",
|
||||||
"esbuild": "^0.19.5",
|
"esbuild": "^0.19.12",
|
||||||
"esbuild-register": "^3.5.0",
|
"esbuild-register": "^3.5.0",
|
||||||
"eslint": "^8.53.0",
|
"eslint": "^8.56.0",
|
||||||
"eslint-config-prettier": "^9.0.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-promise": "^6.1.1",
|
"eslint-plugin-promise": "^6.1.1",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"fast-glob": "^3.3.2",
|
"fast-glob": "^3.3.2",
|
||||||
@ -84,8 +85,8 @@
|
|||||||
"ms": "^2.1.3",
|
"ms": "^2.1.3",
|
||||||
"prettier": "^2.8.8",
|
"prettier": "^2.8.8",
|
||||||
"rimraf": "^5.0.5",
|
"rimraf": "^5.0.5",
|
||||||
"terser": "^5.21.0",
|
"terser": "^5.27.0",
|
||||||
"typescript": "5.2.2",
|
"typescript": "5.3.3",
|
||||||
"winston": "^3.11.0",
|
"winston": "^3.11.0",
|
||||||
"winston-daily-rotate-file": "^4.7.1"
|
"winston-daily-rotate-file": "^4.7.1"
|
||||||
},
|
},
|
||||||
|
@ -74,6 +74,7 @@ await e.build({
|
|||||||
"process.env.ZEIT_BITBUCKET_COMMIT_SHA": "undefined",
|
"process.env.ZEIT_BITBUCKET_COMMIT_SHA": "undefined",
|
||||||
"process.env.VERCEL_GIT_COMMIT_SHA": "undefined",
|
"process.env.VERCEL_GIT_COMMIT_SHA": "undefined",
|
||||||
"process.env.ZEIT_GITLAB_COMMIT_SHA": "undefined",
|
"process.env.ZEIT_GITLAB_COMMIT_SHA": "undefined",
|
||||||
|
"process.env.MSGPACKR_NATIVE_ACCELERATION_DISABLED": "true",
|
||||||
},
|
},
|
||||||
sourcemap: "external",
|
sourcemap: "external",
|
||||||
plugins: [
|
plugins: [
|
||||||
|
@ -23,12 +23,12 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@peacockproject/statemachine-parser": "^5.9.3",
|
"@peacockproject/statemachine-parser": "^5.9.3",
|
||||||
"@types/express": "^4.17.20",
|
"@types/express": "^4.17.20",
|
||||||
"@types/node": "^20.8.10",
|
"@types/node": "*",
|
||||||
"atomically": "^2.0.2",
|
"atomically": "^2.0.2",
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.6.7",
|
||||||
"js-ini": "^1.6.0"
|
"js-ini": "^1.6.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"msgpackr": "^1.9.9"
|
"msgpackr": "^1.10.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,245 +1,242 @@
|
|||||||
{
|
{
|
||||||
"template": null,
|
"SubLocationData": [],
|
||||||
"data": {
|
"PlayerProfileXp": {
|
||||||
"SubLocationData": [],
|
"Total": 0,
|
||||||
"PlayerProfileXp": {
|
"Level": 1,
|
||||||
"Total": 0,
|
"Seasons": [
|
||||||
"Level": 1,
|
{
|
||||||
"Seasons": [
|
"Number": 1,
|
||||||
{
|
"Locations": [
|
||||||
"Number": 1,
|
{
|
||||||
"Locations": [
|
"LocationId": "LOCATION_PARENT_ICA_FACILITY",
|
||||||
{
|
"Xp": 0,
|
||||||
"LocationId": "LOCATION_PARENT_ICA_FACILITY",
|
"ActionXp": 0
|
||||||
"Xp": 0,
|
},
|
||||||
"ActionXp": 0
|
{
|
||||||
},
|
"LocationId": "LOCATION_PARENT_PARIS",
|
||||||
{
|
"Xp": 0,
|
||||||
"LocationId": "LOCATION_PARENT_PARIS",
|
"ActionXp": 0,
|
||||||
"Xp": 0,
|
"LocationProgression": {
|
||||||
"ActionXp": 0,
|
"Level": 1,
|
||||||
"LocationProgression": {
|
"MaxLevel": 20
|
||||||
"Level": 1,
|
|
||||||
"MaxLevel": 20
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"LocationId": "LOCATION_PARENT_COASTALTOWN",
|
|
||||||
"Xp": 0,
|
|
||||||
"ActionXp": 0,
|
|
||||||
"LocationProgression": {
|
|
||||||
"Level": 1,
|
|
||||||
"MaxLevel": 20
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"LocationId": "LOCATION_PARENT_MARRAKECH",
|
|
||||||
"Xp": 0,
|
|
||||||
"ActionXp": 0,
|
|
||||||
"LocationProgression": {
|
|
||||||
"Level": 1,
|
|
||||||
"MaxLevel": 20
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"LocationId": "LOCATION_PARENT_BANGKOK",
|
|
||||||
"Xp": 0,
|
|
||||||
"ActionXp": 0,
|
|
||||||
"LocationProgression": {
|
|
||||||
"Level": 1,
|
|
||||||
"MaxLevel": 20
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"LocationId": "LOCATION_PARENT_COLORADO",
|
|
||||||
"Xp": 0,
|
|
||||||
"ActionXp": 0,
|
|
||||||
"LocationProgression": {
|
|
||||||
"Level": 1,
|
|
||||||
"MaxLevel": 20
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"LocationId": "LOCATION_PARENT_HOKKAIDO",
|
|
||||||
"Xp": 0,
|
|
||||||
"ActionXp": 0,
|
|
||||||
"LocationProgression": {
|
|
||||||
"Level": 1,
|
|
||||||
"MaxLevel": 20
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
},
|
||||||
},
|
{
|
||||||
{
|
"LocationId": "LOCATION_PARENT_COASTALTOWN",
|
||||||
"Number": 2,
|
"Xp": 0,
|
||||||
"Locations": [
|
"ActionXp": 0,
|
||||||
{
|
"LocationProgression": {
|
||||||
"LocationId": "LOCATION_PARENT_NEWZEALAND",
|
"Level": 1,
|
||||||
"Xp": 0,
|
"MaxLevel": 20
|
||||||
"ActionXp": 0,
|
|
||||||
"LocationProgression": {
|
|
||||||
"Level": 1,
|
|
||||||
"MaxLevel": 5
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"LocationId": "LOCATION_PARENT_MIAMI",
|
|
||||||
"Xp": 0,
|
|
||||||
"ActionXp": 0,
|
|
||||||
"LocationProgression": {
|
|
||||||
"Level": 1,
|
|
||||||
"MaxLevel": 20
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"LocationId": "LOCATION_PARENT_COLOMBIA",
|
|
||||||
"Xp": 0,
|
|
||||||
"ActionXp": 0,
|
|
||||||
"LocationProgression": {
|
|
||||||
"Level": 1,
|
|
||||||
"MaxLevel": 20
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"LocationId": "LOCATION_PARENT_MUMBAI",
|
|
||||||
"Xp": 0,
|
|
||||||
"ActionXp": 0,
|
|
||||||
"LocationProgression": {
|
|
||||||
"Level": 1,
|
|
||||||
"MaxLevel": 20
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"LocationId": "LOCATION_PARENT_NORTHAMERICA",
|
|
||||||
"Xp": 0,
|
|
||||||
"ActionXp": 0,
|
|
||||||
"LocationProgression": {
|
|
||||||
"Level": 1,
|
|
||||||
"MaxLevel": 20
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"LocationId": "LOCATION_PARENT_NORTHSEA",
|
|
||||||
"Xp": 0,
|
|
||||||
"ActionXp": 0,
|
|
||||||
"LocationProgression": {
|
|
||||||
"Level": 1,
|
|
||||||
"MaxLevel": 20
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"LocationId": "LOCATION_PARENT_GREEDY",
|
|
||||||
"Xp": 0,
|
|
||||||
"ActionXp": 0,
|
|
||||||
"LocationProgression": {
|
|
||||||
"Level": 1,
|
|
||||||
"MaxLevel": 20
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"LocationId": "LOCATION_PARENT_OPULENT",
|
|
||||||
"Xp": 0,
|
|
||||||
"ActionXp": 0,
|
|
||||||
"LocationProgression": {
|
|
||||||
"Level": 1,
|
|
||||||
"MaxLevel": 20
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"LocationId": "LOCATION_PARENT_AUSTRIA",
|
|
||||||
"Xp": 0,
|
|
||||||
"ActionXp": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"LocationId": "LOCATION_PARENT_SALTY",
|
|
||||||
"Xp": 0,
|
|
||||||
"ActionXp": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"LocationId": "LOCATION_PARENT_CAGED",
|
|
||||||
"Xp": 0,
|
|
||||||
"ActionXp": 0
|
|
||||||
}
|
}
|
||||||
]
|
},
|
||||||
},
|
{
|
||||||
{
|
"LocationId": "LOCATION_PARENT_MARRAKECH",
|
||||||
"Number": 3,
|
"Xp": 0,
|
||||||
"Locations": [
|
"ActionXp": 0,
|
||||||
{
|
"LocationProgression": {
|
||||||
"LocationId": "LOCATION_PARENT_GOLDEN",
|
"Level": 1,
|
||||||
"Xp": 0,
|
"MaxLevel": 20
|
||||||
"ActionXp": 0,
|
|
||||||
"LocationProgression": {
|
|
||||||
"Level": 1,
|
|
||||||
"MaxLevel": 20
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"LocationId": "LOCATION_PARENT_ANCESTRAL",
|
|
||||||
"Xp": 0,
|
|
||||||
"ActionXp": 0,
|
|
||||||
"LocationProgression": {
|
|
||||||
"Level": 1,
|
|
||||||
"MaxLevel": 20
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"LocationId": "LOCATION_PARENT_EDGY",
|
|
||||||
"Xp": 0,
|
|
||||||
"ActionXp": 0,
|
|
||||||
"LocationProgression": {
|
|
||||||
"Level": 1,
|
|
||||||
"MaxLevel": 20
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"LocationId": "LOCATION_PARENT_WET",
|
|
||||||
"Xp": 0,
|
|
||||||
"ActionXp": 0,
|
|
||||||
"LocationProgression": {
|
|
||||||
"Level": 1,
|
|
||||||
"MaxLevel": 20
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"LocationId": "LOCATION_PARENT_ELEGANT",
|
|
||||||
"Xp": 0,
|
|
||||||
"ActionXp": 0,
|
|
||||||
"LocationProgression": {
|
|
||||||
"Level": 1,
|
|
||||||
"MaxLevel": 20
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"LocationId": "LOCATION_PARENT_TRAPPED",
|
|
||||||
"Xp": 0,
|
|
||||||
"ActionXp": 0,
|
|
||||||
"LocationProgression": {
|
|
||||||
"Level": 1,
|
|
||||||
"MaxLevel": 5
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"LocationId": "LOCATION_PARENT_ROCKY",
|
|
||||||
"Xp": 0,
|
|
||||||
"ActionXp": 0,
|
|
||||||
"LocationProgression": {
|
|
||||||
"Level": 1,
|
|
||||||
"MaxLevel": 20
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"LocationId": "LOCATION_PARENT_SNUG",
|
|
||||||
"Xp": 0,
|
|
||||||
"ActionXp": 0,
|
|
||||||
"LocationProgression": {
|
|
||||||
"Level": 1,
|
|
||||||
"MaxLevel": 100
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
},
|
||||||
}
|
{
|
||||||
]
|
"LocationId": "LOCATION_PARENT_BANGKOK",
|
||||||
}
|
"Xp": 0,
|
||||||
|
"ActionXp": 0,
|
||||||
|
"LocationProgression": {
|
||||||
|
"Level": 1,
|
||||||
|
"MaxLevel": 20
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"LocationId": "LOCATION_PARENT_COLORADO",
|
||||||
|
"Xp": 0,
|
||||||
|
"ActionXp": 0,
|
||||||
|
"LocationProgression": {
|
||||||
|
"Level": 1,
|
||||||
|
"MaxLevel": 20
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"LocationId": "LOCATION_PARENT_HOKKAIDO",
|
||||||
|
"Xp": 0,
|
||||||
|
"ActionXp": 0,
|
||||||
|
"LocationProgression": {
|
||||||
|
"Level": 1,
|
||||||
|
"MaxLevel": 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Number": 2,
|
||||||
|
"Locations": [
|
||||||
|
{
|
||||||
|
"LocationId": "LOCATION_PARENT_NEWZEALAND",
|
||||||
|
"Xp": 0,
|
||||||
|
"ActionXp": 0,
|
||||||
|
"LocationProgression": {
|
||||||
|
"Level": 1,
|
||||||
|
"MaxLevel": 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"LocationId": "LOCATION_PARENT_MIAMI",
|
||||||
|
"Xp": 0,
|
||||||
|
"ActionXp": 0,
|
||||||
|
"LocationProgression": {
|
||||||
|
"Level": 1,
|
||||||
|
"MaxLevel": 20
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"LocationId": "LOCATION_PARENT_COLOMBIA",
|
||||||
|
"Xp": 0,
|
||||||
|
"ActionXp": 0,
|
||||||
|
"LocationProgression": {
|
||||||
|
"Level": 1,
|
||||||
|
"MaxLevel": 20
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"LocationId": "LOCATION_PARENT_MUMBAI",
|
||||||
|
"Xp": 0,
|
||||||
|
"ActionXp": 0,
|
||||||
|
"LocationProgression": {
|
||||||
|
"Level": 1,
|
||||||
|
"MaxLevel": 20
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"LocationId": "LOCATION_PARENT_NORTHAMERICA",
|
||||||
|
"Xp": 0,
|
||||||
|
"ActionXp": 0,
|
||||||
|
"LocationProgression": {
|
||||||
|
"Level": 1,
|
||||||
|
"MaxLevel": 20
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"LocationId": "LOCATION_PARENT_NORTHSEA",
|
||||||
|
"Xp": 0,
|
||||||
|
"ActionXp": 0,
|
||||||
|
"LocationProgression": {
|
||||||
|
"Level": 1,
|
||||||
|
"MaxLevel": 20
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"LocationId": "LOCATION_PARENT_GREEDY",
|
||||||
|
"Xp": 0,
|
||||||
|
"ActionXp": 0,
|
||||||
|
"LocationProgression": {
|
||||||
|
"Level": 1,
|
||||||
|
"MaxLevel": 20
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"LocationId": "LOCATION_PARENT_OPULENT",
|
||||||
|
"Xp": 0,
|
||||||
|
"ActionXp": 0,
|
||||||
|
"LocationProgression": {
|
||||||
|
"Level": 1,
|
||||||
|
"MaxLevel": 20
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"LocationId": "LOCATION_PARENT_AUSTRIA",
|
||||||
|
"Xp": 0,
|
||||||
|
"ActionXp": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"LocationId": "LOCATION_PARENT_SALTY",
|
||||||
|
"Xp": 0,
|
||||||
|
"ActionXp": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"LocationId": "LOCATION_PARENT_CAGED",
|
||||||
|
"Xp": 0,
|
||||||
|
"ActionXp": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Number": 3,
|
||||||
|
"Locations": [
|
||||||
|
{
|
||||||
|
"LocationId": "LOCATION_PARENT_GOLDEN",
|
||||||
|
"Xp": 0,
|
||||||
|
"ActionXp": 0,
|
||||||
|
"LocationProgression": {
|
||||||
|
"Level": 1,
|
||||||
|
"MaxLevel": 20
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"LocationId": "LOCATION_PARENT_ANCESTRAL",
|
||||||
|
"Xp": 0,
|
||||||
|
"ActionXp": 0,
|
||||||
|
"LocationProgression": {
|
||||||
|
"Level": 1,
|
||||||
|
"MaxLevel": 20
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"LocationId": "LOCATION_PARENT_EDGY",
|
||||||
|
"Xp": 0,
|
||||||
|
"ActionXp": 0,
|
||||||
|
"LocationProgression": {
|
||||||
|
"Level": 1,
|
||||||
|
"MaxLevel": 20
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"LocationId": "LOCATION_PARENT_WET",
|
||||||
|
"Xp": 0,
|
||||||
|
"ActionXp": 0,
|
||||||
|
"LocationProgression": {
|
||||||
|
"Level": 1,
|
||||||
|
"MaxLevel": 20
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"LocationId": "LOCATION_PARENT_ELEGANT",
|
||||||
|
"Xp": 0,
|
||||||
|
"ActionXp": 0,
|
||||||
|
"LocationProgression": {
|
||||||
|
"Level": 1,
|
||||||
|
"MaxLevel": 20
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"LocationId": "LOCATION_PARENT_TRAPPED",
|
||||||
|
"Xp": 0,
|
||||||
|
"ActionXp": 0,
|
||||||
|
"LocationProgression": {
|
||||||
|
"Level": 1,
|
||||||
|
"MaxLevel": 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"LocationId": "LOCATION_PARENT_ROCKY",
|
||||||
|
"Xp": 0,
|
||||||
|
"ActionXp": 0,
|
||||||
|
"LocationProgression": {
|
||||||
|
"Level": 1,
|
||||||
|
"MaxLevel": 20
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"LocationId": "LOCATION_PARENT_SNUG",
|
||||||
|
"Xp": 0,
|
||||||
|
"ActionXp": 0,
|
||||||
|
"LocationProgression": {
|
||||||
|
"Level": 1,
|
||||||
|
"MaxLevel": 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,8 +26,11 @@ export function asMock<T>(value: T): Mock {
|
|||||||
return value as Mock
|
return value as Mock
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mockRequestWithJwt(): RequestWithJwt<core.Query, any> {
|
export function mockRequestWithJwt<
|
||||||
const mockedRequest = <RequestWithJwt<core.Query, any>>{
|
QS = core.Query,
|
||||||
|
Body = any,
|
||||||
|
>(): RequestWithJwt<QS, Body> {
|
||||||
|
const mockedRequest = <RequestWithJwt<QS, Body>>{
|
||||||
headers: {},
|
headers: {},
|
||||||
header: (name: string) =>
|
header: (name: string) =>
|
||||||
mockedRequest.headers[name.toLowerCase()] as string,
|
mockedRequest.headers[name.toLowerCase()] as string,
|
||||||
@ -36,10 +39,10 @@ export function mockRequestWithJwt(): RequestWithJwt<core.Query, any> {
|
|||||||
return mockedRequest
|
return mockedRequest
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mockRequestWithValidJwt(
|
export function mockRequestWithValidJwt<QS = core.Query, Body = any>(
|
||||||
pId: string,
|
pId: string,
|
||||||
): RequestWithJwt<core.Query, any> {
|
): RequestWithJwt<QS, Body> {
|
||||||
const mockedRequest = mockRequestWithJwt()
|
const mockedRequest = mockRequestWithJwt<QS, Body>()
|
||||||
|
|
||||||
const jwtToken = sign(
|
const jwtToken = sign(
|
||||||
{
|
{
|
||||||
@ -64,15 +67,20 @@ export function mockResponse(): core.Response {
|
|||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @ts-expect-error It works.
|
||||||
response.status = vi.fn().mockImplementation(mockImplementation)
|
response.status = vi.fn().mockImplementation(mockImplementation)
|
||||||
|
// @ts-expect-error It works.
|
||||||
response.json = vi.fn()
|
response.json = vi.fn()
|
||||||
|
// @ts-expect-error It works.
|
||||||
response.end = vi.fn()
|
response.end = vi.fn()
|
||||||
|
|
||||||
|
// @ts-expect-error It works.
|
||||||
return <core.Response>response
|
return <core.Response>response
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getResolvingPromise<T>(value?: T): Promise<T> {
|
export function getResolvingPromise<T>(value?: T): Promise<T> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
|
// @ts-expect-error It works.
|
||||||
resolve(value)
|
resolve(value)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -18,12 +18,15 @@
|
|||||||
|
|
||||||
import * as configSwizzleManager from "../../components/configSwizzleManager"
|
import * as configSwizzleManager from "../../components/configSwizzleManager"
|
||||||
import { readFileSync } from "fs"
|
import { readFileSync } from "fs"
|
||||||
|
import { vi } from "vitest"
|
||||||
|
|
||||||
const originalFilePaths: Record<string, string> = {}
|
const originalFilePaths: Record<string, string> = {}
|
||||||
|
|
||||||
Object.keys(configSwizzleManager.configs).forEach((config: string) => {
|
Object.keys(configSwizzleManager.configs).forEach((config: string) => {
|
||||||
|
// @ts-expect-error It works.
|
||||||
originalFilePaths[config] = <string>configSwizzleManager.configs[config]
|
originalFilePaths[config] = <string>configSwizzleManager.configs[config]
|
||||||
|
|
||||||
|
// @ts-expect-error It works.
|
||||||
configSwizzleManager.configs[config] = undefined
|
configSwizzleManager.configs[config] = undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -34,20 +37,24 @@ export function loadConfig(config: string) {
|
|||||||
|
|
||||||
const contents = readFileSync(originalFilePaths[config], "utf-8")
|
const contents = readFileSync(originalFilePaths[config], "utf-8")
|
||||||
|
|
||||||
|
// @ts-expect-error It works.
|
||||||
configSwizzleManager.configs[config] = JSON.parse(contents)
|
configSwizzleManager.configs[config] = JSON.parse(contents)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setConfig(config: string, data: unknown) {
|
export function setConfig(config: string, data: unknown) {
|
||||||
|
// @ts-expect-error It works.
|
||||||
configSwizzleManager.configs[config] = data
|
configSwizzleManager.configs[config] = data
|
||||||
}
|
}
|
||||||
|
|
||||||
const getConfigOriginal = configSwizzleManager.getConfig
|
const getConfigOriginal = configSwizzleManager.getConfig
|
||||||
vi.spyOn(configSwizzleManager, "getConfig").mockImplementation(
|
vi.spyOn(configSwizzleManager, "getConfig").mockImplementation(
|
||||||
(config: string, clone: boolean) => {
|
(config: string, clone: boolean) => {
|
||||||
|
// @ts-expect-error It works.
|
||||||
if (!configSwizzleManager.configs[config]) {
|
if (!configSwizzleManager.configs[config]) {
|
||||||
throw `Config '${config}' has not been loaded!`
|
throw `Config '${config}' has not been loaded!`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @ts-expect-error It works.
|
||||||
return getConfigOriginal(config, clone)
|
return getConfigOriginal(config, clone)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -3,12 +3,13 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/ui": "^0.34.6",
|
"@vitest/ui": "^1.2.2",
|
||||||
"vite": "^5.0.12",
|
"vite": "^5.0.12",
|
||||||
"vitest": "^0.34.6"
|
"vitest": "^1.2.2"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test:main": "vitest --run --config vitest.config.ts",
|
"test:main": "vitest --run --config vitest.config.ts",
|
||||||
"test:ui": "vitest --config vitest.config.ts --ui"
|
"test:ui": "vitest --config vitest.config.ts --ui",
|
||||||
|
"typecheck-ws": "tsc --noEmit"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,12 @@
|
|||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { UserProfile } from "../../components/types/types"
|
import { UserProfile } from "../../components/types/types"
|
||||||
import { handleOauthToken, JWT_SECRET } from "../../components/oauthToken"
|
import {
|
||||||
|
error406,
|
||||||
|
handleOAuthToken,
|
||||||
|
JWT_SECRET,
|
||||||
|
OAuthTokenResponse,
|
||||||
|
} from "../../components/oauthToken"
|
||||||
import { sign, verify } from "jsonwebtoken"
|
import { sign, verify } from "jsonwebtoken"
|
||||||
import * as databaseHandler from "../../components/databaseHandler"
|
import * as databaseHandler from "../../components/databaseHandler"
|
||||||
import * as platformEntitlements from "../../components/platformEntitlements"
|
import * as platformEntitlements from "../../components/platformEntitlements"
|
||||||
@ -26,11 +31,9 @@ import axios from "axios"
|
|||||||
import { describe, expect, beforeEach, vi, it } from "vitest"
|
import { describe, expect, beforeEach, vi, it } from "vitest"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getMockCallArgument,
|
|
||||||
getResolvingPromise,
|
getResolvingPromise,
|
||||||
mockRequestWithJwt,
|
mockRequestWithJwt,
|
||||||
mockRequestWithValidJwt,
|
mockRequestWithValidJwt,
|
||||||
mockResponse,
|
|
||||||
} from "../helpers/testHelpers"
|
} from "../helpers/testHelpers"
|
||||||
|
|
||||||
describe("oauthToken", () => {
|
describe("oauthToken", () => {
|
||||||
@ -41,6 +44,7 @@ describe("oauthToken", () => {
|
|||||||
.mockResolvedValue("")
|
.mockResolvedValue("")
|
||||||
const loadUserData = vi
|
const loadUserData = vi
|
||||||
.spyOn(databaseHandler, "loadUserData")
|
.spyOn(databaseHandler, "loadUserData")
|
||||||
|
// @ts-expect-error This is okay.
|
||||||
.mockResolvedValue(undefined)
|
.mockResolvedValue(undefined)
|
||||||
const getUserData = vi
|
const getUserData = vi
|
||||||
.spyOn(databaseHandler, "getUserData")
|
.spyOn(databaseHandler, "getUserData")
|
||||||
@ -65,10 +69,10 @@ describe("oauthToken", () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined
|
return getResolvingPromise({})
|
||||||
})
|
})
|
||||||
|
|
||||||
const request = mockRequestWithJwt()
|
const request = mockRequestWithJwt<never, any>()
|
||||||
request.body = {
|
request.body = {
|
||||||
grant_type: "external_steam",
|
grant_type: "external_steam",
|
||||||
steam_userid: "000000000047",
|
steam_userid: "000000000047",
|
||||||
@ -76,9 +80,7 @@ describe("oauthToken", () => {
|
|||||||
pId: pId,
|
pId: pId,
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = mockResponse()
|
const res = await handleOAuthToken(request)
|
||||||
|
|
||||||
await handleOauthToken(request, response)
|
|
||||||
|
|
||||||
expect(getExternalUserData).toHaveBeenCalledWith(
|
expect(getExternalUserData).toHaveBeenCalledWith(
|
||||||
"000000000047",
|
"000000000047",
|
||||||
@ -88,12 +90,15 @@ describe("oauthToken", () => {
|
|||||||
expect(loadUserData).toHaveBeenCalledWith(pId, "h3")
|
expect(loadUserData).toHaveBeenCalledWith(pId, "h3")
|
||||||
expect(getUserData).toHaveBeenCalledWith(pId, "h3")
|
expect(getUserData).toHaveBeenCalledWith(pId, "h3")
|
||||||
|
|
||||||
const jsonResponse = getMockCallArgument<any>(response.json, 0, 0)
|
const accessToken = verify(
|
||||||
const accessToken = verify(jsonResponse.access_token, JWT_SECRET, {
|
(res as OAuthTokenResponse).access_token,
|
||||||
complete: true,
|
JWT_SECRET,
|
||||||
})
|
{
|
||||||
|
complete: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
expect(jsonResponse.token_type).toBe("bearer")
|
expect((res as OAuthTokenResponse).token_type).toBe("bearer")
|
||||||
expect((accessToken.payload as any).unique_name).toBe(pId)
|
expect((accessToken.payload as any).unique_name).toBe(pId)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -102,7 +107,7 @@ describe("oauthToken", () => {
|
|||||||
["mock"],
|
["mock"],
|
||||||
)
|
)
|
||||||
|
|
||||||
const request = mockRequestWithJwt()
|
const request = mockRequestWithJwt<never, any>()
|
||||||
request.body = {
|
request.body = {
|
||||||
grant_type: "external_epic",
|
grant_type: "external_epic",
|
||||||
epic_userid: "0123456789abcdef0123456789abcdef",
|
epic_userid: "0123456789abcdef0123456789abcdef",
|
||||||
@ -118,9 +123,7 @@ describe("oauthToken", () => {
|
|||||||
pId: pId,
|
pId: pId,
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = mockResponse()
|
const res = await handleOAuthToken(request)
|
||||||
|
|
||||||
await handleOauthToken(request, response)
|
|
||||||
|
|
||||||
expect(getExternalUserData).toHaveBeenCalledWith(
|
expect(getExternalUserData).toHaveBeenCalledWith(
|
||||||
"0123456789abcdef0123456789abcdef",
|
"0123456789abcdef0123456789abcdef",
|
||||||
@ -130,85 +133,84 @@ describe("oauthToken", () => {
|
|||||||
expect(loadUserData).toHaveBeenCalledWith(pId, "h3")
|
expect(loadUserData).toHaveBeenCalledWith(pId, "h3")
|
||||||
expect(getUserData).toHaveBeenCalledWith(pId, "h3")
|
expect(getUserData).toHaveBeenCalledWith(pId, "h3")
|
||||||
|
|
||||||
const jsonResponse = getMockCallArgument<any>(response.json, 0, 0)
|
const accessToken = verify(
|
||||||
const accessToken = verify(jsonResponse.access_token, JWT_SECRET, {
|
(res as OAuthTokenResponse).access_token,
|
||||||
complete: true,
|
JWT_SECRET,
|
||||||
})
|
{
|
||||||
|
complete: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
expect(jsonResponse.token_type).toBe("bearer")
|
expect((res as OAuthTokenResponse).token_type).toBe("bearer")
|
||||||
expect((accessToken.payload as any).unique_name).toBe(pId)
|
expect((accessToken.payload as any).unique_name).toBe(pId)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("refresh_token - missing auth header", async () => {
|
it("refresh_token - missing auth header", async () => {
|
||||||
const request = mockRequestWithJwt()
|
const request = mockRequestWithJwt<never, any>()
|
||||||
|
|
||||||
request.body = {
|
request.body = {
|
||||||
grant_type: "refresh_token",
|
grant_type: "refresh_token",
|
||||||
}
|
}
|
||||||
|
|
||||||
const respose = mockResponse()
|
let error: Error | undefined = undefined
|
||||||
|
|
||||||
let error: Error = undefined
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await handleOauthToken(request, respose)
|
await handleOAuthToken(request)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = e
|
error = e as Error
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(error).toBeInstanceOf(TypeError)
|
expect(error).toBeInstanceOf(TypeError)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("refresh_token - invalid auth header", async () => {
|
it("refresh_token - invalid auth header", async () => {
|
||||||
const request = mockRequestWithJwt()
|
const request = mockRequestWithJwt<never, any>()
|
||||||
request.headers.authorization = "Bearer invalid"
|
request.headers.authorization = "Bearer invalid"
|
||||||
|
|
||||||
request.body = {
|
request.body = {
|
||||||
grant_type: "refresh_token",
|
grant_type: "refresh_token",
|
||||||
}
|
}
|
||||||
|
|
||||||
const respose = mockResponse()
|
let error: Error | undefined = undefined
|
||||||
|
|
||||||
let error: Error = undefined
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await handleOauthToken(request, respose)
|
await handleOAuthToken(request)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = e
|
error = e as Error
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(error).toBeInstanceOf(TypeError)
|
expect(error).toBeInstanceOf(TypeError)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("refresh_token - valid auth header", async () => {
|
it("refresh_token - valid auth header", async () => {
|
||||||
const request = mockRequestWithValidJwt(pId)
|
const request = mockRequestWithValidJwt<never>(pId)
|
||||||
|
|
||||||
// NOTE: We don't care about the actual values
|
// NOTE: We don't care about the actual values
|
||||||
request.body = {
|
request.body = {
|
||||||
grant_type: "refresh_token",
|
grant_type: "refresh_token",
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = mockResponse()
|
const res = await handleOAuthToken(request)
|
||||||
|
|
||||||
await handleOauthToken(request, response)
|
const accessToken = verify(
|
||||||
|
(res as OAuthTokenResponse).access_token,
|
||||||
|
JWT_SECRET,
|
||||||
|
{
|
||||||
|
complete: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
const jsonResponse = getMockCallArgument<any>(response.json, 0, 0)
|
expect((res as OAuthTokenResponse).token_type).toBe("bearer")
|
||||||
const accessToken = verify(jsonResponse.access_token, JWT_SECRET, {
|
|
||||||
complete: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(jsonResponse.token_type).toBe("bearer")
|
|
||||||
expect((accessToken.payload as any).unique_name).toBe(pId)
|
expect((accessToken.payload as any).unique_name).toBe(pId)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("no grant_type", async () => {
|
it("no grant_type", async () => {
|
||||||
const request = mockRequestWithJwt()
|
const request = mockRequestWithJwt<never, any>()
|
||||||
request.body = {}
|
request.body = {}
|
||||||
|
request.query = {} as never
|
||||||
|
|
||||||
const respose = mockResponse()
|
const res = await handleOAuthToken(request)
|
||||||
|
|
||||||
await handleOauthToken(request, respose)
|
expect(res).toEqual(error406)
|
||||||
|
|
||||||
expect(respose.status).toHaveBeenCalledWith(406)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -4,7 +4,8 @@
|
|||||||
// Reset rootDir to default to make rootDirs take effect
|
// Reset rootDir to default to make rootDirs take effect
|
||||||
"rootDir": null,
|
"rootDir": null,
|
||||||
"rootDirs": ["../components", "."],
|
"rootDirs": ["../components", "."],
|
||||||
"types": ["vitest/globals"]
|
"noEmit": true,
|
||||||
|
"emitDeclarationOnly": false
|
||||||
},
|
},
|
||||||
"include": ["../components", "**/*.ts"],
|
"include": ["../components", "**/*.ts"],
|
||||||
"exclude": []
|
"exclude": []
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
"lib": ["ESNext"],
|
"lib": ["ESNext"],
|
||||||
"emitDeclarationOnly": true,
|
"emitDeclarationOnly": true,
|
||||||
"checkJs": false,
|
"checkJs": false,
|
||||||
"strict": false,
|
"strict": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noImplicitOverride": true,
|
"noImplicitOverride": true,
|
||||||
@ -21,7 +21,8 @@
|
|||||||
"rootDir": "components",
|
"rootDir": "components",
|
||||||
"outDir": "./build",
|
"outDir": "./build",
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"stripInternal": true
|
"stripInternal": true,
|
||||||
|
"strictNullChecks": true
|
||||||
},
|
},
|
||||||
"include": ["components"],
|
"include": ["components"],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.6.0",
|
||||||
"clsx": "^2.0.0",
|
"clsx": "^2.1.0",
|
||||||
"immer": "^10.0.3",
|
"immer": "^10.0.3",
|
||||||
"infima": "0.2.0-alpha.38",
|
"infima": "0.2.0-alpha.38",
|
||||||
"json-keys-sort": "^2.1.0",
|
"json-keys-sort": "^2.1.0",
|
||||||
@ -20,10 +20,10 @@
|
|||||||
"typecheck-ws": "tsc"
|
"typecheck-ws": "tsc"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.2.36",
|
"@types/react": "^18.2.48",
|
||||||
"@types/react-dom": "^18.2.14",
|
"@types/react-dom": "^18.2.18",
|
||||||
"rollup-plugin-license": "^3.2.0",
|
"rollup-plugin-license": "^3.2.0",
|
||||||
"typescript": "5.2.2",
|
"typescript": "5.3.3",
|
||||||
"vite": "^5.0.12"
|
"vite": "^5.0.12"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
Loading…
Reference in New Issue
Block a user