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

Implement contract history and completion tracker ()

This commit is contained in:
moonysolari 2023-03-20 20:12:54 -04:00 committed by GitHub
parent ed7ca77776
commit 856859f3ca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 276 additions and 41 deletions

View File

@ -148,6 +148,7 @@ export function generateUserCentric(
return undefined
}
const userData = getUserData(userId, gameVersion)
const subLocation = getSubLocationFromContract(contractData, gameVersion)
if (!subLocation) {
@ -168,6 +169,9 @@ export function generateUserCentric(
)
}
const played = userData.Extensions?.PeacockPlayedContracts
const id = contractData.Metadata.Id
const uc: UserCentricContract = {
Contract: contractData,
Data: {
@ -180,8 +184,12 @@ export function generateUserCentric(
LocationHideProgression: false,
ElusiveContractState: "",
IsFeatured: false,
//LastPlayedAt: '2020-01-01T00:00:00.0000000Z', // ISO timestamp
Completed: false, // relevant for featured contracts
LastPlayedAt:
played[id] === undefined
? undefined
: new Date(played[id]?.LastPlayedAt).toISOString(),
// relevant for contracts
Completed: played[id] === undefined ? false : played[id]?.Completed,
LocationId: subLocation.Id,
ParentLocationId: subLocation.Properties.ParentLocation!,
CompletionData: generateCompletionData(
@ -195,8 +203,6 @@ export function generateUserCentric(
}
if (contractData.Metadata.Type === "escalation") {
const userData = getUserData(userId, gameVersion)
const eGroupId = contractData.Metadata.InGroup
if (eGroupId) {

View File

@ -17,19 +17,29 @@
*/
import { HookMap, SyncHook } from "../hooksImpl"
import { GameVersion, HitsCategoryCategory } from "../types/types"
import {
ContractHistory,
GameVersion,
HitsCategoryCategory,
} from "../types/types"
import {
contractIdToHitObject,
controller,
featuredContractGroups,
preserveContracts,
} from "../controller"
import { getUserData } from "../databaseHandler"
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,
@ -87,10 +97,10 @@ export class HitsCategoryService {
public hitsCategories: HookMap<
SyncHook<
[
/** gameVersion */ GameVersion,
/** contractIds */ string[],
/** hitsCategory */ HitsCategoryCategory,
/** gameVersion */ GameVersion,
/** userId */ string,
/** filter */ ContractFilter,
]
>
>
@ -118,33 +128,27 @@ export class HitsCategoryService {
_useDefaultHitsCategories(): void {
const tapName = "HitsCategoryServiceImpl"
this.hitsCategories
.for("Sniper")
.tap(tapName, (gameVersion, 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("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, (gameVersion, contracts) => {
.tap(tapName, (contracts) => {
contracts.push(...orderedETs)
})
this.hitsCategories
.for("MyContracts")
.tap(tapName, (gameVersion, contracts, hitsCategory) => {
hitsCategory.CurrentSubType = "MyContracts"
for (const contract of controller.contracts.values()) {
contracts.push(contract.Metadata.Id)
}
.tap(tapName, (contracts, gameVersion, userId, filter) => {
this.writeMyContracts(gameVersion, contracts, userId, filter)
})
this.hitsCategories
.for("Featured")
.tap(tapName, (gameVersion, contracts) => {
.tap(tapName, (contracts, gameVersion) => {
const cagedBull = "ee0411d6-b3e7-4320-b56b-25c45d8a9d61"
const clonedGroups = fastClone(featuredContractGroups)
@ -160,16 +164,24 @@ export class HitsCategoryService {
}
})
// My Favorites
this.hitsCategories
.for("MyPlaylist")
.tap(tapName, (gameVersion, contracts, hitsCategory, userId) => {
const userProfile = getUserData(userId, gameVersion)
const favs =
userProfile?.Extensions.PeacockFavoriteContracts ?? []
.tap(tapName, (contracts, gameVersion, userId, filter) => {
contracts.push(
...this.getMyPlaylist(gameVersion, userId, filter),
)
})
contracts.push(...favs)
// My History
hitsCategory.CurrentSubType = "MyPlaylist_all"
this.hitsCategories
.for("MyHistory")
.tap(tapName, (contracts, gameVersion, userId, filter) => {
contracts.push(
...this.getMyHistory(gameVersion, userId, filter),
)
})
// intentionally don't handle Arcade
@ -211,9 +223,143 @@ export class HitsCategoryService {
)
controller.storeIdToPublicId(hits.map((hit) => hit.UserCentricContract))
// Fix completion status for retrieved contracts
const userProfile = getUserData(userId, gameVersion)
const played = userProfile?.Extensions.PeacockPlayedContracts
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
}
})
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
case "failed":
return (
played[contractId] !== undefined &&
played[contractId].Completed === undefined
)
case "all":
return true
}
}
/**
* Generate a {@link HitsCategoryCategory} object for the current page.
*
@ -237,36 +383,50 @@ export class HitsCategoryService {
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: categoryName,
Category: category,
Data: {
Type: categoryName,
Type: category,
Hits: [],
Page: pageNumber,
HasMore: false,
},
CurrentSubType: categoryName,
CurrentSubType: undefined,
}
const hook = this.hitsCategories.for(categoryName)
const hook = this.hitsCategories.for(category)
const hits: string[] = []
hook.call(gameVersion, hits, hitsCategory, userId)
hook.call(hits, gameVersion, userId, filter)
const hitObjectList = hits
.map((id) => contractIdToHitObject(id, gameVersion, userId))
.filter(Boolean)
if (!this.paginationExempt.includes(categoryName)) {
if (!this.paginationExempt.includes(category)) {
const paginated = paginate(hitObjectList, this.hitsPerPage)
// ts-expect-error Type things.
hitsCategory.Data.Hits = paginated[pageNumber]
hitsCategory.Data.HasMore = paginated.length > pageNumber + 1
hitsCategory.CurrentSubType = `${category}_${filter}`
} else {
// ts-expect-error Type things.
hitsCategory.Data.Hits = hitObjectList
hitsCategory.CurrentSubType = category
}
return hitsCategory

View File

@ -441,6 +441,23 @@ function contractFailed(
liveSplitManager.failMission(0)
}
const contractData = controller.resolveContract(session.contractId)
// If this is a contract, update the contract in the played list
if (contractTypes.includes(contractData.Metadata.Type)) {
const userData = getUserData(session.userId, session.gameVersion)
const id = session.contractId
if (!userData.Extensions.PeacockPlayedContracts[id]) {
userData.Extensions.PeacockPlayedContracts[id] = {}
}
userData.Extensions.PeacockPlayedContracts[id].LastPlayedAt =
new Date().getTime()
writeUserData(session.userId, session.gameVersion)
}
enqueueEvent(session.userId, {
CreatedAt: new Date().toISOString(),
Token: process.hrtime.bigint().toString(),

View File

@ -20,6 +20,7 @@ import type { Response } from "express"
import {
clampValue,
DEFAULT_MASTERY_MAXLEVEL,
contractTypes,
difficultyToString,
evergreenLevelForXp,
EVERGREEN_LEVEL_INFO,
@ -571,6 +572,18 @@ export async function missionEnd(
userData.Extensions.PeacockEscalations[eGroupId] += 1
}
writeUserData(req.jwt.unique_name, req.gameVersion)
} else if (contractTypes.includes(contractData.Metadata.Type)) {
// Update the contract in the played list
const id = contractData.Metadata.Id
if (!userData.Extensions.PeacockPlayedContracts[id]) {
userData.Extensions.PeacockPlayedContracts[id] = {}
}
userData.Extensions.PeacockPlayedContracts[id].LastPlayedAt =
new Date().getTime()
userData.Extensions.PeacockPlayedContracts[id].Completed = true
writeUserData(req.jwt.unique_name, req.gameVersion)
}

View File

@ -396,6 +396,11 @@ export interface PlayerProfileView {
}
}
export interface ContractHistory {
LastPlayedAt?: number
Completed?: boolean
}
export interface UserProfile {
Id: string
LinkedAccounts: {
@ -414,6 +419,9 @@ export interface UserProfile {
[escalationId: string]: number
}
PeacockFavoriteContracts: string[]
PeacockPlayedContracts: {
[contractId: string]: ContractHistory
}
PeacockCompletedEscalations: string[]
Saves: {
[slot: string]: {
@ -464,6 +472,12 @@ export interface UserProfile {
gamepersistentdata: {
__stats?: unknown
PersistentBool: Record<string, unknown>
HitsFilterType: {
// "all" / "completed" / "failed"
MyHistory: string
MyContracts: string
MyPlaylist: string
}
}
opportunityprogression: {
[opportunityId: RepositoryId]: boolean

View File

@ -50,7 +50,7 @@ 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", "creation"]
export const contractTypes = ["featured", "usercreated"]
export const contractCreationTutorialId = "d7e2607c-6916-48e2-9588-976c7d8998bb"
@ -209,6 +209,7 @@ export function castUserProfile(profile: UserProfile): UserProfile {
for (const item of [
"PeacockEscalations",
"PeacockFavoriteContracts",
"PeacockPlayedContracts",
"PeacockCompletedEscalations",
"CPD",
]) {
@ -239,10 +240,29 @@ export function castUserProfile(profile: UserProfile): UserProfile {
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 (dirty) {
writeFileSync(`userdata/users/${j.Id}.json`, JSON.stringify(j))
log(LogLevel.INFO, "Profile successfully repaired!")

View File

@ -5,6 +5,7 @@
"PeacockEscalations": {},
"PeacockCompletedEscalations": [],
"PeacockFavoriteContracts": [],
"PeacockPlayedContracts": {},
"Saves": {},
"gameclient": {
"LastConnectionVersion": {
@ -189,7 +190,9 @@
"EpilogueSeen_0e81a82e-b409-41e9-9e3b-5f82e57f7a12": true
},
"HitsFilterType": {
"MyContracts": "MyContracts"
"MyHistory": "all",
"MyContracts": "all",
"MyPlaylist": "all"
}
},
"opportunityprogression": {},

View File

@ -4,6 +4,7 @@
"Extensions": {
"PeacockEscalations": {},
"PeacockFavoriteContracts": [],
"PeacockPlayedContracts": {},
"PeacockCompletedEscalations": [],
"Saves": {},
"gameclient": null,
@ -396,8 +397,9 @@
"__Full": {}
},
"HitsFilterType": {
"MyHistory": "MyHistory",
"MyContracts": "MyContracts"
"MyHistory": "all",
"MyContracts": "all",
"MyPlaylist": "all"
},
"VideoShown": {},
"EpilogueSeen": {}