1
mirror of https://github.com/thepeacockproject/Peacock synced 2024-11-29 09:15:11 +01:00
Peacock/components/utils.ts
Reece Dunham 4575924e80
Rewrite the escalation service to use group contracts (#63)
* Rewrite the escalation service to use group contracts (#28)

* Fix build and type errors

Signed-off-by: Reece Dunham <me@rdil.rocks>

* Improve Escalation Functionality with Contract Groups (#160)

* Fix Sinbad escalation and add group definition

* Add group contracts and fix InGroup IDs where needed

* Run prettier

* Add missing group definitions

* Fixed id issues with sinbad

* Fix missionsInLocation.ts

* Added groupdefinitions (#198)

Added localization and missing groupdefinitions for Peacock custom escalations

* Fix incorrect escalation contract ids

* Remove missing escalations

* Add Ataro group definition

* Add 7DS entrances

* Restore no2016 functionality, add xmas to no2016 list

* Add missing deluxe escalation entrance

* Fix linting

* Added h3 escalations (#204)

* Added h3 escalations

Added all remaining escalations from h3 maps

* Prettier

yeehaw

---------

Co-authored-by: Anthony Fuller <24512050+AnthonyFuller@users.noreply.github.com>

* Fix escalation completion

* Fix smilax level 1

* Fix escalation challenges not completing

* Get groups when resolving contracts

* track escalation challenge completion

* fix mission end page for escalation challenges

* Update GameChangerProperties

* Update EvergreenGameChangerProperties

* Add new GameChangerProperties

* Fix aborting on invalid escalation group

* remove dupe yellow rabbit suit

* Fixed DGS having no challenges on career page

* run prettier

* Update Proloff Level 2

* Update escalation hub tile to work with group contracts

* Move escalations and elusives to subfolders

* Add 7DS campaign

* Fix escalation level picker

* Fix escalations being incorrectly marked as completed

* Remove completed status when editing escalation level progress

* Add new H3 escalations to level picker

* Add Season tag to elusives for future use

* Add Season tag to typedefs

* Respect Season tag when sending elusives

* Add Legacy Escalations

* Remove milfoil for now, add escalations to missions

* Move xmas escalation

* Fix Snowdrop not showing in 2016

* Add missing entitlements to escalations

* Fix play next level in 2016, remove use of deprecated function

* Move remaining Peacock escalations

* Swap out featured Peacock escalation

---------

Signed-off-by: Reece Dunham <me@rdil.rocks>
Co-authored-by: moonysolari <118079569+moonysolari@users.noreply.github.com>
Co-authored-by: Kaki <66200818+Kakiking@users.noreply.github.com>
Co-authored-by: moonysolari <changyiding@126.com>
Co-authored-by: riisikumi <54016129+riisikumi@users.noreply.github.com>
Co-authored-by: AnthonyFuller <24512050+AnthonyFuller@users.noreply.github.com>
2023-04-14 03:13:16 +01:00

583 lines
19 KiB
TypeScript

/*
* The Peacock Project - a HITMAN server replacement.
* Copyright (C) 2021-2023 The Peacock Project Team
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { decode } from "jsonwebtoken"
import type { NextFunction, Response } from "express"
import type {
GameVersion,
MissionManifestObjective,
RepositoryId,
RequestWithJwt,
ServerVersion,
Unlockable,
UserProfile,
} from "./types/types"
import axios, { AxiosError } from "axios"
import { log, LogLevel } from "./loggingInterop"
import { writeFileSync } from "fs"
import { getFlag } from "./flags"
import { getConfig } from "./configSwizzleManager"
/**
* True if the server is being run by the launcher, false otherwise.
*/
export const IS_LAUNCHER = process.env.IS_PEACOCK_LAUNCHER === "true"
export const ServerVer: ServerVersion = {
_Major: 8,
_Minor: 10,
_Build: 0,
_Revision: 0,
}
export const PEACOCKVER = REV_IDENT
export const PEACOCKVERSTRING = HUMAN_VERSION
export const uuidRegex =
/^[a-zA-Z0-9]{8}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{12}$/
export const contractTypes = ["featured", "usercreated"]
export const versions: GameVersion[] = ["h1", "h2", "h3"]
export const contractCreationTutorialId = "d7e2607c-6916-48e2-9588-976c7d8998bb"
export async function checkForUpdates(): Promise<void> {
if (getFlag("updateChecking") === false) {
return
}
try {
const res = await axios(
"https://backend.rdil.rocks/peacock/latest-version",
)
const current = res.data
if (PEACOCKVER < 0 && current < -PEACOCKVER) {
log(
LogLevel.INFO,
`Thank you for trying out this testing version of Peacock! Please report any bugs by posting in the #help channel on Discord or by submitting an issue on GitHub.`,
)
} else if (PEACOCKVER > 0 && current === PEACOCKVER) {
log(LogLevel.DEBUG, "Peacock is up to date.")
} else {
log(
LogLevel.WARN,
`Peacock is out-of-date! Check the Discord for the latest release.`,
)
}
} catch (e) {
log(LogLevel.WARN, "Failed to check for updates!")
}
}
export function getRemoteService(gameVersion: GameVersion): string {
return gameVersion === "h3"
? "hm3-service"
: gameVersion === "h2"
? "pc2-service"
: "pc-service"
}
/**
* Middleware that validates the authentication token sent by the client.
*
* @param req The express request object.
* @param res The express response object.
* @param next The express next function.
*/
export function extractToken(
req: RequestWithJwt,
res?: Response,
next?: NextFunction,
): void {
const header = req.header("Authorization")
const auth = header ? header.split(" ") : []
if (auth.length === 2 && auth[0].toLowerCase() === "bearer") {
// @ts-expect-error Type overlap
req.jwt = <string>decode(auth[1])
if (!uuidRegex.test(req.jwt.unique_name)) {
res?.status(424).send("profile appears to be corrupted")
return
}
next?.()
return
}
req.shouldCease = true
next?.("router")
}
export const DEFAULT_MASTERY_MAXLEVEL = 20
export const XP_PER_LEVEL = 6000
export function getMaxProfileLevel(gameVersion: GameVersion): number {
if (gameVersion === "h3") {
return 7500
}
return 5000
}
/**
* Calculates the level for the given XP based on XP_PER_LEVEL.
* Minimum level returned is 1.
*/
export function levelForXp(xp: number): number {
return Math.max(1, Math.floor(xp / XP_PER_LEVEL) + 1)
}
/**
* Calculates the required XP for the given level based on XP_PER_LEVEL.
* Minimum XP returned is 0.
*/
export function xpRequiredForLevel(level: number): number {
return Math.max(0, (level - 1) * XP_PER_LEVEL)
}
//TODO: Determine some mathematical function
export const EVERGREEN_LEVEL_INFO: number[] = [
0, 5000, 10000, 17000, 24000, 31000, 38000, 45000, 52000, 61000, 70000,
79000, 88000, 97000, 106000, 115000, 124000, 133000, 142000, 154000, 166000,
178000, 190000, 202000, 214000, 226000, 238000, 250000, 262000, 280000,
298000, 316000, 334000, 352000, 370000, 388000, 406000, 424000, 442000,
468000, 494000, 520000, 546000, 572000, 598000, 624000, 650000, 676000,
702000, 736000, 770000, 804000, 838000, 872000, 906000, 940000, 974000,
1008000, 1042000, 1082000, 1122000, 1162000, 1202000, 1242000, 1282000,
1322000, 1362000, 1402000, 1442000, 1492000, 1542000, 1592000, 1642000,
1692000, 1742000, 1792000, 1842000, 1892000, 1942000, 2002000, 2062000,
2122000, 2182000, 2242000, 2302000, 2362000, 2422000, 2482000, 2542000,
2692000, 2842000, 2992000, 3142000, 3292000, 3442000, 3592000, 3742000,
3892000, 4042000, 4192000,
]
export function evergreenLevelForXp(xp: number): number {
for (let i = 1; i < EVERGREEN_LEVEL_INFO.length; i++) {
if (xp >= EVERGREEN_LEVEL_INFO[i]) {
continue
}
return i
}
return 1
}
export function xpRequiredForEvergreenLevel(level: number): number {
return EVERGREEN_LEVEL_INFO[level - 1]
}
//TODO: Determine some mathematical function
export const SNIPER_LEVEL_INFO: number[] = [
0, 50000, 150000, 500000, 1000000, 1700000, 2500000, 3500000, 5000000,
7000000, 9500000, 12500000, 16000000, 20000000, 25000000, 31000000,
38000000, 47000000, 58000000, 70000000,
]
/**
* Get the number of xp needed to reach a level in sniper missions.
* @param level The level in question.
* @returns The xp, as a number.
*/
export function xpRequiredForSniperLevel(level: number): number {
return SNIPER_LEVEL_INFO[level - 1]
}
/**
* Clamps the given value between a minimum and maximum value
*/
export function clampValue(value: number, min: number, max: number) {
return Math.max(min, Math.min(value, max))
}
/**
* Returns whether a location is a sniper location. Works for both parent and child locations.
* @param location The location ID string.
* @returns A boolean denoting the result.
*/
export function isSniperLocation(location: string): boolean {
return (
location.includes("AUSTRIA") ||
location.includes("SALTY") ||
location.includes("CAGED")
)
}
export function castUserProfile(profile: UserProfile): UserProfile {
const j = fastClone(profile)
if (!j.Extensions || Object.keys(j.Extensions).length === 0) {
log(LogLevel.DEBUG, "No extensions - skipping validation")
return j
}
let dirty = false
for (const item of [
"PeacockEscalations",
"PeacockFavoriteContracts",
"PeacockPlayedContracts",
"PeacockCompletedEscalations",
"CPD",
]) {
if (!Object.prototype.hasOwnProperty.call(j.Extensions, item)) {
log(LogLevel.DEBUG, `Err missing property ${item}`)
log(
LogLevel.WARN,
`A requested user profile is missing some vital data.`,
)
log(
LogLevel.WARN,
`Attempting to repair the profile automatically...`,
)
if (item === "PeacockEscalations") {
j.Extensions.PeacockEscalations = {}
}
if (item === "PeacockCompletedEscalations") {
j.Extensions.PeacockCompletedEscalations = []
}
if (item === "PeacockFavoriteContracts") {
j.Extensions.PeacockFavoriteContracts = []
}
if (item === "CPD") {
j.Extensions.CPD = {}
}
if (item === "PeacockPlayedContracts") {
j.Extensions.PeacockPlayedContracts = {}
}
dirty = true
}
}
// Fix Extensions.gamepersistentdata.HitsFilterType.
// None of the old profiles should have "MyPlaylist".
if (
!Object.prototype.hasOwnProperty.call(
j.Extensions.gamepersistentdata.HitsFilterType,
"MyPlaylist",
)
) {
j.Extensions.gamepersistentdata.HitsFilterType = {
MyHistory: "all",
MyContracts: "all",
MyPlaylist: "all",
}
}
if (j.Extensions?.gamepersistentdata?.PersistentBool) {
switch (getFlag("mapDiscoveryState")) {
case "REVEALED":
{
const areas = Object.keys(getConfig("AreaMap", false))
for (const area of areas) {
j.Extensions.gamepersistentdata.PersistentBool[area] =
true
}
}
break
case "CLOUDED":
j.Extensions.gamepersistentdata.PersistentBool = {
__Full:
j.Extensions.gamepersistentdata.PersistentBool
?.__Full ?? {},
}
break
}
}
if (dirty) {
writeFileSync(`userdata/users/${j.Id}.json`, JSON.stringify(j))
log(LogLevel.INFO, "Profile successfully repaired!")
}
return j
}
export const defaultSuits = {
LOCATION_PARENT_ICA_FACILITY: "TOKEN_OUTFIT_GREENLAND_HERO_TRAININGSUIT",
LOCATION_PARENT_PARIS: "TOKEN_OUTFIT_PARIS_HERO_PARISSUIT",
LOCATION_PARENT_COASTALTOWN: "TOKEN_OUTFIT_SAPIENZA_HERO_SAPIENZASUIT",
LOCATION_COASTALTOWN_MOVIESET:
"TOKEN_OUTFIT_SAPIENZA_HERO_SAPIENZASUIT_NOGLASSES",
LOCATION_COASTALTOWN_EBOLA:
"TOKEN_OUTFIT_SAPIENZA_HERO_SAPIENZASUIT_NOGLASSES",
LOCATION_PARENT_MARRAKECH: "TOKEN_OUTFIT_MARRAKESH_HERO_MARRAKESHSUIT",
LOCATION_PARENT_BANGKOK: "TOKEN_OUTFIT_BANGKOK_HERO_BANGKOKSUIT",
LOCATION_PARENT_COLORADO: "TOKEN_OUTFIT_COLORADO_HERO_COLORADOSUIT",
LOCATION_PARENT_HOKKAIDO: "TOKEN_OUTFIT_HOKKAIDO_HERO_HOKKAIDOSUIT",
LOCATION_PARENT_NEWZEALAND: "TOKEN_OUTFIT_WET_SUIT",
LOCATION_PARENT_MIAMI: "TOKEN_OUTFIT_MIAMI_HERO_MIAMISUIT",
LOCATION_PARENT_COLOMBIA: "TOKEN_OUTFIT_COLOMBIA_HERO_COLOMBIASUIT",
LOCATION_PARENT_MUMBAI: "TOKEN_OUTFIT_MUMBAI_HERO_MUMBAISUIT",
LOCATION_PARENT_NORTHAMERICA:
"TOKEN_OUTFIT_NORTHAMERICA_HERO_NORTHAMERICASUIT",
LOCATION_PARENT_NORTHSEA: "TOKEN_OUTFIT_NORTHSEA_HERO_NORTHSEASUIT",
LOCATION_PARENT_GREEDY: "TOKEN_OUTFIT_GREEDY_HERO_GREEDYSUIT",
LOCATION_PARENT_OPULENT: "TOKEN_OUTFIT_OPULENT_HERO_OPULENTSUIT",
LOCATION_PARENT_GOLDEN: "TOKEN_OUTFIT_HERO_GECKO_SUIT",
LOCATION_PARENT_ANCESTRAL: "TOKEN_OUTFIT_ANCESTRAL_HERO_ANCESTRALSUIT",
LOCATION_ANCESTRAL_SMOOTHSNAKE:
"TOKEN_OUTFIT_ANCESTRAL_HERO_SMOOTHSNAKESUIT",
LOCATION_PARENT_EDGY: "TOKEN_OUTFIT_EDGY_HERO_EDGYSUIT",
LOCATION_PARENT_WET: "TOKEN_OUTFIT_WET_HERO_WETSUIT",
LOCATION_PARENT_ELEGANT: "TOKEN_OUTFIT_ELEGANT_HERO_LLAMASUIT",
LOCATION_PARENT_TRAPPED: "TOKEN_OUTFIT_TRAPPED_WOLVERINE_SUIT",
LOCATION_PARENT_ROCKY: "TOKEN_OUTFIT_HERO_DUGONG_SUIT",
}
/**
* Default suits that are attainable via challenges or mastery in this version.
* NOTE: Currently this is hardcoded. To allow for flexibility and extensibility, this should be generated in real-time
* using the Drops of challenges and masteries. However, that would require looping through all challenges and masteries
* for all default suits, which is slow. This is a trade-off.
* @param gameVersion The game version.
* @returns The default suits that are attainable via challenges or mastery.
*/
export function attainableDefaults(gameVersion: GameVersion): string[] {
return gameVersion === "h1"
? []
: gameVersion === "h2"
? ["TOKEN_OUTFIT_WET_SUIT"]
: [
"TOKEN_OUTFIT_GREENLAND_HERO_TRAININGSUIT",
"TOKEN_OUTFIT_WET_SUIT",
"TOKEN_OUTFIT_HERO_DUGONG_SUIT",
]
}
/**
* Gets the default suit for a given sublocation and parent location.
* Priority is given to the sublocation, then the parent location, then 47's signature suit.
* @param sub The sublocation.
* @param parent The parent location.
* @returns The default suit for the given sublocation and parent location.
*/
export function getDefaultSuitFor(sublocation: Unlockable) {
return (
defaultSuits[sublocation.Id] ||
defaultSuits[sublocation.Properties.ParentLocation] ||
"TOKEN_OUTFIT_HITMANSUIT"
)
}
export const nilUuid = "00000000-0000-0000-0000-000000000000"
export const hitmapsUrl = "https://backend.rdil.rocks/partners/hitmaps/contract"
export function isObjectiveActive(
objective: MissionManifestObjective,
doneObjectives: Set<RepositoryId>,
): boolean {
if (objective.Activation !== undefined) {
if (Array.isArray(objective.Activation.$eq)) {
return (
objective.Activation.$eq[1] === "Completed" &&
doneObjectives.has(
(<string>objective.Activation.$eq[0]).substring(1),
)
)
}
}
return false
}
export const jokes = [
"STILL... we are close now, 47.",
"James Batty, the plaintiff, wants Janus to stop his annual landing of a helicopter, near the local creek.",
"Searching for local masons in your area...",
"I wonder what kind of curry currymaker makes",
"Listening for uncommon guard voice lines...",
"Mistah Jason Portman, please come to the hospital entrance. A doctor will escort you to your checkup. That was for: mistah Jason Portman.",
"Nakamura-San, please come to the operating theatre. Stat.",
"Ugh, my knee is so sore.",
"Mister Prichard I presume? Hi, I'm Chen Ting. Pleasure to meet you.",
"Find your inner... zen",
"I love those little drones when they scoot around like that.",
"Welcome, welcome! Hello! Good to see you. So lovely to see so many familiar faces here today.",
"hi so i own hitman 1 and 2 i got hitman 1 and 2 on disc hitman 1 def edition and hitman 2 gold disc both on ps4 and when i got hitman 2 i downloaded the best legesy pack and all of my saves are on hitman 2 but i got rid of that game will i need both of them to get all the levels onto hitman 3 and im staying on ps4",
"The weather is always nice in Paris.",
"Have you seen a girl around? Short hair, with a bright green bag?",
"The show is just about to start.",
"Entering the Ether lab requires a uniform and a keycard... luckily, it seems both are within reach.",
"Welcome to Bangkok, 47. The target has been in residence at the hotel for almost a month and security is tight, with several other high-profile guests also at the hotel. Good Hunting.",
]
/**
* The current game difficulty.
*/
export const gameDifficulty = {
/**
* The game isn't on a set difficulty (missions with no difficulties data?).
*/
unset: 0,
/**
* Casual mode.
*/
casual: 1,
easy: 1,
/**
* Professional (normal) mode.
*/
normal: 2,
/**
* Master mode.
*/
master: 4,
hard: 4,
} as const
export function difficultyToString(difficulty: number): string {
switch (difficulty) {
case 1:
return "casual"
case 2:
return "normal"
case 4:
return "master"
case 0:
default:
return "unset"
}
}
/**
* Handle an {@link AxiosError} from axios.
*
* @see https://axios-http.com/docs/handling_errors
* @param error The error from axios.
*/
export function handleAxiosError(error: AxiosError): void {
if (error.response) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
log(LogLevel.DEBUG, `code ${error.response.status}`)
} else if (error.request) {
// The request was made but no response was received
// `error.request` is an instance of http.ClientRequest
log(LogLevel.DEBUG, `bad fetch`)
} else {
// Something happened in setting up the request that triggered an Error
log(LogLevel.DEBUG, `generic`)
}
}
export function unlockOrderComparer(a: Unlockable, b: Unlockable): number {
return (
(a?.Properties?.UnlockOrder ?? Number.POSITIVE_INFINITY) -
(b?.Properties?.UnlockOrder ?? Number.POSITIVE_INFINITY) || 0
)
}
/**
* Converts a contract's public ID as a long-form number into the version with dashes.
*
* @param publicId The ID without dashes.
* @returns The ID with dashes.
*/
export function addDashesToPublicId(publicId: string): string {
if (publicId.includes("-")) {
// this work is already done
return publicId
}
let id = publicId
id = id.slice(0, 1) + "-" + id.slice(1)
id = id.slice(0, 4) + "-" + id.slice(4)
id = id.slice(0, 12) + "-" + id.slice(12)
return id
}
/**
* A fast implementation of deep object cloning.
*
* @param item The item to clone.
* @returns The new item.
*/
export function fastClone<T>(item: T): T {
// null, undefined values check
if (!item) {
return item
}
const types = [Number, String, Boolean]
let result
// normalizing primitives if someone did new String("aaa"), or new Number("444")
for (const type of types) {
if (item instanceof type) {
result = type(item)
}
}
if (typeof result === "undefined") {
if (Array.isArray(item)) {
result = []
// Ugly type casting.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const itemAsArray: Array<typeof item> = item as any
itemAsArray.forEach((child, index) => {
result[index] = fastClone(child)
})
} else if (typeof item === "object") {
// this is a literal
if (item instanceof Date) {
result = new Date(item)
} else {
// object literal
result = {}
for (const i in item) {
result[i] = fastClone(item[i])
}
}
} else {
result = item
}
}
return result
}
/**
* Returns if the specified repository ID is a suit.
*
* @param repoId The repository ID.
* @returns If the repository ID points to a suit.
*/
export function isSuit(repoId: string): boolean {
const suitsToTypeMap: Record<string, string> = {}
getConfig<readonly Unlockable[]>("allunlockables", false)
.filter((unlockable) => unlockable.Type === "disguise")
.forEach((u) => (suitsToTypeMap[u.Properties.RepositoryId] = u.Subtype))
return suitsToTypeMap[repoId]
? suitsToTypeMap[repoId] !== "disguise"
: false
}