1
mirror of https://github.com/thepeacockproject/Peacock synced 2025-02-16 16:34:28 +01:00
Peacock/components/inventory.ts

519 lines
17 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 { getVersionedConfig } from "./configSwizzleManager"
import type { GameVersion, Unlockable, UserProfile } from "./types/types"
import {
brokenItems,
CONCRETEART_UNLOCKABLES,
DELUXE_UNLOCKABLES,
EXECUTIVE_UNLOCKABLES,
H1_GOTY_UNLOCKABLES,
H1_REQUIEM_UNLOCKABLES,
H2_RACCOON_STINGRAY_UNLOCKABLES,
MAKESHIFT_UNLOCKABLES,
SIN_ENVY_UNLOCKABLES,
SIN_GLUTTONY_UNLOCKABLES,
SIN_GREED_UNLOCKABLES,
SIN_LUST_UNLOCKABLES,
SIN_PRIDE_UNLOCKABLES,
SIN_SLOTH_UNLOCKABLES,
SIN_WRATH_UNLOCKABLES,
TRINITY_UNLOCKABLES,
WINTERSPORTS_UNLOCKABLES,
} from "./ownership"
import { EPIC_NAMESPACE_2016 } from "./platformEntitlements"
import { controller } from "./controller"
import { getUserData } from "./databaseHandler"
import { log, LogLevel } from "./loggingInterop"
import { getFlag } from "./flags"
import { UnlockableMasteryData } from "./types/mastery"
const DELUXE_DATA = [
...CONCRETEART_UNLOCKABLES,
...DELUXE_UNLOCKABLES,
...EXECUTIVE_UNLOCKABLES,
...H1_GOTY_UNLOCKABLES,
...H1_REQUIEM_UNLOCKABLES,
...H2_RACCOON_STINGRAY_UNLOCKABLES,
...MAKESHIFT_UNLOCKABLES,
...SIN_ENVY_UNLOCKABLES,
...SIN_GLUTTONY_UNLOCKABLES,
...SIN_GREED_UNLOCKABLES,
...SIN_LUST_UNLOCKABLES,
...SIN_PRIDE_UNLOCKABLES,
...SIN_SLOTH_UNLOCKABLES,
...SIN_WRATH_UNLOCKABLES,
...TRINITY_UNLOCKABLES,
...WINTERSPORTS_UNLOCKABLES,
]
/**
* An inventory item.
*/
export interface InventoryItem {
InstanceId: string
ProfileId: string
Unlockable: Unlockable
Properties: Record<string, string>
}
const inventoryUserCache: Map<string, InventoryItem[]> = new Map()
/**
* Clears a user's inventory.
*
* @param userId The user's ID.
*/
export function clearInventoryFor(userId: string): void {
inventoryUserCache.delete(userId)
}
/**
* Clears the entire inventory cache.
*/
export function clearInventoryCache(): void {
inventoryUserCache.clear()
}
/**
* Filters unlocked unlockables
*
* @param userProfile
* @param packagedUnlocks
* @param challengesUnlockables
* @returns [Unlockable[], Unlockable[]]
*/
function filterUnlockedContent(
userProfile: UserProfile,
packagedUnlocks: Map<string, boolean>,
challengesUnlockables: object,
) {
return function (
acc: [Unlockable[], Unlockable[]],
unlockable: Unlockable,
) {
let unlockableChallengeId: string
let unlockableMasteryData: UnlockableMasteryData
// Handles unlockables that belong to a package or unlocked gear from evergreen
if (packagedUnlocks.has(unlockable.Id)) {
packagedUnlocks.get(unlockable.Id) && acc[0].push(unlockable)
}
// Handles packages
else if (unlockable.Type === "package") {
for (const pkgUnlockableId of unlockable.Properties.Unlocks) {
packagedUnlocks.set(pkgUnlockableId, true)
}
acc[0].push(unlockable)
}
// If the unlockable is challenge reward, check if user has the challenge completed
else if (
(unlockableChallengeId = challengesUnlockables[unlockable.Id])
) {
const challenge =
userProfile.Extensions?.ChallengeProgression?.[
unlockableChallengeId
]
if (challenge?.Completed) acc[0].push(unlockable)
}
// If the unlockable is mastery locked, checks if its unlocked based on user location progression
else if (
(unlockableMasteryData =
controller.masteryService.getMasteryForUnlockable(unlockable))
) {
const locationData =
controller.progressionService.getMasteryProgressionForLocation(
userProfile,
unlockableMasteryData.Location,
)
const canUnlock = locationData.Level >= unlockableMasteryData.Level
if (canUnlock && unlockable.Type !== "evergreenmastery") {
acc[0].push(unlockable)
}
// If the unlock is an evergreen package, adds its unlockables to the list
if (
unlockable.Type === "evergreenmastery" &&
unlockable.Properties.Unlocks
)
for (const evergreenGearId of unlockable.Properties.Unlocks) {
packagedUnlocks.set(evergreenGearId, canUnlock)
}
} else {
const isEvergreen =
unlockable.Type === "evergreenmastery" ||
unlockable.Subtype === "evergreen"
const isDeluxe = DELUXE_DATA.includes(unlockable.Id)
if (isEvergreen || isDeluxe) {
acc[0].push(unlockable)
} else {
/**
* List of untracked items (to award to user until they are tracked to corresponding challenges)
*/
acc[1].push(unlockable)
}
}
return acc
}
}
/**
* Filters allowed unlockables
*
* @param gameVersion
* @param entP
* @returns boolean
*/
function filterAllowedContent(gameVersion: GameVersion, entP: string[]) {
return function (unlockContainer: {
InstanceId: string
ProfileId: string
Unlockable: Unlockable
Properties: object
}) {
if (!unlockContainer) {
return false
}
if (
unlockContainer.Unlockable.Type === "disguise" &&
!unlockContainer.Unlockable.Properties.OrderIndex
) {
return false
}
if (gameVersion === "h1") {
return true
}
const e = entP
const { Id: id } = unlockContainer!.Unlockable
if (!e) {
return false
}
if (unlockContainer.Unlockable.Type === "evergreenmastery") {
return false
}
// This way of doing entitlements is a mess, redo this! - AF
if (gameVersion === "h3") {
if (WINTERSPORTS_UNLOCKABLES.includes(id)) {
return (
e.includes("afa4b921503f43339c360d4b53910791") ||
e.includes("84a1a6fda4fb48afbb78ee9b2addd475") || // WoA Deluxe
e.includes("1829590")
)
}
if (EXECUTIVE_UNLOCKABLES.includes(id)) {
return (
e.includes("6408de14f7dc46b9a33adcf6cbc4d159") ||
e.includes("afa4b921503f43339c360d4b53910791") ||
e.includes("84a1a6fda4fb48afbb78ee9b2addd475") || // WoA Deluxe
e.includes("1829590")
)
}
if (H1_REQUIEM_UNLOCKABLES.includes(id)) {
return (
e.includes("e698e1a4b63947b0bc9349a5ae2dc015") ||
e.includes("a3509775467d4d6a8a7adffe518dc204") || // WoA Standard
e.includes("1843460")
)
}
if (H1_GOTY_UNLOCKABLES.includes(id)) {
return (
e.includes("894d1e6771044f48a8fdde934b8e443a") ||
e.includes("a3509775467d4d6a8a7adffe518dc204") || // WoA Standard
e.includes("1843460") ||
e.includes("1829595")
)
}
if (H2_RACCOON_STINGRAY_UNLOCKABLES.includes(id)) {
return (
e.includes("afa4b921503f43339c360d4b53910791") ||
e.includes("84a1a6fda4fb48afbb78ee9b2addd475") || // WoA Deluxe
e.includes("1829590")
)
}
} else if (gameVersion === "h2") {
if (WINTERSPORTS_UNLOCKABLES.includes(id)) {
return e.includes("957693")
}
} else if (
// @ts-expect-error The types do actually overlap, but there is no way to show that.
gameVersion === "h1" &&
(e.includes("0a73eaedcac84bd28b567dbec764c5cb") ||
e.includes(EPIC_NAMESPACE_2016))
) {
// h1 EGS
if (
H1_REQUIEM_UNLOCKABLES.includes(id) ||
H1_GOTY_UNLOCKABLES.includes(id)
) {
return e.includes("81aecb49a60b47478e61e1cbd68d63c5")
}
}
if (DELUXE_UNLOCKABLES.includes(id)) {
return (
e.includes("bc610b36c75442299edcbe99f6f0fb60") ||
e.includes("84a1a6fda4fb48afbb78ee9b2addd475") || // WoA Deluxe
e.includes("1829591")
)
}
/*
TODO: Fix this entitlement check (confirmed its broken with Blazer)
if (LEGACY_UNLOCKABLES.includes(id)) {
return (
e.includes("0b59243cb8aa420691b66be1ecbe68c0") ||
e.includes("1829593")
)
}
*/
if (SIN_GREED_UNLOCKABLES.includes(id)) {
return (
e.includes("0e8632b4cdfb415e94291d97d727b98d") ||
e.includes("84a1a6fda4fb48afbb78ee9b2addd475") || // WoA Deluxe
e.includes("1829580")
)
}
if (SIN_PRIDE_UNLOCKABLES.includes(id)) {
return (
e.includes("3f9adc216dde44dda5e829f11740a0a2") ||
e.includes("84a1a6fda4fb48afbb78ee9b2addd475") || // WoA Deluxe
e.includes("1829581")
)
}
if (SIN_SLOTH_UNLOCKABLES.includes(id)) {
return (
e.includes("aece009ff59441c0b526f8aa69e24cfb") ||
e.includes("84a1a6fda4fb48afbb78ee9b2addd475") || // WoA Deluxe
e.includes("1829582")
)
}
if (SIN_LUST_UNLOCKABLES.includes(id)) {
return (
e.includes("dfe5aeb89976450ba1e0e2c208b63d33") ||
e.includes("84a1a6fda4fb48afbb78ee9b2addd475") || // WoA Deluxe
e.includes("1829583")
)
}
if (SIN_GLUTTONY_UNLOCKABLES.includes(id)) {
return (
e.includes("30107bff80024d1ab291f9cd3bac9fac") ||
e.includes("84a1a6fda4fb48afbb78ee9b2addd475") || // WoA Deluxe
e.includes("1829584")
)
}
if (SIN_ENVY_UNLOCKABLES.includes(id)) {
return (
e.includes("0403062df0d347619c8dcf043c65c02e") ||
e.includes("84a1a6fda4fb48afbb78ee9b2addd475") || // WoA Deluxe
e.includes("1829585")
)
}
if (SIN_WRATH_UNLOCKABLES.includes(id)) {
return (
e.includes("9e936ed2507a473db6f53ad24d2da587") ||
e.includes("84a1a6fda4fb48afbb78ee9b2addd475") || // WoA Deluxe
e.includes("1829586")
)
}
if (TRINITY_UNLOCKABLES.includes(id)) {
return (
e.includes("5d06a6c6af9b4875b3530d5328f61287") ||
e.includes("1829596")
)
}
// The following two must be confirmed, epic entitlements may be in the wrong order! - AF
if (MAKESHIFT_UNLOCKABLES.includes(id)) {
return (
e.includes("08d2bc4d20754191b6c488541d2b4fa1") ||
e.includes("2184791")
)
}
if (CONCRETEART_UNLOCKABLES.includes(id)) {
return (
e.includes("a1e9a63fa4f3425aa66b9b8fa3c9cc35") ||
e.includes("2184790")
)
}
return true
}
}
export function createInventory(
profileId: string,
gameVersion: GameVersion,
entP: string[],
): InventoryItem[] {
if (inventoryUserCache.has(profileId)) {
return inventoryUserCache.get(profileId)!
}
// Get user data to check on location progression
const userProfile = getUserData(profileId, gameVersion)
// add all unlockables to player's inventory
const allunlockables = getVersionedConfig<Unlockable[]>(
"allunlockables",
gameVersion,
true,
).filter((u) => u.Type !== "location") // locations not in inventory
let unlockables: Unlockable[] = allunlockables
if (getFlag("enableMasteryProgression")) {
const packagedUnlocks: Map<string, boolean> = new Map()
const challengesUnlockables =
controller.challengeService.getChallengesUnlockables(gameVersion)
/**
* Separates unlockable types and lookup for progression level
* on unlockables that are locked behind mastery progression level
*/
const [unlockedItems, otherItems]: [Unlockable[], Unlockable[]] =
allunlockables
// Sorts packages and evergreen gear wrappers first, so related unlockables can be targeted after
.sort((_, b) =>
b.Type === "package" ||
(b.Type === "evergreenmastery" && b.Properties.Unlocks)
? 1
: -1,
)
.reduce(
filterUnlockedContent(
userProfile,
packagedUnlocks,
challengesUnlockables,
),
[[], []],
)
unlockables = [...unlockedItems, ...otherItems]
}
// ts-expect-error It cannot be undefined.
const filtered: InventoryItem[] = unlockables
.map((unlockable) => {
if (brokenItems.includes(unlockable.Guid)) {
return undefined
}
if (unlockable.Guid === "1efe1010-4fff-4ee2-833e-7c58b6518e3e") {
unlockable.Properties.Name =
"char_reward_hero_halloweenoutfit_m_pro140008_name_ebf1e362-671f-47e8-8c88-dd490d8ad866"
unlockable.Properties.Description =
"char_reward_hero_halloweenoutfit_m_pro140008_description_ebf1e362-671f-47e8-8c88-dd490d8ad866"
}
unlockable.GameAsset = null
unlockable.DisplayNameLocKey = `UI_${unlockable.Id}_NAME`
return {
InstanceId: unlockable.Guid,
ProfileId: profileId,
Unlockable: unlockable,
Properties: {},
}
})
// filter again, this time removing legacy unlockables
.filter(filterAllowedContent(gameVersion, entP))
for (const unlockable of filtered) {
unlockable!.ProfileId = profileId
}
inventoryUserCache.set(profileId, filtered)
return filtered
}
export function grantDrops(profileId: string, drops: Unlockable[]): void {
if (inventoryUserCache.has(profileId)) {
const inventoryItems: InventoryItem[] = drops.map((unlockable) => ({
InstanceId: unlockable.Guid,
ProfileId: profileId,
Unlockable: unlockable,
Properties: {},
}))
inventoryUserCache.set(profileId, [
...new Set([
...inventoryUserCache.get(profileId),
...inventoryItems.filter(
(invItem) => invItem.Unlockable.Type !== "evergreenmastery",
),
]),
])
} else {
/**
* @TODO Don't think theres a situation where the user doesn't have an inventory, but I may be wrong so leaving this for now.
* Can delete if unnecessary
*/
log(LogLevel.DEBUG, "No inventory for provided user")
}
}
export function getDataForUnlockables(
gameVersion: GameVersion,
unlockableIds: string[],
): Unlockable[] {
return getVersionedConfig<Unlockable[]>(
"allunlockables",
gameVersion,
true,
).filter((unlockable) => unlockableIds.includes(unlockable.Id))
}
export function getUnlockableById(
gameVersion: GameVersion,
unlockableId: string,
): Unlockable | undefined {
return getVersionedConfig<Unlockable[]>(
"allunlockables",
gameVersion,
true,
).find((unlockable) => unlockable.Id === unlockableId)
}