mirror of
https://github.com/thepeacockproject/Peacock
synced 2025-01-26 13:02:45 +01:00
Enable strict types mode (#362)
Signed-off-by: Reece Dunham <me@rdil.rocks>
This commit is contained in:
parent
0585b35447
commit
5cc69434c6
@ -44,11 +44,13 @@ module.exports = {
|
||||
],
|
||||
},
|
||||
rules: {
|
||||
"@typescript-eslint/prefer-optional-chain": "warn",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"@typescript-eslint/no-extra-semi": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/require-await": "warn",
|
||||
"@typescript-eslint/prefer-ts-expect-error": "error",
|
||||
"no-nested-ternary": "warn",
|
||||
eqeqeq: "error",
|
||||
"no-duplicate-imports": "warn",
|
||||
"promise/always-return": "error",
|
||||
|
@ -33,6 +33,7 @@ const legacyContractRouter = Router()
|
||||
legacyContractRouter.post(
|
||||
"/GetForPlay",
|
||||
jsonMiddleware(),
|
||||
// @ts-expect-error Has jwt props.
|
||||
(req: RequestWithJwt, res) => {
|
||||
if (!uuidRegex.test(req.body.id)) {
|
||||
res.status(400).end()
|
||||
@ -130,6 +131,7 @@ legacyContractRouter.post(
|
||||
legacyContractRouter.post(
|
||||
"/Start",
|
||||
jsonMiddleware(),
|
||||
// @ts-expect-error Has jwt props.
|
||||
(req: RequestWithJwt, res) => {
|
||||
if (req.body.profileId !== req.jwt.unique_name) {
|
||||
res.status(400).end() // requested for different user id
|
||||
|
@ -24,37 +24,9 @@ import { getParentLocationByName } from "../contracts/dataGen"
|
||||
|
||||
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(
|
||||
"/MasteryLocation",
|
||||
// @ts-expect-error Has jwt props.
|
||||
(req: RequestWithJwt<{ locationId: string; difficulty: string }>, res) => {
|
||||
const masteryData =
|
||||
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(
|
||||
"/ChallengesService/GetActiveChallenges",
|
||||
jsonMiddleware(),
|
||||
// @ts-expect-error Has jwt props.
|
||||
(req: RequestWithJwt, res) => {
|
||||
if (!uuidRegex.test(req.body.contractId)) {
|
||||
return res.status(404).send("invalid contract")
|
||||
@ -93,7 +94,13 @@ legacyProfileRouter.post(
|
||||
legacyProfileRouter.post(
|
||||
"/ChallengesService/GetProgression",
|
||||
jsonMiddleware(),
|
||||
// @ts-expect-error Has jwt props.
|
||||
(req: RequestWithJwt<never, LegacyGetProgressionBody>, res) => {
|
||||
if (!Array.isArray(req.body.challengeids)) {
|
||||
res.status(400).send("invalid body")
|
||||
return
|
||||
}
|
||||
|
||||
const legacyGlobalChallenges = getConfig<CompiledChallengeIngameData[]>(
|
||||
"LegacyGlobalChallenges",
|
||||
false,
|
||||
@ -114,10 +121,11 @@ legacyProfileRouter.post(
|
||||
MustBeSaved: false,
|
||||
}))
|
||||
|
||||
/*
|
||||
for (const challengeId of req.body.challengeids) {
|
||||
const challenge =
|
||||
controller.challengeService.getChallengeById(challengeId)
|
||||
const challenge = controller.challengeService.getChallengeById(
|
||||
challengeId,
|
||||
"h1",
|
||||
)
|
||||
|
||||
if (!challenge) {
|
||||
log(
|
||||
@ -128,7 +136,7 @@ legacyProfileRouter.post(
|
||||
}
|
||||
|
||||
const progression =
|
||||
controller.challengeService.getChallengeProgression(
|
||||
controller.challengeService.getPersistentChallengeProgression(
|
||||
req.jwt.unique_name,
|
||||
challengeId,
|
||||
req.gameVersion,
|
||||
@ -138,19 +146,16 @@ legacyProfileRouter.post(
|
||||
ChallengeId: challengeId,
|
||||
ProfileId: req.jwt.unique_name,
|
||||
Completed: progression.Completed,
|
||||
Ticked: progression.Ticked,
|
||||
State: progression.State,
|
||||
ETag: `W/"datetime'${encodeURIComponent(
|
||||
new Date().toISOString(),
|
||||
)}'"`,
|
||||
CompletedAt: progression.CompletedAt,
|
||||
MustBeSaved: false,
|
||||
MustBeSaved: progression.MustBeSaved,
|
||||
})
|
||||
}
|
||||
*/
|
||||
// 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)
|
||||
// TODO: HELP! Please DM rdil if you see this
|
||||
|
||||
res.json(challenges)
|
||||
},
|
||||
|
@ -18,7 +18,6 @@
|
||||
|
||||
import {
|
||||
ChallengeProgressionData,
|
||||
CompiledChallengeRewardData,
|
||||
CompiledChallengeRuntimeData,
|
||||
InclusionData,
|
||||
MissionManifest,
|
||||
@ -28,19 +27,6 @@ import { SavedChallengeGroup } from "../types/challenges"
|
||||
import { controller } from "../controller"
|
||||
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(
|
||||
challenge: RegistryChallenge,
|
||||
progression: ChallengeProgressionData,
|
||||
@ -106,8 +92,8 @@ export type ChallengeFilterOptions =
|
||||
* @returns A boolean as the result.
|
||||
*/
|
||||
export function inclusionDataCheck(
|
||||
incData: InclusionData,
|
||||
contract: MissionManifest,
|
||||
incData: InclusionData | undefined,
|
||||
contract: MissionManifest | undefined,
|
||||
): boolean {
|
||||
if (!incData) return true
|
||||
if (!contract) return false
|
||||
@ -174,9 +160,9 @@ function isChallengeInContract(
|
||||
: {
|
||||
...challenge.InclusionData,
|
||||
ContractTypes:
|
||||
challenge.InclusionData.ContractTypes.filter(
|
||||
challenge.InclusionData?.ContractTypes?.filter(
|
||||
(type) => type !== "tutorial",
|
||||
),
|
||||
) || [],
|
||||
},
|
||||
contract,
|
||||
)
|
||||
@ -184,14 +170,15 @@ function isChallengeInContract(
|
||||
|
||||
// Is this for the current contract or group contract?
|
||||
const isForContract = (challenge.InclusionData?.ContractIds || []).includes(
|
||||
contract.Metadata.Id,
|
||||
contract?.Metadata.Id || "",
|
||||
)
|
||||
|
||||
// Is this for the current contract type?
|
||||
// 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 = (
|
||||
challenge.InclusionData?.ContractTypes || []
|
||||
).includes(controller.resolveContract(contractId).Metadata.Type)
|
||||
).includes(controller.resolveContract(contractId)!.Metadata.Type)
|
||||
|
||||
// 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.
|
||||
@ -287,7 +274,7 @@ export function filterChallenge(
|
||||
*/
|
||||
export function mergeSavedChallengeGroups(
|
||||
g1: SavedChallengeGroup,
|
||||
g2: SavedChallengeGroup,
|
||||
g2?: SavedChallengeGroup,
|
||||
): SavedChallengeGroup {
|
||||
return {
|
||||
...g1,
|
||||
|
@ -49,7 +49,7 @@ import {
|
||||
HandleEventOptions,
|
||||
} from "@peacockproject/statemachine-parser"
|
||||
import { ChallengeContext, SavedChallengeGroup } from "../types/challenges"
|
||||
import { fastClone, isSniperLocation } from "../utils"
|
||||
import { fastClone, gameDifficulty, isSniperLocation } from "../utils"
|
||||
import {
|
||||
ChallengeFilterOptions,
|
||||
ChallengeFilterType,
|
||||
@ -88,12 +88,13 @@ export abstract class ChallengeRegistry {
|
||||
* @Key2 The challenge Id.
|
||||
* @value A `RegistryChallenge` object.
|
||||
*/
|
||||
protected challenges: Map<GameVersion, Map<string, RegistryChallenge>> =
|
||||
new Map([
|
||||
["h1", new Map()],
|
||||
["h2", new Map()],
|
||||
["h3", new Map()],
|
||||
])
|
||||
protected challenges: Record<GameVersion, Map<string, RegistryChallenge>> =
|
||||
{
|
||||
h1: new Map(),
|
||||
h2: new Map(),
|
||||
h3: new Map(),
|
||||
scpc: new Map(),
|
||||
}
|
||||
|
||||
/**
|
||||
* @Key1 Game version.
|
||||
@ -101,14 +102,15 @@ export abstract class ChallengeRegistry {
|
||||
* @Key3 The group Id.
|
||||
* @Value A `SavedChallengeGroup` object.
|
||||
*/
|
||||
protected groups: Map<
|
||||
protected groups: Record<
|
||||
GameVersion,
|
||||
Map<string, Map<string, SavedChallengeGroup>>
|
||||
> = new Map([
|
||||
["h1", new Map()],
|
||||
["h2", new Map()],
|
||||
["h3", new Map()],
|
||||
])
|
||||
> = {
|
||||
h1: new Map(),
|
||||
h2: new Map(),
|
||||
h3: new Map(),
|
||||
scpc: new Map(),
|
||||
}
|
||||
|
||||
/**
|
||||
* @Key1 Game version.
|
||||
@ -116,28 +118,30 @@ export abstract class ChallengeRegistry {
|
||||
* @Key3 The group Id.
|
||||
* @Value A `Set` of challenge Ids.
|
||||
*/
|
||||
protected groupContents: Map<
|
||||
protected groupContents: Record<
|
||||
GameVersion,
|
||||
Map<string, Map<string, Set<string>>>
|
||||
> = new Map([
|
||||
["h1", new Map()],
|
||||
["h2", new Map()],
|
||||
["h3", new Map()],
|
||||
])
|
||||
> = {
|
||||
h1: new Map(),
|
||||
h2: new Map(),
|
||||
h3: new Map(),
|
||||
scpc: new Map(),
|
||||
}
|
||||
|
||||
/**
|
||||
* @Key1 Game version.
|
||||
* @Key2 The challenge Id.
|
||||
* @Value An `array` of challenge Ids that Key2 depends on.
|
||||
*/
|
||||
protected readonly _dependencyTree: Map<
|
||||
protected readonly _dependencyTree: Record<
|
||||
GameVersion,
|
||||
Map<string, readonly string[]>
|
||||
> = new Map([
|
||||
["h1", new Map()],
|
||||
["h2", new Map()],
|
||||
["h3", new Map()],
|
||||
])
|
||||
> = {
|
||||
h1: new Map(),
|
||||
h2: new Map(),
|
||||
h3: new Map(),
|
||||
scpc: new Map(),
|
||||
}
|
||||
|
||||
protected constructor(protected readonly controller: Controller) {}
|
||||
|
||||
@ -147,13 +151,9 @@ export abstract class ChallengeRegistry {
|
||||
location: string,
|
||||
gameVersion: GameVersion,
|
||||
): void {
|
||||
if (!this.groupContents.has(gameVersion)) {
|
||||
return
|
||||
}
|
||||
|
||||
const gameChallenges = this.groupContents.get(gameVersion)
|
||||
const gameChallenges = this.groupContents[gameVersion]
|
||||
challenge.inGroup = groupId
|
||||
this.challenges.get(gameVersion)?.set(challenge.Id, challenge)
|
||||
this.challenges[gameVersion].set(challenge.Id, challenge)
|
||||
|
||||
if (!gameChallenges.has(location)) {
|
||||
gameChallenges.set(location, new Map())
|
||||
@ -176,34 +176,33 @@ export abstract class ChallengeRegistry {
|
||||
location: string,
|
||||
gameVersion: GameVersion,
|
||||
): void {
|
||||
if (!this.groups.has(gameVersion)) {
|
||||
return
|
||||
}
|
||||
|
||||
const gameGroups = this.groups.get(gameVersion)
|
||||
const gameGroups = this.groups[gameVersion]
|
||||
|
||||
if (!gameGroups.has(location)) {
|
||||
gameGroups.set(location, new Map())
|
||||
}
|
||||
|
||||
gameGroups.get(location).set(group.CategoryId, group)
|
||||
gameGroups.get(location)?.set(group.CategoryId, group)
|
||||
}
|
||||
|
||||
getChallengeById(
|
||||
challengeId: string,
|
||||
gameVersion: GameVersion,
|
||||
): 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) {
|
||||
return [...this.challenges.get(gameVersion).values()].reduce(
|
||||
(acc, challenge) => {
|
||||
getChallengesUnlockables(gameVersion: GameVersion): Record<string, string> {
|
||||
return [...this.challenges[gameVersion].values()].reduce(
|
||||
(acc: Record<string, string>, challenge) => {
|
||||
if (challenge?.Drops?.length) {
|
||||
challenge.Drops.forEach(
|
||||
(dropId) => (acc[dropId] = challenge.Id),
|
||||
@ -228,15 +227,18 @@ export abstract class ChallengeRegistry {
|
||||
location: string,
|
||||
gameVersion: GameVersion,
|
||||
): SavedChallengeGroup | undefined {
|
||||
if (!this.groups.has(gameVersion)) {
|
||||
return undefined
|
||||
}
|
||||
const gameGroups = this.groups[gameVersion]
|
||||
|
||||
const gameGroups = this.groups.get(gameVersion)
|
||||
const mainGroup = gameGroups.get(location)?.get(groupId)
|
||||
|
||||
if (groupId === "feats" && gameVersion !== "h3") {
|
||||
if (!mainGroup) {
|
||||
// emergency bailout - shouldn't happen in practice
|
||||
return undefined
|
||||
}
|
||||
|
||||
return mergeSavedChallengeGroups(
|
||||
gameGroups.get(location)?.get(groupId),
|
||||
mainGroup,
|
||||
gameGroups.get("GLOBAL_ESCALATION_CHALLENGES")?.get(groupId),
|
||||
)
|
||||
}
|
||||
@ -255,8 +257,13 @@ export abstract class ChallengeRegistry {
|
||||
|
||||
// Included by default. Filtered later.
|
||||
if (groupId === "classic" && location !== "GLOBAL_CLASSIC_CHALLENGES") {
|
||||
if (!mainGroup) {
|
||||
// emergency bailout - shouldn't happen in practice
|
||||
return undefined
|
||||
}
|
||||
|
||||
return mergeSavedChallengeGroups(
|
||||
gameGroups.get(location)?.get(groupId),
|
||||
mainGroup,
|
||||
gameGroups.get("GLOBAL_CLASSIC_CHALLENGES")?.get(groupId),
|
||||
)
|
||||
}
|
||||
@ -265,13 +272,18 @@ export abstract class ChallengeRegistry {
|
||||
groupId === "elusive" &&
|
||||
location !== "GLOBAL_ELUSIVES_CHALLENGES"
|
||||
) {
|
||||
if (!mainGroup) {
|
||||
// emergency bailout - shouldn't happen in practice
|
||||
return undefined
|
||||
}
|
||||
|
||||
return mergeSavedChallengeGroups(
|
||||
gameGroups.get(location)?.get(groupId),
|
||||
mainGroup,
|
||||
gameGroups.get("GLOBAL_ELUSIVES_CHALLENGES")?.get(groupId),
|
||||
)
|
||||
}
|
||||
|
||||
return gameGroups.get(location)?.get(groupId)
|
||||
return mainGroup
|
||||
}
|
||||
|
||||
public getGroupContentByIdLoc(
|
||||
@ -279,11 +291,7 @@ export abstract class ChallengeRegistry {
|
||||
location: string,
|
||||
gameVersion: GameVersion,
|
||||
): Set<string> | undefined {
|
||||
if (!this.groupContents.has(gameVersion)) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const gameChalGC = this.groupContents.get(gameVersion)
|
||||
const gameChalGC = this.groupContents[gameVersion]
|
||||
|
||||
if (groupId === "feats" && gameVersion !== "h3") {
|
||||
return new Set([
|
||||
@ -334,9 +342,16 @@ export abstract class ChallengeRegistry {
|
||||
challengeId: string,
|
||||
gameVersion: GameVersion,
|
||||
): 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(
|
||||
challenge: RegistryChallenge,
|
||||
gameVersion: GameVersion,
|
||||
@ -344,9 +359,10 @@ export abstract class ChallengeRegistry {
|
||||
const ctxListeners = ChallengeRegistry._parseContextListeners(challenge)
|
||||
|
||||
if (ctxListeners.challengeTreeIds.length > 0) {
|
||||
this._dependencyTree
|
||||
.get(gameVersion)
|
||||
?.set(challenge.Id, ctxListeners.challengeTreeIds)
|
||||
this._dependencyTree[gameVersion].set(
|
||||
challenge.Id,
|
||||
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
|
||||
* i.e. challenges with scopes being "profile" or "hit".
|
||||
* Check if the challenge needs to be saved in the user's progression data.
|
||||
* Challenges with scopes "profile" or "hit".
|
||||
*
|
||||
* @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 {
|
||||
return (
|
||||
@ -512,7 +529,7 @@ export class ChallengeService extends ChallengeRegistry {
|
||||
challenges: [string, RegistryChallenge[]][],
|
||||
gameVersion: GameVersion,
|
||||
) {
|
||||
const groups = this.groups.get(gameVersion).get(location)?.keys() ?? []
|
||||
const groups = this.groups[gameVersion].get(location)?.keys() ?? []
|
||||
|
||||
for (const groupId of groups) {
|
||||
// if this is the global group, skip it.
|
||||
@ -543,9 +560,9 @@ export class ChallengeService extends ChallengeRegistry {
|
||||
return challenge
|
||||
}
|
||||
|
||||
return filterChallenge(filter, challenge)
|
||||
? challenge
|
||||
: undefined
|
||||
const res = filterChallenge(filter, challenge)
|
||||
|
||||
return res ? challenge : undefined
|
||||
})
|
||||
.filter(Boolean) as RegistryChallenge[]
|
||||
|
||||
@ -570,10 +587,6 @@ export class ChallengeService extends ChallengeRegistry {
|
||||
): GroupIndexedChallengeLists {
|
||||
let challenges: [string, RegistryChallenge[]][] = []
|
||||
|
||||
if (!this.groups.has(gameVersion)) {
|
||||
return {}
|
||||
}
|
||||
|
||||
this.getGroupedChallengesByLoc(
|
||||
filter,
|
||||
location,
|
||||
@ -622,25 +635,36 @@ export class ChallengeService extends ChallengeRegistry {
|
||||
difficulty = 4,
|
||||
): GroupIndexedChallengeLists {
|
||||
const userData = getUserData(userId, gameVersion)
|
||||
const contract = this.controller.resolveContract(contractId, true)
|
||||
const contractGroup = this.controller.resolveContract(contractId, true)
|
||||
|
||||
const level =
|
||||
contract.Metadata.Type === "arcade" &&
|
||||
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)
|
||||
if (!contractGroup) {
|
||||
return {}
|
||||
}
|
||||
|
||||
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(
|
||||
level,
|
||||
contract,
|
||||
gameVersion,
|
||||
)?.Properties.ParentLocation
|
||||
|
||||
@ -651,12 +675,12 @@ export class ChallengeService extends ChallengeRegistry {
|
||||
type: ChallengeFilterType.Contract,
|
||||
contractId: contractId,
|
||||
locationId:
|
||||
contract.Metadata.Id ===
|
||||
contractGroup.Metadata.Id ===
|
||||
"aee6a16f-6525-4d63-a37f-225e293c6118" &&
|
||||
gameVersion !== "h1"
|
||||
? "LOCATION_ICA_FACILITY_SHIP"
|
||||
: level.Metadata.Location,
|
||||
isFeatured: contract.Metadata.Type === "featured",
|
||||
: contract.Metadata.Location,
|
||||
isFeatured: contractGroup.Metadata.Type === "featured",
|
||||
difficulty,
|
||||
},
|
||||
levelParentLocation,
|
||||
@ -676,17 +700,23 @@ export class ChallengeService extends ChallengeRegistry {
|
||||
const parent = locations.children[child].Properties.ParentLocation
|
||||
|
||||
let contracts = isSniperLocation(child)
|
||||
? this.controller.missionsInLocations.sniper[child]
|
||||
: (this.controller.missionsInLocations[child] ?? [])
|
||||
? // @ts-expect-error This is fine - we know it will be there
|
||||
this.controller.missionsInLocations.sniper[child]
|
||||
: // @ts-expect-error This is fine - we know it will be there
|
||||
(this.controller.missionsInLocations[child] ?? [])
|
||||
.concat(
|
||||
// @ts-expect-error This is fine - we know it will be there
|
||||
this.controller.missionsInLocations.escalations[child],
|
||||
)
|
||||
// @ts-expect-error This is fine - we know it will be there
|
||||
.concat(this.controller.missionsInLocations.arcade[child])
|
||||
|
||||
if (!contracts) {
|
||||
contracts = []
|
||||
}
|
||||
|
||||
assert.ok(parent, "expected parent location")
|
||||
|
||||
return this.getGroupedChallengeLists(
|
||||
{
|
||||
type: ChallengeFilterType.Contracts,
|
||||
@ -712,26 +742,31 @@ export class ChallengeService extends ChallengeRegistry {
|
||||
session.difficulty,
|
||||
)
|
||||
|
||||
if (contractJson.Metadata.Type === "evergreen") {
|
||||
if (contractJson?.Metadata.Type === "evergreen") {
|
||||
session.evergreen = {
|
||||
payout: 0,
|
||||
scoringScreenEndState: undefined,
|
||||
scoringScreenEndState: null,
|
||||
failed: false,
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Add this to getChallengesForContract without breaking the rest of Peacock?
|
||||
challengeGroups["global"] = this.getGroupByIdLoc(
|
||||
"global",
|
||||
"GLOBAL",
|
||||
session.gameVersion,
|
||||
).Challenges.filter((val) =>
|
||||
inclusionDataCheck(val.InclusionData, contractJson),
|
||||
)
|
||||
challengeGroups["global"] =
|
||||
this.getGroupByIdLoc(
|
||||
"global",
|
||||
"GLOBAL",
|
||||
session.gameVersion,
|
||||
)?.Challenges.filter((val) =>
|
||||
inclusionDataCheck(val.InclusionData, contractJson),
|
||||
) || []
|
||||
|
||||
const profile = getUserData(session.userId, session.gameVersion)
|
||||
|
||||
for (const group of Object.keys(challengeGroups)) {
|
||||
if (!challengeContexts) {
|
||||
break
|
||||
}
|
||||
|
||||
for (const challenge of challengeGroups[group]) {
|
||||
challengeContexts[challenge.Id] = {
|
||||
context: undefined,
|
||||
@ -892,7 +927,7 @@ export class ChallengeService extends ChallengeRegistry {
|
||||
contractId: string,
|
||||
gameVersion: GameVersion,
|
||||
userId: string,
|
||||
difficulty = 4,
|
||||
difficulty = gameDifficulty.master,
|
||||
): CompiledChallengeTreeCategory[] {
|
||||
const userData = getUserData(userId, gameVersion)
|
||||
|
||||
@ -902,18 +937,37 @@ export class ChallengeService extends ChallengeRegistry {
|
||||
return []
|
||||
}
|
||||
|
||||
const levelData =
|
||||
let levelData: MissionManifest | undefined
|
||||
|
||||
if (
|
||||
contractData.Metadata.Type === "arcade" &&
|
||||
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
|
||||
this.controller.resolveContract(
|
||||
contractData.Metadata.GroupDefinition.Order[
|
||||
getUserEscalationProgress(userData, contractId) - 1
|
||||
],
|
||||
false,
|
||||
)
|
||||
: this.controller.resolveContract(contractId, false)
|
||||
) {
|
||||
const order =
|
||||
contractData.Metadata.GroupDefinition?.Order[
|
||||
getUserEscalationProgress(userData, contractId) - 1
|
||||
]
|
||||
|
||||
if (!order) {
|
||||
log(
|
||||
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)
|
||||
|
||||
@ -1116,80 +1170,89 @@ export class ChallengeService extends ChallengeRegistry {
|
||||
gameVersion,
|
||||
)
|
||||
|
||||
return entries.map(([groupId, challenges], index) => {
|
||||
const groupData = this.getGroupByIdLoc(
|
||||
groupId,
|
||||
location.Properties.ParentLocation ?? location.Id,
|
||||
gameVersion,
|
||||
)
|
||||
const challengeProgressionData = challenges.map((challengeData) =>
|
||||
this.getPersistentChallengeProgression(
|
||||
userId,
|
||||
challengeData.Id,
|
||||
return entries
|
||||
.map(([groupId, challenges], index) => {
|
||||
const groupData = this.getGroupByIdLoc(
|
||||
groupId,
|
||||
location.Properties.ParentLocation ?? location.Id,
|
||||
gameVersion,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
const lastGroup = this.getGroupByIdLoc(
|
||||
Object.keys(challengeLists)[index - 1],
|
||||
location.Properties.ParentLocation ?? location.Id,
|
||||
gameVersion,
|
||||
)
|
||||
const nextGroup = this.getGroupByIdLoc(
|
||||
Object.keys(challengeLists)[index + 1],
|
||||
location.Properties.ParentLocation ?? location.Id,
|
||||
gameVersion,
|
||||
)
|
||||
if (!groupData) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return {
|
||||
Name: groupData?.Name,
|
||||
Description: groupData?.Description,
|
||||
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,
|
||||
const challengeProgressionData = challenges.map(
|
||||
(challengeData) =>
|
||||
this.getPersistentChallengeProgression(
|
||||
userId,
|
||||
challengeData.Id,
|
||||
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,
|
||||
)
|
||||
|
||||
const lastGroup = this.getGroupByIdLoc(
|
||||
Object.keys(challengeLists)[index - 1],
|
||||
location.Properties.ParentLocation ?? location.Id,
|
||||
gameVersion,
|
||||
)
|
||||
const nextGroup = this.getGroupByIdLoc(
|
||||
Object.keys(challengeLists)[index + 1],
|
||||
location.Properties.ParentLocation ?? location.Id,
|
||||
gameVersion,
|
||||
)
|
||||
|
||||
return {
|
||||
Name: groupData.Name,
|
||||
Description: groupData.Description,
|
||||
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,
|
||||
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(
|
||||
@ -1201,7 +1264,7 @@ export class ChallengeService extends ChallengeRegistry {
|
||||
): CompiledChallengeTreeData {
|
||||
const drops = challenge.Drops.map((e) =>
|
||||
getUnlockableById(e, gameVersion),
|
||||
).filter(Boolean)
|
||||
).filter(Boolean) as Unlockable[]
|
||||
|
||||
if (drops.length !== challenge.Drops.length) {
|
||||
log(
|
||||
@ -1255,7 +1318,7 @@ export class ChallengeService extends ChallengeRegistry {
|
||||
gameVersion: GameVersion,
|
||||
userId: string,
|
||||
): CompiledChallengeTreeData {
|
||||
let contract: MissionManifest | null
|
||||
let contract: MissionManifest | undefined
|
||||
|
||||
if (challenge.Type === "contract") {
|
||||
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
|
||||
const meta = contract?.Metadata
|
||||
contract = !contract
|
||||
? null
|
||||
: {
|
||||
// The null is for escalations as we cannot currently get groups
|
||||
Data: {
|
||||
Bricks: contract.Data.Bricks,
|
||||
DevOnlyBricks: null,
|
||||
GameChangerReferences:
|
||||
contract.Data.GameChangerReferences || [],
|
||||
GameChangers: contract.Data.GameChangers || [],
|
||||
GameDifficulties:
|
||||
contract.Data.GameDifficulties || [],
|
||||
},
|
||||
Metadata: {
|
||||
CreationTimestamp: null,
|
||||
CreatorUserId: meta.CreatorUserId,
|
||||
DebriefingVideo: meta.DebriefingVideo || "",
|
||||
Description: meta.Description,
|
||||
Drops: meta.Drops || null,
|
||||
Entitlements: meta.Entitlements || [],
|
||||
GroupTitle: meta.GroupTitle || "",
|
||||
Id: meta.Id,
|
||||
IsPublished: meta.IsPublished || true,
|
||||
LastUpdate: null,
|
||||
Location: meta.Location,
|
||||
PublicId: meta.PublicId || "",
|
||||
ScenePath: meta.ScenePath,
|
||||
Subtype: meta.Subtype || "",
|
||||
TileImage: meta.TileImage,
|
||||
Title: meta.Title,
|
||||
Type: meta.Type,
|
||||
},
|
||||
}
|
||||
contract =
|
||||
!contract || !meta
|
||||
? undefined
|
||||
: {
|
||||
// The null is for escalations as we cannot currently get groups
|
||||
Data: {
|
||||
Bricks: contract.Data.Bricks,
|
||||
DevOnlyBricks: null,
|
||||
GameChangerReferences:
|
||||
contract.Data.GameChangerReferences || [],
|
||||
GameChangers: contract.Data.GameChangers || [],
|
||||
GameDifficulties:
|
||||
contract.Data.GameDifficulties || [],
|
||||
},
|
||||
Metadata: {
|
||||
CreationTimestamp: null,
|
||||
CreatorUserId: meta.CreatorUserId,
|
||||
DebriefingVideo: meta.DebriefingVideo || "",
|
||||
Description: meta.Description,
|
||||
Drops: meta.Drops || null,
|
||||
Entitlements: meta.Entitlements || [],
|
||||
GroupTitle: meta.GroupTitle || "",
|
||||
Id: meta.Id,
|
||||
IsPublished: meta.IsPublished || true,
|
||||
LastUpdate: null,
|
||||
Location: meta.Location,
|
||||
PublicId: meta.PublicId || "",
|
||||
ScenePath: meta.ScenePath,
|
||||
Subtype: meta.Subtype || "",
|
||||
TileImage: meta.TileImage,
|
||||
Title: meta.Title,
|
||||
Type: meta.Type,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
@ -1375,12 +1439,11 @@ export class ChallengeService extends ChallengeRegistry {
|
||||
|
||||
if (challengeId === parentId) {
|
||||
// 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
|
||||
// the max call stack size
|
||||
// so we need to skip it, or we'll get an infinite loop
|
||||
return
|
||||
}
|
||||
|
||||
const allDeps = this._dependencyTree.get(gameVersion)?.get(challengeId)
|
||||
const allDeps = this._dependencyTree[gameVersion].get(challengeId)
|
||||
assert.ok(allDeps, `No dep tree for ${challengeId}`)
|
||||
|
||||
if (!allDeps.includes(parentId)) {
|
||||
@ -1393,10 +1456,17 @@ export class ChallengeService extends ChallengeRegistry {
|
||||
|
||||
// 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 } =
|
||||
ChallengeService._parseContextListeners(dep)
|
||||
ChallengeService._parseContextListeners(challengeDependency)
|
||||
|
||||
// First check for challengecounter, then challengetree
|
||||
const completed =
|
||||
@ -1408,11 +1478,15 @@ export class ChallengeService extends ChallengeRegistry {
|
||||
return
|
||||
}
|
||||
|
||||
const challenge = this.getChallengeById(challengeId, gameVersion)
|
||||
|
||||
assert.ok(challenge, `No challenge for ${challengeId}`)
|
||||
|
||||
this.onChallengeCompleted(
|
||||
session,
|
||||
userData.Id,
|
||||
gameVersion,
|
||||
this.getChallengeById(challengeId, gameVersion),
|
||||
challenge,
|
||||
parentId,
|
||||
)
|
||||
}
|
||||
@ -1464,18 +1538,20 @@ export class ChallengeService extends ChallengeRegistry {
|
||||
true
|
||||
}
|
||||
|
||||
// Always count the number of completions
|
||||
if (session.challengeContexts[challenge.Id]) {
|
||||
session.challengeContexts[challenge.Id].timesCompleted++
|
||||
}
|
||||
if (session.challengeContexts) {
|
||||
// Always count the number of completions
|
||||
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.
|
||||
// TODO: Figure out what Base/Delta means. For now if Repeatable is set, we restart the challenge.
|
||||
if (
|
||||
challenge.Definition.Repeatable &&
|
||||
session.challengeContexts[challenge.Id]
|
||||
) {
|
||||
session.challengeContexts[challenge.Id].state = "Start"
|
||||
// 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.
|
||||
if (
|
||||
challenge.Definition.Repeatable &&
|
||||
session.challengeContexts[challenge.Id]
|
||||
) {
|
||||
session.challengeContexts[challenge.Id].state = "Start"
|
||||
}
|
||||
}
|
||||
|
||||
controller.progressionService.grantProfileProgression(
|
||||
@ -1490,7 +1566,7 @@ export class ChallengeService extends ChallengeRegistry {
|
||||
this.hooks.onChallengeCompleted.call(userId, challenge, gameVersion)
|
||||
|
||||
// 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(
|
||||
session,
|
||||
depTreeId,
|
||||
|
@ -21,17 +21,22 @@ import {
|
||||
getSubLocationByName,
|
||||
} from "../contracts/dataGen"
|
||||
import { log, LogLevel } from "../loggingInterop"
|
||||
import { getConfig, getVersionedConfig } from "../configSwizzleManager"
|
||||
import { getVersionedConfig } from "../configSwizzleManager"
|
||||
import { getUserData } from "../databaseHandler"
|
||||
import {
|
||||
LocationMasteryData,
|
||||
MasteryData,
|
||||
MasteryDataTemplate,
|
||||
MasteryDrop,
|
||||
MasteryPackage,
|
||||
MasteryPackageDrop,
|
||||
UnlockableMasteryData,
|
||||
} from "../types/mastery"
|
||||
import { CompletionData, GameVersion, Unlockable } from "../types/types"
|
||||
import {
|
||||
CompletionData,
|
||||
GameVersion,
|
||||
ProgressionData,
|
||||
Unlockable,
|
||||
} from "../types/types"
|
||||
import {
|
||||
clampValue,
|
||||
DEFAULT_MASTERY_MAXLEVEL,
|
||||
@ -42,6 +47,7 @@ import {
|
||||
} from "../utils"
|
||||
|
||||
import { getUnlockablesById } from "../inventory"
|
||||
import assert from "assert"
|
||||
|
||||
export class MasteryService {
|
||||
/**
|
||||
@ -150,22 +156,16 @@ export class MasteryService {
|
||||
)[0]
|
||||
}
|
||||
|
||||
// TODO: what do we want to do with this? We should prob remove the template part
|
||||
// to make this like the other routes, and more testable.
|
||||
getMasteryDataForLocation(
|
||||
locationId: string,
|
||||
gameVersion: GameVersion,
|
||||
userId: string,
|
||||
): MasteryDataTemplate {
|
||||
const location: Unlockable =
|
||||
): LocationMasteryData {
|
||||
const location =
|
||||
getSubLocationByName(locationId, gameVersion) ??
|
||||
getParentLocationByName(locationId, gameVersion)
|
||||
|
||||
const masteryDataTemplate: MasteryDataTemplate =
|
||||
getConfig<MasteryDataTemplate>(
|
||||
"MasteryDataForLocationTemplate",
|
||||
false,
|
||||
)
|
||||
assert.ok(location, "cannot get mastery data for unknown location")
|
||||
|
||||
const masteryData = this.getMasteryData(
|
||||
location.Properties.ParentLocation ?? location.Id,
|
||||
@ -174,11 +174,8 @@ export class MasteryService {
|
||||
)
|
||||
|
||||
return {
|
||||
template: masteryDataTemplate,
|
||||
data: {
|
||||
Location: location,
|
||||
MasteryData: masteryData,
|
||||
},
|
||||
Location: location,
|
||||
MasteryData: masteryData,
|
||||
}
|
||||
}
|
||||
|
||||
@ -202,19 +199,12 @@ export class MasteryService {
|
||||
// Get the user profile
|
||||
const userProfile = getUserData(userId, gameVersion)
|
||||
|
||||
// @since v7.0.0 this has been commented out as the default profile should
|
||||
// have all the required properties - AF
|
||||
/* userProfile.Extensions.progression.Locations[locationParentId] ??= {
|
||||
Xp: 0,
|
||||
Level: 1,
|
||||
PreviouslySeenXp: 0,
|
||||
} */
|
||||
const parent =
|
||||
userProfile.Extensions.progression.Locations[locationParentId]
|
||||
|
||||
const completionData = subPackageId
|
||||
? userProfile.Extensions.progression.Locations[locationParentId][
|
||||
subPackageId
|
||||
]
|
||||
: userProfile.Extensions.progression.Locations[locationParentId]
|
||||
const completionData: ProgressionData = subPackageId
|
||||
? (parent[subPackageId as keyof typeof parent] as ProgressionData)
|
||||
: (parent as ProgressionData)
|
||||
|
||||
const nextLevel: number = clampValue(
|
||||
completionData.Level + 1,
|
||||
@ -279,7 +269,7 @@ export class MasteryService {
|
||||
"SniperUnlockables",
|
||||
gameVersion,
|
||||
false,
|
||||
).find((unlockable) => unlockable.Id === subPackageId).Properties
|
||||
).find((unlockable) => unlockable.Id === subPackageId)?.Properties
|
||||
.Name
|
||||
: undefined
|
||||
|
||||
@ -297,11 +287,11 @@ export class MasteryService {
|
||||
: xpRequiredForLevel,
|
||||
subPackageId,
|
||||
),
|
||||
Id: isSniper ? subPackageId : masteryPkg.LocationId,
|
||||
Id: isSniper ? subPackageId! : masteryPkg.LocationId,
|
||||
SubLocationId: isSniper ? "" : subLocationId,
|
||||
HideProgression: masteryPkg.HideProgression || false,
|
||||
IsLocationProgression: !isSniper,
|
||||
Name: name,
|
||||
Name: name!,
|
||||
}
|
||||
}
|
||||
|
||||
@ -347,7 +337,7 @@ export class MasteryService {
|
||||
subPackageId?: string,
|
||||
): MasteryData[] {
|
||||
// Get the mastery data
|
||||
const masteryPkg: MasteryPackage = this.getMasteryPackage(
|
||||
const masteryPkg: MasteryPackage | undefined = this.getMasteryPackage(
|
||||
locationParentId,
|
||||
gameVersion,
|
||||
)
|
||||
@ -389,16 +379,23 @@ export class MasteryService {
|
||||
}
|
||||
|
||||
// Get all unlockables with matching Ids
|
||||
const unlockableData: Unlockable[] = getUnlockablesById(
|
||||
const unlockableData = getUnlockablesById(
|
||||
Array.from(dropIdSet),
|
||||
gameVersion,
|
||||
)
|
||||
|
||||
// Put all unlockabkes in a map for quick lookup
|
||||
const unlockableMap = new Map(
|
||||
unlockableData.map((unlockable) => [unlockable.Id, unlockable]),
|
||||
const mapped: [string, Unlockable][] = unlockableData.map(
|
||||
(unlockable) => {
|
||||
return [unlockable?.Id, unlockable] as unknown as [
|
||||
string,
|
||||
Unlockable,
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
const unlockableMap: Map<string, Unlockable> = new Map(mapped)
|
||||
|
||||
const masteryData: MasteryData[] = []
|
||||
|
||||
if (masteryPkg.SubPackages) {
|
||||
@ -418,17 +415,19 @@ export class MasteryService {
|
||||
subPkg.Id,
|
||||
)
|
||||
|
||||
masteryData.push({
|
||||
CompletionData: completionData,
|
||||
Drops: this.processDrops(
|
||||
completionData.Level,
|
||||
subPkg.Drops,
|
||||
unlockableMap,
|
||||
),
|
||||
Unlockable: isSniper
|
||||
? unlockableMap.get(subPkg.Id)
|
||||
: undefined,
|
||||
})
|
||||
if (completionData) {
|
||||
masteryData.push({
|
||||
CompletionData: completionData,
|
||||
Drops: this.processDrops(
|
||||
completionData.Level,
|
||||
subPkg.Drops,
|
||||
unlockableMap,
|
||||
),
|
||||
Unlockable: isSniper
|
||||
? unlockableMap.get(subPkg.Id)
|
||||
: undefined,
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 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",
|
||||
)
|
||||
|
||||
masteryData.push({
|
||||
CompletionData: completionData,
|
||||
Drops: this.processDrops(
|
||||
completionData.Level,
|
||||
masteryPkg.Drops,
|
||||
unlockableMap,
|
||||
),
|
||||
})
|
||||
if (completionData) {
|
||||
masteryData.push({
|
||||
CompletionData: completionData,
|
||||
Drops: this.processDrops(
|
||||
completionData.Level,
|
||||
masteryPkg.Drops || [],
|
||||
unlockableMap,
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return masteryData
|
||||
|
@ -19,7 +19,12 @@
|
||||
import { getSubLocationByName } from "../contracts/dataGen"
|
||||
import { controller } from "../controller"
|
||||
import { getUnlockablesById, grantDrops } from "../inventory"
|
||||
import type { ContractSession, UserProfile, GameVersion } from "../types/types"
|
||||
import type {
|
||||
ContractSession,
|
||||
GameVersion,
|
||||
Unlockable,
|
||||
UserProfile,
|
||||
} from "../types/types"
|
||||
import {
|
||||
clampValue,
|
||||
DEFAULT_MASTERY_MAXLEVEL,
|
||||
@ -70,7 +75,9 @@ export class ProgressionService {
|
||||
if (dropIds.length > 0) {
|
||||
grantDrops(
|
||||
userProfile.Id,
|
||||
getUnlockablesById(dropIds, contractSession.gameVersion),
|
||||
getUnlockablesById(dropIds, contractSession.gameVersion).filter(
|
||||
Boolean,
|
||||
) as Unlockable[],
|
||||
)
|
||||
}
|
||||
|
||||
@ -85,7 +92,8 @@ export class ProgressionService {
|
||||
subPkgId?: string,
|
||||
) {
|
||||
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]
|
||||
}
|
||||
|
||||
@ -179,25 +187,29 @@ export class ProgressionService {
|
||||
if (masteryData) {
|
||||
const previousLevel = locationData.Level
|
||||
|
||||
let newLocationXp = xpRequiredForLevel(maxLevel)
|
||||
|
||||
if (isEvergreenContract) {
|
||||
newLocationXp = xpRequiredForEvergreenLevel(maxLevel)
|
||||
} else if (sniperUnlockable) {
|
||||
newLocationXp = xpRequiredForSniperLevel(maxLevel)
|
||||
}
|
||||
|
||||
locationData.Xp = clampValue(
|
||||
locationData.Xp + masteryXp + actionXp,
|
||||
0,
|
||||
isEvergreenContract
|
||||
? xpRequiredForEvergreenLevel(maxLevel)
|
||||
: sniperUnlockable
|
||||
? xpRequiredForSniperLevel(maxLevel)
|
||||
: xpRequiredForLevel(maxLevel),
|
||||
newLocationXp,
|
||||
)
|
||||
|
||||
locationData.Level = clampValue(
|
||||
isEvergreenContract
|
||||
? evergreenLevelForXp(locationData.Xp)
|
||||
: sniperUnlockable
|
||||
? sniperLevelForXp(locationData.Xp)
|
||||
: levelForXp(locationData.Xp),
|
||||
1,
|
||||
maxLevel,
|
||||
)
|
||||
let newLocationLevel = levelForXp(newLocationXp)
|
||||
|
||||
if (isEvergreenContract) {
|
||||
newLocationLevel = evergreenLevelForXp(newLocationXp)
|
||||
} else if (sniperUnlockable) {
|
||||
newLocationLevel = sniperLevelForXp(newLocationXp)
|
||||
}
|
||||
|
||||
locationData.Level = clampValue(newLocationLevel, 1, maxLevel)
|
||||
|
||||
// If mastery level has gone up, check if there are available drop rewards and award them
|
||||
if (locationData.Level > previousLevel) {
|
||||
@ -205,13 +217,13 @@ export class ProgressionService {
|
||||
contractSession.gameVersion,
|
||||
isEvergreenContract,
|
||||
sniperUnlockable
|
||||
? masteryData.SubPackages.find(
|
||||
? masteryData.SubPackages?.find(
|
||||
(pkg) => pkg.Id === sniperUnlockable,
|
||||
).Drops
|
||||
: masteryData.Drops,
|
||||
)?.Drops || []
|
||||
: masteryData.Drops || [],
|
||||
previousLevel,
|
||||
locationData.Level,
|
||||
)
|
||||
).filter(Boolean) as Unlockable[]
|
||||
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 (gameVersion === "h2" && !Object.hasOwn(configs, `H2${config}`)) {
|
||||
if (gameVersion === "h2" && !configs[`H2${config}`]) {
|
||||
return getConfig(config, clone)
|
||||
}
|
||||
|
||||
|
@ -52,7 +52,7 @@ import {
|
||||
createTimeLimit,
|
||||
TargetCreator,
|
||||
} from "../statemachines/contractCreation"
|
||||
import { createSniperLoadouts } from "../menus/sniper"
|
||||
import { createSniperLoadouts, SniperCharacter } from "../menus/sniper"
|
||||
import { GetForPlay2Body } from "../types/gameSchemas"
|
||||
import assert from "assert"
|
||||
import { getUserData } from "../databaseHandler"
|
||||
@ -63,7 +63,8 @@ const contractRoutingRouter = Router()
|
||||
contractRoutingRouter.post(
|
||||
"/GetForPlay2",
|
||||
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)) {
|
||||
res.status(400).end()
|
||||
return // user sent some nasty info
|
||||
@ -84,7 +85,8 @@ contractRoutingRouter.post(
|
||||
req.jwt.unique_name,
|
||||
req.gameVersion,
|
||||
contractData,
|
||||
)
|
||||
) as SniperCharacter[]
|
||||
|
||||
const loadoutData = {
|
||||
CharacterLoadoutData:
|
||||
sniperloadouts.length !== 0 ? sniperloadouts : null,
|
||||
@ -100,7 +102,7 @@ contractRoutingRouter.post(
|
||||
req.gameVersion,
|
||||
)
|
||||
: {}),
|
||||
...loadoutData,
|
||||
...(loadoutData || {}),
|
||||
...{
|
||||
OpportunityData: getContractOpportunityData(req, contractData),
|
||||
},
|
||||
@ -120,7 +122,7 @@ contractRoutingRouter.post(
|
||||
.toString()}-${randomUUID()}`,
|
||||
ContractProgressionData: contractData.Metadata
|
||||
.UseContractProgressionData
|
||||
? await getCpd(req.jwt.unique_name, contractData.Metadata.CpdId)
|
||||
? getCpd(req.jwt.unique_name, contractData.Metadata.CpdId!)
|
||||
: null,
|
||||
}
|
||||
|
||||
@ -159,6 +161,8 @@ contractRoutingRouter.post(
|
||||
continue
|
||||
}
|
||||
|
||||
assert.ok(gameChanger.Objectives, "gc has no objectives")
|
||||
|
||||
contractData.Data.GameChangerReferences.push(gameChanger)
|
||||
contractData.Data.Bricks = [
|
||||
...(contractData.Data.Bricks ?? []),
|
||||
@ -228,6 +232,7 @@ contractRoutingRouter.post(
|
||||
contractRoutingRouter.post(
|
||||
"/CreateFromParams",
|
||||
jsonMiddleware(),
|
||||
// @ts-expect-error Has jwt props.
|
||||
async (
|
||||
req: RequestWithJwt<Record<never, never>, CreateFromParamsBody>,
|
||||
res,
|
||||
@ -340,13 +345,20 @@ contractRoutingRouter.post(
|
||||
contractRoutingRouter.post(
|
||||
"/GetContractOpportunities",
|
||||
jsonMiddleware(),
|
||||
// @ts-expect-error Has jwt props.
|
||||
(req: RequestWithJwt<never, { contractId: string }>, res) => {
|
||||
const contract = controller.resolveContract(req.body.contractId)
|
||||
|
||||
if (!contract) {
|
||||
res.status(400).send("contract not found")
|
||||
return
|
||||
}
|
||||
|
||||
res.json(getContractOpportunityData(req, contract))
|
||||
},
|
||||
)
|
||||
|
||||
function getContractOpportunityData(
|
||||
export function getContractOpportunityData(
|
||||
req: RequestWithJwt,
|
||||
contract: MissionManifest,
|
||||
): MissionStory[] {
|
||||
@ -366,6 +378,7 @@ function getContractOpportunityData(
|
||||
missionStories[ms].PreviouslyCompleted =
|
||||
ms in userData.Extensions.opportunityprogression
|
||||
const current = fastClone(missionStories[ms])
|
||||
// @ts-expect-error Deal with it.
|
||||
delete current.Location
|
||||
result.push(current)
|
||||
}
|
||||
|
@ -38,6 +38,7 @@ import {
|
||||
getUserEscalationProgress,
|
||||
} from "./escalations/escalationService"
|
||||
import { translateEntitlements } from "../ownership"
|
||||
import assert from "assert"
|
||||
|
||||
// TODO: In the near future, this file should be cleaned up where possible.
|
||||
|
||||
@ -123,19 +124,28 @@ export function generateCompletionData(
|
||||
let difficulty = undefined
|
||||
|
||||
if (gameVersion === "h1") {
|
||||
difficulty = getUserData(userId, gameVersion).Extensions
|
||||
.gamepersistentdata.menudata.difficulty.destinations[
|
||||
subLocation ? subLocation.Properties?.ParentLocation : subLocationId
|
||||
]
|
||||
const userData = getUserData(userId, gameVersion)
|
||||
difficulty =
|
||||
userData.Extensions.gamepersistentdata.menudata.difficulty
|
||||
.destinations[
|
||||
subLocation
|
||||
? subLocation.Properties?.ParentLocation || ""
|
||||
: subLocationId
|
||||
]
|
||||
}
|
||||
|
||||
const locationId = subLocation
|
||||
? subLocation.Properties?.ParentLocation
|
||||
: subLocationId
|
||||
|
||||
assert.ok(
|
||||
locationId,
|
||||
`Location ID is undefined for ${subLocationId} in ${gameVersion}!`,
|
||||
)
|
||||
|
||||
const completionData = controller.masteryService.getLocationCompletion(
|
||||
locationId,
|
||||
subLocation?.Id,
|
||||
subLocationId,
|
||||
gameVersion,
|
||||
userId,
|
||||
contractType,
|
||||
@ -153,7 +163,7 @@ export function generateCompletionData(
|
||||
Completion: 1.0,
|
||||
XpLeft: 0,
|
||||
Id: locationId,
|
||||
SubLocationId: subLocation?.Id,
|
||||
SubLocationId: subLocationId,
|
||||
HideProgression: true,
|
||||
IsLocationProgression: true,
|
||||
Name: null,
|
||||
@ -197,7 +207,7 @@ export function generateUserCentric(
|
||||
// fix h1/h2 entitlements
|
||||
contractData.Metadata.Entitlements = translateEntitlements(
|
||||
gameVersion,
|
||||
contractData.Metadata.Entitlements,
|
||||
contractData.Metadata.Entitlements || [],
|
||||
)
|
||||
}
|
||||
|
||||
@ -210,6 +220,12 @@ export function generateUserCentric(
|
||||
gameVersion,
|
||||
)
|
||||
|
||||
let lastPlayed: string | undefined = undefined
|
||||
|
||||
if (played[id]?.LastPlayedAt) {
|
||||
lastPlayed = new Date(played[id].LastPlayedAt!).toISOString()
|
||||
}
|
||||
|
||||
const uc: UserCentricContract = {
|
||||
Contract: contractData,
|
||||
Data: {
|
||||
@ -222,10 +238,7 @@ export function generateUserCentric(
|
||||
LocationHideProgression: completionData.HideProgression,
|
||||
ElusiveContractState: "",
|
||||
IsFeatured: false,
|
||||
LastPlayedAt:
|
||||
played[id] === undefined
|
||||
? undefined
|
||||
: new Date(played[id]?.LastPlayedAt).toISOString(),
|
||||
LastPlayedAt: lastPlayed,
|
||||
// relevant for contracts
|
||||
// Favorite contracts
|
||||
PlaylistData: {
|
||||
@ -316,6 +329,7 @@ export function mapObjectives(
|
||||
gameChangerProps.ObjectivesCategory = (() => {
|
||||
let obj: MissionManifestObjective
|
||||
|
||||
// @ts-expect-error State machines are impossible to type
|
||||
for (obj of gameChangerProps.Objectives) {
|
||||
if (obj.Category === "primary") return "primary"
|
||||
if (obj.Category === "secondary")
|
||||
@ -362,11 +376,9 @@ export function mapObjectives(
|
||||
objective.OnActive.IfInProgress.Visible === false) ||
|
||||
(objective.OnActive?.IfCompleted &&
|
||||
objective.OnActive.IfCompleted.Visible === false &&
|
||||
objective.Definition &&
|
||||
objective.Definition.States &&
|
||||
objective.Definition.States.Start &&
|
||||
objective.Definition.States.Start["-"] &&
|
||||
objective.Definition.States.Start["-"].Transition === "Success")
|
||||
// @ts-expect-error State machines are impossible to type
|
||||
objective.Definition?.States?.Start?.["-"]?.Transition ===
|
||||
"Success")
|
||||
) {
|
||||
continue // do not show objectives with 'ForceShowOnLoadingScreen: false' or objectives that are not visible on start
|
||||
}
|
||||
@ -374,8 +386,7 @@ export function mapObjectives(
|
||||
if (
|
||||
objective.SuccessEvent &&
|
||||
objective.SuccessEvent.EventName === "Kill" &&
|
||||
objective.SuccessEvent.EventValues &&
|
||||
objective.SuccessEvent.EventValues.RepositoryId
|
||||
objective.SuccessEvent.EventValues?.RepositoryId
|
||||
) {
|
||||
result.set(objective.Id, {
|
||||
Type: "kill",
|
||||
@ -396,6 +407,7 @@ export function mapObjectives(
|
||||
objective.Definition?.Context?.Targets &&
|
||||
(objective.Definition.Context.Targets as string[]).length === 1
|
||||
) {
|
||||
// @ts-expect-error State machines are impossible to type
|
||||
id = objective.Definition.Context.Targets[0]
|
||||
}
|
||||
|
||||
@ -437,10 +449,8 @@ export function mapObjectives(
|
||||
})
|
||||
} else if (
|
||||
objective.Type === "statemachine" &&
|
||||
objective.Definition &&
|
||||
objective.Definition.Context &&
|
||||
objective.Definition.Context.Targets &&
|
||||
(objective.Definition.Context.Targets as unknown[]).length === 1 &&
|
||||
(objective.Definition?.Context?.Targets as unknown[])?.length ===
|
||||
1 &&
|
||||
objective.HUDTemplate
|
||||
) {
|
||||
// This objective will be displayed as a kill objective
|
||||
@ -457,6 +467,7 @@ export function mapObjectives(
|
||||
result.set(objective.Id, {
|
||||
Type: "kill",
|
||||
Properties: {
|
||||
// @ts-expect-error State machines are impossible to type
|
||||
Id: objective.Definition.Context.Targets[0],
|
||||
Conditions: Conditions,
|
||||
},
|
||||
|
@ -26,7 +26,6 @@ import type {
|
||||
} from "../../types/types"
|
||||
import { getUserData } from "../../databaseHandler"
|
||||
import { log, LogLevel } from "../../loggingInterop"
|
||||
import assert from "assert"
|
||||
|
||||
/**
|
||||
* 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 groupContractId The escalation's group contract ID.
|
||||
* @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(
|
||||
userId: string,
|
||||
groupContractId: string,
|
||||
groupContractId: string | undefined | null,
|
||||
gameVersion: GameVersion,
|
||||
): EscalationInfo {
|
||||
if (!groupContractId) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const userData = getUserData(userId, gameVersion)
|
||||
|
||||
const p = getUserEscalationProgress(userData, groupContractId)
|
||||
const groupCt = controller.escalationMappings.get(groupContractId)
|
||||
|
||||
assert.ok(groupCt, `No escalation mapping for ${groupContractId}`)
|
||||
if (!groupCt) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const totalLevelCount = getLevelCount(
|
||||
controller.resolveContract(groupContractId),
|
||||
|
@ -21,6 +21,7 @@ import {
|
||||
ContractHistory,
|
||||
GameVersion,
|
||||
HitsCategoryCategory,
|
||||
IHit,
|
||||
} from "../types/types"
|
||||
import {
|
||||
contractIdToHitObject,
|
||||
@ -35,6 +36,7 @@ import { log, LogLevel } from "../loggingInterop"
|
||||
import { fastClone, getRemoteService } from "../utils"
|
||||
import { orderedETAs } from "./elusiveTargetArcades"
|
||||
import { missionsInLocations } from "./missionsInLocation"
|
||||
import assert from "assert"
|
||||
|
||||
/**
|
||||
* The filters supported for HitsCategories.
|
||||
@ -154,11 +156,11 @@ export class HitsCategoryService {
|
||||
|
||||
switch (gameVersion) {
|
||||
case "h1":
|
||||
if (contract.Metadata.Season === 1)
|
||||
if (contract?.Metadata.Season === 1)
|
||||
contracts.push(id)
|
||||
break
|
||||
case "h2":
|
||||
if (contract.Metadata.Season <= 2)
|
||||
if ((contract?.Metadata.Season || 0) <= 2)
|
||||
contracts.push(id)
|
||||
break
|
||||
default:
|
||||
@ -232,7 +234,7 @@ export class HitsCategoryService {
|
||||
.tap(tapName, (contracts, gameVersion) => {
|
||||
// We need to push Peacock contracts first to work around H2 not
|
||||
// having the "order" property for $arraygroupby. (The game just crashes)
|
||||
const nEscalations = []
|
||||
const nEscalations: string[] = []
|
||||
|
||||
for (const escalations of Object.values(
|
||||
missionsInLocations.escalations,
|
||||
@ -240,6 +242,10 @@ export class HitsCategoryService {
|
||||
for (const id of escalations) {
|
||||
const contract = controller.resolveContract(id)
|
||||
|
||||
if (!contract) {
|
||||
continue
|
||||
}
|
||||
|
||||
const isPeacock = contract.Metadata.Season === 0
|
||||
const season = isPeacock
|
||||
? contract.Metadata.OriginalSeason
|
||||
@ -252,7 +258,7 @@ export class HitsCategoryService {
|
||||
if (season === 1) contracts.push(id)
|
||||
break
|
||||
case "h2":
|
||||
if (season <= 2)
|
||||
if ((season || 0) <= 2)
|
||||
(isPeacock ? contracts : nEscalations).push(
|
||||
id,
|
||||
)
|
||||
@ -273,7 +279,7 @@ export class HitsCategoryService {
|
||||
pageNumber: number,
|
||||
gameVersion: GameVersion,
|
||||
userId: string,
|
||||
): Promise<HitsCategoryCategory> {
|
||||
): Promise<HitsCategoryCategory | undefined> {
|
||||
const remoteService = getRemoteService(gameVersion)
|
||||
const user = userAuths.get(userId)
|
||||
|
||||
@ -289,10 +295,12 @@ export class HitsCategoryService {
|
||||
true,
|
||||
)
|
||||
const hits = resp.data.data.Data.Hits
|
||||
preserveContracts(
|
||||
hits.map(
|
||||
(hit) => hit.UserCentricContract.Contract.Metadata.PublicId,
|
||||
),
|
||||
void preserveContracts(
|
||||
hits
|
||||
.map(
|
||||
(hit) => hit.UserCentricContract.Contract.Metadata.PublicId,
|
||||
)
|
||||
.filter(Boolean) as string[],
|
||||
)
|
||||
|
||||
// Fix completion and favorite status for retrieved contracts
|
||||
@ -304,7 +312,7 @@ export class HitsCategoryService {
|
||||
if (Object.keys(played).includes(hit.Id)) {
|
||||
// Replace with data stored by Peacock
|
||||
hit.UserCentricContract.Data.LastPlayedAt = new Date(
|
||||
played[hit.Id].LastPlayedAt,
|
||||
played[hit.Id].LastPlayedAt || 0,
|
||||
).toISOString()
|
||||
hit.UserCentricContract.Data.Completed =
|
||||
played[hit.Id].Completed
|
||||
@ -314,8 +322,10 @@ export class HitsCategoryService {
|
||||
hit.UserCentricContract.Data.Completed = false
|
||||
}
|
||||
|
||||
hit.UserCentricContract.Data.PlaylistData.IsAdded =
|
||||
favorites.includes(hit.Id)
|
||||
if (hit.UserCentricContract.Data.PlaylistData) {
|
||||
hit.UserCentricContract.Data.PlaylistData.IsAdded =
|
||||
favorites.includes(hit.Id)
|
||||
}
|
||||
}
|
||||
|
||||
return resp.data.data
|
||||
@ -378,13 +388,23 @@ export class HitsCategoryService {
|
||||
type: ContractFilter,
|
||||
category: string,
|
||||
): string | undefined {
|
||||
if (!this.filterSupported.includes(category)) return undefined
|
||||
if (!this.filterSupported.includes(category)) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const user = getUserData(userId, gameVersion)
|
||||
type Cast =
|
||||
keyof typeof user.Extensions.gamepersistentdata.HitsFilterType
|
||||
|
||||
if (type === "default") {
|
||||
type = user.Extensions.gamepersistentdata.HitsFilterType[category]
|
||||
type =
|
||||
user.Extensions.gamepersistentdata.HitsFilterType[
|
||||
category as Cast
|
||||
]
|
||||
} else {
|
||||
user.Extensions.gamepersistentdata.HitsFilterType[category] = type
|
||||
user.Extensions.gamepersistentdata.HitsFilterType[
|
||||
category as Cast
|
||||
] = type
|
||||
writeUserData(userId, gameVersion)
|
||||
}
|
||||
|
||||
@ -408,7 +428,10 @@ export class HitsCategoryService {
|
||||
return Object.keys(played)
|
||||
.filter((id) => this.isContractOfType(played, type, id))
|
||||
.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 {
|
||||
switch (type) {
|
||||
case "completed":
|
||||
return (
|
||||
return Boolean(
|
||||
played[contractId]?.Completed &&
|
||||
!played[contractId]?.IsEscalation
|
||||
!played[contractId]?.IsEscalation,
|
||||
)
|
||||
case "failed":
|
||||
return (
|
||||
@ -440,6 +463,8 @@ export class HitsCategoryService {
|
||||
)
|
||||
case "all":
|
||||
return !played[contractId]?.IsEscalation
|
||||
default:
|
||||
assert.fail("Invalid type passed to isContractOfType")
|
||||
}
|
||||
}
|
||||
|
||||
@ -457,7 +482,7 @@ export class HitsCategoryService {
|
||||
pageNumber: number,
|
||||
gameVersion: GameVersion,
|
||||
userId: string,
|
||||
): Promise<HitsCategoryCategory> {
|
||||
): Promise<HitsCategoryCategory | undefined> {
|
||||
if (this.realtimeFetched.includes(categoryName)) {
|
||||
return await this.fetchFromOfficial(
|
||||
categoryName,
|
||||
@ -479,7 +504,7 @@ export class HitsCategoryService {
|
||||
userId,
|
||||
filter,
|
||||
category,
|
||||
)
|
||||
)!
|
||||
|
||||
const hitsCategory: HitsCategoryCategory = {
|
||||
Category: category,
|
||||
@ -489,7 +514,7 @@ export class HitsCategoryService {
|
||||
Page: pageNumber,
|
||||
HasMore: false,
|
||||
},
|
||||
CurrentSubType: undefined,
|
||||
CurrentSubType: "",
|
||||
}
|
||||
|
||||
const hook = this.hitsCategories.for(category)
|
||||
@ -500,7 +525,7 @@ export class HitsCategoryService {
|
||||
|
||||
const hitObjectList = hits
|
||||
.map((id) => contractIdToHitObject(id, gameVersion, userId))
|
||||
.filter(Boolean)
|
||||
.filter(Boolean) as IHit[]
|
||||
|
||||
if (!this.paginationExempt.includes(category)) {
|
||||
const paginated = paginate(hitObjectList, this.hitsPerPage)
|
||||
|
@ -53,9 +53,15 @@ export async function getLeaderboardEntries(
|
||||
platform: JwtData["platform"],
|
||||
gameVersion: GameVersion,
|
||||
difficultyLevel?: string,
|
||||
): Promise<GameFacingLeaderboardData> {
|
||||
): Promise<GameFacingLeaderboardData | undefined> {
|
||||
let difficulty = "unset"
|
||||
|
||||
const contract = controller.resolveContract(contractId)
|
||||
|
||||
if (!contract) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const parsedDifficulty = parseInt(difficultyLevel || "0")
|
||||
|
||||
if (parsedDifficulty === gameDifficulty.casual) {
|
||||
@ -72,7 +78,7 @@ export async function getLeaderboardEntries(
|
||||
|
||||
const response: GameFacingLeaderboardData = {
|
||||
Entries: [],
|
||||
Contract: controller.resolveContract(contractId),
|
||||
Contract: contract,
|
||||
Page: 0,
|
||||
HasMore: false,
|
||||
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/>.
|
||||
*/
|
||||
|
||||
import type { ContractSession } from "./types/types"
|
||||
import { ContractSession } from "../types/types"
|
||||
|
||||
/**
|
||||
* Changes a set to an array.
|
||||
@ -60,23 +60,28 @@ const SESSION_MAP_PROPS: (keyof ContractSession)[] = [
|
||||
* @param session The ContractSession.
|
||||
*/
|
||||
export function serializeSession(session: ContractSession): unknown {
|
||||
const o = {}
|
||||
const o: Partial<ContractSession> = {}
|
||||
|
||||
type K = keyof ContractSession
|
||||
|
||||
// obj clone
|
||||
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(
|
||||
(session[key] as Map<string, unknown>).entries(),
|
||||
(session[key as K] as Map<string, unknown>).entries(),
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
if (session[key] instanceof Set) {
|
||||
if (session[key as K] instanceof Set) {
|
||||
// @ts-expect-error Type mismatch.
|
||||
o[key] = normalizeSet(session[key])
|
||||
continue
|
||||
}
|
||||
|
||||
o[key] = session[key]
|
||||
// @ts-expect-error Type mismatch.
|
||||
o[key] = session[key as K]
|
||||
}
|
||||
|
||||
return o
|
||||
@ -90,19 +95,22 @@ export function serializeSession(session: ContractSession): unknown {
|
||||
export function deserializeSession(
|
||||
saved: Record<string, unknown>,
|
||||
): ContractSession {
|
||||
const session = {}
|
||||
const session: Partial<ContractSession> = {}
|
||||
|
||||
// obj clone
|
||||
for (const key of Object.keys(saved)) {
|
||||
// @ts-expect-error Type mismatch.
|
||||
session[key] = saved[key]
|
||||
}
|
||||
|
||||
for (const collection of SESSION_SET_PROPS) {
|
||||
// @ts-expect-error Type mismatch.
|
||||
session[collection] = new Set(session[collection])
|
||||
}
|
||||
|
||||
for (const map of SESSION_MAP_PROPS) {
|
||||
if (Object.hasOwn(session, map)) {
|
||||
// @ts-expect-error Type mismatch.
|
||||
session[map] = new Map(session[map])
|
||||
}
|
||||
}
|
@ -188,8 +188,10 @@ function createPeacockRequire(pluginName: string): NodeRequire {
|
||||
* @param specifier The requested module.
|
||||
*/
|
||||
const peacockRequire: NodeRequire = (specifier: string) => {
|
||||
if (generatedPeacockRequireTable[specifier]) {
|
||||
return generatedPeacockRequireTable[specifier]
|
||||
type T = keyof typeof generatedPeacockRequireTable
|
||||
|
||||
if (generatedPeacockRequireTable[specifier as T]) {
|
||||
return generatedPeacockRequireTable[specifier as T]
|
||||
}
|
||||
|
||||
try {
|
||||
@ -237,14 +239,22 @@ export const validateMission = (m: MissionManifest): boolean => {
|
||||
return false
|
||||
}
|
||||
|
||||
for (const prop of ["Id", "Title", "Location", "ScenePath"]) {
|
||||
if (!Object.hasOwn(m.Metadata, prop)) {
|
||||
for (const prop of <(keyof MissionManifest["Metadata"])[]>[
|
||||
"Id",
|
||||
"Title",
|
||||
"Location",
|
||||
"ScenePath",
|
||||
]) {
|
||||
if (!m.Metadata[prop]) {
|
||||
log(LogLevel.ERROR, `Contract missing property Metadata.${prop}!`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
for (const prop of ["Objectives", "Bricks"]) {
|
||||
for (const prop of <(keyof MissionManifest["Data"])[]>[
|
||||
"Objectives",
|
||||
"Bricks",
|
||||
]) {
|
||||
if (!Object.hasOwn(m.Data, prop)) {
|
||||
log(LogLevel.ERROR, `Contract missing property Data.${prop}!`)
|
||||
return false
|
||||
@ -365,11 +375,11 @@ export class Controller {
|
||||
*/
|
||||
public fetchedContracts: Map<string, MissionManifest> = new Map()
|
||||
|
||||
public challengeService: ChallengeService
|
||||
public masteryService: MasteryService
|
||||
public challengeService!: ChallengeService
|
||||
public masteryService!: MasteryService
|
||||
escalationMappings: Map<string, Record<string, string>> = new Map()
|
||||
public progressionService: ProgressionService
|
||||
public smf: SMFSupport
|
||||
public progressionService!: ProgressionService
|
||||
public smf!: SMFSupport
|
||||
private _pubIdToContractId: Map<string, string> = new Map()
|
||||
/** Internal elusive target contracts - only accessible during bootstrap. */
|
||||
private _internalElusives: MissionManifest[] | undefined
|
||||
@ -444,12 +454,8 @@ export class Controller {
|
||||
this.hooks.challengesLoaded.call()
|
||||
this.hooks.masteryDataLoaded.call()
|
||||
} catch (e) {
|
||||
log(
|
||||
LogLevel.ERROR,
|
||||
`Fatal error with challenge bootstrap: ${e}`,
|
||||
"boot",
|
||||
)
|
||||
log(LogLevel.ERROR, e.stack)
|
||||
log(LogLevel.ERROR, `Fatal error with challenge bootstrap`, "boot")
|
||||
log(LogLevel.ERROR, e)
|
||||
}
|
||||
}
|
||||
|
||||
@ -461,6 +467,11 @@ export class Controller {
|
||||
continue
|
||||
}
|
||||
|
||||
assert.ok(
|
||||
contract.Metadata.GroupDefinition,
|
||||
"arcade contract has no group definition",
|
||||
)
|
||||
|
||||
for (const lId of contract.Metadata.GroupDefinition.Order) {
|
||||
const level = this.resolveContract(lId, false)
|
||||
|
||||
@ -481,9 +492,10 @@ export class Controller {
|
||||
)
|
||||
|
||||
for (const location of this.locationsWithETA) {
|
||||
this.parentsWithETA.add(
|
||||
locations.children[location].Properties.ParentLocation,
|
||||
)
|
||||
const pl = 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)!) ||
|
||||
undefined
|
||||
)
|
||||
}
|
||||
|
||||
return undefined
|
||||
return this.contracts.get(this._pubIdToContractId.get(pubId)!)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -546,6 +551,10 @@ export class Controller {
|
||||
|
||||
private getGroupContract(json: MissionManifest) {
|
||||
if (escalationTypes.includes(json.Metadata.Type)) {
|
||||
if (!json.Metadata.InGroup) {
|
||||
return 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.
|
||||
*/
|
||||
public resolveContract(
|
||||
id: string,
|
||||
id: string | undefined,
|
||||
getGroup = false,
|
||||
): MissionManifest | undefined {
|
||||
if (!id) {
|
||||
@ -650,11 +659,16 @@ export class Controller {
|
||||
this.addMission(groupContract)
|
||||
fixedLevels.forEach((level) => this.addMission(level))
|
||||
|
||||
this.missionsInLocations.escalations[locationId] ??= []
|
||||
type K = keyof typeof this.missionsInLocations.escalations
|
||||
|
||||
this.missionsInLocations.escalations[locationId].push(
|
||||
groupContract.Metadata.Id,
|
||||
)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
this.missionsInLocations.escalations[locationId as K] ??= <any>[]
|
||||
|
||||
const a = this.missionsInLocations.escalations[
|
||||
locationId as K
|
||||
] as string[]
|
||||
|
||||
a.push(groupContract.Metadata.Id)
|
||||
|
||||
this.scanForGroups()
|
||||
}
|
||||
@ -758,7 +772,6 @@ export class Controller {
|
||||
}
|
||||
} catch (e) {
|
||||
log(LogLevel.ERROR, `Failed to load contract ${i}!`)
|
||||
log(LogLevel.ERROR, e.stack)
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -1028,7 +1041,6 @@ export class Controller {
|
||||
let theExports
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line prefer-const
|
||||
theExports = new Script(pluginContents, {
|
||||
filename: pluginPath,
|
||||
}).runInContext(context)
|
||||
@ -1038,7 +1050,6 @@ export class Controller {
|
||||
`Error while attempting to queue plugin ${pluginName} for loading!`,
|
||||
)
|
||||
log(LogLevel.ERROR, e)
|
||||
log(LogLevel.ERROR, e.stack)
|
||||
return
|
||||
}
|
||||
|
||||
@ -1057,7 +1068,6 @@ export class Controller {
|
||||
} catch (e) {
|
||||
log(LogLevel.ERROR, `Error while evaluating plugin ${pluginName}!`)
|
||||
log(LogLevel.ERROR, e)
|
||||
log(LogLevel.ERROR, e.stack)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1185,7 +1195,7 @@ export function contractIdToHitObject(
|
||||
"LocationsData",
|
||||
gameVersion,
|
||||
false,
|
||||
).parents[subLocation?.Properties?.ParentLocation]
|
||||
).parents[subLocation?.Properties?.ParentLocation || ""]
|
||||
|
||||
// failed to find the location, must be from a newer game
|
||||
if (!subLocation && ["h1", "h2", "scpc"].includes(gameVersion)) {
|
||||
|
@ -19,7 +19,7 @@
|
||||
import { readFile, writeFile } from "atomically"
|
||||
import { join } from "path"
|
||||
import type { ContractSession, GameVersion, UserProfile } from "./types/types"
|
||||
import { serializeSession, deserializeSession } from "./sessionSerialization"
|
||||
import { serializeSession, deserializeSession } from "./contracts/sessions"
|
||||
import { castUserProfile } from "./utils"
|
||||
import { log, LogLevel } from "./loggingInterop"
|
||||
import { unlink, readdir } from "fs/promises"
|
||||
|
@ -16,6 +16,10 @@
|
||||
* 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 { clearTimeout, setTimeout } from "timers"
|
||||
import { IPCTransport } from "./ipc"
|
||||
|
@ -16,6 +16,10 @@
|
||||
* 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 EventEmitter from "events"
|
||||
import axios from "axios"
|
||||
|
@ -67,7 +67,7 @@ export class IOIStrategy extends EntitlementStrategy {
|
||||
constructor(gameVersion: GameVersion, private readonly issuerId: string) {
|
||||
super()
|
||||
this.issuerId = issuerId
|
||||
this._remoteService = getRemoteService(gameVersion)
|
||||
this._remoteService = getRemoteService(gameVersion)!
|
||||
}
|
||||
|
||||
override async get(userId: string) {
|
||||
|
@ -174,8 +174,8 @@ export function setupScoring(
|
||||
|
||||
if (name === "scoring") {
|
||||
const definition: ManifestScoringDefinition = deepmerge(
|
||||
...module.ScoringDefinitions,
|
||||
)
|
||||
...(module.ScoringDefinitions || []),
|
||||
) as unknown as ManifestScoringDefinition
|
||||
|
||||
let state = "Start"
|
||||
let context = definition.Context
|
||||
@ -200,15 +200,21 @@ export function setupScoring(
|
||||
context = immediate.context
|
||||
}
|
||||
|
||||
// @ts-expect-error Type issue
|
||||
scoring.Definition = definition
|
||||
// @ts-expect-error Type issue
|
||||
scoring.Context = context
|
||||
// @ts-expect-error Type issue
|
||||
scoring.State = state
|
||||
} else {
|
||||
// @ts-expect-error Type issue
|
||||
scoring.Settings[name] = module
|
||||
// @ts-expect-error Type issue
|
||||
delete scoring.Settings[name]["Type"]
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-expect-error Type issue
|
||||
session.scoring = scoring
|
||||
}
|
||||
|
||||
@ -373,13 +379,13 @@ export function newSession(
|
||||
}
|
||||
|
||||
export type SSE3Response = {
|
||||
SavedTokens: string[]
|
||||
NewEvents: ServerToClientEvent[]
|
||||
SavedTokens: string[] | null
|
||||
NewEvents: ServerToClientEvent[] | null
|
||||
NextPoll: number
|
||||
}
|
||||
|
||||
export type SSE4Response = SSE3Response & {
|
||||
PushMessages: string[]
|
||||
PushMessages: string[] | null
|
||||
}
|
||||
|
||||
export function saveAndSyncEvents(
|
||||
@ -410,7 +416,7 @@ export function saveAndSyncEvents(
|
||||
let pushMessages: string[] | undefined
|
||||
|
||||
if ((userPushQueue = pushMessageQueue.get(userId))) {
|
||||
userPushQueue = userPushQueue.filter((item) => item.time > lastPushDt)
|
||||
userPushQueue = userPushQueue.filter((item) => item.time > lastPushDt!)
|
||||
pushMessageQueue.set(userId, userPushQueue)
|
||||
|
||||
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(
|
||||
"/SaveAndSynchronizeEvents3",
|
||||
jsonMiddleware({ limit: "10Mb" }),
|
||||
(
|
||||
req: RequestWithJwt<
|
||||
unknown,
|
||||
{
|
||||
lastEventTicks: number | string
|
||||
userId: string
|
||||
values: ClientToServerEvent[]
|
||||
}
|
||||
>,
|
||||
res,
|
||||
) => {
|
||||
// @ts-expect-error Request has jwt props.
|
||||
(req: RequestWithJwt<unknown, SSE3Body>, res) => {
|
||||
if (req.body.userId !== req.jwt.unique_name) {
|
||||
res.status(403).send() // Trying to save events for other user
|
||||
return
|
||||
@ -469,18 +476,8 @@ eventRouter.post(
|
||||
eventRouter.post(
|
||||
"/SaveAndSynchronizeEvents4",
|
||||
jsonMiddleware({ limit: "10Mb" }),
|
||||
(
|
||||
req: RequestWithJwt<
|
||||
unknown,
|
||||
{
|
||||
lastPushDt: number | string
|
||||
lastEventTicks: number | string
|
||||
userId: string
|
||||
values: ClientToServerEvent[]
|
||||
}
|
||||
>,
|
||||
res,
|
||||
) => {
|
||||
// @ts-expect-error Request has jwt props.
|
||||
(req: RequestWithJwt<unknown, SSE4Body>, res) => {
|
||||
if (req.body.userId !== req.jwt.unique_name) {
|
||||
res.status(403).send() // Trying to save events for other user
|
||||
return
|
||||
@ -507,6 +504,7 @@ eventRouter.post(
|
||||
eventRouter.post(
|
||||
"/SaveEvents2",
|
||||
jsonMiddleware({ limit: "10Mb" }),
|
||||
// @ts-expect-error Request has jwt props.
|
||||
(req: RequestWithJwt, res) => {
|
||||
if (req.jwt.unique_name !== req.body.userId) {
|
||||
res.status(403).send() // Trying to save events for other user
|
||||
@ -595,7 +593,7 @@ function contractFailed(
|
||||
) {
|
||||
if (session.completedObjectives.size === 0) break arcadeFail
|
||||
|
||||
for (const obj of json.Data.Objectives) {
|
||||
for (const obj of json.Data.Objectives || []) {
|
||||
if (
|
||||
session.completedObjectives.has(obj.Id) &&
|
||||
obj.Category === "primary"
|
||||
@ -707,7 +705,9 @@ function saveEvents(
|
||||
|
||||
const val = handleEvent(
|
||||
objectiveDefinition as never,
|
||||
objectiveContext,
|
||||
// SMP sucks. Sorry, not sorry.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
objectiveContext as any,
|
||||
event.Value,
|
||||
{
|
||||
eventName: event.Name,
|
||||
@ -734,7 +734,6 @@ function saveEvents(
|
||||
"An error occurred while tracing C2S events, please report this!",
|
||||
)
|
||||
log(LogLevel.ERROR, e)
|
||||
log(LogLevel.ERROR, e.stack)
|
||||
}
|
||||
}
|
||||
|
||||
@ -744,7 +743,9 @@ function saveEvents(
|
||||
|
||||
const val = handleEvent(
|
||||
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,
|
||||
{
|
||||
eventName: event.Name,
|
||||
@ -762,7 +763,10 @@ function saveEvents(
|
||||
|
||||
controller.challengeService.onContractEvent(event, session)
|
||||
|
||||
if (event.Name.startsWith("ScoringScreenEndState_")) {
|
||||
if (
|
||||
event.Name.startsWith("ScoringScreenEndState_") &&
|
||||
session.evergreen
|
||||
) {
|
||||
session.evergreen.scoringScreenEndState = event.Name
|
||||
|
||||
processed.push(event.Name)
|
||||
@ -1010,7 +1014,10 @@ function saveEvents(
|
||||
const areaId = (<AreaDiscoveredC2SEvent>event).Value
|
||||
.RepositoryId
|
||||
|
||||
const challengeId = getConfig("AreaMap", false)[areaId]
|
||||
const challengeId = getConfig<Record<string, string>>(
|
||||
"AreaMap",
|
||||
false,
|
||||
)[areaId]
|
||||
const progress = userData.Extensions.ChallengeProgression
|
||||
|
||||
log(LogLevel.DEBUG, `Area discovered: ${areaId}`)
|
||||
@ -1044,16 +1051,22 @@ function saveEvents(
|
||||
break
|
||||
// Evergreen
|
||||
case "CpdSet":
|
||||
setCpd(
|
||||
event.Value as ContractProgressionData,
|
||||
userId,
|
||||
contract.Metadata.CpdId,
|
||||
)
|
||||
if (contract?.Metadata.CpdId) {
|
||||
setCpd(
|
||||
event.Value as ContractProgressionData,
|
||||
userId,
|
||||
contract.Metadata.CpdId,
|
||||
)
|
||||
}
|
||||
|
||||
break
|
||||
case "Evergreen_Payout_Data":
|
||||
session.evergreen.payout = (<Evergreen_Payout_DataC2SEvent>(
|
||||
event
|
||||
)).Value.Total_Payout
|
||||
if (session.evergreen) {
|
||||
session.evergreen.payout = (<Evergreen_Payout_DataC2SEvent>(
|
||||
event
|
||||
)).Value.Total_Payout
|
||||
}
|
||||
|
||||
break
|
||||
case "MissionFailed_Event":
|
||||
if (session.evergreen) {
|
||||
|
@ -16,7 +16,7 @@
|
||||
* 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 { log, LogLevel } from "./loggingInterop"
|
||||
import { parse } from "js-ini"
|
||||
@ -123,7 +123,6 @@ const defaultFlags: Flags = {
|
||||
},
|
||||
}
|
||||
|
||||
const OLD_FLAGS_FILE = "flags.json5"
|
||||
const NEW_FLAGS_FILE = "options.ini"
|
||||
|
||||
/**
|
||||
@ -152,7 +151,10 @@ const makeFlagsIni = (
|
||||
Object.keys(defaultFlags)
|
||||
.map((flagId) => {
|
||||
return `; ${defaultFlags[flagId].desc}
|
||||
${flagId} = ${_flags[flagId]}`
|
||||
${flagId} = ${
|
||||
// @ts-expect-error You know what, I don't care
|
||||
_flags[flagId]
|
||||
}`
|
||||
})
|
||||
.join("\n\n")
|
||||
|
||||
@ -160,24 +162,11 @@ ${flagId} = ${_flags[flagId]}`
|
||||
* Loads all flags.
|
||||
*/
|
||||
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)) {
|
||||
const allTheFlags = {}
|
||||
|
||||
Object.keys(defaultFlags).forEach((f) => {
|
||||
// @ts-expect-error You know what, I don't care
|
||||
allTheFlags[f] = defaultFlags[f].default
|
||||
})
|
||||
|
||||
|
@ -37,13 +37,11 @@ import * as platformEntitlements from "./platformEntitlements"
|
||||
import * as playStyles from "./playStyles"
|
||||
import * as profileHandler from "./profileHandler"
|
||||
import * as scoreHandler from "./scoreHandler"
|
||||
import * as sessionSerialization from "./sessionSerialization"
|
||||
import * as smfSupport from "./smfSupport"
|
||||
import * as utils from "./utils"
|
||||
import * as webFeatures from "./webFeatures"
|
||||
import * as legacyContractHandler from "./2016/legacyContractHandler"
|
||||
import * as legacyMenuData from "./2016/legacyMenuData"
|
||||
import * as legacyMenuSystem from "./2016/legacyMenuSystem"
|
||||
import * as legacyProfileRouter from "./2016/legacyProfileRouter"
|
||||
import * as challengeHelpers from "./candle/challengeHelpers"
|
||||
import * as challengeService from "./candle/challengeService"
|
||||
@ -57,7 +55,7 @@ import * as elusiveTargets from "./contracts/elusiveTargets"
|
||||
import * as hitsCategoryService from "./contracts/hitsCategoryService"
|
||||
import * as leaderboards from "./contracts/leaderboards"
|
||||
import * as missionsInLocation from "./contracts/missionsInLocation"
|
||||
import * as reportRouting from "./contracts/reportRouting"
|
||||
import * as sessions from "./contracts/sessions"
|
||||
import * as client from "./discord/client"
|
||||
import * as ipc from "./discord/ipc"
|
||||
import * as liveSplitClient from "./livesplit/liveSplitClient"
|
||||
@ -69,6 +67,7 @@ import * as hub from "./menus/hub"
|
||||
import * as imageHandler from "./menus/imageHandler"
|
||||
import * as menuSystem from "./menus/menuSystem"
|
||||
import * as planning from "./menus/planning"
|
||||
import * as playerProfile from "./menus/playerProfile"
|
||||
import * as playnext from "./menus/playnext"
|
||||
import * as sniper from "./menus/sniper"
|
||||
import * as stashpoints from "./menus/stashpoints"
|
||||
@ -125,10 +124,6 @@ export default {
|
||||
...profileHandler,
|
||||
},
|
||||
"@peacockproject/core/scoreHandler": { __esModule: true, ...scoreHandler },
|
||||
"@peacockproject/core/sessionSerialization": {
|
||||
__esModule: true,
|
||||
...sessionSerialization,
|
||||
},
|
||||
"@peacockproject/core/smfSupport": { __esModule: true, ...smfSupport },
|
||||
"@peacockproject/core/utils": { __esModule: true, ...utils },
|
||||
"@peacockproject/core/webFeatures": { __esModule: true, ...webFeatures },
|
||||
@ -140,10 +135,6 @@ export default {
|
||||
__esModule: true,
|
||||
...legacyMenuData,
|
||||
},
|
||||
"@peacockproject/core/2016/legacyMenuSystem": {
|
||||
__esModule: true,
|
||||
...legacyMenuSystem,
|
||||
},
|
||||
"@peacockproject/core/2016/legacyProfileRouter": {
|
||||
__esModule: true,
|
||||
...legacyProfileRouter,
|
||||
@ -193,9 +184,9 @@ export default {
|
||||
__esModule: true,
|
||||
...missionsInLocation,
|
||||
},
|
||||
"@peacockproject/core/contracts/reportRouting": {
|
||||
"@peacockproject/core/contracts/sessions": {
|
||||
__esModule: true,
|
||||
...reportRouting,
|
||||
...sessions,
|
||||
},
|
||||
"@peacockproject/core/discord/client": { __esModule: true, ...client },
|
||||
"@peacockproject/core/discord/ipc": { __esModule: true, ...ipc },
|
||||
@ -226,6 +217,10 @@ export default {
|
||||
...menuSystem,
|
||||
},
|
||||
"@peacockproject/core/menus/planning": { __esModule: true, ...planning },
|
||||
"@peacockproject/core/menus/playerProfile": {
|
||||
__esModule: true,
|
||||
...playerProfile,
|
||||
},
|
||||
"@peacockproject/core/menus/playnext": { __esModule: true, ...playnext },
|
||||
"@peacockproject/core/menus/sniper": { __esModule: true, ...sniper },
|
||||
"@peacockproject/core/menus/stashpoints": {
|
||||
|
@ -89,7 +89,7 @@ export interface Intercept<Params, Return> {
|
||||
* @param context The context object. 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.
|
||||
@ -108,8 +108,8 @@ export interface Intercept<Params, Return> {
|
||||
* @see AsyncSeriesHook
|
||||
*/
|
||||
export abstract class BaseImpl<Params, Return = void> {
|
||||
protected _intercepts: Intercept<Params, Return>[]
|
||||
protected _taps: Tap<Params, Return>[]
|
||||
protected _intercepts!: Intercept<Params, Return>[]
|
||||
protected _taps!: Tap<Params, Return>[]
|
||||
|
||||
/**
|
||||
* Register an interceptor.
|
||||
|
@ -16,8 +16,6 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// noinspection RequiredAttributes
|
||||
|
||||
// load as soon as possible to prevent dependency issues
|
||||
import "./generatedPeacockRequireTable"
|
||||
|
||||
@ -26,7 +24,6 @@ import { getFlag, loadFlags } from "./flags"
|
||||
|
||||
loadFlags()
|
||||
|
||||
import { setFlagsFromString } from "v8"
|
||||
import { program } from "commander"
|
||||
import express, { Request, Router } from "express"
|
||||
import http from "http"
|
||||
@ -41,7 +38,12 @@ import {
|
||||
ServerVer,
|
||||
} from "./utils"
|
||||
import { getConfig } from "./configSwizzleManager"
|
||||
import { handleOauthToken } from "./oauthToken"
|
||||
import {
|
||||
error400,
|
||||
error406,
|
||||
handleOAuthToken,
|
||||
OAuthTokenBody,
|
||||
} from "./oauthToken"
|
||||
import type {
|
||||
RequestWithJwt,
|
||||
S2CEventWithTimestamp,
|
||||
@ -61,7 +63,6 @@ import { contractRoutingRouter } from "./contracts/contractRouting"
|
||||
import { profileRouter } from "./profileHandler"
|
||||
import { menuDataRouter } from "./menuData"
|
||||
import { menuSystemPreRouter, menuSystemRouter } from "./menus/menuSystem"
|
||||
import { legacyMenuSystemRouter } from "./2016/legacyMenuSystem"
|
||||
import { _theLastYardbirdScpc, controller } from "./controller"
|
||||
import {
|
||||
STEAM_NAMESPACE_2016,
|
||||
@ -88,10 +89,8 @@ import { multiplayerMenuDataRouter } from "./multiplayer/multiplayerMenuData"
|
||||
import { pack, unpack } from "msgpackr"
|
||||
import { liveSplitManager } from "./livesplit/liveSplitManager"
|
||||
import { cheapLoadUserData } from "./databaseHandler"
|
||||
import { reportRouter } from "./contracts/reportRouting"
|
||||
|
||||
// welcome to the bleeding edge
|
||||
setFlagsFromString("--harmony")
|
||||
loadFlags()
|
||||
|
||||
const host = process.env.HOST || "0.0.0.0"
|
||||
const port = process.env.PORT || 80
|
||||
@ -145,12 +144,13 @@ app.get("/", (_: Request, res) => {
|
||||
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>',
|
||||
)
|
||||
} else {
|
||||
const data = readFileSync("webui/dist/index.html").toString()
|
||||
|
||||
res.contentType("text/html")
|
||||
res.send(data)
|
||||
return
|
||||
}
|
||||
|
||||
const data = readFileSync("webui/dist/index.html").toString()
|
||||
|
||||
res.contentType("text/html")
|
||||
res.send(data)
|
||||
})
|
||||
|
||||
serveStatic.mime.define({ "application/javascript": ["js"] })
|
||||
@ -169,6 +169,7 @@ if (getFlag("loadoutSaving") === "PROFILES") {
|
||||
|
||||
app.get(
|
||||
"/config/:audience/:serverVersion(\\d+_\\d+_\\d+)",
|
||||
// @ts-expect-error Has jwt props.
|
||||
(req: RequestWithJwt<{ issuer: string }>, res) => {
|
||||
const proto = req.protocol
|
||||
const config = getConfig(
|
||||
@ -177,11 +178,13 @@ app.get(
|
||||
) as ServerConnectionConfig
|
||||
const serverhost = req.get("Host")
|
||||
|
||||
config.Versions[0].GAME_VER = req.params.serverVersion.startsWith("8")
|
||||
? `${ServerVer._Major}.${ServerVer._Minor}.${ServerVer._Build}`
|
||||
: req.params.serverVersion.startsWith("7")
|
||||
? "7.17.0"
|
||||
: "6.74.0"
|
||||
config.Versions[0].GAME_VER = "6.74.0"
|
||||
|
||||
if (req.params.serverVersion.startsWith("8")) {
|
||||
req.params.serverVersion = `${ServerVer._Major}.${ServerVer._Minor}.${ServerVer._Build}`
|
||||
} else if (req.params.serverVersion.startsWith("7")) {
|
||||
req.params.serverVersion = "7.17.0"
|
||||
}
|
||||
|
||||
if (req.params.serverVersion.startsWith("8")) {
|
||||
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("x-ms-meta-version", "20181001")
|
||||
res.send(getConfig("PrivacyPolicy", false))
|
||||
@ -238,6 +241,7 @@ app.get("/files/privacypolicy/hm3/privacypolicy_*.json", (req, res) => {
|
||||
app.post(
|
||||
"/api/metrics/*",
|
||||
jsonMiddleware({ limit: "10Mb" }),
|
||||
// @ts-expect-error jwt props.
|
||||
(req: RequestWithJwt<never, S2CEventWithTimestamp[]>, res) => {
|
||||
for (const event of req.body) {
|
||||
controller.hooks.newMetricsEvent.call(event, req)
|
||||
@ -247,8 +251,26 @@ app.post(
|
||||
},
|
||||
)
|
||||
|
||||
app.post("/oauth/token", urlencoded(), (req: RequestWithJwt, res) =>
|
||||
handleOauthToken(req, res),
|
||||
app.post(
|
||||
"/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) => {
|
||||
@ -263,15 +285,16 @@ app.use(
|
||||
Router()
|
||||
.use(
|
||||
"/resources-:serverVersion(\\d+-\\d+)/",
|
||||
(req: RequestWithJwt, res, next) => {
|
||||
// @ts-expect-error Has jwt props.
|
||||
(req: RequestWithJwt, _, next) => {
|
||||
req.serverVersion = req.params.serverVersion
|
||||
req.gameVersion = req.serverVersion.startsWith("8")
|
||||
? "h3"
|
||||
: req.serverVersion.startsWith("7")
|
||||
? // prettier-ignore
|
||||
"h2"
|
||||
: // prettier-ignore
|
||||
"h1"
|
||||
req.gameVersion = "h1"
|
||||
|
||||
if (req.serverVersion.startsWith("8")) {
|
||||
req.gameVersion = "h3"
|
||||
} else if (req.serverVersion.startsWith("7")) {
|
||||
req.gameVersion = "h2"
|
||||
}
|
||||
|
||||
if (req.serverVersion === "7.3.0") {
|
||||
req.gameVersion = "scpc"
|
||||
@ -281,6 +304,7 @@ app.use(
|
||||
},
|
||||
)
|
||||
// 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) => {
|
||||
switch (req.jwt?.pis) {
|
||||
case "egp_io_interactive_hitman_the_complete_first_season":
|
||||
@ -300,11 +324,13 @@ app.use(
|
||||
return
|
||||
}
|
||||
|
||||
req.gameVersion = req.serverVersion.startsWith("8")
|
||||
? "h3"
|
||||
: req.serverVersion.startsWith("7")
|
||||
? "h2"
|
||||
: "h1"
|
||||
req.gameVersion = "h1"
|
||||
|
||||
if (req.serverVersion.startsWith("8")) {
|
||||
req.gameVersion = "h3"
|
||||
} else if (req.serverVersion.startsWith("7")) {
|
||||
req.gameVersion = "h2"
|
||||
}
|
||||
|
||||
if (req.jwt?.aud === "scpc-prod") {
|
||||
req.gameVersion = "scpc"
|
||||
@ -316,6 +342,7 @@ app.use(
|
||||
|
||||
app.get(
|
||||
"/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) => {
|
||||
res.json({
|
||||
template: getConfig("FrankensteinMmSpTemplate", false),
|
||||
@ -339,6 +366,7 @@ app.get(
|
||||
// We handle this for now, but it's not used. For the future though.
|
||||
app.get(
|
||||
"/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) => {
|
||||
const template = getConfig("FrankensteinMmMpTemplate", false)
|
||||
|
||||
@ -371,6 +399,7 @@ app.get(
|
||||
)
|
||||
|
||||
if (PEACOCK_DEV) {
|
||||
// @ts-expect-error Has jwt props.
|
||||
app.use(async (req: RequestWithJwt, _res, next): Promise<void> => {
|
||||
if (!req.jwt) {
|
||||
next()
|
||||
@ -397,6 +426,7 @@ function generateBlobConfig(req: RequestWithJwt) {
|
||||
|
||||
app.get(
|
||||
"/authentication/api/configuration/Init?*",
|
||||
// @ts-expect-error jwt props.
|
||||
extractToken,
|
||||
(req: RequestWithJwt, res) => {
|
||||
// configName=pc-prod&lockedContentDisabled=false&isFreePrologueUser=false&isIntroPackUser=false&isFullExperienceUser=false
|
||||
@ -413,6 +443,7 @@ app.get(
|
||||
|
||||
app.post(
|
||||
"/authentication/api/userchannel/AuthenticationService/RenewBlobSignature",
|
||||
// @ts-expect-error jwt props.
|
||||
(req: RequestWithJwt, res) => {
|
||||
res.json(generateBlobConfig(req))
|
||||
},
|
||||
@ -421,7 +452,6 @@ app.post(
|
||||
const legacyRouter = Router()
|
||||
const primaryRouter = Router()
|
||||
|
||||
legacyRouter.use("/resources-(\\d+-\\d+)/", legacyMenuSystemRouter)
|
||||
legacyRouter.use("/authentication/api/userchannel/", legacyProfileRouter)
|
||||
legacyRouter.use("/profiles/page/", legacyMenuDataRouter)
|
||||
legacyRouter.use(
|
||||
@ -442,9 +472,12 @@ primaryRouter.use(
|
||||
"/authentication/api/userchannel/ContractsService/",
|
||||
contractRoutingRouter,
|
||||
)
|
||||
primaryRouter.use(
|
||||
"/authentication/api/userchannel/ReportingService/",
|
||||
reportRouter,
|
||||
primaryRouter.get(
|
||||
"/authentication/api/userchannel/ReportingService/ReportContract",
|
||||
(_, res) => {
|
||||
// TODO
|
||||
res.json({})
|
||||
},
|
||||
)
|
||||
primaryRouter.use("/authentication/api/userchannel/", profileRouter)
|
||||
primaryRouter.use("/profiles/page", multiplayerMenuDataRouter)
|
||||
@ -454,6 +487,7 @@ primaryRouter.use("/resources-(\\d+-\\d+)/", menuSystemRouter)
|
||||
|
||||
app.use(
|
||||
Router()
|
||||
// @ts-expect-error Has jwt props.
|
||||
.use((req: RequestWithJwt, _, next) => {
|
||||
if (req.shouldCease) {
|
||||
return next("router")
|
||||
@ -467,6 +501,7 @@ app.use(
|
||||
})
|
||||
.use(legacyRouter),
|
||||
Router()
|
||||
// @ts-expect-error Has jwt props.
|
||||
.use((req: RequestWithJwt, _, next) => {
|
||||
if (req.shouldCease) {
|
||||
return next("router")
|
||||
@ -493,28 +528,15 @@ app.all("*", (req, res) => {
|
||||
app.use(errorLoggingMiddleware)
|
||||
|
||||
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 {
|
||||
checkForUpdates()
|
||||
void checkForUpdates()
|
||||
|
||||
if (!IS_LAUNCHER) {
|
||||
console.log(
|
||||
Math.random() < 0.001
|
||||
? PEECOCK_ART
|
||||
: picocolors.greenBright(`
|
||||
picocolors.greenBright(`
|
||||
███████████ ██████████ █████████ █████████ ███████ █████████ █████ ████
|
||||
░░███░░░░░███░░███░░░░░█ ███░░░░░███ ███░░░░░███ ███░░░░░███ ███░░░░░███░░███ ███░
|
||||
░███ ░███ ░███ █ ░ ░███ ░███ ███ ░░░ ███ ░░███ ███ ░░░ ░███ ███
|
||||
@ -576,7 +598,7 @@ function startServer(options: { hmr: boolean; pluginDevHost: boolean }): void {
|
||||
if (options.hmr) {
|
||||
log(LogLevel.DEBUG, "Experimental HMR enabled.")
|
||||
|
||||
setupHotListener("contracts", () => {
|
||||
void setupHotListener("contracts", () => {
|
||||
log(LogLevel.INFO, "Detected a change in contracts! Re-indexing...")
|
||||
controller.index()
|
||||
})
|
||||
@ -584,7 +606,7 @@ function startServer(options: { hmr: boolean; pluginDevHost: boolean }): void {
|
||||
|
||||
// once contracts directory is present, we are clear to boot
|
||||
loadouts.init()
|
||||
controller.boot(options.pluginDevHost)
|
||||
void controller.boot(options.pluginDevHost)
|
||||
|
||||
const httpServer = http.createServer(app)
|
||||
|
||||
@ -597,7 +619,7 @@ function startServer(options: { hmr: boolean; pluginDevHost: boolean }): void {
|
||||
}
|
||||
|
||||
// initialize livesplit
|
||||
liveSplitManager.init()
|
||||
void liveSplitManager.init()
|
||||
}
|
||||
|
||||
program.option(
|
||||
@ -616,9 +638,10 @@ program
|
||||
.command("tools")
|
||||
.description("open the tools UI")
|
||||
.action(() => {
|
||||
toolsMenu()
|
||||
void toolsMenu()
|
||||
})
|
||||
|
||||
// noinspection RequiredAttributes
|
||||
program
|
||||
.command("pack")
|
||||
.argument("<input>", "input file to pack")
|
||||
@ -636,6 +659,7 @@ program
|
||||
log(LogLevel.INFO, `Packed "${input}" to "${outputPath}" successfully.`)
|
||||
})
|
||||
|
||||
// noinspection RequiredAttributes
|
||||
program
|
||||
.command("unpack")
|
||||
.argument("<input>", "input file to unpack")
|
||||
|
@ -106,7 +106,7 @@ export function clearInventoryCache(): void {
|
||||
function filterUnlockedContent(
|
||||
userProfile: UserProfile,
|
||||
packagedUnlocks: Map<string, boolean>,
|
||||
challengesUnlockables: object,
|
||||
challengesUnlockables: Record<string, string>,
|
||||
gameVersion: GameVersion,
|
||||
) {
|
||||
return function (
|
||||
@ -114,7 +114,7 @@ function filterUnlockedContent(
|
||||
unlockable: Unlockable,
|
||||
) {
|
||||
let unlockableChallengeId: string
|
||||
let unlockableMasteryData: UnlockableMasteryData
|
||||
let unlockableMasteryData: UnlockableMasteryData | undefined
|
||||
|
||||
// Handles unlockables that belong to a package or unlocked gear from evergreen
|
||||
if (packagedUnlocks.has(unlockable.Id)) {
|
||||
@ -123,7 +123,7 @@ function filterUnlockedContent(
|
||||
|
||||
// Handles packages
|
||||
else if (unlockable.Type === "package") {
|
||||
for (const pkgUnlockableId of unlockable.Properties.Unlocks) {
|
||||
for (const pkgUnlockableId of unlockable.Properties.Unlocks || []) {
|
||||
packagedUnlocks.set(pkgUnlockableId, true)
|
||||
}
|
||||
|
||||
@ -199,7 +199,7 @@ function filterUnlockedContent(
|
||||
* @returns boolean
|
||||
*/
|
||||
function filterAllowedContent(gameVersion: GameVersion, entP: string[]) {
|
||||
return function (unlockContainer: {
|
||||
return function (unlockContainer?: {
|
||||
InstanceId: string
|
||||
ProfileId: string
|
||||
Unlockable: Unlockable
|
||||
@ -474,21 +474,25 @@ function updateWithDefaultSuit(
|
||||
profileId: string,
|
||||
gameVersion: GameVersion,
|
||||
inv: InventoryItem[],
|
||||
sublocation: Unlockable,
|
||||
sublocation?: Unlockable,
|
||||
): InventoryItem[] {
|
||||
if (sublocation === undefined) {
|
||||
if (!sublocation) {
|
||||
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.
|
||||
const locationSuit = getUnlockableById(
|
||||
getDefaultSuitFor(sublocation),
|
||||
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
|
||||
if (newInv.every((i) => i.Unlockable.Id !== locationSuit.Id)) {
|
||||
// if not, add it
|
||||
@ -576,7 +580,7 @@ export function createInventory(
|
||||
// and location-wide default suits will be given afterwards.
|
||||
const defaults = Object.values(defaultSuits)
|
||||
|
||||
if ((getFlag("getDefaultSuits") as boolean) === false) {
|
||||
if (!getFlag("getDefaultSuits")) {
|
||||
unlockables = unlockables.filter(
|
||||
(u) =>
|
||||
!defaults.includes(u.Id) ||
|
||||
@ -584,8 +588,7 @@ export function createInventory(
|
||||
)
|
||||
}
|
||||
|
||||
// ts-expect-error It cannot be undefined.
|
||||
const filtered: InventoryItem[] = unlockables
|
||||
const filtered = unlockables
|
||||
.map((unlockable) => {
|
||||
if (brokenItems.includes(unlockable.Guid)) {
|
||||
return undefined
|
||||
@ -601,7 +604,9 @@ export function createInventory(
|
||||
}
|
||||
})
|
||||
// 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) {
|
||||
unlockable!.ProfileId = profileId
|
||||
@ -627,7 +632,7 @@ export function grantDrops(profileId: string, drops: Unlockable[]): void {
|
||||
|
||||
inventoryUserCache.set(profileId, [
|
||||
...new Set([
|
||||
...inventoryUserCache.get(profileId),
|
||||
...(inventoryUserCache.get(profileId) || []),
|
||||
...inventoryItems.filter(
|
||||
(invItem) => invItem.Unlockable.Type !== "evergreenmastery",
|
||||
),
|
||||
|
@ -50,14 +50,7 @@ const defaultValue: LoadoutFile = {
|
||||
* A class for managing loadouts.
|
||||
*/
|
||||
export class Loadouts {
|
||||
private _loadouts: LoadoutFile
|
||||
|
||||
/**
|
||||
* Creates a new instance of the class.
|
||||
*/
|
||||
public constructor() {
|
||||
this._loadouts = undefined
|
||||
}
|
||||
private _loadouts!: LoadoutFile
|
||||
|
||||
/**
|
||||
* 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 (
|
||||
!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
|
||||
|
||||
@ -121,7 +117,7 @@ export class Loadouts {
|
||||
}
|
||||
}
|
||||
|
||||
if (dirty === true) {
|
||||
if (dirty) {
|
||||
writeFileSync(LOADOUT_PROFILES_FILE, JSON.stringify(this._loadouts))
|
||||
}
|
||||
}
|
||||
@ -217,7 +213,7 @@ loadoutRouter.patch(
|
||||
async (
|
||||
req: Request<
|
||||
never,
|
||||
string,
|
||||
string | { error?: string; message?: string },
|
||||
{ gameVersion: "h1" | "h2" | "h3"; id: string }
|
||||
>,
|
||||
res,
|
||||
|
@ -16,8 +16,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type { NextFunction, Response } from "express"
|
||||
import type { RequestWithJwt } from "./types/types"
|
||||
import type { NextFunction, Request, Response } from "express"
|
||||
import picocolors from "picocolors"
|
||||
import winston from "winston"
|
||||
import "winston-daily-rotate-file"
|
||||
@ -135,6 +134,7 @@ if (consoleLogLevel !== LOG_LEVEL_NONE) {
|
||||
}
|
||||
|
||||
const winstonLogLevel = {}
|
||||
// @ts-expect-error Type mismatch.
|
||||
Object.values(LogLevel).forEach((e, i) => (winstonLogLevel[e] = i))
|
||||
|
||||
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.
|
||||
*
|
||||
* @param req The Express request object.
|
||||
* @param res The Express response object.
|
||||
* @param _ The Express response object.
|
||||
* @param next The Express next function.
|
||||
* @see LogLevel.INFO
|
||||
*/
|
||||
export function loggingMiddleware(
|
||||
req: RequestWithJwt,
|
||||
res: Response,
|
||||
req: Request,
|
||||
_: Response,
|
||||
next?: NextFunction,
|
||||
): void {
|
||||
log(
|
||||
@ -273,7 +273,7 @@ export function loggingMiddleware(
|
||||
}
|
||||
|
||||
export function requestLoggingMiddleware(
|
||||
req: RequestWithJwt,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next?: NextFunction,
|
||||
): void {
|
||||
@ -294,8 +294,8 @@ export function requestLoggingMiddleware(
|
||||
|
||||
export function errorLoggingMiddleware(
|
||||
err: Error,
|
||||
req: RequestWithJwt,
|
||||
res: Response,
|
||||
req: Request,
|
||||
_: Response,
|
||||
next?: NextFunction,
|
||||
): void {
|
||||
const debug = {
|
||||
|
@ -21,7 +21,6 @@ import { Response, Router } from "express"
|
||||
import {
|
||||
contractCreationTutorialId,
|
||||
getMaxProfileLevel,
|
||||
isSniperLocation,
|
||||
isSuit,
|
||||
unlockOrderComparer,
|
||||
uuidRegex,
|
||||
@ -29,22 +28,15 @@ import {
|
||||
import { contractSessions, getSession } from "./eventHandler"
|
||||
import { getConfig, getVersionedConfig } from "./configSwizzleManager"
|
||||
import { controller } from "./controller"
|
||||
import {
|
||||
createLocationsData,
|
||||
getDestination,
|
||||
getDestinationCompletion,
|
||||
} from "./menus/destinations"
|
||||
import { createLocationsData, getDestination } from "./menus/destinations"
|
||||
import type {
|
||||
ChallengeCategoryCompletion,
|
||||
SelectEntranceOrPickupData,
|
||||
ContractSearchResult,
|
||||
GameVersion,
|
||||
HitsCategoryCategory,
|
||||
PeacockLocationsData,
|
||||
PlayerProfileView,
|
||||
ProgressionData,
|
||||
RequestWithJwt,
|
||||
SceneConfig,
|
||||
SelectEntranceOrPickupData,
|
||||
UserCentricContract,
|
||||
} from "./types/types"
|
||||
import {
|
||||
@ -79,7 +71,9 @@ import {
|
||||
DebriefingLeaderboardsQuery,
|
||||
GetCompletionDataForLocationQuery,
|
||||
GetDestinationQuery,
|
||||
GetMasteryCompletionDataForUnlockableQuery,
|
||||
LeaderboardEntriesCommonQuery,
|
||||
LookupContractPublicIdQuery,
|
||||
MasteryUnlockableQuery,
|
||||
MissionEndRequestQuery,
|
||||
PlanningQuery,
|
||||
@ -97,6 +91,7 @@ import {
|
||||
getSafehouseCategory,
|
||||
} from "./menus/stashpoints"
|
||||
import { getHubData } from "./menus/hub"
|
||||
import { getPlayerProfileData } from "./menus/playerProfile"
|
||||
|
||||
const menuDataRouter = Router()
|
||||
|
||||
@ -104,18 +99,19 @@ const menuDataRouter = Router()
|
||||
|
||||
menuDataRouter.get(
|
||||
"/ChallengeLocation",
|
||||
// @ts-expect-error Jwt props.
|
||||
(req: RequestWithJwt<ChallengeLocationQuery>, res) => {
|
||||
if (typeof req.query.locationId !== "string") {
|
||||
res.status(400).send("Invalid locationId")
|
||||
return
|
||||
}
|
||||
|
||||
const location = getVersionedConfig<PeacockLocationsData>(
|
||||
"LocationsData",
|
||||
req.gameVersion,
|
||||
true,
|
||||
).children[req.query.locationId]
|
||||
|
||||
if (!location) {
|
||||
res.status(400).send("Invalid locationId")
|
||||
return
|
||||
}
|
||||
|
||||
const data = {
|
||||
Name: location.DisplayNameLocKey,
|
||||
Location: location,
|
||||
@ -137,17 +133,20 @@ menuDataRouter.get(
|
||||
},
|
||||
)
|
||||
|
||||
// @ts-expect-error Jwt props.
|
||||
menuDataRouter.get("/Hub", (req: RequestWithJwt, res) => {
|
||||
const hubInfo = getHubData(req.gameVersion, req.jwt)
|
||||
const hubInfo = getHubData(req.gameVersion, req.jwt.unique_name)
|
||||
|
||||
const template =
|
||||
req.gameVersion === "h3"
|
||||
? null
|
||||
: req.gameVersion === "h2"
|
||||
? null
|
||||
: req.gameVersion === "scpc"
|
||||
? getConfig("FrankensteinHubTemplate", false)
|
||||
: getConfig("LegacyHubTemplate", false)
|
||||
let template: unknown
|
||||
|
||||
if (req.gameVersion === "h3" || req.gameVersion === "h2") {
|
||||
template = null
|
||||
} else {
|
||||
template =
|
||||
req.gameVersion === "scpc"
|
||||
? getConfig("FrankensteinHubTemplate", false)
|
||||
: getConfig("LegacyHubTemplate", false)
|
||||
}
|
||||
|
||||
res.json({
|
||||
template,
|
||||
@ -155,6 +154,7 @@ menuDataRouter.get("/Hub", (req: RequestWithJwt, res) => {
|
||||
})
|
||||
})
|
||||
|
||||
// @ts-expect-error Jwt props.
|
||||
menuDataRouter.get("/SafehouseCategory", (req: RequestWithJwt, res) => {
|
||||
res.json({
|
||||
template:
|
||||
@ -165,6 +165,7 @@ menuDataRouter.get("/SafehouseCategory", (req: RequestWithJwt, res) => {
|
||||
})
|
||||
})
|
||||
|
||||
// @ts-expect-error Jwt props.
|
||||
menuDataRouter.get("/Safehouse", (req: RequestWithJwt<SafehouseQuery>, res) => {
|
||||
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) => {
|
||||
res.json({
|
||||
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
|
||||
menuDataRouter.get(
|
||||
"/stashpoint",
|
||||
// @ts-expect-error Jwt props.
|
||||
(req: RequestWithJwt<StashpointQuery | StashpointQueryH2016>, res) => {
|
||||
function isValidModernQuery(
|
||||
query: StashpointQuery | StashpointQueryH2016,
|
||||
@ -214,7 +217,7 @@ menuDataRouter.get(
|
||||
|
||||
if (["h1", "scpc"].includes(req.gameVersion)) {
|
||||
// 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")
|
||||
return
|
||||
}
|
||||
@ -264,15 +267,22 @@ menuDataRouter.get(
|
||||
|
||||
menuDataRouter.get(
|
||||
"/missionrewards",
|
||||
// @ts-expect-error Jwt props.
|
||||
(
|
||||
req: RequestWithJwt<{
|
||||
contractSessionId: string
|
||||
}>,
|
||||
res,
|
||||
) => {
|
||||
const { contractId } = getSession(req.jwt.unique_name)
|
||||
const contractData = controller.resolveContract(contractId, true)
|
||||
const s = getSession(req.jwt.unique_name)
|
||||
|
||||
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)
|
||||
|
||||
res.json({
|
||||
@ -321,7 +331,7 @@ menuDataRouter.get(
|
||||
LocationHideProgression: true,
|
||||
Difficulty: "normal", // FIXME: is this right?
|
||||
CompletionData: generateCompletionData(
|
||||
contractData.Metadata.Location,
|
||||
contractData?.Metadata.Location || "",
|
||||
req.jwt.unique_name,
|
||||
req.gameVersion,
|
||||
),
|
||||
@ -332,6 +342,7 @@ menuDataRouter.get(
|
||||
|
||||
menuDataRouter.get(
|
||||
"/Planning",
|
||||
// @ts-expect-error Jwt props.
|
||||
async (req: RequestWithJwt<PlanningQuery>, res) => {
|
||||
if (!req.query.contractid || !req.query.resetescalation) {
|
||||
res.status(400).send("invalid query")
|
||||
@ -341,7 +352,7 @@ menuDataRouter.get(
|
||||
const planningData = await getPlanningData(
|
||||
req.query.contractid,
|
||||
req.query.resetescalation === "true",
|
||||
req.jwt,
|
||||
req.jwt.unique_name,
|
||||
req.gameVersion,
|
||||
)
|
||||
|
||||
@ -350,13 +361,16 @@ menuDataRouter.get(
|
||||
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({
|
||||
template:
|
||||
req.gameVersion === "h1"
|
||||
? getConfig("LegacyPlanningTemplate", false)
|
||||
: req.gameVersion === "scpc"
|
||||
? getConfig("FrankensteinPlanningTemplate", false)
|
||||
: null,
|
||||
template,
|
||||
data: planningData,
|
||||
})
|
||||
},
|
||||
@ -364,6 +378,7 @@ menuDataRouter.get(
|
||||
|
||||
menuDataRouter.get(
|
||||
"/selectagencypickup",
|
||||
// @ts-expect-error Jwt props.
|
||||
(
|
||||
req: RequestWithJwt<{
|
||||
contractId: string
|
||||
@ -407,7 +422,7 @@ menuDataRouter.get(
|
||||
contractData,
|
||||
req.jwt.unique_name,
|
||||
req.gameVersion,
|
||||
),
|
||||
)!,
|
||||
}
|
||||
|
||||
res.json({
|
||||
@ -440,14 +455,16 @@ menuDataRouter.get(
|
||||
Contract: contractData,
|
||||
OrderedUnlocks: unlockedAgencyPickups
|
||||
.filter((unlockable) =>
|
||||
pickupsInScene.includes(unlockable.Properties.RepositoryId),
|
||||
pickupsInScene.includes(
|
||||
unlockable.Properties.RepositoryId || "",
|
||||
),
|
||||
)
|
||||
.sort(unlockOrderComparer),
|
||||
UserCentric: generateUserCentric(
|
||||
contractData,
|
||||
req.jwt.unique_name,
|
||||
req.gameVersion,
|
||||
),
|
||||
)!,
|
||||
}
|
||||
|
||||
res.json({
|
||||
@ -463,6 +480,7 @@ menuDataRouter.get(
|
||||
|
||||
menuDataRouter.get(
|
||||
"/selectentrance",
|
||||
// @ts-expect-error Jwt props.
|
||||
(
|
||||
req: RequestWithJwt<{
|
||||
contractId: string
|
||||
@ -522,7 +540,7 @@ menuDataRouter.get(
|
||||
contractData,
|
||||
req.jwt.unique_name,
|
||||
req.gameVersion,
|
||||
),
|
||||
)!,
|
||||
}
|
||||
|
||||
res.json({
|
||||
@ -580,6 +598,12 @@ const missionEndRequest = async (
|
||||
return
|
||||
}
|
||||
|
||||
// prototype pollution prevention
|
||||
if (/(__proto__|prototype|constructor)/.test(req.query.contractSessionId)) {
|
||||
res.status(400).send("invalid session id")
|
||||
return
|
||||
}
|
||||
|
||||
const missionEndOutput = await getMissionEndData(
|
||||
req.query,
|
||||
req.jwt,
|
||||
@ -613,21 +637,29 @@ const missionEndRequest = async (
|
||||
})
|
||||
}
|
||||
|
||||
// @ts-expect-error Has jwt props.
|
||||
menuDataRouter.get("/missionend", missionEndRequest)
|
||||
|
||||
// @ts-expect-error Has jwt props.
|
||||
menuDataRouter.get("/scoreoverviewandunlocks", missionEndRequest)
|
||||
|
||||
// @ts-expect-error Has jwt props.
|
||||
menuDataRouter.get("/scoreoverview", missionEndRequest)
|
||||
|
||||
menuDataRouter.get(
|
||||
"/Destination",
|
||||
// @ts-expect-error Jwt props.
|
||||
(req: RequestWithJwt<GetDestinationQuery>, res) => {
|
||||
if (!req.query.locationId) {
|
||||
res.status(400).send("Invalid locationId")
|
||||
return
|
||||
}
|
||||
|
||||
const destination = getDestination(req.query, req.gameVersion, req.jwt)
|
||||
const destination = getDestination(
|
||||
req.query,
|
||||
req.gameVersion,
|
||||
req.jwt.unique_name,
|
||||
)
|
||||
|
||||
res.json({
|
||||
template:
|
||||
@ -681,13 +713,9 @@ async function lookupContractPublicId(
|
||||
|
||||
menuDataRouter.get(
|
||||
"/LookupContractPublicId",
|
||||
async (
|
||||
req: RequestWithJwt<{
|
||||
publicid: string
|
||||
}>,
|
||||
res,
|
||||
) => {
|
||||
if (!req.query.publicid || typeof req.query.publicid !== "string") {
|
||||
// @ts-expect-error Has jwt props.
|
||||
async (req: RequestWithJwt<LookupContractPublicIdQuery>, res) => {
|
||||
if (typeof req.query.publicid !== "string") {
|
||||
return res.status(400).send("no/invalid public id specified!")
|
||||
}
|
||||
|
||||
@ -708,6 +736,7 @@ menuDataRouter.get(
|
||||
|
||||
menuDataRouter.get(
|
||||
"/HitsCategory",
|
||||
// @ts-expect-error Has jwt props.
|
||||
async (
|
||||
req: RequestWithJwt<{
|
||||
type: string
|
||||
@ -749,6 +778,7 @@ menuDataRouter.get(
|
||||
|
||||
menuDataRouter.get(
|
||||
"/PlayNext",
|
||||
// @ts-expect-error Has jwt props.
|
||||
(
|
||||
req: RequestWithJwt<{
|
||||
contractId: string
|
||||
@ -764,14 +794,14 @@ menuDataRouter.get(
|
||||
template: getConfig("PlayNextTemplate", false),
|
||||
data: getGamePlayNextData(
|
||||
req.query.contractId,
|
||||
req.jwt,
|
||||
req.jwt.unique_name,
|
||||
req.gameVersion,
|
||||
),
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
menuDataRouter.get("/LeaderboardsView", (req, res) => {
|
||||
menuDataRouter.get("/LeaderboardsView", (_, res) => {
|
||||
res.json({
|
||||
template: getConfig("LeaderboardsViewTemplate", false),
|
||||
data: {
|
||||
@ -783,6 +813,7 @@ menuDataRouter.get("/LeaderboardsView", (req, res) => {
|
||||
|
||||
menuDataRouter.get(
|
||||
"/LeaderboardEntries",
|
||||
// @ts-expect-error Has jwt props.
|
||||
async (req: RequestWithJwt<LeaderboardEntriesCommonQuery>, res) => {
|
||||
if (!req.query.contractid) {
|
||||
res.status(400).send("no contract id!")
|
||||
@ -810,6 +841,7 @@ menuDataRouter.get(
|
||||
|
||||
menuDataRouter.get(
|
||||
"/DebriefingLeaderboards",
|
||||
// @ts-expect-error Has jwt props.
|
||||
async (req: RequestWithJwt<DebriefingLeaderboardsQuery>, res) => {
|
||||
if (!req.query.contractid) {
|
||||
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(
|
||||
"/contractcreation/planning",
|
||||
// @ts-expect-error Has jwt props.
|
||||
async (
|
||||
req: RequestWithJwt<{
|
||||
contractCreationIdOverwrite: string
|
||||
@ -858,7 +892,7 @@ menuDataRouter.get(
|
||||
const planningData = await getPlanningData(
|
||||
req.query.contractCreationIdOverwrite,
|
||||
false,
|
||||
req.jwt,
|
||||
req.jwt.unique_name,
|
||||
req.gameVersion,
|
||||
)
|
||||
|
||||
@ -890,6 +924,7 @@ menuDataRouter.get(
|
||||
},
|
||||
)
|
||||
|
||||
// @ts-expect-error Has jwt props.
|
||||
menuDataRouter.get("/contractsearchpage", (req: RequestWithJwt, res) => {
|
||||
const createContractTutorial = controller.resolveContract(
|
||||
contractCreationTutorialId,
|
||||
@ -920,6 +955,7 @@ menuDataRouter.get("/contractsearchpage", (req: RequestWithJwt, res) => {
|
||||
menuDataRouter.post(
|
||||
"/ContractSearch",
|
||||
jsonMiddleware(),
|
||||
// @ts-expect-error Jwt props.
|
||||
async (
|
||||
req: RequestWithJwt<
|
||||
{
|
||||
@ -977,12 +1013,21 @@ menuDataRouter.post(
|
||||
}
|
||||
} else {
|
||||
// No plugins handle this. Getting search results from official
|
||||
searchResult = await officialSearchContract(
|
||||
searchResult = (await officialSearchContract(
|
||||
req.jwt.unique_name,
|
||||
req.gameVersion,
|
||||
req.body,
|
||||
0,
|
||||
)
|
||||
)) || {
|
||||
Data: {
|
||||
Contracts: [],
|
||||
TotalCount: 0,
|
||||
Page: 0,
|
||||
ErrorReason: "",
|
||||
HasPrevious: false,
|
||||
HasMore: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
@ -999,6 +1044,7 @@ menuDataRouter.post(
|
||||
menuDataRouter.post(
|
||||
"/ContractSearchPaginate",
|
||||
jsonMiddleware(),
|
||||
// @ts-expect-error Has jwt props.
|
||||
async (
|
||||
req: RequestWithJwt<
|
||||
{
|
||||
@ -1022,14 +1068,26 @@ menuDataRouter.post(
|
||||
|
||||
menuDataRouter.get(
|
||||
"/DebriefingChallenges",
|
||||
// @ts-expect-error Has jwt props.
|
||||
(
|
||||
req: RequestWithJwt<{
|
||||
contractId: string
|
||||
}>,
|
||||
req: RequestWithJwt<
|
||||
Partial<{
|
||||
contractId: string
|
||||
}>
|
||||
>,
|
||||
res,
|
||||
) => {
|
||||
if (typeof req.query.contractId !== "string") {
|
||||
res.status(400).send("invalid contractId")
|
||||
return
|
||||
}
|
||||
|
||||
res.json({
|
||||
template: getConfig("DebriefingChallengesTemplate", false),
|
||||
template: getVersionedConfig(
|
||||
"DebriefingChallengesTemplate",
|
||||
req.gameVersion,
|
||||
false,
|
||||
),
|
||||
data: {
|
||||
ChallengeData: {
|
||||
Children:
|
||||
@ -1044,6 +1102,7 @@ menuDataRouter.get(
|
||||
},
|
||||
)
|
||||
|
||||
// @ts-expect-error Has jwt props.
|
||||
menuDataRouter.get("/contractcreation/create", (req: RequestWithJwt, res) => {
|
||||
let cUuid = randomUUID()
|
||||
const createContractReturnTemplate = getConfig(
|
||||
@ -1059,6 +1118,11 @@ menuDataRouter.get("/contractcreation/create", (req: RequestWithJwt, res) => {
|
||||
|
||||
const sesh = getSession(req.jwt.unique_name)
|
||||
|
||||
if (!sesh) {
|
||||
res.status(400).send("no session")
|
||||
return
|
||||
}
|
||||
|
||||
const one = "1"
|
||||
const two = `${random.int(10, 99)}`
|
||||
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",
|
||||
Targets: Array.from(sesh.kills)
|
||||
.filter((kill) =>
|
||||
sesh.markedTargets.has(kill._RepositoryId),
|
||||
sesh.markedTargets.has(kill._RepositoryId || ""),
|
||||
)
|
||||
.map((km) => {
|
||||
return {
|
||||
RepositoryId: km._RepositoryId,
|
||||
Selected: true,
|
||||
Weapon: {
|
||||
RepositoryId: km.KillItemRepositoryId,
|
||||
KillMethodBroad: km.KillMethodBroad,
|
||||
KillMethodStrict: km.KillMethodStrict,
|
||||
RequiredKillMethodType: 3,
|
||||
},
|
||||
Outfit: {
|
||||
RepositoryId: km.OutfitRepoId,
|
||||
Required: true,
|
||||
IsHitmanSuit: isSuit(km.OutfitRepoId),
|
||||
},
|
||||
}
|
||||
}),
|
||||
.map((km) => ({
|
||||
RepositoryId: km._RepositoryId,
|
||||
Selected: true,
|
||||
Weapon: {
|
||||
RepositoryId: km.KillItemRepositoryId,
|
||||
KillMethodBroad: km.KillMethodBroad,
|
||||
KillMethodStrict: km.KillMethodStrict,
|
||||
RequiredKillMethodType: 3,
|
||||
},
|
||||
Outfit: {
|
||||
RepositoryId: km.OutfitRepoId,
|
||||
Required: true,
|
||||
IsHitmanSuit: isSuit(km.OutfitRepoId),
|
||||
},
|
||||
})),
|
||||
ContractConditions: complications(timeLimitStr),
|
||||
PublishingDisabled:
|
||||
sesh.contractId === contractCreationTutorialId,
|
||||
@ -1143,7 +1205,7 @@ const createLoadSaveMiddleware =
|
||||
template,
|
||||
data: {
|
||||
Contracts: [] as UserCentricContract[],
|
||||
PaymentEligiblity: {},
|
||||
PaymentEligiblity: {} as Record<string, boolean>,
|
||||
},
|
||||
}
|
||||
|
||||
@ -1186,144 +1248,46 @@ const createLoadSaveMiddleware =
|
||||
menuDataRouter.post(
|
||||
"/Load",
|
||||
jsonMiddleware(),
|
||||
// @ts-expect-error Has jwt props.
|
||||
createLoadSaveMiddleware("LoadMenuTemplate"),
|
||||
)
|
||||
|
||||
menuDataRouter.post(
|
||||
"/Save",
|
||||
jsonMiddleware(),
|
||||
// @ts-expect-error Has jwt props.
|
||||
createLoadSaveMiddleware("SaveMenuTemplate"),
|
||||
)
|
||||
|
||||
// @ts-expect-error Has jwt props.
|
||||
menuDataRouter.get("/PlayerProfile", (req: RequestWithJwt, res) => {
|
||||
const playerProfilePage = getConfig<PlayerProfileView>(
|
||||
"PlayerProfilePage",
|
||||
true,
|
||||
)
|
||||
|
||||
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)
|
||||
res.json({
|
||||
template: null,
|
||||
data: getPlayerProfileData(req.gameVersion, req.jwt.unique_name),
|
||||
})
|
||||
})
|
||||
|
||||
menuDataRouter.get(
|
||||
// who at IOI decided this was a good route name???!
|
||||
"/LookupContractDialogAddOrDeleteFromPlaylist",
|
||||
// @ts-expect-error Has jwt props.
|
||||
withLookupDialog,
|
||||
)
|
||||
|
||||
menuDataRouter.get(
|
||||
// this one is sane Kappa
|
||||
"/contractplaylist/addordelete/:contractId",
|
||||
// @ts-expect-error Has jwt props.
|
||||
directRoute,
|
||||
)
|
||||
|
||||
menuDataRouter.post(
|
||||
"/contractplaylist/deletemultiple",
|
||||
jsonMiddleware(),
|
||||
// @ts-expect-error Has jwt props.
|
||||
deleteMultiple,
|
||||
)
|
||||
|
||||
// @ts-expect-error Has jwt props.
|
||||
menuDataRouter.get("/GetPlayerProfileXpData", (req: RequestWithJwt, res) => {
|
||||
const userData = getUserData(req.jwt.unique_name, req.gameVersion)
|
||||
|
||||
@ -1342,7 +1306,13 @@ menuDataRouter.get("/GetPlayerProfileXpData", (req: RequestWithJwt, res) => {
|
||||
|
||||
menuDataRouter.get(
|
||||
"/GetMasteryCompletionDataForLocation",
|
||||
// @ts-expect-error Has jwt props.
|
||||
(req: RequestWithJwt<GetCompletionDataForLocationQuery>, res) => {
|
||||
if (!req.query.locationId) {
|
||||
res.status(400).send("no location id")
|
||||
return
|
||||
}
|
||||
|
||||
res.json(
|
||||
generateCompletionData(
|
||||
req.query.locationId,
|
||||
@ -1355,6 +1325,7 @@ menuDataRouter.get(
|
||||
|
||||
menuDataRouter.get(
|
||||
"/MasteryUnlockable",
|
||||
// @ts-expect-error Has jwt props.
|
||||
(req: RequestWithJwt<MasteryUnlockableQuery>, res) => {
|
||||
let masteryUnlockTemplate = getConfig(
|
||||
"MasteryUnlockablesTemplate",
|
||||
@ -1402,32 +1373,30 @@ menuDataRouter.get(
|
||||
|
||||
menuDataRouter.get(
|
||||
"/MasteryDataForLocation",
|
||||
// @ts-expect-error Has jwt props.
|
||||
(
|
||||
req: RequestWithJwt<{
|
||||
locationId: string
|
||||
}>,
|
||||
res,
|
||||
) => {
|
||||
res.json(
|
||||
controller.masteryService.getMasteryDataForLocation(
|
||||
res.json({
|
||||
template: getConfig("MasteryDataForLocationTemplate", false),
|
||||
data: controller.masteryService.getMasteryDataForLocation(
|
||||
req.query.locationId,
|
||||
req.gameVersion,
|
||||
req.jwt.unique_name,
|
||||
),
|
||||
)
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
menuDataRouter.get(
|
||||
"/GetMasteryCompletionDataForUnlockable",
|
||||
(
|
||||
req: RequestWithJwt<{
|
||||
unlockableId: string
|
||||
}>,
|
||||
res,
|
||||
) => {
|
||||
// @ts-expect-error Has jwt props.
|
||||
(req: RequestWithJwt<GetMasteryCompletionDataForUnlockableQuery>, res) => {
|
||||
// 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_KNIGHT: "LOCATION_PARENT_AUSTRIA",
|
||||
FIREARMS_SC_HERO_SNIPER_STONE: "LOCATION_PARENT_AUSTRIA",
|
||||
|
@ -21,8 +21,8 @@ import type {
|
||||
Campaign,
|
||||
GameVersion,
|
||||
GenSingleMissionFunc,
|
||||
ICampaignMission,
|
||||
ICampaignVideo,
|
||||
CampaignMission,
|
||||
CampaignVideo,
|
||||
IVideo,
|
||||
StoryData,
|
||||
} from "../types/types"
|
||||
@ -37,7 +37,7 @@ const genSingleMissionFactory = (userId: string): GenSingleMissionFunc => {
|
||||
return function genSingleMission(
|
||||
contractId: string,
|
||||
gameVersion: GameVersion,
|
||||
): ICampaignMission {
|
||||
): CampaignMission {
|
||||
assert.ok(
|
||||
contractId,
|
||||
"Plugin tried to generate mission with no contract ID",
|
||||
@ -51,11 +51,12 @@ const genSingleMissionFactory = (userId: string): GenSingleMissionFunc => {
|
||||
|
||||
if (!actualContractData) {
|
||||
log(LogLevel.ERROR, `Failed to resolve contract ${contractId}!`)
|
||||
assert.fail(`Failed to resolve contract ${contractId}! (campaign)`)
|
||||
}
|
||||
|
||||
return {
|
||||
Type: "Mission",
|
||||
Data: contractIdToHitObject(contractId, gameVersion, userId),
|
||||
Data: contractIdToHitObject(contractId, gameVersion, userId)!,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -63,7 +64,7 @@ const genSingleMissionFactory = (userId: string): GenSingleMissionFunc => {
|
||||
function genSingleVideo(
|
||||
videoId: string,
|
||||
gameVersion: GameVersion,
|
||||
): ICampaignVideo {
|
||||
): CampaignVideo {
|
||||
const videos = getConfig<Record<string, IVideo>>("Videos", true) // we modify videos so we need to clone this
|
||||
const video = videos[videoId]
|
||||
|
||||
|
@ -18,16 +18,18 @@
|
||||
|
||||
import { getConfig, getVersionedConfig } from "../configSwizzleManager"
|
||||
import type {
|
||||
ChallengeCompletion,
|
||||
CompiledChallengeTreeCategory,
|
||||
CompletionData,
|
||||
GameLocationsData,
|
||||
GameVersion,
|
||||
IHit,
|
||||
JwtData,
|
||||
MissionStory,
|
||||
OpportunityStatistics,
|
||||
PeacockLocationsData,
|
||||
Unlockable,
|
||||
} from "../types/types"
|
||||
import type { MasteryData } from "../types/mastery"
|
||||
import { contractIdToHitObject, controller } from "../controller"
|
||||
import { generateCompletionData } from "../contracts/dataGen"
|
||||
import { getUserData } from "../databaseHandler"
|
||||
@ -37,6 +39,17 @@ import { createInventory } from "../inventory"
|
||||
import { log, LogLevel } from "../loggingInterop"
|
||||
import { no2016 } from "../contracts/escalations/escalationService"
|
||||
import { missionsInLocations } from "../contracts/missionsInLocation"
|
||||
import assert from "assert"
|
||||
|
||||
type LegacyData = {
|
||||
[difficulty: string]: {
|
||||
ChallengeCompletion: {
|
||||
ChallengesCount: number
|
||||
CompletedChallengesCount: number
|
||||
}
|
||||
CompletionData: CompletionData
|
||||
}
|
||||
}
|
||||
|
||||
type GameFacingDestination = {
|
||||
ChallengeCompletion: {
|
||||
@ -48,14 +61,45 @@ type GameFacingDestination = {
|
||||
LocationCompletionPercent: number
|
||||
Location: Unlockable
|
||||
// H2016 only
|
||||
Data?: {
|
||||
[difficulty: string]: {
|
||||
ChallengeCompletion: {
|
||||
ChallengesCount: number
|
||||
CompletedChallengesCount: number
|
||||
}
|
||||
CompletionData: CompletionData
|
||||
Data?: LegacyData
|
||||
}
|
||||
|
||||
type LocationMissionData = {
|
||||
Location: Unlockable
|
||||
SubLocation: Unlockable
|
||||
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,
|
||||
child: Unlockable | undefined,
|
||||
gameVersion: GameVersion,
|
||||
jwt: JwtData,
|
||||
userId: string,
|
||||
) {
|
||||
const missionStories = getConfig<Record<string, MissionStory>>(
|
||||
"MissionStories",
|
||||
false,
|
||||
)
|
||||
|
||||
const userData = getUserData(jwt.unique_name, gameVersion)
|
||||
const userData = getUserData(userId, gameVersion)
|
||||
const challenges = controller.challengeService.getGroupedChallengeLists(
|
||||
{
|
||||
type: ChallengeFilterType.ParentLocation,
|
||||
@ -123,21 +167,10 @@ export function getCompletionPercent(
|
||||
opportunityDone: number,
|
||||
opportunityTotal: number,
|
||||
): number {
|
||||
if (challengeDone === undefined) {
|
||||
challengeDone = 0
|
||||
}
|
||||
|
||||
if (challengeTotal === undefined) {
|
||||
challengeTotal = 0
|
||||
}
|
||||
|
||||
if (opportunityDone === undefined) {
|
||||
opportunityDone = 0
|
||||
}
|
||||
|
||||
if (opportunityTotal === undefined) {
|
||||
opportunityTotal = 0
|
||||
}
|
||||
challengeDone ??= 0
|
||||
challengeTotal ??= 0
|
||||
opportunityDone ??= 0
|
||||
opportunityTotal ??= 0
|
||||
|
||||
const totalCompletables = challengeTotal + opportunityTotal
|
||||
const totalCompleted = challengeDone + opportunityDone
|
||||
@ -150,11 +183,11 @@ export function getCompletionPercent(
|
||||
* Get the list of destinations used by the `/profiles/page/Destinations` endpoint.
|
||||
*
|
||||
* @param gameVersion
|
||||
* @param jwt
|
||||
* @param userId The user ID.
|
||||
*/
|
||||
export function getAllGameDestinations(
|
||||
gameVersion: GameVersion,
|
||||
jwt: JwtData,
|
||||
userId: string,
|
||||
): GameFacingDestination[] {
|
||||
const result: GameFacingDestination[] = []
|
||||
const locations = getVersionedConfig<PeacockLocationsData>(
|
||||
@ -169,38 +202,13 @@ export function getAllGameDestinations(
|
||||
"UI_LOCATION_PARENT_" + destination.substring(16) + "_NAME"
|
||||
|
||||
const template: GameFacingDestination = {
|
||||
...getDestinationCompletion(parent, undefined, gameVersion, jwt),
|
||||
...getDestinationCompletion(parent, undefined, gameVersion, userId),
|
||||
...{
|
||||
CompletionData: generateCompletionData(
|
||||
destination,
|
||||
jwt.unique_name,
|
||||
userId,
|
||||
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.
|
||||
// We're just reusing this for now.
|
||||
if (gameVersion === "h1") {
|
||||
template.Data.normal.ChallengeCompletion =
|
||||
template.ChallengeCompletion
|
||||
template.Data.pro1.ChallengeCompletion =
|
||||
template.ChallengeCompletion
|
||||
template.Data = {
|
||||
normal: {
|
||||
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)
|
||||
@ -258,10 +284,15 @@ export function createLocationsData(
|
||||
}
|
||||
|
||||
const sublocation = locData.children[sublocationId]
|
||||
|
||||
if (!sublocation.Properties.ParentLocation) {
|
||||
assert.fail("sublocation has no parent, that's illegal")
|
||||
}
|
||||
|
||||
const parentLocation =
|
||||
locData.parents[sublocation.Properties.ParentLocation]
|
||||
const creationContract = controller.resolveContract(
|
||||
sublocation.Properties.CreateContractId,
|
||||
sublocation.Properties.CreateContractId!,
|
||||
)
|
||||
|
||||
if (!creationContract && excludeIfNoContracts) {
|
||||
@ -288,12 +319,18 @@ export function createLocationsData(
|
||||
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(
|
||||
query: GetDestinationQuery,
|
||||
gameVersion: GameVersion,
|
||||
jwt: JwtData,
|
||||
) {
|
||||
userId: string,
|
||||
): GameDestination {
|
||||
const LOCATION = query.locationId
|
||||
|
||||
const locData = getVersionedConfig<PeacockLocationsData>(
|
||||
@ -306,18 +343,30 @@ export function getDestination(
|
||||
const masteryData = controller.masteryService.getMasteryDataForDestination(
|
||||
query.locationId,
|
||||
gameVersion,
|
||||
jwt.unique_name,
|
||||
userId,
|
||||
query.difficulty,
|
||||
)
|
||||
|
||||
const response = {
|
||||
Location: {},
|
||||
let resMasteryData: GameDestination["MasteryData"]
|
||||
|
||||
if (LOCATION !== "LOCATION_PARENT_ICA_FACILITY") {
|
||||
if (gameVersion === "h1") {
|
||||
resMasteryData = masteryData[0]
|
||||
} else {
|
||||
resMasteryData = masteryData
|
||||
}
|
||||
} else {
|
||||
resMasteryData = {}
|
||||
}
|
||||
|
||||
const response: Partial<GameDestination> = {
|
||||
Location: locationData,
|
||||
MissionData: {
|
||||
...getDestinationCompletion(
|
||||
locationData,
|
||||
undefined,
|
||||
gameVersion,
|
||||
jwt,
|
||||
userId,
|
||||
),
|
||||
...{ SubLocationMissionsData: [] },
|
||||
},
|
||||
@ -326,20 +375,14 @@ export function getDestination(
|
||||
controller.challengeService.getChallengeDataForDestination(
|
||||
query.locationId,
|
||||
gameVersion,
|
||||
jwt.unique_name,
|
||||
userId,
|
||||
),
|
||||
},
|
||||
MasteryData:
|
||||
LOCATION !== "LOCATION_PARENT_ICA_FACILITY"
|
||||
? gameVersion === "h1"
|
||||
? masteryData[0]
|
||||
: masteryData
|
||||
: {},
|
||||
DifficultyData: undefined,
|
||||
MasteryData: resMasteryData,
|
||||
}
|
||||
|
||||
if (gameVersion === "h1" && LOCATION !== "LOCATION_PARENT_ICA_FACILITY") {
|
||||
const inventory = createInventory(jwt.unique_name, gameVersion)
|
||||
const inventory = createInventory(userId, gameVersion)
|
||||
|
||||
response.DifficultyData = {
|
||||
AvailableDifficultyModes: [
|
||||
@ -352,7 +395,7 @@ export function getDestination(
|
||||
Available: inventory.some(
|
||||
(e) =>
|
||||
e.Unlockable.Id ===
|
||||
locationData.Properties.DifficultyUnlock.pro1,
|
||||
locationData.Properties.DifficultyUnlock?.pro1,
|
||||
),
|
||||
},
|
||||
],
|
||||
@ -369,15 +412,15 @@ export function getDestination(
|
||||
(subLocation) => subLocation.Properties.ParentLocation === LOCATION,
|
||||
)
|
||||
|
||||
response.Location = locationData
|
||||
|
||||
if (query.difficulty === "pro1") {
|
||||
const obj = {
|
||||
type Cast = keyof typeof controller.missionsInLocations.pro1
|
||||
|
||||
const obj: LocationMissionData = {
|
||||
Location: locationData,
|
||||
SubLocation: locationData,
|
||||
Missions: [controller.missionsInLocations.pro1[LOCATION]].map(
|
||||
(id) => contractIdToHitObject(id, gameVersion, jwt.unique_name),
|
||||
),
|
||||
Missions: [controller.missionsInLocations.pro1[LOCATION as Cast]]
|
||||
.map((id) => contractIdToHitObject(id, gameVersion, userId))
|
||||
.filter(Boolean) as IHit[],
|
||||
SarajevoSixMissions: [],
|
||||
ElusiveMissions: [],
|
||||
EscalationMissions: [],
|
||||
@ -386,14 +429,14 @@ export function getDestination(
|
||||
CampaignMissions: [],
|
||||
CompletionData: generateCompletionData(
|
||||
sublocationsData[0].Id,
|
||||
jwt.unique_name,
|
||||
userId,
|
||||
gameVersion,
|
||||
),
|
||||
}
|
||||
|
||||
response.MissionData.SubLocationMissionsData.push(obj)
|
||||
response.MissionData?.SubLocationMissionsData.push(obj)
|
||||
|
||||
return response
|
||||
return response as GameDestination
|
||||
}
|
||||
|
||||
for (const e of sublocationsData) {
|
||||
@ -401,6 +444,7 @@ export function getDestination(
|
||||
|
||||
const escalations: IHit[] = []
|
||||
|
||||
type ECast = keyof typeof controller.missionsInLocations.escalations
|
||||
// every unique escalation from the sublocation
|
||||
const allUniqueEscalations: string[] = [
|
||||
...(gameVersion === "h1" && e.Id === "LOCATION_ICA_FACILITY"
|
||||
@ -409,7 +453,7 @@ export function getDestination(
|
||||
]
|
||||
: []),
|
||||
...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(
|
||||
escalation,
|
||||
gameVersion,
|
||||
jwt.unique_name,
|
||||
userId,
|
||||
)
|
||||
|
||||
if (details) {
|
||||
@ -428,17 +472,18 @@ export function getDestination(
|
||||
}
|
||||
|
||||
const sniperMissions: IHit[] = []
|
||||
type SCast = keyof typeof controller.missionsInLocations.sniper
|
||||
|
||||
for (const sniperMission of controller.missionsInLocations.sniper[
|
||||
e.Id
|
||||
e.Id as SCast
|
||||
] ?? []) {
|
||||
sniperMissions.push(
|
||||
contractIdToHitObject(
|
||||
sniperMission,
|
||||
gameVersion,
|
||||
jwt.unique_name,
|
||||
),
|
||||
const hit = contractIdToHitObject(
|
||||
sniperMission,
|
||||
gameVersion,
|
||||
userId,
|
||||
)
|
||||
|
||||
if (hit) sniperMissions.push(hit)
|
||||
}
|
||||
|
||||
const obj = {
|
||||
@ -451,11 +496,7 @@ export function getDestination(
|
||||
SniperMissions: sniperMissions,
|
||||
PlaceholderMissions: [],
|
||||
CampaignMissions: [],
|
||||
CompletionData: generateCompletionData(
|
||||
e.Id,
|
||||
jwt.unique_name,
|
||||
gameVersion,
|
||||
),
|
||||
CompletionData: generateCompletionData(e.Id, userId, gameVersion),
|
||||
}
|
||||
|
||||
const types = [
|
||||
@ -464,6 +505,7 @@ export function getDestination(
|
||||
["elusive", "ElusiveMissions"],
|
||||
],
|
||||
...((gameVersion === "h1" &&
|
||||
// @ts-expect-error Hack.
|
||||
missionsInLocations.sarajevo["h2016enabled"]) ||
|
||||
gameVersion === "h3"
|
||||
? [["sarajevo", "SarajevoSixMissions"]]
|
||||
@ -472,8 +514,10 @@ export function getDestination(
|
||||
|
||||
for (const t of types) {
|
||||
let theMissions: string[] | undefined = !t[0] // no specific type
|
||||
? controller.missionsInLocations[e.Id]
|
||||
: controller.missionsInLocations[t[0]][e.Id]
|
||||
? // @ts-expect-error Yup.
|
||||
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
|
||||
// these into a single array
|
||||
@ -504,16 +548,17 @@ export function getDestination(
|
||||
const mission = contractIdToHitObject(
|
||||
c,
|
||||
gameVersion,
|
||||
jwt.unique_name,
|
||||
userId,
|
||||
)
|
||||
|
||||
// @ts-expect-error Yup.
|
||||
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,
|
||||
req.jwt.unique_name,
|
||||
req.gameVersion,
|
||||
),
|
||||
)!,
|
||||
},
|
||||
...(flag && { AddedSuccessfully: true }),
|
||||
}
|
||||
|
@ -16,7 +16,12 @@
|
||||
* 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 { getUserData } from "../databaseHandler"
|
||||
import { controller } from "../controller"
|
||||
@ -29,10 +34,32 @@ import {
|
||||
import { createLocationsData, getAllGameDestinations } from "./destinations"
|
||||
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)
|
||||
|
||||
const userdata = getUserData(jwt.unique_name, gameVersion)
|
||||
const userdata = getUserData(userId, gameVersion)
|
||||
|
||||
const contractCreationTutorial =
|
||||
gameVersion !== "scpc"
|
||||
@ -44,7 +71,7 @@ export function getHubData(gameVersion: GameVersion, jwt: JwtData) {
|
||||
gameVersion,
|
||||
true,
|
||||
)
|
||||
const career =
|
||||
const career: Record<string, CareerEntry> =
|
||||
gameVersion === "h3"
|
||||
? {}
|
||||
: {
|
||||
@ -73,7 +100,7 @@ export function getHubData(gameVersion: GameVersion, jwt: JwtData) {
|
||||
controller.masteryService.getMasteryDataForDestination(
|
||||
parent,
|
||||
gameVersion,
|
||||
jwt.unique_name,
|
||||
userId,
|
||||
).length
|
||||
) {
|
||||
const completionData =
|
||||
@ -81,7 +108,7 @@ export function getHubData(gameVersion: GameVersion, jwt: JwtData) {
|
||||
parent,
|
||||
parent,
|
||||
gameVersion,
|
||||
jwt.unique_name,
|
||||
userId,
|
||||
parent.includes("SNUG") ? "evergreen" : "mission",
|
||||
gameVersion === "h1" ? "normal" : undefined,
|
||||
)
|
||||
@ -100,7 +127,7 @@ export function getHubData(gameVersion: GameVersion, jwt: JwtData) {
|
||||
parent,
|
||||
parent,
|
||||
gameVersion,
|
||||
jwt.unique_name,
|
||||
userId,
|
||||
parent.includes("SNUG")
|
||||
? "evergreen"
|
||||
: "mission",
|
||||
@ -137,14 +164,14 @@ export function getHubData(gameVersion: GameVersion, jwt: JwtData) {
|
||||
const challengeCompletion =
|
||||
controller.challengeService.countTotalNCompletedChallenges(
|
||||
challenges,
|
||||
jwt.unique_name,
|
||||
userId,
|
||||
gameVersion,
|
||||
)
|
||||
|
||||
career[parent]?.Children.push({
|
||||
IsLocked: location.Properties.IsLocked,
|
||||
career[parent!]?.Children.push({
|
||||
IsLocked: Boolean(location.Properties.IsLocked),
|
||||
Name: location.DisplayNameLocKey,
|
||||
Image: location.Properties.Icon,
|
||||
Image: location.Properties.Icon || "",
|
||||
Icon: location.Type, // should be "location" for all locations
|
||||
CompletedChallengesCount:
|
||||
challengeCompletion.CompletedChallengesCount,
|
||||
@ -152,14 +179,10 @@ export function getHubData(gameVersion: GameVersion, jwt: JwtData) {
|
||||
CategoryId: child,
|
||||
Description: `UI_${child}_PRIMARY_DESC`,
|
||||
Location: location,
|
||||
ImageLocked: location.Properties.LockedIcon,
|
||||
RequiredResources: location.Properties.RequiredResources,
|
||||
ImageLocked: location.Properties.LockedIcon || "",
|
||||
RequiredResources: location.Properties.RequiredResources || [],
|
||||
IsPack: false, // should be false for all locations
|
||||
CompletionData: generateCompletionData(
|
||||
child,
|
||||
jwt.unique_name,
|
||||
gameVersion,
|
||||
),
|
||||
CompletionData: generateCompletionData(child, userId, gameVersion),
|
||||
})
|
||||
}
|
||||
|
||||
@ -176,10 +199,10 @@ export function getHubData(gameVersion: GameVersion, jwt: JwtData) {
|
||||
},
|
||||
},
|
||||
DashboardData: [],
|
||||
DestinationsData: getAllGameDestinations(gameVersion, jwt),
|
||||
DestinationsData: getAllGameDestinations(gameVersion, userId),
|
||||
CreateContractTutorial: generateUserCentric(
|
||||
contractCreationTutorial,
|
||||
jwt.unique_name,
|
||||
userId,
|
||||
gameVersion,
|
||||
),
|
||||
LocationsData: createLocationsData(gameVersion, true),
|
||||
@ -189,7 +212,7 @@ export function getHubData(gameVersion: GameVersion, jwt: JwtData) {
|
||||
},
|
||||
MasteryData: masteryData,
|
||||
},
|
||||
StoryData: makeCampaigns(gameVersion, jwt.unique_name),
|
||||
StoryData: makeCampaigns(gameVersion, userId),
|
||||
FilterData: getVersionedConfig("FilterData", gameVersion, false),
|
||||
StoreData: getVersionedConfig("StoreData", gameVersion, false),
|
||||
IOIAccountStatus: {
|
||||
|
@ -544,8 +544,11 @@ export const menuSystemDatabase = new MenuSystemDatabase()
|
||||
|
||||
menuSystemRouter.get(
|
||||
"/dynamic_resources_pc_release_rpkg",
|
||||
// @ts-expect-error No type issue is actually here.
|
||||
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(
|
||||
PEACOCK_DEV ? process.cwd() : __dirname,
|
||||
"resources",
|
||||
@ -565,6 +568,7 @@ menuSystemRouter.get(
|
||||
},
|
||||
)
|
||||
|
||||
// @ts-expect-error No type issue is actually here.
|
||||
menuSystemRouter.use("/menusystem/", MenuSystemDatabase.configMiddleware)
|
||||
|
||||
// Miranda Jamison's image path in the repository is escaped for some reason
|
||||
@ -587,6 +591,7 @@ menuSystemPreRouter.get(
|
||||
|
||||
menuSystemRouter.use(
|
||||
"/images/",
|
||||
// @ts-expect-error No type issue is actually here.
|
||||
serveStatic("images", { fallthrough: true }),
|
||||
imageFetchingMiddleware,
|
||||
)
|
||||
|
@ -19,9 +19,9 @@
|
||||
import type {
|
||||
CompiledChallengeTreeCategory,
|
||||
GameVersion,
|
||||
JwtData,
|
||||
MissionManifest,
|
||||
MissionStory,
|
||||
ProgressionData,
|
||||
SceneConfig,
|
||||
Unlockable,
|
||||
UserCentricContract,
|
||||
@ -51,11 +51,12 @@ import {
|
||||
} from "../utils"
|
||||
|
||||
import { createInventory, getUnlockableById } from "../inventory"
|
||||
import { createSniperLoadouts } from "./sniper"
|
||||
import { createSniperLoadouts, SniperCharacter, SniperLoadout } from "./sniper"
|
||||
import { getFlag } from "../flags"
|
||||
import { loadouts } from "../loadouts"
|
||||
import { resolveProfiles } from "../profileHandler"
|
||||
import { userAuths } from "../officialServerAuth"
|
||||
import assert from "assert"
|
||||
|
||||
export type PlanningError = { error: boolean }
|
||||
|
||||
@ -77,11 +78,11 @@ export type GamePlanningData = {
|
||||
IsFirstInGroup: boolean
|
||||
Creator: UserProfile
|
||||
UserContract?: boolean
|
||||
UnlockedEntrances?: string[]
|
||||
UnlockedAgencyPickups?: string[]
|
||||
UnlockedEntrances?: string[] | null
|
||||
UnlockedAgencyPickups?: string[] | null
|
||||
Objectives?: unknown
|
||||
GroupData?: PlanningGroupData
|
||||
Entrances: Unlockable[]
|
||||
Entrances: Unlockable[] | null
|
||||
Location: Unlockable
|
||||
LoadoutData: unknown
|
||||
LimitedLoadoutUnlockLevel: number
|
||||
@ -113,7 +114,7 @@ export type GamePlanningData = {
|
||||
export async function getPlanningData(
|
||||
contractId: string,
|
||||
resetEscalation: boolean,
|
||||
jwt: JwtData,
|
||||
userId: string,
|
||||
gameVersion: GameVersion,
|
||||
): Promise<PlanningError | GamePlanningData> {
|
||||
const entranceData = getConfig<SceneConfig>("Entrances", false)
|
||||
@ -122,7 +123,7 @@ export async function getPlanningData(
|
||||
false,
|
||||
)
|
||||
|
||||
const userData = getUserData(jwt.unique_name, gameVersion)
|
||||
const userData = getUserData(userId, gameVersion)
|
||||
|
||||
for (const ms in userData.Extensions.opportunityprogression) {
|
||||
if (Object.keys(missionStories).includes(ms)) {
|
||||
@ -130,13 +131,24 @@ export async function getPlanningData(
|
||||
}
|
||||
}
|
||||
|
||||
let contractData =
|
||||
let contractData: MissionManifest | undefined
|
||||
|
||||
if (
|
||||
gameVersion === "h1" &&
|
||||
contractId === "42bac555-bbb9-429d-a8ce-f1ffdf94211c"
|
||||
? _legacyBull
|
||||
: contractId === "ff9f46cf-00bd-4c12-b887-eac491c3a96d"
|
||||
? _theLastYardbirdScpc
|
||||
: controller.resolveContract(contractId)
|
||||
) {
|
||||
contractData = _legacyBull
|
||||
} else if (contractId === "ff9f46cf-00bd-4c12-b887-eac491c3a96d") {
|
||||
contractData = _theLastYardbirdScpc
|
||||
} else {
|
||||
contractData = controller.resolveContract(contractId)
|
||||
}
|
||||
|
||||
if (!contractData) {
|
||||
return {
|
||||
error: true,
|
||||
}
|
||||
}
|
||||
|
||||
if (resetEscalation) {
|
||||
const escalationGroupId =
|
||||
@ -144,10 +156,20 @@ export async function getPlanningData(
|
||||
|
||||
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
|
||||
contractId = controller.escalationMappings.get(escalationGroupId)["1"]
|
||||
contractId = group["1"]
|
||||
|
||||
contractData = controller.resolveContract(contractId)
|
||||
}
|
||||
@ -161,20 +183,29 @@ export async function getPlanningData(
|
||||
LogLevel.WARN,
|
||||
`Trying to download contract ${contractId} due to it not found locally.`,
|
||||
)
|
||||
const user = userAuths.get(jwt.unique_name)
|
||||
const resp = await user._useService(
|
||||
const user = userAuths.get(userId)
|
||||
const resp = await user?._useService(
|
||||
`https://${getRemoteService(
|
||||
gameVersion,
|
||||
)}.hitman.io/profiles/page/Planning?contractid=${contractId}&resetescalation=false&forcecurrentcontract=false&errorhandling=false`,
|
||||
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)
|
||||
}
|
||||
|
||||
if (!contractData) {
|
||||
log(LogLevel.ERROR, `Not found: ${contractId}, .`)
|
||||
log(LogLevel.ERROR, `Not found: ${contractId}, planning regular.`)
|
||||
return { error: true }
|
||||
}
|
||||
|
||||
@ -198,6 +229,11 @@ export async function getPlanningData(
|
||||
if (escalation) {
|
||||
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 done =
|
||||
@ -216,9 +252,12 @@ export async function getPlanningData(
|
||||
|
||||
// Fix contractData to the data of the level in the group.
|
||||
if (!contractData.Metadata.InGroup) {
|
||||
contractData = controller.resolveContract(
|
||||
contractData.Metadata.GroupDefinition.Order[p - 1],
|
||||
)
|
||||
const newLevelId =
|
||||
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)
|
||||
|
||||
assert.ok(sublocation, "contract sublocation is null")
|
||||
|
||||
if (!entranceData[scenePath]) {
|
||||
log(
|
||||
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 typedInv = createInventory(jwt.unique_name, gameVersion, sublocation)
|
||||
const typedInv = createInventory(userId, gameVersion, sublocation)
|
||||
|
||||
const unlockedEntrances = typedInv
|
||||
.filter((item) => item.Unlockable.Type === "access")
|
||||
@ -267,6 +311,9 @@ export async function getPlanningData(
|
||||
LogLevel.ERROR,
|
||||
"No matching entrance data found in planning, this is a bug!",
|
||||
)
|
||||
return {
|
||||
error: true,
|
||||
}
|
||||
}
|
||||
|
||||
sublocation.DisplayNameLocKey = `UI_${sublocation.Id}_NAME`
|
||||
@ -283,7 +330,7 @@ export async function getPlanningData(
|
||||
let suit = getDefaultSuitFor(sublocation)
|
||||
let tool1 = "TOKEN_FIBERWIRE"
|
||||
let tool2 = "PROP_TOOL_COIN"
|
||||
let briefcaseProp: string | undefined = undefined
|
||||
let briefcaseContainedItemId: string | undefined = undefined
|
||||
let briefcaseId: string | undefined = undefined
|
||||
|
||||
const dlForLocation =
|
||||
@ -293,16 +340,13 @@ export async function getPlanningData(
|
||||
contractData.Metadata.Location
|
||||
]
|
||||
: // new loadout profiles system
|
||||
Object.hasOwn(
|
||||
currentLoadout.data,
|
||||
contractData.Metadata.Location,
|
||||
) && currentLoadout.data[contractData.Metadata.Location]
|
||||
currentLoadout.data[contractData.Metadata.Location]
|
||||
|
||||
if (dlForLocation) {
|
||||
pistol = dlForLocation["2"]
|
||||
suit = dlForLocation["3"]
|
||||
tool1 = dlForLocation["4"]
|
||||
tool2 = dlForLocation["5"]
|
||||
pistol = dlForLocation["2"]!
|
||||
suit = dlForLocation["3"]!
|
||||
tool1 = dlForLocation["4"]!
|
||||
tool2 = dlForLocation["5"]!
|
||||
|
||||
for (const key of Object.keys(dlForLocation)) {
|
||||
if (["2", "3", "4", "5"].includes(key)) {
|
||||
@ -311,28 +355,30 @@ export async function getPlanningData(
|
||||
}
|
||||
|
||||
briefcaseId = key
|
||||
briefcaseProp = dlForLocation[key]
|
||||
// @ts-expect-error This will work.
|
||||
briefcaseContainedItemId = dlForLocation[key]
|
||||
}
|
||||
}
|
||||
|
||||
const i = typedInv.find((item) => item.Unlockable.Id === briefcaseProp)
|
||||
|
||||
const userCentric = generateUserCentric(
|
||||
contractData,
|
||||
jwt.unique_name,
|
||||
gameVersion,
|
||||
const briefcaseContainedItem = typedInv.find(
|
||||
(item) => item.Unlockable.Id === briefcaseContainedItemId,
|
||||
)
|
||||
|
||||
const userCentric = generateUserCentric(contractData, userId, gameVersion)
|
||||
|
||||
const sniperLoadouts = createSniperLoadouts(
|
||||
jwt.unique_name,
|
||||
userId,
|
||||
gameVersion,
|
||||
contractData,
|
||||
)
|
||||
|
||||
if (gameVersion === "scpc") {
|
||||
for (const loadout of sniperLoadouts) {
|
||||
loadout["LoadoutData"] = loadout["Loadout"]["LoadoutData"]
|
||||
delete loadout["Loadout"]
|
||||
const l = loadout as SniperLoadout
|
||||
l["LoadoutData"] = (loadout as SniperCharacter)["Loadout"][
|
||||
"LoadoutData"
|
||||
]
|
||||
delete (loadout as Partial<SniperCharacter>)["Loadout"]
|
||||
}
|
||||
}
|
||||
|
||||
@ -395,19 +441,20 @@ export async function getPlanningData(
|
||||
SlotId: "6",
|
||||
Recommended: null,
|
||||
},
|
||||
briefcaseId && {
|
||||
SlotName: briefcaseProp,
|
||||
SlotId: briefcaseId,
|
||||
Recommended: {
|
||||
item: {
|
||||
...i,
|
||||
Properties: {},
|
||||
briefcaseId &&
|
||||
briefcaseContainedItem && {
|
||||
SlotName: briefcaseContainedItemId,
|
||||
SlotId: briefcaseId,
|
||||
Recommended: {
|
||||
item: {
|
||||
...briefcaseContainedItem,
|
||||
Properties: {},
|
||||
},
|
||||
type: briefcaseContainedItem.Unlockable.Id,
|
||||
owned: true,
|
||||
},
|
||||
type: i.Unlockable.Id,
|
||||
owned: true,
|
||||
IsContainer: true,
|
||||
},
|
||||
IsContainer: true,
|
||||
},
|
||||
].filter(Boolean)
|
||||
|
||||
/**
|
||||
@ -426,7 +473,8 @@ export async function getPlanningData(
|
||||
) {
|
||||
const loadoutUnlockable = getUnlockableById(
|
||||
gameVersion === "h1"
|
||||
? sublocation?.Properties?.NormalLoadoutUnlock[
|
||||
? // @ts-expect-error This works.
|
||||
sublocation?.Properties?.NormalLoadoutUnlock[
|
||||
contractData.Metadata.Difficulty ?? "normal"
|
||||
]
|
||||
: sublocation?.Properties?.NormalLoadoutUnlock,
|
||||
@ -440,23 +488,31 @@ export async function getPlanningData(
|
||||
gameVersion,
|
||||
)
|
||||
|
||||
const locationProgression =
|
||||
loadoutMasteryData &&
|
||||
(loadoutMasteryData.SubPackageId
|
||||
? userData.Extensions.progression.Locations[
|
||||
const locationProgression: ProgressionData =
|
||||
loadoutMasteryData?.SubPackageId
|
||||
? // @ts-expect-error This works
|
||||
userData.Extensions.progression.Locations[
|
||||
loadoutMasteryData.Location
|
||||
][loadoutMasteryData.SubPackageId]
|
||||
: 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(
|
||||
(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 {
|
||||
Contract: contractData,
|
||||
ElusiveContractState: "not_completed",
|
||||
@ -467,7 +523,7 @@ export async function getPlanningData(
|
||||
UnlockedEntrances:
|
||||
contractData.Metadata.Type === "sniper"
|
||||
? null
|
||||
: typedInv
|
||||
: (typedInv
|
||||
.filter(
|
||||
(item) =>
|
||||
item.Unlockable.Subtype === "startinglocation",
|
||||
@ -475,27 +531,28 @@ export async function getPlanningData(
|
||||
.filter(
|
||||
(item) =>
|
||||
item.Unlockable.Properties.Difficulty ===
|
||||
contractData.Metadata.Difficulty,
|
||||
contractData!.Metadata.Difficulty,
|
||||
)
|
||||
.map((i) => i.Unlockable.Properties.RepositoryId)
|
||||
.filter((id) => id),
|
||||
.filter(Boolean) as string[]),
|
||||
UnlockedAgencyPickups:
|
||||
contractData.Metadata.Type === "sniper"
|
||||
? null
|
||||
: typedInv
|
||||
: (typedInv
|
||||
.filter((item) => item.Unlockable.Type === "agencypickup")
|
||||
.filter(
|
||||
(item) =>
|
||||
item.Unlockable.Properties.Difficulty ===
|
||||
contractData.Metadata.Difficulty,
|
||||
// we already know it's not undefined
|
||||
contractData!.Metadata.Difficulty,
|
||||
)
|
||||
.map((i) => i.Unlockable.Properties.RepositoryId)
|
||||
.filter((id) => id),
|
||||
.filter(Boolean) as string[]),
|
||||
Objectives: mapObjectives(
|
||||
contractData.Data.Objectives,
|
||||
contractData.Data.Objectives!,
|
||||
contractData.Data.GameChangers || [],
|
||||
contractData.Metadata.GroupObjectiveDisplayOrder || [],
|
||||
contractData.Metadata.IsEvergreenSafehouse,
|
||||
Boolean(contractData.Metadata.IsEvergreenSafehouse),
|
||||
),
|
||||
GroupData: groupData,
|
||||
Entrances:
|
||||
@ -504,27 +561,28 @@ export async function getPlanningData(
|
||||
: unlockedEntrances
|
||||
.filter((unlockable) =>
|
||||
entrancesInScene.includes(
|
||||
unlockable.Properties.RepositoryId,
|
||||
unlockable.Properties.RepositoryId || "",
|
||||
),
|
||||
)
|
||||
.filter(
|
||||
(unlockable) =>
|
||||
unlockable.Properties.Difficulty ===
|
||||
contractData.Metadata.Difficulty,
|
||||
// we already know it's not undefined
|
||||
contractData!.Metadata.Difficulty,
|
||||
)
|
||||
.sort(unlockOrderComparer),
|
||||
Location: sublocation,
|
||||
LoadoutData:
|
||||
contractData.Metadata.Type === "sniper" ? null : loadoutSlots,
|
||||
LimitedLoadoutUnlockLevel:
|
||||
limitedLoadoutUnlockLevelMap[sublocation.Id] ?? 0,
|
||||
limitedLoadoutUnlockLevelMap[sublocation.Id as Cast] ?? 0,
|
||||
CharacterLoadoutData:
|
||||
sniperLoadouts.length !== 0 ? sniperLoadouts : null,
|
||||
ChallengeData: {
|
||||
Children: controller.challengeService.getChallengeTreeForContract(
|
||||
contractId,
|
||||
gameVersion,
|
||||
jwt.unique_name,
|
||||
userId,
|
||||
),
|
||||
},
|
||||
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 type {
|
||||
GameVersion,
|
||||
JwtData,
|
||||
MissionStory,
|
||||
PlayNextCampaignDetails,
|
||||
UserCentricContract,
|
||||
} from "../types/types"
|
||||
import assert from "assert"
|
||||
|
||||
/**
|
||||
* Main story campaign ordered mission IDs.
|
||||
@ -157,6 +157,8 @@ export function createMainOpportunityTile(
|
||||
false,
|
||||
)
|
||||
|
||||
assert.ok(contractData)
|
||||
|
||||
return {
|
||||
CategoryType: "MainOpportunity",
|
||||
CategoryName: "UI_PLAYNEXT_MAINOPPORTUNITY_CATEGORY_NAME",
|
||||
@ -202,7 +204,7 @@ export type GameFacingPlayNextData = {
|
||||
|
||||
export function getGamePlayNextData(
|
||||
contractId: string,
|
||||
jwt: JwtData,
|
||||
userId: string,
|
||||
gameVersion: GameVersion,
|
||||
): GameFacingPlayNextData {
|
||||
const cats: PlayNextCategory[] = []
|
||||
@ -225,14 +227,9 @@ export function getGamePlayNextData(
|
||||
|
||||
if (shouldContinue) {
|
||||
cats.push(
|
||||
createPlayNextMission(
|
||||
jwt.unique_name,
|
||||
nextMissionId,
|
||||
gameVersion,
|
||||
{
|
||||
CampaignName: `UI_SEASON_${nextSeasonId}`,
|
||||
},
|
||||
),
|
||||
createPlayNextMission(userId, nextMissionId, gameVersion, {
|
||||
CampaignName: `UI_SEASON_${nextSeasonId}`,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@ -244,7 +241,7 @@ export function getGamePlayNextData(
|
||||
if (pzIdIndex !== -1 && pzIdIndex !== orderedPZMissions.length - 1) {
|
||||
const nextMissionId = orderedPZMissions[pzIdIndex + 1]
|
||||
cats.push(
|
||||
createPlayNextMission(jwt.unique_name, nextMissionId, gameVersion, {
|
||||
createPlayNextMission(userId, nextMissionId, gameVersion, {
|
||||
CampaignName: "UI_CONTRACT_CAMPAIGN_WHITE_SPIDER_TITLE",
|
||||
ParentCampaignName: "UI_MENU_PAGE_SIDE_MISSIONS_TITLE",
|
||||
}),
|
||||
@ -255,7 +252,7 @@ export function getGamePlayNextData(
|
||||
if (contractId === "f1ba328f-e3dd-4ef8-bb26-0363499fdd95") {
|
||||
const nextMissionId = "0b616e62-af0c-495b-82e3-b778e82b5912"
|
||||
cats.push(
|
||||
createPlayNextMission(jwt.unique_name, nextMissionId, gameVersion, {
|
||||
createPlayNextMission(userId, nextMissionId, gameVersion, {
|
||||
CampaignName: "UI_MENU_PAGE_SPECIAL_ASSIGNMENTS_TITLE",
|
||||
ParentCampaignName: "UI_MENU_PAGE_SIDE_MISSIONS_TITLE",
|
||||
}),
|
||||
@ -274,7 +271,7 @@ export function getGamePlayNextData(
|
||||
if (pluginData) {
|
||||
if (pluginData.overrideIndex !== undefined) {
|
||||
cats[pluginData.overrideIndex] = createPlayNextMission(
|
||||
jwt.unique_name,
|
||||
userId,
|
||||
pluginData.nextContractId,
|
||||
gameVersion,
|
||||
pluginData.campaignDetails,
|
||||
@ -282,7 +279,7 @@ export function getGamePlayNextData(
|
||||
} else {
|
||||
cats.push(
|
||||
createPlayNextMission(
|
||||
jwt.unique_name,
|
||||
userId,
|
||||
pluginData.nextContractId,
|
||||
gameVersion,
|
||||
pluginData.campaignDetails,
|
||||
@ -293,6 +290,6 @@ export function getGamePlayNextData(
|
||||
|
||||
return {
|
||||
Categories: cats,
|
||||
ProfileId: jwt.unique_name,
|
||||
ProfileId: userId,
|
||||
}
|
||||
}
|
||||
|
@ -17,8 +17,44 @@
|
||||
*/
|
||||
|
||||
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 { 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
|
||||
@ -38,13 +74,15 @@ export function createSniperLoadouts(
|
||||
gameVersion: GameVersion,
|
||||
contractData: MissionManifest,
|
||||
loadoutData = false,
|
||||
) {
|
||||
const sniperLoadouts = []
|
||||
): Return {
|
||||
const sniperLoadouts: Return = []
|
||||
const parentLocation = getSubLocationByName(
|
||||
contractData.Metadata.Location,
|
||||
gameVersion,
|
||||
)?.Properties.ParentLocation
|
||||
|
||||
assert.ok(parentLocation, "Parent location not found")
|
||||
|
||||
// This function call is used as it gets all mastery data for the current location
|
||||
// which includes all the characters we'll need.
|
||||
// We map it by Id for quick lookup.
|
||||
@ -54,85 +92,97 @@ export function createSniperLoadouts(
|
||||
.map((data) => [data.CompletionData.Id, data]),
|
||||
)
|
||||
|
||||
if (contractData.Metadata.Type === "sniper") {
|
||||
for (const charSetup of contractData.Metadata.CharacterSetup) {
|
||||
for (const character of charSetup.Characters) {
|
||||
// Get the mastery data for this character
|
||||
const masteryData = masteryMap.get(
|
||||
character.MandatoryLoadout[0],
|
||||
)
|
||||
if (contractData.Metadata.Type !== "sniper") {
|
||||
return sniperLoadouts
|
||||
}
|
||||
|
||||
// Get the unlockable that is currently unlocked
|
||||
const curUnlockable =
|
||||
masteryData.CompletionData.Level === 1
|
||||
? masteryData.Unlockable
|
||||
: masteryData.Drops[
|
||||
masteryData.CompletionData.Level - 2
|
||||
].Unlockable
|
||||
assert.ok(
|
||||
contractData.Metadata.CharacterSetup,
|
||||
"Contract missing sniper character setup",
|
||||
)
|
||||
|
||||
const data = {
|
||||
Id: character.Id,
|
||||
Loadout: {
|
||||
LoadoutData: [
|
||||
{
|
||||
SlotId: "0",
|
||||
SlotName: "carriedweapon",
|
||||
Items: [
|
||||
{
|
||||
Item: {
|
||||
InstanceId: character.Id,
|
||||
ProfileId: userId,
|
||||
Unlockable: curUnlockable,
|
||||
Properties: {},
|
||||
},
|
||||
ItemDetails: {
|
||||
Capabilities: [],
|
||||
StatList: Object.keys(
|
||||
curUnlockable.Properties
|
||||
.Gameplay,
|
||||
).map((key) => {
|
||||
return {
|
||||
Name: key,
|
||||
Ratio: curUnlockable
|
||||
.Properties.Gameplay[
|
||||
key
|
||||
],
|
||||
}
|
||||
}),
|
||||
PropertyTexts: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
Page: 0,
|
||||
Recommended: {
|
||||
item: {
|
||||
InstanceId: character.Id,
|
||||
ProfileId: userId,
|
||||
Unlockable: curUnlockable,
|
||||
Properties: {},
|
||||
},
|
||||
type: "carriedweapon",
|
||||
owned: true,
|
||||
for (const charSetup of contractData.Metadata.CharacterSetup) {
|
||||
for (const character of charSetup.Characters) {
|
||||
// Get the mastery data for this character
|
||||
const masteryData = masteryMap.get(
|
||||
character.MandatoryLoadout?.[0] || "",
|
||||
)
|
||||
|
||||
assert.ok(
|
||||
masteryData,
|
||||
`Mastery data not found for ${contractData.Metadata.Id}`,
|
||||
)
|
||||
|
||||
// Get the unlockable that is currently unlocked
|
||||
const curUnlockable =
|
||||
masteryData.CompletionData.Level === 1
|
||||
? masteryData.Unlockable
|
||||
: masteryData.Drops[masteryData.CompletionData.Level - 2]
|
||||
.Unlockable
|
||||
|
||||
assert.ok(curUnlockable, "Unlockable not found")
|
||||
assert.ok(
|
||||
curUnlockable.Properties.Gameplay,
|
||||
"Unlockable has no gameplay data",
|
||||
)
|
||||
|
||||
const data: SniperCharacter = {
|
||||
Id: character.Id,
|
||||
Loadout: {
|
||||
LoadoutData: [
|
||||
{
|
||||
SlotId: "0",
|
||||
SlotName: "carriedweapon",
|
||||
Items: [],
|
||||
Page: 0,
|
||||
Recommended: {
|
||||
item: {
|
||||
InstanceId: character.Id,
|
||||
ProfileId: userId,
|
||||
Unlockable: curUnlockable,
|
||||
Properties: {},
|
||||
},
|
||||
HasMore: false,
|
||||
HasMoreLeft: false,
|
||||
HasMoreRight: false,
|
||||
OptionalData: {},
|
||||
type: "carriedweapon",
|
||||
owned: true,
|
||||
},
|
||||
],
|
||||
LimitedLoadoutUnlockLevel: 0 as number | undefined,
|
||||
},
|
||||
CompletionData: masteryData?.CompletionData,
|
||||
}
|
||||
|
||||
if (loadoutData) {
|
||||
delete data.Loadout.LimitedLoadoutUnlockLevel
|
||||
sniperLoadouts.push(data.Loadout)
|
||||
continue
|
||||
}
|
||||
|
||||
sniperLoadouts.push(data)
|
||||
HasMore: false,
|
||||
HasMoreLeft: false,
|
||||
HasMoreRight: false,
|
||||
OptionalData: {},
|
||||
},
|
||||
],
|
||||
LimitedLoadoutUnlockLevel: 0 as number | undefined,
|
||||
},
|
||||
CompletionData: masteryData.CompletionData,
|
||||
}
|
||||
|
||||
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 { getFlag } from "../flags"
|
||||
import { loadouts } from "../loadouts"
|
||||
import assert from "assert"
|
||||
|
||||
/**
|
||||
* Algorithm to get the stashpoint items data for H2 and H3.
|
||||
@ -139,10 +140,13 @@ export function getModernStashData(
|
||||
const inventory = createInventory(
|
||||
userId,
|
||||
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(
|
||||
0,
|
||||
-query.slotid!.toString().length,
|
||||
@ -150,7 +154,7 @@ export function getModernStashData(
|
||||
}
|
||||
|
||||
const stashData: ModernStashData = {
|
||||
SlotId: query.slotid,
|
||||
SlotId: query.slotid!,
|
||||
LoadoutItemsData: {
|
||||
SlotId: query.slotid,
|
||||
Items: getModernStashItemsData(
|
||||
@ -169,7 +173,7 @@ export function getModernStashData(
|
||||
AllowContainers: query.allowcontainers, // ?? true
|
||||
},
|
||||
},
|
||||
ShowSlotName: query.slotname,
|
||||
ShowSlotName: query.slotname!,
|
||||
}
|
||||
|
||||
if (contractData) {
|
||||
@ -256,6 +260,10 @@ export function getLegacyStashData(
|
||||
userId: string,
|
||||
gameVersion: GameVersion,
|
||||
) {
|
||||
if (!query.contractid || !query.slotname) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const contractData = controller.resolveContract(query.contractid)
|
||||
|
||||
if (!contractData) {
|
||||
@ -277,6 +285,8 @@ export function getLegacyStashData(
|
||||
gameVersion,
|
||||
)
|
||||
|
||||
assert.ok(sublocation, "Sublocation not found")
|
||||
|
||||
const inventory = createInventory(userId, gameVersion, sublocation)
|
||||
|
||||
const userCentricContract = generateUserCentric(
|
||||
@ -297,14 +307,17 @@ export function getLegacyStashData(
|
||||
const dl = userProfile.Extensions.defaultloadout
|
||||
|
||||
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 || {})[
|
||||
sublocation?.Properties?.ParentLocation
|
||||
sublocation?.Properties?.ParentLocation || ""
|
||||
]
|
||||
|
||||
return (forLocation || defaultLoadout)[id]
|
||||
return (forLocation || defaultLoadout)[
|
||||
id as keyof typeof defaultLoadout
|
||||
]
|
||||
} else {
|
||||
let dl = loadouts.getLoadoutFor("h1")
|
||||
|
||||
@ -312,7 +325,8 @@ export function getLegacyStashData(
|
||||
dl = loadouts.createDefault("h1")
|
||||
}
|
||||
|
||||
const forLocation = dl.data[sublocation?.Properties?.ParentLocation]
|
||||
const forLocation =
|
||||
dl.data[sublocation?.Properties?.ParentLocation || ""]
|
||||
|
||||
return (forLocation || defaultLoadout)[id]
|
||||
}
|
||||
@ -329,7 +343,7 @@ export function getLegacyStashData(
|
||||
Recommended: getLoadoutItem(slotid)
|
||||
? {
|
||||
item: getUnlockableById(
|
||||
getLoadoutItem(slotid),
|
||||
getLoadoutItem(slotid)!,
|
||||
gameVersion,
|
||||
),
|
||||
type: loadoutSlots[slotid],
|
||||
@ -348,7 +362,7 @@ export function getLegacyStashData(
|
||||
}
|
||||
: {},
|
||||
})),
|
||||
Contract: userCentricContract.Contract,
|
||||
Contract: userCentricContract?.Contract,
|
||||
ShowSlotName: query.slotname,
|
||||
UserCentric: userCentricContract,
|
||||
}
|
||||
@ -398,10 +412,10 @@ export function getSafehouseCategory(
|
||||
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,
|
||||
)
|
||||
let subcategory
|
||||
let subcategory: SafehouseCategory | undefined
|
||||
|
||||
if (!category) {
|
||||
category = {
|
||||
@ -410,16 +424,16 @@ export function getSafehouseCategory(
|
||||
IsLeaf: false,
|
||||
Data: null,
|
||||
}
|
||||
safehouseData.SubCategories.push(category)
|
||||
safehouseData.SubCategories?.push(category)
|
||||
}
|
||||
|
||||
subcategory = category.SubCategories.find(
|
||||
subcategory = category.SubCategories?.find(
|
||||
(cat) => cat.Category === item.Unlockable.Subtype,
|
||||
)
|
||||
|
||||
if (!subcategory) {
|
||||
subcategory = {
|
||||
Category: item.Unlockable.Subtype,
|
||||
Category: item.Unlockable.Subtype!,
|
||||
SubCategories: null,
|
||||
IsLeaf: true,
|
||||
Data: {
|
||||
@ -430,13 +444,14 @@ export function getSafehouseCategory(
|
||||
HasMore: false,
|
||||
},
|
||||
}
|
||||
category.SubCategories.push(subcategory)
|
||||
category.SubCategories?.push(subcategory!)
|
||||
}
|
||||
|
||||
subcategory.Data?.Items.push({
|
||||
subcategory!.Data?.Items.push({
|
||||
Item: item,
|
||||
ItemDetails: {
|
||||
Capabilities: [],
|
||||
// @ts-expect-error It just works. Types are probably wrong somewhere up the chain.
|
||||
StatList: item.Unlockable.Properties.Gameplay
|
||||
? Object.entries(item.Unlockable.Properties.Gameplay).map(
|
||||
([key, value]) => ({
|
||||
@ -452,15 +467,15 @@ export function getSafehouseCategory(
|
||||
})
|
||||
}
|
||||
|
||||
for (const [id, category] of safehouseData.SubCategories.entries()) {
|
||||
if (category.SubCategories.length === 1) {
|
||||
for (const [id, category] of safehouseData.SubCategories?.entries() || []) {
|
||||
if (category.SubCategories?.length === 1) {
|
||||
// if category only has one subcategory
|
||||
safehouseData.SubCategories[id] = category.SubCategories[0] // flatten it
|
||||
safehouseData.SubCategories[id].Category = category.Category // but keep the top category's name
|
||||
safehouseData.SubCategories![id] = category.SubCategories[0] // flatten it
|
||||
safehouseData.SubCategories![id].Category = category.Category // but keep the top category's name
|
||||
}
|
||||
}
|
||||
|
||||
if (safehouseData.SubCategories.length === 1) {
|
||||
if (safehouseData.SubCategories?.length === 1) {
|
||||
// if root has only one subcategory
|
||||
safehouseData = safehouseData.SubCategories[0] // flatten it
|
||||
}
|
||||
|
@ -39,6 +39,7 @@ export const multiplayerMenuDataRouter = Router()
|
||||
|
||||
multiplayerMenuDataRouter.post(
|
||||
"/multiplayermatchstatsready",
|
||||
// @ts-expect-error Has JWT data.
|
||||
(req: RequestWithJwt<MissionEndRequestQuery>, res) => {
|
||||
res.json({
|
||||
template: null,
|
||||
@ -53,8 +54,11 @@ multiplayerMenuDataRouter.post(
|
||||
|
||||
multiplayerMenuDataRouter.post(
|
||||
"/multiplayermatchstats",
|
||||
// @ts-expect-error Has JWT data.
|
||||
(req: RequestWithJwt<MultiplayerMatchStatsQuery>, res) => {
|
||||
const sessionDetails = contractSessions.get(req.query.contractSessionId)
|
||||
const sessionDetails = contractSessions.get(
|
||||
req.query.contractSessionId || "",
|
||||
)
|
||||
|
||||
if (!sessionDetails) {
|
||||
// contract session not found
|
||||
@ -90,16 +94,22 @@ multiplayerMenuDataRouter.post(
|
||||
},
|
||||
)
|
||||
|
||||
interface MultiplayerPresetsQuery {
|
||||
type MultiplayerPresetsQuery = {
|
||||
gamemode?: string
|
||||
disguiseUnlockableId?: string
|
||||
}
|
||||
|
||||
multiplayerMenuDataRouter.get(
|
||||
"/multiplayerpresets",
|
||||
// @ts-expect-error Has JWT data.
|
||||
(req: RequestWithJwt<MultiplayerPresetsQuery>, res) => {
|
||||
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
|
||||
}
|
||||
|
||||
@ -141,9 +151,16 @@ multiplayerMenuDataRouter.get(
|
||||
|
||||
multiplayerMenuDataRouter.get(
|
||||
"/multiplayer",
|
||||
// @ts-expect-error Has JWT data.
|
||||
(req: RequestWithJwt<MultiplayerQuery>, res) => {
|
||||
// /multiplayer?gamemode=versus&disguiseUnlockableId=TOKEN_OUTFIT_ELUSIVE_COMPLETE_15_SUIT
|
||||
if (req.query.gamemode !== "versus") {
|
||||
res.status(400).send("unknown gamemode")
|
||||
return
|
||||
}
|
||||
|
||||
if (!req.query.disguiseUnlockableId) {
|
||||
res.status(400).send("no disguiseUnlockableId")
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -31,7 +31,7 @@ import { randomUUID } from "crypto"
|
||||
import { getConfig } from "../configSwizzleManager"
|
||||
import { generateUserCentric } from "../contracts/dataGen"
|
||||
import { controller } from "../controller"
|
||||
import { MatchOverC2SEvent } from "../types/events"
|
||||
import { MatchOverC2SEvent, OpponentsC2sEvent } from "../types/events"
|
||||
|
||||
/**
|
||||
* A multiplayer preset.
|
||||
@ -89,6 +89,7 @@ const activeMatches: Map<string, MatchData> = new Map()
|
||||
multiplayerRouter.post(
|
||||
"/GetRequiredResourcesForPreset",
|
||||
jsonMiddleware(),
|
||||
// @ts-expect-error Has JWT data.
|
||||
(req: RequestWithJwt, res) => {
|
||||
const allPresets = getConfig<MultiplayerPreset[]>(
|
||||
"MultiplayerPresets",
|
||||
@ -114,7 +115,7 @@ multiplayerRouter.post(
|
||||
req.gameVersion,
|
||||
),
|
||||
)
|
||||
.filter(Boolean)
|
||||
.filter(Boolean) as UserCentricContract[]
|
||||
|
||||
res.json(
|
||||
userCentrics.map((userCentric: UserCentricContract) => ({
|
||||
@ -132,6 +133,7 @@ multiplayerRouter.post(
|
||||
multiplayerRouter.post(
|
||||
"/RegisterToMatch",
|
||||
jsonMiddleware(),
|
||||
// @ts-expect-error Has JWT data.
|
||||
(req: RequestWithJwt, res) => {
|
||||
// get a random contract from the list of possible ones in the selected preset
|
||||
const multiplayerPresets = getConfig<MultiplayerPreset[]>(
|
||||
@ -212,6 +214,7 @@ multiplayerRouter.post(
|
||||
multiplayerRouter.post(
|
||||
"/SetMatchData",
|
||||
jsonMiddleware(),
|
||||
// @ts-expect-error Has JWT data.
|
||||
(req: RequestWithJwt, res) => {
|
||||
const match = activeMatches.get(req.body.matchId)
|
||||
|
||||
@ -256,9 +259,7 @@ export function handleMultiplayerEvent(
|
||||
ghost.unnoticedKills += 1
|
||||
return true
|
||||
case "Opponents": {
|
||||
const value = event.Value as {
|
||||
ConnectedSessions: string[]
|
||||
}
|
||||
const value = (event as OpponentsC2sEvent).Value
|
||||
|
||||
ghost.Opponents = value.ConnectedSessions
|
||||
return true
|
||||
|
@ -16,7 +16,6 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type { Response } from "express"
|
||||
import { decode, sign } from "jsonwebtoken"
|
||||
import { extractToken, uuidRegex } from "./utils"
|
||||
import type { GameVersion, RequestWithJwt, UserProfile } from "./types/types"
|
||||
@ -49,10 +48,37 @@ export const JWT_SECRET = PEACOCK_DEV
|
||||
? "secret"
|
||||
: randomBytes(32).toString("hex")
|
||||
|
||||
export async function handleOauthToken(
|
||||
req: RequestWithJwt,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
export type OAuthTokenBody = {
|
||||
grant_type: "external_steam" | "external_epic" | "refresh_token"
|
||||
steam_userid?: string
|
||||
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 signOptions = {
|
||||
@ -69,19 +95,17 @@ export async function handleOauthToken(
|
||||
external_appid: string
|
||||
|
||||
if (req.body.grant_type === "external_steam") {
|
||||
if (!/^\d{1,20}$/.test(req.body.steam_userid)) {
|
||||
res.status(400).end() // invalid steam user id
|
||||
return
|
||||
if (!/^\d{1,20}$/.test(req.body.steam_userid || "")) {
|
||||
return error400 // invalid steam user id
|
||||
}
|
||||
|
||||
external_platform = "steam"
|
||||
external_userid = req.body.steam_userid
|
||||
external_userid = req.body.steam_userid || ""
|
||||
external_users_folder = "steamids"
|
||||
external_appid = req.body.steam_appid
|
||||
} else if (req.body.grant_type === "external_epic") {
|
||||
if (!/^[\da-f]{32}$/.test(req.body.epic_userid)) {
|
||||
res.status(400).end() // invalid epic user id
|
||||
return
|
||||
if (!/^[\da-f]{32}$/.test(req.body.epic_userid || "")) {
|
||||
return error400 // invalid epic user id
|
||||
}
|
||||
|
||||
const epic_token = decode(
|
||||
@ -92,25 +116,24 @@ export async function handleOauthToken(
|
||||
}
|
||||
|
||||
if (!epic_token || !(epic_token.appid || epic_token.app)) {
|
||||
res.status(400).end() // invalid epic access token
|
||||
return
|
||||
return error400 // invalid epic access token
|
||||
}
|
||||
|
||||
external_appid = epic_token.appid || epic_token.app
|
||||
external_platform = "epic"
|
||||
external_userid = req.body.epic_userid
|
||||
external_userid = req.body.epic_userid || ""
|
||||
external_users_folder = "epicids"
|
||||
} else if (req.body.grant_type === "refresh_token") {
|
||||
// send back the token from the request (re-signed so the timestamps update)
|
||||
extractToken(req) // init req.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
|
||||
// ts-expect-error Non-optional, we're reassigning.
|
||||
// @ts-expect-error Non-optional, we're reassigning.
|
||||
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
|
||||
// ts-expect-error Non-optional, we're reassigning.
|
||||
// @ts-expect-error Non-optional, we're reassigning.
|
||||
delete req.jwt.aud // audience
|
||||
|
||||
if (!isFrankenstein) {
|
||||
@ -126,35 +149,33 @@ export async function handleOauthToken(
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
return {
|
||||
access_token: sign(req.jwt, JWT_SECRET, signOptions),
|
||||
token_type: "bearer",
|
||||
expires_in: 5000,
|
||||
refresh_token: randomUUID(),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
} else {
|
||||
res.status(406).end() // unsupported auth method
|
||||
return
|
||||
return error406 // unsupported auth method
|
||||
}
|
||||
|
||||
if (req.body.pId && !uuidRegex.test(req.body.pId)) {
|
||||
res.status(400).end() // pId is not a GUID
|
||||
return
|
||||
return error406 // pId is not a GUID
|
||||
}
|
||||
|
||||
const isHitman3 =
|
||||
external_appid === "fghi4567xQOCheZIin0pazB47qGUvZw4" ||
|
||||
external_appid === STEAM_NAMESPACE_2021
|
||||
|
||||
const gameVersion: GameVersion = isFrankenstein
|
||||
? "scpc"
|
||||
: isHitman3
|
||||
? "h3"
|
||||
: external_appid === STEAM_NAMESPACE_2018
|
||||
? "h2"
|
||||
: "h1"
|
||||
let gameVersion: GameVersion = "h1"
|
||||
|
||||
if (isFrankenstein) {
|
||||
gameVersion = "scpc"
|
||||
} else if (isHitman3) {
|
||||
gameVersion = "h3"
|
||||
} else if (external_appid === STEAM_NAMESPACE_2018) {
|
||||
gameVersion = "h2"
|
||||
}
|
||||
|
||||
if (!req.body.pId) {
|
||||
// if no profile id supplied
|
||||
@ -184,7 +205,8 @@ export async function handleOauthToken(
|
||||
await writeExternalUserData(
|
||||
external_userid,
|
||||
external_users_folder,
|
||||
req.body.pId,
|
||||
// we've already confirmed this will be there, and it's a GUID
|
||||
req.body.pId!,
|
||||
gameVersion,
|
||||
)
|
||||
})
|
||||
@ -227,9 +249,9 @@ export async function handleOauthToken(
|
||||
userData.LinkedAccounts[external_platform] = external_userid
|
||||
|
||||
if (external_platform === "steam") {
|
||||
userData.SteamId = req.body.steam_userid
|
||||
userData.SteamId = req.body.steam_userid!
|
||||
} else if (external_platform === "epic") {
|
||||
userData.EpicId = req.body.epic_userid
|
||||
userData.EpicId = req.body.epic_userid!
|
||||
}
|
||||
|
||||
if (Object.hasOwn(userData.Extensions, "inventory")) {
|
||||
@ -262,13 +284,13 @@ export async function handleOauthToken(
|
||||
if (external_platform === "epic") {
|
||||
return await new EpicH3Strategy().get(
|
||||
req.body.access_token,
|
||||
req.body.epic_userid,
|
||||
req.body.epic_userid!,
|
||||
)
|
||||
} else if (external_platform === "steam") {
|
||||
return await new IOIStrategy(
|
||||
gameVersion,
|
||||
STEAM_NAMESPACE_2021,
|
||||
).get(req.body.pId)
|
||||
).get(req.body.pId!)
|
||||
} else {
|
||||
log(LogLevel.ERROR, "Unsupported platform.")
|
||||
return []
|
||||
@ -316,10 +338,10 @@ export async function handleOauthToken(
|
||||
|
||||
clearInventoryFor(req.body.pId)
|
||||
|
||||
res!.json({
|
||||
return {
|
||||
access_token: sign(userinfo, JWT_SECRET, signOptions),
|
||||
token_type: "bearer",
|
||||
expires_in: 5000,
|
||||
refresh_token: randomUUID(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,7 @@
|
||||
* 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 { log, LogLevel } from "./loggingInterop"
|
||||
import { handleAxiosError } from "./utils"
|
||||
@ -106,7 +106,7 @@ export class OfficialServerAuth {
|
||||
this._refreshToken = r.refresh_token
|
||||
this.initialized = true
|
||||
} catch (e) {
|
||||
handleAxiosError(e)
|
||||
handleAxiosError(e as AxiosError)
|
||||
|
||||
if (PEACOCK_DEV) {
|
||||
log(
|
||||
|
@ -42,7 +42,7 @@ export function calculatePlaystyle(
|
||||
const doneKillMethods: string[] = []
|
||||
const doneAccidents: string[] = []
|
||||
|
||||
session.kills.forEach((k) => {
|
||||
session.kills?.forEach((k) => {
|
||||
if (k.KillClass === "ballistic") {
|
||||
if (k.KillItemCategory === "pistol") {
|
||||
playstylesCopy[1].Score += 6000
|
||||
|
@ -29,7 +29,8 @@ import {
|
||||
import { json as jsonMiddleware } from "body-parser"
|
||||
import { getPlatformEntitlements } from "./platformEntitlements"
|
||||
import { contractSessions, newSession } from "./eventHandler"
|
||||
import type {
|
||||
import {
|
||||
ChallengeProgressionData,
|
||||
CompiledChallengeIngameData,
|
||||
ContractSession,
|
||||
GameVersion,
|
||||
@ -57,7 +58,8 @@ import {
|
||||
compileRuntimeChallenge,
|
||||
inclusionDataCheck,
|
||||
} from "./candle/challengeHelpers"
|
||||
import { LoadSaveBody } from "./types/gameSchemas"
|
||||
import { LoadSaveBody, ResolveGamerTagsBody } from "./types/gameSchemas"
|
||||
import assert from "assert"
|
||||
|
||||
const profileRouter = Router()
|
||||
|
||||
@ -109,8 +111,9 @@ export const fakePlayerRegistry: {
|
||||
|
||||
profileRouter.post(
|
||||
"/AuthenticationService/GetBlobOfflineCacheDatabaseDiff",
|
||||
// @ts-expect-error Has jwt props.
|
||||
(req: RequestWithJwt, res) => {
|
||||
const configs = []
|
||||
const configs: string[] = []
|
||||
|
||||
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")
|
||||
})
|
||||
|
||||
profileRouter.post(
|
||||
"/ProfileService/GetPlatformEntitlements",
|
||||
jsonMiddleware(),
|
||||
// @ts-expect-error Jwt props.
|
||||
getPlatformEntitlements,
|
||||
)
|
||||
|
||||
profileRouter.post(
|
||||
"/ProfileService/UpdateProfileStats",
|
||||
jsonMiddleware(),
|
||||
// @ts-expect-error Has jwt props.
|
||||
(req: RequestWithJwt, res) => {
|
||||
if (req.jwt.unique_name !== req.body.id) {
|
||||
return res.status(403).end() // data submitted for different profile id
|
||||
@ -148,18 +153,19 @@ profileRouter.post(
|
||||
|
||||
profileRouter.post(
|
||||
"/ProfileService/SynchronizeOfflineUnlockables",
|
||||
(req, res) => {
|
||||
(_, res) => {
|
||||
res.status(204).end()
|
||||
},
|
||||
)
|
||||
|
||||
profileRouter.post("/ProfileService/GetUserConfig", (req, res) => {
|
||||
profileRouter.post("/ProfileService/GetUserConfig", (_, res) => {
|
||||
res.json({})
|
||||
})
|
||||
|
||||
profileRouter.post(
|
||||
"/ProfileService/GetProfile",
|
||||
jsonMiddleware(),
|
||||
// @ts-expect-error Has jwt props.
|
||||
(req: RequestWithJwt, res) => {
|
||||
if (req.body.id !== req.jwt.unique_name) {
|
||||
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 extensions = req.body.extensions.reduce(
|
||||
(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]
|
||||
}
|
||||
|
||||
@ -188,6 +197,7 @@ profileRouter.post(
|
||||
|
||||
profileRouter.post(
|
||||
"/UnlockableService/GetInventory",
|
||||
// @ts-expect-error Has jwt props.
|
||||
(req: RequestWithJwt, res) => {
|
||||
res.json(createInventory(req.jwt.unique_name, req.gameVersion))
|
||||
},
|
||||
@ -196,6 +206,7 @@ profileRouter.post(
|
||||
profileRouter.post(
|
||||
"/ProfileService/UpdateExtensions",
|
||||
jsonMiddleware(),
|
||||
// @ts-expect-error jwt props.
|
||||
(
|
||||
req: RequestWithJwt<
|
||||
Record<string, never>,
|
||||
@ -213,6 +224,7 @@ profileRouter.post(
|
||||
|
||||
for (const extension in req.body.extensionsData) {
|
||||
if (Object.hasOwn(req.body.extensionsData, extension)) {
|
||||
// @ts-expect-error It's fine.
|
||||
userdata.Extensions[extension] =
|
||||
req.body.extensionsData[extension]
|
||||
}
|
||||
@ -226,6 +238,7 @@ profileRouter.post(
|
||||
profileRouter.post(
|
||||
"/ProfileService/SynchroniseGameStats",
|
||||
jsonMiddleware(),
|
||||
// @ts-expect-error jwt props.
|
||||
(req: RequestWithJwt, res) => {
|
||||
if (req.body.profileId !== req.jwt.unique_name) {
|
||||
// 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)
|
||||
|
||||
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>) => {
|
||||
if (outcome.status !== "fulfilled") {
|
||||
if (outcome.reason.code === "ENOENT") {
|
||||
@ -387,6 +375,7 @@ export async function resolveProfiles(
|
||||
profileRouter.post(
|
||||
"/ProfileService/ResolveProfiles",
|
||||
jsonMiddleware(),
|
||||
// @ts-expect-error Has jwt props.
|
||||
async (req: RequestWithJwt, res) => {
|
||||
res.json(await resolveProfiles(req.body.profileIDs, req.gameVersion))
|
||||
},
|
||||
@ -395,16 +384,22 @@ profileRouter.post(
|
||||
profileRouter.post(
|
||||
"/ProfileService/ResolveGamerTags",
|
||||
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(
|
||||
req.body.profileIds,
|
||||
req.gameVersion,
|
||||
)) as UserProfile[]
|
||||
|
||||
const result = {
|
||||
steam: {},
|
||||
epic: {},
|
||||
dev: {},
|
||||
steam: {} as Record<string, string>,
|
||||
epic: {} as Record<string, string>,
|
||||
dev: {} as Record<string, string>,
|
||||
}
|
||||
|
||||
for (const profile of profiles) {
|
||||
@ -427,26 +422,27 @@ profileRouter.post(
|
||||
},
|
||||
)
|
||||
|
||||
profileRouter.post("/ProfileService/GetFriendsCount", (req, res) =>
|
||||
res.send("0"),
|
||||
)
|
||||
profileRouter.post("/ProfileService/GetFriendsCount", (_, res) => res.send("0"))
|
||||
|
||||
profileRouter.post(
|
||||
"/GamePersistentDataService/GetData",
|
||||
jsonMiddleware(),
|
||||
// @ts-expect-error Has jwt props.
|
||||
(req: RequestWithJwt, res) => {
|
||||
if (req.jwt.unique_name !== req.body.userId) {
|
||||
return res.status(403).end()
|
||||
}
|
||||
|
||||
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(
|
||||
"/GamePersistentDataService/SaveData",
|
||||
jsonMiddleware(),
|
||||
// @ts-expect-error jwt props.
|
||||
(req: RequestWithJwt, res) => {
|
||||
if (req.jwt.unique_name !== req.body.userId) {
|
||||
return res.status(403).end()
|
||||
@ -454,6 +450,7 @@ profileRouter.post(
|
||||
|
||||
const userdata = getUserData(req.body.userId, req.gameVersion)
|
||||
|
||||
// @ts-expect-error This is fine.
|
||||
userdata.Extensions.gamepersistentdata[req.body.key] = req.body.data
|
||||
writeUserData(req.body.userId, req.gameVersion)
|
||||
|
||||
@ -464,6 +461,7 @@ profileRouter.post(
|
||||
profileRouter.post(
|
||||
"/ChallengesService/GetActiveChallengesAndProgression",
|
||||
jsonMiddleware(),
|
||||
// @ts-expect-error Has jwt props.
|
||||
(
|
||||
req: RequestWithJwt<
|
||||
Record<string, never>,
|
||||
@ -489,11 +487,14 @@ profileRouter.post(
|
||||
return res.json([])
|
||||
}
|
||||
|
||||
let challenges = getVersionedConfig<CompiledChallengeIngameData[]>(
|
||||
"GlobalChallenges",
|
||||
req.gameVersion,
|
||||
true,
|
||||
)
|
||||
type CWP = {
|
||||
Challenge: CompiledChallengeIngameData
|
||||
Progression: ChallengeProgressionData | undefined
|
||||
}
|
||||
|
||||
let challenges: CWP[] = getVersionedConfig<
|
||||
CompiledChallengeIngameData[]
|
||||
>("GlobalChallenges", req.gameVersion, true)
|
||||
.filter((val) => inclusionDataCheck(val.InclusionData, json))
|
||||
.map((item) => ({ Challenge: item, Progression: undefined }))
|
||||
|
||||
@ -528,6 +529,7 @@ profileRouter.post(
|
||||
challenges.forEach((val) => {
|
||||
// prettier-ignore
|
||||
if (val.Challenge.Id === "b1a85feb-55af-4707-8271-b3522661c0b1") {
|
||||
// @ts-expect-error State machines impossible to type.
|
||||
// prettier-ignore
|
||||
val.Challenge.Definition!["States"]["Start"][
|
||||
"CrowdNPC_Died"
|
||||
@ -580,6 +582,7 @@ profileRouter.post(
|
||||
profileRouter.post(
|
||||
"/HubPagesService/GetChallengeTreeFor",
|
||||
jsonMiddleware(),
|
||||
// @ts-expect-error Has jwt props.
|
||||
(req: RequestWithJwt, res) => {
|
||||
res.json({
|
||||
Data: {
|
||||
@ -607,6 +610,7 @@ profileRouter.post(
|
||||
profileRouter.post(
|
||||
"/DefaultLoadoutService/Set",
|
||||
jsonMiddleware(),
|
||||
// @ts-expect-error jwt props.
|
||||
async (req: RequestWithJwt, res) => {
|
||||
if (getFlag("loadoutSaving") === "PROFILES") {
|
||||
let loadout = loadouts.getLoadoutFor(req.gameVersion)
|
||||
@ -638,6 +642,7 @@ profileRouter.post(
|
||||
profileRouter.post(
|
||||
"/ProfileService/UpdateUserSaveFileTable",
|
||||
jsonMiddleware(),
|
||||
// @ts-expect-error Has jwt props.
|
||||
async (req: RequestWithJwt<never, UpdateUserSaveFileTableBody>, res) => {
|
||||
if (req.body.clientSaveFileList.length > 0) {
|
||||
// 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
|
||||
return String(error)
|
||||
}
|
||||
@ -691,7 +696,82 @@ function getErrorCause(error: unknown) {
|
||||
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,
|
||||
userData: UserProfile,
|
||||
): Promise<void> {
|
||||
@ -747,69 +827,12 @@ async function saveSession(
|
||||
log(
|
||||
LogLevel.DEBUG,
|
||||
`Saved contract to slot ${slot} with token = ${token}, session id = ${sessionId}, start time = ${
|
||||
contractSessions.get(sessionId).timerStart
|
||||
contractSessions.get(sessionId)!.timerStart
|
||||
}.`,
|
||||
)
|
||||
}
|
||||
|
||||
profileRouter.post(
|
||||
"/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(
|
||||
export async function loadSession(
|
||||
sessionId: string,
|
||||
token: string,
|
||||
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
|
||||
for (const cid in sessionData.challengeContexts) {
|
||||
// Make sure the ChallengeProgression is available, otherwise loading might fail!
|
||||
@ -846,6 +871,11 @@ async function loadSession(
|
||||
sessionData.gameVersion,
|
||||
)
|
||||
|
||||
assert.ok(
|
||||
challenge,
|
||||
`session has context for unregistered challenge ${cid}`,
|
||||
)
|
||||
|
||||
if (
|
||||
!userData.Extensions.ChallengeProgression[cid].Completed &&
|
||||
controller.challengeService.needSaveProgression(challenge)
|
||||
@ -859,22 +889,9 @@ async function loadSession(
|
||||
log(
|
||||
LogLevel.DEBUG,
|
||||
`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 }
|
||||
|
@ -48,7 +48,7 @@ import {
|
||||
getLevelCount,
|
||||
} from "./contracts/escalations/escalationService"
|
||||
import { getUserData, writeUserData } from "./databaseHandler"
|
||||
import axios from "axios"
|
||||
import axios, { AxiosError } from "axios"
|
||||
import { getFlag } from "./flags"
|
||||
import { log, LogLevel } from "./loggingInterop"
|
||||
import {
|
||||
@ -64,6 +64,7 @@ import {
|
||||
CalculateScoreResult,
|
||||
CalculateSniperScoreResult,
|
||||
CalculateXpResult,
|
||||
ContractScore,
|
||||
MissionEndChallenge,
|
||||
MissionEndDrop,
|
||||
MissionEndEvergreen,
|
||||
@ -72,6 +73,7 @@ import {
|
||||
import { MasteryData } from "./types/mastery"
|
||||
import { createInventory, InventoryItem, getUnlockablesById } from "./inventory"
|
||||
import { calculatePlaystyle } from "./playStyles"
|
||||
import assert from "assert"
|
||||
|
||||
export function calculateGlobalXp(
|
||||
contractSession: ContractSession,
|
||||
@ -81,8 +83,10 @@ export function calculateGlobalXp(
|
||||
let totalXp = 0
|
||||
|
||||
// TODO: Merge with the non-global challenges?
|
||||
for (const challengeId of Object.keys(contractSession.challengeContexts)) {
|
||||
const data = contractSession.challengeContexts[challengeId]
|
||||
for (const challengeId of Object.keys(
|
||||
contractSession.challengeContexts || {},
|
||||
)) {
|
||||
const data = contractSession.challengeContexts![challengeId]
|
||||
|
||||
if (data.timesCompleted <= 0) {
|
||||
continue
|
||||
@ -93,7 +97,7 @@ export function calculateGlobalXp(
|
||||
gameVersion,
|
||||
)
|
||||
|
||||
if (!challenge || !challenge.Xp || !challenge.Tags.includes("global")) {
|
||||
if (!challenge?.Xp || !challenge.Tags.includes("global")) {
|
||||
continue
|
||||
}
|
||||
|
||||
@ -181,7 +185,7 @@ export function calculateScore(
|
||||
headline: "UI_SCORING_SUMMARY_NO_BODIES_FOUND",
|
||||
bonusId: "NoBodiesFound",
|
||||
condition:
|
||||
contractSession.legacyHasBodyBeenFound === false &&
|
||||
!contractSession.legacyHasBodyBeenFound &&
|
||||
[...contractSession.bodiesFoundBy].every(
|
||||
(witness) =>
|
||||
(gameVersion === "h1"
|
||||
@ -386,7 +390,8 @@ export function calculateSniperScore(
|
||||
[480, 35000], // 35000 bonus score at 480 secs (8 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) {
|
||||
if (bonusTimeTotal > secs) {
|
||||
@ -414,36 +419,46 @@ export function calculateSniperScore(
|
||||
scoreTotal: 0,
|
||||
}
|
||||
|
||||
const baseScore = contractSession.scoring.Context["TotalScore"]
|
||||
const challengeMultiplier = contractSession.scoring.Settings["challenges"][
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const baseScore = (contractSession.scoring?.Context as any)["TotalScore"]
|
||||
// @ts-expect-error it's a number
|
||||
const challengeMultiplier = contractSession.scoring?.Settings["challenges"][
|
||||
"Unlockables"
|
||||
].reduce((acc, unlockable) => {
|
||||
const item = inventory.find((item) => item.Unlockable.Id === unlockable)
|
||||
|
||||
if (item) {
|
||||
// @ts-expect-error it's a number
|
||||
return acc + item.Unlockable.Properties["Multiplier"]
|
||||
}
|
||||
|
||||
return acc
|
||||
}, 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 bulletsMissedPenalty =
|
||||
bulletsMissed *
|
||||
contractSession.scoring.Settings["bulletsused"]["penalty"]
|
||||
(contractSession.scoring?.Settings["bulletsused"]["penalty"] || 0)
|
||||
// Get SA status from global SA challenge for contracttype sniper
|
||||
const silentAssassin =
|
||||
contractSession.challengeContexts[
|
||||
contractSession.challengeContexts?.[
|
||||
"029c4971-0ddd-47ab-a568-17b007eec04e"
|
||||
].state !== "Failure"
|
||||
const saBonus = silentAssassin
|
||||
? contractSession.scoring.Settings["silentassassin"]["score"]
|
||||
? contractSession.scoring?.Settings["silentassassin"]["score"]
|
||||
: 0
|
||||
const saMultiplier = silentAssassin
|
||||
? contractSession.scoring.Settings["silentassassin"]["multiplier"]
|
||||
? contractSession.scoring?.Settings["silentassassin"]["multiplier"]
|
||||
: 1.0
|
||||
|
||||
const subTotalScore = baseScore + timeBonus + saBonus - bulletsMissedPenalty
|
||||
const totalScore = Math.round(
|
||||
// @ts-expect-error it's a number
|
||||
subTotalScore * challengeMultiplier * saMultiplier,
|
||||
)
|
||||
|
||||
@ -518,7 +533,7 @@ export async function getMissionEndData(
|
||||
gameVersion: GameVersion,
|
||||
): Promise<MissionEndError | MissionEndResult> {
|
||||
// TODO: For this entire function, add support for 2016 difficulties
|
||||
const sessionDetails = contractSessions.get(query.contractSessionId)
|
||||
const sessionDetails = contractSessions.get(query.contractSessionId || "")
|
||||
|
||||
if (!sessionDetails) {
|
||||
return {
|
||||
@ -625,6 +640,8 @@ export async function getMissionEndData(
|
||||
false,
|
||||
)
|
||||
|
||||
assert.ok(levelData, "contract not found")
|
||||
|
||||
// Resolve the id of the parent location
|
||||
const subLocation = getSubLocationByName(
|
||||
levelData.Metadata.Location,
|
||||
@ -777,9 +794,9 @@ export async function getMissionEndData(
|
||||
if (masteryData) {
|
||||
maxLevel =
|
||||
(query.masteryUnlockableId
|
||||
? masteryData.SubPackages.find(
|
||||
? masteryData.SubPackages?.find(
|
||||
(subPkg) => subPkg.Id === query.masteryUnlockableId,
|
||||
).MaxLevel
|
||||
)?.MaxLevel
|
||||
: masteryData.MaxLevel) || DEFAULT_MASTERY_MAXLEVEL
|
||||
|
||||
locationLevelInfo = Array.from({ length: maxLevel }, (_, i) => {
|
||||
@ -828,7 +845,8 @@ export async function getMissionEndData(
|
||||
)
|
||||
|
||||
// Evergreen
|
||||
const evergreenData: MissionEndEvergreen = <MissionEndEvergreen>{
|
||||
const evergreenData: MissionEndEvergreen = {
|
||||
Payout: 0,
|
||||
PayoutsCompleted: [],
|
||||
PayoutsFailed: [],
|
||||
}
|
||||
@ -846,14 +864,14 @@ export async function getMissionEndData(
|
||||
Object.keys(gameChangerProperties).forEach((e) => {
|
||||
const gameChanger = gameChangerProperties[e]
|
||||
|
||||
const conditionObjective = gameChanger.Objectives.find(
|
||||
const conditionObjective = gameChanger.Objectives?.find(
|
||||
(e) => e.Category === "condition",
|
||||
)
|
||||
|
||||
const secondaryObjective = gameChanger.Objectives.find(
|
||||
const secondaryObjective = gameChanger.Objectives?.find(
|
||||
(e) =>
|
||||
e.Category === "secondary" &&
|
||||
e.Definition.Context["MyPayout"],
|
||||
e.Definition?.Context?.["MyPayout"],
|
||||
)
|
||||
|
||||
if (
|
||||
@ -862,18 +880,20 @@ export async function getMissionEndData(
|
||||
sessionDetails.objectiveStates.get(conditionObjective.Id) ===
|
||||
"Success"
|
||||
) {
|
||||
type P = { MyPayout: string }
|
||||
|
||||
const context = sessionDetails.objectiveContexts.get(
|
||||
secondaryObjective.Id,
|
||||
) as P | undefined
|
||||
|
||||
const payoutObjective = {
|
||||
Name: gameChanger.Name,
|
||||
Payout: parseInt(
|
||||
sessionDetails.objectiveContexts.get(
|
||||
secondaryObjective.Id,
|
||||
)["MyPayout"] || 0,
|
||||
),
|
||||
Payout: parseInt(context?.["MyPayout"] || "0"),
|
||||
IsPrestige: gameChanger.IsPrestigeObjective || false,
|
||||
}
|
||||
|
||||
if (
|
||||
!sessionDetails.evergreen.failed &&
|
||||
!sessionDetails.evergreen?.failed &&
|
||||
sessionDetails.objectiveStates.get(
|
||||
secondaryObjective.Id,
|
||||
) === "Success"
|
||||
@ -888,7 +908,7 @@ export async function getMissionEndData(
|
||||
|
||||
evergreenData.Payout = totalPayout
|
||||
evergreenData.EndStateEventName =
|
||||
sessionDetails.evergreen.scoringScreenEndState
|
||||
sessionDetails.evergreen?.scoringScreenEndState
|
||||
|
||||
locationLevelInfo = EVERGREEN_LEVEL_INFO
|
||||
|
||||
@ -909,14 +929,14 @@ export async function getMissionEndData(
|
||||
calculateScoreResult.silentAssassin = false
|
||||
|
||||
// Overide the calculated score
|
||||
calculateScoreResult.stars = undefined
|
||||
calculateScoreResult.stars = 0
|
||||
}
|
||||
|
||||
// Sniper
|
||||
let unlockableProgression = undefined
|
||||
let sniperChallengeScore = undefined
|
||||
let sniperChallengeScore: CalculateSniperScoreResult | undefined = undefined
|
||||
|
||||
let contractScore = {
|
||||
let contractScore: ContractScore | undefined = {
|
||||
Total: calculateScoreResult.scoreWithBonus,
|
||||
AchievedMasteries: calculateScoreResult.achievedMasteries,
|
||||
AwardedBonuses: calculateScoreResult.awardedBonuses,
|
||||
@ -969,7 +989,7 @@ export async function getMissionEndData(
|
||||
Id: completionData.Id,
|
||||
Level: completionData.Level,
|
||||
LevelInfo: locationLevelInfo,
|
||||
Name: completionData.Name,
|
||||
Name: completionData.Name!,
|
||||
XP: completionData.XP,
|
||||
XPGain:
|
||||
completionData.Level === completionData.MaxLevel
|
||||
@ -977,8 +997,9 @@ export async function getMissionEndData(
|
||||
: sniperScore.FinalScore,
|
||||
}
|
||||
|
||||
// @ts-expect-error should be fine (allegedly)
|
||||
userData.Extensions.progression.Locations[locationParentId][
|
||||
query.masteryUnlockableId
|
||||
query.masteryUnlockableId!
|
||||
].PreviouslySeenXp = completionData.XP
|
||||
|
||||
writeUserData(jwt.unique_name, gameVersion)
|
||||
@ -996,7 +1017,7 @@ export async function getMissionEndData(
|
||||
// Override the playstyle
|
||||
playstyle = undefined
|
||||
|
||||
calculateScoreResult.stars = undefined
|
||||
calculateScoreResult.stars = 0
|
||||
calculateScoreResult.scoringHeadlines = headlines
|
||||
}
|
||||
|
||||
@ -1009,7 +1030,7 @@ export async function getMissionEndData(
|
||||
const masteryData =
|
||||
controller.masteryService.getMasteryDataForSubPackage(
|
||||
locationParentId,
|
||||
query.masteryUnlockableId ?? undefined,
|
||||
query.masteryUnlockableId!,
|
||||
gameVersion,
|
||||
jwt.unique_name,
|
||||
) as MasteryData
|
||||
@ -1018,31 +1039,39 @@ export async function getMissionEndData(
|
||||
masteryDrops = masteryData.Drops.filter(
|
||||
(e) =>
|
||||
e.Level > oldLocationLevel && e.Level <= newLocationLevel,
|
||||
).map((e) => {
|
||||
return {
|
||||
Unlockable: e.Unlockable,
|
||||
}
|
||||
})
|
||||
).map((e) => ({
|
||||
Unlockable: e.Unlockable,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
// Challenge Drops
|
||||
const challengeDrops: MissionEndDrop[] =
|
||||
calculateXpResult.completedChallenges.reduce((acc, challenge) => {
|
||||
if (challenge?.Drops?.length) {
|
||||
const drops = getUnlockablesById(challenge.Drops, gameVersion)
|
||||
delete challenge.Drops
|
||||
calculateXpResult.completedChallenges.reduce(
|
||||
(acc: MissionEndDrop[], challenge) => {
|
||||
if (challenge?.Drops?.length) {
|
||||
const drops = getUnlockablesById(
|
||||
challenge.Drops,
|
||||
gameVersion,
|
||||
)
|
||||
delete challenge.Drops
|
||||
|
||||
for (const drop of drops) {
|
||||
acc.push({
|
||||
Unlockable: drop,
|
||||
SourceChallenge: challenge,
|
||||
})
|
||||
for (const drop of drops) {
|
||||
if (!drop) {
|
||||
continue
|
||||
}
|
||||
|
||||
acc.push({
|
||||
Unlockable: drop,
|
||||
SourceChallenge: challenge,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return acc
|
||||
}, [])
|
||||
return acc
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
// Setup the result
|
||||
const result: MissionEndResult = {
|
||||
@ -1094,7 +1123,7 @@ export async function getMissionEndData(
|
||||
SniperChallengeScore: sniperChallengeScore,
|
||||
SilentAssassin:
|
||||
contractScore?.SilentAssassin ||
|
||||
sniperChallengeScore?.silentAssassin ||
|
||||
sniperChallengeScore?.SilentAssassin ||
|
||||
false,
|
||||
// TODO: Use data from the leaderboard?
|
||||
NewRank: 1,
|
||||
@ -1112,7 +1141,7 @@ export async function getMissionEndData(
|
||||
}
|
||||
|
||||
// Finalize the response
|
||||
if ((getFlag("autoSplitterForceSilentAssassin") as boolean) === true) {
|
||||
if (getFlag("autoSplitterForceSilentAssassin")) {
|
||||
if (result.ScoreOverview.SilentAssassin) {
|
||||
await liveSplitManager.completeMission(timeTotal)
|
||||
} else {
|
||||
@ -1124,7 +1153,7 @@ export async function getMissionEndData(
|
||||
|
||||
if (
|
||||
getFlag("leaderboards") === true &&
|
||||
sessionDetails.compat === true &&
|
||||
sessionDetails.compat &&
|
||||
contractData.Metadata.Type !== "vsrace" &&
|
||||
contractData.Metadata.Type !== "evergreen" &&
|
||||
// Disable sending sniper scores for now
|
||||
@ -1187,7 +1216,7 @@ export async function getMissionEndData(
|
||||
},
|
||||
)
|
||||
} catch (e) {
|
||||
handleAxiosError(e)
|
||||
handleAxiosError(e as AxiosError)
|
||||
log(
|
||||
LogLevel.WARN,
|
||||
"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 placeBefore = contractData.SMF?.destinations.placeBefore
|
||||
const placeAfter = contractData.SMF?.destinations.placeAfter
|
||||
// @ts-expect-error I know what I'm doing.
|
||||
const inLocation = (this.controller.missionsInLocations[location] ??
|
||||
// @ts-expect-error I know what I'm doing.
|
||||
(this.controller.missionsInLocations[location] = [])) as string[]
|
||||
|
||||
if (placeBefore) {
|
||||
|
@ -165,7 +165,6 @@ export function parseContextListeners(
|
||||
info.challengeCountData.total = test(total, context)
|
||||
|
||||
// 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")) {
|
||||
info.challengeTreeIds.push(
|
||||
...test("$.RequiredChallenges", context),
|
||||
|
@ -42,7 +42,9 @@ export interface IContractCreationPayload {
|
||||
* The target creator API.
|
||||
*/
|
||||
export class TargetCreator {
|
||||
// @ts-expect-error TODO: type this
|
||||
private _targetSm
|
||||
// @ts-expect-error TODO: type this
|
||||
private _outfitSm
|
||||
private _targetConds: undefined | unknown[] = undefined
|
||||
|
||||
|
@ -23,6 +23,7 @@ import {
|
||||
InclusionData,
|
||||
MissionManifestObjective,
|
||||
} from "./types"
|
||||
import { gameDifficulty } from "../utils"
|
||||
|
||||
export interface SavedChallenge {
|
||||
Id: string
|
||||
@ -45,7 +46,7 @@ export interface SavedChallenge {
|
||||
RuntimeType: "Hit" | string
|
||||
Xp: number
|
||||
XpModifier?: unknown
|
||||
DifficultyLevels: string[]
|
||||
DifficultyLevels: (keyof typeof gameDifficulty)[]
|
||||
Definition: MissionManifestObjective["Definition"] & {
|
||||
Scope: ContextScopedStorageLocation
|
||||
Repeatable?: {
|
||||
@ -93,7 +94,7 @@ export type ProfileChallengeData = {
|
||||
|
||||
export type ChallengeContext = {
|
||||
context: unknown
|
||||
state: string
|
||||
state: string | undefined
|
||||
timers: Timer[]
|
||||
timesCompleted: number
|
||||
}
|
||||
|
@ -257,3 +257,7 @@ export type Dart_HitC2SEvent = ClientToServerEvent<{
|
||||
export type Evergreen_Payout_DataC2SEvent = ClientToServerEvent<{
|
||||
Total_Payout: number
|
||||
}>
|
||||
|
||||
export type OpponentsC2sEvent = ClientToServerEvent<{
|
||||
ConnectedSessions: string[]
|
||||
}>
|
||||
|
@ -32,7 +32,7 @@ export type StashpointSlotName =
|
||||
| string
|
||||
|
||||
/**
|
||||
* Query that the game sends for the stashpoint route.
|
||||
* Query for `/profiles/page/stashpoint`.
|
||||
*/
|
||||
export type StashpointQuery = Partial<{
|
||||
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
|
||||
*/
|
||||
@ -94,8 +94,7 @@ export type GetCompletionDataForLocationQuery = Partial<{
|
||||
}>
|
||||
|
||||
/**
|
||||
* Body that the game sends for the
|
||||
* `/authentication/api/userchannel/ContractSessionsService/Load` route.
|
||||
* Body for `/authentication/api/userchannel/ContractSessionsService/Load`.
|
||||
*/
|
||||
export type LoadSaveBody = Partial<{
|
||||
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.
|
||||
*/
|
||||
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 = {
|
||||
type?: string
|
||||
@ -122,7 +121,7 @@ export type SafehouseCategoryQuery = {
|
||||
}
|
||||
|
||||
/**
|
||||
* Query params that `/profiles/page/Destination` gets.
|
||||
* Query for `/profiles/page/Destination`.
|
||||
*/
|
||||
export type GetDestinationQuery = {
|
||||
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.
|
||||
*/
|
||||
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 = {
|
||||
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"
|
||||
|
||||
export interface MasteryDataTemplate {
|
||||
template: unknown
|
||||
data: {
|
||||
Location: Unlockable
|
||||
MasteryData: MasteryData[]
|
||||
}
|
||||
export interface LocationMasteryData {
|
||||
Location: Unlockable
|
||||
MasteryData: MasteryData[]
|
||||
}
|
||||
|
||||
export interface MasteryPackageDrop {
|
||||
@ -41,19 +38,27 @@ interface MasterySubPackage {
|
||||
* @since v7.0.0
|
||||
* 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
|
||||
* and mastery on H2016 as it is separated by difficulty.
|
||||
*
|
||||
* Also, a GameVersions array has been added to support multi-version mastery.
|
||||
*/
|
||||
export interface MasteryPackage {
|
||||
export type MasteryPackage = {
|
||||
LocationId: string
|
||||
GameVersions: GameVersion[]
|
||||
MaxLevel?: number
|
||||
HideProgression?: boolean
|
||||
Drops?: MasteryPackageDrop[]
|
||||
SubPackages?: MasterySubPackage[]
|
||||
} & (HasDrop | HasSubPackage)
|
||||
|
||||
type HasDrop = {
|
||||
Drops: MasteryPackageDrop[]
|
||||
SubPackages?: never
|
||||
}
|
||||
|
||||
type HasSubPackage = {
|
||||
Drops?: never
|
||||
SubPackages: MasterySubPackage[]
|
||||
}
|
||||
|
||||
export interface MasteryData {
|
||||
|
@ -25,12 +25,31 @@ import {
|
||||
Unlockable,
|
||||
} from "./types"
|
||||
|
||||
export interface CalculateXpResult {
|
||||
export type CalculateXpResult = {
|
||||
completedChallenges: MissionEndChallenge[]
|
||||
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
|
||||
scoringHeadlines: ScoringHeadline[]
|
||||
awardedBonuses: ScoringBonus[]
|
||||
@ -41,7 +60,7 @@ export interface CalculateScoreResult {
|
||||
scoreWithBonus: number
|
||||
}
|
||||
|
||||
export interface CalculateSniperScoreResult {
|
||||
export type CalculateSniperScoreResult = {
|
||||
FinalScore: number
|
||||
BaseScore: number
|
||||
TotalChallengeMultiplier: number
|
||||
@ -50,11 +69,11 @@ export interface CalculateSniperScoreResult {
|
||||
TimeTaken: number
|
||||
TimeBonus: number
|
||||
SilentAssassin: boolean
|
||||
SilentAssassinBonus: number
|
||||
SilentAssassinMultiplier: number
|
||||
SilentAssassinBonus: number | undefined
|
||||
SilentAssassinMultiplier: number | undefined
|
||||
}
|
||||
|
||||
export interface MissionEndChallenge {
|
||||
export type MissionEndChallenge = {
|
||||
ChallengeId: string
|
||||
ChallengeTags: string[]
|
||||
ChallengeName: string
|
||||
@ -66,7 +85,7 @@ export interface MissionEndChallenge {
|
||||
Drops?: string[]
|
||||
}
|
||||
|
||||
export interface MissionEndSourceChallenge {
|
||||
export type MissionEndSourceChallenge = {
|
||||
ChallengeId: string
|
||||
ChallengeTags: string[]
|
||||
ChallengeName: string
|
||||
@ -77,12 +96,12 @@ export interface MissionEndSourceChallenge {
|
||||
IsActionReward: boolean
|
||||
}
|
||||
|
||||
export interface MissionEndDrop {
|
||||
export type MissionEndDrop = {
|
||||
Unlockable: Unlockable
|
||||
SourceChallenge?: MissionEndSourceChallenge
|
||||
}
|
||||
|
||||
export interface MissionEndAchievedMastery {
|
||||
export type MissionEndAchievedMastery = {
|
||||
score: number
|
||||
RatioParts: number
|
||||
RatioTotal: number
|
||||
@ -90,47 +109,38 @@ export interface MissionEndAchievedMastery {
|
||||
BaseScore: number
|
||||
}
|
||||
|
||||
export interface MissionEndEvergreen {
|
||||
export type MissionEndEvergreen = {
|
||||
Payout: number
|
||||
EndStateEventName?: string
|
||||
EndStateEventName?: string | null
|
||||
PayoutsCompleted: MissionEndEvergreenPayout[]
|
||||
PayoutsFailed: MissionEndEvergreenPayout[]
|
||||
}
|
||||
|
||||
export interface MissionEndEvergreenPayout {
|
||||
export type MissionEndEvergreenPayout = {
|
||||
Name: string
|
||||
Payout: number
|
||||
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: {
|
||||
LocationProgression: {
|
||||
LevelInfo: number[]
|
||||
XP: number
|
||||
Level: number
|
||||
Completion: number
|
||||
XPGain: number
|
||||
HideProgression: boolean
|
||||
}
|
||||
ProfileProgression: {
|
||||
LevelInfo: number[]
|
||||
LevelInfoOffset: number
|
||||
XP: number
|
||||
Level: number
|
||||
XPGain: number
|
||||
}
|
||||
LocationProgression: ScoreProgressionStats
|
||||
ProfileProgression: ScoreProfileProgressionStats
|
||||
Challenges: MissionEndChallenge[]
|
||||
Drops: MissionEndDrop[]
|
||||
OpportunityRewards: unknown[] // ?
|
||||
UnlockableProgression?: {
|
||||
LevelInfo: number[]
|
||||
XP: number
|
||||
Level: number
|
||||
XPGain: number
|
||||
Id: string
|
||||
Name: string
|
||||
}
|
||||
UnlockableProgression?: ScoreProgressionStats
|
||||
CompletionData: CompletionData
|
||||
ChallengeCompletion: ChallengeCompletion
|
||||
ContractChallengeCompletion: ChallengeCompletion
|
||||
@ -149,28 +159,8 @@ export interface MissionEndResult {
|
||||
ScoreDetails: {
|
||||
Headlines: ScoringHeadline[]
|
||||
}
|
||||
ContractScore?: {
|
||||
Total: number
|
||||
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
|
||||
}
|
||||
ContractScore?: ContractScore
|
||||
SniperChallengeScore?: CalculateSniperScoreResult
|
||||
SilentAssassin: boolean
|
||||
NewRank: number
|
||||
RankCount: number
|
||||
|
@ -19,7 +19,7 @@
|
||||
import type * as core from "express-serve-static-core"
|
||||
|
||||
import type { IContractCreationPayload } from "../statemachines/contractCreation"
|
||||
import type { Request } from "express"
|
||||
import { Request } from "express"
|
||||
import {
|
||||
ChallengeContext,
|
||||
ProfileChallengeData,
|
||||
@ -29,6 +29,7 @@ import { SessionGhostModeDetails } from "../multiplayer/multiplayerService"
|
||||
import { IContextListener } from "../statemachines/contextListeners"
|
||||
import { ManifestScoringModule, ScoringModule } from "./scoring"
|
||||
import { Timer } from "@peacockproject/statemachine-parser"
|
||||
import { InventoryItem } from "../inventory"
|
||||
|
||||
/**
|
||||
* A duration or relative point in time expressed in seconds.
|
||||
@ -274,7 +275,7 @@ export interface ContractSession {
|
||||
*/
|
||||
evergreen?: {
|
||||
payout: number
|
||||
scoringScreenEndState: string
|
||||
scoringScreenEndState: string | null
|
||||
failed: boolean
|
||||
}
|
||||
/**
|
||||
@ -367,7 +368,7 @@ export interface S2CEventWithTimestamp<EventValue = unknown> {
|
||||
* A server to client push message. The message component is encoded JSON.
|
||||
*/
|
||||
export interface PushMessage {
|
||||
time: number | string
|
||||
time: number | string | bigint
|
||||
message: string
|
||||
}
|
||||
|
||||
@ -405,33 +406,30 @@ export interface MissionStory {
|
||||
}
|
||||
|
||||
export interface PlayerProfileView {
|
||||
template: unknown
|
||||
data: {
|
||||
SubLocationData: {
|
||||
ParentLocation: Unlockable
|
||||
Location: Unlockable
|
||||
CompletionData: CompletionData
|
||||
ChallengeCategoryCompletion: ChallengeCategoryCompletion[]
|
||||
ChallengeCompletion: ChallengeCompletion
|
||||
OpportunityStatistics: OpportunityStatistics
|
||||
LocationCompletionPercent: number
|
||||
}[]
|
||||
PlayerProfileXp: {
|
||||
Total: number
|
||||
Level: number
|
||||
Seasons: {
|
||||
Number: number
|
||||
Locations: {
|
||||
LocationId: string
|
||||
Xp: number
|
||||
ActionXp: number
|
||||
LocationProgression?: {
|
||||
Level: number
|
||||
MaxLevel: number
|
||||
}
|
||||
}[]
|
||||
SubLocationData: {
|
||||
ParentLocation: Unlockable
|
||||
Location: Unlockable
|
||||
CompletionData: CompletionData
|
||||
ChallengeCategoryCompletion: ChallengeCategoryCompletion[]
|
||||
ChallengeCompletion: ChallengeCompletion
|
||||
OpportunityStatistics: OpportunityStatistics
|
||||
LocationCompletionPercent: number
|
||||
}[]
|
||||
PlayerProfileXp: {
|
||||
Total: number
|
||||
Level: number
|
||||
Seasons: {
|
||||
Number: number
|
||||
Locations: {
|
||||
LocationId: string
|
||||
Xp: number
|
||||
ActionXp: number
|
||||
LocationProgression?: {
|
||||
Level: number
|
||||
MaxLevel: number
|
||||
}
|
||||
}[]
|
||||
}
|
||||
}[]
|
||||
}
|
||||
}
|
||||
|
||||
@ -878,7 +876,7 @@ export type ContractGroupDefinition = {
|
||||
Order: string[]
|
||||
}
|
||||
|
||||
export interface EscalationInfo {
|
||||
export type EscalationInfo = {
|
||||
Type?: MissionType
|
||||
InGroup?: string
|
||||
NextContractId?: string
|
||||
@ -893,77 +891,76 @@ export interface EscalationInfo {
|
||||
export interface MissionManifestMetadata {
|
||||
Id: string
|
||||
Location: string
|
||||
IsPublished?: boolean
|
||||
CreationTimestamp?: string
|
||||
CreatorUserId?: string
|
||||
IsPublished?: boolean | null
|
||||
CreationTimestamp?: string | null
|
||||
CreatorUserId?: string | null
|
||||
Title: string
|
||||
Description?: string
|
||||
Description?: string | null
|
||||
BriefingVideo?:
|
||||
| string
|
||||
| {
|
||||
Mode: string
|
||||
VideoId: string
|
||||
}[]
|
||||
DebriefingVideo?: string
|
||||
DebriefingVideo?: string | null
|
||||
TileImage?:
|
||||
| string
|
||||
| {
|
||||
Mode: string
|
||||
Image: string
|
||||
}[]
|
||||
CodeName_Hint?: string
|
||||
CodeName_Hint?: string | null
|
||||
ScenePath: string
|
||||
Type: MissionType
|
||||
Release?: string | object
|
||||
RequiredUnlockable?: string
|
||||
Drops?: string[]
|
||||
Opportunities?: string[]
|
||||
OpportunityData?: MissionStory[]
|
||||
Entitlements: string[]
|
||||
LastUpdate?: string
|
||||
PublicId?: string
|
||||
GroupObjectiveDisplayOrder?: GroupObjectiveDisplayOrderItem[]
|
||||
GameVersion?: string
|
||||
ServerVersion?: string
|
||||
AllowNonTargetKills?: boolean
|
||||
Difficulty?: "pro1" | string
|
||||
CharacterSetup?: {
|
||||
Mode: "singleplayer" | "multiplayer" | string
|
||||
Characters: {
|
||||
Name: string
|
||||
Id: string
|
||||
MandatoryLoadout?: string[]
|
||||
}[]
|
||||
}[]
|
||||
CharacterLoadoutData?: {
|
||||
Id: string
|
||||
Loadout: unknown
|
||||
CompletionData: CompletionData
|
||||
}[]
|
||||
SpawnSelectionType?: "random" | string
|
||||
Gamemodes?: ("versus" | string)[]
|
||||
Enginemodes?: ("singleplayer" | "multiplayer" | string)[]
|
||||
Release?: string | object | null
|
||||
RequiredUnlockable?: string | null
|
||||
Drops?: string[] | null
|
||||
Opportunities?: string[] | null
|
||||
OpportunityData?: MissionStory[] | null
|
||||
Entitlements: string[] | null
|
||||
LastUpdate?: string | null
|
||||
PublicId?: string | null
|
||||
GroupObjectiveDisplayOrder?: GroupObjectiveDisplayOrderItem[] | null
|
||||
GameVersion?: string | null
|
||||
ServerVersion?: string | null
|
||||
AllowNonTargetKills?: boolean | null
|
||||
Difficulty?: "pro1" | string | null
|
||||
CharacterSetup?:
|
||||
| {
|
||||
Mode: "singleplayer" | "multiplayer" | string
|
||||
Characters: {
|
||||
Name: string
|
||||
Id: string
|
||||
MandatoryLoadout?: string[]
|
||||
}[]
|
||||
}[]
|
||||
| null
|
||||
CharacterLoadoutData?:
|
||||
| {
|
||||
Id: string
|
||||
Loadout: unknown
|
||||
CompletionData: CompletionData
|
||||
}[]
|
||||
| null
|
||||
SpawnSelectionType?: "random" | string | null
|
||||
Gamemodes?: ("versus" | string)[] | null
|
||||
Enginemodes?: ("singleplayer" | "multiplayer" | string)[] | null
|
||||
EndConditions?: {
|
||||
PointLimit?: number
|
||||
}
|
||||
Subtype?: string
|
||||
GroupTitle?: string
|
||||
TargetExpiration?: number
|
||||
TargetExpirationReduced?: number
|
||||
TargetLifeTime?: number
|
||||
NonTargetKillPenaltyEnabled?: boolean
|
||||
NoticedTargetStreakPenaltyMax?: number
|
||||
IsFeatured?: boolean
|
||||
} | null
|
||||
Subtype?: string | null
|
||||
GroupTitle?: string | null
|
||||
TargetExpiration?: number | null
|
||||
TargetExpirationReduced?: number | null
|
||||
TargetLifeTime?: number | null
|
||||
NonTargetKillPenaltyEnabled?: boolean | null
|
||||
NoticedTargetStreakPenaltyMax?: number | null
|
||||
IsFeatured?: boolean | null
|
||||
// Begin escalation-exclusive properties
|
||||
InGroup?: string
|
||||
NextContractId?: string
|
||||
GroupDefinition?: ContractGroupDefinition
|
||||
GroupData?: {
|
||||
Level: number
|
||||
TotalLevels: number
|
||||
Completed: boolean
|
||||
FirstContractId: string
|
||||
}
|
||||
InGroup?: string | null
|
||||
NextContractId?: string | null
|
||||
GroupDefinition?: ContractGroupDefinition | null
|
||||
GroupData?: EscalationInfo["GroupData"] | null
|
||||
// End escalation-exclusive properties
|
||||
/**
|
||||
* Useless property.
|
||||
@ -971,19 +968,19 @@ export interface MissionManifestMetadata {
|
||||
* @deprecated
|
||||
*/
|
||||
readonly UserData?: unknown | null
|
||||
IsVersus?: boolean
|
||||
IsEvergreenSafehouse?: boolean
|
||||
UseContractProgressionData?: boolean
|
||||
CpdId?: string
|
||||
IsVersus?: boolean | null
|
||||
IsEvergreenSafehouse?: boolean | null
|
||||
UseContractProgressionData?: boolean | null
|
||||
CpdId?: string | null
|
||||
/**
|
||||
* Custom property used for Elusives (like official's year)
|
||||
* and Escalations (if it's 0, it is a Peacock escalation,
|
||||
* and OriginalSeason will exist for filtering).
|
||||
*/
|
||||
Season?: number
|
||||
OriginalSeason?: number
|
||||
Season?: number | null
|
||||
OriginalSeason?: number | null
|
||||
// Used for sniper scoring
|
||||
Modules?: ManifestScoringModule[]
|
||||
Modules?: ManifestScoringModule[] | null
|
||||
}
|
||||
|
||||
export interface GroupObjectiveDisplayOrderItem {
|
||||
@ -1039,7 +1036,7 @@ export interface MissionManifest {
|
||||
EnableExits?: {
|
||||
$eq?: (string | boolean)[]
|
||||
}
|
||||
DevOnlyBricks?: string[]
|
||||
DevOnlyBricks?: string[] | null
|
||||
}
|
||||
Metadata: MissionManifestMetadata
|
||||
readonly UserData?: Record<string, never> | never[]
|
||||
@ -1216,7 +1213,7 @@ export interface CompiledChallengeTreeData {
|
||||
CategoryName: string
|
||||
ChallengeProgress?: ChallengeTreeWaterfallState
|
||||
Completed: boolean
|
||||
CompletionData: CompletionData
|
||||
CompletionData?: CompletionData
|
||||
Description: string
|
||||
// A string array of at most one element ("easy", "normal", or "hard").
|
||||
// If empty, then the challenge should appear in sessions on any difficulty.
|
||||
@ -1276,6 +1273,7 @@ export interface ChallengeProgressionData {
|
||||
ProfileId: string
|
||||
Completed: boolean
|
||||
Ticked: boolean
|
||||
ETag?: string
|
||||
State: Record<string, unknown>
|
||||
CompletedAt: Date | string | null
|
||||
MustBeSaved: boolean
|
||||
@ -1286,14 +1284,6 @@ export interface CompiledChallengeRuntimeData {
|
||||
Progression: ChallengeProgressionData
|
||||
}
|
||||
|
||||
export interface CompiledChallengeRewardData {
|
||||
ChallengeId: string
|
||||
ChallengeName: string
|
||||
ChallengeDescription: string
|
||||
ChallengeImageUrl: string
|
||||
XPGain: number
|
||||
}
|
||||
|
||||
export type LoadoutSavingMechanism = "PROFILES" | "LEGACY"
|
||||
export type ImageLoadingStrategy = "SAVEASREQUESTED" | "ONLINE" | "OFFLINE"
|
||||
|
||||
@ -1322,7 +1312,7 @@ export interface IHit {
|
||||
/**
|
||||
* A video object.
|
||||
*
|
||||
* @see ICampaignVideo
|
||||
* @see CampaignVideo
|
||||
* @see StoryData
|
||||
*/
|
||||
export interface IVideo {
|
||||
@ -1346,7 +1336,7 @@ export interface IVideo {
|
||||
*
|
||||
* @see IHit
|
||||
*/
|
||||
export type ICampaignMission = {
|
||||
export type CampaignMission = {
|
||||
Type: "Mission"
|
||||
Data: IHit
|
||||
}
|
||||
@ -1356,7 +1346,7 @@ export type ICampaignMission = {
|
||||
*
|
||||
* @see IVideo
|
||||
*/
|
||||
export type ICampaignVideo = {
|
||||
export type CampaignVideo = {
|
||||
Type: "Video"
|
||||
Data: IVideo
|
||||
}
|
||||
@ -1373,7 +1363,7 @@ export interface RegistryChallenge extends SavedChallenge {
|
||||
/**
|
||||
* An element for the game's story data.
|
||||
*/
|
||||
export type StoryData = ICampaignMission | ICampaignVideo
|
||||
export type StoryData = CampaignMission | CampaignVideo
|
||||
|
||||
/**
|
||||
* A campaign object.
|
||||
@ -1415,7 +1405,7 @@ export interface Loadout {
|
||||
*
|
||||
* @see LoadoutFile
|
||||
*/
|
||||
export interface LoadoutsGameVersion {
|
||||
export type LoadoutsGameVersion = {
|
||||
selected: string | null
|
||||
loadouts: Loadout[]
|
||||
}
|
||||
@ -1423,19 +1413,20 @@ export interface LoadoutsGameVersion {
|
||||
/**
|
||||
* The top-level format for the loadout profiles storage file.
|
||||
*/
|
||||
export interface LoadoutFile {
|
||||
h1: LoadoutsGameVersion
|
||||
h2: LoadoutsGameVersion
|
||||
h3: LoadoutsGameVersion
|
||||
}
|
||||
export type LoadoutFile = Record<
|
||||
// game version but not scpc
|
||||
Exclude<GameVersion, "scpc">,
|
||||
LoadoutsGameVersion
|
||||
>
|
||||
|
||||
/**
|
||||
* A function that generates a campaign mission object for use in the campaigns menu.
|
||||
* Will throw if contract is not found.
|
||||
*/
|
||||
export type GenSingleMissionFunc = (
|
||||
contractId: string,
|
||||
gameVersion: GameVersion,
|
||||
) => ICampaignMission
|
||||
) => CampaignMission
|
||||
|
||||
/**
|
||||
* A function that generates a campaign video object for use in the campaigns menu.
|
||||
@ -1443,7 +1434,7 @@ export type GenSingleMissionFunc = (
|
||||
export type GenSingleVideoFunc = (
|
||||
videoId: string,
|
||||
gameVersion: GameVersion,
|
||||
) => ICampaignVideo
|
||||
) => CampaignVideo
|
||||
|
||||
/**
|
||||
* A "hits category" is used to display lists of contracts in-game.
|
||||
@ -1486,16 +1477,25 @@ export interface PlayNextGetCampaignsHookReturn {
|
||||
|
||||
export type SafehouseCategory = {
|
||||
Category: string
|
||||
SubCategories: SafehouseCategory[]
|
||||
SubCategories: SafehouseCategory[] | null
|
||||
IsLeaf: boolean
|
||||
Data: null
|
||||
}
|
||||
|
||||
export type SniperLoadout = {
|
||||
ID: string
|
||||
InstanceID: string
|
||||
Unlockable: Unlockable[]
|
||||
MainUnlockable: Unlockable
|
||||
Data: null | {
|
||||
Type: string
|
||||
SubType: string | undefined
|
||||
Items: {
|
||||
Item: InventoryItem
|
||||
ItemDetails: {
|
||||
Capabilities: []
|
||||
StatList: {
|
||||
Name: string
|
||||
Ratio: unknown
|
||||
PropertyTexts: []
|
||||
}[]
|
||||
}
|
||||
}[]
|
||||
Page: number
|
||||
HasMore: boolean
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -28,7 +28,7 @@ import type {
|
||||
Unlockable,
|
||||
UserProfile,
|
||||
} from "./types/types"
|
||||
import axios, { AxiosError } from "axios"
|
||||
import { AxiosError } from "axios"
|
||||
import { log, LogLevel } from "./loggingInterop"
|
||||
import { writeFileSync } from "fs"
|
||||
import { getFlag } from "./flags"
|
||||
@ -54,7 +54,7 @@ export const uuidRegex =
|
||||
|
||||
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"
|
||||
|
||||
@ -71,10 +71,10 @@ export async function checkForUpdates(): Promise<void> {
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await axios(
|
||||
const res = await fetch(
|
||||
"https://backend.rdil.rocks/peacock/latest-version",
|
||||
)
|
||||
const current = res.data
|
||||
const current = parseInt(await res.text(), 10)
|
||||
|
||||
if (PEACOCKVER < 0 && current < -PEACOCKVER) {
|
||||
log(
|
||||
@ -94,12 +94,15 @@ export async function checkForUpdates(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
export function getRemoteService(gameVersion: GameVersion): string {
|
||||
return gameVersion === "h3"
|
||||
? "hm3-service"
|
||||
: gameVersion === "h2"
|
||||
? "pc2-service"
|
||||
: "pc-service"
|
||||
export function getRemoteService(gameVersion: GameVersion): string | undefined {
|
||||
switch (gameVersion) {
|
||||
case "h3":
|
||||
return "hm3-service"
|
||||
case "h2":
|
||||
return "pc2-service"
|
||||
default:
|
||||
return "pc-service"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -306,6 +309,7 @@ function updateUserProfile(
|
||||
|
||||
if (gameVersion === "h1") {
|
||||
// No sniper locations, but we add normal and pro1
|
||||
// @ts-expect-error I know what I'm doing.
|
||||
obj[newKey] = {
|
||||
// Data from previous profiles only contains normal and is the default.
|
||||
normal: {
|
||||
@ -321,8 +325,10 @@ function updateUserProfile(
|
||||
}
|
||||
} else {
|
||||
// We need to update sniper locations.
|
||||
// @ts-expect-error I know what I'm doing.
|
||||
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] = {
|
||||
Xp: 0,
|
||||
Level: 1,
|
||||
@ -344,7 +350,7 @@ function updateUserProfile(
|
||||
{},
|
||||
)
|
||||
|
||||
// ts-expect-error Legacy property.
|
||||
// @ts-expect-error Legacy property.
|
||||
delete profile.Extensions.progression["Unlockables"]
|
||||
|
||||
profile.Version = 1
|
||||
@ -426,12 +432,7 @@ export function castUserProfile(
|
||||
|
||||
// Fix Extensions.gamepersistentdata.HitsFilterType.
|
||||
// None of the old profiles should have "MyPlaylist".
|
||||
if (
|
||||
!Object.hasOwn(
|
||||
j.Extensions.gamepersistentdata.HitsFilterType,
|
||||
"MyPlaylist",
|
||||
)
|
||||
) {
|
||||
if (j.Extensions.gamepersistentdata.HitsFilterType["MyPlaylist"]) {
|
||||
j.Extensions.gamepersistentdata.HitsFilterType = {
|
||||
MyHistory: "all",
|
||||
MyContracts: "all",
|
||||
@ -520,15 +521,17 @@ export const defaultSuits = {
|
||||
* @returns The default suits that are attainable via challenges or mastery.
|
||||
*/
|
||||
export function attainableDefaults(gameVersion: GameVersion): string[] {
|
||||
return gameVersion === "h1"
|
||||
? []
|
||||
: gameVersion === "h2"
|
||||
? ["TOKEN_OUTFIT_WET_SUIT"]
|
||||
: [
|
||||
"TOKEN_OUTFIT_GREENLAND_HERO_TRAININGSUIT",
|
||||
"TOKEN_OUTFIT_WET_SUIT",
|
||||
"TOKEN_OUTFIT_HERO_DUGONG_SUIT",
|
||||
]
|
||||
if (gameVersion === "h1") {
|
||||
return []
|
||||
} else if (gameVersion === "h2") {
|
||||
return ["TOKEN_OUTFIT_WET_SUIT"]
|
||||
} else {
|
||||
return [
|
||||
"TOKEN_OUTFIT_GREENLAND_HERO_TRAININGSUIT",
|
||||
"TOKEN_OUTFIT_WET_SUIT",
|
||||
"TOKEN_OUTFIT_HERO_DUGONG_SUIT",
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -538,10 +541,11 @@ export function attainableDefaults(gameVersion: GameVersion): string[] {
|
||||
* @param subLocation The sub-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 (
|
||||
defaultSuits[subLocation.Id] ||
|
||||
defaultSuits[subLocation.Properties.ParentLocation] ||
|
||||
defaultSuits[subLocation.Id as Cast] ||
|
||||
defaultSuits[subLocation.Properties.ParentLocation as Cast] ||
|
||||
"TOKEN_OUTFIT_HITMANSUIT"
|
||||
)
|
||||
}
|
||||
|
@ -16,7 +16,7 @@
|
||||
* 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 { readFileSync } from "atomically"
|
||||
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 {
|
||||
res.json({
|
||||
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))
|
||||
})
|
||||
|
||||
webFeaturesRouter.get(
|
||||
"/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 {
|
||||
webFeaturesRouter.get("/local-users", (req: CommonRequest, res) => {
|
||||
if (!req.query.gv || !versions.includes(req.query.gv ?? null)) {
|
||||
formErrorMessage(
|
||||
res,
|
||||
'The request must contain a valid game version among "h1", "h2", and "h3".',
|
||||
)
|
||||
return false
|
||||
res.json([])
|
||||
return
|
||||
}
|
||||
|
||||
if (!req.query.user || !uuidRegex.test(req.query.user)) {
|
||||
formErrorMessage(res, "The request must contain the uuid of a user.")
|
||||
return false
|
||||
let dir
|
||||
|
||||
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(
|
||||
"/modify",
|
||||
async (
|
||||
req: Request<
|
||||
unknown,
|
||||
unknown,
|
||||
unknown,
|
||||
{ gv: GameVersion; user: string; level: string; id: string }
|
||||
>,
|
||||
res,
|
||||
) => {
|
||||
if (!validateUserAndGv(req, res)) {
|
||||
return
|
||||
}
|
||||
|
||||
commonValidationMiddleware,
|
||||
async (req: CommonRequest<{ level: string; id: string }>, res) => {
|
||||
if (!req.query.level) {
|
||||
formErrorMessage(
|
||||
res,
|
||||
@ -158,7 +158,7 @@ webFeaturesRouter.get(
|
||||
|
||||
const mapping = controller.escalationMappings.get(req.query.id)
|
||||
|
||||
if (mapping === undefined) {
|
||||
if (!mapping) {
|
||||
formErrorMessage(res, "Unknown escalation.")
|
||||
return
|
||||
}
|
||||
@ -198,19 +198,8 @@ webFeaturesRouter.get(
|
||||
|
||||
webFeaturesRouter.get(
|
||||
"/user-progress",
|
||||
async (
|
||||
req: Request<
|
||||
unknown,
|
||||
unknown,
|
||||
unknown,
|
||||
{ gv: GameVersion; user: string }
|
||||
>,
|
||||
res,
|
||||
) => {
|
||||
if (!validateUserAndGv(req, res)) {
|
||||
return
|
||||
}
|
||||
|
||||
commonValidationMiddleware,
|
||||
async (req: CommonRequest, res) => {
|
||||
try {
|
||||
await loadUserData(req.query.user, req.query.gv)
|
||||
} catch (e) {
|
||||
|
27
package.json
27
package.json
@ -19,7 +19,8 @@
|
||||
"webui": "yarn workspace @peacockproject/web-ui",
|
||||
"typedefs": "yarn workspace @peacockproject/core",
|
||||
"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": {
|
||||
"semi": false,
|
||||
@ -27,7 +28,7 @@
|
||||
"trailingComma": "all"
|
||||
},
|
||||
"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",
|
||||
"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",
|
||||
@ -40,18 +41,18 @@
|
||||
"atomically": "^2.0.2",
|
||||
"axios": "^1.6.0",
|
||||
"body-parser": "*",
|
||||
"clipanion": "^3.2.1",
|
||||
"clipanion": "^4.0.0-rc.3",
|
||||
"commander": "^11.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",
|
||||
"jest-diff": "^29.7.0",
|
||||
"js-ini": "^1.6.0",
|
||||
"json5": "^2.2.3",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"md5-file": "^5.0.0",
|
||||
"msgpackr": "^1.9.9",
|
||||
"nanoid": "^5.0.3",
|
||||
"msgpackr": "^1.10.1",
|
||||
"nanoid": "^5.0.4",
|
||||
"parseurl": "^1.3.3",
|
||||
"picocolors": "patch:picocolors@npm%3A1.0.0#~/.yarn/patches/picocolors-npm-1.0.0-d81e0b1927.patch",
|
||||
"progress": "^2.0.3",
|
||||
@ -71,12 +72,12 @@
|
||||
"@types/progress": "^2.0.6",
|
||||
"@types/prompts": "^2.4.7",
|
||||
"@types/send": "^0.17.3",
|
||||
"@typescript-eslint/eslint-plugin": "^6.10.0",
|
||||
"@typescript-eslint/parser": "^6.10.0",
|
||||
"esbuild": "^0.19.5",
|
||||
"@typescript-eslint/eslint-plugin": "^6.19.1",
|
||||
"@typescript-eslint/parser": "^6.19.1",
|
||||
"esbuild": "^0.19.12",
|
||||
"esbuild-register": "^3.5.0",
|
||||
"eslint": "^8.53.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"fast-glob": "^3.3.2",
|
||||
@ -84,8 +85,8 @@
|
||||
"ms": "^2.1.3",
|
||||
"prettier": "^2.8.8",
|
||||
"rimraf": "^5.0.5",
|
||||
"terser": "^5.21.0",
|
||||
"typescript": "5.2.2",
|
||||
"terser": "^5.27.0",
|
||||
"typescript": "5.3.3",
|
||||
"winston": "^3.11.0",
|
||||
"winston-daily-rotate-file": "^4.7.1"
|
||||
},
|
||||
|
@ -74,6 +74,7 @@ await e.build({
|
||||
"process.env.ZEIT_BITBUCKET_COMMIT_SHA": "undefined",
|
||||
"process.env.VERCEL_GIT_COMMIT_SHA": "undefined",
|
||||
"process.env.ZEIT_GITLAB_COMMIT_SHA": "undefined",
|
||||
"process.env.MSGPACKR_NATIVE_ACCELERATION_DISABLED": "true",
|
||||
},
|
||||
sourcemap: "external",
|
||||
plugins: [
|
||||
|
@ -23,12 +23,12 @@
|
||||
"dependencies": {
|
||||
"@peacockproject/statemachine-parser": "^5.9.3",
|
||||
"@types/express": "^4.17.20",
|
||||
"@types/node": "^20.8.10",
|
||||
"@types/node": "*",
|
||||
"atomically": "^2.0.2",
|
||||
"axios": "^1.6.0",
|
||||
"axios": "^1.6.7",
|
||||
"js-ini": "^1.6.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"msgpackr": "^1.9.9"
|
||||
"msgpackr": "^1.10.1"
|
||||
}
|
||||
}
|
||||
|
@ -1,245 +1,242 @@
|
||||
{
|
||||
"template": null,
|
||||
"data": {
|
||||
"SubLocationData": [],
|
||||
"PlayerProfileXp": {
|
||||
"Total": 0,
|
||||
"Level": 1,
|
||||
"Seasons": [
|
||||
{
|
||||
"Number": 1,
|
||||
"Locations": [
|
||||
{
|
||||
"LocationId": "LOCATION_PARENT_ICA_FACILITY",
|
||||
"Xp": 0,
|
||||
"ActionXp": 0
|
||||
},
|
||||
{
|
||||
"LocationId": "LOCATION_PARENT_PARIS",
|
||||
"Xp": 0,
|
||||
"ActionXp": 0,
|
||||
"LocationProgression": {
|
||||
"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
|
||||
}
|
||||
"SubLocationData": [],
|
||||
"PlayerProfileXp": {
|
||||
"Total": 0,
|
||||
"Level": 1,
|
||||
"Seasons": [
|
||||
{
|
||||
"Number": 1,
|
||||
"Locations": [
|
||||
{
|
||||
"LocationId": "LOCATION_PARENT_ICA_FACILITY",
|
||||
"Xp": 0,
|
||||
"ActionXp": 0
|
||||
},
|
||||
{
|
||||
"LocationId": "LOCATION_PARENT_PARIS",
|
||||
"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
|
||||
},
|
||||
{
|
||||
"LocationId": "LOCATION_PARENT_COASTALTOWN",
|
||||
"Xp": 0,
|
||||
"ActionXp": 0,
|
||||
"LocationProgression": {
|
||||
"Level": 1,
|
||||
"MaxLevel": 20
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
}
|
||||
},
|
||||
{
|
||||
"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
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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
|
||||
}
|
||||
|
||||
export function mockRequestWithJwt(): RequestWithJwt<core.Query, any> {
|
||||
const mockedRequest = <RequestWithJwt<core.Query, any>>{
|
||||
export function mockRequestWithJwt<
|
||||
QS = core.Query,
|
||||
Body = any,
|
||||
>(): RequestWithJwt<QS, Body> {
|
||||
const mockedRequest = <RequestWithJwt<QS, Body>>{
|
||||
headers: {},
|
||||
header: (name: string) =>
|
||||
mockedRequest.headers[name.toLowerCase()] as string,
|
||||
@ -36,10 +39,10 @@ export function mockRequestWithJwt(): RequestWithJwt<core.Query, any> {
|
||||
return mockedRequest
|
||||
}
|
||||
|
||||
export function mockRequestWithValidJwt(
|
||||
export function mockRequestWithValidJwt<QS = core.Query, Body = any>(
|
||||
pId: string,
|
||||
): RequestWithJwt<core.Query, any> {
|
||||
const mockedRequest = mockRequestWithJwt()
|
||||
): RequestWithJwt<QS, Body> {
|
||||
const mockedRequest = mockRequestWithJwt<QS, Body>()
|
||||
|
||||
const jwtToken = sign(
|
||||
{
|
||||
@ -64,15 +67,20 @@ export function mockResponse(): core.Response {
|
||||
return response
|
||||
}
|
||||
|
||||
// @ts-expect-error It works.
|
||||
response.status = vi.fn().mockImplementation(mockImplementation)
|
||||
// @ts-expect-error It works.
|
||||
response.json = vi.fn()
|
||||
// @ts-expect-error It works.
|
||||
response.end = vi.fn()
|
||||
|
||||
// @ts-expect-error It works.
|
||||
return <core.Response>response
|
||||
}
|
||||
|
||||
export function getResolvingPromise<T>(value?: T): Promise<T> {
|
||||
return new Promise((resolve) => {
|
||||
// @ts-expect-error It works.
|
||||
resolve(value)
|
||||
})
|
||||
}
|
||||
|
@ -18,12 +18,15 @@
|
||||
|
||||
import * as configSwizzleManager from "../../components/configSwizzleManager"
|
||||
import { readFileSync } from "fs"
|
||||
import { vi } from "vitest"
|
||||
|
||||
const originalFilePaths: Record<string, string> = {}
|
||||
|
||||
Object.keys(configSwizzleManager.configs).forEach((config: string) => {
|
||||
// @ts-expect-error It works.
|
||||
originalFilePaths[config] = <string>configSwizzleManager.configs[config]
|
||||
|
||||
// @ts-expect-error It works.
|
||||
configSwizzleManager.configs[config] = undefined
|
||||
})
|
||||
|
||||
@ -34,20 +37,24 @@ export function loadConfig(config: string) {
|
||||
|
||||
const contents = readFileSync(originalFilePaths[config], "utf-8")
|
||||
|
||||
// @ts-expect-error It works.
|
||||
configSwizzleManager.configs[config] = JSON.parse(contents)
|
||||
}
|
||||
|
||||
export function setConfig(config: string, data: unknown) {
|
||||
// @ts-expect-error It works.
|
||||
configSwizzleManager.configs[config] = data
|
||||
}
|
||||
|
||||
const getConfigOriginal = configSwizzleManager.getConfig
|
||||
vi.spyOn(configSwizzleManager, "getConfig").mockImplementation(
|
||||
(config: string, clone: boolean) => {
|
||||
// @ts-expect-error It works.
|
||||
if (!configSwizzleManager.configs[config]) {
|
||||
throw `Config '${config}' has not been loaded!`
|
||||
}
|
||||
|
||||
// @ts-expect-error It works.
|
||||
return getConfigOriginal(config, clone)
|
||||
},
|
||||
)
|
||||
|
@ -3,12 +3,13 @@
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@vitest/ui": "^0.34.6",
|
||||
"@vitest/ui": "^1.2.2",
|
||||
"vite": "^5.0.12",
|
||||
"vitest": "^0.34.6"
|
||||
"vitest": "^1.2.2"
|
||||
},
|
||||
"scripts": {
|
||||
"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 */
|
||||
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 * as databaseHandler from "../../components/databaseHandler"
|
||||
import * as platformEntitlements from "../../components/platformEntitlements"
|
||||
@ -26,11 +31,9 @@ import axios from "axios"
|
||||
import { describe, expect, beforeEach, vi, it } from "vitest"
|
||||
|
||||
import {
|
||||
getMockCallArgument,
|
||||
getResolvingPromise,
|
||||
mockRequestWithJwt,
|
||||
mockRequestWithValidJwt,
|
||||
mockResponse,
|
||||
} from "../helpers/testHelpers"
|
||||
|
||||
describe("oauthToken", () => {
|
||||
@ -41,6 +44,7 @@ describe("oauthToken", () => {
|
||||
.mockResolvedValue("")
|
||||
const loadUserData = vi
|
||||
.spyOn(databaseHandler, "loadUserData")
|
||||
// @ts-expect-error This is okay.
|
||||
.mockResolvedValue(undefined)
|
||||
const getUserData = vi
|
||||
.spyOn(databaseHandler, "getUserData")
|
||||
@ -65,10 +69,10 @@ describe("oauthToken", () => {
|
||||
})
|
||||
}
|
||||
|
||||
return undefined
|
||||
return getResolvingPromise({})
|
||||
})
|
||||
|
||||
const request = mockRequestWithJwt()
|
||||
const request = mockRequestWithJwt<never, any>()
|
||||
request.body = {
|
||||
grant_type: "external_steam",
|
||||
steam_userid: "000000000047",
|
||||
@ -76,9 +80,7 @@ describe("oauthToken", () => {
|
||||
pId: pId,
|
||||
}
|
||||
|
||||
const response = mockResponse()
|
||||
|
||||
await handleOauthToken(request, response)
|
||||
const res = await handleOAuthToken(request)
|
||||
|
||||
expect(getExternalUserData).toHaveBeenCalledWith(
|
||||
"000000000047",
|
||||
@ -88,12 +90,15 @@ describe("oauthToken", () => {
|
||||
expect(loadUserData).toHaveBeenCalledWith(pId, "h3")
|
||||
expect(getUserData).toHaveBeenCalledWith(pId, "h3")
|
||||
|
||||
const jsonResponse = getMockCallArgument<any>(response.json, 0, 0)
|
||||
const accessToken = verify(jsonResponse.access_token, JWT_SECRET, {
|
||||
complete: true,
|
||||
})
|
||||
const accessToken = verify(
|
||||
(res as OAuthTokenResponse).access_token,
|
||||
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)
|
||||
})
|
||||
|
||||
@ -102,7 +107,7 @@ describe("oauthToken", () => {
|
||||
["mock"],
|
||||
)
|
||||
|
||||
const request = mockRequestWithJwt()
|
||||
const request = mockRequestWithJwt<never, any>()
|
||||
request.body = {
|
||||
grant_type: "external_epic",
|
||||
epic_userid: "0123456789abcdef0123456789abcdef",
|
||||
@ -118,9 +123,7 @@ describe("oauthToken", () => {
|
||||
pId: pId,
|
||||
}
|
||||
|
||||
const response = mockResponse()
|
||||
|
||||
await handleOauthToken(request, response)
|
||||
const res = await handleOAuthToken(request)
|
||||
|
||||
expect(getExternalUserData).toHaveBeenCalledWith(
|
||||
"0123456789abcdef0123456789abcdef",
|
||||
@ -130,85 +133,84 @@ describe("oauthToken", () => {
|
||||
expect(loadUserData).toHaveBeenCalledWith(pId, "h3")
|
||||
expect(getUserData).toHaveBeenCalledWith(pId, "h3")
|
||||
|
||||
const jsonResponse = getMockCallArgument<any>(response.json, 0, 0)
|
||||
const accessToken = verify(jsonResponse.access_token, JWT_SECRET, {
|
||||
complete: true,
|
||||
})
|
||||
const accessToken = verify(
|
||||
(res as OAuthTokenResponse).access_token,
|
||||
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)
|
||||
})
|
||||
|
||||
it("refresh_token - missing auth header", async () => {
|
||||
const request = mockRequestWithJwt()
|
||||
const request = mockRequestWithJwt<never, any>()
|
||||
|
||||
request.body = {
|
||||
grant_type: "refresh_token",
|
||||
}
|
||||
|
||||
const respose = mockResponse()
|
||||
|
||||
let error: Error = undefined
|
||||
let error: Error | undefined = undefined
|
||||
|
||||
try {
|
||||
await handleOauthToken(request, respose)
|
||||
await handleOAuthToken(request)
|
||||
} catch (e) {
|
||||
error = e
|
||||
error = e as Error
|
||||
}
|
||||
|
||||
expect(error).toBeInstanceOf(TypeError)
|
||||
})
|
||||
|
||||
it("refresh_token - invalid auth header", async () => {
|
||||
const request = mockRequestWithJwt()
|
||||
const request = mockRequestWithJwt<never, any>()
|
||||
request.headers.authorization = "Bearer invalid"
|
||||
|
||||
request.body = {
|
||||
grant_type: "refresh_token",
|
||||
}
|
||||
|
||||
const respose = mockResponse()
|
||||
|
||||
let error: Error = undefined
|
||||
let error: Error | undefined = undefined
|
||||
|
||||
try {
|
||||
await handleOauthToken(request, respose)
|
||||
await handleOAuthToken(request)
|
||||
} catch (e) {
|
||||
error = e
|
||||
error = e as Error
|
||||
}
|
||||
|
||||
expect(error).toBeInstanceOf(TypeError)
|
||||
})
|
||||
|
||||
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
|
||||
request.body = {
|
||||
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)
|
||||
const accessToken = verify(jsonResponse.access_token, 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)
|
||||
})
|
||||
|
||||
it("no grant_type", async () => {
|
||||
const request = mockRequestWithJwt()
|
||||
const request = mockRequestWithJwt<never, any>()
|
||||
request.body = {}
|
||||
request.query = {} as never
|
||||
|
||||
const respose = mockResponse()
|
||||
const res = await handleOAuthToken(request)
|
||||
|
||||
await handleOauthToken(request, respose)
|
||||
|
||||
expect(respose.status).toHaveBeenCalledWith(406)
|
||||
expect(res).toEqual(error406)
|
||||
})
|
||||
})
|
||||
|
@ -4,7 +4,8 @@
|
||||
// Reset rootDir to default to make rootDirs take effect
|
||||
"rootDir": null,
|
||||
"rootDirs": ["../components", "."],
|
||||
"types": ["vitest/globals"]
|
||||
"noEmit": true,
|
||||
"emitDeclarationOnly": false
|
||||
},
|
||||
"include": ["../components", "**/*.ts"],
|
||||
"exclude": []
|
||||
|
@ -6,7 +6,7 @@
|
||||
"lib": ["ESNext"],
|
||||
"emitDeclarationOnly": true,
|
||||
"checkJs": false,
|
||||
"strict": false,
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitOverride": true,
|
||||
@ -21,7 +21,8 @@
|
||||
"rootDir": "components",
|
||||
"outDir": "./build",
|
||||
"isolatedModules": true,
|
||||
"stripInternal": true
|
||||
"stripInternal": true,
|
||||
"strictNullChecks": true
|
||||
},
|
||||
"include": ["components"],
|
||||
"exclude": [
|
||||
|
@ -4,7 +4,7 @@
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"axios": "^1.6.0",
|
||||
"clsx": "^2.0.0",
|
||||
"clsx": "^2.1.0",
|
||||
"immer": "^10.0.3",
|
||||
"infima": "0.2.0-alpha.38",
|
||||
"json-keys-sort": "^2.1.0",
|
||||
@ -20,10 +20,10 @@
|
||||
"typecheck-ws": "tsc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.36",
|
||||
"@types/react-dom": "^18.2.14",
|
||||
"@types/react": "^18.2.48",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"rollup-plugin-license": "^3.2.0",
|
||||
"typescript": "5.2.2",
|
||||
"typescript": "5.3.3",
|
||||
"vite": "^5.0.12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
Loading…
Reference in New Issue
Block a user