mirror of
https://github.com/thepeacockproject/Peacock
synced 2025-02-10 05:24:28 +01:00
Implement contract history and completion tracker (#98)
This commit is contained in:
parent
ed7ca77776
commit
856859f3ca
components
static
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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(),
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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!")
|
||||
|
@ -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": {},
|
||||
|
@ -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": {}
|
||||
|
Loading…
Reference in New Issue
Block a user