1
mirror of https://github.com/thepeacockproject/Peacock synced 2024-11-29 09:15:11 +01:00
Peacock/components/contracts/hitsCategoryService.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

460 lines
15 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 { HookMap, SyncHook } from "../hooksImpl"
import {
ContractHistory,
GameVersion,
HitsCategoryCategory,
} from "../types/types"
import {
contractIdToHitObject,
controller,
featuredContractGroups,
preserveContracts,
} from "../controller"
import { getUserData, writeUserData } from "../databaseHandler"
import { orderedETs } from "./elusiveTargets"
import { userAuths } from "../officialServerAuth"
import { log, LogLevel } from "../loggingInterop"
import { fastClone, getRemoteService } from "../utils"
/**
* The filters supported for HitsCategories.
* Supported for "MyPlaylist" "MyHistory" and "MyContracts".
*/
type ContractFilter = "default" | "all" | "completed" | "failed" | string
function paginate<Element>(
elements: Element[],
displayPerPage: number,
): Element[][] {
const totalElementCount: number = elements.length
const pageCount = Math.ceil(totalElementCount / displayPerPage)
const pages: Element[][] = []
let perPageArray: Element[] = []
let index = 0
let condition = 0
let pendingDispatchCount = 0
for (let i = 0; i < pageCount; i++) {
if (i === 0) {
index = 0
condition = displayPerPage
}
for (let j = index; j < condition; j++) {
if (!elements[j]) {
break
}
perPageArray.push(elements[j])
}
pages.push(perPageArray)
if (i === 0) {
pendingDispatchCount = totalElementCount - perPageArray.length
} else {
pendingDispatchCount = pendingDispatchCount - perPageArray.length
}
if (pendingDispatchCount > 0) {
if (pendingDispatchCount > displayPerPage) {
index = index + displayPerPage
condition = condition + displayPerPage
} else {
index = index + perPageArray.length
condition = condition + pendingDispatchCount
}
}
perPageArray = []
}
return pages
}
export class HitsCategoryService {
/**
* A hook map for all the hits categories.
*/
public hitsCategories: HookMap<
SyncHook<
[
/** contractIds */ string[],
/** gameVersion */ GameVersion,
/** userId */ string,
/** filter */ ContractFilter,
]
>
>
/**
* 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.
*/
public hitsPerPage = 22
constructor() {
this.hitsCategories = new HookMap(() => new SyncHook())
this._useDefaultHitsCategories()
}
/**
* Enable the default hits categories.
*/
_useDefaultHitsCategories(): void {
const tapName = "HitsCategoryServiceImpl"
this.hitsCategories.for("Sniper").tap(tapName, (contracts) => {
contracts.push("ff9f46cf-00bd-4c12-b887-eac491c3a96d")
contracts.push("00e57709-e049-44c9-a2c3-7655e19884fb")
contracts.push("25b20d86-bb5a-4ebd-b6bb-81ed2779c180")
})
this.hitsCategories
.for("Elusive_Target_Hits")
.tap(tapName, (contracts, gameVersion) => {
for (const id of orderedETs) {
const contract = controller.resolveContract(id)
switch (gameVersion) {
case "h1":
if (contract.Metadata.Season === 1)
contracts.push(id)
break
case "h2":
if (contract.Metadata.Season <= 2)
contracts.push(id)
break
default:
contracts.push(id)
}
}
})
this.hitsCategories
.for("MyContracts")
.tap(tapName, (contracts, gameVersion, userId, filter) => {
this.writeMyContracts(gameVersion, contracts, userId, filter)
})
this.hitsCategories
.for("Featured")
.tap(tapName, (contracts, gameVersion) => {
const cagedBull = "ee0411d6-b3e7-4320-b56b-25c45d8a9d61"
const clonedGroups = fastClone(featuredContractGroups)
for (const fcGroup of clonedGroups) {
if (gameVersion === "h1" && fcGroup.includes(cagedBull)) {
fcGroup.splice(
fcGroup.findIndex((id) => id === cagedBull),
1,
)
}
contracts.push(...fcGroup)
}
})
// My Favorites
this.hitsCategories
.for("MyPlaylist")
.tap(tapName, (contracts, gameVersion, userId, filter) => {
contracts.push(
...this.getMyPlaylist(gameVersion, userId, filter),
)
})
// My History
this.hitsCategories
.for("MyHistory")
.tap(tapName, (contracts, gameVersion, userId, filter) => {
contracts.push(
...this.getMyHistory(gameVersion, userId, filter),
)
})
// 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))
// Fix completion and favorite status for retrieved contracts
const userProfile = getUserData(userId, gameVersion)
const played = userProfile?.Extensions.PeacockPlayedContracts
const favorites = userProfile?.Extensions.PeacockFavoriteContracts
hits.forEach((hit) => {
if (Object.keys(played).includes(hit.Id)) {
// Replace with data stored by Peacock
hit.UserCentricContract.Data.LastPlayedAt = new Date(
played[hit.Id].LastPlayedAt,
).toISOString()
hit.UserCentricContract.Data.Completed =
played[hit.Id].Completed
} else {
// Never played on Peacock
delete hit.UserCentricContract.Data.LastPlayedAt
hit.UserCentricContract.Data.Completed = false
}
hit.UserCentricContract.Data.PlaylistData.IsAdded =
favorites.includes(hit.Id)
})
return resp.data.data
}
/**
* Writes the contracts array with the repoId of all contracts in the contracts folder that meet the player completion requirement specified by the type.
* @param gameVersion The gameVersion the player is playing on.
* @param contracts The array to write into.
* @param userId The Id of the user.
* @param type A filter for the contracts to fetch.
*/
private writeMyContracts(
gameVersion: GameVersion,
contracts: string[],
userId: string,
type: ContractFilter,
): void {
const userProfile = getUserData(userId, gameVersion)
const played = userProfile?.Extensions.PeacockPlayedContracts
for (const contract of controller.contracts.values()) {
if (this.isContractOfType(played, type, contract.Metadata.Id)) {
contracts.push(contract.Metadata.Id)
}
}
}
/**
* Gets the contracts array with the repoId of all contracts of the specified type that is in the player's favorite list.
* @param gameVersion The gameVersion the player is playing on.
* @param userId The Id of the user.
* @param type A filter for the contracts to fetch.
* @returns The resulting array.
*/
private getMyPlaylist(
gameVersion: GameVersion,
userId: string,
type: ContractFilter,
): string[] {
const userProfile = getUserData(userId, gameVersion)
const played = userProfile?.Extensions.PeacockPlayedContracts
const favs = userProfile?.Extensions.PeacockFavoriteContracts ?? []
return favs.filter((id) => this.isContractOfType(played, type, id))
}
/**
* This function will get or set the default filter of a category for a user, depending on the "type" passed.
* If the type is "default", then it will get the default filter and return it.
* Otherwise, it will set the default filter to the type passed, and return the type itself.
* @param gameVersion The GameVersion that the user is playing on.
* @param userId The ID of the user.
* @param type The type of the filter.
* @param category The category in question.
* @returns The filter to use for this request.
*/
private getOrSetDefaultFilter(
gameVersion: GameVersion,
userId: string,
type: ContractFilter,
category: string,
): string {
const user = getUserData(userId, gameVersion)
if (type === "default") {
type = user.Extensions.gamepersistentdata.HitsFilterType[category]
} else {
user.Extensions.gamepersistentdata.HitsFilterType[category] = type
writeUserData(userId, gameVersion)
}
return type
}
/**
* Gets the contracts array with the repoId of all contracts of the specified type that the player has played before, sorted by LastPlayedTime.
* @param gameVersion The gameVersion the player is playing on.
* @param userId The Id of the user.
* @param type A filter for the contracts to fetch.
* @returns The resulting array.
*/
private getMyHistory(
gameVersion: GameVersion,
userId: string,
type: ContractFilter,
): string[] {
const userProfile = getUserData(userId, gameVersion)
const played = userProfile?.Extensions.PeacockPlayedContracts
return Object.keys(played)
.filter((id) => this.isContractOfType(played, type, id))
.sort((a, b) => {
return played[b].LastPlayedAt - played[a].LastPlayedAt
})
}
/**
* For a user, returns whether a contract is of the given type of completion.
* @param played The user's played contracts.
* @param type The type of completion in question.
* @param contractId The id of the contract.
* @returns A boolean, denoting the result.
*/
private isContractOfType(
played: {
[contractId: string]: ContractHistory
},
type: ContractFilter,
contractId: string,
): boolean {
switch (type) {
case "completed":
return (
played[contractId]?.Completed &&
!played[contractId]?.IsEscalation
)
case "failed":
return (
played[contractId] !== undefined &&
played[contractId].Completed === undefined &&
!played[contractId]?.IsEscalation
)
case "all":
return !played[contractId]?.IsEscalation ?? true
}
}
/**
* Generate a {@link HitsCategoryCategory} object for the current page.
*
* @param categoryName The hits category's ID (the key for the hooks map).
* @param pageNumber The current page's number.
* @param gameVersion The game version being used for the request.
* @param userId The current user's ID.
* @returns The {@link HitsCategoryCategory} object.
*/
public async paginateHitsCategory(
categoryName: string,
pageNumber: number,
gameVersion: GameVersion,
userId: string,
): Promise<HitsCategoryCategory> {
if (this.realtimeFetched.includes(categoryName)) {
return await this.fetchFromOfficial(
categoryName,
pageNumber,
gameVersion,
userId,
)
}
const categoryTypes = categoryName.split("_")
const category =
categoryName === "Elusive_Target_Hits"
? categoryName
: categoryTypes[0]
let filter = categoryTypes.length === 2 ? categoryTypes[1] : "default"
filter = this.getOrSetDefaultFilter(
gameVersion,
userId,
filter,
category,
)
const hitsCategory: HitsCategoryCategory = {
Category: category,
Data: {
Type: category,
Hits: [],
Page: pageNumber,
HasMore: false,
},
CurrentSubType: undefined,
}
const hook = this.hitsCategories.for(category)
const hits: string[] = []
hook.call(hits, gameVersion, userId, filter)
const hitObjectList = hits
.map((id) => contractIdToHitObject(id, gameVersion, userId))
.filter(Boolean)
if (!this.paginationExempt.includes(category)) {
const paginated = paginate(hitObjectList, this.hitsPerPage)
hitsCategory.Data.Hits = paginated[pageNumber]
hitsCategory.Data.HasMore = paginated.length > pageNumber + 1
hitsCategory.CurrentSubType = `${category}_${filter}`
} else {
hitsCategory.Data.Hits = hitObjectList
hitsCategory.CurrentSubType = category
}
return hitsCategory
}
}
export const hitsCategoryService = new HitsCategoryService()