mirror of
https://github.com/thepeacockproject/Peacock
synced 2024-11-29 09:15:11 +01:00
4575924e80
* 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>
460 lines
15 KiB
TypeScript
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()
|