1
mirror of https://github.com/thepeacockproject/Peacock synced 2025-02-10 05:24:28 +01:00

Implement "Contract search", "Trending", and "Most played last week" tiles for contracts menu ()

* Refactor: use function for lookupContractPublicId

* Trending and Mostplayed now display stuff

* Add call to contract-preserving backend

* Actually download contract data from official

* add getRemoteService function

* implement "contract search" tile

* Change variable naming

* Run prettier

* Change naming to hitmaps

* officialSearchContract -> contractsModeRouting.ts

* Fix imports
This commit is contained in:
moonysolari 2023-03-06 13:17:22 -05:00 committed by GitHub
parent 5f5ce0abe0
commit 4a08faeec0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 2337 additions and 128 deletions

View File

@ -93,6 +93,7 @@ import FrankensteinPlanningTemplate from "../static/FrankensteinPlanningTemplate
import Videos from "../static/Videos.json"
import ChallengeLocationTemplate from "../static/ChallengeLocationTemplate.json"
import ContractSearchPageTemplate from "../static/ContractSearchPageTemplate.json"
import ContractSearchPaginateTemplate from "../static/ContractSearchPaginateTemplate.json"
import ContractSearchResponseTemplate from "../static/ContractSearchResponseTemplate.json"
import LegacyDebriefingChallengesTemplate from "../static/LegacyDebriefingChallengesTemplate.json"
import DebriefingChallengesTemplate from "../static/DebriefingChallengesTemplate.json"
@ -189,6 +190,7 @@ const configs: Record<string, unknown> = {
Videos,
ChallengeLocationTemplate,
ContractSearchPageTemplate,
ContractSearchPaginateTemplate,
ContractSearchResponseTemplate,
MasteryUnlockablesTemplate,
SniperLoadouts,

View File

@ -16,14 +16,20 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import type { RequestWithJwt } from "../types/types"
import type {
contractSearchResult,
GameVersion,
RequestWithJwt,
} from "../types/types"
import type { Response } from "express"
import { getConfig, getVersionedConfig } from "../configSwizzleManager"
import { getUserData } from "../databaseHandler"
import { generateUserCentric } from "./dataGen"
import { controller } from "../controller"
import { controller, preserveContracts } from "../controller"
import { createLocationsData } from "../menus/destinations"
import { contractCreationTutorialId } from "../utils"
import { userAuths } from "../officialServerAuth"
import { log, LogLevel } from "../loggingInterop"
import { getRemoteService, contractCreationTutorialId } from "../utils"
export function contractsModeHome(req: RequestWithJwt, res: Response): void {
const contractsHomeTemplate = getConfig("ContractsTemplate", false)
@ -57,3 +63,39 @@ export function contractsModeHome(req: RequestWithJwt, res: Response): void {
},
})
}
export async function officialSearchContract(
userId: string,
gameVersion: GameVersion,
filters: string[],
pageNumber: number,
): Promise<contractSearchResult> {
const remoteService = getRemoteService(gameVersion)
const user = userAuths.get(userId)
if (!user) {
log(LogLevel.WARN, `No authentication for user ${userId}!`)
return undefined
}
const resp = await user._useService<{
data: contractSearchResult
}>(
pageNumber === 0
? `https://${remoteService}.hitman.io/profiles/page/ContractSearch?sorting=`
: `https://${remoteService}.hitman.io/profiles/page/ContractSearchPaginate?page=${pageNumber}&sorting=`,
false,
filters,
)
preserveContracts(
resp.data.data.Data.Contracts.map(
(c) => c.UserCentricContract.Contract.Metadata.PublicId,
),
)
controller.storeIdToPublicId(
resp.data.data.Data.Contracts.map((c) => c.UserCentricContract),
)
return resp.data.data
}

View File

@ -22,10 +22,13 @@ import {
contractIdToHitObject,
controller,
featuredContractGroups,
preserveContracts,
} from "../controller"
import { getUserData } from "../databaseHandler"
import { orderedETs } from "./elusiveTargets"
import { fastClone } from "components/utils"
import { userAuths } from "../officialServerAuth"
import { log, LogLevel } from "../loggingInterop"
import { fastClone, getRemoteService } from "../utils"
function paginate<Element>(
elements: Element[],
@ -96,6 +99,7 @@ export class HitsCategoryService {
* Hits categories that should not be automatically paginated.
*/
public paginationExempt = ["Elusive_Target_Hits", "Arcade", "Sniper"]
public realtimeFetched = ["Trending", "MostPlayedLastWeek"]
/**
* The number of hits per page.
@ -168,11 +172,48 @@ export class HitsCategoryService {
hitsCategory.CurrentSubType = "MyPlaylist_all"
})
// intentionally don't handle Trending
// intentionally don't handle MostPlayedLastWeek
// intentionally don't handle Arcade
}
private async fetchFromOfficial(
categoryName: string,
pageNumber: number,
gameVersion: GameVersion,
userId: string,
): Promise<HitsCategoryCategory> {
const remoteService = getRemoteService(gameVersion)
const user = userAuths.get(userId)
if (!user) {
log(LogLevel.WARN, `No authentication for user ${userId}!`)
return undefined
}
const resp = await user._useService<{
data: HitsCategoryCategory
}>(
`https://${remoteService}.hitman.io/profiles/page/HitsCategory?page=${pageNumber}&type=${categoryName}&mode=dataonly`,
true,
)
const hits = resp.data.data.Data.Hits
preserveContracts(
hits.map(
(hit) => hit.UserCentricContract.Contract.Metadata.PublicId,
),
)
// Stores the repo ID —— public ID lookup for the planning page to use.
hits.forEach((hit) =>
controller.contractIdToPublicId.set(
hit.UserCentricContract.Contract.Metadata.Id,
hit.UserCentricContract.Contract.Metadata.PublicId,
),
)
controller.storeIdToPublicId(hits.map((hit) => hit.UserCentricContract))
return resp.data.data
}
/**
* Generate a {@link HitsCategoryCategory} object for the current page.
*
@ -182,12 +223,20 @@ export class HitsCategoryService {
* @param userId The current user's ID.
* @returns The {@link HitsCategoryCategory} object.
*/
public paginateHitsCategory(
public async paginateHitsCategory(
categoryName: string,
pageNumber: number,
gameVersion: GameVersion,
userId: string,
): HitsCategoryCategory {
): Promise<HitsCategoryCategory> {
if (this.realtimeFetched.includes(categoryName)) {
return await this.fetchFromOfficial(
categoryName,
pageNumber,
gameVersion,
userId,
)
}
const hitsCategory: HitsCategoryCategory = {
Category: categoryName,
Data: {

View File

@ -56,7 +56,12 @@ import * as axios from "axios"
import * as ini from "js-ini"
import * as statemachineParser from "@peacockproject/statemachine-parser"
import * as utils from "./utils"
import { addDashesToPublicId, fastClone } from "./utils"
import {
addDashesToPublicId,
fastClone,
getRemoteService,
hitmapsUrl,
} from "./utils"
import * as sessionSerialization from "./sessionSerialization"
import * as databaseHandler from "./databaseHandler"
import * as playnext from "./menus/playnext"
@ -393,6 +398,10 @@ export class Controller {
* Note: if you are adding a contract, please use {@link addMission}!
*/
public contracts: Map<string, MissionManifest> = new Map()
// Converts a contract's ID to public ID.
public contractIdToPublicId: Map<string, string> = new Map()
public challengeService: ChallengeService
public masteryService: MasteryService
/**
@ -880,14 +889,11 @@ export class Controller {
ErrorReason?: string | null
}
const resp = await axios.default.get<Response>(
`https://backend.rdil.rocks/partners/hitmaps/contract`,
{
params: {
publicId: id,
},
const resp = await axios.default.get<Response>(hitmapsUrl, {
params: {
publicId: id,
},
)
})
const fetchedData = resp.data
const hasData = !!fetchedData?.contract?.Contract
@ -989,12 +995,7 @@ export class Controller {
gameVersion: GameVersion,
userId: string,
): Promise<MissionManifest | undefined> {
const remoteService =
gameVersion === "h3"
? "hm3-service"
: gameVersion === "h2"
? "pc2-service"
: "pc-service"
const remoteService = getRemoteService(gameVersion)
const user = userAuths.get(userId)
@ -1136,6 +1137,15 @@ export class Controller {
this._internalContracts = decompressed.b
this._internalElusives = decompressed.el
}
public storeIdToPublicId(contracts: UserCentricContract[]): void {
contracts.forEach((c) =>
controller.contractIdToPublicId.set(
c.Contract.Metadata.Id,
c.Contract.Metadata.PublicId,
),
)
}
}
/**
@ -1265,4 +1275,18 @@ export function contractIdToHitObject(
}
}
/**
* Sends an array of publicIds to the contract preservation backend.
* @param publicIds The contract publicIds to send.
*/
export async function preserveContracts(publicIds: string[]): Promise<void> {
for (const id of publicIds) {
await axios.default.get<Response>(hitmapsUrl, {
params: {
publicId: addDashesToPublicId(id),
},
})
}
}
export const controller = new Controller()

View File

@ -27,6 +27,7 @@ import {
STEAM_NAMESPACE_2016,
} from "./platformEntitlements"
import { GameVersion } from "./types/types"
import { getRemoteService } from "./utils"
/**
* The base class for an entitlement strategy.
@ -66,12 +67,7 @@ export class IOIStrategy extends EntitlementStrategy {
constructor(gameVersion: GameVersion, private readonly issuerId: string) {
super()
this.issuerId = issuerId
this._remoteService =
gameVersion === "h3"
? "hm3-service"
: gameVersion === "h2"
? "pc2-service"
: "pc-service"
this._remoteService = getRemoteService(gameVersion)
}
override async get(userId: string) {

View File

@ -41,6 +41,8 @@ import {
} from "./menus/destinations"
import type {
CommonSelectScreenConfig,
contractSearchResult,
GameVersion,
HitsCategoryCategory,
IHit,
MissionManifest,
@ -62,7 +64,10 @@ import {
generateUserCentric,
} from "./contracts/dataGen"
import { log, LogLevel } from "./loggingInterop"
import { contractsModeHome } from "./contracts/contractsModeRouting"
import {
contractsModeHome,
officialSearchContract,
} from "./contracts/contractsModeRouting"
import random from "random"
import { getUserData } from "./databaseHandler"
import {
@ -1131,79 +1136,80 @@ menuDataRouter.get(
},
)
async function lookupContractPublicId(
publicid: string,
userId: string,
gameVersion: GameVersion,
) {
const publicIdRegex = /\d{12}/
while (publicid.includes("-")) {
publicid = publicid.replace("-", "")
}
if (!publicIdRegex.test(publicid)) {
return {
PublicId: publicid,
ErrorReason: "notfound",
}
}
const contract = await controller.contractByPubId(
publicid,
userId,
gameVersion,
)
if (!contract) {
return {
PublicId: publicid,
ErrorReason: "notfound",
}
}
const location = (
getVersionedConfig(
"allunlockables",
gameVersion,
false,
) as readonly Unlockable[]
).find((entry) => entry.Id === contract.Metadata.Location)
return {
Contract: contract,
Location: location,
UserCentricContract: generateUserCentric(contract, userId, gameVersion),
}
}
menuDataRouter.get(
"/LookupContractPublicId",
async (req: RequestWithJwt<{ publicid: string }>, res) => {
const publicIdRegex = /\d{12}/
let publicid = req.query.publicid
const templateLookupContractById = getVersionedConfig(
"LookupContractByIdTemplate",
req.gameVersion,
false,
)
if (!publicid) {
if (!req.query.publicid) {
return res.status(400).send("no public id specified!")
}
while (publicid.includes("-")) {
publicid = publicid.replace("-", "")
}
if (!publicIdRegex.test(publicid)) {
res.json({
template: templateLookupContractById,
data: {
PublicId: req.query.publicid,
ErrorReason: "notfound",
},
})
return
}
const contract = await controller.contractByPubId(
publicid,
req.jwt.unique_name,
req.gameVersion,
)
if (!contract) {
res.json({
template: templateLookupContractById,
data: {
PublicId: req.query.publicid,
ErrorReason: "notfound",
},
})
return
}
const location = (
getVersionedConfig(
"allunlockables",
res.json({
template: getVersionedConfig(
"LookupContractByIdTemplate",
req.gameVersion,
false,
) as readonly Unlockable[]
).find((entry) => entry.Id === contract.Metadata.Location)
res.json({
template: templateLookupContractById,
data: {
Contract: contract,
Location: location,
UserCentricContract: generateUserCentric(
contract,
req.jwt.unique_name,
req.gameVersion,
),
},
),
data: await lookupContractPublicId(
req.query.publicid,
req.jwt.unique_name,
req.gameVersion,
),
})
},
)
menuDataRouter.get(
"/HitsCategory",
(req: RequestWithJwt<{ type: string; page?: number | string }>, res) => {
async (
req: RequestWithJwt<{ type: string; page?: number | string }>,
res,
) => {
const category = req.query.type
const response: {
@ -1225,7 +1231,7 @@ menuDataRouter.get(
pageNumber = pageNumber < 0 ? 0 : pageNumber
response.data = hitsCategoryService.paginateHitsCategory(
response.data = await hitsCategoryService.paginateHitsCategory(
category,
pageNumber as number,
req.gameVersion,
@ -1555,32 +1561,34 @@ menuDataRouter.post(
specialContracts,
)
const contracts: { UserCentricContract: UserCentricContract }[] = []
let searchResult: contractSearchResult = undefined
for (const contract of specialContracts) {
const userCentric = generateUserCentric(
controller.resolveContract(contract),
req.jwt.unique_name,
req.gameVersion,
)
if (specialContracts.length > 0) {
// Handled by a plugin
if (!userCentric) {
log(LogLevel.ERROR, "UC is null! (contract not registered?)")
continue
const contracts: { UserCentricContract: UserCentricContract }[] = []
for (const contract of specialContracts) {
const userCentric = generateUserCentric(
controller.resolveContract(contract),
req.jwt.unique_name,
req.gameVersion,
)
if (!userCentric) {
log(
LogLevel.ERROR,
"UC is null! (contract not registered?)",
)
continue
}
contracts.push({
UserCentricContract: userCentric,
})
}
contracts.push({
UserCentricContract: userCentric,
})
}
res.json({
template: getVersionedConfig(
"ContractSearchResponseTemplate",
req.gameVersion,
false,
),
data: {
searchResult = {
Data: {
Contracts: contracts,
TotalCount: contracts.length,
@ -1589,7 +1597,40 @@ menuDataRouter.post(
HasPrevious: false,
HasMore: false,
},
},
}
} else {
// No plugins handle this. Getting search results from official
searchResult = await officialSearchContract(
req.jwt.unique_name,
req.gameVersion,
req.body,
0,
)
}
res.json({
template: getVersionedConfig(
"ContractSearchResponseTemplate",
req.gameVersion,
false,
),
data: searchResult,
})
},
)
menuDataRouter.post(
"/ContractSearchPaginate",
jsonMiddleware(),
async (req: RequestWithJwt<{ page: number }, string[]>, res) => {
res.json({
template: getConfig("ContractSearchPaginateTemplate", false),
data: await officialSearchContract(
req.jwt.unique_name,
req.gameVersion,
req.body,
req.query.page,
),
})
},
)

View File

@ -32,7 +32,12 @@ import {
} from "../contracts/dataGen"
import { getConfig } from "../configSwizzleManager"
import { getUserData, writeUserData } from "../databaseHandler"
import { getDefaultSuitFor, nilUuid, unlockOrderComparer } from "../utils"
import {
fastClone,
getDefaultSuitFor,
nilUuid,
unlockOrderComparer,
} from "../utils"
import type { Response } from "express"
import { createInventory } from "../inventory"
@ -81,7 +86,7 @@ export async function planningView(
controller.escalationMappings[escalationGroupId]["1"]
}
const contractData =
let contractData =
req.gameVersion === "h1" &&
req.query.contractid === "42bac555-bbb9-429d-a8ce-f1ffdf94211c"
? _legacyBull
@ -90,7 +95,27 @@ export async function planningView(
: controller.resolveContract(req.query.contractid)
if (!contractData) {
log(LogLevel.ERROR, `Not found: ${req.query.contractid}.`)
log(
LogLevel.WARN,
`Trying to download contract ${req.query.contractid} due to it not found locally.`,
)
const publicId = controller.contractIdToPublicId.get(
req.query.contractid,
)
if (publicId) {
const officialJson = await controller.downloadContract(
req.jwt.unique_name,
publicId,
req.gameVersion,
)
if (officialJson) {
contractData = fastClone(officialJson)
}
}
}
if (!contractData) {
log(LogLevel.ERROR, `Not found: ${req.query.contractid}, .`)
res.status(400).send("no ct")
return
}

View File

@ -35,7 +35,6 @@ import {
} from "./databaseHandler"
import { OfficialServerAuth, userAuths } from "./officialServerAuth"
import { randomUUID } from "crypto"
import { getFlag } from "./flags"
import { clearInventoryFor } from "./inventory"
import {
EpicH1Strategy,
@ -125,12 +124,7 @@ export async function handleOauthToken(
// ts-expect-error Non-optional, we're reassigning.
delete req.jwt.aud // audience
if (
((external_platform === "steam" ||
getFlag("legacyContractDownloader") === true) &&
isHitman3) ||
!isFrankenstein
) {
if (!isFrankenstein) {
if (userAuths.has(req.jwt.unique_name)) {
userAuths
.get(req.jwt.unique_name)!
@ -206,12 +200,7 @@ export async function handleOauthToken(
Always store user auth for H1 & H2
If on steam or using legacy contract downloader, then store user auth for H3
*/
if (
((external_platform === "steam" ||
getFlag("legacyContractDownloader") === true) &&
isHitman3) ||
!isFrankenstein
) {
if (!isFrankenstein) {
const authContainer = new OfficialServerAuth(
gameVersion,
req.body.access_token,

View File

@ -182,6 +182,22 @@ export type MissionType =
| "vsrace"
| "evergreen"
/**
* The data acquired when using the "contract search" functionality.
*/
export interface contractSearchResult {
Data: {
Contracts: {
UserCentricContract: UserCentricContract
}[]
ErrorReason: string
HasMore: boolean
HasPrevious: boolean
Page: number
TotalCount: number
}
}
/**
* The last kill in a contract session.
*

View File

@ -19,6 +19,7 @@
import { decode } from "jsonwebtoken"
import type { NextFunction, Response } from "express"
import type {
GameVersion,
MissionManifestObjective,
RepositoryId,
RequestWithJwt,
@ -82,6 +83,14 @@ export async function checkForUpdates(): Promise<void> {
}
}
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.
*
@ -224,6 +233,8 @@ export function getDefaultSuitFor(location: string) {
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>,

File diff suppressed because it is too large Load Diff