1
mirror of https://github.com/thepeacockproject/Peacock synced 2025-02-16 16:34:28 +01:00

Enable strict types mode ()

Signed-off-by: Reece Dunham <me@rdil.rocks>
This commit is contained in:
Reece Dunham 2024-02-02 14:44:21 -05:00
parent 0585b35447
commit 5cc69434c6
No known key found for this signature in database
GPG Key ID: F087CB485320F19F
71 changed files with 3454 additions and 2960 deletions

View File

@ -44,11 +44,13 @@ module.exports = {
], ],
}, },
rules: { rules: {
"@typescript-eslint/prefer-optional-chain": "warn",
"@typescript-eslint/explicit-module-boundary-types": "off", "@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-extra-semi": "off", "@typescript-eslint/no-extra-semi": "off",
"@typescript-eslint/no-non-null-assertion": "off", "@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/require-await": "warn", "@typescript-eslint/require-await": "warn",
"@typescript-eslint/prefer-ts-expect-error": "error", "@typescript-eslint/prefer-ts-expect-error": "error",
"no-nested-ternary": "warn",
eqeqeq: "error", eqeqeq: "error",
"no-duplicate-imports": "warn", "no-duplicate-imports": "warn",
"promise/always-return": "error", "promise/always-return": "error",

View File

@ -33,6 +33,7 @@ const legacyContractRouter = Router()
legacyContractRouter.post( legacyContractRouter.post(
"/GetForPlay", "/GetForPlay",
jsonMiddleware(), jsonMiddleware(),
// @ts-expect-error Has jwt props.
(req: RequestWithJwt, res) => { (req: RequestWithJwt, res) => {
if (!uuidRegex.test(req.body.id)) { if (!uuidRegex.test(req.body.id)) {
res.status(400).end() res.status(400).end()
@ -130,6 +131,7 @@ legacyContractRouter.post(
legacyContractRouter.post( legacyContractRouter.post(
"/Start", "/Start",
jsonMiddleware(), jsonMiddleware(),
// @ts-expect-error Has jwt props.
(req: RequestWithJwt, res) => { (req: RequestWithJwt, res) => {
if (req.body.profileId !== req.jwt.unique_name) { if (req.body.profileId !== req.jwt.unique_name) {
res.status(400).end() // requested for different user id res.status(400).end() // requested for different user id

View File

@ -24,37 +24,9 @@ import { getParentLocationByName } from "../contracts/dataGen"
const legacyMenuDataRouter = Router() const legacyMenuDataRouter = Router()
legacyMenuDataRouter.get(
"/debriefingchallenges",
(
req: RequestWithJwt<{ contractSessionId: string; contractId: string }>,
res,
) => {
if (typeof req.query.contractId !== "string") {
res.status(400).send("invalid contractId")
return
}
// debriefingchallenges?contractSessionId=00000000000000-00000000-0000-0000-0000-000000000001&contractId=dd906289-7c32-427f-b689-98ae645b407f
res.json({
template: getConfig("LegacyDebriefingChallengesTemplate", false),
data: {
ChallengeData: {
// FIXME: This may not work correctly; I don't know the actual format so I'm assuming challenge tree
Children:
controller.challengeService.getChallengeTreeForContract(
req.query.contractId,
req.gameVersion,
req.jwt.unique_name,
),
},
},
})
},
)
legacyMenuDataRouter.get( legacyMenuDataRouter.get(
"/MasteryLocation", "/MasteryLocation",
// @ts-expect-error Has jwt props.
(req: RequestWithJwt<{ locationId: string; difficulty: string }>, res) => { (req: RequestWithJwt<{ locationId: string; difficulty: string }>, res) => {
const masteryData = const masteryData =
controller.masteryService.getMasteryDataForDestination( controller.masteryService.getMasteryDataForDestination(

View File

@ -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 }

View File

@ -38,6 +38,7 @@ const legacyProfileRouter = Router()
legacyProfileRouter.post( legacyProfileRouter.post(
"/ChallengesService/GetActiveChallenges", "/ChallengesService/GetActiveChallenges",
jsonMiddleware(), jsonMiddleware(),
// @ts-expect-error Has jwt props.
(req: RequestWithJwt, res) => { (req: RequestWithJwt, res) => {
if (!uuidRegex.test(req.body.contractId)) { if (!uuidRegex.test(req.body.contractId)) {
return res.status(404).send("invalid contract") return res.status(404).send("invalid contract")
@ -93,7 +94,13 @@ legacyProfileRouter.post(
legacyProfileRouter.post( legacyProfileRouter.post(
"/ChallengesService/GetProgression", "/ChallengesService/GetProgression",
jsonMiddleware(), jsonMiddleware(),
// @ts-expect-error Has jwt props.
(req: RequestWithJwt<never, LegacyGetProgressionBody>, res) => { (req: RequestWithJwt<never, LegacyGetProgressionBody>, res) => {
if (!Array.isArray(req.body.challengeids)) {
res.status(400).send("invalid body")
return
}
const legacyGlobalChallenges = getConfig<CompiledChallengeIngameData[]>( const legacyGlobalChallenges = getConfig<CompiledChallengeIngameData[]>(
"LegacyGlobalChallenges", "LegacyGlobalChallenges",
false, false,
@ -114,10 +121,11 @@ legacyProfileRouter.post(
MustBeSaved: false, MustBeSaved: false,
})) }))
/*
for (const challengeId of req.body.challengeids) { for (const challengeId of req.body.challengeids) {
const challenge = const challenge = controller.challengeService.getChallengeById(
controller.challengeService.getChallengeById(challengeId) challengeId,
"h1",
)
if (!challenge) { if (!challenge) {
log( log(
@ -128,7 +136,7 @@ legacyProfileRouter.post(
} }
const progression = const progression =
controller.challengeService.getChallengeProgression( controller.challengeService.getPersistentChallengeProgression(
req.jwt.unique_name, req.jwt.unique_name,
challengeId, challengeId,
req.gameVersion, req.gameVersion,
@ -138,19 +146,16 @@ legacyProfileRouter.post(
ChallengeId: challengeId, ChallengeId: challengeId,
ProfileId: req.jwt.unique_name, ProfileId: req.jwt.unique_name,
Completed: progression.Completed, Completed: progression.Completed,
Ticked: progression.Ticked,
State: progression.State, State: progression.State,
ETag: `W/"datetime'${encodeURIComponent( ETag: `W/"datetime'${encodeURIComponent(
new Date().toISOString(), new Date().toISOString(),
)}'"`, )}'"`,
CompletedAt: progression.CompletedAt, CompletedAt: progression.CompletedAt,
MustBeSaved: false, MustBeSaved: progression.MustBeSaved,
}) })
} }
*/ // TODO: HELP! Please DM rdil if you see this
// TODO: atampy broke this - please fix
// update(RD) nov 18 '22: fixed but still missing challenges in
// 2016 engine (e.g. showstopper is missing 9, 5 of which are the
// classics I think, not sure about the other 4)
res.json(challenges) res.json(challenges)
}, },

View File

@ -18,7 +18,6 @@
import { import {
ChallengeProgressionData, ChallengeProgressionData,
CompiledChallengeRewardData,
CompiledChallengeRuntimeData, CompiledChallengeRuntimeData,
InclusionData, InclusionData,
MissionManifest, MissionManifest,
@ -28,19 +27,6 @@ import { SavedChallengeGroup } from "../types/challenges"
import { controller } from "../controller" import { controller } from "../controller"
import { gameDifficulty, isSniperLocation } from "../utils" import { gameDifficulty, isSniperLocation } from "../utils"
// TODO: unused?
export function compileScoringChallenge(
challenge: RegistryChallenge,
): CompiledChallengeRewardData {
return {
ChallengeId: challenge.Id,
ChallengeName: challenge.Name,
ChallengeDescription: challenge.Description,
ChallengeImageUrl: challenge.ImageName,
XPGain: challenge.Rewards?.MasteryXP || 0,
}
}
export function compileRuntimeChallenge( export function compileRuntimeChallenge(
challenge: RegistryChallenge, challenge: RegistryChallenge,
progression: ChallengeProgressionData, progression: ChallengeProgressionData,
@ -106,8 +92,8 @@ export type ChallengeFilterOptions =
* @returns A boolean as the result. * @returns A boolean as the result.
*/ */
export function inclusionDataCheck( export function inclusionDataCheck(
incData: InclusionData, incData: InclusionData | undefined,
contract: MissionManifest, contract: MissionManifest | undefined,
): boolean { ): boolean {
if (!incData) return true if (!incData) return true
if (!contract) return false if (!contract) return false
@ -174,9 +160,9 @@ function isChallengeInContract(
: { : {
...challenge.InclusionData, ...challenge.InclusionData,
ContractTypes: ContractTypes:
challenge.InclusionData.ContractTypes.filter( challenge.InclusionData?.ContractTypes?.filter(
(type) => type !== "tutorial", (type) => type !== "tutorial",
), ) || [],
}, },
contract, contract,
) )
@ -184,14 +170,15 @@ function isChallengeInContract(
// Is this for the current contract or group contract? // Is this for the current contract or group contract?
const isForContract = (challenge.InclusionData?.ContractIds || []).includes( const isForContract = (challenge.InclusionData?.ContractIds || []).includes(
contract.Metadata.Id, contract?.Metadata.Id || "",
) )
// Is this for the current contract type? // Is this for the current contract type?
// As of v6.1.0, this is only used for ET challenges. // As of v6.1.0, this is only used for ET challenges.
// We have to resolve the non-group contract, `contract` is the group contract
const isForContractType = ( const isForContractType = (
challenge.InclusionData?.ContractTypes || [] challenge.InclusionData?.ContractTypes || []
).includes(controller.resolveContract(contractId).Metadata.Type) ).includes(controller.resolveContract(contractId)!.Metadata.Type)
// Is this a location-wide challenge? // Is this a location-wide challenge?
// "location" is more widely used, but "parentlocation" is used in Ambrose and Berlin, as well as some "Discover XX" challenges. // "location" is more widely used, but "parentlocation" is used in Ambrose and Berlin, as well as some "Discover XX" challenges.
@ -287,7 +274,7 @@ export function filterChallenge(
*/ */
export function mergeSavedChallengeGroups( export function mergeSavedChallengeGroups(
g1: SavedChallengeGroup, g1: SavedChallengeGroup,
g2: SavedChallengeGroup, g2?: SavedChallengeGroup,
): SavedChallengeGroup { ): SavedChallengeGroup {
return { return {
...g1, ...g1,

View File

@ -49,7 +49,7 @@ import {
HandleEventOptions, HandleEventOptions,
} from "@peacockproject/statemachine-parser" } from "@peacockproject/statemachine-parser"
import { ChallengeContext, SavedChallengeGroup } from "../types/challenges" import { ChallengeContext, SavedChallengeGroup } from "../types/challenges"
import { fastClone, isSniperLocation } from "../utils" import { fastClone, gameDifficulty, isSniperLocation } from "../utils"
import { import {
ChallengeFilterOptions, ChallengeFilterOptions,
ChallengeFilterType, ChallengeFilterType,
@ -88,12 +88,13 @@ export abstract class ChallengeRegistry {
* @Key2 The challenge Id. * @Key2 The challenge Id.
* @value A `RegistryChallenge` object. * @value A `RegistryChallenge` object.
*/ */
protected challenges: Map<GameVersion, Map<string, RegistryChallenge>> = protected challenges: Record<GameVersion, Map<string, RegistryChallenge>> =
new Map([ {
["h1", new Map()], h1: new Map(),
["h2", new Map()], h2: new Map(),
["h3", new Map()], h3: new Map(),
]) scpc: new Map(),
}
/** /**
* @Key1 Game version. * @Key1 Game version.
@ -101,14 +102,15 @@ export abstract class ChallengeRegistry {
* @Key3 The group Id. * @Key3 The group Id.
* @Value A `SavedChallengeGroup` object. * @Value A `SavedChallengeGroup` object.
*/ */
protected groups: Map< protected groups: Record<
GameVersion, GameVersion,
Map<string, Map<string, SavedChallengeGroup>> Map<string, Map<string, SavedChallengeGroup>>
> = new Map([ > = {
["h1", new Map()], h1: new Map(),
["h2", new Map()], h2: new Map(),
["h3", new Map()], h3: new Map(),
]) scpc: new Map(),
}
/** /**
* @Key1 Game version. * @Key1 Game version.
@ -116,28 +118,30 @@ export abstract class ChallengeRegistry {
* @Key3 The group Id. * @Key3 The group Id.
* @Value A `Set` of challenge Ids. * @Value A `Set` of challenge Ids.
*/ */
protected groupContents: Map< protected groupContents: Record<
GameVersion, GameVersion,
Map<string, Map<string, Set<string>>> Map<string, Map<string, Set<string>>>
> = new Map([ > = {
["h1", new Map()], h1: new Map(),
["h2", new Map()], h2: new Map(),
["h3", new Map()], h3: new Map(),
]) scpc: new Map(),
}
/** /**
* @Key1 Game version. * @Key1 Game version.
* @Key2 The challenge Id. * @Key2 The challenge Id.
* @Value An `array` of challenge Ids that Key2 depends on. * @Value An `array` of challenge Ids that Key2 depends on.
*/ */
protected readonly _dependencyTree: Map< protected readonly _dependencyTree: Record<
GameVersion, GameVersion,
Map<string, readonly string[]> Map<string, readonly string[]>
> = new Map([ > = {
["h1", new Map()], h1: new Map(),
["h2", new Map()], h2: new Map(),
["h3", new Map()], h3: new Map(),
]) scpc: new Map(),
}
protected constructor(protected readonly controller: Controller) {} protected constructor(protected readonly controller: Controller) {}
@ -147,13 +151,9 @@ export abstract class ChallengeRegistry {
location: string, location: string,
gameVersion: GameVersion, gameVersion: GameVersion,
): void { ): void {
if (!this.groupContents.has(gameVersion)) { const gameChallenges = this.groupContents[gameVersion]
return
}
const gameChallenges = this.groupContents.get(gameVersion)
challenge.inGroup = groupId challenge.inGroup = groupId
this.challenges.get(gameVersion)?.set(challenge.Id, challenge) this.challenges[gameVersion].set(challenge.Id, challenge)
if (!gameChallenges.has(location)) { if (!gameChallenges.has(location)) {
gameChallenges.set(location, new Map()) gameChallenges.set(location, new Map())
@ -176,34 +176,33 @@ export abstract class ChallengeRegistry {
location: string, location: string,
gameVersion: GameVersion, gameVersion: GameVersion,
): void { ): void {
if (!this.groups.has(gameVersion)) { const gameGroups = this.groups[gameVersion]
return
}
const gameGroups = this.groups.get(gameVersion)
if (!gameGroups.has(location)) { if (!gameGroups.has(location)) {
gameGroups.set(location, new Map()) gameGroups.set(location, new Map())
} }
gameGroups.get(location).set(group.CategoryId, group) gameGroups.get(location)?.set(group.CategoryId, group)
} }
getChallengeById( getChallengeById(
challengeId: string, challengeId: string,
gameVersion: GameVersion, gameVersion: GameVersion,
): RegistryChallenge | undefined { ): RegistryChallenge | undefined {
return this.challenges.get(gameVersion)?.get(challengeId) return this.challenges[gameVersion].get(challengeId)
} }
/** /**
* Returns a list of all challenges unlockables * This method retrieves all the unlockables associated with the challenges for a given game version.
* It iterates over all the challenges for the specified game version and for each challenge, it checks if there are any unlockables (Drops).
* If there are unlockables, it adds them to the accumulator object with the dropId as the key and the challenge Id as the value.
* *
* @todo This is bad, untyped, and undocumented. Fix it. * @param gameVersion - The version of the game for which to retrieve the unlockables.
* @returns {Record<string, string>} - An object where each key is an unlockable's id (dropId) and the corresponding value is the id of the challenge that unlocks it.
*/ */
getChallengesUnlockables(gameVersion: GameVersion) { getChallengesUnlockables(gameVersion: GameVersion): Record<string, string> {
return [...this.challenges.get(gameVersion).values()].reduce( return [...this.challenges[gameVersion].values()].reduce(
(acc, challenge) => { (acc: Record<string, string>, challenge) => {
if (challenge?.Drops?.length) { if (challenge?.Drops?.length) {
challenge.Drops.forEach( challenge.Drops.forEach(
(dropId) => (acc[dropId] = challenge.Id), (dropId) => (acc[dropId] = challenge.Id),
@ -228,15 +227,18 @@ export abstract class ChallengeRegistry {
location: string, location: string,
gameVersion: GameVersion, gameVersion: GameVersion,
): SavedChallengeGroup | undefined { ): SavedChallengeGroup | undefined {
if (!this.groups.has(gameVersion)) { const gameGroups = this.groups[gameVersion]
return undefined
}
const gameGroups = this.groups.get(gameVersion) const mainGroup = gameGroups.get(location)?.get(groupId)
if (groupId === "feats" && gameVersion !== "h3") { if (groupId === "feats" && gameVersion !== "h3") {
if (!mainGroup) {
// emergency bailout - shouldn't happen in practice
return undefined
}
return mergeSavedChallengeGroups( return mergeSavedChallengeGroups(
gameGroups.get(location)?.get(groupId), mainGroup,
gameGroups.get("GLOBAL_ESCALATION_CHALLENGES")?.get(groupId), gameGroups.get("GLOBAL_ESCALATION_CHALLENGES")?.get(groupId),
) )
} }
@ -255,8 +257,13 @@ export abstract class ChallengeRegistry {
// Included by default. Filtered later. // Included by default. Filtered later.
if (groupId === "classic" && location !== "GLOBAL_CLASSIC_CHALLENGES") { if (groupId === "classic" && location !== "GLOBAL_CLASSIC_CHALLENGES") {
if (!mainGroup) {
// emergency bailout - shouldn't happen in practice
return undefined
}
return mergeSavedChallengeGroups( return mergeSavedChallengeGroups(
gameGroups.get(location)?.get(groupId), mainGroup,
gameGroups.get("GLOBAL_CLASSIC_CHALLENGES")?.get(groupId), gameGroups.get("GLOBAL_CLASSIC_CHALLENGES")?.get(groupId),
) )
} }
@ -265,13 +272,18 @@ export abstract class ChallengeRegistry {
groupId === "elusive" && groupId === "elusive" &&
location !== "GLOBAL_ELUSIVES_CHALLENGES" location !== "GLOBAL_ELUSIVES_CHALLENGES"
) { ) {
if (!mainGroup) {
// emergency bailout - shouldn't happen in practice
return undefined
}
return mergeSavedChallengeGroups( return mergeSavedChallengeGroups(
gameGroups.get(location)?.get(groupId), mainGroup,
gameGroups.get("GLOBAL_ELUSIVES_CHALLENGES")?.get(groupId), gameGroups.get("GLOBAL_ELUSIVES_CHALLENGES")?.get(groupId),
) )
} }
return gameGroups.get(location)?.get(groupId) return mainGroup
} }
public getGroupContentByIdLoc( public getGroupContentByIdLoc(
@ -279,11 +291,7 @@ export abstract class ChallengeRegistry {
location: string, location: string,
gameVersion: GameVersion, gameVersion: GameVersion,
): Set<string> | undefined { ): Set<string> | undefined {
if (!this.groupContents.has(gameVersion)) { const gameChalGC = this.groupContents[gameVersion]
return undefined
}
const gameChalGC = this.groupContents.get(gameVersion)
if (groupId === "feats" && gameVersion !== "h3") { if (groupId === "feats" && gameVersion !== "h3") {
return new Set([ return new Set([
@ -334,9 +342,16 @@ export abstract class ChallengeRegistry {
challengeId: string, challengeId: string,
gameVersion: GameVersion, gameVersion: GameVersion,
): readonly string[] { ): readonly string[] {
return this._dependencyTree.get(gameVersion)?.get(challengeId) || [] return this._dependencyTree[gameVersion].get(challengeId) || []
} }
/**
* This method checks the heuristics of a challenge.
* It parses the context listeners of the challenge and if the challenge has any dependencies (other challenges that need to be completed before this one), it adds them to the dependency tree.
*
* @param challenge The challenge to check.
* @param gameVersion The game version this challenge belongs to.
*/
protected checkHeuristics( protected checkHeuristics(
challenge: RegistryChallenge, challenge: RegistryChallenge,
gameVersion: GameVersion, gameVersion: GameVersion,
@ -344,9 +359,10 @@ export abstract class ChallengeRegistry {
const ctxListeners = ChallengeRegistry._parseContextListeners(challenge) const ctxListeners = ChallengeRegistry._parseContextListeners(challenge)
if (ctxListeners.challengeTreeIds.length > 0) { if (ctxListeners.challengeTreeIds.length > 0) {
this._dependencyTree this._dependencyTree[gameVersion].set(
.get(gameVersion) challenge.Id,
?.set(challenge.Id, ctxListeners.challengeTreeIds) ctxListeners.challengeTreeIds,
)
} }
} }
@ -392,10 +408,11 @@ export class ChallengeService extends ChallengeRegistry {
} }
/** /**
* Check if the challenge needs to be saved in the user's progression data * Check if the challenge needs to be saved in the user's progression data.
* i.e. challenges with scopes being "profile" or "hit". * Challenges with scopes "profile" or "hit".
*
* @param challenge The challenge. * @param challenge The challenge.
* @returns Whether the challenge needs to be saved in the user's progression data. * @returns Whether the challenge needs to be saved in the user's progression data.
*/ */
needSaveProgression(challenge: RegistryChallenge): boolean { needSaveProgression(challenge: RegistryChallenge): boolean {
return ( return (
@ -512,7 +529,7 @@ export class ChallengeService extends ChallengeRegistry {
challenges: [string, RegistryChallenge[]][], challenges: [string, RegistryChallenge[]][],
gameVersion: GameVersion, gameVersion: GameVersion,
) { ) {
const groups = this.groups.get(gameVersion).get(location)?.keys() ?? [] const groups = this.groups[gameVersion].get(location)?.keys() ?? []
for (const groupId of groups) { for (const groupId of groups) {
// if this is the global group, skip it. // if this is the global group, skip it.
@ -543,9 +560,9 @@ export class ChallengeService extends ChallengeRegistry {
return challenge return challenge
} }
return filterChallenge(filter, challenge) const res = filterChallenge(filter, challenge)
? challenge
: undefined return res ? challenge : undefined
}) })
.filter(Boolean) as RegistryChallenge[] .filter(Boolean) as RegistryChallenge[]
@ -570,10 +587,6 @@ export class ChallengeService extends ChallengeRegistry {
): GroupIndexedChallengeLists { ): GroupIndexedChallengeLists {
let challenges: [string, RegistryChallenge[]][] = [] let challenges: [string, RegistryChallenge[]][] = []
if (!this.groups.has(gameVersion)) {
return {}
}
this.getGroupedChallengesByLoc( this.getGroupedChallengesByLoc(
filter, filter,
location, location,
@ -622,25 +635,36 @@ export class ChallengeService extends ChallengeRegistry {
difficulty = 4, difficulty = 4,
): GroupIndexedChallengeLists { ): GroupIndexedChallengeLists {
const userData = getUserData(userId, gameVersion) const userData = getUserData(userId, gameVersion)
const contract = this.controller.resolveContract(contractId, true) const contractGroup = this.controller.resolveContract(contractId, true)
const level = if (!contractGroup) {
contract.Metadata.Type === "arcade" && return {}
contract.Metadata.Id === contractId }
? // contractData, being a group contract, has the same Id as the input id parameter.
// This means that we are requesting the challenges for the next level of the group
this.controller.resolveContract(
contract.Metadata.GroupDefinition.Order[
getUserEscalationProgress(userData, contractId) - 1
],
false,
)
: this.controller.resolveContract(contractId, false)
assert.ok(contract) let contract: MissionManifest | undefined
if (
contractGroup.Metadata.Type === "arcade" &&
contractGroup.Metadata.Id === contractId
) {
const currentLevel =
contractGroup.Metadata.GroupDefinition?.Order[
getUserEscalationProgress(userData, contractId) - 1
]
assert.ok(currentLevel, "expected current level ID in escalation")
contract = this.controller.resolveContract(currentLevel, false)
} else {
contract = this.controller.resolveContract(contractId, false)
}
if (!contract) {
return {}
}
const levelParentLocation = getSubLocationFromContract( const levelParentLocation = getSubLocationFromContract(
level, contract,
gameVersion, gameVersion,
)?.Properties.ParentLocation )?.Properties.ParentLocation
@ -651,12 +675,12 @@ export class ChallengeService extends ChallengeRegistry {
type: ChallengeFilterType.Contract, type: ChallengeFilterType.Contract,
contractId: contractId, contractId: contractId,
locationId: locationId:
contract.Metadata.Id === contractGroup.Metadata.Id ===
"aee6a16f-6525-4d63-a37f-225e293c6118" && "aee6a16f-6525-4d63-a37f-225e293c6118" &&
gameVersion !== "h1" gameVersion !== "h1"
? "LOCATION_ICA_FACILITY_SHIP" ? "LOCATION_ICA_FACILITY_SHIP"
: level.Metadata.Location, : contract.Metadata.Location,
isFeatured: contract.Metadata.Type === "featured", isFeatured: contractGroup.Metadata.Type === "featured",
difficulty, difficulty,
}, },
levelParentLocation, levelParentLocation,
@ -676,17 +700,23 @@ export class ChallengeService extends ChallengeRegistry {
const parent = locations.children[child].Properties.ParentLocation const parent = locations.children[child].Properties.ParentLocation
let contracts = isSniperLocation(child) let contracts = isSniperLocation(child)
? this.controller.missionsInLocations.sniper[child] ? // @ts-expect-error This is fine - we know it will be there
: (this.controller.missionsInLocations[child] ?? []) this.controller.missionsInLocations.sniper[child]
: // @ts-expect-error This is fine - we know it will be there
(this.controller.missionsInLocations[child] ?? [])
.concat( .concat(
// @ts-expect-error This is fine - we know it will be there
this.controller.missionsInLocations.escalations[child], this.controller.missionsInLocations.escalations[child],
) )
// @ts-expect-error This is fine - we know it will be there
.concat(this.controller.missionsInLocations.arcade[child]) .concat(this.controller.missionsInLocations.arcade[child])
if (!contracts) { if (!contracts) {
contracts = [] contracts = []
} }
assert.ok(parent, "expected parent location")
return this.getGroupedChallengeLists( return this.getGroupedChallengeLists(
{ {
type: ChallengeFilterType.Contracts, type: ChallengeFilterType.Contracts,
@ -712,26 +742,31 @@ export class ChallengeService extends ChallengeRegistry {
session.difficulty, session.difficulty,
) )
if (contractJson.Metadata.Type === "evergreen") { if (contractJson?.Metadata.Type === "evergreen") {
session.evergreen = { session.evergreen = {
payout: 0, payout: 0,
scoringScreenEndState: undefined, scoringScreenEndState: null,
failed: false, failed: false,
} }
} }
// TODO: Add this to getChallengesForContract without breaking the rest of Peacock? // TODO: Add this to getChallengesForContract without breaking the rest of Peacock?
challengeGroups["global"] = this.getGroupByIdLoc( challengeGroups["global"] =
"global", this.getGroupByIdLoc(
"GLOBAL", "global",
session.gameVersion, "GLOBAL",
).Challenges.filter((val) => session.gameVersion,
inclusionDataCheck(val.InclusionData, contractJson), )?.Challenges.filter((val) =>
) inclusionDataCheck(val.InclusionData, contractJson),
) || []
const profile = getUserData(session.userId, session.gameVersion) const profile = getUserData(session.userId, session.gameVersion)
for (const group of Object.keys(challengeGroups)) { for (const group of Object.keys(challengeGroups)) {
if (!challengeContexts) {
break
}
for (const challenge of challengeGroups[group]) { for (const challenge of challengeGroups[group]) {
challengeContexts[challenge.Id] = { challengeContexts[challenge.Id] = {
context: undefined, context: undefined,
@ -892,7 +927,7 @@ export class ChallengeService extends ChallengeRegistry {
contractId: string, contractId: string,
gameVersion: GameVersion, gameVersion: GameVersion,
userId: string, userId: string,
difficulty = 4, difficulty = gameDifficulty.master,
): CompiledChallengeTreeCategory[] { ): CompiledChallengeTreeCategory[] {
const userData = getUserData(userId, gameVersion) const userData = getUserData(userId, gameVersion)
@ -902,18 +937,37 @@ export class ChallengeService extends ChallengeRegistry {
return [] return []
} }
const levelData = let levelData: MissionManifest | undefined
if (
contractData.Metadata.Type === "arcade" && contractData.Metadata.Type === "arcade" &&
contractData.Metadata.Id === contractId contractData.Metadata.Id === contractId
? // contractData, being a group contract, has the same Id as the input id parameter. ) {
// This means that we are requesting the challenges for the next level of the group const order =
this.controller.resolveContract( contractData.Metadata.GroupDefinition?.Order[
contractData.Metadata.GroupDefinition.Order[ getUserEscalationProgress(userData, contractId) - 1
getUserEscalationProgress(userData, contractId) - 1 ]
],
false, if (!order) {
) log(
: this.controller.resolveContract(contractId, false) LogLevel.WARN,
`Failed to get escalation order in CTREE [${contractData.Metadata.GroupDefinition?.Order}]`,
)
return []
}
levelData = this.controller.resolveContract(order, false)
} else {
levelData = this.controller.resolveContract(contractId, false)
}
if (!levelData) {
log(
LogLevel.WARN,
`Failed to get level data in CTREE [${contractId}]`,
)
return []
}
const subLocation = getSubLocationFromContract(levelData, gameVersion) const subLocation = getSubLocationFromContract(levelData, gameVersion)
@ -1116,80 +1170,89 @@ export class ChallengeService extends ChallengeRegistry {
gameVersion, gameVersion,
) )
return entries.map(([groupId, challenges], index) => { return entries
const groupData = this.getGroupByIdLoc( .map(([groupId, challenges], index) => {
groupId, const groupData = this.getGroupByIdLoc(
location.Properties.ParentLocation ?? location.Id, groupId,
gameVersion, location.Properties.ParentLocation ?? location.Id,
)
const challengeProgressionData = challenges.map((challengeData) =>
this.getPersistentChallengeProgression(
userId,
challengeData.Id,
gameVersion, gameVersion,
), )
)
const lastGroup = this.getGroupByIdLoc( if (!groupData) {
Object.keys(challengeLists)[index - 1], return undefined
location.Properties.ParentLocation ?? location.Id, }
gameVersion,
)
const nextGroup = this.getGroupByIdLoc(
Object.keys(challengeLists)[index + 1],
location.Properties.ParentLocation ?? location.Id,
gameVersion,
)
return { const challengeProgressionData = challenges.map(
Name: groupData?.Name, (challengeData) =>
Description: groupData?.Description, this.getPersistentChallengeProgression(
Image: groupData?.Image,
CategoryId: groupData?.CategoryId,
Icon: groupData?.Icon,
ChallengesCount: challenges.length,
CompletedChallengesCount: challengeProgressionData.filter(
(progressionData) => progressionData.Completed,
).length,
CompletionData: completion,
Location: location,
IsLocked: location.Properties.IsLocked || false,
ImageLocked: location.Properties.LockedIcon || "",
RequiredResources: location.Properties.RequiredResources!,
SwitchData: {
Data: {
Challenges: this.mapSwitchChallenges(
challenges,
userId, userId,
challengeData.Id,
gameVersion, gameVersion,
compiler,
), ),
HasPrevious: index !== 0, // whether we are not at the first group )
HasNext:
index !== Object.keys(challengeLists).length - 1, // whether we are not at the final group const lastGroup = this.getGroupByIdLoc(
PreviousCategoryIcon: Object.keys(challengeLists)[index - 1],
index !== 0 ? lastGroup?.Icon : "", location.Properties.ParentLocation ?? location.Id,
NextCategoryIcon: gameVersion,
index !== Object.keys(challengeLists).length - 1 )
? nextGroup?.Icon const nextGroup = this.getGroupByIdLoc(
: "", Object.keys(challengeLists)[index + 1],
CategoryData: { location.Properties.ParentLocation ?? location.Id,
Name: groupData.Name, gameVersion,
Image: groupData.Image, )
Icon: groupData.Icon,
ChallengesCount: challenges.length, return {
CompletedChallengesCount: Name: groupData.Name,
challengeProgressionData.filter( Description: groupData.Description,
(progressionData) => Image: groupData.Image,
progressionData.Completed, CategoryId: groupData.CategoryId,
).length, Icon: groupData.Icon,
ChallengesCount: challenges.length,
CompletedChallengesCount: challengeProgressionData.filter(
(progressionData) => progressionData.Completed,
).length,
CompletionData: completion,
Location: location,
IsLocked: location.Properties.IsLocked || false,
ImageLocked: location.Properties.LockedIcon || "",
RequiredResources: location.Properties.RequiredResources!,
SwitchData: {
Data: {
Challenges: this.mapSwitchChallenges(
challenges,
userId,
gameVersion,
compiler,
),
HasPrevious: index !== 0, // whether we are not at the first group
HasNext:
index !==
Object.keys(challengeLists).length - 1, // whether we are not at the final group
PreviousCategoryIcon:
index !== 0 ? lastGroup?.Icon : "",
NextCategoryIcon:
index !== Object.keys(challengeLists).length - 1
? nextGroup?.Icon
: "",
CategoryData: {
Name: groupData.Name,
Image: groupData.Image,
Icon: groupData.Icon,
ChallengesCount: challenges.length,
CompletedChallengesCount:
challengeProgressionData.filter(
(progressionData) =>
progressionData.Completed,
).length,
},
CompletionData: completion,
}, },
CompletionData: completion, IsLeaf: true,
}, },
IsLeaf: true, }
}, })
} .filter(Boolean) as CompiledChallengeTreeCategory[]
})
} }
compileRegistryChallengeTreeData( compileRegistryChallengeTreeData(
@ -1201,7 +1264,7 @@ export class ChallengeService extends ChallengeRegistry {
): CompiledChallengeTreeData { ): CompiledChallengeTreeData {
const drops = challenge.Drops.map((e) => const drops = challenge.Drops.map((e) =>
getUnlockableById(e, gameVersion), getUnlockableById(e, gameVersion),
).filter(Boolean) ).filter(Boolean) as Unlockable[]
if (drops.length !== challenge.Drops.length) { if (drops.length !== challenge.Drops.length) {
log( log(
@ -1255,7 +1318,7 @@ export class ChallengeService extends ChallengeRegistry {
gameVersion: GameVersion, gameVersion: GameVersion,
userId: string, userId: string,
): CompiledChallengeTreeData { ): CompiledChallengeTreeData {
let contract: MissionManifest | null let contract: MissionManifest | undefined
if (challenge.Type === "contract") { if (challenge.Type === "contract") {
contract = this.controller.resolveContract( contract = this.controller.resolveContract(
@ -1264,39 +1327,40 @@ export class ChallengeService extends ChallengeRegistry {
// This is so we can remove unused data and make it more like official - AF // This is so we can remove unused data and make it more like official - AF
const meta = contract?.Metadata const meta = contract?.Metadata
contract = !contract contract =
? null !contract || !meta
: { ? undefined
// The null is for escalations as we cannot currently get groups : {
Data: { // The null is for escalations as we cannot currently get groups
Bricks: contract.Data.Bricks, Data: {
DevOnlyBricks: null, Bricks: contract.Data.Bricks,
GameChangerReferences: DevOnlyBricks: null,
contract.Data.GameChangerReferences || [], GameChangerReferences:
GameChangers: contract.Data.GameChangers || [], contract.Data.GameChangerReferences || [],
GameDifficulties: GameChangers: contract.Data.GameChangers || [],
contract.Data.GameDifficulties || [], GameDifficulties:
}, contract.Data.GameDifficulties || [],
Metadata: { },
CreationTimestamp: null, Metadata: {
CreatorUserId: meta.CreatorUserId, CreationTimestamp: null,
DebriefingVideo: meta.DebriefingVideo || "", CreatorUserId: meta.CreatorUserId,
Description: meta.Description, DebriefingVideo: meta.DebriefingVideo || "",
Drops: meta.Drops || null, Description: meta.Description,
Entitlements: meta.Entitlements || [], Drops: meta.Drops || null,
GroupTitle: meta.GroupTitle || "", Entitlements: meta.Entitlements || [],
Id: meta.Id, GroupTitle: meta.GroupTitle || "",
IsPublished: meta.IsPublished || true, Id: meta.Id,
LastUpdate: null, IsPublished: meta.IsPublished || true,
Location: meta.Location, LastUpdate: null,
PublicId: meta.PublicId || "", Location: meta.Location,
ScenePath: meta.ScenePath, PublicId: meta.PublicId || "",
Subtype: meta.Subtype || "", ScenePath: meta.ScenePath,
TileImage: meta.TileImage, Subtype: meta.Subtype || "",
Title: meta.Title, TileImage: meta.TileImage,
Type: meta.Type, Title: meta.Title,
}, Type: meta.Type,
} },
}
} }
return { return {
@ -1375,12 +1439,11 @@ export class ChallengeService extends ChallengeRegistry {
if (challengeId === parentId) { if (challengeId === parentId) {
// we're checking the tree of the challenge that was just completed, // we're checking the tree of the challenge that was just completed,
// so we need to skip it, or we'll get an infinite loop and hit // so we need to skip it, or we'll get an infinite loop
// the max call stack size
return return
} }
const allDeps = this._dependencyTree.get(gameVersion)?.get(challengeId) const allDeps = this._dependencyTree[gameVersion].get(challengeId)
assert.ok(allDeps, `No dep tree for ${challengeId}`) assert.ok(allDeps, `No dep tree for ${challengeId}`)
if (!allDeps.includes(parentId)) { if (!allDeps.includes(parentId)) {
@ -1393,10 +1456,17 @@ export class ChallengeService extends ChallengeRegistry {
// Check if the dependency tree is completed now // Check if the dependency tree is completed now
const dep = this.getChallengeById(challengeId, gameVersion) const challengeDependency = this.getChallengeById(
challengeId,
gameVersion,
)
if (!challengeDependency) {
return
}
const { challengeCountData } = const { challengeCountData } =
ChallengeService._parseContextListeners(dep) ChallengeService._parseContextListeners(challengeDependency)
// First check for challengecounter, then challengetree // First check for challengecounter, then challengetree
const completed = const completed =
@ -1408,11 +1478,15 @@ export class ChallengeService extends ChallengeRegistry {
return return
} }
const challenge = this.getChallengeById(challengeId, gameVersion)
assert.ok(challenge, `No challenge for ${challengeId}`)
this.onChallengeCompleted( this.onChallengeCompleted(
session, session,
userData.Id, userData.Id,
gameVersion, gameVersion,
this.getChallengeById(challengeId, gameVersion), challenge,
parentId, parentId,
) )
} }
@ -1464,18 +1538,20 @@ export class ChallengeService extends ChallengeRegistry {
true true
} }
// Always count the number of completions if (session.challengeContexts) {
if (session.challengeContexts[challenge.Id]) { // Always count the number of completions
session.challengeContexts[challenge.Id].timesCompleted++ if (session.challengeContexts[challenge.Id]) {
} session.challengeContexts[challenge.Id].timesCompleted++
}
// If we have a Definition-scope with a Repeatable, we may want to restart it. // If we have a Definition-scope with a Repeatable, we may want to restart it.
// TODO: Figure out what Base/Delta means. For now if Repeatable is set, we restart the challenge. // TODO: Figure out what Base/Delta means. For now if Repeatable is set, we restart the challenge.
if ( if (
challenge.Definition.Repeatable && challenge.Definition.Repeatable &&
session.challengeContexts[challenge.Id] session.challengeContexts[challenge.Id]
) { ) {
session.challengeContexts[challenge.Id].state = "Start" session.challengeContexts[challenge.Id].state = "Start"
}
} }
controller.progressionService.grantProfileProgression( controller.progressionService.grantProfileProgression(
@ -1490,7 +1566,7 @@ export class ChallengeService extends ChallengeRegistry {
this.hooks.onChallengeCompleted.call(userId, challenge, gameVersion) this.hooks.onChallengeCompleted.call(userId, challenge, gameVersion)
// Check if completing this challenge also completes any dependency trees depending on it // Check if completing this challenge also completes any dependency trees depending on it
for (const depTreeId of this._dependencyTree.get(gameVersion).keys()) { for (const depTreeId of this._dependencyTree[gameVersion].keys()) {
this.tryToCompleteChallenge( this.tryToCompleteChallenge(
session, session,
depTreeId, depTreeId,

View File

@ -21,17 +21,22 @@ import {
getSubLocationByName, getSubLocationByName,
} from "../contracts/dataGen" } from "../contracts/dataGen"
import { log, LogLevel } from "../loggingInterop" import { log, LogLevel } from "../loggingInterop"
import { getConfig, getVersionedConfig } from "../configSwizzleManager" import { getVersionedConfig } from "../configSwizzleManager"
import { getUserData } from "../databaseHandler" import { getUserData } from "../databaseHandler"
import { import {
LocationMasteryData,
MasteryData, MasteryData,
MasteryDataTemplate,
MasteryDrop, MasteryDrop,
MasteryPackage, MasteryPackage,
MasteryPackageDrop, MasteryPackageDrop,
UnlockableMasteryData, UnlockableMasteryData,
} from "../types/mastery" } from "../types/mastery"
import { CompletionData, GameVersion, Unlockable } from "../types/types" import {
CompletionData,
GameVersion,
ProgressionData,
Unlockable,
} from "../types/types"
import { import {
clampValue, clampValue,
DEFAULT_MASTERY_MAXLEVEL, DEFAULT_MASTERY_MAXLEVEL,
@ -42,6 +47,7 @@ import {
} from "../utils" } from "../utils"
import { getUnlockablesById } from "../inventory" import { getUnlockablesById } from "../inventory"
import assert from "assert"
export class MasteryService { export class MasteryService {
/** /**
@ -150,22 +156,16 @@ export class MasteryService {
)[0] )[0]
} }
// TODO: what do we want to do with this? We should prob remove the template part
// to make this like the other routes, and more testable.
getMasteryDataForLocation( getMasteryDataForLocation(
locationId: string, locationId: string,
gameVersion: GameVersion, gameVersion: GameVersion,
userId: string, userId: string,
): MasteryDataTemplate { ): LocationMasteryData {
const location: Unlockable = const location =
getSubLocationByName(locationId, gameVersion) ?? getSubLocationByName(locationId, gameVersion) ??
getParentLocationByName(locationId, gameVersion) getParentLocationByName(locationId, gameVersion)
const masteryDataTemplate: MasteryDataTemplate = assert.ok(location, "cannot get mastery data for unknown location")
getConfig<MasteryDataTemplate>(
"MasteryDataForLocationTemplate",
false,
)
const masteryData = this.getMasteryData( const masteryData = this.getMasteryData(
location.Properties.ParentLocation ?? location.Id, location.Properties.ParentLocation ?? location.Id,
@ -174,11 +174,8 @@ export class MasteryService {
) )
return { return {
template: masteryDataTemplate, Location: location,
data: { MasteryData: masteryData,
Location: location,
MasteryData: masteryData,
},
} }
} }
@ -202,19 +199,12 @@ export class MasteryService {
// Get the user profile // Get the user profile
const userProfile = getUserData(userId, gameVersion) const userProfile = getUserData(userId, gameVersion)
// @since v7.0.0 this has been commented out as the default profile should const parent =
// have all the required properties - AF userProfile.Extensions.progression.Locations[locationParentId]
/* userProfile.Extensions.progression.Locations[locationParentId] ??= {
Xp: 0,
Level: 1,
PreviouslySeenXp: 0,
} */
const completionData = subPackageId const completionData: ProgressionData = subPackageId
? userProfile.Extensions.progression.Locations[locationParentId][ ? (parent[subPackageId as keyof typeof parent] as ProgressionData)
subPackageId : (parent as ProgressionData)
]
: userProfile.Extensions.progression.Locations[locationParentId]
const nextLevel: number = clampValue( const nextLevel: number = clampValue(
completionData.Level + 1, completionData.Level + 1,
@ -279,7 +269,7 @@ export class MasteryService {
"SniperUnlockables", "SniperUnlockables",
gameVersion, gameVersion,
false, false,
).find((unlockable) => unlockable.Id === subPackageId).Properties ).find((unlockable) => unlockable.Id === subPackageId)?.Properties
.Name .Name
: undefined : undefined
@ -297,11 +287,11 @@ export class MasteryService {
: xpRequiredForLevel, : xpRequiredForLevel,
subPackageId, subPackageId,
), ),
Id: isSniper ? subPackageId : masteryPkg.LocationId, Id: isSniper ? subPackageId! : masteryPkg.LocationId,
SubLocationId: isSniper ? "" : subLocationId, SubLocationId: isSniper ? "" : subLocationId,
HideProgression: masteryPkg.HideProgression || false, HideProgression: masteryPkg.HideProgression || false,
IsLocationProgression: !isSniper, IsLocationProgression: !isSniper,
Name: name, Name: name!,
} }
} }
@ -347,7 +337,7 @@ export class MasteryService {
subPackageId?: string, subPackageId?: string,
): MasteryData[] { ): MasteryData[] {
// Get the mastery data // Get the mastery data
const masteryPkg: MasteryPackage = this.getMasteryPackage( const masteryPkg: MasteryPackage | undefined = this.getMasteryPackage(
locationParentId, locationParentId,
gameVersion, gameVersion,
) )
@ -389,16 +379,23 @@ export class MasteryService {
} }
// Get all unlockables with matching Ids // Get all unlockables with matching Ids
const unlockableData: Unlockable[] = getUnlockablesById( const unlockableData = getUnlockablesById(
Array.from(dropIdSet), Array.from(dropIdSet),
gameVersion, gameVersion,
) )
// Put all unlockabkes in a map for quick lookup // Put all unlockabkes in a map for quick lookup
const unlockableMap = new Map( const mapped: [string, Unlockable][] = unlockableData.map(
unlockableData.map((unlockable) => [unlockable.Id, unlockable]), (unlockable) => {
return [unlockable?.Id, unlockable] as unknown as [
string,
Unlockable,
]
},
) )
const unlockableMap: Map<string, Unlockable> = new Map(mapped)
const masteryData: MasteryData[] = [] const masteryData: MasteryData[] = []
if (masteryPkg.SubPackages) { if (masteryPkg.SubPackages) {
@ -418,17 +415,19 @@ export class MasteryService {
subPkg.Id, subPkg.Id,
) )
masteryData.push({ if (completionData) {
CompletionData: completionData, masteryData.push({
Drops: this.processDrops( CompletionData: completionData,
completionData.Level, Drops: this.processDrops(
subPkg.Drops, completionData.Level,
unlockableMap, subPkg.Drops,
), unlockableMap,
Unlockable: isSniper ),
? unlockableMap.get(subPkg.Id) Unlockable: isSniper
: undefined, ? unlockableMap.get(subPkg.Id)
}) : undefined,
})
}
} }
} else { } else {
// All sniper locations are subpackages, so we don't need to add "sniper" // All sniper locations are subpackages, so we don't need to add "sniper"
@ -441,14 +440,16 @@ export class MasteryService {
locationParentId.includes("SNUG") ? "evergreen" : "mission", locationParentId.includes("SNUG") ? "evergreen" : "mission",
) )
masteryData.push({ if (completionData) {
CompletionData: completionData, masteryData.push({
Drops: this.processDrops( CompletionData: completionData,
completionData.Level, Drops: this.processDrops(
masteryPkg.Drops, completionData.Level,
unlockableMap, masteryPkg.Drops || [],
), unlockableMap,
}) ),
})
}
} }
return masteryData return masteryData

View File

@ -19,7 +19,12 @@
import { getSubLocationByName } from "../contracts/dataGen" import { getSubLocationByName } from "../contracts/dataGen"
import { controller } from "../controller" import { controller } from "../controller"
import { getUnlockablesById, grantDrops } from "../inventory" import { getUnlockablesById, grantDrops } from "../inventory"
import type { ContractSession, UserProfile, GameVersion } from "../types/types" import type {
ContractSession,
GameVersion,
Unlockable,
UserProfile,
} from "../types/types"
import { import {
clampValue, clampValue,
DEFAULT_MASTERY_MAXLEVEL, DEFAULT_MASTERY_MAXLEVEL,
@ -70,7 +75,9 @@ export class ProgressionService {
if (dropIds.length > 0) { if (dropIds.length > 0) {
grantDrops( grantDrops(
userProfile.Id, userProfile.Id,
getUnlockablesById(dropIds, contractSession.gameVersion), getUnlockablesById(dropIds, contractSession.gameVersion).filter(
Boolean,
) as Unlockable[],
) )
} }
@ -85,7 +92,8 @@ export class ProgressionService {
subPkgId?: string, subPkgId?: string,
) { ) {
return subPkgId return subPkgId
? userProfile.Extensions.progression.Locations[location][subPkgId] ? // @ts-expect-error It is possible to index into an object with a string
userProfile.Extensions.progression.Locations[location][subPkgId]
: userProfile.Extensions.progression.Locations[location] : userProfile.Extensions.progression.Locations[location]
} }
@ -179,25 +187,29 @@ export class ProgressionService {
if (masteryData) { if (masteryData) {
const previousLevel = locationData.Level const previousLevel = locationData.Level
let newLocationXp = xpRequiredForLevel(maxLevel)
if (isEvergreenContract) {
newLocationXp = xpRequiredForEvergreenLevel(maxLevel)
} else if (sniperUnlockable) {
newLocationXp = xpRequiredForSniperLevel(maxLevel)
}
locationData.Xp = clampValue( locationData.Xp = clampValue(
locationData.Xp + masteryXp + actionXp, locationData.Xp + masteryXp + actionXp,
0, 0,
isEvergreenContract newLocationXp,
? xpRequiredForEvergreenLevel(maxLevel)
: sniperUnlockable
? xpRequiredForSniperLevel(maxLevel)
: xpRequiredForLevel(maxLevel),
) )
locationData.Level = clampValue( let newLocationLevel = levelForXp(newLocationXp)
isEvergreenContract
? evergreenLevelForXp(locationData.Xp) if (isEvergreenContract) {
: sniperUnlockable newLocationLevel = evergreenLevelForXp(newLocationXp)
? sniperLevelForXp(locationData.Xp) } else if (sniperUnlockable) {
: levelForXp(locationData.Xp), newLocationLevel = sniperLevelForXp(newLocationXp)
1, }
maxLevel,
) locationData.Level = clampValue(newLocationLevel, 1, maxLevel)
// If mastery level has gone up, check if there are available drop rewards and award them // If mastery level has gone up, check if there are available drop rewards and award them
if (locationData.Level > previousLevel) { if (locationData.Level > previousLevel) {
@ -205,13 +217,13 @@ export class ProgressionService {
contractSession.gameVersion, contractSession.gameVersion,
isEvergreenContract, isEvergreenContract,
sniperUnlockable sniperUnlockable
? masteryData.SubPackages.find( ? masteryData.SubPackages?.find(
(pkg) => pkg.Id === sniperUnlockable, (pkg) => pkg.Id === sniperUnlockable,
).Drops )?.Drops || []
: masteryData.Drops, : masteryData.Drops || [],
previousLevel, previousLevel,
locationData.Level, locationData.Level,
) ).filter(Boolean) as Unlockable[]
grantDrops(userProfile.Id, masteryLocationDrops) grantDrops(userProfile.Id, masteryLocationDrops)
} }
} }

View File

@ -291,7 +291,7 @@ export function getVersionedConfig<T = unknown>(
} }
// if this is H2, but we don't have a h2 specific config, fall back to h3 // if this is H2, but we don't have a h2 specific config, fall back to h3
if (gameVersion === "h2" && !Object.hasOwn(configs, `H2${config}`)) { if (gameVersion === "h2" && !configs[`H2${config}`]) {
return getConfig(config, clone) return getConfig(config, clone)
} }

View File

@ -52,7 +52,7 @@ import {
createTimeLimit, createTimeLimit,
TargetCreator, TargetCreator,
} from "../statemachines/contractCreation" } from "../statemachines/contractCreation"
import { createSniperLoadouts } from "../menus/sniper" import { createSniperLoadouts, SniperCharacter } from "../menus/sniper"
import { GetForPlay2Body } from "../types/gameSchemas" import { GetForPlay2Body } from "../types/gameSchemas"
import assert from "assert" import assert from "assert"
import { getUserData } from "../databaseHandler" import { getUserData } from "../databaseHandler"
@ -63,7 +63,8 @@ const contractRoutingRouter = Router()
contractRoutingRouter.post( contractRoutingRouter.post(
"/GetForPlay2", "/GetForPlay2",
jsonMiddleware(), jsonMiddleware(),
async (req: RequestWithJwt<never, GetForPlay2Body>, res) => { // @ts-expect-error Has jwt props.
(req: RequestWithJwt<never, GetForPlay2Body>, res) => {
if (!req.body.id || !uuidRegex.test(req.body.id)) { if (!req.body.id || !uuidRegex.test(req.body.id)) {
res.status(400).end() res.status(400).end()
return // user sent some nasty info return // user sent some nasty info
@ -84,7 +85,8 @@ contractRoutingRouter.post(
req.jwt.unique_name, req.jwt.unique_name,
req.gameVersion, req.gameVersion,
contractData, contractData,
) ) as SniperCharacter[]
const loadoutData = { const loadoutData = {
CharacterLoadoutData: CharacterLoadoutData:
sniperloadouts.length !== 0 ? sniperloadouts : null, sniperloadouts.length !== 0 ? sniperloadouts : null,
@ -100,7 +102,7 @@ contractRoutingRouter.post(
req.gameVersion, req.gameVersion,
) )
: {}), : {}),
...loadoutData, ...(loadoutData || {}),
...{ ...{
OpportunityData: getContractOpportunityData(req, contractData), OpportunityData: getContractOpportunityData(req, contractData),
}, },
@ -120,7 +122,7 @@ contractRoutingRouter.post(
.toString()}-${randomUUID()}`, .toString()}-${randomUUID()}`,
ContractProgressionData: contractData.Metadata ContractProgressionData: contractData.Metadata
.UseContractProgressionData .UseContractProgressionData
? await getCpd(req.jwt.unique_name, contractData.Metadata.CpdId) ? getCpd(req.jwt.unique_name, contractData.Metadata.CpdId!)
: null, : null,
} }
@ -159,6 +161,8 @@ contractRoutingRouter.post(
continue continue
} }
assert.ok(gameChanger.Objectives, "gc has no objectives")
contractData.Data.GameChangerReferences.push(gameChanger) contractData.Data.GameChangerReferences.push(gameChanger)
contractData.Data.Bricks = [ contractData.Data.Bricks = [
...(contractData.Data.Bricks ?? []), ...(contractData.Data.Bricks ?? []),
@ -228,6 +232,7 @@ contractRoutingRouter.post(
contractRoutingRouter.post( contractRoutingRouter.post(
"/CreateFromParams", "/CreateFromParams",
jsonMiddleware(), jsonMiddleware(),
// @ts-expect-error Has jwt props.
async ( async (
req: RequestWithJwt<Record<never, never>, CreateFromParamsBody>, req: RequestWithJwt<Record<never, never>, CreateFromParamsBody>,
res, res,
@ -340,13 +345,20 @@ contractRoutingRouter.post(
contractRoutingRouter.post( contractRoutingRouter.post(
"/GetContractOpportunities", "/GetContractOpportunities",
jsonMiddleware(), jsonMiddleware(),
// @ts-expect-error Has jwt props.
(req: RequestWithJwt<never, { contractId: string }>, res) => { (req: RequestWithJwt<never, { contractId: string }>, res) => {
const contract = controller.resolveContract(req.body.contractId) const contract = controller.resolveContract(req.body.contractId)
if (!contract) {
res.status(400).send("contract not found")
return
}
res.json(getContractOpportunityData(req, contract)) res.json(getContractOpportunityData(req, contract))
}, },
) )
function getContractOpportunityData( export function getContractOpportunityData(
req: RequestWithJwt, req: RequestWithJwt,
contract: MissionManifest, contract: MissionManifest,
): MissionStory[] { ): MissionStory[] {
@ -366,6 +378,7 @@ function getContractOpportunityData(
missionStories[ms].PreviouslyCompleted = missionStories[ms].PreviouslyCompleted =
ms in userData.Extensions.opportunityprogression ms in userData.Extensions.opportunityprogression
const current = fastClone(missionStories[ms]) const current = fastClone(missionStories[ms])
// @ts-expect-error Deal with it.
delete current.Location delete current.Location
result.push(current) result.push(current)
} }

View File

@ -38,6 +38,7 @@ import {
getUserEscalationProgress, getUserEscalationProgress,
} from "./escalations/escalationService" } from "./escalations/escalationService"
import { translateEntitlements } from "../ownership" import { translateEntitlements } from "../ownership"
import assert from "assert"
// TODO: In the near future, this file should be cleaned up where possible. // TODO: In the near future, this file should be cleaned up where possible.
@ -123,19 +124,28 @@ export function generateCompletionData(
let difficulty = undefined let difficulty = undefined
if (gameVersion === "h1") { if (gameVersion === "h1") {
difficulty = getUserData(userId, gameVersion).Extensions const userData = getUserData(userId, gameVersion)
.gamepersistentdata.menudata.difficulty.destinations[ difficulty =
subLocation ? subLocation.Properties?.ParentLocation : subLocationId userData.Extensions.gamepersistentdata.menudata.difficulty
] .destinations[
subLocation
? subLocation.Properties?.ParentLocation || ""
: subLocationId
]
} }
const locationId = subLocation const locationId = subLocation
? subLocation.Properties?.ParentLocation ? subLocation.Properties?.ParentLocation
: subLocationId : subLocationId
assert.ok(
locationId,
`Location ID is undefined for ${subLocationId} in ${gameVersion}!`,
)
const completionData = controller.masteryService.getLocationCompletion( const completionData = controller.masteryService.getLocationCompletion(
locationId, locationId,
subLocation?.Id, subLocationId,
gameVersion, gameVersion,
userId, userId,
contractType, contractType,
@ -153,7 +163,7 @@ export function generateCompletionData(
Completion: 1.0, Completion: 1.0,
XpLeft: 0, XpLeft: 0,
Id: locationId, Id: locationId,
SubLocationId: subLocation?.Id, SubLocationId: subLocationId,
HideProgression: true, HideProgression: true,
IsLocationProgression: true, IsLocationProgression: true,
Name: null, Name: null,
@ -197,7 +207,7 @@ export function generateUserCentric(
// fix h1/h2 entitlements // fix h1/h2 entitlements
contractData.Metadata.Entitlements = translateEntitlements( contractData.Metadata.Entitlements = translateEntitlements(
gameVersion, gameVersion,
contractData.Metadata.Entitlements, contractData.Metadata.Entitlements || [],
) )
} }
@ -210,6 +220,12 @@ export function generateUserCentric(
gameVersion, gameVersion,
) )
let lastPlayed: string | undefined = undefined
if (played[id]?.LastPlayedAt) {
lastPlayed = new Date(played[id].LastPlayedAt!).toISOString()
}
const uc: UserCentricContract = { const uc: UserCentricContract = {
Contract: contractData, Contract: contractData,
Data: { Data: {
@ -222,10 +238,7 @@ export function generateUserCentric(
LocationHideProgression: completionData.HideProgression, LocationHideProgression: completionData.HideProgression,
ElusiveContractState: "", ElusiveContractState: "",
IsFeatured: false, IsFeatured: false,
LastPlayedAt: LastPlayedAt: lastPlayed,
played[id] === undefined
? undefined
: new Date(played[id]?.LastPlayedAt).toISOString(),
// relevant for contracts // relevant for contracts
// Favorite contracts // Favorite contracts
PlaylistData: { PlaylistData: {
@ -316,6 +329,7 @@ export function mapObjectives(
gameChangerProps.ObjectivesCategory = (() => { gameChangerProps.ObjectivesCategory = (() => {
let obj: MissionManifestObjective let obj: MissionManifestObjective
// @ts-expect-error State machines are impossible to type
for (obj of gameChangerProps.Objectives) { for (obj of gameChangerProps.Objectives) {
if (obj.Category === "primary") return "primary" if (obj.Category === "primary") return "primary"
if (obj.Category === "secondary") if (obj.Category === "secondary")
@ -362,11 +376,9 @@ export function mapObjectives(
objective.OnActive.IfInProgress.Visible === false) || objective.OnActive.IfInProgress.Visible === false) ||
(objective.OnActive?.IfCompleted && (objective.OnActive?.IfCompleted &&
objective.OnActive.IfCompleted.Visible === false && objective.OnActive.IfCompleted.Visible === false &&
objective.Definition && // @ts-expect-error State machines are impossible to type
objective.Definition.States && objective.Definition?.States?.Start?.["-"]?.Transition ===
objective.Definition.States.Start && "Success")
objective.Definition.States.Start["-"] &&
objective.Definition.States.Start["-"].Transition === "Success")
) { ) {
continue // do not show objectives with 'ForceShowOnLoadingScreen: false' or objectives that are not visible on start continue // do not show objectives with 'ForceShowOnLoadingScreen: false' or objectives that are not visible on start
} }
@ -374,8 +386,7 @@ export function mapObjectives(
if ( if (
objective.SuccessEvent && objective.SuccessEvent &&
objective.SuccessEvent.EventName === "Kill" && objective.SuccessEvent.EventName === "Kill" &&
objective.SuccessEvent.EventValues && objective.SuccessEvent.EventValues?.RepositoryId
objective.SuccessEvent.EventValues.RepositoryId
) { ) {
result.set(objective.Id, { result.set(objective.Id, {
Type: "kill", Type: "kill",
@ -396,6 +407,7 @@ export function mapObjectives(
objective.Definition?.Context?.Targets && objective.Definition?.Context?.Targets &&
(objective.Definition.Context.Targets as string[]).length === 1 (objective.Definition.Context.Targets as string[]).length === 1
) { ) {
// @ts-expect-error State machines are impossible to type
id = objective.Definition.Context.Targets[0] id = objective.Definition.Context.Targets[0]
} }
@ -437,10 +449,8 @@ export function mapObjectives(
}) })
} else if ( } else if (
objective.Type === "statemachine" && objective.Type === "statemachine" &&
objective.Definition && (objective.Definition?.Context?.Targets as unknown[])?.length ===
objective.Definition.Context && 1 &&
objective.Definition.Context.Targets &&
(objective.Definition.Context.Targets as unknown[]).length === 1 &&
objective.HUDTemplate objective.HUDTemplate
) { ) {
// This objective will be displayed as a kill objective // This objective will be displayed as a kill objective
@ -457,6 +467,7 @@ export function mapObjectives(
result.set(objective.Id, { result.set(objective.Id, {
Type: "kill", Type: "kill",
Properties: { Properties: {
// @ts-expect-error State machines are impossible to type
Id: objective.Definition.Context.Targets[0], Id: objective.Definition.Context.Targets[0],
Conditions: Conditions, Conditions: Conditions,
}, },

View File

@ -26,7 +26,6 @@ import type {
} from "../../types/types" } from "../../types/types"
import { getUserData } from "../../databaseHandler" import { getUserData } from "../../databaseHandler"
import { log, LogLevel } from "../../loggingInterop" import { log, LogLevel } from "../../loggingInterop"
import assert from "assert"
/** /**
* Put a group id in here to hide it from the menus on 2016. * Put a group id in here to hide it from the menus on 2016.
@ -113,19 +112,25 @@ export function getLevelCount(
* @param userId The current user's ID. * @param userId The current user's ID.
* @param groupContractId The escalation's group contract ID. * @param groupContractId The escalation's group contract ID.
* @param gameVersion The game's version. * @param gameVersion The game's version.
* @returns The escalation play details. * @returns The escalation play details, or an empty object if not applicable.
*/ */
export function getPlayEscalationInfo( export function getPlayEscalationInfo(
userId: string, userId: string,
groupContractId: string, groupContractId: string | undefined | null,
gameVersion: GameVersion, gameVersion: GameVersion,
): EscalationInfo { ): EscalationInfo {
if (!groupContractId) {
return {}
}
const userData = getUserData(userId, gameVersion) const userData = getUserData(userId, gameVersion)
const p = getUserEscalationProgress(userData, groupContractId) const p = getUserEscalationProgress(userData, groupContractId)
const groupCt = controller.escalationMappings.get(groupContractId) const groupCt = controller.escalationMappings.get(groupContractId)
assert.ok(groupCt, `No escalation mapping for ${groupContractId}`) if (!groupCt) {
return {}
}
const totalLevelCount = getLevelCount( const totalLevelCount = getLevelCount(
controller.resolveContract(groupContractId), controller.resolveContract(groupContractId),

View File

@ -21,6 +21,7 @@ import {
ContractHistory, ContractHistory,
GameVersion, GameVersion,
HitsCategoryCategory, HitsCategoryCategory,
IHit,
} from "../types/types" } from "../types/types"
import { import {
contractIdToHitObject, contractIdToHitObject,
@ -35,6 +36,7 @@ import { log, LogLevel } from "../loggingInterop"
import { fastClone, getRemoteService } from "../utils" import { fastClone, getRemoteService } from "../utils"
import { orderedETAs } from "./elusiveTargetArcades" import { orderedETAs } from "./elusiveTargetArcades"
import { missionsInLocations } from "./missionsInLocation" import { missionsInLocations } from "./missionsInLocation"
import assert from "assert"
/** /**
* The filters supported for HitsCategories. * The filters supported for HitsCategories.
@ -154,11 +156,11 @@ export class HitsCategoryService {
switch (gameVersion) { switch (gameVersion) {
case "h1": case "h1":
if (contract.Metadata.Season === 1) if (contract?.Metadata.Season === 1)
contracts.push(id) contracts.push(id)
break break
case "h2": case "h2":
if (contract.Metadata.Season <= 2) if ((contract?.Metadata.Season || 0) <= 2)
contracts.push(id) contracts.push(id)
break break
default: default:
@ -232,7 +234,7 @@ export class HitsCategoryService {
.tap(tapName, (contracts, gameVersion) => { .tap(tapName, (contracts, gameVersion) => {
// We need to push Peacock contracts first to work around H2 not // We need to push Peacock contracts first to work around H2 not
// having the "order" property for $arraygroupby. (The game just crashes) // having the "order" property for $arraygroupby. (The game just crashes)
const nEscalations = [] const nEscalations: string[] = []
for (const escalations of Object.values( for (const escalations of Object.values(
missionsInLocations.escalations, missionsInLocations.escalations,
@ -240,6 +242,10 @@ export class HitsCategoryService {
for (const id of escalations) { for (const id of escalations) {
const contract = controller.resolveContract(id) const contract = controller.resolveContract(id)
if (!contract) {
continue
}
const isPeacock = contract.Metadata.Season === 0 const isPeacock = contract.Metadata.Season === 0
const season = isPeacock const season = isPeacock
? contract.Metadata.OriginalSeason ? contract.Metadata.OriginalSeason
@ -252,7 +258,7 @@ export class HitsCategoryService {
if (season === 1) contracts.push(id) if (season === 1) contracts.push(id)
break break
case "h2": case "h2":
if (season <= 2) if ((season || 0) <= 2)
(isPeacock ? contracts : nEscalations).push( (isPeacock ? contracts : nEscalations).push(
id, id,
) )
@ -273,7 +279,7 @@ export class HitsCategoryService {
pageNumber: number, pageNumber: number,
gameVersion: GameVersion, gameVersion: GameVersion,
userId: string, userId: string,
): Promise<HitsCategoryCategory> { ): Promise<HitsCategoryCategory | undefined> {
const remoteService = getRemoteService(gameVersion) const remoteService = getRemoteService(gameVersion)
const user = userAuths.get(userId) const user = userAuths.get(userId)
@ -289,10 +295,12 @@ export class HitsCategoryService {
true, true,
) )
const hits = resp.data.data.Data.Hits const hits = resp.data.data.Data.Hits
preserveContracts( void preserveContracts(
hits.map( hits
(hit) => hit.UserCentricContract.Contract.Metadata.PublicId, .map(
), (hit) => hit.UserCentricContract.Contract.Metadata.PublicId,
)
.filter(Boolean) as string[],
) )
// Fix completion and favorite status for retrieved contracts // Fix completion and favorite status for retrieved contracts
@ -304,7 +312,7 @@ export class HitsCategoryService {
if (Object.keys(played).includes(hit.Id)) { if (Object.keys(played).includes(hit.Id)) {
// Replace with data stored by Peacock // Replace with data stored by Peacock
hit.UserCentricContract.Data.LastPlayedAt = new Date( hit.UserCentricContract.Data.LastPlayedAt = new Date(
played[hit.Id].LastPlayedAt, played[hit.Id].LastPlayedAt || 0,
).toISOString() ).toISOString()
hit.UserCentricContract.Data.Completed = hit.UserCentricContract.Data.Completed =
played[hit.Id].Completed played[hit.Id].Completed
@ -314,8 +322,10 @@ export class HitsCategoryService {
hit.UserCentricContract.Data.Completed = false hit.UserCentricContract.Data.Completed = false
} }
hit.UserCentricContract.Data.PlaylistData.IsAdded = if (hit.UserCentricContract.Data.PlaylistData) {
favorites.includes(hit.Id) hit.UserCentricContract.Data.PlaylistData.IsAdded =
favorites.includes(hit.Id)
}
} }
return resp.data.data return resp.data.data
@ -378,13 +388,23 @@ export class HitsCategoryService {
type: ContractFilter, type: ContractFilter,
category: string, category: string,
): string | undefined { ): string | undefined {
if (!this.filterSupported.includes(category)) return undefined if (!this.filterSupported.includes(category)) {
return undefined
}
const user = getUserData(userId, gameVersion) const user = getUserData(userId, gameVersion)
type Cast =
keyof typeof user.Extensions.gamepersistentdata.HitsFilterType
if (type === "default") { if (type === "default") {
type = user.Extensions.gamepersistentdata.HitsFilterType[category] type =
user.Extensions.gamepersistentdata.HitsFilterType[
category as Cast
]
} else { } else {
user.Extensions.gamepersistentdata.HitsFilterType[category] = type user.Extensions.gamepersistentdata.HitsFilterType[
category as Cast
] = type
writeUserData(userId, gameVersion) writeUserData(userId, gameVersion)
} }
@ -408,7 +428,10 @@ export class HitsCategoryService {
return Object.keys(played) return Object.keys(played)
.filter((id) => this.isContractOfType(played, type, id)) .filter((id) => this.isContractOfType(played, type, id))
.sort((a, b) => { .sort((a, b) => {
return played[b].LastPlayedAt - played[a].LastPlayedAt return (
(played[b].LastPlayedAt || 0) -
(played[a].LastPlayedAt || 0)
)
}) })
} }
@ -428,9 +451,9 @@ export class HitsCategoryService {
): boolean { ): boolean {
switch (type) { switch (type) {
case "completed": case "completed":
return ( return Boolean(
played[contractId]?.Completed && played[contractId]?.Completed &&
!played[contractId]?.IsEscalation !played[contractId]?.IsEscalation,
) )
case "failed": case "failed":
return ( return (
@ -440,6 +463,8 @@ export class HitsCategoryService {
) )
case "all": case "all":
return !played[contractId]?.IsEscalation return !played[contractId]?.IsEscalation
default:
assert.fail("Invalid type passed to isContractOfType")
} }
} }
@ -457,7 +482,7 @@ export class HitsCategoryService {
pageNumber: number, pageNumber: number,
gameVersion: GameVersion, gameVersion: GameVersion,
userId: string, userId: string,
): Promise<HitsCategoryCategory> { ): Promise<HitsCategoryCategory | undefined> {
if (this.realtimeFetched.includes(categoryName)) { if (this.realtimeFetched.includes(categoryName)) {
return await this.fetchFromOfficial( return await this.fetchFromOfficial(
categoryName, categoryName,
@ -479,7 +504,7 @@ export class HitsCategoryService {
userId, userId,
filter, filter,
category, category,
) )!
const hitsCategory: HitsCategoryCategory = { const hitsCategory: HitsCategoryCategory = {
Category: category, Category: category,
@ -489,7 +514,7 @@ export class HitsCategoryService {
Page: pageNumber, Page: pageNumber,
HasMore: false, HasMore: false,
}, },
CurrentSubType: undefined, CurrentSubType: "",
} }
const hook = this.hitsCategories.for(category) const hook = this.hitsCategories.for(category)
@ -500,7 +525,7 @@ export class HitsCategoryService {
const hitObjectList = hits const hitObjectList = hits
.map((id) => contractIdToHitObject(id, gameVersion, userId)) .map((id) => contractIdToHitObject(id, gameVersion, userId))
.filter(Boolean) .filter(Boolean) as IHit[]
if (!this.paginationExempt.includes(category)) { if (!this.paginationExempt.includes(category)) {
const paginated = paginate(hitObjectList, this.hitsPerPage) const paginated = paginate(hitObjectList, this.hitsPerPage)

View File

@ -53,9 +53,15 @@ export async function getLeaderboardEntries(
platform: JwtData["platform"], platform: JwtData["platform"],
gameVersion: GameVersion, gameVersion: GameVersion,
difficultyLevel?: string, difficultyLevel?: string,
): Promise<GameFacingLeaderboardData> { ): Promise<GameFacingLeaderboardData | undefined> {
let difficulty = "unset" let difficulty = "unset"
const contract = controller.resolveContract(contractId)
if (!contract) {
return undefined
}
const parsedDifficulty = parseInt(difficultyLevel || "0") const parsedDifficulty = parseInt(difficultyLevel || "0")
if (parsedDifficulty === gameDifficulty.casual) { if (parsedDifficulty === gameDifficulty.casual) {
@ -72,7 +78,7 @@ export async function getLeaderboardEntries(
const response: GameFacingLeaderboardData = { const response: GameFacingLeaderboardData = {
Entries: [], Entries: [],
Contract: controller.resolveContract(contractId), Contract: contract,
Page: 0, Page: 0,
HasMore: false, HasMore: false,
LeaderboardType: "singleplayer", LeaderboardType: "singleplayer",

View File

@ -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 }

View File

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import type { ContractSession } from "./types/types" import { ContractSession } from "../types/types"
/** /**
* Changes a set to an array. * Changes a set to an array.
@ -60,23 +60,28 @@ const SESSION_MAP_PROPS: (keyof ContractSession)[] = [
* @param session The ContractSession. * @param session The ContractSession.
*/ */
export function serializeSession(session: ContractSession): unknown { export function serializeSession(session: ContractSession): unknown {
const o = {} const o: Partial<ContractSession> = {}
type K = keyof ContractSession
// obj clone // obj clone
for (const key of Object.keys(session)) { for (const key of Object.keys(session)) {
if (session[key] instanceof Map) { if (session[key as K] instanceof Map) {
// @ts-expect-error Type mismatch.
o[key] = Array.from( o[key] = Array.from(
(session[key] as Map<string, unknown>).entries(), (session[key as K] as Map<string, unknown>).entries(),
) )
continue continue
} }
if (session[key] instanceof Set) { if (session[key as K] instanceof Set) {
// @ts-expect-error Type mismatch.
o[key] = normalizeSet(session[key]) o[key] = normalizeSet(session[key])
continue continue
} }
o[key] = session[key] // @ts-expect-error Type mismatch.
o[key] = session[key as K]
} }
return o return o
@ -90,19 +95,22 @@ export function serializeSession(session: ContractSession): unknown {
export function deserializeSession( export function deserializeSession(
saved: Record<string, unknown>, saved: Record<string, unknown>,
): ContractSession { ): ContractSession {
const session = {} const session: Partial<ContractSession> = {}
// obj clone // obj clone
for (const key of Object.keys(saved)) { for (const key of Object.keys(saved)) {
// @ts-expect-error Type mismatch.
session[key] = saved[key] session[key] = saved[key]
} }
for (const collection of SESSION_SET_PROPS) { for (const collection of SESSION_SET_PROPS) {
// @ts-expect-error Type mismatch.
session[collection] = new Set(session[collection]) session[collection] = new Set(session[collection])
} }
for (const map of SESSION_MAP_PROPS) { for (const map of SESSION_MAP_PROPS) {
if (Object.hasOwn(session, map)) { if (Object.hasOwn(session, map)) {
// @ts-expect-error Type mismatch.
session[map] = new Map(session[map]) session[map] = new Map(session[map])
} }
} }

View File

@ -188,8 +188,10 @@ function createPeacockRequire(pluginName: string): NodeRequire {
* @param specifier The requested module. * @param specifier The requested module.
*/ */
const peacockRequire: NodeRequire = (specifier: string) => { const peacockRequire: NodeRequire = (specifier: string) => {
if (generatedPeacockRequireTable[specifier]) { type T = keyof typeof generatedPeacockRequireTable
return generatedPeacockRequireTable[specifier]
if (generatedPeacockRequireTable[specifier as T]) {
return generatedPeacockRequireTable[specifier as T]
} }
try { try {
@ -237,14 +239,22 @@ export const validateMission = (m: MissionManifest): boolean => {
return false return false
} }
for (const prop of ["Id", "Title", "Location", "ScenePath"]) { for (const prop of <(keyof MissionManifest["Metadata"])[]>[
if (!Object.hasOwn(m.Metadata, prop)) { "Id",
"Title",
"Location",
"ScenePath",
]) {
if (!m.Metadata[prop]) {
log(LogLevel.ERROR, `Contract missing property Metadata.${prop}!`) log(LogLevel.ERROR, `Contract missing property Metadata.${prop}!`)
return false return false
} }
} }
for (const prop of ["Objectives", "Bricks"]) { for (const prop of <(keyof MissionManifest["Data"])[]>[
"Objectives",
"Bricks",
]) {
if (!Object.hasOwn(m.Data, prop)) { if (!Object.hasOwn(m.Data, prop)) {
log(LogLevel.ERROR, `Contract missing property Data.${prop}!`) log(LogLevel.ERROR, `Contract missing property Data.${prop}!`)
return false return false
@ -365,11 +375,11 @@ export class Controller {
*/ */
public fetchedContracts: Map<string, MissionManifest> = new Map() public fetchedContracts: Map<string, MissionManifest> = new Map()
public challengeService: ChallengeService public challengeService!: ChallengeService
public masteryService: MasteryService public masteryService!: MasteryService
escalationMappings: Map<string, Record<string, string>> = new Map() escalationMappings: Map<string, Record<string, string>> = new Map()
public progressionService: ProgressionService public progressionService!: ProgressionService
public smf: SMFSupport public smf!: SMFSupport
private _pubIdToContractId: Map<string, string> = new Map() private _pubIdToContractId: Map<string, string> = new Map()
/** Internal elusive target contracts - only accessible during bootstrap. */ /** Internal elusive target contracts - only accessible during bootstrap. */
private _internalElusives: MissionManifest[] | undefined private _internalElusives: MissionManifest[] | undefined
@ -444,12 +454,8 @@ export class Controller {
this.hooks.challengesLoaded.call() this.hooks.challengesLoaded.call()
this.hooks.masteryDataLoaded.call() this.hooks.masteryDataLoaded.call()
} catch (e) { } catch (e) {
log( log(LogLevel.ERROR, `Fatal error with challenge bootstrap`, "boot")
LogLevel.ERROR, log(LogLevel.ERROR, e)
`Fatal error with challenge bootstrap: ${e}`,
"boot",
)
log(LogLevel.ERROR, e.stack)
} }
} }
@ -461,6 +467,11 @@ export class Controller {
continue continue
} }
assert.ok(
contract.Metadata.GroupDefinition,
"arcade contract has no group definition",
)
for (const lId of contract.Metadata.GroupDefinition.Order) { for (const lId of contract.Metadata.GroupDefinition.Order) {
const level = this.resolveContract(lId, false) const level = this.resolveContract(lId, false)
@ -481,9 +492,10 @@ export class Controller {
) )
for (const location of this.locationsWithETA) { for (const location of this.locationsWithETA) {
this.parentsWithETA.add( const pl = locations.children[location].Properties.ParentLocation
locations.children[location].Properties.ParentLocation, assert.ok(pl, "no parent location")
)
this.parentsWithETA.add(pl)
} }
} }
@ -509,14 +521,7 @@ export class Controller {
) )
} }
if (this.contracts.has(this._pubIdToContractId.get(pubId)!)) { return this.contracts.get(this._pubIdToContractId.get(pubId)!)
return (
this.contracts.get(this._pubIdToContractId.get(pubId)!) ||
undefined
)
}
return undefined
} }
/** /**
@ -546,6 +551,10 @@ export class Controller {
private getGroupContract(json: MissionManifest) { private getGroupContract(json: MissionManifest) {
if (escalationTypes.includes(json.Metadata.Type)) { if (escalationTypes.includes(json.Metadata.Type)) {
if (!json.Metadata.InGroup) {
return json
}
return this.resolveContract(json.Metadata.InGroup) ?? json return this.resolveContract(json.Metadata.InGroup) ?? json
} }
@ -565,7 +574,7 @@ export class Controller {
* @returns The mission manifest object, or undefined if it wasn't found. * @returns The mission manifest object, or undefined if it wasn't found.
*/ */
public resolveContract( public resolveContract(
id: string, id: string | undefined,
getGroup = false, getGroup = false,
): MissionManifest | undefined { ): MissionManifest | undefined {
if (!id) { if (!id) {
@ -650,11 +659,16 @@ export class Controller {
this.addMission(groupContract) this.addMission(groupContract)
fixedLevels.forEach((level) => this.addMission(level)) fixedLevels.forEach((level) => this.addMission(level))
this.missionsInLocations.escalations[locationId] ??= [] type K = keyof typeof this.missionsInLocations.escalations
this.missionsInLocations.escalations[locationId].push( // eslint-disable-next-line @typescript-eslint/no-explicit-any
groupContract.Metadata.Id, this.missionsInLocations.escalations[locationId as K] ??= <any>[]
)
const a = this.missionsInLocations.escalations[
locationId as K
] as string[]
a.push(groupContract.Metadata.Id)
this.scanForGroups() this.scanForGroups()
} }
@ -758,7 +772,6 @@ export class Controller {
} }
} catch (e) { } catch (e) {
log(LogLevel.ERROR, `Failed to load contract ${i}!`) log(LogLevel.ERROR, `Failed to load contract ${i}!`)
log(LogLevel.ERROR, e.stack)
} }
}) })
} }
@ -1028,7 +1041,6 @@ export class Controller {
let theExports let theExports
try { try {
// eslint-disable-next-line prefer-const
theExports = new Script(pluginContents, { theExports = new Script(pluginContents, {
filename: pluginPath, filename: pluginPath,
}).runInContext(context) }).runInContext(context)
@ -1038,7 +1050,6 @@ export class Controller {
`Error while attempting to queue plugin ${pluginName} for loading!`, `Error while attempting to queue plugin ${pluginName} for loading!`,
) )
log(LogLevel.ERROR, e) log(LogLevel.ERROR, e)
log(LogLevel.ERROR, e.stack)
return return
} }
@ -1057,7 +1068,6 @@ export class Controller {
} catch (e) { } catch (e) {
log(LogLevel.ERROR, `Error while evaluating plugin ${pluginName}!`) log(LogLevel.ERROR, `Error while evaluating plugin ${pluginName}!`)
log(LogLevel.ERROR, e) log(LogLevel.ERROR, e)
log(LogLevel.ERROR, e.stack)
} }
} }
@ -1185,7 +1195,7 @@ export function contractIdToHitObject(
"LocationsData", "LocationsData",
gameVersion, gameVersion,
false, false,
).parents[subLocation?.Properties?.ParentLocation] ).parents[subLocation?.Properties?.ParentLocation || ""]
// failed to find the location, must be from a newer game // failed to find the location, must be from a newer game
if (!subLocation && ["h1", "h2", "scpc"].includes(gameVersion)) { if (!subLocation && ["h1", "h2", "scpc"].includes(gameVersion)) {

View File

@ -19,7 +19,7 @@
import { readFile, writeFile } from "atomically" import { readFile, writeFile } from "atomically"
import { join } from "path" import { join } from "path"
import type { ContractSession, GameVersion, UserProfile } from "./types/types" import type { ContractSession, GameVersion, UserProfile } from "./types/types"
import { serializeSession, deserializeSession } from "./sessionSerialization" import { serializeSession, deserializeSession } from "./contracts/sessions"
import { castUserProfile } from "./utils" import { castUserProfile } from "./utils"
import { log, LogLevel } from "./loggingInterop" import { log, LogLevel } from "./loggingInterop"
import { unlink, readdir } from "fs/promises" import { unlink, readdir } from "fs/promises"

View File

@ -16,6 +16,10 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
// Vendor code - does not need type-checking.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck
import EventEmitter from "events" import EventEmitter from "events"
import { clearTimeout, setTimeout } from "timers" import { clearTimeout, setTimeout } from "timers"
import { IPCTransport } from "./ipc" import { IPCTransport } from "./ipc"

View File

@ -16,6 +16,10 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
// Vendor code - does not need type-checking.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck
import net from "net" import net from "net"
import EventEmitter from "events" import EventEmitter from "events"
import axios from "axios" import axios from "axios"

View File

@ -67,7 +67,7 @@ export class IOIStrategy extends EntitlementStrategy {
constructor(gameVersion: GameVersion, private readonly issuerId: string) { constructor(gameVersion: GameVersion, private readonly issuerId: string) {
super() super()
this.issuerId = issuerId this.issuerId = issuerId
this._remoteService = getRemoteService(gameVersion) this._remoteService = getRemoteService(gameVersion)!
} }
override async get(userId: string) { override async get(userId: string) {

View File

@ -174,8 +174,8 @@ export function setupScoring(
if (name === "scoring") { if (name === "scoring") {
const definition: ManifestScoringDefinition = deepmerge( const definition: ManifestScoringDefinition = deepmerge(
...module.ScoringDefinitions, ...(module.ScoringDefinitions || []),
) ) as unknown as ManifestScoringDefinition
let state = "Start" let state = "Start"
let context = definition.Context let context = definition.Context
@ -200,15 +200,21 @@ export function setupScoring(
context = immediate.context context = immediate.context
} }
// @ts-expect-error Type issue
scoring.Definition = definition scoring.Definition = definition
// @ts-expect-error Type issue
scoring.Context = context scoring.Context = context
// @ts-expect-error Type issue
scoring.State = state scoring.State = state
} else { } else {
// @ts-expect-error Type issue
scoring.Settings[name] = module scoring.Settings[name] = module
// @ts-expect-error Type issue
delete scoring.Settings[name]["Type"] delete scoring.Settings[name]["Type"]
} }
} }
// @ts-expect-error Type issue
session.scoring = scoring session.scoring = scoring
} }
@ -373,13 +379,13 @@ export function newSession(
} }
export type SSE3Response = { export type SSE3Response = {
SavedTokens: string[] SavedTokens: string[] | null
NewEvents: ServerToClientEvent[] NewEvents: ServerToClientEvent[] | null
NextPoll: number NextPoll: number
} }
export type SSE4Response = SSE3Response & { export type SSE4Response = SSE3Response & {
PushMessages: string[] PushMessages: string[] | null
} }
export function saveAndSyncEvents( export function saveAndSyncEvents(
@ -410,7 +416,7 @@ export function saveAndSyncEvents(
let pushMessages: string[] | undefined let pushMessages: string[] | undefined
if ((userPushQueue = pushMessageQueue.get(userId))) { if ((userPushQueue = pushMessageQueue.get(userId))) {
userPushQueue = userPushQueue.filter((item) => item.time > lastPushDt) userPushQueue = userPushQueue.filter((item) => item.time > lastPushDt!)
pushMessageQueue.set(userId, userPushQueue) pushMessageQueue.set(userId, userPushQueue)
pushMessages = Array.from(userPushQueue, (item) => item.message) pushMessages = Array.from(userPushQueue, (item) => item.message)
@ -430,20 +436,21 @@ export function saveAndSyncEvents(
} }
} }
type SSE3Body = {
lastEventTicks: number | string
userId: string
values: ClientToServerEvent[]
}
type SSE4Body = SSE3Body & {
lastPushDt: number | string
}
eventRouter.post( eventRouter.post(
"/SaveAndSynchronizeEvents3", "/SaveAndSynchronizeEvents3",
jsonMiddleware({ limit: "10Mb" }), jsonMiddleware({ limit: "10Mb" }),
( // @ts-expect-error Request has jwt props.
req: RequestWithJwt< (req: RequestWithJwt<unknown, SSE3Body>, res) => {
unknown,
{
lastEventTicks: number | string
userId: string
values: ClientToServerEvent[]
}
>,
res,
) => {
if (req.body.userId !== req.jwt.unique_name) { if (req.body.userId !== req.jwt.unique_name) {
res.status(403).send() // Trying to save events for other user res.status(403).send() // Trying to save events for other user
return return
@ -469,18 +476,8 @@ eventRouter.post(
eventRouter.post( eventRouter.post(
"/SaveAndSynchronizeEvents4", "/SaveAndSynchronizeEvents4",
jsonMiddleware({ limit: "10Mb" }), jsonMiddleware({ limit: "10Mb" }),
( // @ts-expect-error Request has jwt props.
req: RequestWithJwt< (req: RequestWithJwt<unknown, SSE4Body>, res) => {
unknown,
{
lastPushDt: number | string
lastEventTicks: number | string
userId: string
values: ClientToServerEvent[]
}
>,
res,
) => {
if (req.body.userId !== req.jwt.unique_name) { if (req.body.userId !== req.jwt.unique_name) {
res.status(403).send() // Trying to save events for other user res.status(403).send() // Trying to save events for other user
return return
@ -507,6 +504,7 @@ eventRouter.post(
eventRouter.post( eventRouter.post(
"/SaveEvents2", "/SaveEvents2",
jsonMiddleware({ limit: "10Mb" }), jsonMiddleware({ limit: "10Mb" }),
// @ts-expect-error Request has jwt props.
(req: RequestWithJwt, res) => { (req: RequestWithJwt, res) => {
if (req.jwt.unique_name !== req.body.userId) { if (req.jwt.unique_name !== req.body.userId) {
res.status(403).send() // Trying to save events for other user res.status(403).send() // Trying to save events for other user
@ -595,7 +593,7 @@ function contractFailed(
) { ) {
if (session.completedObjectives.size === 0) break arcadeFail if (session.completedObjectives.size === 0) break arcadeFail
for (const obj of json.Data.Objectives) { for (const obj of json.Data.Objectives || []) {
if ( if (
session.completedObjectives.has(obj.Id) && session.completedObjectives.has(obj.Id) &&
obj.Category === "primary" obj.Category === "primary"
@ -707,7 +705,9 @@ function saveEvents(
const val = handleEvent( const val = handleEvent(
objectiveDefinition as never, objectiveDefinition as never,
objectiveContext, // SMP sucks. Sorry, not sorry.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
objectiveContext as any,
event.Value, event.Value,
{ {
eventName: event.Name, eventName: event.Name,
@ -734,7 +734,6 @@ function saveEvents(
"An error occurred while tracing C2S events, please report this!", "An error occurred while tracing C2S events, please report this!",
) )
log(LogLevel.ERROR, e) log(LogLevel.ERROR, e)
log(LogLevel.ERROR, e.stack)
} }
} }
@ -744,7 +743,9 @@ function saveEvents(
const val = handleEvent( const val = handleEvent(
session.scoring.Definition as never, session.scoring.Definition as never,
scoringContext, // SMP sucks. Sorry, not sorry.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
scoringContext as any,
event.Value, event.Value,
{ {
eventName: event.Name, eventName: event.Name,
@ -762,7 +763,10 @@ function saveEvents(
controller.challengeService.onContractEvent(event, session) controller.challengeService.onContractEvent(event, session)
if (event.Name.startsWith("ScoringScreenEndState_")) { if (
event.Name.startsWith("ScoringScreenEndState_") &&
session.evergreen
) {
session.evergreen.scoringScreenEndState = event.Name session.evergreen.scoringScreenEndState = event.Name
processed.push(event.Name) processed.push(event.Name)
@ -1010,7 +1014,10 @@ function saveEvents(
const areaId = (<AreaDiscoveredC2SEvent>event).Value const areaId = (<AreaDiscoveredC2SEvent>event).Value
.RepositoryId .RepositoryId
const challengeId = getConfig("AreaMap", false)[areaId] const challengeId = getConfig<Record<string, string>>(
"AreaMap",
false,
)[areaId]
const progress = userData.Extensions.ChallengeProgression const progress = userData.Extensions.ChallengeProgression
log(LogLevel.DEBUG, `Area discovered: ${areaId}`) log(LogLevel.DEBUG, `Area discovered: ${areaId}`)
@ -1044,16 +1051,22 @@ function saveEvents(
break break
// Evergreen // Evergreen
case "CpdSet": case "CpdSet":
setCpd( if (contract?.Metadata.CpdId) {
event.Value as ContractProgressionData, setCpd(
userId, event.Value as ContractProgressionData,
contract.Metadata.CpdId, userId,
) contract.Metadata.CpdId,
)
}
break break
case "Evergreen_Payout_Data": case "Evergreen_Payout_Data":
session.evergreen.payout = (<Evergreen_Payout_DataC2SEvent>( if (session.evergreen) {
event session.evergreen.payout = (<Evergreen_Payout_DataC2SEvent>(
)).Value.Total_Payout event
)).Value.Total_Payout
}
break break
case "MissionFailed_Event": case "MissionFailed_Event":
if (session.evergreen) { if (session.evergreen) {

View File

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "fs" import { existsSync, readFileSync, writeFileSync } from "fs"
import type { Flags } from "./types/types" import type { Flags } from "./types/types"
import { log, LogLevel } from "./loggingInterop" import { log, LogLevel } from "./loggingInterop"
import { parse } from "js-ini" import { parse } from "js-ini"
@ -123,7 +123,6 @@ const defaultFlags: Flags = {
}, },
} }
const OLD_FLAGS_FILE = "flags.json5"
const NEW_FLAGS_FILE = "options.ini" const NEW_FLAGS_FILE = "options.ini"
/** /**
@ -152,7 +151,10 @@ const makeFlagsIni = (
Object.keys(defaultFlags) Object.keys(defaultFlags)
.map((flagId) => { .map((flagId) => {
return `; ${defaultFlags[flagId].desc} return `; ${defaultFlags[flagId].desc}
${flagId} = ${_flags[flagId]}` ${flagId} = ${
// @ts-expect-error You know what, I don't care
_flags[flagId]
}`
}) })
.join("\n\n") .join("\n\n")
@ -160,24 +162,11 @@ ${flagId} = ${_flags[flagId]}`
* Loads all flags. * Loads all flags.
*/ */
export function loadFlags(): void { export function loadFlags(): void {
// somebody please, clean this method up, I hate it
if (existsSync(OLD_FLAGS_FILE)) {
log(
LogLevel.WARN,
"The flags file (flags.json5) has been revamped in the latest Peacock version, and we had to remove your settings.",
)
log(
LogLevel.INFO,
"You can take a look at the new options.ini file, which includes descriptions and more!",
)
unlinkSync(OLD_FLAGS_FILE)
}
if (!existsSync(NEW_FLAGS_FILE)) { if (!existsSync(NEW_FLAGS_FILE)) {
const allTheFlags = {} const allTheFlags = {}
Object.keys(defaultFlags).forEach((f) => { Object.keys(defaultFlags).forEach((f) => {
// @ts-expect-error You know what, I don't care
allTheFlags[f] = defaultFlags[f].default allTheFlags[f] = defaultFlags[f].default
}) })

View File

@ -37,13 +37,11 @@ import * as platformEntitlements from "./platformEntitlements"
import * as playStyles from "./playStyles" import * as playStyles from "./playStyles"
import * as profileHandler from "./profileHandler" import * as profileHandler from "./profileHandler"
import * as scoreHandler from "./scoreHandler" import * as scoreHandler from "./scoreHandler"
import * as sessionSerialization from "./sessionSerialization"
import * as smfSupport from "./smfSupport" import * as smfSupport from "./smfSupport"
import * as utils from "./utils" import * as utils from "./utils"
import * as webFeatures from "./webFeatures" import * as webFeatures from "./webFeatures"
import * as legacyContractHandler from "./2016/legacyContractHandler" import * as legacyContractHandler from "./2016/legacyContractHandler"
import * as legacyMenuData from "./2016/legacyMenuData" import * as legacyMenuData from "./2016/legacyMenuData"
import * as legacyMenuSystem from "./2016/legacyMenuSystem"
import * as legacyProfileRouter from "./2016/legacyProfileRouter" import * as legacyProfileRouter from "./2016/legacyProfileRouter"
import * as challengeHelpers from "./candle/challengeHelpers" import * as challengeHelpers from "./candle/challengeHelpers"
import * as challengeService from "./candle/challengeService" import * as challengeService from "./candle/challengeService"
@ -57,7 +55,7 @@ import * as elusiveTargets from "./contracts/elusiveTargets"
import * as hitsCategoryService from "./contracts/hitsCategoryService" import * as hitsCategoryService from "./contracts/hitsCategoryService"
import * as leaderboards from "./contracts/leaderboards" import * as leaderboards from "./contracts/leaderboards"
import * as missionsInLocation from "./contracts/missionsInLocation" import * as missionsInLocation from "./contracts/missionsInLocation"
import * as reportRouting from "./contracts/reportRouting" import * as sessions from "./contracts/sessions"
import * as client from "./discord/client" import * as client from "./discord/client"
import * as ipc from "./discord/ipc" import * as ipc from "./discord/ipc"
import * as liveSplitClient from "./livesplit/liveSplitClient" import * as liveSplitClient from "./livesplit/liveSplitClient"
@ -69,6 +67,7 @@ import * as hub from "./menus/hub"
import * as imageHandler from "./menus/imageHandler" import * as imageHandler from "./menus/imageHandler"
import * as menuSystem from "./menus/menuSystem" import * as menuSystem from "./menus/menuSystem"
import * as planning from "./menus/planning" import * as planning from "./menus/planning"
import * as playerProfile from "./menus/playerProfile"
import * as playnext from "./menus/playnext" import * as playnext from "./menus/playnext"
import * as sniper from "./menus/sniper" import * as sniper from "./menus/sniper"
import * as stashpoints from "./menus/stashpoints" import * as stashpoints from "./menus/stashpoints"
@ -125,10 +124,6 @@ export default {
...profileHandler, ...profileHandler,
}, },
"@peacockproject/core/scoreHandler": { __esModule: true, ...scoreHandler }, "@peacockproject/core/scoreHandler": { __esModule: true, ...scoreHandler },
"@peacockproject/core/sessionSerialization": {
__esModule: true,
...sessionSerialization,
},
"@peacockproject/core/smfSupport": { __esModule: true, ...smfSupport }, "@peacockproject/core/smfSupport": { __esModule: true, ...smfSupport },
"@peacockproject/core/utils": { __esModule: true, ...utils }, "@peacockproject/core/utils": { __esModule: true, ...utils },
"@peacockproject/core/webFeatures": { __esModule: true, ...webFeatures }, "@peacockproject/core/webFeatures": { __esModule: true, ...webFeatures },
@ -140,10 +135,6 @@ export default {
__esModule: true, __esModule: true,
...legacyMenuData, ...legacyMenuData,
}, },
"@peacockproject/core/2016/legacyMenuSystem": {
__esModule: true,
...legacyMenuSystem,
},
"@peacockproject/core/2016/legacyProfileRouter": { "@peacockproject/core/2016/legacyProfileRouter": {
__esModule: true, __esModule: true,
...legacyProfileRouter, ...legacyProfileRouter,
@ -193,9 +184,9 @@ export default {
__esModule: true, __esModule: true,
...missionsInLocation, ...missionsInLocation,
}, },
"@peacockproject/core/contracts/reportRouting": { "@peacockproject/core/contracts/sessions": {
__esModule: true, __esModule: true,
...reportRouting, ...sessions,
}, },
"@peacockproject/core/discord/client": { __esModule: true, ...client }, "@peacockproject/core/discord/client": { __esModule: true, ...client },
"@peacockproject/core/discord/ipc": { __esModule: true, ...ipc }, "@peacockproject/core/discord/ipc": { __esModule: true, ...ipc },
@ -226,6 +217,10 @@ export default {
...menuSystem, ...menuSystem,
}, },
"@peacockproject/core/menus/planning": { __esModule: true, ...planning }, "@peacockproject/core/menus/planning": { __esModule: true, ...planning },
"@peacockproject/core/menus/playerProfile": {
__esModule: true,
...playerProfile,
},
"@peacockproject/core/menus/playnext": { __esModule: true, ...playnext }, "@peacockproject/core/menus/playnext": { __esModule: true, ...playnext },
"@peacockproject/core/menus/sniper": { __esModule: true, ...sniper }, "@peacockproject/core/menus/sniper": { __esModule: true, ...sniper },
"@peacockproject/core/menus/stashpoints": { "@peacockproject/core/menus/stashpoints": {

View File

@ -89,7 +89,7 @@ export interface Intercept<Params, Return> {
* @param context The context object. Can be modified. * @param context The context object. Can be modified.
* @param params The parameters that the taps will get. Can be modified. * @param params The parameters that the taps will get. Can be modified.
*/ */
call(context, ...params: AsArray<Params>): void call(context: unknown, ...params: AsArray<Params>): void | Promise<void>
/** /**
* A function called when the hook is tapped. Note that it will not be called when an interceptor is registered, since that doesn't count as a tap. * A function called when the hook is tapped. Note that it will not be called when an interceptor is registered, since that doesn't count as a tap.
@ -108,8 +108,8 @@ export interface Intercept<Params, Return> {
* @see AsyncSeriesHook * @see AsyncSeriesHook
*/ */
export abstract class BaseImpl<Params, Return = void> { export abstract class BaseImpl<Params, Return = void> {
protected _intercepts: Intercept<Params, Return>[] protected _intercepts!: Intercept<Params, Return>[]
protected _taps: Tap<Params, Return>[] protected _taps!: Tap<Params, Return>[]
/** /**
* Register an interceptor. * Register an interceptor.

View File

@ -16,8 +16,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
// noinspection RequiredAttributes
// load as soon as possible to prevent dependency issues // load as soon as possible to prevent dependency issues
import "./generatedPeacockRequireTable" import "./generatedPeacockRequireTable"
@ -26,7 +24,6 @@ import { getFlag, loadFlags } from "./flags"
loadFlags() loadFlags()
import { setFlagsFromString } from "v8"
import { program } from "commander" import { program } from "commander"
import express, { Request, Router } from "express" import express, { Request, Router } from "express"
import http from "http" import http from "http"
@ -41,7 +38,12 @@ import {
ServerVer, ServerVer,
} from "./utils" } from "./utils"
import { getConfig } from "./configSwizzleManager" import { getConfig } from "./configSwizzleManager"
import { handleOauthToken } from "./oauthToken" import {
error400,
error406,
handleOAuthToken,
OAuthTokenBody,
} from "./oauthToken"
import type { import type {
RequestWithJwt, RequestWithJwt,
S2CEventWithTimestamp, S2CEventWithTimestamp,
@ -61,7 +63,6 @@ import { contractRoutingRouter } from "./contracts/contractRouting"
import { profileRouter } from "./profileHandler" import { profileRouter } from "./profileHandler"
import { menuDataRouter } from "./menuData" import { menuDataRouter } from "./menuData"
import { menuSystemPreRouter, menuSystemRouter } from "./menus/menuSystem" import { menuSystemPreRouter, menuSystemRouter } from "./menus/menuSystem"
import { legacyMenuSystemRouter } from "./2016/legacyMenuSystem"
import { _theLastYardbirdScpc, controller } from "./controller" import { _theLastYardbirdScpc, controller } from "./controller"
import { import {
STEAM_NAMESPACE_2016, STEAM_NAMESPACE_2016,
@ -88,10 +89,8 @@ import { multiplayerMenuDataRouter } from "./multiplayer/multiplayerMenuData"
import { pack, unpack } from "msgpackr" import { pack, unpack } from "msgpackr"
import { liveSplitManager } from "./livesplit/liveSplitManager" import { liveSplitManager } from "./livesplit/liveSplitManager"
import { cheapLoadUserData } from "./databaseHandler" import { cheapLoadUserData } from "./databaseHandler"
import { reportRouter } from "./contracts/reportRouting"
// welcome to the bleeding edge loadFlags()
setFlagsFromString("--harmony")
const host = process.env.HOST || "0.0.0.0" const host = process.env.HOST || "0.0.0.0"
const port = process.env.PORT || 80 const port = process.env.PORT || 80
@ -145,12 +144,13 @@ app.get("/", (_: Request, res) => {
res.send( res.send(
'<html lang="en">PEACOCK_DEV active, please run "yarn webui start" to start the web UI on port 3000 and access it there.</html>', '<html lang="en">PEACOCK_DEV active, please run "yarn webui start" to start the web UI on port 3000 and access it there.</html>',
) )
} else { return
const data = readFileSync("webui/dist/index.html").toString()
res.contentType("text/html")
res.send(data)
} }
const data = readFileSync("webui/dist/index.html").toString()
res.contentType("text/html")
res.send(data)
}) })
serveStatic.mime.define({ "application/javascript": ["js"] }) serveStatic.mime.define({ "application/javascript": ["js"] })
@ -169,6 +169,7 @@ if (getFlag("loadoutSaving") === "PROFILES") {
app.get( app.get(
"/config/:audience/:serverVersion(\\d+_\\d+_\\d+)", "/config/:audience/:serverVersion(\\d+_\\d+_\\d+)",
// @ts-expect-error Has jwt props.
(req: RequestWithJwt<{ issuer: string }>, res) => { (req: RequestWithJwt<{ issuer: string }>, res) => {
const proto = req.protocol const proto = req.protocol
const config = getConfig( const config = getConfig(
@ -177,11 +178,13 @@ app.get(
) as ServerConnectionConfig ) as ServerConnectionConfig
const serverhost = req.get("Host") const serverhost = req.get("Host")
config.Versions[0].GAME_VER = req.params.serverVersion.startsWith("8") config.Versions[0].GAME_VER = "6.74.0"
? `${ServerVer._Major}.${ServerVer._Minor}.${ServerVer._Build}`
: req.params.serverVersion.startsWith("7") if (req.params.serverVersion.startsWith("8")) {
? "7.17.0" req.params.serverVersion = `${ServerVer._Major}.${ServerVer._Minor}.${ServerVer._Build}`
: "6.74.0" } else if (req.params.serverVersion.startsWith("7")) {
req.params.serverVersion = "7.17.0"
}
if (req.params.serverVersion.startsWith("8")) { if (req.params.serverVersion.startsWith("8")) {
config.Versions[0].SERVER_VER.GlobalAuthentication.RequestedAudience = config.Versions[0].SERVER_VER.GlobalAuthentication.RequestedAudience =
@ -229,7 +232,7 @@ app.get(
}, },
) )
app.get("/files/privacypolicy/hm3/privacypolicy_*.json", (req, res) => { app.get("/files/privacypolicy/hm3/privacypolicy_*.json", (_, res) => {
res.set("Content-Type", "application/octet-stream") res.set("Content-Type", "application/octet-stream")
res.set("x-ms-meta-version", "20181001") res.set("x-ms-meta-version", "20181001")
res.send(getConfig("PrivacyPolicy", false)) res.send(getConfig("PrivacyPolicy", false))
@ -238,6 +241,7 @@ app.get("/files/privacypolicy/hm3/privacypolicy_*.json", (req, res) => {
app.post( app.post(
"/api/metrics/*", "/api/metrics/*",
jsonMiddleware({ limit: "10Mb" }), jsonMiddleware({ limit: "10Mb" }),
// @ts-expect-error jwt props.
(req: RequestWithJwt<never, S2CEventWithTimestamp[]>, res) => { (req: RequestWithJwt<never, S2CEventWithTimestamp[]>, res) => {
for (const event of req.body) { for (const event of req.body) {
controller.hooks.newMetricsEvent.call(event, req) controller.hooks.newMetricsEvent.call(event, req)
@ -247,8 +251,26 @@ app.post(
}, },
) )
app.post("/oauth/token", urlencoded(), (req: RequestWithJwt, res) => app.post(
handleOauthToken(req, res), "/oauth/token",
urlencoded(),
// @ts-expect-error jwt props.
(req: RequestWithJwt<never, OAuthTokenBody>, res) => {
handleOAuthToken(req)
.then((token) => {
if (token === error400) {
return res.status(400).send()
} else if (token === error406) {
return res.status(406).send()
} else {
return res.json(token)
}
})
.catch((err) => {
log(LogLevel.ERROR, err.message)
res.status(500).send()
})
},
) )
app.get("/files/onlineconfig.json", (_, res) => { app.get("/files/onlineconfig.json", (_, res) => {
@ -263,15 +285,16 @@ app.use(
Router() Router()
.use( .use(
"/resources-:serverVersion(\\d+-\\d+)/", "/resources-:serverVersion(\\d+-\\d+)/",
(req: RequestWithJwt, res, next) => { // @ts-expect-error Has jwt props.
(req: RequestWithJwt, _, next) => {
req.serverVersion = req.params.serverVersion req.serverVersion = req.params.serverVersion
req.gameVersion = req.serverVersion.startsWith("8") req.gameVersion = "h1"
? "h3"
: req.serverVersion.startsWith("7") if (req.serverVersion.startsWith("8")) {
? // prettier-ignore req.gameVersion = "h3"
"h2" } else if (req.serverVersion.startsWith("7")) {
: // prettier-ignore req.gameVersion = "h2"
"h1" }
if (req.serverVersion === "7.3.0") { if (req.serverVersion === "7.3.0") {
req.gameVersion = "scpc" req.gameVersion = "scpc"
@ -281,6 +304,7 @@ app.use(
}, },
) )
// we're fine with skipping to the next router if we don't have auth // we're fine with skipping to the next router if we don't have auth
// @ts-expect-error Has jwt props.
.use(extractToken, (req: RequestWithJwt, res, next) => { .use(extractToken, (req: RequestWithJwt, res, next) => {
switch (req.jwt?.pis) { switch (req.jwt?.pis) {
case "egp_io_interactive_hitman_the_complete_first_season": case "egp_io_interactive_hitman_the_complete_first_season":
@ -300,11 +324,13 @@ app.use(
return return
} }
req.gameVersion = req.serverVersion.startsWith("8") req.gameVersion = "h1"
? "h3"
: req.serverVersion.startsWith("7") if (req.serverVersion.startsWith("8")) {
? "h2" req.gameVersion = "h3"
: "h1" } else if (req.serverVersion.startsWith("7")) {
req.gameVersion = "h2"
}
if (req.jwt?.aud === "scpc-prod") { if (req.jwt?.aud === "scpc-prod") {
req.gameVersion = "scpc" req.gameVersion = "scpc"
@ -316,6 +342,7 @@ app.use(
app.get( app.get(
"/profiles/page//dashboard//Dashboard_Category_Sniper_Singleplayer/00000000-0000-0000-0000-000000000015/Contract/ff9f46cf-00bd-4c12-b887-eac491c3a96d", "/profiles/page//dashboard//Dashboard_Category_Sniper_Singleplayer/00000000-0000-0000-0000-000000000015/Contract/ff9f46cf-00bd-4c12-b887-eac491c3a96d",
// @ts-expect-error jwt props.
(req: RequestWithJwt, res) => { (req: RequestWithJwt, res) => {
res.json({ res.json({
template: getConfig("FrankensteinMmSpTemplate", false), template: getConfig("FrankensteinMmSpTemplate", false),
@ -339,6 +366,7 @@ app.get(
// We handle this for now, but it's not used. For the future though. // We handle this for now, but it's not used. For the future though.
app.get( app.get(
"/profiles/page//dashboard//Dashboard_Category_Sniper_Multiplayer/00000000-0000-0000-0000-000000000015/Contract/ff9f46cf-00bd-4c12-b887-eac491c3a96d", "/profiles/page//dashboard//Dashboard_Category_Sniper_Multiplayer/00000000-0000-0000-0000-000000000015/Contract/ff9f46cf-00bd-4c12-b887-eac491c3a96d",
// @ts-expect-error jwt props.
(req: RequestWithJwt, res) => { (req: RequestWithJwt, res) => {
const template = getConfig("FrankensteinMmMpTemplate", false) const template = getConfig("FrankensteinMmMpTemplate", false)
@ -371,6 +399,7 @@ app.get(
) )
if (PEACOCK_DEV) { if (PEACOCK_DEV) {
// @ts-expect-error Has jwt props.
app.use(async (req: RequestWithJwt, _res, next): Promise<void> => { app.use(async (req: RequestWithJwt, _res, next): Promise<void> => {
if (!req.jwt) { if (!req.jwt) {
next() next()
@ -397,6 +426,7 @@ function generateBlobConfig(req: RequestWithJwt) {
app.get( app.get(
"/authentication/api/configuration/Init?*", "/authentication/api/configuration/Init?*",
// @ts-expect-error jwt props.
extractToken, extractToken,
(req: RequestWithJwt, res) => { (req: RequestWithJwt, res) => {
// configName=pc-prod&lockedContentDisabled=false&isFreePrologueUser=false&isIntroPackUser=false&isFullExperienceUser=false // configName=pc-prod&lockedContentDisabled=false&isFreePrologueUser=false&isIntroPackUser=false&isFullExperienceUser=false
@ -413,6 +443,7 @@ app.get(
app.post( app.post(
"/authentication/api/userchannel/AuthenticationService/RenewBlobSignature", "/authentication/api/userchannel/AuthenticationService/RenewBlobSignature",
// @ts-expect-error jwt props.
(req: RequestWithJwt, res) => { (req: RequestWithJwt, res) => {
res.json(generateBlobConfig(req)) res.json(generateBlobConfig(req))
}, },
@ -421,7 +452,6 @@ app.post(
const legacyRouter = Router() const legacyRouter = Router()
const primaryRouter = Router() const primaryRouter = Router()
legacyRouter.use("/resources-(\\d+-\\d+)/", legacyMenuSystemRouter)
legacyRouter.use("/authentication/api/userchannel/", legacyProfileRouter) legacyRouter.use("/authentication/api/userchannel/", legacyProfileRouter)
legacyRouter.use("/profiles/page/", legacyMenuDataRouter) legacyRouter.use("/profiles/page/", legacyMenuDataRouter)
legacyRouter.use( legacyRouter.use(
@ -442,9 +472,12 @@ primaryRouter.use(
"/authentication/api/userchannel/ContractsService/", "/authentication/api/userchannel/ContractsService/",
contractRoutingRouter, contractRoutingRouter,
) )
primaryRouter.use( primaryRouter.get(
"/authentication/api/userchannel/ReportingService/", "/authentication/api/userchannel/ReportingService/ReportContract",
reportRouter, (_, res) => {
// TODO
res.json({})
},
) )
primaryRouter.use("/authentication/api/userchannel/", profileRouter) primaryRouter.use("/authentication/api/userchannel/", profileRouter)
primaryRouter.use("/profiles/page", multiplayerMenuDataRouter) primaryRouter.use("/profiles/page", multiplayerMenuDataRouter)
@ -454,6 +487,7 @@ primaryRouter.use("/resources-(\\d+-\\d+)/", menuSystemRouter)
app.use( app.use(
Router() Router()
// @ts-expect-error Has jwt props.
.use((req: RequestWithJwt, _, next) => { .use((req: RequestWithJwt, _, next) => {
if (req.shouldCease) { if (req.shouldCease) {
return next("router") return next("router")
@ -467,6 +501,7 @@ app.use(
}) })
.use(legacyRouter), .use(legacyRouter),
Router() Router()
// @ts-expect-error Has jwt props.
.use((req: RequestWithJwt, _, next) => { .use((req: RequestWithJwt, _, next) => {
if (req.shouldCease) { if (req.shouldCease) {
return next("router") return next("router")
@ -493,28 +528,15 @@ app.all("*", (req, res) => {
app.use(errorLoggingMiddleware) app.use(errorLoggingMiddleware)
program.description( program.description(
"The Peacock Project is a HITMAN™ World of Assassination Trilogy server built for general use.", "The Peacock Project is a HITMAN™ World of Assassination Trilogy server replacement.",
) )
const PEECOCK_ART = picocolors.yellow(`
`)
function startServer(options: { hmr: boolean; pluginDevHost: boolean }): void { function startServer(options: { hmr: boolean; pluginDevHost: boolean }): void {
checkForUpdates() void checkForUpdates()
if (!IS_LAUNCHER) { if (!IS_LAUNCHER) {
console.log( console.log(
Math.random() < 0.001 picocolors.greenBright(`
? PEECOCK_ART
: picocolors.greenBright(`
@ -576,7 +598,7 @@ function startServer(options: { hmr: boolean; pluginDevHost: boolean }): void {
if (options.hmr) { if (options.hmr) {
log(LogLevel.DEBUG, "Experimental HMR enabled.") log(LogLevel.DEBUG, "Experimental HMR enabled.")
setupHotListener("contracts", () => { void setupHotListener("contracts", () => {
log(LogLevel.INFO, "Detected a change in contracts! Re-indexing...") log(LogLevel.INFO, "Detected a change in contracts! Re-indexing...")
controller.index() controller.index()
}) })
@ -584,7 +606,7 @@ function startServer(options: { hmr: boolean; pluginDevHost: boolean }): void {
// once contracts directory is present, we are clear to boot // once contracts directory is present, we are clear to boot
loadouts.init() loadouts.init()
controller.boot(options.pluginDevHost) void controller.boot(options.pluginDevHost)
const httpServer = http.createServer(app) const httpServer = http.createServer(app)
@ -597,7 +619,7 @@ function startServer(options: { hmr: boolean; pluginDevHost: boolean }): void {
} }
// initialize livesplit // initialize livesplit
liveSplitManager.init() void liveSplitManager.init()
} }
program.option( program.option(
@ -616,9 +638,10 @@ program
.command("tools") .command("tools")
.description("open the tools UI") .description("open the tools UI")
.action(() => { .action(() => {
toolsMenu() void toolsMenu()
}) })
// noinspection RequiredAttributes
program program
.command("pack") .command("pack")
.argument("<input>", "input file to pack") .argument("<input>", "input file to pack")
@ -636,6 +659,7 @@ program
log(LogLevel.INFO, `Packed "${input}" to "${outputPath}" successfully.`) log(LogLevel.INFO, `Packed "${input}" to "${outputPath}" successfully.`)
}) })
// noinspection RequiredAttributes
program program
.command("unpack") .command("unpack")
.argument("<input>", "input file to unpack") .argument("<input>", "input file to unpack")

View File

@ -106,7 +106,7 @@ export function clearInventoryCache(): void {
function filterUnlockedContent( function filterUnlockedContent(
userProfile: UserProfile, userProfile: UserProfile,
packagedUnlocks: Map<string, boolean>, packagedUnlocks: Map<string, boolean>,
challengesUnlockables: object, challengesUnlockables: Record<string, string>,
gameVersion: GameVersion, gameVersion: GameVersion,
) { ) {
return function ( return function (
@ -114,7 +114,7 @@ function filterUnlockedContent(
unlockable: Unlockable, unlockable: Unlockable,
) { ) {
let unlockableChallengeId: string let unlockableChallengeId: string
let unlockableMasteryData: UnlockableMasteryData let unlockableMasteryData: UnlockableMasteryData | undefined
// Handles unlockables that belong to a package or unlocked gear from evergreen // Handles unlockables that belong to a package or unlocked gear from evergreen
if (packagedUnlocks.has(unlockable.Id)) { if (packagedUnlocks.has(unlockable.Id)) {
@ -123,7 +123,7 @@ function filterUnlockedContent(
// Handles packages // Handles packages
else if (unlockable.Type === "package") { else if (unlockable.Type === "package") {
for (const pkgUnlockableId of unlockable.Properties.Unlocks) { for (const pkgUnlockableId of unlockable.Properties.Unlocks || []) {
packagedUnlocks.set(pkgUnlockableId, true) packagedUnlocks.set(pkgUnlockableId, true)
} }
@ -199,7 +199,7 @@ function filterUnlockedContent(
* @returns boolean * @returns boolean
*/ */
function filterAllowedContent(gameVersion: GameVersion, entP: string[]) { function filterAllowedContent(gameVersion: GameVersion, entP: string[]) {
return function (unlockContainer: { return function (unlockContainer?: {
InstanceId: string InstanceId: string
ProfileId: string ProfileId: string
Unlockable: Unlockable Unlockable: Unlockable
@ -474,21 +474,25 @@ function updateWithDefaultSuit(
profileId: string, profileId: string,
gameVersion: GameVersion, gameVersion: GameVersion,
inv: InventoryItem[], inv: InventoryItem[],
sublocation: Unlockable, sublocation?: Unlockable,
): InventoryItem[] { ): InventoryItem[] {
if (sublocation === undefined) { if (!sublocation) {
return inv return inv
} }
// We need to add a suit, so need to copy the cache to prevent modifying it.
const newInv = [...inv]
// Yes this is slow. We should organize the unlockables into a { [Id: string]: Unlockable } map. // Yes this is slow. We should organize the unlockables into a { [Id: string]: Unlockable } map.
const locationSuit = getUnlockableById( const locationSuit = getUnlockableById(
getDefaultSuitFor(sublocation), getDefaultSuitFor(sublocation),
gameVersion, gameVersion,
) )
if (!locationSuit) {
return inv
}
// We need to add a suit, so need to copy the cache to prevent modifying it.
const newInv = [...inv]
// check if any inventoryItem's unlockable is the default suit for the sublocation // check if any inventoryItem's unlockable is the default suit for the sublocation
if (newInv.every((i) => i.Unlockable.Id !== locationSuit.Id)) { if (newInv.every((i) => i.Unlockable.Id !== locationSuit.Id)) {
// if not, add it // if not, add it
@ -576,7 +580,7 @@ export function createInventory(
// and location-wide default suits will be given afterwards. // and location-wide default suits will be given afterwards.
const defaults = Object.values(defaultSuits) const defaults = Object.values(defaultSuits)
if ((getFlag("getDefaultSuits") as boolean) === false) { if (!getFlag("getDefaultSuits")) {
unlockables = unlockables.filter( unlockables = unlockables.filter(
(u) => (u) =>
!defaults.includes(u.Id) || !defaults.includes(u.Id) ||
@ -584,8 +588,7 @@ export function createInventory(
) )
} }
// ts-expect-error It cannot be undefined. const filtered = unlockables
const filtered: InventoryItem[] = unlockables
.map((unlockable) => { .map((unlockable) => {
if (brokenItems.includes(unlockable.Guid)) { if (brokenItems.includes(unlockable.Guid)) {
return undefined return undefined
@ -601,7 +604,9 @@ export function createInventory(
} }
}) })
// filter again, this time removing legacy unlockables // filter again, this time removing legacy unlockables
.filter(filterAllowedContent(gameVersion, userProfile.Extensions.entP)) .filter(
filterAllowedContent(gameVersion, userProfile.Extensions.entP),
) as InventoryItem[]
for (const unlockable of filtered) { for (const unlockable of filtered) {
unlockable!.ProfileId = profileId unlockable!.ProfileId = profileId
@ -627,7 +632,7 @@ export function grantDrops(profileId: string, drops: Unlockable[]): void {
inventoryUserCache.set(profileId, [ inventoryUserCache.set(profileId, [
...new Set([ ...new Set([
...inventoryUserCache.get(profileId), ...(inventoryUserCache.get(profileId) || []),
...inventoryItems.filter( ...inventoryItems.filter(
(invItem) => invItem.Unlockable.Type !== "evergreenmastery", (invItem) => invItem.Unlockable.Type !== "evergreenmastery",
), ),

View File

@ -50,14 +50,7 @@ const defaultValue: LoadoutFile = {
* A class for managing loadouts. * A class for managing loadouts.
*/ */
export class Loadouts { export class Loadouts {
private _loadouts: LoadoutFile private _loadouts!: LoadoutFile
/**
* Creates a new instance of the class.
*/
public constructor() {
this._loadouts = undefined
}
/** /**
* Get the loadouts data. * Get the loadouts data.
@ -107,7 +100,10 @@ export class Loadouts {
// if the selected value is null/undefined or is not length 0 or 21, it's not a valid id // if the selected value is null/undefined or is not length 0 or 21, it's not a valid id
if ( if (
!this._loadouts[gameVersion].selected || !this._loadouts[gameVersion].selected ||
![0, 21].includes(this._loadouts[gameVersion].selected.length) // first condition ensures selected is truthy, but TS doesn't know
![0, 21].includes(
this._loadouts[gameVersion].selected?.length || -1,
)
) { ) {
dirty = true dirty = true
@ -121,7 +117,7 @@ export class Loadouts {
} }
} }
if (dirty === true) { if (dirty) {
writeFileSync(LOADOUT_PROFILES_FILE, JSON.stringify(this._loadouts)) writeFileSync(LOADOUT_PROFILES_FILE, JSON.stringify(this._loadouts))
} }
} }
@ -217,7 +213,7 @@ loadoutRouter.patch(
async ( async (
req: Request< req: Request<
never, never,
string, string | { error?: string; message?: string },
{ gameVersion: "h1" | "h2" | "h3"; id: string } { gameVersion: "h1" | "h2" | "h3"; id: string }
>, >,
res, res,

View File

@ -16,8 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import type { NextFunction, Response } from "express" import type { NextFunction, Request, Response } from "express"
import type { RequestWithJwt } from "./types/types"
import picocolors from "picocolors" import picocolors from "picocolors"
import winston from "winston" import winston from "winston"
import "winston-daily-rotate-file" import "winston-daily-rotate-file"
@ -135,6 +134,7 @@ if (consoleLogLevel !== LOG_LEVEL_NONE) {
} }
const winstonLogLevel = {} const winstonLogLevel = {}
// @ts-expect-error Type mismatch.
Object.values(LogLevel).forEach((e, i) => (winstonLogLevel[e] = i)) Object.values(LogLevel).forEach((e, i) => (winstonLogLevel[e] = i))
const logger = winston.createLogger({ const logger = winston.createLogger({
@ -255,13 +255,13 @@ export function log(
* Express middleware that logs all requests and their details with the info log level. * Express middleware that logs all requests and their details with the info log level.
* *
* @param req The Express request object. * @param req The Express request object.
* @param res The Express response object. * @param _ The Express response object.
* @param next The Express next function. * @param next The Express next function.
* @see LogLevel.INFO * @see LogLevel.INFO
*/ */
export function loggingMiddleware( export function loggingMiddleware(
req: RequestWithJwt, req: Request,
res: Response, _: Response,
next?: NextFunction, next?: NextFunction,
): void { ): void {
log( log(
@ -273,7 +273,7 @@ export function loggingMiddleware(
} }
export function requestLoggingMiddleware( export function requestLoggingMiddleware(
req: RequestWithJwt, req: Request,
res: Response, res: Response,
next?: NextFunction, next?: NextFunction,
): void { ): void {
@ -294,8 +294,8 @@ export function requestLoggingMiddleware(
export function errorLoggingMiddleware( export function errorLoggingMiddleware(
err: Error, err: Error,
req: RequestWithJwt, req: Request,
res: Response, _: Response,
next?: NextFunction, next?: NextFunction,
): void { ): void {
const debug = { const debug = {

View File

@ -21,7 +21,6 @@ import { Response, Router } from "express"
import { import {
contractCreationTutorialId, contractCreationTutorialId,
getMaxProfileLevel, getMaxProfileLevel,
isSniperLocation,
isSuit, isSuit,
unlockOrderComparer, unlockOrderComparer,
uuidRegex, uuidRegex,
@ -29,22 +28,15 @@ import {
import { contractSessions, getSession } from "./eventHandler" import { contractSessions, getSession } from "./eventHandler"
import { getConfig, getVersionedConfig } from "./configSwizzleManager" import { getConfig, getVersionedConfig } from "./configSwizzleManager"
import { controller } from "./controller" import { controller } from "./controller"
import { import { createLocationsData, getDestination } from "./menus/destinations"
createLocationsData,
getDestination,
getDestinationCompletion,
} from "./menus/destinations"
import type { import type {
ChallengeCategoryCompletion,
SelectEntranceOrPickupData,
ContractSearchResult, ContractSearchResult,
GameVersion, GameVersion,
HitsCategoryCategory, HitsCategoryCategory,
PeacockLocationsData, PeacockLocationsData,
PlayerProfileView,
ProgressionData,
RequestWithJwt, RequestWithJwt,
SceneConfig, SceneConfig,
SelectEntranceOrPickupData,
UserCentricContract, UserCentricContract,
} from "./types/types" } from "./types/types"
import { import {
@ -79,7 +71,9 @@ import {
DebriefingLeaderboardsQuery, DebriefingLeaderboardsQuery,
GetCompletionDataForLocationQuery, GetCompletionDataForLocationQuery,
GetDestinationQuery, GetDestinationQuery,
GetMasteryCompletionDataForUnlockableQuery,
LeaderboardEntriesCommonQuery, LeaderboardEntriesCommonQuery,
LookupContractPublicIdQuery,
MasteryUnlockableQuery, MasteryUnlockableQuery,
MissionEndRequestQuery, MissionEndRequestQuery,
PlanningQuery, PlanningQuery,
@ -97,6 +91,7 @@ import {
getSafehouseCategory, getSafehouseCategory,
} from "./menus/stashpoints" } from "./menus/stashpoints"
import { getHubData } from "./menus/hub" import { getHubData } from "./menus/hub"
import { getPlayerProfileData } from "./menus/playerProfile"
const menuDataRouter = Router() const menuDataRouter = Router()
@ -104,18 +99,19 @@ const menuDataRouter = Router()
menuDataRouter.get( menuDataRouter.get(
"/ChallengeLocation", "/ChallengeLocation",
// @ts-expect-error Jwt props.
(req: RequestWithJwt<ChallengeLocationQuery>, res) => { (req: RequestWithJwt<ChallengeLocationQuery>, res) => {
if (typeof req.query.locationId !== "string") {
res.status(400).send("Invalid locationId")
return
}
const location = getVersionedConfig<PeacockLocationsData>( const location = getVersionedConfig<PeacockLocationsData>(
"LocationsData", "LocationsData",
req.gameVersion, req.gameVersion,
true, true,
).children[req.query.locationId] ).children[req.query.locationId]
if (!location) {
res.status(400).send("Invalid locationId")
return
}
const data = { const data = {
Name: location.DisplayNameLocKey, Name: location.DisplayNameLocKey,
Location: location, Location: location,
@ -137,17 +133,20 @@ menuDataRouter.get(
}, },
) )
// @ts-expect-error Jwt props.
menuDataRouter.get("/Hub", (req: RequestWithJwt, res) => { menuDataRouter.get("/Hub", (req: RequestWithJwt, res) => {
const hubInfo = getHubData(req.gameVersion, req.jwt) const hubInfo = getHubData(req.gameVersion, req.jwt.unique_name)
const template = let template: unknown
req.gameVersion === "h3"
? null if (req.gameVersion === "h3" || req.gameVersion === "h2") {
: req.gameVersion === "h2" template = null
? null } else {
: req.gameVersion === "scpc" template =
? getConfig("FrankensteinHubTemplate", false) req.gameVersion === "scpc"
: getConfig("LegacyHubTemplate", false) ? getConfig("FrankensteinHubTemplate", false)
: getConfig("LegacyHubTemplate", false)
}
res.json({ res.json({
template, template,
@ -155,6 +154,7 @@ menuDataRouter.get("/Hub", (req: RequestWithJwt, res) => {
}) })
}) })
// @ts-expect-error Jwt props.
menuDataRouter.get("/SafehouseCategory", (req: RequestWithJwt, res) => { menuDataRouter.get("/SafehouseCategory", (req: RequestWithJwt, res) => {
res.json({ res.json({
template: template:
@ -165,6 +165,7 @@ menuDataRouter.get("/SafehouseCategory", (req: RequestWithJwt, res) => {
}) })
}) })
// @ts-expect-error Jwt props.
menuDataRouter.get("/Safehouse", (req: RequestWithJwt<SafehouseQuery>, res) => { menuDataRouter.get("/Safehouse", (req: RequestWithJwt<SafehouseQuery>, res) => {
const template = getConfig("LegacySafehouseTemplate", false) const template = getConfig("LegacySafehouseTemplate", false)
@ -184,6 +185,7 @@ menuDataRouter.get("/Safehouse", (req: RequestWithJwt<SafehouseQuery>, res) => {
}) })
}) })
// @ts-expect-error Jwt props.
menuDataRouter.get("/report", (req: RequestWithJwt, res) => { menuDataRouter.get("/report", (req: RequestWithJwt, res) => {
res.json({ res.json({
template: getVersionedConfig("ReportTemplate", req.gameVersion, false), template: getVersionedConfig("ReportTemplate", req.gameVersion, false),
@ -202,6 +204,7 @@ menuDataRouter.get("/report", (req: RequestWithJwt, res) => {
// /stashpoint?contractid=5b5f8aa4-ecb4-4a0a-9aff-98aa1de43dcc&slotid=6&slotname=stashpoint6&stashpoint=28b03709-d1f0-4388-b207-f03611eafb64&allowlargeitems=true&allowcontainers=false // /stashpoint?contractid=5b5f8aa4-ecb4-4a0a-9aff-98aa1de43dcc&slotid=6&slotname=stashpoint6&stashpoint=28b03709-d1f0-4388-b207-f03611eafb64&allowlargeitems=true&allowcontainers=false
menuDataRouter.get( menuDataRouter.get(
"/stashpoint", "/stashpoint",
// @ts-expect-error Jwt props.
(req: RequestWithJwt<StashpointQuery | StashpointQueryH2016>, res) => { (req: RequestWithJwt<StashpointQuery | StashpointQueryH2016>, res) => {
function isValidModernQuery( function isValidModernQuery(
query: StashpointQuery | StashpointQueryH2016, query: StashpointQuery | StashpointQueryH2016,
@ -214,7 +217,7 @@ menuDataRouter.get(
if (["h1", "scpc"].includes(req.gameVersion)) { if (["h1", "scpc"].includes(req.gameVersion)) {
// H1 or SCPC // H1 or SCPC
if (!uuidRegex.test(req.query.contractid)) { if (!uuidRegex.test(req.query.contractid!)) {
res.status(400).send("contract id was not a uuid") res.status(400).send("contract id was not a uuid")
return return
} }
@ -264,15 +267,22 @@ menuDataRouter.get(
menuDataRouter.get( menuDataRouter.get(
"/missionrewards", "/missionrewards",
// @ts-expect-error Jwt props.
( (
req: RequestWithJwt<{ req: RequestWithJwt<{
contractSessionId: string contractSessionId: string
}>, }>,
res, res,
) => { ) => {
const { contractId } = getSession(req.jwt.unique_name) const s = getSession(req.jwt.unique_name)
const contractData = controller.resolveContract(contractId, true)
if (!s) {
res.status(400).send("no session")
return
}
const { contractId } = s
const contractData = controller.resolveContract(contractId, true)
const userData = getUserData(req.jwt.unique_name, req.gameVersion) const userData = getUserData(req.jwt.unique_name, req.gameVersion)
res.json({ res.json({
@ -321,7 +331,7 @@ menuDataRouter.get(
LocationHideProgression: true, LocationHideProgression: true,
Difficulty: "normal", // FIXME: is this right? Difficulty: "normal", // FIXME: is this right?
CompletionData: generateCompletionData( CompletionData: generateCompletionData(
contractData.Metadata.Location, contractData?.Metadata.Location || "",
req.jwt.unique_name, req.jwt.unique_name,
req.gameVersion, req.gameVersion,
), ),
@ -332,6 +342,7 @@ menuDataRouter.get(
menuDataRouter.get( menuDataRouter.get(
"/Planning", "/Planning",
// @ts-expect-error Jwt props.
async (req: RequestWithJwt<PlanningQuery>, res) => { async (req: RequestWithJwt<PlanningQuery>, res) => {
if (!req.query.contractid || !req.query.resetescalation) { if (!req.query.contractid || !req.query.resetescalation) {
res.status(400).send("invalid query") res.status(400).send("invalid query")
@ -341,7 +352,7 @@ menuDataRouter.get(
const planningData = await getPlanningData( const planningData = await getPlanningData(
req.query.contractid, req.query.contractid,
req.query.resetescalation === "true", req.query.resetescalation === "true",
req.jwt, req.jwt.unique_name,
req.gameVersion, req.gameVersion,
) )
@ -350,13 +361,16 @@ menuDataRouter.get(
return return
} }
let template: unknown | null = null
if (req.gameVersion === "h1") {
template = getConfig("LegacyPlanningTemplate", false)
} else if (req.gameVersion === "scpc") {
template = getConfig("FrankensteinPlanningTemplate", false)
}
res.json({ res.json({
template: template,
req.gameVersion === "h1"
? getConfig("LegacyPlanningTemplate", false)
: req.gameVersion === "scpc"
? getConfig("FrankensteinPlanningTemplate", false)
: null,
data: planningData, data: planningData,
}) })
}, },
@ -364,6 +378,7 @@ menuDataRouter.get(
menuDataRouter.get( menuDataRouter.get(
"/selectagencypickup", "/selectagencypickup",
// @ts-expect-error Jwt props.
( (
req: RequestWithJwt<{ req: RequestWithJwt<{
contractId: string contractId: string
@ -407,7 +422,7 @@ menuDataRouter.get(
contractData, contractData,
req.jwt.unique_name, req.jwt.unique_name,
req.gameVersion, req.gameVersion,
), )!,
} }
res.json({ res.json({
@ -440,14 +455,16 @@ menuDataRouter.get(
Contract: contractData, Contract: contractData,
OrderedUnlocks: unlockedAgencyPickups OrderedUnlocks: unlockedAgencyPickups
.filter((unlockable) => .filter((unlockable) =>
pickupsInScene.includes(unlockable.Properties.RepositoryId), pickupsInScene.includes(
unlockable.Properties.RepositoryId || "",
),
) )
.sort(unlockOrderComparer), .sort(unlockOrderComparer),
UserCentric: generateUserCentric( UserCentric: generateUserCentric(
contractData, contractData,
req.jwt.unique_name, req.jwt.unique_name,
req.gameVersion, req.gameVersion,
), )!,
} }
res.json({ res.json({
@ -463,6 +480,7 @@ menuDataRouter.get(
menuDataRouter.get( menuDataRouter.get(
"/selectentrance", "/selectentrance",
// @ts-expect-error Jwt props.
( (
req: RequestWithJwt<{ req: RequestWithJwt<{
contractId: string contractId: string
@ -522,7 +540,7 @@ menuDataRouter.get(
contractData, contractData,
req.jwt.unique_name, req.jwt.unique_name,
req.gameVersion, req.gameVersion,
), )!,
} }
res.json({ res.json({
@ -580,6 +598,12 @@ const missionEndRequest = async (
return return
} }
// prototype pollution prevention
if (/(__proto__|prototype|constructor)/.test(req.query.contractSessionId)) {
res.status(400).send("invalid session id")
return
}
const missionEndOutput = await getMissionEndData( const missionEndOutput = await getMissionEndData(
req.query, req.query,
req.jwt, req.jwt,
@ -613,21 +637,29 @@ const missionEndRequest = async (
}) })
} }
// @ts-expect-error Has jwt props.
menuDataRouter.get("/missionend", missionEndRequest) menuDataRouter.get("/missionend", missionEndRequest)
// @ts-expect-error Has jwt props.
menuDataRouter.get("/scoreoverviewandunlocks", missionEndRequest) menuDataRouter.get("/scoreoverviewandunlocks", missionEndRequest)
// @ts-expect-error Has jwt props.
menuDataRouter.get("/scoreoverview", missionEndRequest) menuDataRouter.get("/scoreoverview", missionEndRequest)
menuDataRouter.get( menuDataRouter.get(
"/Destination", "/Destination",
// @ts-expect-error Jwt props.
(req: RequestWithJwt<GetDestinationQuery>, res) => { (req: RequestWithJwt<GetDestinationQuery>, res) => {
if (!req.query.locationId) { if (!req.query.locationId) {
res.status(400).send("Invalid locationId") res.status(400).send("Invalid locationId")
return return
} }
const destination = getDestination(req.query, req.gameVersion, req.jwt) const destination = getDestination(
req.query,
req.gameVersion,
req.jwt.unique_name,
)
res.json({ res.json({
template: template:
@ -681,13 +713,9 @@ async function lookupContractPublicId(
menuDataRouter.get( menuDataRouter.get(
"/LookupContractPublicId", "/LookupContractPublicId",
async ( // @ts-expect-error Has jwt props.
req: RequestWithJwt<{ async (req: RequestWithJwt<LookupContractPublicIdQuery>, res) => {
publicid: string if (typeof req.query.publicid !== "string") {
}>,
res,
) => {
if (!req.query.publicid || typeof req.query.publicid !== "string") {
return res.status(400).send("no/invalid public id specified!") return res.status(400).send("no/invalid public id specified!")
} }
@ -708,6 +736,7 @@ menuDataRouter.get(
menuDataRouter.get( menuDataRouter.get(
"/HitsCategory", "/HitsCategory",
// @ts-expect-error Has jwt props.
async ( async (
req: RequestWithJwt<{ req: RequestWithJwt<{
type: string type: string
@ -749,6 +778,7 @@ menuDataRouter.get(
menuDataRouter.get( menuDataRouter.get(
"/PlayNext", "/PlayNext",
// @ts-expect-error Has jwt props.
( (
req: RequestWithJwt<{ req: RequestWithJwt<{
contractId: string contractId: string
@ -764,14 +794,14 @@ menuDataRouter.get(
template: getConfig("PlayNextTemplate", false), template: getConfig("PlayNextTemplate", false),
data: getGamePlayNextData( data: getGamePlayNextData(
req.query.contractId, req.query.contractId,
req.jwt, req.jwt.unique_name,
req.gameVersion, req.gameVersion,
), ),
}) })
}, },
) )
menuDataRouter.get("/LeaderboardsView", (req, res) => { menuDataRouter.get("/LeaderboardsView", (_, res) => {
res.json({ res.json({
template: getConfig("LeaderboardsViewTemplate", false), template: getConfig("LeaderboardsViewTemplate", false),
data: { data: {
@ -783,6 +813,7 @@ menuDataRouter.get("/LeaderboardsView", (req, res) => {
menuDataRouter.get( menuDataRouter.get(
"/LeaderboardEntries", "/LeaderboardEntries",
// @ts-expect-error Has jwt props.
async (req: RequestWithJwt<LeaderboardEntriesCommonQuery>, res) => { async (req: RequestWithJwt<LeaderboardEntriesCommonQuery>, res) => {
if (!req.query.contractid) { if (!req.query.contractid) {
res.status(400).send("no contract id!") res.status(400).send("no contract id!")
@ -810,6 +841,7 @@ menuDataRouter.get(
menuDataRouter.get( menuDataRouter.get(
"/DebriefingLeaderboards", "/DebriefingLeaderboards",
// @ts-expect-error Has jwt props.
async (req: RequestWithJwt<DebriefingLeaderboardsQuery>, res) => { async (req: RequestWithJwt<DebriefingLeaderboardsQuery>, res) => {
if (!req.query.contractid) { if (!req.query.contractid) {
res.status(400).send("no contract id!") res.status(400).send("no contract id!")
@ -835,10 +867,12 @@ menuDataRouter.get(
}, },
) )
// @ts-expect-error Has jwt props.
menuDataRouter.get("/Contracts", contractsModeHome) menuDataRouter.get("/Contracts", contractsModeHome)
menuDataRouter.get( menuDataRouter.get(
"/contractcreation/planning", "/contractcreation/planning",
// @ts-expect-error Has jwt props.
async ( async (
req: RequestWithJwt<{ req: RequestWithJwt<{
contractCreationIdOverwrite: string contractCreationIdOverwrite: string
@ -858,7 +892,7 @@ menuDataRouter.get(
const planningData = await getPlanningData( const planningData = await getPlanningData(
req.query.contractCreationIdOverwrite, req.query.contractCreationIdOverwrite,
false, false,
req.jwt, req.jwt.unique_name,
req.gameVersion, req.gameVersion,
) )
@ -890,6 +924,7 @@ menuDataRouter.get(
}, },
) )
// @ts-expect-error Has jwt props.
menuDataRouter.get("/contractsearchpage", (req: RequestWithJwt, res) => { menuDataRouter.get("/contractsearchpage", (req: RequestWithJwt, res) => {
const createContractTutorial = controller.resolveContract( const createContractTutorial = controller.resolveContract(
contractCreationTutorialId, contractCreationTutorialId,
@ -920,6 +955,7 @@ menuDataRouter.get("/contractsearchpage", (req: RequestWithJwt, res) => {
menuDataRouter.post( menuDataRouter.post(
"/ContractSearch", "/ContractSearch",
jsonMiddleware(), jsonMiddleware(),
// @ts-expect-error Jwt props.
async ( async (
req: RequestWithJwt< req: RequestWithJwt<
{ {
@ -977,12 +1013,21 @@ menuDataRouter.post(
} }
} else { } else {
// No plugins handle this. Getting search results from official // No plugins handle this. Getting search results from official
searchResult = await officialSearchContract( searchResult = (await officialSearchContract(
req.jwt.unique_name, req.jwt.unique_name,
req.gameVersion, req.gameVersion,
req.body, req.body,
0, 0,
) )) || {
Data: {
Contracts: [],
TotalCount: 0,
Page: 0,
ErrorReason: "",
HasPrevious: false,
HasMore: false,
},
}
} }
res.json({ res.json({
@ -999,6 +1044,7 @@ menuDataRouter.post(
menuDataRouter.post( menuDataRouter.post(
"/ContractSearchPaginate", "/ContractSearchPaginate",
jsonMiddleware(), jsonMiddleware(),
// @ts-expect-error Has jwt props.
async ( async (
req: RequestWithJwt< req: RequestWithJwt<
{ {
@ -1022,14 +1068,26 @@ menuDataRouter.post(
menuDataRouter.get( menuDataRouter.get(
"/DebriefingChallenges", "/DebriefingChallenges",
// @ts-expect-error Has jwt props.
( (
req: RequestWithJwt<{ req: RequestWithJwt<
contractId: string Partial<{
}>, contractId: string
}>
>,
res, res,
) => { ) => {
if (typeof req.query.contractId !== "string") {
res.status(400).send("invalid contractId")
return
}
res.json({ res.json({
template: getConfig("DebriefingChallengesTemplate", false), template: getVersionedConfig(
"DebriefingChallengesTemplate",
req.gameVersion,
false,
),
data: { data: {
ChallengeData: { ChallengeData: {
Children: Children:
@ -1044,6 +1102,7 @@ menuDataRouter.get(
}, },
) )
// @ts-expect-error Has jwt props.
menuDataRouter.get("/contractcreation/create", (req: RequestWithJwt, res) => { menuDataRouter.get("/contractcreation/create", (req: RequestWithJwt, res) => {
let cUuid = randomUUID() let cUuid = randomUUID()
const createContractReturnTemplate = getConfig( const createContractReturnTemplate = getConfig(
@ -1059,6 +1118,11 @@ menuDataRouter.get("/contractcreation/create", (req: RequestWithJwt, res) => {
const sesh = getSession(req.jwt.unique_name) const sesh = getSession(req.jwt.unique_name)
if (!sesh) {
res.status(400).send("no session")
return
}
const one = "1" const one = "1"
const two = `${random.int(10, 99)}` const two = `${random.int(10, 99)}`
const three = `${random.int(1_000_000, 9_999_999)}` const three = `${random.int(1_000_000, 9_999_999)}`
@ -1091,25 +1155,23 @@ menuDataRouter.get("/contractcreation/create", (req: RequestWithJwt, res) => {
Description: "UI_CONTRACTS_UGC_DESCRIPTION", Description: "UI_CONTRACTS_UGC_DESCRIPTION",
Targets: Array.from(sesh.kills) Targets: Array.from(sesh.kills)
.filter((kill) => .filter((kill) =>
sesh.markedTargets.has(kill._RepositoryId), sesh.markedTargets.has(kill._RepositoryId || ""),
) )
.map((km) => { .map((km) => ({
return { RepositoryId: km._RepositoryId,
RepositoryId: km._RepositoryId, Selected: true,
Selected: true, Weapon: {
Weapon: { RepositoryId: km.KillItemRepositoryId,
RepositoryId: km.KillItemRepositoryId, KillMethodBroad: km.KillMethodBroad,
KillMethodBroad: km.KillMethodBroad, KillMethodStrict: km.KillMethodStrict,
KillMethodStrict: km.KillMethodStrict, RequiredKillMethodType: 3,
RequiredKillMethodType: 3, },
}, Outfit: {
Outfit: { RepositoryId: km.OutfitRepoId,
RepositoryId: km.OutfitRepoId, Required: true,
Required: true, IsHitmanSuit: isSuit(km.OutfitRepoId),
IsHitmanSuit: isSuit(km.OutfitRepoId), },
}, })),
}
}),
ContractConditions: complications(timeLimitStr), ContractConditions: complications(timeLimitStr),
PublishingDisabled: PublishingDisabled:
sesh.contractId === contractCreationTutorialId, sesh.contractId === contractCreationTutorialId,
@ -1143,7 +1205,7 @@ const createLoadSaveMiddleware =
template, template,
data: { data: {
Contracts: [] as UserCentricContract[], Contracts: [] as UserCentricContract[],
PaymentEligiblity: {}, PaymentEligiblity: {} as Record<string, boolean>,
}, },
} }
@ -1186,144 +1248,46 @@ const createLoadSaveMiddleware =
menuDataRouter.post( menuDataRouter.post(
"/Load", "/Load",
jsonMiddleware(), jsonMiddleware(),
// @ts-expect-error Has jwt props.
createLoadSaveMiddleware("LoadMenuTemplate"), createLoadSaveMiddleware("LoadMenuTemplate"),
) )
menuDataRouter.post( menuDataRouter.post(
"/Save", "/Save",
jsonMiddleware(), jsonMiddleware(),
// @ts-expect-error Has jwt props.
createLoadSaveMiddleware("SaveMenuTemplate"), createLoadSaveMiddleware("SaveMenuTemplate"),
) )
// @ts-expect-error Has jwt props.
menuDataRouter.get("/PlayerProfile", (req: RequestWithJwt, res) => { menuDataRouter.get("/PlayerProfile", (req: RequestWithJwt, res) => {
const playerProfilePage = getConfig<PlayerProfileView>( res.json({
"PlayerProfilePage", template: null,
true, data: getPlayerProfileData(req.gameVersion, req.jwt.unique_name),
) })
const locationData = getVersionedConfig<PeacockLocationsData>(
"LocationsData",
req.gameVersion,
false,
)
playerProfilePage.data.SubLocationData = []
for (const subLocationKey in locationData.children) {
// Ewww...
if (
subLocationKey === "LOCATION_ICA_FACILITY_ARRIVAL" ||
subLocationKey.includes("SNUG_")
) {
continue
}
const subLocation = locationData.children[subLocationKey]
const parentLocation =
locationData.parents[subLocation.Properties.ParentLocation]
const completionData = generateCompletionData(
subLocation.Id,
req.jwt.unique_name,
req.gameVersion,
)
// TODO: Make getDestinationCompletion do something like this.
const challenges = controller.challengeService.getChallengesForLocation(
subLocation.Id,
req.gameVersion,
)
const challengeCategoryCompletion: ChallengeCategoryCompletion[] = []
for (const challengeGroup in challenges) {
const challengeCompletion =
controller.challengeService.countTotalNCompletedChallenges(
{
challengeGroup: challenges[challengeGroup],
},
req.jwt.unique_name,
req.gameVersion,
)
challengeCategoryCompletion.push({
Name: challenges[challengeGroup][0].CategoryName,
...challengeCompletion,
})
}
const destinationCompletion = getDestinationCompletion(
parentLocation,
subLocation,
req.gameVersion,
req.jwt,
)
playerProfilePage.data.SubLocationData.push({
ParentLocation: parentLocation,
Location: subLocation,
CompletionData: completionData,
ChallengeCategoryCompletion: challengeCategoryCompletion,
ChallengeCompletion: destinationCompletion.ChallengeCompletion,
OpportunityStatistics: destinationCompletion.OpportunityStatistics,
LocationCompletionPercent:
destinationCompletion.LocationCompletionPercent,
})
}
const userProfile = getUserData(req.jwt.unique_name, req.gameVersion)
playerProfilePage.data.PlayerProfileXp.Total =
userProfile.Extensions.progression.PlayerProfileXP.Total
playerProfilePage.data.PlayerProfileXp.Level =
userProfile.Extensions.progression.PlayerProfileXP.ProfileLevel
const subLocationMap = new Map(
userProfile.Extensions.progression.PlayerProfileXP.Sublocations.map(
(obj) => [obj.Location, obj],
),
)
for (const e of playerProfilePage.data.PlayerProfileXp.Seasons) {
for (const f of e.Locations) {
const subLocationData = subLocationMap.get(f.LocationId)
f.Xp = subLocationData?.Xp || 0
f.ActionXp = subLocationData?.ActionXp || 0
if (f.LocationProgression && !isSniperLocation(f.LocationId)) {
// We typecast below as it could be an object for subpackages.
// Checks before this ensure it isn't, but TS doesn't realise this.
f.LocationProgression.Level =
(
userProfile.Extensions.progression.Locations[
f.LocationId
] as ProgressionData
).Level || 1
}
}
}
res.json(playerProfilePage)
}) })
menuDataRouter.get( menuDataRouter.get(
// who at IOI decided this was a good route name???! // who at IOI decided this was a good route name???!
"/LookupContractDialogAddOrDeleteFromPlaylist", "/LookupContractDialogAddOrDeleteFromPlaylist",
// @ts-expect-error Has jwt props.
withLookupDialog, withLookupDialog,
) )
menuDataRouter.get( menuDataRouter.get(
// this one is sane Kappa
"/contractplaylist/addordelete/:contractId", "/contractplaylist/addordelete/:contractId",
// @ts-expect-error Has jwt props.
directRoute, directRoute,
) )
menuDataRouter.post( menuDataRouter.post(
"/contractplaylist/deletemultiple", "/contractplaylist/deletemultiple",
jsonMiddleware(), jsonMiddleware(),
// @ts-expect-error Has jwt props.
deleteMultiple, deleteMultiple,
) )
// @ts-expect-error Has jwt props.
menuDataRouter.get("/GetPlayerProfileXpData", (req: RequestWithJwt, res) => { menuDataRouter.get("/GetPlayerProfileXpData", (req: RequestWithJwt, res) => {
const userData = getUserData(req.jwt.unique_name, req.gameVersion) const userData = getUserData(req.jwt.unique_name, req.gameVersion)
@ -1342,7 +1306,13 @@ menuDataRouter.get("/GetPlayerProfileXpData", (req: RequestWithJwt, res) => {
menuDataRouter.get( menuDataRouter.get(
"/GetMasteryCompletionDataForLocation", "/GetMasteryCompletionDataForLocation",
// @ts-expect-error Has jwt props.
(req: RequestWithJwt<GetCompletionDataForLocationQuery>, res) => { (req: RequestWithJwt<GetCompletionDataForLocationQuery>, res) => {
if (!req.query.locationId) {
res.status(400).send("no location id")
return
}
res.json( res.json(
generateCompletionData( generateCompletionData(
req.query.locationId, req.query.locationId,
@ -1355,6 +1325,7 @@ menuDataRouter.get(
menuDataRouter.get( menuDataRouter.get(
"/MasteryUnlockable", "/MasteryUnlockable",
// @ts-expect-error Has jwt props.
(req: RequestWithJwt<MasteryUnlockableQuery>, res) => { (req: RequestWithJwt<MasteryUnlockableQuery>, res) => {
let masteryUnlockTemplate = getConfig( let masteryUnlockTemplate = getConfig(
"MasteryUnlockablesTemplate", "MasteryUnlockablesTemplate",
@ -1402,32 +1373,30 @@ menuDataRouter.get(
menuDataRouter.get( menuDataRouter.get(
"/MasteryDataForLocation", "/MasteryDataForLocation",
// @ts-expect-error Has jwt props.
( (
req: RequestWithJwt<{ req: RequestWithJwt<{
locationId: string locationId: string
}>, }>,
res, res,
) => { ) => {
res.json( res.json({
controller.masteryService.getMasteryDataForLocation( template: getConfig("MasteryDataForLocationTemplate", false),
data: controller.masteryService.getMasteryDataForLocation(
req.query.locationId, req.query.locationId,
req.gameVersion, req.gameVersion,
req.jwt.unique_name, req.jwt.unique_name,
), ),
) })
}, },
) )
menuDataRouter.get( menuDataRouter.get(
"/GetMasteryCompletionDataForUnlockable", "/GetMasteryCompletionDataForUnlockable",
( // @ts-expect-error Has jwt props.
req: RequestWithJwt<{ (req: RequestWithJwt<GetMasteryCompletionDataForUnlockableQuery>, res) => {
unlockableId: string
}>,
res,
) => {
// We make this lookup table to quickly get it, there's no other quick way for it. // We make this lookup table to quickly get it, there's no other quick way for it.
const unlockToLoc = { const unlockToLoc: Record<string, string> = {
FIREARMS_SC_HERO_SNIPER_HM: "LOCATION_PARENT_AUSTRIA", FIREARMS_SC_HERO_SNIPER_HM: "LOCATION_PARENT_AUSTRIA",
FIREARMS_SC_HERO_SNIPER_KNIGHT: "LOCATION_PARENT_AUSTRIA", FIREARMS_SC_HERO_SNIPER_KNIGHT: "LOCATION_PARENT_AUSTRIA",
FIREARMS_SC_HERO_SNIPER_STONE: "LOCATION_PARENT_AUSTRIA", FIREARMS_SC_HERO_SNIPER_STONE: "LOCATION_PARENT_AUSTRIA",

View File

@ -21,8 +21,8 @@ import type {
Campaign, Campaign,
GameVersion, GameVersion,
GenSingleMissionFunc, GenSingleMissionFunc,
ICampaignMission, CampaignMission,
ICampaignVideo, CampaignVideo,
IVideo, IVideo,
StoryData, StoryData,
} from "../types/types" } from "../types/types"
@ -37,7 +37,7 @@ const genSingleMissionFactory = (userId: string): GenSingleMissionFunc => {
return function genSingleMission( return function genSingleMission(
contractId: string, contractId: string,
gameVersion: GameVersion, gameVersion: GameVersion,
): ICampaignMission { ): CampaignMission {
assert.ok( assert.ok(
contractId, contractId,
"Plugin tried to generate mission with no contract ID", "Plugin tried to generate mission with no contract ID",
@ -51,11 +51,12 @@ const genSingleMissionFactory = (userId: string): GenSingleMissionFunc => {
if (!actualContractData) { if (!actualContractData) {
log(LogLevel.ERROR, `Failed to resolve contract ${contractId}!`) log(LogLevel.ERROR, `Failed to resolve contract ${contractId}!`)
assert.fail(`Failed to resolve contract ${contractId}! (campaign)`)
} }
return { return {
Type: "Mission", Type: "Mission",
Data: contractIdToHitObject(contractId, gameVersion, userId), Data: contractIdToHitObject(contractId, gameVersion, userId)!,
} }
} }
} }
@ -63,7 +64,7 @@ const genSingleMissionFactory = (userId: string): GenSingleMissionFunc => {
function genSingleVideo( function genSingleVideo(
videoId: string, videoId: string,
gameVersion: GameVersion, gameVersion: GameVersion,
): ICampaignVideo { ): CampaignVideo {
const videos = getConfig<Record<string, IVideo>>("Videos", true) // we modify videos so we need to clone this const videos = getConfig<Record<string, IVideo>>("Videos", true) // we modify videos so we need to clone this
const video = videos[videoId] const video = videos[videoId]

View File

@ -18,16 +18,18 @@
import { getConfig, getVersionedConfig } from "../configSwizzleManager" import { getConfig, getVersionedConfig } from "../configSwizzleManager"
import type { import type {
ChallengeCompletion,
CompiledChallengeTreeCategory,
CompletionData, CompletionData,
GameLocationsData, GameLocationsData,
GameVersion, GameVersion,
IHit, IHit,
JwtData,
MissionStory, MissionStory,
OpportunityStatistics, OpportunityStatistics,
PeacockLocationsData, PeacockLocationsData,
Unlockable, Unlockable,
} from "../types/types" } from "../types/types"
import type { MasteryData } from "../types/mastery"
import { contractIdToHitObject, controller } from "../controller" import { contractIdToHitObject, controller } from "../controller"
import { generateCompletionData } from "../contracts/dataGen" import { generateCompletionData } from "../contracts/dataGen"
import { getUserData } from "../databaseHandler" import { getUserData } from "../databaseHandler"
@ -37,6 +39,17 @@ import { createInventory } from "../inventory"
import { log, LogLevel } from "../loggingInterop" import { log, LogLevel } from "../loggingInterop"
import { no2016 } from "../contracts/escalations/escalationService" import { no2016 } from "../contracts/escalations/escalationService"
import { missionsInLocations } from "../contracts/missionsInLocation" import { missionsInLocations } from "../contracts/missionsInLocation"
import assert from "assert"
type LegacyData = {
[difficulty: string]: {
ChallengeCompletion: {
ChallengesCount: number
CompletedChallengesCount: number
}
CompletionData: CompletionData
}
}
type GameFacingDestination = { type GameFacingDestination = {
ChallengeCompletion: { ChallengeCompletion: {
@ -48,14 +61,45 @@ type GameFacingDestination = {
LocationCompletionPercent: number LocationCompletionPercent: number
Location: Unlockable Location: Unlockable
// H2016 only // H2016 only
Data?: { Data?: LegacyData
[difficulty: string]: { }
ChallengeCompletion: {
ChallengesCount: number type LocationMissionData = {
CompletedChallengesCount: number Location: Unlockable
} SubLocation: Unlockable
CompletionData: CompletionData Missions: IHit[]
SarajevoSixMissions: IHit[]
ElusiveMissions: IHit[]
EscalationMissions: IHit[]
SniperMissions: IHit[]
PlaceholderMissions: IHit[]
CampaignMissions: IHit[]
CompletionData: CompletionData
}
type GameDestination = {
ChallengeData: {
Children: CompiledChallengeTreeCategory[]
}
DifficultyData: {
AvailableDifficultyModes: {
Name: string
Available: boolean
}[]
Difficulty: string | undefined
LocationId: string
}
Location: Unlockable
MasteryData: MasteryData | MasteryData[] | Record<string, never>
MissionData: {
ChallengeCompletion: ChallengeCompletion
Location: Unlockable
LocationCompletionPercent: number
OpportunityStatistics: {
Completed: number
Count: number
} }
SubLocationMissionsData: LocationMissionData[]
} }
} }
@ -63,14 +107,14 @@ export function getDestinationCompletion(
parent: Unlockable, parent: Unlockable,
child: Unlockable | undefined, child: Unlockable | undefined,
gameVersion: GameVersion, gameVersion: GameVersion,
jwt: JwtData, userId: string,
) { ) {
const missionStories = getConfig<Record<string, MissionStory>>( const missionStories = getConfig<Record<string, MissionStory>>(
"MissionStories", "MissionStories",
false, false,
) )
const userData = getUserData(jwt.unique_name, gameVersion) const userData = getUserData(userId, gameVersion)
const challenges = controller.challengeService.getGroupedChallengeLists( const challenges = controller.challengeService.getGroupedChallengeLists(
{ {
type: ChallengeFilterType.ParentLocation, type: ChallengeFilterType.ParentLocation,
@ -123,21 +167,10 @@ export function getCompletionPercent(
opportunityDone: number, opportunityDone: number,
opportunityTotal: number, opportunityTotal: number,
): number { ): number {
if (challengeDone === undefined) { challengeDone ??= 0
challengeDone = 0 challengeTotal ??= 0
} opportunityDone ??= 0
opportunityTotal ??= 0
if (challengeTotal === undefined) {
challengeTotal = 0
}
if (opportunityDone === undefined) {
opportunityDone = 0
}
if (opportunityTotal === undefined) {
opportunityTotal = 0
}
const totalCompletables = challengeTotal + opportunityTotal const totalCompletables = challengeTotal + opportunityTotal
const totalCompleted = challengeDone + opportunityDone const totalCompleted = challengeDone + opportunityDone
@ -150,11 +183,11 @@ export function getCompletionPercent(
* Get the list of destinations used by the `/profiles/page/Destinations` endpoint. * Get the list of destinations used by the `/profiles/page/Destinations` endpoint.
* *
* @param gameVersion * @param gameVersion
* @param jwt * @param userId The user ID.
*/ */
export function getAllGameDestinations( export function getAllGameDestinations(
gameVersion: GameVersion, gameVersion: GameVersion,
jwt: JwtData, userId: string,
): GameFacingDestination[] { ): GameFacingDestination[] {
const result: GameFacingDestination[] = [] const result: GameFacingDestination[] = []
const locations = getVersionedConfig<PeacockLocationsData>( const locations = getVersionedConfig<PeacockLocationsData>(
@ -169,38 +202,13 @@ export function getAllGameDestinations(
"UI_LOCATION_PARENT_" + destination.substring(16) + "_NAME" "UI_LOCATION_PARENT_" + destination.substring(16) + "_NAME"
const template: GameFacingDestination = { const template: GameFacingDestination = {
...getDestinationCompletion(parent, undefined, gameVersion, jwt), ...getDestinationCompletion(parent, undefined, gameVersion, userId),
...{ ...{
CompletionData: generateCompletionData( CompletionData: generateCompletionData(
destination, destination,
jwt.unique_name, userId,
gameVersion, gameVersion,
), ),
Data:
gameVersion === "h1"
? {
normal: {
ChallengeCompletion: undefined,
CompletionData: generateCompletionData(
destination,
jwt.unique_name,
gameVersion,
"mission",
"normal",
),
},
pro1: {
ChallengeCompletion: undefined,
CompletionData: generateCompletionData(
destination,
jwt.unique_name,
gameVersion,
"mission",
"pro1",
),
},
}
: undefined,
}, },
} }
@ -208,10 +216,28 @@ export function getAllGameDestinations(
// There are different challenges for normal and pro1 in 2016, right now, we do not support this. // There are different challenges for normal and pro1 in 2016, right now, we do not support this.
// We're just reusing this for now. // We're just reusing this for now.
if (gameVersion === "h1") { if (gameVersion === "h1") {
template.Data.normal.ChallengeCompletion = template.Data = {
template.ChallengeCompletion normal: {
template.Data.pro1.ChallengeCompletion = ChallengeCompletion: template.ChallengeCompletion,
template.ChallengeCompletion CompletionData: generateCompletionData(
destination,
userId,
gameVersion,
"mission",
"normal",
),
},
pro1: {
ChallengeCompletion: template.ChallengeCompletion,
CompletionData: generateCompletionData(
destination,
userId,
gameVersion,
"mission",
"pro1",
),
},
} satisfies LegacyData
} }
result.push(template) result.push(template)
@ -258,10 +284,15 @@ export function createLocationsData(
} }
const sublocation = locData.children[sublocationId] const sublocation = locData.children[sublocationId]
if (!sublocation.Properties.ParentLocation) {
assert.fail("sublocation has no parent, that's illegal")
}
const parentLocation = const parentLocation =
locData.parents[sublocation.Properties.ParentLocation] locData.parents[sublocation.Properties.ParentLocation]
const creationContract = controller.resolveContract( const creationContract = controller.resolveContract(
sublocation.Properties.CreateContractId, sublocation.Properties.CreateContractId!,
) )
if (!creationContract && excludeIfNoContracts) { if (!creationContract && excludeIfNoContracts) {
@ -288,12 +319,18 @@ export function createLocationsData(
return finalData return finalData
} }
// TODO: this is a mess, write docs and type explicitly /**
* This gets the game-facing data for a destination.
*
* @param query
* @param gameVersion
* @param userId
*/
export function getDestination( export function getDestination(
query: GetDestinationQuery, query: GetDestinationQuery,
gameVersion: GameVersion, gameVersion: GameVersion,
jwt: JwtData, userId: string,
) { ): GameDestination {
const LOCATION = query.locationId const LOCATION = query.locationId
const locData = getVersionedConfig<PeacockLocationsData>( const locData = getVersionedConfig<PeacockLocationsData>(
@ -306,18 +343,30 @@ export function getDestination(
const masteryData = controller.masteryService.getMasteryDataForDestination( const masteryData = controller.masteryService.getMasteryDataForDestination(
query.locationId, query.locationId,
gameVersion, gameVersion,
jwt.unique_name, userId,
query.difficulty, query.difficulty,
) )
const response = { let resMasteryData: GameDestination["MasteryData"]
Location: {},
if (LOCATION !== "LOCATION_PARENT_ICA_FACILITY") {
if (gameVersion === "h1") {
resMasteryData = masteryData[0]
} else {
resMasteryData = masteryData
}
} else {
resMasteryData = {}
}
const response: Partial<GameDestination> = {
Location: locationData,
MissionData: { MissionData: {
...getDestinationCompletion( ...getDestinationCompletion(
locationData, locationData,
undefined, undefined,
gameVersion, gameVersion,
jwt, userId,
), ),
...{ SubLocationMissionsData: [] }, ...{ SubLocationMissionsData: [] },
}, },
@ -326,20 +375,14 @@ export function getDestination(
controller.challengeService.getChallengeDataForDestination( controller.challengeService.getChallengeDataForDestination(
query.locationId, query.locationId,
gameVersion, gameVersion,
jwt.unique_name, userId,
), ),
}, },
MasteryData: MasteryData: resMasteryData,
LOCATION !== "LOCATION_PARENT_ICA_FACILITY"
? gameVersion === "h1"
? masteryData[0]
: masteryData
: {},
DifficultyData: undefined,
} }
if (gameVersion === "h1" && LOCATION !== "LOCATION_PARENT_ICA_FACILITY") { if (gameVersion === "h1" && LOCATION !== "LOCATION_PARENT_ICA_FACILITY") {
const inventory = createInventory(jwt.unique_name, gameVersion) const inventory = createInventory(userId, gameVersion)
response.DifficultyData = { response.DifficultyData = {
AvailableDifficultyModes: [ AvailableDifficultyModes: [
@ -352,7 +395,7 @@ export function getDestination(
Available: inventory.some( Available: inventory.some(
(e) => (e) =>
e.Unlockable.Id === e.Unlockable.Id ===
locationData.Properties.DifficultyUnlock.pro1, locationData.Properties.DifficultyUnlock?.pro1,
), ),
}, },
], ],
@ -369,15 +412,15 @@ export function getDestination(
(subLocation) => subLocation.Properties.ParentLocation === LOCATION, (subLocation) => subLocation.Properties.ParentLocation === LOCATION,
) )
response.Location = locationData
if (query.difficulty === "pro1") { if (query.difficulty === "pro1") {
const obj = { type Cast = keyof typeof controller.missionsInLocations.pro1
const obj: LocationMissionData = {
Location: locationData, Location: locationData,
SubLocation: locationData, SubLocation: locationData,
Missions: [controller.missionsInLocations.pro1[LOCATION]].map( Missions: [controller.missionsInLocations.pro1[LOCATION as Cast]]
(id) => contractIdToHitObject(id, gameVersion, jwt.unique_name), .map((id) => contractIdToHitObject(id, gameVersion, userId))
), .filter(Boolean) as IHit[],
SarajevoSixMissions: [], SarajevoSixMissions: [],
ElusiveMissions: [], ElusiveMissions: [],
EscalationMissions: [], EscalationMissions: [],
@ -386,14 +429,14 @@ export function getDestination(
CampaignMissions: [], CampaignMissions: [],
CompletionData: generateCompletionData( CompletionData: generateCompletionData(
sublocationsData[0].Id, sublocationsData[0].Id,
jwt.unique_name, userId,
gameVersion, gameVersion,
), ),
} }
response.MissionData.SubLocationMissionsData.push(obj) response.MissionData?.SubLocationMissionsData.push(obj)
return response return response as GameDestination
} }
for (const e of sublocationsData) { for (const e of sublocationsData) {
@ -401,6 +444,7 @@ export function getDestination(
const escalations: IHit[] = [] const escalations: IHit[] = []
type ECast = keyof typeof controller.missionsInLocations.escalations
// every unique escalation from the sublocation // every unique escalation from the sublocation
const allUniqueEscalations: string[] = [ const allUniqueEscalations: string[] = [
...(gameVersion === "h1" && e.Id === "LOCATION_ICA_FACILITY" ...(gameVersion === "h1" && e.Id === "LOCATION_ICA_FACILITY"
@ -409,7 +453,7 @@ export function getDestination(
] ]
: []), : []),
...new Set<string>( ...new Set<string>(
controller.missionsInLocations.escalations[e.Id] || [], controller.missionsInLocations.escalations[e.Id as ECast] || [],
), ),
] ]
@ -419,7 +463,7 @@ export function getDestination(
const details = contractIdToHitObject( const details = contractIdToHitObject(
escalation, escalation,
gameVersion, gameVersion,
jwt.unique_name, userId,
) )
if (details) { if (details) {
@ -428,17 +472,18 @@ export function getDestination(
} }
const sniperMissions: IHit[] = [] const sniperMissions: IHit[] = []
type SCast = keyof typeof controller.missionsInLocations.sniper
for (const sniperMission of controller.missionsInLocations.sniper[ for (const sniperMission of controller.missionsInLocations.sniper[
e.Id e.Id as SCast
] ?? []) { ] ?? []) {
sniperMissions.push( const hit = contractIdToHitObject(
contractIdToHitObject( sniperMission,
sniperMission, gameVersion,
gameVersion, userId,
jwt.unique_name,
),
) )
if (hit) sniperMissions.push(hit)
} }
const obj = { const obj = {
@ -451,11 +496,7 @@ export function getDestination(
SniperMissions: sniperMissions, SniperMissions: sniperMissions,
PlaceholderMissions: [], PlaceholderMissions: [],
CampaignMissions: [], CampaignMissions: [],
CompletionData: generateCompletionData( CompletionData: generateCompletionData(e.Id, userId, gameVersion),
e.Id,
jwt.unique_name,
gameVersion,
),
} }
const types = [ const types = [
@ -464,6 +505,7 @@ export function getDestination(
["elusive", "ElusiveMissions"], ["elusive", "ElusiveMissions"],
], ],
...((gameVersion === "h1" && ...((gameVersion === "h1" &&
// @ts-expect-error Hack.
missionsInLocations.sarajevo["h2016enabled"]) || missionsInLocations.sarajevo["h2016enabled"]) ||
gameVersion === "h3" gameVersion === "h3"
? [["sarajevo", "SarajevoSixMissions"]] ? [["sarajevo", "SarajevoSixMissions"]]
@ -472,8 +514,10 @@ export function getDestination(
for (const t of types) { for (const t of types) {
let theMissions: string[] | undefined = !t[0] // no specific type let theMissions: string[] | undefined = !t[0] // no specific type
? controller.missionsInLocations[e.Id] ? // @ts-expect-error Yup.
: controller.missionsInLocations[t[0]][e.Id] controller.missionsInLocations[e.Id]
: // @ts-expect-error Yup.
controller.missionsInLocations[t[0]][e.Id]
// edge case: ica facility in h1 was only 1 sublocation, so we merge // edge case: ica facility in h1 was only 1 sublocation, so we merge
// these into a single array // these into a single array
@ -504,16 +548,17 @@ export function getDestination(
const mission = contractIdToHitObject( const mission = contractIdToHitObject(
c, c,
gameVersion, gameVersion,
jwt.unique_name, userId,
) )
// @ts-expect-error Yup.
obj[t[1]].push(mission) obj[t[1]].push(mission)
} }
} }
} }
response.MissionData.SubLocationMissionsData.push(obj) response.MissionData?.SubLocationMissionsData.push(obj)
} }
return response return response as GameDestination
} }

View File

@ -82,7 +82,7 @@ export function withLookupDialog(
contract, contract,
req.jwt.unique_name, req.jwt.unique_name,
req.gameVersion, req.gameVersion,
), )!,
}, },
...(flag && { AddedSuccessfully: true }), ...(flag && { AddedSuccessfully: true }),
} }

View File

@ -16,7 +16,12 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import type { GameVersion, JwtData, PeacockLocationsData } from "../types/types" import type {
CompletionData,
GameVersion,
PeacockLocationsData,
Unlockable,
} from "../types/types"
import { swapToBrowsingMenusStatus } from "../discordRp" import { swapToBrowsingMenusStatus } from "../discordRp"
import { getUserData } from "../databaseHandler" import { getUserData } from "../databaseHandler"
import { controller } from "../controller" import { controller } from "../controller"
@ -29,10 +34,32 @@ import {
import { createLocationsData, getAllGameDestinations } from "./destinations" import { createLocationsData, getAllGameDestinations } from "./destinations"
import { makeCampaigns } from "./campaigns" import { makeCampaigns } from "./campaigns"
export function getHubData(gameVersion: GameVersion, jwt: JwtData) { type CareerEntry = {
Children: CareerEntryChild[]
Name: string
Location: Unlockable
}
type CareerEntryChild = {
IsLocked: boolean
Name: string
Image: string
Icon: string
CompletedChallengesCount: number
ChallengesCount: number
CategoryId: string
Description: string
Location: Unlockable
ImageLocked: string
RequiredResources: string[]
IsPack?: boolean
CompletionData: CompletionData
}
export function getHubData(gameVersion: GameVersion, userId: string) {
swapToBrowsingMenusStatus(gameVersion) swapToBrowsingMenusStatus(gameVersion)
const userdata = getUserData(jwt.unique_name, gameVersion) const userdata = getUserData(userId, gameVersion)
const contractCreationTutorial = const contractCreationTutorial =
gameVersion !== "scpc" gameVersion !== "scpc"
@ -44,7 +71,7 @@ export function getHubData(gameVersion: GameVersion, jwt: JwtData) {
gameVersion, gameVersion,
true, true,
) )
const career = const career: Record<string, CareerEntry> =
gameVersion === "h3" gameVersion === "h3"
? {} ? {}
: { : {
@ -73,7 +100,7 @@ export function getHubData(gameVersion: GameVersion, jwt: JwtData) {
controller.masteryService.getMasteryDataForDestination( controller.masteryService.getMasteryDataForDestination(
parent, parent,
gameVersion, gameVersion,
jwt.unique_name, userId,
).length ).length
) { ) {
const completionData = const completionData =
@ -81,7 +108,7 @@ export function getHubData(gameVersion: GameVersion, jwt: JwtData) {
parent, parent,
parent, parent,
gameVersion, gameVersion,
jwt.unique_name, userId,
parent.includes("SNUG") ? "evergreen" : "mission", parent.includes("SNUG") ? "evergreen" : "mission",
gameVersion === "h1" ? "normal" : undefined, gameVersion === "h1" ? "normal" : undefined,
) )
@ -100,7 +127,7 @@ export function getHubData(gameVersion: GameVersion, jwt: JwtData) {
parent, parent,
parent, parent,
gameVersion, gameVersion,
jwt.unique_name, userId,
parent.includes("SNUG") parent.includes("SNUG")
? "evergreen" ? "evergreen"
: "mission", : "mission",
@ -137,14 +164,14 @@ export function getHubData(gameVersion: GameVersion, jwt: JwtData) {
const challengeCompletion = const challengeCompletion =
controller.challengeService.countTotalNCompletedChallenges( controller.challengeService.countTotalNCompletedChallenges(
challenges, challenges,
jwt.unique_name, userId,
gameVersion, gameVersion,
) )
career[parent]?.Children.push({ career[parent!]?.Children.push({
IsLocked: location.Properties.IsLocked, IsLocked: Boolean(location.Properties.IsLocked),
Name: location.DisplayNameLocKey, Name: location.DisplayNameLocKey,
Image: location.Properties.Icon, Image: location.Properties.Icon || "",
Icon: location.Type, // should be "location" for all locations Icon: location.Type, // should be "location" for all locations
CompletedChallengesCount: CompletedChallengesCount:
challengeCompletion.CompletedChallengesCount, challengeCompletion.CompletedChallengesCount,
@ -152,14 +179,10 @@ export function getHubData(gameVersion: GameVersion, jwt: JwtData) {
CategoryId: child, CategoryId: child,
Description: `UI_${child}_PRIMARY_DESC`, Description: `UI_${child}_PRIMARY_DESC`,
Location: location, Location: location,
ImageLocked: location.Properties.LockedIcon, ImageLocked: location.Properties.LockedIcon || "",
RequiredResources: location.Properties.RequiredResources, RequiredResources: location.Properties.RequiredResources || [],
IsPack: false, // should be false for all locations IsPack: false, // should be false for all locations
CompletionData: generateCompletionData( CompletionData: generateCompletionData(child, userId, gameVersion),
child,
jwt.unique_name,
gameVersion,
),
}) })
} }
@ -176,10 +199,10 @@ export function getHubData(gameVersion: GameVersion, jwt: JwtData) {
}, },
}, },
DashboardData: [], DashboardData: [],
DestinationsData: getAllGameDestinations(gameVersion, jwt), DestinationsData: getAllGameDestinations(gameVersion, userId),
CreateContractTutorial: generateUserCentric( CreateContractTutorial: generateUserCentric(
contractCreationTutorial, contractCreationTutorial,
jwt.unique_name, userId,
gameVersion, gameVersion,
), ),
LocationsData: createLocationsData(gameVersion, true), LocationsData: createLocationsData(gameVersion, true),
@ -189,7 +212,7 @@ export function getHubData(gameVersion: GameVersion, jwt: JwtData) {
}, },
MasteryData: masteryData, MasteryData: masteryData,
}, },
StoryData: makeCampaigns(gameVersion, jwt.unique_name), StoryData: makeCampaigns(gameVersion, userId),
FilterData: getVersionedConfig("FilterData", gameVersion, false), FilterData: getVersionedConfig("FilterData", gameVersion, false),
StoreData: getVersionedConfig("StoreData", gameVersion, false), StoreData: getVersionedConfig("StoreData", gameVersion, false),
IOIAccountStatus: { IOIAccountStatus: {

View File

@ -544,8 +544,11 @@ export const menuSystemDatabase = new MenuSystemDatabase()
menuSystemRouter.get( menuSystemRouter.get(
"/dynamic_resources_pc_release_rpkg", "/dynamic_resources_pc_release_rpkg",
// @ts-expect-error No type issue is actually here.
async (req: RequestWithJwt, res) => { async (req: RequestWithJwt, res) => {
const dynamicResourceName = `dynamic_resources_${req.gameVersion}.rpkg` const dynamicResourceName = `dynamic_resources_${
req.gameVersion === "scpc" ? "h1" : req.gameVersion
}.rpkg`
const dynamicResourcePath = join( const dynamicResourcePath = join(
PEACOCK_DEV ? process.cwd() : __dirname, PEACOCK_DEV ? process.cwd() : __dirname,
"resources", "resources",
@ -565,6 +568,7 @@ menuSystemRouter.get(
}, },
) )
// @ts-expect-error No type issue is actually here.
menuSystemRouter.use("/menusystem/", MenuSystemDatabase.configMiddleware) menuSystemRouter.use("/menusystem/", MenuSystemDatabase.configMiddleware)
// Miranda Jamison's image path in the repository is escaped for some reason // Miranda Jamison's image path in the repository is escaped for some reason
@ -587,6 +591,7 @@ menuSystemPreRouter.get(
menuSystemRouter.use( menuSystemRouter.use(
"/images/", "/images/",
// @ts-expect-error No type issue is actually here.
serveStatic("images", { fallthrough: true }), serveStatic("images", { fallthrough: true }),
imageFetchingMiddleware, imageFetchingMiddleware,
) )

View File

@ -19,9 +19,9 @@
import type { import type {
CompiledChallengeTreeCategory, CompiledChallengeTreeCategory,
GameVersion, GameVersion,
JwtData,
MissionManifest, MissionManifest,
MissionStory, MissionStory,
ProgressionData,
SceneConfig, SceneConfig,
Unlockable, Unlockable,
UserCentricContract, UserCentricContract,
@ -51,11 +51,12 @@ import {
} from "../utils" } from "../utils"
import { createInventory, getUnlockableById } from "../inventory" import { createInventory, getUnlockableById } from "../inventory"
import { createSniperLoadouts } from "./sniper" import { createSniperLoadouts, SniperCharacter, SniperLoadout } from "./sniper"
import { getFlag } from "../flags" import { getFlag } from "../flags"
import { loadouts } from "../loadouts" import { loadouts } from "../loadouts"
import { resolveProfiles } from "../profileHandler" import { resolveProfiles } from "../profileHandler"
import { userAuths } from "../officialServerAuth" import { userAuths } from "../officialServerAuth"
import assert from "assert"
export type PlanningError = { error: boolean } export type PlanningError = { error: boolean }
@ -77,11 +78,11 @@ export type GamePlanningData = {
IsFirstInGroup: boolean IsFirstInGroup: boolean
Creator: UserProfile Creator: UserProfile
UserContract?: boolean UserContract?: boolean
UnlockedEntrances?: string[] UnlockedEntrances?: string[] | null
UnlockedAgencyPickups?: string[] UnlockedAgencyPickups?: string[] | null
Objectives?: unknown Objectives?: unknown
GroupData?: PlanningGroupData GroupData?: PlanningGroupData
Entrances: Unlockable[] Entrances: Unlockable[] | null
Location: Unlockable Location: Unlockable
LoadoutData: unknown LoadoutData: unknown
LimitedLoadoutUnlockLevel: number LimitedLoadoutUnlockLevel: number
@ -113,7 +114,7 @@ export type GamePlanningData = {
export async function getPlanningData( export async function getPlanningData(
contractId: string, contractId: string,
resetEscalation: boolean, resetEscalation: boolean,
jwt: JwtData, userId: string,
gameVersion: GameVersion, gameVersion: GameVersion,
): Promise<PlanningError | GamePlanningData> { ): Promise<PlanningError | GamePlanningData> {
const entranceData = getConfig<SceneConfig>("Entrances", false) const entranceData = getConfig<SceneConfig>("Entrances", false)
@ -122,7 +123,7 @@ export async function getPlanningData(
false, false,
) )
const userData = getUserData(jwt.unique_name, gameVersion) const userData = getUserData(userId, gameVersion)
for (const ms in userData.Extensions.opportunityprogression) { for (const ms in userData.Extensions.opportunityprogression) {
if (Object.keys(missionStories).includes(ms)) { if (Object.keys(missionStories).includes(ms)) {
@ -130,13 +131,24 @@ export async function getPlanningData(
} }
} }
let contractData = let contractData: MissionManifest | undefined
if (
gameVersion === "h1" && gameVersion === "h1" &&
contractId === "42bac555-bbb9-429d-a8ce-f1ffdf94211c" contractId === "42bac555-bbb9-429d-a8ce-f1ffdf94211c"
? _legacyBull ) {
: contractId === "ff9f46cf-00bd-4c12-b887-eac491c3a96d" contractData = _legacyBull
? _theLastYardbirdScpc } else if (contractId === "ff9f46cf-00bd-4c12-b887-eac491c3a96d") {
: controller.resolveContract(contractId) contractData = _theLastYardbirdScpc
} else {
contractData = controller.resolveContract(contractId)
}
if (!contractData) {
return {
error: true,
}
}
if (resetEscalation) { if (resetEscalation) {
const escalationGroupId = const escalationGroupId =
@ -144,10 +156,20 @@ export async function getPlanningData(
resetUserEscalationProgress(userData, escalationGroupId) resetUserEscalationProgress(userData, escalationGroupId)
writeUserData(jwt.unique_name, gameVersion) writeUserData(userId, gameVersion)
const group = controller.escalationMappings.get(escalationGroupId)
if (!group) {
log(
LogLevel.ERROR,
`Unknown escalation group: ${escalationGroupId}`,
)
return { error: true }
}
// now reassign properties and continue // now reassign properties and continue
contractId = controller.escalationMappings.get(escalationGroupId)["1"] contractId = group["1"]
contractData = controller.resolveContract(contractId) contractData = controller.resolveContract(contractId)
} }
@ -161,20 +183,29 @@ export async function getPlanningData(
LogLevel.WARN, LogLevel.WARN,
`Trying to download contract ${contractId} due to it not found locally.`, `Trying to download contract ${contractId} due to it not found locally.`,
) )
const user = userAuths.get(jwt.unique_name) const user = userAuths.get(userId)
const resp = await user._useService( const resp = await user?._useService(
`https://${getRemoteService( `https://${getRemoteService(
gameVersion, gameVersion,
)}.hitman.io/profiles/page/Planning?contractid=${contractId}&resetescalation=false&forcecurrentcontract=false&errorhandling=false`, )}.hitman.io/profiles/page/Planning?contractid=${contractId}&resetescalation=false&forcecurrentcontract=false&errorhandling=false`,
true, true,
) )
contractData = resp.data.data.Contract contractData = resp?.data.data.Contract
if (!contractData) {
log(
LogLevel.ERROR,
`Official planning lookup no result: ${contractId}`,
)
return { error: true }
}
controller.fetchedContracts.set(contractData.Metadata.Id, contractData) controller.fetchedContracts.set(contractData.Metadata.Id, contractData)
} }
if (!contractData) { if (!contractData) {
log(LogLevel.ERROR, `Not found: ${contractId}, .`) log(LogLevel.ERROR, `Not found: ${contractId}, planning regular.`)
return { error: true } return { error: true }
} }
@ -198,6 +229,11 @@ export async function getPlanningData(
if (escalation) { if (escalation) {
const groupContractData = controller.resolveContract(escalationGroupId) const groupContractData = controller.resolveContract(escalationGroupId)
if (!groupContractData) {
log(LogLevel.ERROR, `Not found: ${contractId}, planning esc group`)
return { error: true }
}
const p = getUserEscalationProgress(userData, escalationGroupId) const p = getUserEscalationProgress(userData, escalationGroupId)
const done = const done =
@ -216,9 +252,12 @@ export async function getPlanningData(
// Fix contractData to the data of the level in the group. // Fix contractData to the data of the level in the group.
if (!contractData.Metadata.InGroup) { if (!contractData.Metadata.InGroup) {
contractData = controller.resolveContract( const newLevelId =
contractData.Metadata.GroupDefinition.Order[p - 1], contractData.Metadata.GroupDefinition?.Order[p - 1]
)
assert(typeof newLevelId === "string", "newLevelId is not a string")
contractData = controller.resolveContract(newLevelId)
} }
} }
@ -246,16 +285,21 @@ export async function getPlanningData(
const sublocation = getSubLocationFromContract(contractData, gameVersion) const sublocation = getSubLocationFromContract(contractData, gameVersion)
assert.ok(sublocation, "contract sublocation is null")
if (!entranceData[scenePath]) { if (!entranceData[scenePath]) {
log( log(
LogLevel.ERROR, LogLevel.ERROR,
`Could not find Entrance data for ${scenePath} (loc Planning)! This may cause an unhandled promise rejection.`, `Could not find Entrance data for ${scenePath} in planning`,
) )
return {
error: true,
}
} }
const entrancesInScene = entranceData[scenePath] const entrancesInScene = entranceData[scenePath]
const typedInv = createInventory(jwt.unique_name, gameVersion, sublocation) const typedInv = createInventory(userId, gameVersion, sublocation)
const unlockedEntrances = typedInv const unlockedEntrances = typedInv
.filter((item) => item.Unlockable.Type === "access") .filter((item) => item.Unlockable.Type === "access")
@ -267,6 +311,9 @@ export async function getPlanningData(
LogLevel.ERROR, LogLevel.ERROR,
"No matching entrance data found in planning, this is a bug!", "No matching entrance data found in planning, this is a bug!",
) )
return {
error: true,
}
} }
sublocation.DisplayNameLocKey = `UI_${sublocation.Id}_NAME` sublocation.DisplayNameLocKey = `UI_${sublocation.Id}_NAME`
@ -283,7 +330,7 @@ export async function getPlanningData(
let suit = getDefaultSuitFor(sublocation) let suit = getDefaultSuitFor(sublocation)
let tool1 = "TOKEN_FIBERWIRE" let tool1 = "TOKEN_FIBERWIRE"
let tool2 = "PROP_TOOL_COIN" let tool2 = "PROP_TOOL_COIN"
let briefcaseProp: string | undefined = undefined let briefcaseContainedItemId: string | undefined = undefined
let briefcaseId: string | undefined = undefined let briefcaseId: string | undefined = undefined
const dlForLocation = const dlForLocation =
@ -293,16 +340,13 @@ export async function getPlanningData(
contractData.Metadata.Location contractData.Metadata.Location
] ]
: // new loadout profiles system : // new loadout profiles system
Object.hasOwn( currentLoadout.data[contractData.Metadata.Location]
currentLoadout.data,
contractData.Metadata.Location,
) && currentLoadout.data[contractData.Metadata.Location]
if (dlForLocation) { if (dlForLocation) {
pistol = dlForLocation["2"] pistol = dlForLocation["2"]!
suit = dlForLocation["3"] suit = dlForLocation["3"]!
tool1 = dlForLocation["4"] tool1 = dlForLocation["4"]!
tool2 = dlForLocation["5"] tool2 = dlForLocation["5"]!
for (const key of Object.keys(dlForLocation)) { for (const key of Object.keys(dlForLocation)) {
if (["2", "3", "4", "5"].includes(key)) { if (["2", "3", "4", "5"].includes(key)) {
@ -311,28 +355,30 @@ export async function getPlanningData(
} }
briefcaseId = key briefcaseId = key
briefcaseProp = dlForLocation[key] // @ts-expect-error This will work.
briefcaseContainedItemId = dlForLocation[key]
} }
} }
const i = typedInv.find((item) => item.Unlockable.Id === briefcaseProp) const briefcaseContainedItem = typedInv.find(
(item) => item.Unlockable.Id === briefcaseContainedItemId,
const userCentric = generateUserCentric(
contractData,
jwt.unique_name,
gameVersion,
) )
const userCentric = generateUserCentric(contractData, userId, gameVersion)
const sniperLoadouts = createSniperLoadouts( const sniperLoadouts = createSniperLoadouts(
jwt.unique_name, userId,
gameVersion, gameVersion,
contractData, contractData,
) )
if (gameVersion === "scpc") { if (gameVersion === "scpc") {
for (const loadout of sniperLoadouts) { for (const loadout of sniperLoadouts) {
loadout["LoadoutData"] = loadout["Loadout"]["LoadoutData"] const l = loadout as SniperLoadout
delete loadout["Loadout"] l["LoadoutData"] = (loadout as SniperCharacter)["Loadout"][
"LoadoutData"
]
delete (loadout as Partial<SniperCharacter>)["Loadout"]
} }
} }
@ -395,19 +441,20 @@ export async function getPlanningData(
SlotId: "6", SlotId: "6",
Recommended: null, Recommended: null,
}, },
briefcaseId && { briefcaseId &&
SlotName: briefcaseProp, briefcaseContainedItem && {
SlotId: briefcaseId, SlotName: briefcaseContainedItemId,
Recommended: { SlotId: briefcaseId,
item: { Recommended: {
...i, item: {
Properties: {}, ...briefcaseContainedItem,
Properties: {},
},
type: briefcaseContainedItem.Unlockable.Id,
owned: true,
}, },
type: i.Unlockable.Id, IsContainer: true,
owned: true,
}, },
IsContainer: true,
},
].filter(Boolean) ].filter(Boolean)
/** /**
@ -426,7 +473,8 @@ export async function getPlanningData(
) { ) {
const loadoutUnlockable = getUnlockableById( const loadoutUnlockable = getUnlockableById(
gameVersion === "h1" gameVersion === "h1"
? sublocation?.Properties?.NormalLoadoutUnlock[ ? // @ts-expect-error This works.
sublocation?.Properties?.NormalLoadoutUnlock[
contractData.Metadata.Difficulty ?? "normal" contractData.Metadata.Difficulty ?? "normal"
] ]
: sublocation?.Properties?.NormalLoadoutUnlock, : sublocation?.Properties?.NormalLoadoutUnlock,
@ -440,23 +488,31 @@ export async function getPlanningData(
gameVersion, gameVersion,
) )
const locationProgression = const locationProgression: ProgressionData =
loadoutMasteryData && loadoutMasteryData?.SubPackageId
(loadoutMasteryData.SubPackageId ? // @ts-expect-error This works
? userData.Extensions.progression.Locations[ userData.Extensions.progression.Locations[
loadoutMasteryData.Location loadoutMasteryData.Location
][loadoutMasteryData.SubPackageId] ][loadoutMasteryData.SubPackageId]
: userData.Extensions.progression.Locations[ : userData.Extensions.progression.Locations[
loadoutMasteryData.Location loadoutMasteryData?.Location as unknown as string
]) ]
if (locationProgression.Level < loadoutMasteryData.Level) if (locationProgression.Level < (loadoutMasteryData?.Level || 0)) {
type S = {
SlotId: string
}
loadoutSlots = loadoutSlots.filter( loadoutSlots = loadoutSlots.filter(
(slot) => !["2", "4", "5"].includes(slot.SlotId), (slot) => !["2", "4", "5"].includes((slot as S)?.SlotId),
) )
}
} }
} }
assert.ok(contractData, "no contract data at final - planning")
type Cast = keyof typeof limitedLoadoutUnlockLevelMap
return { return {
Contract: contractData, Contract: contractData,
ElusiveContractState: "not_completed", ElusiveContractState: "not_completed",
@ -467,7 +523,7 @@ export async function getPlanningData(
UnlockedEntrances: UnlockedEntrances:
contractData.Metadata.Type === "sniper" contractData.Metadata.Type === "sniper"
? null ? null
: typedInv : (typedInv
.filter( .filter(
(item) => (item) =>
item.Unlockable.Subtype === "startinglocation", item.Unlockable.Subtype === "startinglocation",
@ -475,27 +531,28 @@ export async function getPlanningData(
.filter( .filter(
(item) => (item) =>
item.Unlockable.Properties.Difficulty === item.Unlockable.Properties.Difficulty ===
contractData.Metadata.Difficulty, contractData!.Metadata.Difficulty,
) )
.map((i) => i.Unlockable.Properties.RepositoryId) .map((i) => i.Unlockable.Properties.RepositoryId)
.filter((id) => id), .filter(Boolean) as string[]),
UnlockedAgencyPickups: UnlockedAgencyPickups:
contractData.Metadata.Type === "sniper" contractData.Metadata.Type === "sniper"
? null ? null
: typedInv : (typedInv
.filter((item) => item.Unlockable.Type === "agencypickup") .filter((item) => item.Unlockable.Type === "agencypickup")
.filter( .filter(
(item) => (item) =>
item.Unlockable.Properties.Difficulty === item.Unlockable.Properties.Difficulty ===
contractData.Metadata.Difficulty, // we already know it's not undefined
contractData!.Metadata.Difficulty,
) )
.map((i) => i.Unlockable.Properties.RepositoryId) .map((i) => i.Unlockable.Properties.RepositoryId)
.filter((id) => id), .filter(Boolean) as string[]),
Objectives: mapObjectives( Objectives: mapObjectives(
contractData.Data.Objectives, contractData.Data.Objectives!,
contractData.Data.GameChangers || [], contractData.Data.GameChangers || [],
contractData.Metadata.GroupObjectiveDisplayOrder || [], contractData.Metadata.GroupObjectiveDisplayOrder || [],
contractData.Metadata.IsEvergreenSafehouse, Boolean(contractData.Metadata.IsEvergreenSafehouse),
), ),
GroupData: groupData, GroupData: groupData,
Entrances: Entrances:
@ -504,27 +561,28 @@ export async function getPlanningData(
: unlockedEntrances : unlockedEntrances
.filter((unlockable) => .filter((unlockable) =>
entrancesInScene.includes( entrancesInScene.includes(
unlockable.Properties.RepositoryId, unlockable.Properties.RepositoryId || "",
), ),
) )
.filter( .filter(
(unlockable) => (unlockable) =>
unlockable.Properties.Difficulty === unlockable.Properties.Difficulty ===
contractData.Metadata.Difficulty, // we already know it's not undefined
contractData!.Metadata.Difficulty,
) )
.sort(unlockOrderComparer), .sort(unlockOrderComparer),
Location: sublocation, Location: sublocation,
LoadoutData: LoadoutData:
contractData.Metadata.Type === "sniper" ? null : loadoutSlots, contractData.Metadata.Type === "sniper" ? null : loadoutSlots,
LimitedLoadoutUnlockLevel: LimitedLoadoutUnlockLevel:
limitedLoadoutUnlockLevelMap[sublocation.Id] ?? 0, limitedLoadoutUnlockLevelMap[sublocation.Id as Cast] ?? 0,
CharacterLoadoutData: CharacterLoadoutData:
sniperLoadouts.length !== 0 ? sniperLoadouts : null, sniperLoadouts.length !== 0 ? sniperLoadouts : null,
ChallengeData: { ChallengeData: {
Children: controller.challengeService.getChallengeTreeForContract( Children: controller.challengeService.getChallengeTreeForContract(
contractId, contractId,
gameVersion, gameVersion,
jwt.unique_name, userId,
), ),
}, },
Currency: { Currency: {

View 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
}

View File

@ -21,11 +21,11 @@ import { generateUserCentric } from "../contracts/dataGen"
import { controller } from "../controller" import { controller } from "../controller"
import type { import type {
GameVersion, GameVersion,
JwtData,
MissionStory, MissionStory,
PlayNextCampaignDetails, PlayNextCampaignDetails,
UserCentricContract, UserCentricContract,
} from "../types/types" } from "../types/types"
import assert from "assert"
/** /**
* Main story campaign ordered mission IDs. * Main story campaign ordered mission IDs.
@ -157,6 +157,8 @@ export function createMainOpportunityTile(
false, false,
) )
assert.ok(contractData)
return { return {
CategoryType: "MainOpportunity", CategoryType: "MainOpportunity",
CategoryName: "UI_PLAYNEXT_MAINOPPORTUNITY_CATEGORY_NAME", CategoryName: "UI_PLAYNEXT_MAINOPPORTUNITY_CATEGORY_NAME",
@ -202,7 +204,7 @@ export type GameFacingPlayNextData = {
export function getGamePlayNextData( export function getGamePlayNextData(
contractId: string, contractId: string,
jwt: JwtData, userId: string,
gameVersion: GameVersion, gameVersion: GameVersion,
): GameFacingPlayNextData { ): GameFacingPlayNextData {
const cats: PlayNextCategory[] = [] const cats: PlayNextCategory[] = []
@ -225,14 +227,9 @@ export function getGamePlayNextData(
if (shouldContinue) { if (shouldContinue) {
cats.push( cats.push(
createPlayNextMission( createPlayNextMission(userId, nextMissionId, gameVersion, {
jwt.unique_name, CampaignName: `UI_SEASON_${nextSeasonId}`,
nextMissionId, }),
gameVersion,
{
CampaignName: `UI_SEASON_${nextSeasonId}`,
},
),
) )
} }
@ -244,7 +241,7 @@ export function getGamePlayNextData(
if (pzIdIndex !== -1 && pzIdIndex !== orderedPZMissions.length - 1) { if (pzIdIndex !== -1 && pzIdIndex !== orderedPZMissions.length - 1) {
const nextMissionId = orderedPZMissions[pzIdIndex + 1] const nextMissionId = orderedPZMissions[pzIdIndex + 1]
cats.push( cats.push(
createPlayNextMission(jwt.unique_name, nextMissionId, gameVersion, { createPlayNextMission(userId, nextMissionId, gameVersion, {
CampaignName: "UI_CONTRACT_CAMPAIGN_WHITE_SPIDER_TITLE", CampaignName: "UI_CONTRACT_CAMPAIGN_WHITE_SPIDER_TITLE",
ParentCampaignName: "UI_MENU_PAGE_SIDE_MISSIONS_TITLE", ParentCampaignName: "UI_MENU_PAGE_SIDE_MISSIONS_TITLE",
}), }),
@ -255,7 +252,7 @@ export function getGamePlayNextData(
if (contractId === "f1ba328f-e3dd-4ef8-bb26-0363499fdd95") { if (contractId === "f1ba328f-e3dd-4ef8-bb26-0363499fdd95") {
const nextMissionId = "0b616e62-af0c-495b-82e3-b778e82b5912" const nextMissionId = "0b616e62-af0c-495b-82e3-b778e82b5912"
cats.push( cats.push(
createPlayNextMission(jwt.unique_name, nextMissionId, gameVersion, { createPlayNextMission(userId, nextMissionId, gameVersion, {
CampaignName: "UI_MENU_PAGE_SPECIAL_ASSIGNMENTS_TITLE", CampaignName: "UI_MENU_PAGE_SPECIAL_ASSIGNMENTS_TITLE",
ParentCampaignName: "UI_MENU_PAGE_SIDE_MISSIONS_TITLE", ParentCampaignName: "UI_MENU_PAGE_SIDE_MISSIONS_TITLE",
}), }),
@ -274,7 +271,7 @@ export function getGamePlayNextData(
if (pluginData) { if (pluginData) {
if (pluginData.overrideIndex !== undefined) { if (pluginData.overrideIndex !== undefined) {
cats[pluginData.overrideIndex] = createPlayNextMission( cats[pluginData.overrideIndex] = createPlayNextMission(
jwt.unique_name, userId,
pluginData.nextContractId, pluginData.nextContractId,
gameVersion, gameVersion,
pluginData.campaignDetails, pluginData.campaignDetails,
@ -282,7 +279,7 @@ export function getGamePlayNextData(
} else { } else {
cats.push( cats.push(
createPlayNextMission( createPlayNextMission(
jwt.unique_name, userId,
pluginData.nextContractId, pluginData.nextContractId,
gameVersion, gameVersion,
pluginData.campaignDetails, pluginData.campaignDetails,
@ -293,6 +290,6 @@ export function getGamePlayNextData(
return { return {
Categories: cats, Categories: cats,
ProfileId: jwt.unique_name, ProfileId: userId,
} }
} }

View File

@ -17,8 +17,44 @@
*/ */
import { controller } from "../controller" import { controller } from "../controller"
import type { GameVersion, MissionManifest } from "../types/types" import type {
CompletionData,
GameVersion,
MissionManifest,
} from "../types/types"
import { getSubLocationByName } from "../contracts/dataGen" import { getSubLocationByName } from "../contracts/dataGen"
import { InventoryItem } from "../inventory"
import assert from "assert"
export type SniperCharacter = {
Id: string
Loadout: SniperLoadout
CompletionData: CompletionData
}
export type SniperLoadout = {
LoadoutData: {
SlotId: string
SlotName: string
Items: {
Item: InventoryItem
ItemDetails: unknown
}[]
Page: number
Recommended: {
item: InventoryItem
type: string
owned: boolean
}
HasMore: boolean
HasMoreLeft: boolean
HasMoreRight: boolean
OptionalData: Record<never, never>
}[]
LimitedLoadoutUnlockLevel: number | undefined
}
type Return = (SniperLoadout | SniperCharacter)[]
/** /**
* Creates the sniper loadouts data for a contract. Returns loadouts for all three * Creates the sniper loadouts data for a contract. Returns loadouts for all three
@ -38,13 +74,15 @@ export function createSniperLoadouts(
gameVersion: GameVersion, gameVersion: GameVersion,
contractData: MissionManifest, contractData: MissionManifest,
loadoutData = false, loadoutData = false,
) { ): Return {
const sniperLoadouts = [] const sniperLoadouts: Return = []
const parentLocation = getSubLocationByName( const parentLocation = getSubLocationByName(
contractData.Metadata.Location, contractData.Metadata.Location,
gameVersion, gameVersion,
)?.Properties.ParentLocation )?.Properties.ParentLocation
assert.ok(parentLocation, "Parent location not found")
// This function call is used as it gets all mastery data for the current location // This function call is used as it gets all mastery data for the current location
// which includes all the characters we'll need. // which includes all the characters we'll need.
// We map it by Id for quick lookup. // We map it by Id for quick lookup.
@ -54,85 +92,97 @@ export function createSniperLoadouts(
.map((data) => [data.CompletionData.Id, data]), .map((data) => [data.CompletionData.Id, data]),
) )
if (contractData.Metadata.Type === "sniper") { if (contractData.Metadata.Type !== "sniper") {
for (const charSetup of contractData.Metadata.CharacterSetup) { return sniperLoadouts
for (const character of charSetup.Characters) { }
// Get the mastery data for this character
const masteryData = masteryMap.get(
character.MandatoryLoadout[0],
)
// Get the unlockable that is currently unlocked assert.ok(
const curUnlockable = contractData.Metadata.CharacterSetup,
masteryData.CompletionData.Level === 1 "Contract missing sniper character setup",
? masteryData.Unlockable )
: masteryData.Drops[
masteryData.CompletionData.Level - 2
].Unlockable
const data = { for (const charSetup of contractData.Metadata.CharacterSetup) {
Id: character.Id, for (const character of charSetup.Characters) {
Loadout: { // Get the mastery data for this character
LoadoutData: [ const masteryData = masteryMap.get(
{ character.MandatoryLoadout?.[0] || "",
SlotId: "0", )
SlotName: "carriedweapon",
Items: [ assert.ok(
{ masteryData,
Item: { `Mastery data not found for ${contractData.Metadata.Id}`,
InstanceId: character.Id, )
ProfileId: userId,
Unlockable: curUnlockable, // Get the unlockable that is currently unlocked
Properties: {}, const curUnlockable =
}, masteryData.CompletionData.Level === 1
ItemDetails: { ? masteryData.Unlockable
Capabilities: [], : masteryData.Drops[masteryData.CompletionData.Level - 2]
StatList: Object.keys( .Unlockable
curUnlockable.Properties
.Gameplay, assert.ok(curUnlockable, "Unlockable not found")
).map((key) => { assert.ok(
return { curUnlockable.Properties.Gameplay,
Name: key, "Unlockable has no gameplay data",
Ratio: curUnlockable )
.Properties.Gameplay[
key const data: SniperCharacter = {
], Id: character.Id,
} Loadout: {
}), LoadoutData: [
PropertyTexts: [], {
}, SlotId: "0",
}, SlotName: "carriedweapon",
], Items: [],
Page: 0, Page: 0,
Recommended: { Recommended: {
item: { item: {
InstanceId: character.Id, InstanceId: character.Id,
ProfileId: userId, ProfileId: userId,
Unlockable: curUnlockable, Unlockable: curUnlockable,
Properties: {}, Properties: {},
},
type: "carriedweapon",
owned: true,
}, },
HasMore: false, type: "carriedweapon",
HasMoreLeft: false, owned: true,
HasMoreRight: false,
OptionalData: {},
}, },
], HasMore: false,
LimitedLoadoutUnlockLevel: 0 as number | undefined, HasMoreLeft: false,
}, HasMoreRight: false,
CompletionData: masteryData?.CompletionData, OptionalData: {},
} },
],
if (loadoutData) { LimitedLoadoutUnlockLevel: 0 as number | undefined,
delete data.Loadout.LimitedLoadoutUnlockLevel },
sniperLoadouts.push(data.Loadout) CompletionData: masteryData.CompletionData,
continue
}
sniperLoadouts.push(data)
} }
data.Loadout.LoadoutData[0].Items.push({
Item: {
InstanceId: character.Id,
ProfileId: userId,
Unlockable: curUnlockable,
Properties: {},
},
ItemDetails: {
Capabilities: [],
StatList: Object.keys(
curUnlockable.Properties.Gameplay,
).map((key) => ({
Name: key,
// @ts-expect-error This will work.
Ratio: curUnlockable.Properties.Gameplay[key],
})),
PropertyTexts: [],
},
})
if (loadoutData) {
delete data.Loadout.LimitedLoadoutUnlockLevel
sniperLoadouts.push(data.Loadout)
continue
}
sniperLoadouts.push(data)
} }
} }

View File

@ -37,6 +37,7 @@ import { log, LogLevel } from "../loggingInterop"
import { getUserData } from "../databaseHandler" import { getUserData } from "../databaseHandler"
import { getFlag } from "../flags" import { getFlag } from "../flags"
import { loadouts } from "../loadouts" import { loadouts } from "../loadouts"
import assert from "assert"
/** /**
* Algorithm to get the stashpoint items data for H2 and H3. * Algorithm to get the stashpoint items data for H2 and H3.
@ -139,10 +140,13 @@ export function getModernStashData(
const inventory = createInventory( const inventory = createInventory(
userId, userId,
gameVersion, gameVersion,
getSubLocationByName(contractData?.Metadata.Location, gameVersion), getSubLocationByName(
contractData?.Metadata.Location || "",
gameVersion,
),
) )
if (query.slotname.endsWith(query.slotid!.toString())) { if (query.slotname?.endsWith(query.slotid!.toString())) {
query.slotname = query.slotname.slice( query.slotname = query.slotname.slice(
0, 0,
-query.slotid!.toString().length, -query.slotid!.toString().length,
@ -150,7 +154,7 @@ export function getModernStashData(
} }
const stashData: ModernStashData = { const stashData: ModernStashData = {
SlotId: query.slotid, SlotId: query.slotid!,
LoadoutItemsData: { LoadoutItemsData: {
SlotId: query.slotid, SlotId: query.slotid,
Items: getModernStashItemsData( Items: getModernStashItemsData(
@ -169,7 +173,7 @@ export function getModernStashData(
AllowContainers: query.allowcontainers, // ?? true AllowContainers: query.allowcontainers, // ?? true
}, },
}, },
ShowSlotName: query.slotname, ShowSlotName: query.slotname!,
} }
if (contractData) { if (contractData) {
@ -256,6 +260,10 @@ export function getLegacyStashData(
userId: string, userId: string,
gameVersion: GameVersion, gameVersion: GameVersion,
) { ) {
if (!query.contractid || !query.slotname) {
return undefined
}
const contractData = controller.resolveContract(query.contractid) const contractData = controller.resolveContract(query.contractid)
if (!contractData) { if (!contractData) {
@ -277,6 +285,8 @@ export function getLegacyStashData(
gameVersion, gameVersion,
) )
assert.ok(sublocation, "Sublocation not found")
const inventory = createInventory(userId, gameVersion, sublocation) const inventory = createInventory(userId, gameVersion, sublocation)
const userCentricContract = generateUserCentric( const userCentricContract = generateUserCentric(
@ -297,14 +307,17 @@ export function getLegacyStashData(
const dl = userProfile.Extensions.defaultloadout const dl = userProfile.Extensions.defaultloadout
if (!dl) { if (!dl) {
return defaultLoadout[id] return defaultLoadout[id as keyof typeof defaultLoadout]
} }
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain -- it makes the code 10x less readable
const forLocation = (userProfile.Extensions.defaultloadout || {})[ const forLocation = (userProfile.Extensions.defaultloadout || {})[
sublocation?.Properties?.ParentLocation sublocation?.Properties?.ParentLocation || ""
] ]
return (forLocation || defaultLoadout)[id] return (forLocation || defaultLoadout)[
id as keyof typeof defaultLoadout
]
} else { } else {
let dl = loadouts.getLoadoutFor("h1") let dl = loadouts.getLoadoutFor("h1")
@ -312,7 +325,8 @@ export function getLegacyStashData(
dl = loadouts.createDefault("h1") dl = loadouts.createDefault("h1")
} }
const forLocation = dl.data[sublocation?.Properties?.ParentLocation] const forLocation =
dl.data[sublocation?.Properties?.ParentLocation || ""]
return (forLocation || defaultLoadout)[id] return (forLocation || defaultLoadout)[id]
} }
@ -329,7 +343,7 @@ export function getLegacyStashData(
Recommended: getLoadoutItem(slotid) Recommended: getLoadoutItem(slotid)
? { ? {
item: getUnlockableById( item: getUnlockableById(
getLoadoutItem(slotid), getLoadoutItem(slotid)!,
gameVersion, gameVersion,
), ),
type: loadoutSlots[slotid], type: loadoutSlots[slotid],
@ -348,7 +362,7 @@ export function getLegacyStashData(
} }
: {}, : {},
})), })),
Contract: userCentricContract.Contract, Contract: userCentricContract?.Contract,
ShowSlotName: query.slotname, ShowSlotName: query.slotname,
UserCentric: userCentricContract, UserCentric: userCentricContract,
} }
@ -398,10 +412,10 @@ export function getSafehouseCategory(
continue // I don't want to put this in that elif statement continue // I don't want to put this in that elif statement
} }
let category = safehouseData.SubCategories.find( let category = safehouseData.SubCategories?.find(
(cat) => cat.Category === item.Unlockable.Type, (cat) => cat.Category === item.Unlockable.Type,
) )
let subcategory let subcategory: SafehouseCategory | undefined
if (!category) { if (!category) {
category = { category = {
@ -410,16 +424,16 @@ export function getSafehouseCategory(
IsLeaf: false, IsLeaf: false,
Data: null, Data: null,
} }
safehouseData.SubCategories.push(category) safehouseData.SubCategories?.push(category)
} }
subcategory = category.SubCategories.find( subcategory = category.SubCategories?.find(
(cat) => cat.Category === item.Unlockable.Subtype, (cat) => cat.Category === item.Unlockable.Subtype,
) )
if (!subcategory) { if (!subcategory) {
subcategory = { subcategory = {
Category: item.Unlockable.Subtype, Category: item.Unlockable.Subtype!,
SubCategories: null, SubCategories: null,
IsLeaf: true, IsLeaf: true,
Data: { Data: {
@ -430,13 +444,14 @@ export function getSafehouseCategory(
HasMore: false, HasMore: false,
}, },
} }
category.SubCategories.push(subcategory) category.SubCategories?.push(subcategory!)
} }
subcategory.Data?.Items.push({ subcategory!.Data?.Items.push({
Item: item, Item: item,
ItemDetails: { ItemDetails: {
Capabilities: [], Capabilities: [],
// @ts-expect-error It just works. Types are probably wrong somewhere up the chain.
StatList: item.Unlockable.Properties.Gameplay StatList: item.Unlockable.Properties.Gameplay
? Object.entries(item.Unlockable.Properties.Gameplay).map( ? Object.entries(item.Unlockable.Properties.Gameplay).map(
([key, value]) => ({ ([key, value]) => ({
@ -452,15 +467,15 @@ export function getSafehouseCategory(
}) })
} }
for (const [id, category] of safehouseData.SubCategories.entries()) { for (const [id, category] of safehouseData.SubCategories?.entries() || []) {
if (category.SubCategories.length === 1) { if (category.SubCategories?.length === 1) {
// if category only has one subcategory // if category only has one subcategory
safehouseData.SubCategories[id] = category.SubCategories[0] // flatten it safehouseData.SubCategories![id] = category.SubCategories[0] // flatten it
safehouseData.SubCategories[id].Category = category.Category // but keep the top category's name safehouseData.SubCategories![id].Category = category.Category // but keep the top category's name
} }
} }
if (safehouseData.SubCategories.length === 1) { if (safehouseData.SubCategories?.length === 1) {
// if root has only one subcategory // if root has only one subcategory
safehouseData = safehouseData.SubCategories[0] // flatten it safehouseData = safehouseData.SubCategories[0] // flatten it
} }

View File

@ -39,6 +39,7 @@ export const multiplayerMenuDataRouter = Router()
multiplayerMenuDataRouter.post( multiplayerMenuDataRouter.post(
"/multiplayermatchstatsready", "/multiplayermatchstatsready",
// @ts-expect-error Has JWT data.
(req: RequestWithJwt<MissionEndRequestQuery>, res) => { (req: RequestWithJwt<MissionEndRequestQuery>, res) => {
res.json({ res.json({
template: null, template: null,
@ -53,8 +54,11 @@ multiplayerMenuDataRouter.post(
multiplayerMenuDataRouter.post( multiplayerMenuDataRouter.post(
"/multiplayermatchstats", "/multiplayermatchstats",
// @ts-expect-error Has JWT data.
(req: RequestWithJwt<MultiplayerMatchStatsQuery>, res) => { (req: RequestWithJwt<MultiplayerMatchStatsQuery>, res) => {
const sessionDetails = contractSessions.get(req.query.contractSessionId) const sessionDetails = contractSessions.get(
req.query.contractSessionId || "",
)
if (!sessionDetails) { if (!sessionDetails) {
// contract session not found // contract session not found
@ -90,16 +94,22 @@ multiplayerMenuDataRouter.post(
}, },
) )
interface MultiplayerPresetsQuery { type MultiplayerPresetsQuery = {
gamemode?: string gamemode?: string
disguiseUnlockableId?: string disguiseUnlockableId?: string
} }
multiplayerMenuDataRouter.get( multiplayerMenuDataRouter.get(
"/multiplayerpresets", "/multiplayerpresets",
// @ts-expect-error Has JWT data.
(req: RequestWithJwt<MultiplayerPresetsQuery>, res) => { (req: RequestWithJwt<MultiplayerPresetsQuery>, res) => {
if (req.query.gamemode !== "versus") { if (req.query.gamemode !== "versus") {
res.status(401).send("unknown gamemode") res.status(400).send("unknown gamemode")
return
}
if (!req.query.disguiseUnlockableId) {
res.status(400).send("no disguiseUnlockableId")
return return
} }
@ -141,9 +151,16 @@ multiplayerMenuDataRouter.get(
multiplayerMenuDataRouter.get( multiplayerMenuDataRouter.get(
"/multiplayer", "/multiplayer",
// @ts-expect-error Has JWT data.
(req: RequestWithJwt<MultiplayerQuery>, res) => { (req: RequestWithJwt<MultiplayerQuery>, res) => {
// /multiplayer?gamemode=versus&disguiseUnlockableId=TOKEN_OUTFIT_ELUSIVE_COMPLETE_15_SUIT // /multiplayer?gamemode=versus&disguiseUnlockableId=TOKEN_OUTFIT_ELUSIVE_COMPLETE_15_SUIT
if (req.query.gamemode !== "versus") { if (req.query.gamemode !== "versus") {
res.status(400).send("unknown gamemode")
return
}
if (!req.query.disguiseUnlockableId) {
res.status(400).send("no disguiseUnlockableId")
return return
} }

View File

@ -31,7 +31,7 @@ import { randomUUID } from "crypto"
import { getConfig } from "../configSwizzleManager" import { getConfig } from "../configSwizzleManager"
import { generateUserCentric } from "../contracts/dataGen" import { generateUserCentric } from "../contracts/dataGen"
import { controller } from "../controller" import { controller } from "../controller"
import { MatchOverC2SEvent } from "../types/events" import { MatchOverC2SEvent, OpponentsC2sEvent } from "../types/events"
/** /**
* A multiplayer preset. * A multiplayer preset.
@ -89,6 +89,7 @@ const activeMatches: Map<string, MatchData> = new Map()
multiplayerRouter.post( multiplayerRouter.post(
"/GetRequiredResourcesForPreset", "/GetRequiredResourcesForPreset",
jsonMiddleware(), jsonMiddleware(),
// @ts-expect-error Has JWT data.
(req: RequestWithJwt, res) => { (req: RequestWithJwt, res) => {
const allPresets = getConfig<MultiplayerPreset[]>( const allPresets = getConfig<MultiplayerPreset[]>(
"MultiplayerPresets", "MultiplayerPresets",
@ -114,7 +115,7 @@ multiplayerRouter.post(
req.gameVersion, req.gameVersion,
), ),
) )
.filter(Boolean) .filter(Boolean) as UserCentricContract[]
res.json( res.json(
userCentrics.map((userCentric: UserCentricContract) => ({ userCentrics.map((userCentric: UserCentricContract) => ({
@ -132,6 +133,7 @@ multiplayerRouter.post(
multiplayerRouter.post( multiplayerRouter.post(
"/RegisterToMatch", "/RegisterToMatch",
jsonMiddleware(), jsonMiddleware(),
// @ts-expect-error Has JWT data.
(req: RequestWithJwt, res) => { (req: RequestWithJwt, res) => {
// get a random contract from the list of possible ones in the selected preset // get a random contract from the list of possible ones in the selected preset
const multiplayerPresets = getConfig<MultiplayerPreset[]>( const multiplayerPresets = getConfig<MultiplayerPreset[]>(
@ -212,6 +214,7 @@ multiplayerRouter.post(
multiplayerRouter.post( multiplayerRouter.post(
"/SetMatchData", "/SetMatchData",
jsonMiddleware(), jsonMiddleware(),
// @ts-expect-error Has JWT data.
(req: RequestWithJwt, res) => { (req: RequestWithJwt, res) => {
const match = activeMatches.get(req.body.matchId) const match = activeMatches.get(req.body.matchId)
@ -256,9 +259,7 @@ export function handleMultiplayerEvent(
ghost.unnoticedKills += 1 ghost.unnoticedKills += 1
return true return true
case "Opponents": { case "Opponents": {
const value = event.Value as { const value = (event as OpponentsC2sEvent).Value
ConnectedSessions: string[]
}
ghost.Opponents = value.ConnectedSessions ghost.Opponents = value.ConnectedSessions
return true return true

View File

@ -16,7 +16,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import type { Response } from "express"
import { decode, sign } from "jsonwebtoken" import { decode, sign } from "jsonwebtoken"
import { extractToken, uuidRegex } from "./utils" import { extractToken, uuidRegex } from "./utils"
import type { GameVersion, RequestWithJwt, UserProfile } from "./types/types" import type { GameVersion, RequestWithJwt, UserProfile } from "./types/types"
@ -49,10 +48,37 @@ export const JWT_SECRET = PEACOCK_DEV
? "secret" ? "secret"
: randomBytes(32).toString("hex") : randomBytes(32).toString("hex")
export async function handleOauthToken( export type OAuthTokenBody = {
req: RequestWithJwt, grant_type: "external_steam" | "external_epic" | "refresh_token"
res: Response, steam_userid?: string
): Promise<void> { epic_userid?: string
access_token: string
pId?: string
locale: string
rgn: string
gs: string
steam_appid: string
}
export type OAuthTokenResponse = {
access_token: string
token_type: "bearer" | string
expires_in: number
refresh_token: string
}
export const error400: unique symbol = Symbol("http400")
export const error406: unique symbol = Symbol("http406")
/**
* This is the code that handles the OAuth token request.
* We cannot do this without a request object because of the refresh token use case.
*
* @param req The request object.
*/
export async function handleOAuthToken(
req: RequestWithJwt<never, OAuthTokenBody>,
): Promise<typeof error400 | typeof error406 | OAuthTokenResponse> {
const isFrankenstein = req.body.gs === "scpc-prod" const isFrankenstein = req.body.gs === "scpc-prod"
const signOptions = { const signOptions = {
@ -69,19 +95,17 @@ export async function handleOauthToken(
external_appid: string external_appid: string
if (req.body.grant_type === "external_steam") { if (req.body.grant_type === "external_steam") {
if (!/^\d{1,20}$/.test(req.body.steam_userid)) { if (!/^\d{1,20}$/.test(req.body.steam_userid || "")) {
res.status(400).end() // invalid steam user id return error400 // invalid steam user id
return
} }
external_platform = "steam" external_platform = "steam"
external_userid = req.body.steam_userid external_userid = req.body.steam_userid || ""
external_users_folder = "steamids" external_users_folder = "steamids"
external_appid = req.body.steam_appid external_appid = req.body.steam_appid
} else if (req.body.grant_type === "external_epic") { } else if (req.body.grant_type === "external_epic") {
if (!/^[\da-f]{32}$/.test(req.body.epic_userid)) { if (!/^[\da-f]{32}$/.test(req.body.epic_userid || "")) {
res.status(400).end() // invalid epic user id return error400 // invalid epic user id
return
} }
const epic_token = decode( const epic_token = decode(
@ -92,25 +116,24 @@ export async function handleOauthToken(
} }
if (!epic_token || !(epic_token.appid || epic_token.app)) { if (!epic_token || !(epic_token.appid || epic_token.app)) {
res.status(400).end() // invalid epic access token return error400 // invalid epic access token
return
} }
external_appid = epic_token.appid || epic_token.app external_appid = epic_token.appid || epic_token.app
external_platform = "epic" external_platform = "epic"
external_userid = req.body.epic_userid external_userid = req.body.epic_userid || ""
external_users_folder = "epicids" external_users_folder = "epicids"
} else if (req.body.grant_type === "refresh_token") { } else if (req.body.grant_type === "refresh_token") {
// send back the token from the request (re-signed so the timestamps update) // send back the token from the request (re-signed so the timestamps update)
extractToken(req) // init req.jwt extractToken(req) // init req.jwt
// remove signOptions from existing jwt // remove signOptions from existing jwt
// ts-expect-error Non-optional, we're reassigning. // @ts-expect-error Non-optional, we're reassigning.
delete req.jwt.nbf // notBefore delete req.jwt.nbf // notBefore
// ts-expect-error Non-optional, we're reassigning. // @ts-expect-error Non-optional, we're reassigning.
delete req.jwt.exp // expiresIn delete req.jwt.exp // expiresIn
// ts-expect-error Non-optional, we're reassigning. // @ts-expect-error Non-optional, we're reassigning.
delete req.jwt.iss // issuer delete req.jwt.iss // issuer
// ts-expect-error Non-optional, we're reassigning. // @ts-expect-error Non-optional, we're reassigning.
delete req.jwt.aud // audience delete req.jwt.aud // audience
if (!isFrankenstein) { if (!isFrankenstein) {
@ -126,35 +149,33 @@ export async function handleOauthToken(
} }
} }
res.json({ return {
access_token: sign(req.jwt, JWT_SECRET, signOptions), access_token: sign(req.jwt, JWT_SECRET, signOptions),
token_type: "bearer", token_type: "bearer",
expires_in: 5000, expires_in: 5000,
refresh_token: randomUUID(), refresh_token: randomUUID(),
}) }
return
} else { } else {
res.status(406).end() // unsupported auth method return error406 // unsupported auth method
return
} }
if (req.body.pId && !uuidRegex.test(req.body.pId)) { if (req.body.pId && !uuidRegex.test(req.body.pId)) {
res.status(400).end() // pId is not a GUID return error406 // pId is not a GUID
return
} }
const isHitman3 = const isHitman3 =
external_appid === "fghi4567xQOCheZIin0pazB47qGUvZw4" || external_appid === "fghi4567xQOCheZIin0pazB47qGUvZw4" ||
external_appid === STEAM_NAMESPACE_2021 external_appid === STEAM_NAMESPACE_2021
const gameVersion: GameVersion = isFrankenstein let gameVersion: GameVersion = "h1"
? "scpc"
: isHitman3 if (isFrankenstein) {
? "h3" gameVersion = "scpc"
: external_appid === STEAM_NAMESPACE_2018 } else if (isHitman3) {
? "h2" gameVersion = "h3"
: "h1" } else if (external_appid === STEAM_NAMESPACE_2018) {
gameVersion = "h2"
}
if (!req.body.pId) { if (!req.body.pId) {
// if no profile id supplied // if no profile id supplied
@ -184,7 +205,8 @@ export async function handleOauthToken(
await writeExternalUserData( await writeExternalUserData(
external_userid, external_userid,
external_users_folder, external_users_folder,
req.body.pId, // we've already confirmed this will be there, and it's a GUID
req.body.pId!,
gameVersion, gameVersion,
) )
}) })
@ -227,9 +249,9 @@ export async function handleOauthToken(
userData.LinkedAccounts[external_platform] = external_userid userData.LinkedAccounts[external_platform] = external_userid
if (external_platform === "steam") { if (external_platform === "steam") {
userData.SteamId = req.body.steam_userid userData.SteamId = req.body.steam_userid!
} else if (external_platform === "epic") { } else if (external_platform === "epic") {
userData.EpicId = req.body.epic_userid userData.EpicId = req.body.epic_userid!
} }
if (Object.hasOwn(userData.Extensions, "inventory")) { if (Object.hasOwn(userData.Extensions, "inventory")) {
@ -262,13 +284,13 @@ export async function handleOauthToken(
if (external_platform === "epic") { if (external_platform === "epic") {
return await new EpicH3Strategy().get( return await new EpicH3Strategy().get(
req.body.access_token, req.body.access_token,
req.body.epic_userid, req.body.epic_userid!,
) )
} else if (external_platform === "steam") { } else if (external_platform === "steam") {
return await new IOIStrategy( return await new IOIStrategy(
gameVersion, gameVersion,
STEAM_NAMESPACE_2021, STEAM_NAMESPACE_2021,
).get(req.body.pId) ).get(req.body.pId!)
} else { } else {
log(LogLevel.ERROR, "Unsupported platform.") log(LogLevel.ERROR, "Unsupported platform.")
return [] return []
@ -316,10 +338,10 @@ export async function handleOauthToken(
clearInventoryFor(req.body.pId) clearInventoryFor(req.body.pId)
res!.json({ return {
access_token: sign(userinfo, JWT_SECRET, signOptions), access_token: sign(userinfo, JWT_SECRET, signOptions),
token_type: "bearer", token_type: "bearer",
expires_in: 5000, expires_in: 5000,
refresh_token: randomUUID(), refresh_token: randomUUID(),
}) }
} }

View File

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import axios, { AxiosResponse } from "axios" import axios, { AxiosError, AxiosResponse } from "axios"
import type { Request } from "express" import type { Request } from "express"
import { log, LogLevel } from "./loggingInterop" import { log, LogLevel } from "./loggingInterop"
import { handleAxiosError } from "./utils" import { handleAxiosError } from "./utils"
@ -106,7 +106,7 @@ export class OfficialServerAuth {
this._refreshToken = r.refresh_token this._refreshToken = r.refresh_token
this.initialized = true this.initialized = true
} catch (e) { } catch (e) {
handleAxiosError(e) handleAxiosError(e as AxiosError)
if (PEACOCK_DEV) { if (PEACOCK_DEV) {
log( log(

View File

@ -42,7 +42,7 @@ export function calculatePlaystyle(
const doneKillMethods: string[] = [] const doneKillMethods: string[] = []
const doneAccidents: string[] = [] const doneAccidents: string[] = []
session.kills.forEach((k) => { session.kills?.forEach((k) => {
if (k.KillClass === "ballistic") { if (k.KillClass === "ballistic") {
if (k.KillItemCategory === "pistol") { if (k.KillItemCategory === "pistol") {
playstylesCopy[1].Score += 6000 playstylesCopy[1].Score += 6000

View File

@ -29,7 +29,8 @@ import {
import { json as jsonMiddleware } from "body-parser" import { json as jsonMiddleware } from "body-parser"
import { getPlatformEntitlements } from "./platformEntitlements" import { getPlatformEntitlements } from "./platformEntitlements"
import { contractSessions, newSession } from "./eventHandler" import { contractSessions, newSession } from "./eventHandler"
import type { import {
ChallengeProgressionData,
CompiledChallengeIngameData, CompiledChallengeIngameData,
ContractSession, ContractSession,
GameVersion, GameVersion,
@ -57,7 +58,8 @@ import {
compileRuntimeChallenge, compileRuntimeChallenge,
inclusionDataCheck, inclusionDataCheck,
} from "./candle/challengeHelpers" } from "./candle/challengeHelpers"
import { LoadSaveBody } from "./types/gameSchemas" import { LoadSaveBody, ResolveGamerTagsBody } from "./types/gameSchemas"
import assert from "assert"
const profileRouter = Router() const profileRouter = Router()
@ -109,8 +111,9 @@ export const fakePlayerRegistry: {
profileRouter.post( profileRouter.post(
"/AuthenticationService/GetBlobOfflineCacheDatabaseDiff", "/AuthenticationService/GetBlobOfflineCacheDatabaseDiff",
// @ts-expect-error Has jwt props.
(req: RequestWithJwt, res) => { (req: RequestWithJwt, res) => {
const configs = [] const configs: string[] = []
menuSystemDatabase.hooks.getDatabaseDiff.call(configs, req.gameVersion) menuSystemDatabase.hooks.getDatabaseDiff.call(configs, req.gameVersion)
@ -118,19 +121,21 @@ profileRouter.post(
}, },
) )
profileRouter.post("/ProfileService/SetClientEntitlements", (req, res) => { profileRouter.post("/ProfileService/SetClientEntitlements", (_, res) => {
res.json("null") res.json("null")
}) })
profileRouter.post( profileRouter.post(
"/ProfileService/GetPlatformEntitlements", "/ProfileService/GetPlatformEntitlements",
jsonMiddleware(), jsonMiddleware(),
// @ts-expect-error Jwt props.
getPlatformEntitlements, getPlatformEntitlements,
) )
profileRouter.post( profileRouter.post(
"/ProfileService/UpdateProfileStats", "/ProfileService/UpdateProfileStats",
jsonMiddleware(), jsonMiddleware(),
// @ts-expect-error Has jwt props.
(req: RequestWithJwt, res) => { (req: RequestWithJwt, res) => {
if (req.jwt.unique_name !== req.body.id) { if (req.jwt.unique_name !== req.body.id) {
return res.status(403).end() // data submitted for different profile id return res.status(403).end() // data submitted for different profile id
@ -148,18 +153,19 @@ profileRouter.post(
profileRouter.post( profileRouter.post(
"/ProfileService/SynchronizeOfflineUnlockables", "/ProfileService/SynchronizeOfflineUnlockables",
(req, res) => { (_, res) => {
res.status(204).end() res.status(204).end()
}, },
) )
profileRouter.post("/ProfileService/GetUserConfig", (req, res) => { profileRouter.post("/ProfileService/GetUserConfig", (_, res) => {
res.json({}) res.json({})
}) })
profileRouter.post( profileRouter.post(
"/ProfileService/GetProfile", "/ProfileService/GetProfile",
jsonMiddleware(), jsonMiddleware(),
// @ts-expect-error Has jwt props.
(req: RequestWithJwt, res) => { (req: RequestWithJwt, res) => {
if (req.body.id !== req.jwt.unique_name) { if (req.body.id !== req.jwt.unique_name) {
res.status(403).end() // data requested for different profile id res.status(403).end() // data requested for different profile id
@ -173,7 +179,10 @@ profileRouter.post(
const userdata = getUserData(req.jwt.unique_name, req.gameVersion) const userdata = getUserData(req.jwt.unique_name, req.gameVersion)
const extensions = req.body.extensions.reduce( const extensions = req.body.extensions.reduce(
(acc: object, key: string) => { (acc: object, key: string) => {
if (Object.hasOwn(userdata.Extensions, key)) { if (
userdata.Extensions[key as keyof typeof userdata.Extensions]
) {
// @ts-expect-error Ok.
acc[key] = userdata.Extensions[key] acc[key] = userdata.Extensions[key]
} }
@ -188,6 +197,7 @@ profileRouter.post(
profileRouter.post( profileRouter.post(
"/UnlockableService/GetInventory", "/UnlockableService/GetInventory",
// @ts-expect-error Has jwt props.
(req: RequestWithJwt, res) => { (req: RequestWithJwt, res) => {
res.json(createInventory(req.jwt.unique_name, req.gameVersion)) res.json(createInventory(req.jwt.unique_name, req.gameVersion))
}, },
@ -196,6 +206,7 @@ profileRouter.post(
profileRouter.post( profileRouter.post(
"/ProfileService/UpdateExtensions", "/ProfileService/UpdateExtensions",
jsonMiddleware(), jsonMiddleware(),
// @ts-expect-error jwt props.
( (
req: RequestWithJwt< req: RequestWithJwt<
Record<string, never>, Record<string, never>,
@ -213,6 +224,7 @@ profileRouter.post(
for (const extension in req.body.extensionsData) { for (const extension in req.body.extensionsData) {
if (Object.hasOwn(req.body.extensionsData, extension)) { if (Object.hasOwn(req.body.extensionsData, extension)) {
// @ts-expect-error It's fine.
userdata.Extensions[extension] = userdata.Extensions[extension] =
req.body.extensionsData[extension] req.body.extensionsData[extension]
} }
@ -226,6 +238,7 @@ profileRouter.post(
profileRouter.post( profileRouter.post(
"/ProfileService/SynchroniseGameStats", "/ProfileService/SynchroniseGameStats",
jsonMiddleware(), jsonMiddleware(),
// @ts-expect-error jwt props.
(req: RequestWithJwt, res) => { (req: RequestWithJwt, res) => {
if (req.body.profileId !== req.jwt.unique_name) { if (req.body.profileId !== req.jwt.unique_name) {
// data requested for different profile id // data requested for different profile id
@ -282,32 +295,6 @@ export async function resolveProfiles(
}) })
} }
if (id === "a38faeaa-5b5b-4d7e-af90-329e98a26652") {
log(
LogLevel.WARN,
"The game tried to resolve the PeacockProject account, which should no longer be used!",
)
return Promise.resolve({
Id: "a38faeaa-5b5b-4d7e-af90-329e98a26652",
LinkedAccounts: {
dev: "PeacockProject",
},
Extensions: {},
ETag: null,
Gamertag: "PeacockProject",
DevId: "PeacockProject",
SteamId: null,
StadiaId: null,
EpicId: null,
NintendoId: null,
XboxLiveId: null,
PSNAccountId: null,
PSNOnlineId: null,
Version: LATEST_PROFILE_VERSION,
})
}
const fakePlayer = fakePlayerRegistry.getFromId(id) const fakePlayer = fakePlayerRegistry.getFromId(id)
if (fakePlayer) { if (fakePlayer) {
@ -350,6 +337,7 @@ export async function resolveProfiles(
}), }),
) )
) )
// @ts-expect-error This whole function is an exception handling clusterfunk and needs to be rewritten.
.map((outcome: PromiseSettledResult<UserProfile>) => { .map((outcome: PromiseSettledResult<UserProfile>) => {
if (outcome.status !== "fulfilled") { if (outcome.status !== "fulfilled") {
if (outcome.reason.code === "ENOENT") { if (outcome.reason.code === "ENOENT") {
@ -387,6 +375,7 @@ export async function resolveProfiles(
profileRouter.post( profileRouter.post(
"/ProfileService/ResolveProfiles", "/ProfileService/ResolveProfiles",
jsonMiddleware(), jsonMiddleware(),
// @ts-expect-error Has jwt props.
async (req: RequestWithJwt, res) => { async (req: RequestWithJwt, res) => {
res.json(await resolveProfiles(req.body.profileIDs, req.gameVersion)) res.json(await resolveProfiles(req.body.profileIDs, req.gameVersion))
}, },
@ -395,16 +384,22 @@ profileRouter.post(
profileRouter.post( profileRouter.post(
"/ProfileService/ResolveGamerTags", "/ProfileService/ResolveGamerTags",
jsonMiddleware(), jsonMiddleware(),
async (req: RequestWithJwt, res) => { // @ts-expect-error Has jwt props.
async (req: RequestWithJwt<never, ResolveGamerTagsBody>, res) => {
if (!Array.isArray(req.body.profileIds)) {
res.status(400).send("bad body")
return
}
const profiles = (await resolveProfiles( const profiles = (await resolveProfiles(
req.body.profileIds, req.body.profileIds,
req.gameVersion, req.gameVersion,
)) as UserProfile[] )) as UserProfile[]
const result = { const result = {
steam: {}, steam: {} as Record<string, string>,
epic: {}, epic: {} as Record<string, string>,
dev: {}, dev: {} as Record<string, string>,
} }
for (const profile of profiles) { for (const profile of profiles) {
@ -427,26 +422,27 @@ profileRouter.post(
}, },
) )
profileRouter.post("/ProfileService/GetFriendsCount", (req, res) => profileRouter.post("/ProfileService/GetFriendsCount", (_, res) => res.send("0"))
res.send("0"),
)
profileRouter.post( profileRouter.post(
"/GamePersistentDataService/GetData", "/GamePersistentDataService/GetData",
jsonMiddleware(), jsonMiddleware(),
// @ts-expect-error Has jwt props.
(req: RequestWithJwt, res) => { (req: RequestWithJwt, res) => {
if (req.jwt.unique_name !== req.body.userId) { if (req.jwt.unique_name !== req.body.userId) {
return res.status(403).end() return res.status(403).end()
} }
const userdata = getUserData(req.body.userId, req.gameVersion) const userdata = getUserData(req.body.userId, req.gameVersion)
res.json(userdata.Extensions.gamepersistentdata[req.body.key]) type Cast = keyof typeof userdata.Extensions.gamepersistentdata
res.json(userdata.Extensions.gamepersistentdata[req.body.key as Cast])
}, },
) )
profileRouter.post( profileRouter.post(
"/GamePersistentDataService/SaveData", "/GamePersistentDataService/SaveData",
jsonMiddleware(), jsonMiddleware(),
// @ts-expect-error jwt props.
(req: RequestWithJwt, res) => { (req: RequestWithJwt, res) => {
if (req.jwt.unique_name !== req.body.userId) { if (req.jwt.unique_name !== req.body.userId) {
return res.status(403).end() return res.status(403).end()
@ -454,6 +450,7 @@ profileRouter.post(
const userdata = getUserData(req.body.userId, req.gameVersion) const userdata = getUserData(req.body.userId, req.gameVersion)
// @ts-expect-error This is fine.
userdata.Extensions.gamepersistentdata[req.body.key] = req.body.data userdata.Extensions.gamepersistentdata[req.body.key] = req.body.data
writeUserData(req.body.userId, req.gameVersion) writeUserData(req.body.userId, req.gameVersion)
@ -464,6 +461,7 @@ profileRouter.post(
profileRouter.post( profileRouter.post(
"/ChallengesService/GetActiveChallengesAndProgression", "/ChallengesService/GetActiveChallengesAndProgression",
jsonMiddleware(), jsonMiddleware(),
// @ts-expect-error Has jwt props.
( (
req: RequestWithJwt< req: RequestWithJwt<
Record<string, never>, Record<string, never>,
@ -489,11 +487,14 @@ profileRouter.post(
return res.json([]) return res.json([])
} }
let challenges = getVersionedConfig<CompiledChallengeIngameData[]>( type CWP = {
"GlobalChallenges", Challenge: CompiledChallengeIngameData
req.gameVersion, Progression: ChallengeProgressionData | undefined
true, }
)
let challenges: CWP[] = getVersionedConfig<
CompiledChallengeIngameData[]
>("GlobalChallenges", req.gameVersion, true)
.filter((val) => inclusionDataCheck(val.InclusionData, json)) .filter((val) => inclusionDataCheck(val.InclusionData, json))
.map((item) => ({ Challenge: item, Progression: undefined })) .map((item) => ({ Challenge: item, Progression: undefined }))
@ -528,6 +529,7 @@ profileRouter.post(
challenges.forEach((val) => { challenges.forEach((val) => {
// prettier-ignore // prettier-ignore
if (val.Challenge.Id === "b1a85feb-55af-4707-8271-b3522661c0b1") { if (val.Challenge.Id === "b1a85feb-55af-4707-8271-b3522661c0b1") {
// @ts-expect-error State machines impossible to type.
// prettier-ignore // prettier-ignore
val.Challenge.Definition!["States"]["Start"][ val.Challenge.Definition!["States"]["Start"][
"CrowdNPC_Died" "CrowdNPC_Died"
@ -580,6 +582,7 @@ profileRouter.post(
profileRouter.post( profileRouter.post(
"/HubPagesService/GetChallengeTreeFor", "/HubPagesService/GetChallengeTreeFor",
jsonMiddleware(), jsonMiddleware(),
// @ts-expect-error Has jwt props.
(req: RequestWithJwt, res) => { (req: RequestWithJwt, res) => {
res.json({ res.json({
Data: { Data: {
@ -607,6 +610,7 @@ profileRouter.post(
profileRouter.post( profileRouter.post(
"/DefaultLoadoutService/Set", "/DefaultLoadoutService/Set",
jsonMiddleware(), jsonMiddleware(),
// @ts-expect-error jwt props.
async (req: RequestWithJwt, res) => { async (req: RequestWithJwt, res) => {
if (getFlag("loadoutSaving") === "PROFILES") { if (getFlag("loadoutSaving") === "PROFILES") {
let loadout = loadouts.getLoadoutFor(req.gameVersion) let loadout = loadouts.getLoadoutFor(req.gameVersion)
@ -638,6 +642,7 @@ profileRouter.post(
profileRouter.post( profileRouter.post(
"/ProfileService/UpdateUserSaveFileTable", "/ProfileService/UpdateUserSaveFileTable",
jsonMiddleware(), jsonMiddleware(),
// @ts-expect-error Has jwt props.
async (req: RequestWithJwt<never, UpdateUserSaveFileTableBody>, res) => { async (req: RequestWithJwt<never, UpdateUserSaveFileTableBody>, res) => {
if (req.body.clientSaveFileList.length > 0) { if (req.body.clientSaveFileList.length > 0) {
// We are saving to the SaveFile with the most recent timestamp. // We are saving to the SaveFile with the most recent timestamp.
@ -681,7 +686,7 @@ profileRouter.post(
}, },
) )
function getErrorMessage(error: unknown) { export function getErrorMessage(error: unknown) {
if (error instanceof Error) return error.message if (error instanceof Error) return error.message
return String(error) return String(error)
} }
@ -691,7 +696,82 @@ function getErrorCause(error: unknown) {
return String(error) return String(error)
} }
async function saveSession( profileRouter.post(
"/ContractSessionsService/Load",
jsonMiddleware(),
// @ts-expect-error Has jwt props.
async (req: RequestWithJwt<never, LoadSaveBody>, res) => {
const userData = getUserData(req.jwt.unique_name, req.gameVersion)
if (
!req.body.contractSessionId ||
!req.body.saveToken ||
!req.body.contractId
) {
res.status(400).send("bad body")
return
}
try {
await loadSession(
req.body.contractSessionId,
req.body.saveToken,
userData,
)
} catch (e) {
log(
LogLevel.DEBUG,
`Failed to load contract with token = ${
req.body.saveToken
}, session id = ${req.body.contractSessionId} because ${
(e as Error)?.message
}`,
)
log(
LogLevel.WARN,
"No such save detected! Might be an official servers save.",
)
if (PEACOCK_DEV) {
log(
LogLevel.DEBUG,
`(Save-context: ${req.body.contractSessionId}; ${req.body.saveToken})`,
)
}
log(
LogLevel.WARN,
"Creating a fake session to avoid problems... scoring will not work!",
)
newSession(
req.body.contractSessionId,
req.body.contractId,
req.jwt.unique_name,
req.body.difficultyLevel!,
req.gameVersion,
false,
)
}
res.send(`"${req.body.contractSessionId}"`)
},
)
profileRouter.post(
"/ProfileService/GetSemLinkStatus",
jsonMiddleware(),
(_, res) => {
res.json({
IsConfirmed: true,
LinkedEmail: "mail@example.com",
IOIAccountId: nilUuid,
IOIAccountBaseUrl: "https://account.ioi.dk",
})
},
)
export async function saveSession(
save: SaveFile, save: SaveFile,
userData: UserProfile, userData: UserProfile,
): Promise<void> { ): Promise<void> {
@ -747,69 +827,12 @@ async function saveSession(
log( log(
LogLevel.DEBUG, LogLevel.DEBUG,
`Saved contract to slot ${slot} with token = ${token}, session id = ${sessionId}, start time = ${ `Saved contract to slot ${slot} with token = ${token}, session id = ${sessionId}, start time = ${
contractSessions.get(sessionId).timerStart contractSessions.get(sessionId)!.timerStart
}.`, }.`,
) )
} }
profileRouter.post( export async function loadSession(
"/ContractSessionsService/Load",
jsonMiddleware(),
async (req: RequestWithJwt<never, LoadSaveBody>, res) => {
const userData = getUserData(req.jwt.unique_name, req.gameVersion)
if (
!req.body.contractSessionId ||
!req.body.saveToken ||
!req.body.contractId
) {
res.status(400).send("bad body")
return
}
try {
await loadSession(
req.body.contractSessionId,
req.body.saveToken,
userData,
)
} catch (e) {
log(
LogLevel.DEBUG,
`Failed to load contract with token = ${req.body.saveToken}, session id = ${req.body.contractSessionId} because ${e.message}`,
)
log(
LogLevel.WARN,
"No such save detected! Might be an official servers save.",
)
if (PEACOCK_DEV) {
log(
LogLevel.DEBUG,
`(Save-context: ${req.body.contractSessionId}; ${req.body.saveToken})`,
)
}
log(
LogLevel.WARN,
"Creating a fake session to avoid problems... scoring will not work!",
)
newSession(
req.body.contractSessionId,
req.body.contractId,
req.jwt.unique_name,
req.body.difficultyLevel!,
req.gameVersion,
false,
)
}
res.send(`"${req.body.contractSessionId}"`)
},
)
async function loadSession(
sessionId: string, sessionId: string,
token: string, token: string,
userData: UserProfile, userData: UserProfile,
@ -831,6 +854,8 @@ async function loadSession(
} }
} }
assert.ok(sessionData, "should have session data")
// Update challenge progression with the user's latest progression data // Update challenge progression with the user's latest progression data
for (const cid in sessionData.challengeContexts) { for (const cid in sessionData.challengeContexts) {
// Make sure the ChallengeProgression is available, otherwise loading might fail! // Make sure the ChallengeProgression is available, otherwise loading might fail!
@ -846,6 +871,11 @@ async function loadSession(
sessionData.gameVersion, sessionData.gameVersion,
) )
assert.ok(
challenge,
`session has context for unregistered challenge ${cid}`,
)
if ( if (
!userData.Extensions.ChallengeProgression[cid].Completed && !userData.Extensions.ChallengeProgression[cid].Completed &&
controller.challengeService.needSaveProgression(challenge) controller.challengeService.needSaveProgression(challenge)
@ -859,22 +889,9 @@ async function loadSession(
log( log(
LogLevel.DEBUG, LogLevel.DEBUG,
`Loaded contract with token = ${token}, session id = ${sessionId}, start time = ${ `Loaded contract with token = ${token}, session id = ${sessionId}, start time = ${
contractSessions.get(sessionId).timerStart contractSessions.get(sessionId)!.timerStart
}.`, }.`,
) )
} }
profileRouter.post(
"/ProfileService/GetSemLinkStatus",
jsonMiddleware(),
(req, res) => {
res.json({
IsConfirmed: true,
LinkedEmail: "mail@example.com",
IOIAccountId: nilUuid,
IOIAccountBaseUrl: "https://account.ioi.dk",
})
},
)
export { profileRouter } export { profileRouter }

View File

@ -48,7 +48,7 @@ import {
getLevelCount, getLevelCount,
} from "./contracts/escalations/escalationService" } from "./contracts/escalations/escalationService"
import { getUserData, writeUserData } from "./databaseHandler" import { getUserData, writeUserData } from "./databaseHandler"
import axios from "axios" import axios, { AxiosError } from "axios"
import { getFlag } from "./flags" import { getFlag } from "./flags"
import { log, LogLevel } from "./loggingInterop" import { log, LogLevel } from "./loggingInterop"
import { import {
@ -64,6 +64,7 @@ import {
CalculateScoreResult, CalculateScoreResult,
CalculateSniperScoreResult, CalculateSniperScoreResult,
CalculateXpResult, CalculateXpResult,
ContractScore,
MissionEndChallenge, MissionEndChallenge,
MissionEndDrop, MissionEndDrop,
MissionEndEvergreen, MissionEndEvergreen,
@ -72,6 +73,7 @@ import {
import { MasteryData } from "./types/mastery" import { MasteryData } from "./types/mastery"
import { createInventory, InventoryItem, getUnlockablesById } from "./inventory" import { createInventory, InventoryItem, getUnlockablesById } from "./inventory"
import { calculatePlaystyle } from "./playStyles" import { calculatePlaystyle } from "./playStyles"
import assert from "assert"
export function calculateGlobalXp( export function calculateGlobalXp(
contractSession: ContractSession, contractSession: ContractSession,
@ -81,8 +83,10 @@ export function calculateGlobalXp(
let totalXp = 0 let totalXp = 0
// TODO: Merge with the non-global challenges? // TODO: Merge with the non-global challenges?
for (const challengeId of Object.keys(contractSession.challengeContexts)) { for (const challengeId of Object.keys(
const data = contractSession.challengeContexts[challengeId] contractSession.challengeContexts || {},
)) {
const data = contractSession.challengeContexts![challengeId]
if (data.timesCompleted <= 0) { if (data.timesCompleted <= 0) {
continue continue
@ -93,7 +97,7 @@ export function calculateGlobalXp(
gameVersion, gameVersion,
) )
if (!challenge || !challenge.Xp || !challenge.Tags.includes("global")) { if (!challenge?.Xp || !challenge.Tags.includes("global")) {
continue continue
} }
@ -181,7 +185,7 @@ export function calculateScore(
headline: "UI_SCORING_SUMMARY_NO_BODIES_FOUND", headline: "UI_SCORING_SUMMARY_NO_BODIES_FOUND",
bonusId: "NoBodiesFound", bonusId: "NoBodiesFound",
condition: condition:
contractSession.legacyHasBodyBeenFound === false && !contractSession.legacyHasBodyBeenFound &&
[...contractSession.bodiesFoundBy].every( [...contractSession.bodiesFoundBy].every(
(witness) => (witness) =>
(gameVersion === "h1" (gameVersion === "h1"
@ -386,7 +390,8 @@ export function calculateSniperScore(
[480, 35000], // 35000 bonus score at 480 secs (8 min) [480, 35000], // 35000 bonus score at 480 secs (8 min)
[900, 0], // 0 bonus score at 900 secs (15 min) [900, 0], // 0 bonus score at 900 secs (15 min)
] ]
let prevsecs: number, prevscore: number let prevsecs: number = 0
let prevscore: number = 0
for (const [secs, score] of scorePoints) { for (const [secs, score] of scorePoints) {
if (bonusTimeTotal > secs) { if (bonusTimeTotal > secs) {
@ -414,36 +419,46 @@ export function calculateSniperScore(
scoreTotal: 0, scoreTotal: 0,
} }
const baseScore = contractSession.scoring.Context["TotalScore"] // eslint-disable-next-line @typescript-eslint/no-explicit-any
const challengeMultiplier = contractSession.scoring.Settings["challenges"][ const baseScore = (contractSession.scoring?.Context as any)["TotalScore"]
// @ts-expect-error it's a number
const challengeMultiplier = contractSession.scoring?.Settings["challenges"][
"Unlockables" "Unlockables"
].reduce((acc, unlockable) => { ].reduce((acc, unlockable) => {
const item = inventory.find((item) => item.Unlockable.Id === unlockable) const item = inventory.find((item) => item.Unlockable.Id === unlockable)
if (item) { if (item) {
// @ts-expect-error it's a number
return acc + item.Unlockable.Properties["Multiplier"] return acc + item.Unlockable.Properties["Multiplier"]
} }
return acc return acc
}, 1.0) }, 1.0)
assert(
typeof challengeMultiplier === "number",
"challengeMultiplier is falsey/NaN",
)
const bulletsMissed = 0 // TODO? not sure if neccessary, the penalty is always 0 for inbuilt contracts const bulletsMissed = 0 // TODO? not sure if neccessary, the penalty is always 0 for inbuilt contracts
const bulletsMissedPenalty = const bulletsMissedPenalty =
bulletsMissed * bulletsMissed *
contractSession.scoring.Settings["bulletsused"]["penalty"] (contractSession.scoring?.Settings["bulletsused"]["penalty"] || 0)
// Get SA status from global SA challenge for contracttype sniper // Get SA status from global SA challenge for contracttype sniper
const silentAssassin = const silentAssassin =
contractSession.challengeContexts[ contractSession.challengeContexts?.[
"029c4971-0ddd-47ab-a568-17b007eec04e" "029c4971-0ddd-47ab-a568-17b007eec04e"
].state !== "Failure" ].state !== "Failure"
const saBonus = silentAssassin const saBonus = silentAssassin
? contractSession.scoring.Settings["silentassassin"]["score"] ? contractSession.scoring?.Settings["silentassassin"]["score"]
: 0 : 0
const saMultiplier = silentAssassin const saMultiplier = silentAssassin
? contractSession.scoring.Settings["silentassassin"]["multiplier"] ? contractSession.scoring?.Settings["silentassassin"]["multiplier"]
: 1.0 : 1.0
const subTotalScore = baseScore + timeBonus + saBonus - bulletsMissedPenalty const subTotalScore = baseScore + timeBonus + saBonus - bulletsMissedPenalty
const totalScore = Math.round( const totalScore = Math.round(
// @ts-expect-error it's a number
subTotalScore * challengeMultiplier * saMultiplier, subTotalScore * challengeMultiplier * saMultiplier,
) )
@ -518,7 +533,7 @@ export async function getMissionEndData(
gameVersion: GameVersion, gameVersion: GameVersion,
): Promise<MissionEndError | MissionEndResult> { ): Promise<MissionEndError | MissionEndResult> {
// TODO: For this entire function, add support for 2016 difficulties // TODO: For this entire function, add support for 2016 difficulties
const sessionDetails = contractSessions.get(query.contractSessionId) const sessionDetails = contractSessions.get(query.contractSessionId || "")
if (!sessionDetails) { if (!sessionDetails) {
return { return {
@ -625,6 +640,8 @@ export async function getMissionEndData(
false, false,
) )
assert.ok(levelData, "contract not found")
// Resolve the id of the parent location // Resolve the id of the parent location
const subLocation = getSubLocationByName( const subLocation = getSubLocationByName(
levelData.Metadata.Location, levelData.Metadata.Location,
@ -777,9 +794,9 @@ export async function getMissionEndData(
if (masteryData) { if (masteryData) {
maxLevel = maxLevel =
(query.masteryUnlockableId (query.masteryUnlockableId
? masteryData.SubPackages.find( ? masteryData.SubPackages?.find(
(subPkg) => subPkg.Id === query.masteryUnlockableId, (subPkg) => subPkg.Id === query.masteryUnlockableId,
).MaxLevel )?.MaxLevel
: masteryData.MaxLevel) || DEFAULT_MASTERY_MAXLEVEL : masteryData.MaxLevel) || DEFAULT_MASTERY_MAXLEVEL
locationLevelInfo = Array.from({ length: maxLevel }, (_, i) => { locationLevelInfo = Array.from({ length: maxLevel }, (_, i) => {
@ -828,7 +845,8 @@ export async function getMissionEndData(
) )
// Evergreen // Evergreen
const evergreenData: MissionEndEvergreen = <MissionEndEvergreen>{ const evergreenData: MissionEndEvergreen = {
Payout: 0,
PayoutsCompleted: [], PayoutsCompleted: [],
PayoutsFailed: [], PayoutsFailed: [],
} }
@ -846,14 +864,14 @@ export async function getMissionEndData(
Object.keys(gameChangerProperties).forEach((e) => { Object.keys(gameChangerProperties).forEach((e) => {
const gameChanger = gameChangerProperties[e] const gameChanger = gameChangerProperties[e]
const conditionObjective = gameChanger.Objectives.find( const conditionObjective = gameChanger.Objectives?.find(
(e) => e.Category === "condition", (e) => e.Category === "condition",
) )
const secondaryObjective = gameChanger.Objectives.find( const secondaryObjective = gameChanger.Objectives?.find(
(e) => (e) =>
e.Category === "secondary" && e.Category === "secondary" &&
e.Definition.Context["MyPayout"], e.Definition?.Context?.["MyPayout"],
) )
if ( if (
@ -862,18 +880,20 @@ export async function getMissionEndData(
sessionDetails.objectiveStates.get(conditionObjective.Id) === sessionDetails.objectiveStates.get(conditionObjective.Id) ===
"Success" "Success"
) { ) {
type P = { MyPayout: string }
const context = sessionDetails.objectiveContexts.get(
secondaryObjective.Id,
) as P | undefined
const payoutObjective = { const payoutObjective = {
Name: gameChanger.Name, Name: gameChanger.Name,
Payout: parseInt( Payout: parseInt(context?.["MyPayout"] || "0"),
sessionDetails.objectiveContexts.get(
secondaryObjective.Id,
)["MyPayout"] || 0,
),
IsPrestige: gameChanger.IsPrestigeObjective || false, IsPrestige: gameChanger.IsPrestigeObjective || false,
} }
if ( if (
!sessionDetails.evergreen.failed && !sessionDetails.evergreen?.failed &&
sessionDetails.objectiveStates.get( sessionDetails.objectiveStates.get(
secondaryObjective.Id, secondaryObjective.Id,
) === "Success" ) === "Success"
@ -888,7 +908,7 @@ export async function getMissionEndData(
evergreenData.Payout = totalPayout evergreenData.Payout = totalPayout
evergreenData.EndStateEventName = evergreenData.EndStateEventName =
sessionDetails.evergreen.scoringScreenEndState sessionDetails.evergreen?.scoringScreenEndState
locationLevelInfo = EVERGREEN_LEVEL_INFO locationLevelInfo = EVERGREEN_LEVEL_INFO
@ -909,14 +929,14 @@ export async function getMissionEndData(
calculateScoreResult.silentAssassin = false calculateScoreResult.silentAssassin = false
// Overide the calculated score // Overide the calculated score
calculateScoreResult.stars = undefined calculateScoreResult.stars = 0
} }
// Sniper // Sniper
let unlockableProgression = undefined let unlockableProgression = undefined
let sniperChallengeScore = undefined let sniperChallengeScore: CalculateSniperScoreResult | undefined = undefined
let contractScore = { let contractScore: ContractScore | undefined = {
Total: calculateScoreResult.scoreWithBonus, Total: calculateScoreResult.scoreWithBonus,
AchievedMasteries: calculateScoreResult.achievedMasteries, AchievedMasteries: calculateScoreResult.achievedMasteries,
AwardedBonuses: calculateScoreResult.awardedBonuses, AwardedBonuses: calculateScoreResult.awardedBonuses,
@ -969,7 +989,7 @@ export async function getMissionEndData(
Id: completionData.Id, Id: completionData.Id,
Level: completionData.Level, Level: completionData.Level,
LevelInfo: locationLevelInfo, LevelInfo: locationLevelInfo,
Name: completionData.Name, Name: completionData.Name!,
XP: completionData.XP, XP: completionData.XP,
XPGain: XPGain:
completionData.Level === completionData.MaxLevel completionData.Level === completionData.MaxLevel
@ -977,8 +997,9 @@ export async function getMissionEndData(
: sniperScore.FinalScore, : sniperScore.FinalScore,
} }
// @ts-expect-error should be fine (allegedly)
userData.Extensions.progression.Locations[locationParentId][ userData.Extensions.progression.Locations[locationParentId][
query.masteryUnlockableId query.masteryUnlockableId!
].PreviouslySeenXp = completionData.XP ].PreviouslySeenXp = completionData.XP
writeUserData(jwt.unique_name, gameVersion) writeUserData(jwt.unique_name, gameVersion)
@ -996,7 +1017,7 @@ export async function getMissionEndData(
// Override the playstyle // Override the playstyle
playstyle = undefined playstyle = undefined
calculateScoreResult.stars = undefined calculateScoreResult.stars = 0
calculateScoreResult.scoringHeadlines = headlines calculateScoreResult.scoringHeadlines = headlines
} }
@ -1009,7 +1030,7 @@ export async function getMissionEndData(
const masteryData = const masteryData =
controller.masteryService.getMasteryDataForSubPackage( controller.masteryService.getMasteryDataForSubPackage(
locationParentId, locationParentId,
query.masteryUnlockableId ?? undefined, query.masteryUnlockableId!,
gameVersion, gameVersion,
jwt.unique_name, jwt.unique_name,
) as MasteryData ) as MasteryData
@ -1018,31 +1039,39 @@ export async function getMissionEndData(
masteryDrops = masteryData.Drops.filter( masteryDrops = masteryData.Drops.filter(
(e) => (e) =>
e.Level > oldLocationLevel && e.Level <= newLocationLevel, e.Level > oldLocationLevel && e.Level <= newLocationLevel,
).map((e) => { ).map((e) => ({
return { Unlockable: e.Unlockable,
Unlockable: e.Unlockable, }))
}
})
} }
} }
// Challenge Drops // Challenge Drops
const challengeDrops: MissionEndDrop[] = const challengeDrops: MissionEndDrop[] =
calculateXpResult.completedChallenges.reduce((acc, challenge) => { calculateXpResult.completedChallenges.reduce(
if (challenge?.Drops?.length) { (acc: MissionEndDrop[], challenge) => {
const drops = getUnlockablesById(challenge.Drops, gameVersion) if (challenge?.Drops?.length) {
delete challenge.Drops const drops = getUnlockablesById(
challenge.Drops,
gameVersion,
)
delete challenge.Drops
for (const drop of drops) { for (const drop of drops) {
acc.push({ if (!drop) {
Unlockable: drop, continue
SourceChallenge: challenge, }
})
acc.push({
Unlockable: drop,
SourceChallenge: challenge,
})
}
} }
}
return acc return acc
}, []) },
[],
)
// Setup the result // Setup the result
const result: MissionEndResult = { const result: MissionEndResult = {
@ -1094,7 +1123,7 @@ export async function getMissionEndData(
SniperChallengeScore: sniperChallengeScore, SniperChallengeScore: sniperChallengeScore,
SilentAssassin: SilentAssassin:
contractScore?.SilentAssassin || contractScore?.SilentAssassin ||
sniperChallengeScore?.silentAssassin || sniperChallengeScore?.SilentAssassin ||
false, false,
// TODO: Use data from the leaderboard? // TODO: Use data from the leaderboard?
NewRank: 1, NewRank: 1,
@ -1112,7 +1141,7 @@ export async function getMissionEndData(
} }
// Finalize the response // Finalize the response
if ((getFlag("autoSplitterForceSilentAssassin") as boolean) === true) { if (getFlag("autoSplitterForceSilentAssassin")) {
if (result.ScoreOverview.SilentAssassin) { if (result.ScoreOverview.SilentAssassin) {
await liveSplitManager.completeMission(timeTotal) await liveSplitManager.completeMission(timeTotal)
} else { } else {
@ -1124,7 +1153,7 @@ export async function getMissionEndData(
if ( if (
getFlag("leaderboards") === true && getFlag("leaderboards") === true &&
sessionDetails.compat === true && sessionDetails.compat &&
contractData.Metadata.Type !== "vsrace" && contractData.Metadata.Type !== "vsrace" &&
contractData.Metadata.Type !== "evergreen" && contractData.Metadata.Type !== "evergreen" &&
// Disable sending sniper scores for now // Disable sending sniper scores for now
@ -1187,7 +1216,7 @@ export async function getMissionEndData(
}, },
) )
} catch (e) { } catch (e) {
handleAxiosError(e) handleAxiosError(e as AxiosError)
log( log(
LogLevel.WARN, LogLevel.WARN,
"Failed to commit leaderboards data! Either you or the server may be offline.", "Failed to commit leaderboards data! Either you or the server may be offline.",

View File

@ -120,7 +120,9 @@ export class SMFSupport {
const id = contractData.Metadata.Id const id = contractData.Metadata.Id
const placeBefore = contractData.SMF?.destinations.placeBefore const placeBefore = contractData.SMF?.destinations.placeBefore
const placeAfter = contractData.SMF?.destinations.placeAfter const placeAfter = contractData.SMF?.destinations.placeAfter
// @ts-expect-error I know what I'm doing.
const inLocation = (this.controller.missionsInLocations[location] ?? const inLocation = (this.controller.missionsInLocations[location] ??
// @ts-expect-error I know what I'm doing.
(this.controller.missionsInLocations[location] = [])) as string[] (this.controller.missionsInLocations[location] = [])) as string[]
if (placeBefore) { if (placeBefore) {

View File

@ -165,7 +165,6 @@ export function parseContextListeners(
info.challengeCountData.total = test(total, context) info.challengeCountData.total = test(total, context)
// Might be counting finished challenges, so need required challenges list. e.g. (SA5, SA12, SA17) // Might be counting finished challenges, so need required challenges list. e.g. (SA5, SA12, SA17)
// todo: maybe not hard-code this?
if ((count as string).includes("CompletedChallenges")) { if ((count as string).includes("CompletedChallenges")) {
info.challengeTreeIds.push( info.challengeTreeIds.push(
...test("$.RequiredChallenges", context), ...test("$.RequiredChallenges", context),

View File

@ -42,7 +42,9 @@ export interface IContractCreationPayload {
* The target creator API. * The target creator API.
*/ */
export class TargetCreator { export class TargetCreator {
// @ts-expect-error TODO: type this
private _targetSm private _targetSm
// @ts-expect-error TODO: type this
private _outfitSm private _outfitSm
private _targetConds: undefined | unknown[] = undefined private _targetConds: undefined | unknown[] = undefined

View File

@ -23,6 +23,7 @@ import {
InclusionData, InclusionData,
MissionManifestObjective, MissionManifestObjective,
} from "./types" } from "./types"
import { gameDifficulty } from "../utils"
export interface SavedChallenge { export interface SavedChallenge {
Id: string Id: string
@ -45,7 +46,7 @@ export interface SavedChallenge {
RuntimeType: "Hit" | string RuntimeType: "Hit" | string
Xp: number Xp: number
XpModifier?: unknown XpModifier?: unknown
DifficultyLevels: string[] DifficultyLevels: (keyof typeof gameDifficulty)[]
Definition: MissionManifestObjective["Definition"] & { Definition: MissionManifestObjective["Definition"] & {
Scope: ContextScopedStorageLocation Scope: ContextScopedStorageLocation
Repeatable?: { Repeatable?: {
@ -93,7 +94,7 @@ export type ProfileChallengeData = {
export type ChallengeContext = { export type ChallengeContext = {
context: unknown context: unknown
state: string state: string | undefined
timers: Timer[] timers: Timer[]
timesCompleted: number timesCompleted: number
} }

View File

@ -257,3 +257,7 @@ export type Dart_HitC2SEvent = ClientToServerEvent<{
export type Evergreen_Payout_DataC2SEvent = ClientToServerEvent<{ export type Evergreen_Payout_DataC2SEvent = ClientToServerEvent<{
Total_Payout: number Total_Payout: number
}> }>
export type OpponentsC2sEvent = ClientToServerEvent<{
ConnectedSessions: string[]
}>

View File

@ -32,7 +32,7 @@ export type StashpointSlotName =
| string | string
/** /**
* Query that the game sends for the stashpoint route. * Query for `/profiles/page/stashpoint`.
*/ */
export type StashpointQuery = Partial<{ export type StashpointQuery = Partial<{
contractid: string contractid: string
@ -47,7 +47,7 @@ export type StashpointQuery = Partial<{
}> }>
/** /**
* Query that the game sends for the stashpoint route in H2016. * Query for `/profiles/page/stashpoint` (H2016 ONLY).
* *
* @see StashpointQuery * @see StashpointQuery
*/ */
@ -94,8 +94,7 @@ export type GetCompletionDataForLocationQuery = Partial<{
}> }>
/** /**
* Body that the game sends for the * Body for `/authentication/api/userchannel/ContractSessionsService/Load`.
* `/authentication/api/userchannel/ContractSessionsService/Load` route.
*/ */
export type LoadSaveBody = Partial<{ export type LoadSaveBody = Partial<{
saveToken: string saveToken: string
@ -106,7 +105,7 @@ export type LoadSaveBody = Partial<{
}> }>
/** /**
* Query params that `/profiles/page/Safehouse` gets. * Query for `/profiles/page/Safehouse`.
* Roughly the same as {@link SafehouseCategoryQuery} but this route is only for H1. * Roughly the same as {@link SafehouseCategoryQuery} but this route is only for H1.
*/ */
export type SafehouseQuery = { export type SafehouseQuery = {
@ -114,7 +113,7 @@ export type SafehouseQuery = {
} }
/** /**
* Query params that `/profiles/page/SafehouseCategory` (used for Career > Inventory and possibly some of the H1 stuff) gets. * Query for `/profiles/page/SafehouseCategory` (used for Career > Inventory and possibly some of the H1 stuff).
*/ */
export type SafehouseCategoryQuery = { export type SafehouseCategoryQuery = {
type?: string type?: string
@ -122,7 +121,7 @@ export type SafehouseCategoryQuery = {
} }
/** /**
* Query params that `/profiles/page/Destination` gets. * Query for `/profiles/page/Destination`.
*/ */
export type GetDestinationQuery = { export type GetDestinationQuery = {
locationId: string locationId: string
@ -138,7 +137,7 @@ export type LeaderboardEntriesCommonQuery = {
} }
/** /**
* Query params that `/profiles/page/DebriefingLeaderboards` gets. * Query for `/profiles/page/DebriefingLeaderboards`.
* Because ofc it's different. Thanks IOI. * Because ofc it's different. Thanks IOI.
*/ */
export type DebriefingLeaderboardsQuery = { export type DebriefingLeaderboardsQuery = {
@ -147,8 +146,37 @@ export type DebriefingLeaderboardsQuery = {
} }
/** /**
* Query params that `/profiles/page/ChallengeLocation` gets. * Query for `/profiles/page/ChallengeLocation`.
*/ */
export type ChallengeLocationQuery = { export type ChallengeLocationQuery = {
locationId: string locationId: string
} }
/**
* Body for `/authentication/api/userchannel/ReportingService/ReportContract`.
*/
export type ContractReportBody = {
contractId: string
reason: number
}
/**
* Query for `/profiles/page/LookupContractPublicId`.
*/
export type LookupContractPublicIdQuery = {
publicid: string
}
/**
* Body for `/authentication/api/userchannel/ProfileService/ResolveGamerTags`.
*/
export type ResolveGamerTagsBody = {
profileIds: string[]
}
/**
* Query for `/profiles/page/GetMasteryCompletionDataForUnlockable`.
*/
export type GetMasteryCompletionDataForUnlockableQuery = {
unlockableId: string
}

View File

@ -18,12 +18,9 @@
import { CompletionData, GameVersion, Unlockable } from "./types" import { CompletionData, GameVersion, Unlockable } from "./types"
export interface MasteryDataTemplate { export interface LocationMasteryData {
template: unknown Location: Unlockable
data: { MasteryData: MasteryData[]
Location: Unlockable
MasteryData: MasteryData[]
}
} }
export interface MasteryPackageDrop { export interface MasteryPackageDrop {
@ -41,19 +38,27 @@ interface MasterySubPackage {
* @since v7.0.0 * @since v7.0.0
* The Id field has been renamed to LocationId to properly reflect what it is. * The Id field has been renamed to LocationId to properly reflect what it is.
* *
* Mastery packages may have Drops OR SubPackages, never the two. * Mastery packages may have Drops OR SubPackages, never both.
* This is to properly support sniper mastery by integrating it into the current system * This is to properly support sniper mastery by integrating it into the current system
* and mastery on H2016 as it is separated by difficulty. * and mastery on H2016 as it is separated by difficulty.
* *
* Also, a GameVersions array has been added to support multi-version mastery. * Also, a GameVersions array has been added to support multi-version mastery.
*/ */
export interface MasteryPackage { export type MasteryPackage = {
LocationId: string LocationId: string
GameVersions: GameVersion[] GameVersions: GameVersion[]
MaxLevel?: number MaxLevel?: number
HideProgression?: boolean HideProgression?: boolean
Drops?: MasteryPackageDrop[] } & (HasDrop | HasSubPackage)
SubPackages?: MasterySubPackage[]
type HasDrop = {
Drops: MasteryPackageDrop[]
SubPackages?: never
}
type HasSubPackage = {
Drops?: never
SubPackages: MasterySubPackage[]
} }
export interface MasteryData { export interface MasteryData {

View File

@ -25,12 +25,31 @@ import {
Unlockable, Unlockable,
} from "./types" } from "./types"
export interface CalculateXpResult { export type CalculateXpResult = {
completedChallenges: MissionEndChallenge[] completedChallenges: MissionEndChallenge[]
xp: number xp: number
} }
export interface CalculateScoreResult { export type ScoreProgressionStats = {
LevelInfo: number[]
XP: number
Level: number
XPGain: number
Id?: string
Name?: string
Completion?: number
HideProgression?: boolean
}
export type ScoreProfileProgressionStats = {
LevelInfo: number[]
LevelInfoOffset: number
XP: number
Level: number
XPGain: number
}
export type CalculateScoreResult = {
stars: number stars: number
scoringHeadlines: ScoringHeadline[] scoringHeadlines: ScoringHeadline[]
awardedBonuses: ScoringBonus[] awardedBonuses: ScoringBonus[]
@ -41,7 +60,7 @@ export interface CalculateScoreResult {
scoreWithBonus: number scoreWithBonus: number
} }
export interface CalculateSniperScoreResult { export type CalculateSniperScoreResult = {
FinalScore: number FinalScore: number
BaseScore: number BaseScore: number
TotalChallengeMultiplier: number TotalChallengeMultiplier: number
@ -50,11 +69,11 @@ export interface CalculateSniperScoreResult {
TimeTaken: number TimeTaken: number
TimeBonus: number TimeBonus: number
SilentAssassin: boolean SilentAssassin: boolean
SilentAssassinBonus: number SilentAssassinBonus: number | undefined
SilentAssassinMultiplier: number SilentAssassinMultiplier: number | undefined
} }
export interface MissionEndChallenge { export type MissionEndChallenge = {
ChallengeId: string ChallengeId: string
ChallengeTags: string[] ChallengeTags: string[]
ChallengeName: string ChallengeName: string
@ -66,7 +85,7 @@ export interface MissionEndChallenge {
Drops?: string[] Drops?: string[]
} }
export interface MissionEndSourceChallenge { export type MissionEndSourceChallenge = {
ChallengeId: string ChallengeId: string
ChallengeTags: string[] ChallengeTags: string[]
ChallengeName: string ChallengeName: string
@ -77,12 +96,12 @@ export interface MissionEndSourceChallenge {
IsActionReward: boolean IsActionReward: boolean
} }
export interface MissionEndDrop { export type MissionEndDrop = {
Unlockable: Unlockable Unlockable: Unlockable
SourceChallenge?: MissionEndSourceChallenge SourceChallenge?: MissionEndSourceChallenge
} }
export interface MissionEndAchievedMastery { export type MissionEndAchievedMastery = {
score: number score: number
RatioParts: number RatioParts: number
RatioTotal: number RatioTotal: number
@ -90,47 +109,38 @@ export interface MissionEndAchievedMastery {
BaseScore: number BaseScore: number
} }
export interface MissionEndEvergreen { export type MissionEndEvergreen = {
Payout: number Payout: number
EndStateEventName?: string EndStateEventName?: string | null
PayoutsCompleted: MissionEndEvergreenPayout[] PayoutsCompleted: MissionEndEvergreenPayout[]
PayoutsFailed: MissionEndEvergreenPayout[] PayoutsFailed: MissionEndEvergreenPayout[]
} }
export interface MissionEndEvergreenPayout { export type MissionEndEvergreenPayout = {
Name: string Name: string
Payout: number Payout: number
IsPrestige: boolean IsPrestige: boolean
} }
export interface MissionEndResult { export type ContractScore = {
Total: number
AchievedMasteries: MissionEndAchievedMastery[]
AwardedBonuses: ScoringBonus[]
TotalNoMultipliers: number
TimeUsedSecs: Seconds
StarCount: number
FailedBonuses: ScoringBonus[]
SilentAssassin: boolean
}
export type MissionEndResult = {
MissionReward: { MissionReward: {
LocationProgression: { LocationProgression: ScoreProgressionStats
LevelInfo: number[] ProfileProgression: ScoreProfileProgressionStats
XP: number
Level: number
Completion: number
XPGain: number
HideProgression: boolean
}
ProfileProgression: {
LevelInfo: number[]
LevelInfoOffset: number
XP: number
Level: number
XPGain: number
}
Challenges: MissionEndChallenge[] Challenges: MissionEndChallenge[]
Drops: MissionEndDrop[] Drops: MissionEndDrop[]
OpportunityRewards: unknown[] // ? OpportunityRewards: unknown[] // ?
UnlockableProgression?: { UnlockableProgression?: ScoreProgressionStats
LevelInfo: number[]
XP: number
Level: number
XPGain: number
Id: string
Name: string
}
CompletionData: CompletionData CompletionData: CompletionData
ChallengeCompletion: ChallengeCompletion ChallengeCompletion: ChallengeCompletion
ContractChallengeCompletion: ChallengeCompletion ContractChallengeCompletion: ChallengeCompletion
@ -149,28 +159,8 @@ export interface MissionEndResult {
ScoreDetails: { ScoreDetails: {
Headlines: ScoringHeadline[] Headlines: ScoringHeadline[]
} }
ContractScore?: { ContractScore?: ContractScore
Total: number SniperChallengeScore?: CalculateSniperScoreResult
AchievedMasteries: MissionEndAchievedMastery[]
AwardedBonuses: ScoringBonus[]
TotalNoMultipliers: number
TimeUsedSecs: Seconds
StarCount: number
FailedBonuses: ScoringBonus[]
SilentAssassin: boolean
}
SniperChallengeScore?: {
FinalScore: number
BaseScore: number
TotalChallengeMultiplier: number
BulletsMissed: number
BulletsMissedPenalty: number
TimeTaken: number
TimeBonus: number
SilentAssassin: boolean
SilentAssassinBonus: number
SilentAssassinMultiplier: number
}
SilentAssassin: boolean SilentAssassin: boolean
NewRank: number NewRank: number
RankCount: number RankCount: number

View File

@ -19,7 +19,7 @@
import type * as core from "express-serve-static-core" import type * as core from "express-serve-static-core"
import type { IContractCreationPayload } from "../statemachines/contractCreation" import type { IContractCreationPayload } from "../statemachines/contractCreation"
import type { Request } from "express" import { Request } from "express"
import { import {
ChallengeContext, ChallengeContext,
ProfileChallengeData, ProfileChallengeData,
@ -29,6 +29,7 @@ import { SessionGhostModeDetails } from "../multiplayer/multiplayerService"
import { IContextListener } from "../statemachines/contextListeners" import { IContextListener } from "../statemachines/contextListeners"
import { ManifestScoringModule, ScoringModule } from "./scoring" import { ManifestScoringModule, ScoringModule } from "./scoring"
import { Timer } from "@peacockproject/statemachine-parser" import { Timer } from "@peacockproject/statemachine-parser"
import { InventoryItem } from "../inventory"
/** /**
* A duration or relative point in time expressed in seconds. * A duration or relative point in time expressed in seconds.
@ -274,7 +275,7 @@ export interface ContractSession {
*/ */
evergreen?: { evergreen?: {
payout: number payout: number
scoringScreenEndState: string scoringScreenEndState: string | null
failed: boolean failed: boolean
} }
/** /**
@ -367,7 +368,7 @@ export interface S2CEventWithTimestamp<EventValue = unknown> {
* A server to client push message. The message component is encoded JSON. * A server to client push message. The message component is encoded JSON.
*/ */
export interface PushMessage { export interface PushMessage {
time: number | string time: number | string | bigint
message: string message: string
} }
@ -405,33 +406,30 @@ export interface MissionStory {
} }
export interface PlayerProfileView { export interface PlayerProfileView {
template: unknown SubLocationData: {
data: { ParentLocation: Unlockable
SubLocationData: { Location: Unlockable
ParentLocation: Unlockable CompletionData: CompletionData
Location: Unlockable ChallengeCategoryCompletion: ChallengeCategoryCompletion[]
CompletionData: CompletionData ChallengeCompletion: ChallengeCompletion
ChallengeCategoryCompletion: ChallengeCategoryCompletion[] OpportunityStatistics: OpportunityStatistics
ChallengeCompletion: ChallengeCompletion LocationCompletionPercent: number
OpportunityStatistics: OpportunityStatistics }[]
LocationCompletionPercent: number PlayerProfileXp: {
}[] Total: number
PlayerProfileXp: { Level: number
Total: number Seasons: {
Level: number Number: number
Seasons: { Locations: {
Number: number LocationId: string
Locations: { Xp: number
LocationId: string ActionXp: number
Xp: number LocationProgression?: {
ActionXp: number Level: number
LocationProgression?: { MaxLevel: number
Level: number }
MaxLevel: number
}
}[]
}[] }[]
} }[]
} }
} }
@ -878,7 +876,7 @@ export type ContractGroupDefinition = {
Order: string[] Order: string[]
} }
export interface EscalationInfo { export type EscalationInfo = {
Type?: MissionType Type?: MissionType
InGroup?: string InGroup?: string
NextContractId?: string NextContractId?: string
@ -893,77 +891,76 @@ export interface EscalationInfo {
export interface MissionManifestMetadata { export interface MissionManifestMetadata {
Id: string Id: string
Location: string Location: string
IsPublished?: boolean IsPublished?: boolean | null
CreationTimestamp?: string CreationTimestamp?: string | null
CreatorUserId?: string CreatorUserId?: string | null
Title: string Title: string
Description?: string Description?: string | null
BriefingVideo?: BriefingVideo?:
| string | string
| { | {
Mode: string Mode: string
VideoId: string VideoId: string
}[] }[]
DebriefingVideo?: string DebriefingVideo?: string | null
TileImage?: TileImage?:
| string | string
| { | {
Mode: string Mode: string
Image: string Image: string
}[] }[]
CodeName_Hint?: string CodeName_Hint?: string | null
ScenePath: string ScenePath: string
Type: MissionType Type: MissionType
Release?: string | object Release?: string | object | null
RequiredUnlockable?: string RequiredUnlockable?: string | null
Drops?: string[] Drops?: string[] | null
Opportunities?: string[] Opportunities?: string[] | null
OpportunityData?: MissionStory[] OpportunityData?: MissionStory[] | null
Entitlements: string[] Entitlements: string[] | null
LastUpdate?: string LastUpdate?: string | null
PublicId?: string PublicId?: string | null
GroupObjectiveDisplayOrder?: GroupObjectiveDisplayOrderItem[] GroupObjectiveDisplayOrder?: GroupObjectiveDisplayOrderItem[] | null
GameVersion?: string GameVersion?: string | null
ServerVersion?: string ServerVersion?: string | null
AllowNonTargetKills?: boolean AllowNonTargetKills?: boolean | null
Difficulty?: "pro1" | string Difficulty?: "pro1" | string | null
CharacterSetup?: { CharacterSetup?:
Mode: "singleplayer" | "multiplayer" | string | {
Characters: { Mode: "singleplayer" | "multiplayer" | string
Name: string Characters: {
Id: string Name: string
MandatoryLoadout?: string[] Id: string
}[] MandatoryLoadout?: string[]
}[] }[]
CharacterLoadoutData?: { }[]
Id: string | null
Loadout: unknown CharacterLoadoutData?:
CompletionData: CompletionData | {
}[] Id: string
SpawnSelectionType?: "random" | string Loadout: unknown
Gamemodes?: ("versus" | string)[] CompletionData: CompletionData
Enginemodes?: ("singleplayer" | "multiplayer" | string)[] }[]
| null
SpawnSelectionType?: "random" | string | null
Gamemodes?: ("versus" | string)[] | null
Enginemodes?: ("singleplayer" | "multiplayer" | string)[] | null
EndConditions?: { EndConditions?: {
PointLimit?: number PointLimit?: number
} } | null
Subtype?: string Subtype?: string | null
GroupTitle?: string GroupTitle?: string | null
TargetExpiration?: number TargetExpiration?: number | null
TargetExpirationReduced?: number TargetExpirationReduced?: number | null
TargetLifeTime?: number TargetLifeTime?: number | null
NonTargetKillPenaltyEnabled?: boolean NonTargetKillPenaltyEnabled?: boolean | null
NoticedTargetStreakPenaltyMax?: number NoticedTargetStreakPenaltyMax?: number | null
IsFeatured?: boolean IsFeatured?: boolean | null
// Begin escalation-exclusive properties // Begin escalation-exclusive properties
InGroup?: string InGroup?: string | null
NextContractId?: string NextContractId?: string | null
GroupDefinition?: ContractGroupDefinition GroupDefinition?: ContractGroupDefinition | null
GroupData?: { GroupData?: EscalationInfo["GroupData"] | null
Level: number
TotalLevels: number
Completed: boolean
FirstContractId: string
}
// End escalation-exclusive properties // End escalation-exclusive properties
/** /**
* Useless property. * Useless property.
@ -971,19 +968,19 @@ export interface MissionManifestMetadata {
* @deprecated * @deprecated
*/ */
readonly UserData?: unknown | null readonly UserData?: unknown | null
IsVersus?: boolean IsVersus?: boolean | null
IsEvergreenSafehouse?: boolean IsEvergreenSafehouse?: boolean | null
UseContractProgressionData?: boolean UseContractProgressionData?: boolean | null
CpdId?: string CpdId?: string | null
/** /**
* Custom property used for Elusives (like official's year) * Custom property used for Elusives (like official's year)
* and Escalations (if it's 0, it is a Peacock escalation, * and Escalations (if it's 0, it is a Peacock escalation,
* and OriginalSeason will exist for filtering). * and OriginalSeason will exist for filtering).
*/ */
Season?: number Season?: number | null
OriginalSeason?: number OriginalSeason?: number | null
// Used for sniper scoring // Used for sniper scoring
Modules?: ManifestScoringModule[] Modules?: ManifestScoringModule[] | null
} }
export interface GroupObjectiveDisplayOrderItem { export interface GroupObjectiveDisplayOrderItem {
@ -1039,7 +1036,7 @@ export interface MissionManifest {
EnableExits?: { EnableExits?: {
$eq?: (string | boolean)[] $eq?: (string | boolean)[]
} }
DevOnlyBricks?: string[] DevOnlyBricks?: string[] | null
} }
Metadata: MissionManifestMetadata Metadata: MissionManifestMetadata
readonly UserData?: Record<string, never> | never[] readonly UserData?: Record<string, never> | never[]
@ -1216,7 +1213,7 @@ export interface CompiledChallengeTreeData {
CategoryName: string CategoryName: string
ChallengeProgress?: ChallengeTreeWaterfallState ChallengeProgress?: ChallengeTreeWaterfallState
Completed: boolean Completed: boolean
CompletionData: CompletionData CompletionData?: CompletionData
Description: string Description: string
// A string array of at most one element ("easy", "normal", or "hard"). // A string array of at most one element ("easy", "normal", or "hard").
// If empty, then the challenge should appear in sessions on any difficulty. // If empty, then the challenge should appear in sessions on any difficulty.
@ -1276,6 +1273,7 @@ export interface ChallengeProgressionData {
ProfileId: string ProfileId: string
Completed: boolean Completed: boolean
Ticked: boolean Ticked: boolean
ETag?: string
State: Record<string, unknown> State: Record<string, unknown>
CompletedAt: Date | string | null CompletedAt: Date | string | null
MustBeSaved: boolean MustBeSaved: boolean
@ -1286,14 +1284,6 @@ export interface CompiledChallengeRuntimeData {
Progression: ChallengeProgressionData Progression: ChallengeProgressionData
} }
export interface CompiledChallengeRewardData {
ChallengeId: string
ChallengeName: string
ChallengeDescription: string
ChallengeImageUrl: string
XPGain: number
}
export type LoadoutSavingMechanism = "PROFILES" | "LEGACY" export type LoadoutSavingMechanism = "PROFILES" | "LEGACY"
export type ImageLoadingStrategy = "SAVEASREQUESTED" | "ONLINE" | "OFFLINE" export type ImageLoadingStrategy = "SAVEASREQUESTED" | "ONLINE" | "OFFLINE"
@ -1322,7 +1312,7 @@ export interface IHit {
/** /**
* A video object. * A video object.
* *
* @see ICampaignVideo * @see CampaignVideo
* @see StoryData * @see StoryData
*/ */
export interface IVideo { export interface IVideo {
@ -1346,7 +1336,7 @@ export interface IVideo {
* *
* @see IHit * @see IHit
*/ */
export type ICampaignMission = { export type CampaignMission = {
Type: "Mission" Type: "Mission"
Data: IHit Data: IHit
} }
@ -1356,7 +1346,7 @@ export type ICampaignMission = {
* *
* @see IVideo * @see IVideo
*/ */
export type ICampaignVideo = { export type CampaignVideo = {
Type: "Video" Type: "Video"
Data: IVideo Data: IVideo
} }
@ -1373,7 +1363,7 @@ export interface RegistryChallenge extends SavedChallenge {
/** /**
* An element for the game's story data. * An element for the game's story data.
*/ */
export type StoryData = ICampaignMission | ICampaignVideo export type StoryData = CampaignMission | CampaignVideo
/** /**
* A campaign object. * A campaign object.
@ -1415,7 +1405,7 @@ export interface Loadout {
* *
* @see LoadoutFile * @see LoadoutFile
*/ */
export interface LoadoutsGameVersion { export type LoadoutsGameVersion = {
selected: string | null selected: string | null
loadouts: Loadout[] loadouts: Loadout[]
} }
@ -1423,19 +1413,20 @@ export interface LoadoutsGameVersion {
/** /**
* The top-level format for the loadout profiles storage file. * The top-level format for the loadout profiles storage file.
*/ */
export interface LoadoutFile { export type LoadoutFile = Record<
h1: LoadoutsGameVersion // game version but not scpc
h2: LoadoutsGameVersion Exclude<GameVersion, "scpc">,
h3: LoadoutsGameVersion LoadoutsGameVersion
} >
/** /**
* A function that generates a campaign mission object for use in the campaigns menu. * A function that generates a campaign mission object for use in the campaigns menu.
* Will throw if contract is not found.
*/ */
export type GenSingleMissionFunc = ( export type GenSingleMissionFunc = (
contractId: string, contractId: string,
gameVersion: GameVersion, gameVersion: GameVersion,
) => ICampaignMission ) => CampaignMission
/** /**
* A function that generates a campaign video object for use in the campaigns menu. * A function that generates a campaign video object for use in the campaigns menu.
@ -1443,7 +1434,7 @@ export type GenSingleMissionFunc = (
export type GenSingleVideoFunc = ( export type GenSingleVideoFunc = (
videoId: string, videoId: string,
gameVersion: GameVersion, gameVersion: GameVersion,
) => ICampaignVideo ) => CampaignVideo
/** /**
* A "hits category" is used to display lists of contracts in-game. * A "hits category" is used to display lists of contracts in-game.
@ -1486,16 +1477,25 @@ export interface PlayNextGetCampaignsHookReturn {
export type SafehouseCategory = { export type SafehouseCategory = {
Category: string Category: string
SubCategories: SafehouseCategory[] SubCategories: SafehouseCategory[] | null
IsLeaf: boolean IsLeaf: boolean
Data: null Data: null | {
} Type: string
SubType: string | undefined
export type SniperLoadout = { Items: {
ID: string Item: InventoryItem
InstanceID: string ItemDetails: {
Unlockable: Unlockable[] Capabilities: []
MainUnlockable: Unlockable StatList: {
Name: string
Ratio: unknown
PropertyTexts: []
}[]
}
}[]
Page: number
HasMore: boolean
}
} }
/** /**

View File

@ -28,7 +28,7 @@ import type {
Unlockable, Unlockable,
UserProfile, UserProfile,
} from "./types/types" } from "./types/types"
import axios, { AxiosError } from "axios" import { AxiosError } from "axios"
import { log, LogLevel } from "./loggingInterop" import { log, LogLevel } from "./loggingInterop"
import { writeFileSync } from "fs" import { writeFileSync } from "fs"
import { getFlag } from "./flags" import { getFlag } from "./flags"
@ -54,7 +54,7 @@ export const uuidRegex =
export const contractTypes = ["featured", "usercreated"] export const contractTypes = ["featured", "usercreated"]
export const versions: GameVersion[] = ["h1", "h2", "h3"] export const versions: Exclude<GameVersion, "scpc">[] = ["h1", "h2", "h3"]
export const contractCreationTutorialId = "d7e2607c-6916-48e2-9588-976c7d8998bb" export const contractCreationTutorialId = "d7e2607c-6916-48e2-9588-976c7d8998bb"
@ -71,10 +71,10 @@ export async function checkForUpdates(): Promise<void> {
} }
try { try {
const res = await axios( const res = await fetch(
"https://backend.rdil.rocks/peacock/latest-version", "https://backend.rdil.rocks/peacock/latest-version",
) )
const current = res.data const current = parseInt(await res.text(), 10)
if (PEACOCKVER < 0 && current < -PEACOCKVER) { if (PEACOCKVER < 0 && current < -PEACOCKVER) {
log( log(
@ -94,12 +94,15 @@ export async function checkForUpdates(): Promise<void> {
} }
} }
export function getRemoteService(gameVersion: GameVersion): string { export function getRemoteService(gameVersion: GameVersion): string | undefined {
return gameVersion === "h3" switch (gameVersion) {
? "hm3-service" case "h3":
: gameVersion === "h2" return "hm3-service"
? "pc2-service" case "h2":
: "pc-service" return "pc2-service"
default:
return "pc-service"
}
} }
/** /**
@ -306,6 +309,7 @@ function updateUserProfile(
if (gameVersion === "h1") { if (gameVersion === "h1") {
// No sniper locations, but we add normal and pro1 // No sniper locations, but we add normal and pro1
// @ts-expect-error I know what I'm doing.
obj[newKey] = { obj[newKey] = {
// Data from previous profiles only contains normal and is the default. // Data from previous profiles only contains normal and is the default.
normal: { normal: {
@ -321,8 +325,10 @@ function updateUserProfile(
} }
} else { } else {
// We need to update sniper locations. // We need to update sniper locations.
// @ts-expect-error I know what I'm doing.
obj[newKey] = sniperLocs[newKey] obj[newKey] = sniperLocs[newKey]
? sniperLocs[newKey].reduce((obj, uId) => { ? // @ts-expect-error I know what I'm doing.
sniperLocs[newKey].reduce((obj, uId) => {
obj[uId] = { obj[uId] = {
Xp: 0, Xp: 0,
Level: 1, Level: 1,
@ -344,7 +350,7 @@ function updateUserProfile(
{}, {},
) )
// ts-expect-error Legacy property. // @ts-expect-error Legacy property.
delete profile.Extensions.progression["Unlockables"] delete profile.Extensions.progression["Unlockables"]
profile.Version = 1 profile.Version = 1
@ -426,12 +432,7 @@ export function castUserProfile(
// Fix Extensions.gamepersistentdata.HitsFilterType. // Fix Extensions.gamepersistentdata.HitsFilterType.
// None of the old profiles should have "MyPlaylist". // None of the old profiles should have "MyPlaylist".
if ( if (j.Extensions.gamepersistentdata.HitsFilterType["MyPlaylist"]) {
!Object.hasOwn(
j.Extensions.gamepersistentdata.HitsFilterType,
"MyPlaylist",
)
) {
j.Extensions.gamepersistentdata.HitsFilterType = { j.Extensions.gamepersistentdata.HitsFilterType = {
MyHistory: "all", MyHistory: "all",
MyContracts: "all", MyContracts: "all",
@ -520,15 +521,17 @@ export const defaultSuits = {
* @returns The default suits that are attainable via challenges or mastery. * @returns The default suits that are attainable via challenges or mastery.
*/ */
export function attainableDefaults(gameVersion: GameVersion): string[] { export function attainableDefaults(gameVersion: GameVersion): string[] {
return gameVersion === "h1" if (gameVersion === "h1") {
? [] return []
: gameVersion === "h2" } else if (gameVersion === "h2") {
? ["TOKEN_OUTFIT_WET_SUIT"] return ["TOKEN_OUTFIT_WET_SUIT"]
: [ } else {
"TOKEN_OUTFIT_GREENLAND_HERO_TRAININGSUIT", return [
"TOKEN_OUTFIT_WET_SUIT", "TOKEN_OUTFIT_GREENLAND_HERO_TRAININGSUIT",
"TOKEN_OUTFIT_HERO_DUGONG_SUIT", "TOKEN_OUTFIT_WET_SUIT",
] "TOKEN_OUTFIT_HERO_DUGONG_SUIT",
]
}
} }
/** /**
@ -538,10 +541,11 @@ export function attainableDefaults(gameVersion: GameVersion): string[] {
* @param subLocation The sub-location. * @param subLocation The sub-location.
* @returns The default suit for the given sub-location and parent location. * @returns The default suit for the given sub-location and parent location.
*/ */
export function getDefaultSuitFor(subLocation: Unlockable): string | undefined { export function getDefaultSuitFor(subLocation: Unlockable): string {
type Cast = keyof typeof defaultSuits
return ( return (
defaultSuits[subLocation.Id] || defaultSuits[subLocation.Id as Cast] ||
defaultSuits[subLocation.Properties.ParentLocation] || defaultSuits[subLocation.Properties.ParentLocation as Cast] ||
"TOKEN_OUTFIT_HITMANSUIT" "TOKEN_OUTFIT_HITMANSUIT"
) )
} }

View File

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Request, Response, Router } from "express" import { NextFunction, Request, Response, Router } from "express"
import { getConfig } from "./configSwizzleManager" import { getConfig } from "./configSwizzleManager"
import { readFileSync } from "atomically" import { readFileSync } from "atomically"
import { GameVersion, UserProfile } from "./types/types" import { GameVersion, UserProfile } from "./types/types"
@ -41,6 +41,40 @@ if (PEACOCK_DEV) {
}) })
} }
type CommonRequest<ExtraQuery = Record<never, never>> = Request<
unknown,
unknown,
unknown,
{
user: string
gv: Exclude<GameVersion, "scpc">
} & ExtraQuery
>
function commonValidationMiddleware(
req: CommonRequest,
res: Response,
next: NextFunction,
): void {
if (!req.query.gv || !versions.includes(req.query.gv ?? null)) {
res.json({
success: false,
error: "invalid game version",
})
return
}
if (!req.query.user || !uuidRegex.test(req.query.user)) {
res.json({
success: false,
error: "The request must contain the uuid of a user.",
})
return
}
next()
}
function formErrorMessage(res: Response, message: string): void { function formErrorMessage(res: Response, message: string): void {
res.json({ res.json({
success: false, success: false,
@ -48,83 +82,49 @@ function formErrorMessage(res: Response, message: string): void {
}) })
} }
webFeaturesRouter.get("/codenames", (req, res) => { webFeaturesRouter.get("/codenames", (_, res) => {
res.json(getConfig("EscalationCodenames", false)) res.json(getConfig("EscalationCodenames", false))
}) })
webFeaturesRouter.get( webFeaturesRouter.get("/local-users", (req: CommonRequest, res) => {
"/local-users",
(req: Request<unknown, unknown, unknown, { gv: GameVersion }>, res) => {
if (!req.query.gv || !versions.includes(req.query.gv ?? null)) {
res.json([])
return
}
let dir
if (req.query.gv === "h3") {
dir = join("userdata", "users")
} else {
dir = join("userdata", req.query.gv, "users")
}
const files: string[] = readdirSync(dir).filter(
(name) => name !== "lop.json",
)
const result = []
for (const file of files) {
const read = JSON.parse(
readFileSync(join(dir, file)).toString(),
) as UserProfile
result.push({
id: read.Id,
name: read.Gamertag,
platform: read.EpicId ? "Epic" : "Steam",
})
}
res.json(result)
},
)
function validateUserAndGv(
req: Request<unknown, unknown, unknown, { gv: GameVersion; user: string }>,
res: Response,
): boolean {
if (!req.query.gv || !versions.includes(req.query.gv ?? null)) { if (!req.query.gv || !versions.includes(req.query.gv ?? null)) {
formErrorMessage( res.json([])
res, return
'The request must contain a valid game version among "h1", "h2", and "h3".',
)
return false
} }
if (!req.query.user || !uuidRegex.test(req.query.user)) { let dir
formErrorMessage(res, "The request must contain the uuid of a user.")
return false if (req.query.gv === "h3") {
dir = join("userdata", "users")
} else {
dir = join("userdata", req.query.gv, "users")
} }
return true const files: string[] = readdirSync(dir).filter(
} (name) => name !== "lop.json",
)
const result = []
for (const file of files) {
const read = JSON.parse(
readFileSync(join(dir, file)).toString(),
) as UserProfile
result.push({
id: read.Id,
name: read.Gamertag,
platform: read.EpicId ? "Epic" : "Steam",
})
}
res.json(result)
})
webFeaturesRouter.get( webFeaturesRouter.get(
"/modify", "/modify",
async ( commonValidationMiddleware,
req: Request< async (req: CommonRequest<{ level: string; id: string }>, res) => {
unknown,
unknown,
unknown,
{ gv: GameVersion; user: string; level: string; id: string }
>,
res,
) => {
if (!validateUserAndGv(req, res)) {
return
}
if (!req.query.level) { if (!req.query.level) {
formErrorMessage( formErrorMessage(
res, res,
@ -158,7 +158,7 @@ webFeaturesRouter.get(
const mapping = controller.escalationMappings.get(req.query.id) const mapping = controller.escalationMappings.get(req.query.id)
if (mapping === undefined) { if (!mapping) {
formErrorMessage(res, "Unknown escalation.") formErrorMessage(res, "Unknown escalation.")
return return
} }
@ -198,19 +198,8 @@ webFeaturesRouter.get(
webFeaturesRouter.get( webFeaturesRouter.get(
"/user-progress", "/user-progress",
async ( commonValidationMiddleware,
req: Request< async (req: CommonRequest, res) => {
unknown,
unknown,
unknown,
{ gv: GameVersion; user: string }
>,
res,
) => {
if (!validateUserAndGv(req, res)) {
return
}
try { try {
await loadUserData(req.query.user, req.query.gv) await loadUserData(req.query.user, req.query.gv)
} catch (e) { } catch (e) {

View File

@ -19,7 +19,8 @@
"webui": "yarn workspace @peacockproject/web-ui", "webui": "yarn workspace @peacockproject/web-ui",
"typedefs": "yarn workspace @peacockproject/core", "typedefs": "yarn workspace @peacockproject/core",
"run-dev": "node packaging/devLoader.mjs", "run-dev": "node packaging/devLoader.mjs",
"extract-challenge-data": "node packaging/extractChallengeData.mjs" "extract-challenge-data": "node packaging/extractChallengeData.mjs",
"find-circular": "yarn dlx dpdm components/index.ts --exclude \"(components/types)|(node_modules)\" -T"
}, },
"prettier": { "prettier": {
"semi": false, "semi": false,
@ -27,7 +28,7 @@
"trailingComma": "all" "trailingComma": "all"
}, },
"resolutions": { "resolutions": {
"body-parser": "npm:@peacockproject/body-parser@npm:2.0.0-peacock.6", "body-parser": "npm:@peacockproject/body-parser@npm:3.0.0-peacock.1",
"debug": "^4.3.4", "debug": "^4.3.4",
"http-errors": "patch:http-errors@npm:2.0.0#.yarn/patches/http-errors-npm-2.0.0-3f1c503428.patch", "http-errors": "patch:http-errors@npm:2.0.0#.yarn/patches/http-errors-npm-2.0.0-3f1c503428.patch",
"iconv-lite": "patch:iconv-lite@npm:0.6.3#.yarn/patches/iconv-lite-npm-0.6.3-24b8aae27e.patch", "iconv-lite": "patch:iconv-lite@npm:0.6.3#.yarn/patches/iconv-lite-npm-0.6.3-24b8aae27e.patch",
@ -40,18 +41,18 @@
"atomically": "^2.0.2", "atomically": "^2.0.2",
"axios": "^1.6.0", "axios": "^1.6.0",
"body-parser": "*", "body-parser": "*",
"clipanion": "^3.2.1", "clipanion": "^4.0.0-rc.3",
"commander": "^11.1.0", "commander": "^11.1.0",
"deepmerge-ts": "^5.1.0", "deepmerge-ts": "^5.1.0",
"esbuild-wasm": "^0.19.5", "esbuild-wasm": "^0.19.12",
"express": "patch:express@npm%3A4.18.2#~/.yarn/patches/express-npm-4.18.2-bb15ff679a.patch", "express": "patch:express@npm%3A4.18.2#~/.yarn/patches/express-npm-4.18.2-bb15ff679a.patch",
"jest-diff": "^29.7.0", "jest-diff": "^29.7.0",
"js-ini": "^1.6.0", "js-ini": "^1.6.0",
"json5": "^2.2.3", "json5": "^2.2.3",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"md5-file": "^5.0.0", "md5-file": "^5.0.0",
"msgpackr": "^1.9.9", "msgpackr": "^1.10.1",
"nanoid": "^5.0.3", "nanoid": "^5.0.4",
"parseurl": "^1.3.3", "parseurl": "^1.3.3",
"picocolors": "patch:picocolors@npm%3A1.0.0#~/.yarn/patches/picocolors-npm-1.0.0-d81e0b1927.patch", "picocolors": "patch:picocolors@npm%3A1.0.0#~/.yarn/patches/picocolors-npm-1.0.0-d81e0b1927.patch",
"progress": "^2.0.3", "progress": "^2.0.3",
@ -71,12 +72,12 @@
"@types/progress": "^2.0.6", "@types/progress": "^2.0.6",
"@types/prompts": "^2.4.7", "@types/prompts": "^2.4.7",
"@types/send": "^0.17.3", "@types/send": "^0.17.3",
"@typescript-eslint/eslint-plugin": "^6.10.0", "@typescript-eslint/eslint-plugin": "^6.19.1",
"@typescript-eslint/parser": "^6.10.0", "@typescript-eslint/parser": "^6.19.1",
"esbuild": "^0.19.5", "esbuild": "^0.19.12",
"esbuild-register": "^3.5.0", "esbuild-register": "^3.5.0",
"eslint": "^8.53.0", "eslint": "^8.56.0",
"eslint-config-prettier": "^9.0.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-promise": "^6.1.1", "eslint-plugin-promise": "^6.1.1",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"fast-glob": "^3.3.2", "fast-glob": "^3.3.2",
@ -84,8 +85,8 @@
"ms": "^2.1.3", "ms": "^2.1.3",
"prettier": "^2.8.8", "prettier": "^2.8.8",
"rimraf": "^5.0.5", "rimraf": "^5.0.5",
"terser": "^5.21.0", "terser": "^5.27.0",
"typescript": "5.2.2", "typescript": "5.3.3",
"winston": "^3.11.0", "winston": "^3.11.0",
"winston-daily-rotate-file": "^4.7.1" "winston-daily-rotate-file": "^4.7.1"
}, },

View File

@ -74,6 +74,7 @@ await e.build({
"process.env.ZEIT_BITBUCKET_COMMIT_SHA": "undefined", "process.env.ZEIT_BITBUCKET_COMMIT_SHA": "undefined",
"process.env.VERCEL_GIT_COMMIT_SHA": "undefined", "process.env.VERCEL_GIT_COMMIT_SHA": "undefined",
"process.env.ZEIT_GITLAB_COMMIT_SHA": "undefined", "process.env.ZEIT_GITLAB_COMMIT_SHA": "undefined",
"process.env.MSGPACKR_NATIVE_ACCELERATION_DISABLED": "true",
}, },
sourcemap: "external", sourcemap: "external",
plugins: [ plugins: [

View File

@ -23,12 +23,12 @@
"dependencies": { "dependencies": {
"@peacockproject/statemachine-parser": "^5.9.3", "@peacockproject/statemachine-parser": "^5.9.3",
"@types/express": "^4.17.20", "@types/express": "^4.17.20",
"@types/node": "^20.8.10", "@types/node": "*",
"atomically": "^2.0.2", "atomically": "^2.0.2",
"axios": "^1.6.0", "axios": "^1.6.7",
"js-ini": "^1.6.0" "js-ini": "^1.6.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"msgpackr": "^1.9.9" "msgpackr": "^1.10.1"
} }
} }

View File

@ -1,245 +1,242 @@
{ {
"template": null, "SubLocationData": [],
"data": { "PlayerProfileXp": {
"SubLocationData": [], "Total": 0,
"PlayerProfileXp": { "Level": 1,
"Total": 0, "Seasons": [
"Level": 1, {
"Seasons": [ "Number": 1,
{ "Locations": [
"Number": 1, {
"Locations": [ "LocationId": "LOCATION_PARENT_ICA_FACILITY",
{ "Xp": 0,
"LocationId": "LOCATION_PARENT_ICA_FACILITY", "ActionXp": 0
"Xp": 0, },
"ActionXp": 0 {
}, "LocationId": "LOCATION_PARENT_PARIS",
{ "Xp": 0,
"LocationId": "LOCATION_PARENT_PARIS", "ActionXp": 0,
"Xp": 0, "LocationProgression": {
"ActionXp": 0, "Level": 1,
"LocationProgression": { "MaxLevel": 20
"Level": 1,
"MaxLevel": 20
}
},
{
"LocationId": "LOCATION_PARENT_COASTALTOWN",
"Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 20
}
},
{
"LocationId": "LOCATION_PARENT_MARRAKECH",
"Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 20
}
},
{
"LocationId": "LOCATION_PARENT_BANGKOK",
"Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 20
}
},
{
"LocationId": "LOCATION_PARENT_COLORADO",
"Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 20
}
},
{
"LocationId": "LOCATION_PARENT_HOKKAIDO",
"Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 20
}
} }
] },
}, {
{ "LocationId": "LOCATION_PARENT_COASTALTOWN",
"Number": 2, "Xp": 0,
"Locations": [ "ActionXp": 0,
{ "LocationProgression": {
"LocationId": "LOCATION_PARENT_NEWZEALAND", "Level": 1,
"Xp": 0, "MaxLevel": 20
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 5
}
},
{
"LocationId": "LOCATION_PARENT_MIAMI",
"Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 20
}
},
{
"LocationId": "LOCATION_PARENT_COLOMBIA",
"Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 20
}
},
{
"LocationId": "LOCATION_PARENT_MUMBAI",
"Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 20
}
},
{
"LocationId": "LOCATION_PARENT_NORTHAMERICA",
"Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 20
}
},
{
"LocationId": "LOCATION_PARENT_NORTHSEA",
"Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 20
}
},
{
"LocationId": "LOCATION_PARENT_GREEDY",
"Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 20
}
},
{
"LocationId": "LOCATION_PARENT_OPULENT",
"Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 20
}
},
{
"LocationId": "LOCATION_PARENT_AUSTRIA",
"Xp": 0,
"ActionXp": 0
},
{
"LocationId": "LOCATION_PARENT_SALTY",
"Xp": 0,
"ActionXp": 0
},
{
"LocationId": "LOCATION_PARENT_CAGED",
"Xp": 0,
"ActionXp": 0
} }
] },
}, {
{ "LocationId": "LOCATION_PARENT_MARRAKECH",
"Number": 3, "Xp": 0,
"Locations": [ "ActionXp": 0,
{ "LocationProgression": {
"LocationId": "LOCATION_PARENT_GOLDEN", "Level": 1,
"Xp": 0, "MaxLevel": 20
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 20
}
},
{
"LocationId": "LOCATION_PARENT_ANCESTRAL",
"Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 20
}
},
{
"LocationId": "LOCATION_PARENT_EDGY",
"Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 20
}
},
{
"LocationId": "LOCATION_PARENT_WET",
"Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 20
}
},
{
"LocationId": "LOCATION_PARENT_ELEGANT",
"Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 20
}
},
{
"LocationId": "LOCATION_PARENT_TRAPPED",
"Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 5
}
},
{
"LocationId": "LOCATION_PARENT_ROCKY",
"Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 20
}
},
{
"LocationId": "LOCATION_PARENT_SNUG",
"Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 100
}
} }
] },
} {
] "LocationId": "LOCATION_PARENT_BANGKOK",
} "Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 20
}
},
{
"LocationId": "LOCATION_PARENT_COLORADO",
"Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 20
}
},
{
"LocationId": "LOCATION_PARENT_HOKKAIDO",
"Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 20
}
}
]
},
{
"Number": 2,
"Locations": [
{
"LocationId": "LOCATION_PARENT_NEWZEALAND",
"Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 5
}
},
{
"LocationId": "LOCATION_PARENT_MIAMI",
"Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 20
}
},
{
"LocationId": "LOCATION_PARENT_COLOMBIA",
"Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 20
}
},
{
"LocationId": "LOCATION_PARENT_MUMBAI",
"Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 20
}
},
{
"LocationId": "LOCATION_PARENT_NORTHAMERICA",
"Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 20
}
},
{
"LocationId": "LOCATION_PARENT_NORTHSEA",
"Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 20
}
},
{
"LocationId": "LOCATION_PARENT_GREEDY",
"Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 20
}
},
{
"LocationId": "LOCATION_PARENT_OPULENT",
"Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 20
}
},
{
"LocationId": "LOCATION_PARENT_AUSTRIA",
"Xp": 0,
"ActionXp": 0
},
{
"LocationId": "LOCATION_PARENT_SALTY",
"Xp": 0,
"ActionXp": 0
},
{
"LocationId": "LOCATION_PARENT_CAGED",
"Xp": 0,
"ActionXp": 0
}
]
},
{
"Number": 3,
"Locations": [
{
"LocationId": "LOCATION_PARENT_GOLDEN",
"Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 20
}
},
{
"LocationId": "LOCATION_PARENT_ANCESTRAL",
"Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 20
}
},
{
"LocationId": "LOCATION_PARENT_EDGY",
"Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 20
}
},
{
"LocationId": "LOCATION_PARENT_WET",
"Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 20
}
},
{
"LocationId": "LOCATION_PARENT_ELEGANT",
"Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 20
}
},
{
"LocationId": "LOCATION_PARENT_TRAPPED",
"Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 5
}
},
{
"LocationId": "LOCATION_PARENT_ROCKY",
"Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 20
}
},
{
"LocationId": "LOCATION_PARENT_SNUG",
"Xp": 0,
"ActionXp": 0,
"LocationProgression": {
"Level": 1,
"MaxLevel": 100
}
}
]
}
]
} }
} }

View File

@ -26,8 +26,11 @@ export function asMock<T>(value: T): Mock {
return value as Mock return value as Mock
} }
export function mockRequestWithJwt(): RequestWithJwt<core.Query, any> { export function mockRequestWithJwt<
const mockedRequest = <RequestWithJwt<core.Query, any>>{ QS = core.Query,
Body = any,
>(): RequestWithJwt<QS, Body> {
const mockedRequest = <RequestWithJwt<QS, Body>>{
headers: {}, headers: {},
header: (name: string) => header: (name: string) =>
mockedRequest.headers[name.toLowerCase()] as string, mockedRequest.headers[name.toLowerCase()] as string,
@ -36,10 +39,10 @@ export function mockRequestWithJwt(): RequestWithJwt<core.Query, any> {
return mockedRequest return mockedRequest
} }
export function mockRequestWithValidJwt( export function mockRequestWithValidJwt<QS = core.Query, Body = any>(
pId: string, pId: string,
): RequestWithJwt<core.Query, any> { ): RequestWithJwt<QS, Body> {
const mockedRequest = mockRequestWithJwt() const mockedRequest = mockRequestWithJwt<QS, Body>()
const jwtToken = sign( const jwtToken = sign(
{ {
@ -64,15 +67,20 @@ export function mockResponse(): core.Response {
return response return response
} }
// @ts-expect-error It works.
response.status = vi.fn().mockImplementation(mockImplementation) response.status = vi.fn().mockImplementation(mockImplementation)
// @ts-expect-error It works.
response.json = vi.fn() response.json = vi.fn()
// @ts-expect-error It works.
response.end = vi.fn() response.end = vi.fn()
// @ts-expect-error It works.
return <core.Response>response return <core.Response>response
} }
export function getResolvingPromise<T>(value?: T): Promise<T> { export function getResolvingPromise<T>(value?: T): Promise<T> {
return new Promise((resolve) => { return new Promise((resolve) => {
// @ts-expect-error It works.
resolve(value) resolve(value)
}) })
} }

View File

@ -18,12 +18,15 @@
import * as configSwizzleManager from "../../components/configSwizzleManager" import * as configSwizzleManager from "../../components/configSwizzleManager"
import { readFileSync } from "fs" import { readFileSync } from "fs"
import { vi } from "vitest"
const originalFilePaths: Record<string, string> = {} const originalFilePaths: Record<string, string> = {}
Object.keys(configSwizzleManager.configs).forEach((config: string) => { Object.keys(configSwizzleManager.configs).forEach((config: string) => {
// @ts-expect-error It works.
originalFilePaths[config] = <string>configSwizzleManager.configs[config] originalFilePaths[config] = <string>configSwizzleManager.configs[config]
// @ts-expect-error It works.
configSwizzleManager.configs[config] = undefined configSwizzleManager.configs[config] = undefined
}) })
@ -34,20 +37,24 @@ export function loadConfig(config: string) {
const contents = readFileSync(originalFilePaths[config], "utf-8") const contents = readFileSync(originalFilePaths[config], "utf-8")
// @ts-expect-error It works.
configSwizzleManager.configs[config] = JSON.parse(contents) configSwizzleManager.configs[config] = JSON.parse(contents)
} }
export function setConfig(config: string, data: unknown) { export function setConfig(config: string, data: unknown) {
// @ts-expect-error It works.
configSwizzleManager.configs[config] = data configSwizzleManager.configs[config] = data
} }
const getConfigOriginal = configSwizzleManager.getConfig const getConfigOriginal = configSwizzleManager.getConfig
vi.spyOn(configSwizzleManager, "getConfig").mockImplementation( vi.spyOn(configSwizzleManager, "getConfig").mockImplementation(
(config: string, clone: boolean) => { (config: string, clone: boolean) => {
// @ts-expect-error It works.
if (!configSwizzleManager.configs[config]) { if (!configSwizzleManager.configs[config]) {
throw `Config '${config}' has not been loaded!` throw `Config '${config}' has not been loaded!`
} }
// @ts-expect-error It works.
return getConfigOriginal(config, clone) return getConfigOriginal(config, clone)
}, },
) )

View File

@ -3,12 +3,13 @@
"type": "module", "type": "module",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@vitest/ui": "^0.34.6", "@vitest/ui": "^1.2.2",
"vite": "^5.0.12", "vite": "^5.0.12",
"vitest": "^0.34.6" "vitest": "^1.2.2"
}, },
"scripts": { "scripts": {
"test:main": "vitest --run --config vitest.config.ts", "test:main": "vitest --run --config vitest.config.ts",
"test:ui": "vitest --config vitest.config.ts --ui" "test:ui": "vitest --config vitest.config.ts --ui",
"typecheck-ws": "tsc --noEmit"
} }
} }

View File

@ -18,7 +18,12 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import { UserProfile } from "../../components/types/types" import { UserProfile } from "../../components/types/types"
import { handleOauthToken, JWT_SECRET } from "../../components/oauthToken" import {
error406,
handleOAuthToken,
JWT_SECRET,
OAuthTokenResponse,
} from "../../components/oauthToken"
import { sign, verify } from "jsonwebtoken" import { sign, verify } from "jsonwebtoken"
import * as databaseHandler from "../../components/databaseHandler" import * as databaseHandler from "../../components/databaseHandler"
import * as platformEntitlements from "../../components/platformEntitlements" import * as platformEntitlements from "../../components/platformEntitlements"
@ -26,11 +31,9 @@ import axios from "axios"
import { describe, expect, beforeEach, vi, it } from "vitest" import { describe, expect, beforeEach, vi, it } from "vitest"
import { import {
getMockCallArgument,
getResolvingPromise, getResolvingPromise,
mockRequestWithJwt, mockRequestWithJwt,
mockRequestWithValidJwt, mockRequestWithValidJwt,
mockResponse,
} from "../helpers/testHelpers" } from "../helpers/testHelpers"
describe("oauthToken", () => { describe("oauthToken", () => {
@ -41,6 +44,7 @@ describe("oauthToken", () => {
.mockResolvedValue("") .mockResolvedValue("")
const loadUserData = vi const loadUserData = vi
.spyOn(databaseHandler, "loadUserData") .spyOn(databaseHandler, "loadUserData")
// @ts-expect-error This is okay.
.mockResolvedValue(undefined) .mockResolvedValue(undefined)
const getUserData = vi const getUserData = vi
.spyOn(databaseHandler, "getUserData") .spyOn(databaseHandler, "getUserData")
@ -65,10 +69,10 @@ describe("oauthToken", () => {
}) })
} }
return undefined return getResolvingPromise({})
}) })
const request = mockRequestWithJwt() const request = mockRequestWithJwt<never, any>()
request.body = { request.body = {
grant_type: "external_steam", grant_type: "external_steam",
steam_userid: "000000000047", steam_userid: "000000000047",
@ -76,9 +80,7 @@ describe("oauthToken", () => {
pId: pId, pId: pId,
} }
const response = mockResponse() const res = await handleOAuthToken(request)
await handleOauthToken(request, response)
expect(getExternalUserData).toHaveBeenCalledWith( expect(getExternalUserData).toHaveBeenCalledWith(
"000000000047", "000000000047",
@ -88,12 +90,15 @@ describe("oauthToken", () => {
expect(loadUserData).toHaveBeenCalledWith(pId, "h3") expect(loadUserData).toHaveBeenCalledWith(pId, "h3")
expect(getUserData).toHaveBeenCalledWith(pId, "h3") expect(getUserData).toHaveBeenCalledWith(pId, "h3")
const jsonResponse = getMockCallArgument<any>(response.json, 0, 0) const accessToken = verify(
const accessToken = verify(jsonResponse.access_token, JWT_SECRET, { (res as OAuthTokenResponse).access_token,
complete: true, JWT_SECRET,
}) {
complete: true,
},
)
expect(jsonResponse.token_type).toBe("bearer") expect((res as OAuthTokenResponse).token_type).toBe("bearer")
expect((accessToken.payload as any).unique_name).toBe(pId) expect((accessToken.payload as any).unique_name).toBe(pId)
}) })
@ -102,7 +107,7 @@ describe("oauthToken", () => {
["mock"], ["mock"],
) )
const request = mockRequestWithJwt() const request = mockRequestWithJwt<never, any>()
request.body = { request.body = {
grant_type: "external_epic", grant_type: "external_epic",
epic_userid: "0123456789abcdef0123456789abcdef", epic_userid: "0123456789abcdef0123456789abcdef",
@ -118,9 +123,7 @@ describe("oauthToken", () => {
pId: pId, pId: pId,
} }
const response = mockResponse() const res = await handleOAuthToken(request)
await handleOauthToken(request, response)
expect(getExternalUserData).toHaveBeenCalledWith( expect(getExternalUserData).toHaveBeenCalledWith(
"0123456789abcdef0123456789abcdef", "0123456789abcdef0123456789abcdef",
@ -130,85 +133,84 @@ describe("oauthToken", () => {
expect(loadUserData).toHaveBeenCalledWith(pId, "h3") expect(loadUserData).toHaveBeenCalledWith(pId, "h3")
expect(getUserData).toHaveBeenCalledWith(pId, "h3") expect(getUserData).toHaveBeenCalledWith(pId, "h3")
const jsonResponse = getMockCallArgument<any>(response.json, 0, 0) const accessToken = verify(
const accessToken = verify(jsonResponse.access_token, JWT_SECRET, { (res as OAuthTokenResponse).access_token,
complete: true, JWT_SECRET,
}) {
complete: true,
},
)
expect(jsonResponse.token_type).toBe("bearer") expect((res as OAuthTokenResponse).token_type).toBe("bearer")
expect((accessToken.payload as any).unique_name).toBe(pId) expect((accessToken.payload as any).unique_name).toBe(pId)
}) })
it("refresh_token - missing auth header", async () => { it("refresh_token - missing auth header", async () => {
const request = mockRequestWithJwt() const request = mockRequestWithJwt<never, any>()
request.body = { request.body = {
grant_type: "refresh_token", grant_type: "refresh_token",
} }
const respose = mockResponse() let error: Error | undefined = undefined
let error: Error = undefined
try { try {
await handleOauthToken(request, respose) await handleOAuthToken(request)
} catch (e) { } catch (e) {
error = e error = e as Error
} }
expect(error).toBeInstanceOf(TypeError) expect(error).toBeInstanceOf(TypeError)
}) })
it("refresh_token - invalid auth header", async () => { it("refresh_token - invalid auth header", async () => {
const request = mockRequestWithJwt() const request = mockRequestWithJwt<never, any>()
request.headers.authorization = "Bearer invalid" request.headers.authorization = "Bearer invalid"
request.body = { request.body = {
grant_type: "refresh_token", grant_type: "refresh_token",
} }
const respose = mockResponse() let error: Error | undefined = undefined
let error: Error = undefined
try { try {
await handleOauthToken(request, respose) await handleOAuthToken(request)
} catch (e) { } catch (e) {
error = e error = e as Error
} }
expect(error).toBeInstanceOf(TypeError) expect(error).toBeInstanceOf(TypeError)
}) })
it("refresh_token - valid auth header", async () => { it("refresh_token - valid auth header", async () => {
const request = mockRequestWithValidJwt(pId) const request = mockRequestWithValidJwt<never>(pId)
// NOTE: We don't care about the actual values // NOTE: We don't care about the actual values
request.body = { request.body = {
grant_type: "refresh_token", grant_type: "refresh_token",
} }
const response = mockResponse() const res = await handleOAuthToken(request)
await handleOauthToken(request, response) const accessToken = verify(
(res as OAuthTokenResponse).access_token,
JWT_SECRET,
{
complete: true,
},
)
const jsonResponse = getMockCallArgument<any>(response.json, 0, 0) expect((res as OAuthTokenResponse).token_type).toBe("bearer")
const accessToken = verify(jsonResponse.access_token, JWT_SECRET, {
complete: true,
})
expect(jsonResponse.token_type).toBe("bearer")
expect((accessToken.payload as any).unique_name).toBe(pId) expect((accessToken.payload as any).unique_name).toBe(pId)
}) })
it("no grant_type", async () => { it("no grant_type", async () => {
const request = mockRequestWithJwt() const request = mockRequestWithJwt<never, any>()
request.body = {} request.body = {}
request.query = {} as never
const respose = mockResponse() const res = await handleOAuthToken(request)
await handleOauthToken(request, respose) expect(res).toEqual(error406)
expect(respose.status).toHaveBeenCalledWith(406)
}) })
}) })

View File

@ -4,7 +4,8 @@
// Reset rootDir to default to make rootDirs take effect // Reset rootDir to default to make rootDirs take effect
"rootDir": null, "rootDir": null,
"rootDirs": ["../components", "."], "rootDirs": ["../components", "."],
"types": ["vitest/globals"] "noEmit": true,
"emitDeclarationOnly": false
}, },
"include": ["../components", "**/*.ts"], "include": ["../components", "**/*.ts"],
"exclude": [] "exclude": []

View File

@ -6,7 +6,7 @@
"lib": ["ESNext"], "lib": ["ESNext"],
"emitDeclarationOnly": true, "emitDeclarationOnly": true,
"checkJs": false, "checkJs": false,
"strict": false, "strict": true,
"noUnusedLocals": true, "noUnusedLocals": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noImplicitOverride": true, "noImplicitOverride": true,
@ -21,7 +21,8 @@
"rootDir": "components", "rootDir": "components",
"outDir": "./build", "outDir": "./build",
"isolatedModules": true, "isolatedModules": true,
"stripInternal": true "stripInternal": true,
"strictNullChecks": true
}, },
"include": ["components"], "include": ["components"],
"exclude": [ "exclude": [

View File

@ -4,7 +4,7 @@
"private": true, "private": true,
"dependencies": { "dependencies": {
"axios": "^1.6.0", "axios": "^1.6.0",
"clsx": "^2.0.0", "clsx": "^2.1.0",
"immer": "^10.0.3", "immer": "^10.0.3",
"infima": "0.2.0-alpha.38", "infima": "0.2.0-alpha.38",
"json-keys-sort": "^2.1.0", "json-keys-sort": "^2.1.0",
@ -20,10 +20,10 @@
"typecheck-ws": "tsc" "typecheck-ws": "tsc"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^18.2.36", "@types/react": "^18.2.48",
"@types/react-dom": "^18.2.14", "@types/react-dom": "^18.2.18",
"rollup-plugin-license": "^3.2.0", "rollup-plugin-license": "^3.2.0",
"typescript": "5.2.2", "typescript": "5.3.3",
"vite": "^5.0.12" "vite": "^5.0.12"
}, },
"peerDependencies": { "peerDependencies": {

1717
yarn.lock

File diff suppressed because it is too large Load Diff