1
mirror of https://github.com/thepeacockproject/Peacock synced 2025-02-23 03:35:25 +01:00
Peacock/components/contracts/contractRouting.ts
Anthony Fuller 46052c7b0e
Multi-Version Mastery and Sniper Scoring (#270)
* Add multi-version mastery files

* Add pro1 unlocks to legacy allunlockables

* Add 47's suit to scpc all unlockables

* Add and remove various configs

* Remove some useless promises

* Fix scpc hub

* Fix issue with user profile saving

* Fix scpc issues for hub

* Add singleplayer/multiplayer sniper

* A great many things

- Add multi-version mastery
- Improve sniper mastery support
- Improve general H2016 support

* Fix some warnings

* Fix pro1 mastery on destination screens

* Remove entP from createInventory, lock/unlock pro1 accordingly

* Remove JSDoc entP parameter from createInventory

* Remove difficultyunlocks from safehouse pages

* Add versioned user profiles

* Prettier run

* Remove false point from user profiles docs

* Add comment about profile versioning to types

* Fix default profile links

* Remove remaining lowercase

* Fix sniper showing XP as XP

* Add game versions to the unlockable map

* Update getMasteryForUnlockable call in planning

* Fix missing locations when updating profiles

* Update versions to v7

* Fix ICA Facility destination mastery

* Fix sniper challenge unlockables showing in inventory

* Sniper Scoring (#273)

* Initial sniper scoring

* Fix linting errors

* Update require table

* Calculate and display final sniper score on end screen

* Bump SMP version to v5.7.0

* Update since version for scoring

* Fix create inventory call for sniper scoring

* Support sniper unlockables in the inventory

* Update versions to v7

* Reflect changes to createInventory in scoreHandler

* Get unlockable name in completion data

* It was not okay.

* Thanks webstorm

* Add support for /profiles/page/GetMasteryCompletionDataForUnlockable

* Support sniper play next

* Remove sniper gamemodes template from overrides

* Remove debug prints from scoring event handler

* Fix challenge multiplier

* Exclude sniper unlockables from stashpoint

* Start fixing up the missionEnd response for sniper

* Update misleading comment

* Use existing global challenge to check for SA on sniper contracts

* Re-add removed global challenges

* Proper support for the mission end screen on sniper contracts

* Remove redundant label

---------

Signed-off-by: Anthony Fuller <24512050+AnthonyFuller@users.noreply.github.com>
Co-authored-by: Govert de Gans <grappigegovert@hotmail.com>

* Add co-op sniper scoring defs

* Update MasteryUnlockable template

* Bump SMP version to v5.9.3

* Re-add deepmerge

* Fix SMP checksum

* Fix linting errors caused by merge

* Fix score handler imports

* Move load flags

* Remove unnecessary game version arg

* Whoopsies

Co-authored-by: Reece Dunham <me@rdil.rocks>
Signed-off-by: Anthony Fuller <24512050+AnthonyFuller@users.noreply.github.com>

---------

Signed-off-by: Anthony Fuller <24512050+AnthonyFuller@users.noreply.github.com>
Co-authored-by: Govert de Gans <grappigegovert@hotmail.com>
Co-authored-by: Reece Dunham <me@rdil.rocks>
2023-07-24 23:47:28 +01:00

388 lines
13 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 { Router } from "express"
import {
contractTypes,
fastClone,
nilUuid,
ServerVer,
uuidRegex,
} from "../utils"
import { json as jsonMiddleware } from "body-parser"
import {
enqueueEvent,
getSession,
newSession,
registerObjectiveListener,
setupScoring,
} from "../eventHandler"
import { controller } from "../controller"
import { getConfig } from "../configSwizzleManager"
import type {
CreateFromParamsBody,
GameChanger,
MissionManifest,
MissionManifestObjective,
MissionStory,
RequestWithJwt,
} from "../types/types"
import {
escalationTypes,
getPlayEscalationInfo,
} from "./escalations/escalationService"
import { log, LogLevel } from "../loggingInterop"
import { randomUUID } from "crypto"
import {
createTimeLimit,
TargetCreator,
} from "../statemachines/contractCreation"
import { createSniperLoadouts } from "../menus/sniper"
import { GetForPlay2Body } from "../types/gameSchemas"
import assert from "assert"
import { getUserData } from "../databaseHandler"
import { getCpd } from "../evergreen"
const contractRoutingRouter = Router()
contractRoutingRouter.post(
"/GetForPlay2",
jsonMiddleware(),
async (req: RequestWithJwt<never, GetForPlay2Body>, res) => {
if (!req.body.id || !uuidRegex.test(req.body.id)) {
res.status(400).end()
return // user sent some nasty info
}
const contractData = controller.resolveContract(req.body.id)
if (!contractData) {
log(
LogLevel.ERROR,
`Requested unknown contract in GetForPlay2: ${req.body.id}`,
)
res.status(404).end()
return
}
const sniperloadouts = createSniperLoadouts(
req.jwt.unique_name,
req.gameVersion,
contractData,
)
const loadoutData = {
CharacterLoadoutData:
sniperloadouts.length !== 0 ? sniperloadouts : null,
}
// Add escalation data to Contract data HERE
contractData.Metadata = {
...contractData.Metadata,
...(escalationTypes.includes(contractData.Metadata.Type)
? getPlayEscalationInfo(
req.jwt.unique_name,
contractData.Metadata.InGroup,
req.gameVersion,
)
: {}),
...loadoutData,
...{
OpportunityData: getContractOpportunityData(req, contractData),
},
}
// Edit usercreated contract data HERE
if (contractTypes.includes(contractData.Metadata.Type)) {
contractData.Data.EnableSaving = false
}
// Edit elusive contract data HERE
const contractSesh = {
Contract: contractData,
ContractSessionId: `${process.hrtime
.bigint()
.toString()}-${randomUUID()}`,
ContractProgressionData: contractData.Metadata
.UseContractProgressionData
? await getCpd(req.jwt.unique_name, contractData.Metadata.CpdId)
: null,
}
if (
contractData.Data.GameChangers &&
contractData.Data.GameChangers.length > 0
) {
type GCPConfig = Record<string, GameChanger>
const gameChangerData: GCPConfig = {
...getConfig<GCPConfig>("GameChangerProperties", true),
...getConfig<GCPConfig>("PeacockGameChangerProperties", true),
...getConfig<GCPConfig>("EvergreenGameChangerProperties", true),
}
contractData.Data.GameChangerReferences =
contractData.Data.GameChangerReferences || []
for (const gameChangerId of contractData.Data.GameChangers) {
if (
!Object.prototype.hasOwnProperty.call(
gameChangerData,
gameChangerId,
)
) {
log(
LogLevel.ERROR,
`GetForPlay has detected a missing GameChanger: ${gameChangerId}! This is a bug.`,
)
}
const gameChanger = gameChangerData[gameChangerId]
gameChanger.Id = gameChangerId
delete gameChanger.ObjectivesCategory
if (
contractData.Data.GameChangerReferences.filter(
(value) => value.Id === gameChangerId,
).length !== 0
) {
continue
}
contractData.Data.GameChangerReferences.push(gameChanger)
contractData.Data.Bricks = [
...(contractData.Data.Bricks ?? []),
...(gameChanger.Resource ?? []),
]
contractData.Data.Objectives = [
...(contractData.Data.Objectives ?? []),
...(gameChanger.Objectives.map((val) => {
if (contractData.Metadata.Type !== "evergreen")
return val
return {
...val,
GameChangerName: gameChanger.Name,
IsPrestigeObjective:
gameChanger.IsPrestigeObjective ?? false,
}
}) ?? []),
]
}
}
enqueueEvent(req.jwt.unique_name, {
Version: ServerVer,
IsReplicated: false,
CreatedContract: null,
Id: randomUUID(),
Name: "ContractSessionMarker",
UserId: nilUuid,
ContractId: nilUuid,
SessionId: null,
ContractSessionId: contractSesh.ContractSessionId,
Timestamp: 0.0,
Value: {
Currency: {
ContractPaymentAllowed: true,
ContractPayment: null,
},
},
Origin: null,
})
res.json(contractSesh)
newSession(
contractSesh.ContractSessionId,
contractSesh.Contract.Metadata.Id,
req.jwt.unique_name,
req.body.difficultyLevel!,
req.gameVersion,
)
const theSession = getSession(req.jwt.unique_name)
assert.ok(theSession, "Session should exist")
for (const obj of contractData.Data.Objectives || []) {
// register the objective as a tracked statemachine
registerObjectiveListener(theSession, obj)
}
if (contractData.Metadata.Modules) {
setupScoring(theSession, contractData.Metadata.Modules)
}
},
)
contractRoutingRouter.post(
"/CreateFromParams",
jsonMiddleware(),
async (
req: RequestWithJwt<Record<never, never>, CreateFromParamsBody>,
res,
) => {
const gameChangerData = getConfig<Record<string, GameChanger>>(
"GameChangerProperties",
true,
)
const objectives: MissionManifestObjective[] = []
const gamechangers: string[] = []
const sessionDetails = getSession(req.jwt.unique_name)
if (!sessionDetails) {
res.status(400).end()
log(
LogLevel.WARN,
`CreateFromParams called without a valid session`,
)
return
}
// I'm using Math.ceil here to round the time to the nearest next full second
// IOI servers don't do this, but that means that the displayed time in the objective
// is not accurate with the actual time limit.
// If you change this, also change it in menuData.ts
const timeLimit = Math.ceil(
(sessionDetails.timerEnd as number) -
(sessionDetails.timerStart as number),
)
const contractData = controller.resolveContract(
sessionDetails.contractId,
)
if (!contractData) {
res.status(400).end()
log(
LogLevel.ERROR,
`No such contract creation contract: ${sessionDetails.contractId}`,
)
return
}
req.body.creationData.Targets.forEach((target) => {
if (!target.Selected) {
return
}
objectives.push(...new TargetCreator(target).build())
})
req.body.creationData.ContractConditionIds.forEach(
(contractConditionId) => {
if (
Object.prototype.hasOwnProperty.call(
gameChangerData,
contractConditionId,
)
) {
gamechangers.push(contractConditionId)
} else if (
contractConditionId ===
"1a596216-381e-4592-9798-26f156973942"
) {
// Optional time limit
objectives.push(createTimeLimit(timeLimit, true))
} else if (
contractConditionId ===
"3d6f9119-7ec8-496f-ab4c-ed9757d976a4"
) {
// Mandatory time limit
objectives.push(createTimeLimit(timeLimit, false))
}
},
)
const theVersion = `${ServerVer._Major}.${ServerVer._Minor}.${ServerVer._Build}.${ServerVer._Revision}`
const manifest: MissionManifest = {
Data: {
Objectives: objectives,
GameChangers: gamechangers,
Bricks: [],
},
Metadata: {
Title: req.body.creationData.Title,
Description: req.body.creationData.Description,
Entitlements: contractData.Metadata.Entitlements,
ScenePath: contractData.Metadata.ScenePath,
Location: contractData.Metadata.Location,
IsPublished: true,
CreatorUserId: "fadb923c-e6bb-4283-a537-eb4d1150262e",
GameVersion: theVersion,
ServerVersion: theVersion,
Type: "usercreated",
Id: req.body.creationData.ContractId,
PublicId: req.body.creationData.ContractPublicId,
TileImage: `$($repository ${req.body.creationData.Targets[0]?.RepositoryId}).Image`,
GroupObjectiveDisplayOrder: req.body.creationData.Targets.map(
(t) => ({
Id: t.RepositoryId,
}),
),
CreationTimestamp: new Date().toISOString(),
},
UserData: {},
}
await controller.commitNewContract(manifest)
res.json(manifest)
},
)
contractRoutingRouter.post(
"/GetContractOpportunities",
jsonMiddleware(),
(req: RequestWithJwt<never, { contractId: string }>, res) => {
const contract = controller.resolveContract(req.body.contractId)
res.json(getContractOpportunityData(req, contract))
},
)
function getContractOpportunityData(
req: RequestWithJwt,
contract: MissionManifest,
): MissionStory[] {
const userData = getUserData(req.jwt.unique_name, req.gameVersion)
const result = []
const missionStories = getConfig<Record<string, MissionStory>>(
"MissionStories",
false,
)
if (contract.Metadata.Opportunities) {
for (const ms of contract.Metadata.Opportunities) {
if (!Object.keys(missionStories).includes(ms)) {
continue
}
missionStories[ms].PreviouslyCompleted =
ms in userData.Extensions.opportunityprogression
const current = fastClone(missionStories[ms])
delete current.Location
result.push(current)
}
}
return result
}
export { contractRoutingRouter }