From c08d0f56a1dd135feb57118cd220bb8a5c84c964 Mon Sep 17 00:00:00 2001 From: Reece Dunham Date: Sat, 16 Dec 2023 20:55:19 -0500 Subject: [PATCH] Gnarly stashpoint refactoring Signed-off-by: Reece Dunham --- components/2016/legacyMenuData.ts | 213 +------------ components/2016/legacyMenuSystem.ts | 11 - components/candle/challengeRouting.ts | 66 ---- components/generatedPeacockRequireTable.ts | 10 +- components/menuData.ts | 290 ++++++++--------- components/menus/stashpoints.ts | 346 +++++++++++++++++++++ components/types/gameSchemas.ts | 15 + components/types/types.ts | 7 - 8 files changed, 490 insertions(+), 468 deletions(-) delete mode 100644 components/candle/challengeRouting.ts create mode 100644 components/menus/stashpoints.ts diff --git a/components/2016/legacyMenuData.ts b/components/2016/legacyMenuData.ts index ae1a63540..52d444f78 100644 --- a/components/2016/legacyMenuData.ts +++ b/components/2016/legacyMenuData.ts @@ -19,223 +19,13 @@ import { Router } from "express" import { RequestWithJwt } from "../types/types" import { getConfig } from "../configSwizzleManager" -import { getDefaultSuitFor, uuidRegex } from "../utils" -import { json as jsonMiddleware } from "body-parser" import { controller } from "../controller" -import { - generateUserCentric, - getParentLocationByName, - getSubLocationByName, -} from "../contracts/dataGen" -import { getUserData } from "../databaseHandler" -import { log, LogLevel } from "../loggingInterop" -import { createInventory, getUnlockableById } from "../inventory" -import { getFlag } from "../flags" -import { loadouts } from "../loadouts" -import { StashpointQueryH2016, StashpointSlotName } from "../types/gameSchemas" +import { getParentLocationByName } from "../contracts/dataGen" const legacyMenuDataRouter = Router() -legacyMenuDataRouter.get( - "/stashpoint", - (req: RequestWithJwt, res) => { - if (!uuidRegex.test(req.query.contractid)) { - res.status(400).send("contract id was not a uuid") - return - } - - if (typeof req.query.slotname !== "string") { - res.status(400).send("invalid slot data") - return - } - - const contractData = controller.resolveContract(req.query.contractid) - - if (!contractData) { - res.status(404).send("contract not found") - return - } - - const loadoutSlots: StashpointSlotName[] = [ - "carriedweapon", - "carrieditem", - "concealedweapon", - "disguise", - "gear", - "gear", - "stashpoint", - ] - - if (loadoutSlots.includes(req.query.slotname.slice(0, -1))) { - req.query.slotid = req.query.slotname.slice(0, -1) - } else { - log( - LogLevel.ERROR, - `Unknown slotname in legacy stashpoint: ${req.query.slotname}`, - ) - return - } - - const userProfile = getUserData(req.jwt.unique_name, req.gameVersion) - - const sublocation = getSubLocationByName( - contractData.Metadata.Location, - req.gameVersion, - ) - - const inventory = createInventory( - req.jwt.unique_name, - req.gameVersion, - sublocation, - ) - - const userCentricContract = generateUserCentric( - contractData, - req.jwt.unique_name, - "h1", - ) - - const defaultLoadout = { - 2: "FIREARMS_HERO_PISTOL_TACTICAL_001_SU_SKIN01", - 3: getDefaultSuitFor(sublocation), - 4: "TOKEN_FIBERWIRE", - 5: "PROP_TOOL_COIN", - } - - const getLoadoutItem = (id: number) => { - if (getFlag("loadoutSaving") === "LEGACY") { - const dl = userProfile.Extensions.defaultloadout - - if (!dl) { - return defaultLoadout[id] - } - - const forLocation = (userProfile.Extensions.defaultloadout || - {})[sublocation?.Properties?.ParentLocation] - - if (!forLocation) { - return defaultLoadout[id] - } - - return forLocation[id] - } else { - let dl = loadouts.getLoadoutFor("h1") - - if (!dl) { - dl = loadouts.createDefault("h1") - } - - const forLocation = - dl.data[sublocation?.Properties?.ParentLocation] - - if (!forLocation) { - return defaultLoadout[id] - } - - return forLocation[id] - } - } - - res.json({ - template: getConfig("LegacyStashpointTemplate", false), - data: { - ContractId: req.query.contractid, - // the game actually only needs the loadoutdata from the requested slotid, but this is what IOI servers do - LoadoutData: [...loadoutSlots.entries()].map( - ([slotid, slotname]) => ({ - SlotName: slotname, - SlotId: slotid.toString(), - Items: inventory - .filter((item) => { - return ( - item.Unlockable.Properties.LoadoutSlot && // only display items - (item.Unlockable.Properties.LoadoutSlot === - slotname || // display items for requested slot - (slotname === "stashpoint" && // else: if stashpoint - item.Unlockable.Properties - .LoadoutSlot !== "disguise")) && // => display all non-disguise items - (req.query.allowlargeitems === "true" || - item.Unlockable.Properties - .LoadoutSlot !== "carriedweapon") && - item.Unlockable.Type !== - "challengemultipler" && - !item.Unlockable.Properties.InclusionData - ) // not sure about this one - }) - .map((item) => ({ - Item: item, - ItemDetails: { - Capabilities: [], - StatList: item.Unlockable.Properties - .Gameplay - ? Object.entries( - item.Unlockable.Properties - .Gameplay, - ).map(([key, value]) => ({ - Name: key, - Ratio: value, - })) - : [], - PropertyTexts: [], - }, - SlotId: slotid.toString(), - SlotName: slotname, - })), - Page: 0, - Recommended: getLoadoutItem(slotid) - ? { - item: getUnlockableById( - getLoadoutItem(slotid), - req.gameVersion, - ), - type: loadoutSlots[slotid], - owned: true, - } - : null, - HasMore: false, - HasMoreLeft: false, - HasMoreRight: false, - OptionalData: - slotid === 6 - ? { - stashpoint: req.query.stashpoint, - AllowLargeItems: - req.query.allowlargeitems || - !req.query.stashpoint, - } - : {}, - }), - ), - Contract: userCentricContract.Contract, - ShowSlotName: req.query.slotname, - UserCentric: userCentricContract, - }, - }) - }, -) - -legacyMenuDataRouter.get("/Safehouse", (req: RequestWithJwt, res, next) => { - const template = getConfig("LegacySafehouseTemplate", false) - - // call /SafehouseCategory but rewrite the result a bit - req.url = `/SafehouseCategory?page=0&type=${req.query.type}&subtype=` - const originalJsonFunc = res.json - - res.json = function json(originalData) { - return originalJsonFunc.call(this, { - template, - data: { - SafehouseData: originalData.data, - }, - }) - } - - next() -}) - legacyMenuDataRouter.get( "/debriefingchallenges", - jsonMiddleware(), ( req: RequestWithJwt<{ contractSessionId: string; contractId: string }>, res, @@ -265,7 +55,6 @@ legacyMenuDataRouter.get( legacyMenuDataRouter.get( "/MasteryLocation", - jsonMiddleware(), (req: RequestWithJwt<{ locationId: string; difficulty: string }>, res) => { const masteryData = controller.masteryService.getMasteryDataForDestination( diff --git a/components/2016/legacyMenuSystem.ts b/components/2016/legacyMenuSystem.ts index 130ace88e..624b319bf 100644 --- a/components/2016/legacyMenuSystem.ts +++ b/components/2016/legacyMenuSystem.ts @@ -16,13 +16,10 @@ * along with this program. If not, see . */ -import serveStatic from "serve-static" import { Router } from "express" import { join } from "path" import md5File from "md5-file" import { readFile } from "atomically" -import { imageFetchingMiddleware } from "../menus/imageHandler" -import { MenuSystemDatabase } from "../menus/menuSystem" const legacyMenuSystemRouter = Router() @@ -46,12 +43,4 @@ legacyMenuSystemRouter.get( }, ) -legacyMenuSystemRouter.use(MenuSystemDatabase.configMiddleware) - -legacyMenuSystemRouter.use( - "/images/", - serveStatic("images", { fallthrough: true }), - imageFetchingMiddleware, -) - export { legacyMenuSystemRouter } diff --git a/components/candle/challengeRouting.ts b/components/candle/challengeRouting.ts deleted file mode 100644 index aa267be31..000000000 --- a/components/candle/challengeRouting.ts +++ /dev/null @@ -1,66 +0,0 @@ -/* - * 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 . - */ - -import { getVersionedConfig } from "../configSwizzleManager" -import type { - CompiledChallengeTreeCategory, - PeacockLocationsData, - StandardRequest, - Unlockable, -} from "../types/types" -import { controller } from "../controller" - -export type ChallengeLocationQuery = { - locationId: string -} - -type ChallengeLocationResponse = { - template: unknown - data: { - Name: string - Location: Unlockable - Children: CompiledChallengeTreeCategory[] - } -} - -export function challengeLocation( - req: StandardRequest, -): ChallengeLocationResponse { - const location = getVersionedConfig( - "LocationsData", - req.gameVersion, - true, - ).children[req.query.locationId] - - return { - template: getVersionedConfig( - "ChallengeLocationTemplate", - req.gameVersion, - false, - ), - data: { - Name: location.DisplayNameLocKey, - Location: location, - Children: controller.challengeService.getChallengeDataForLocation( - req.query.locationId, - req.gameVersion, - req.jwt.unique_name, - ), - }, - } -} diff --git a/components/generatedPeacockRequireTable.ts b/components/generatedPeacockRequireTable.ts index 2ccadb3ea..8a0b201dd 100644 --- a/components/generatedPeacockRequireTable.ts +++ b/components/generatedPeacockRequireTable.ts @@ -46,7 +46,6 @@ import * as legacyMenuData from "./2016/legacyMenuData" import * as legacyMenuSystem from "./2016/legacyMenuSystem" import * as legacyProfileRouter from "./2016/legacyProfileRouter" import * as challengeHelpers from "./candle/challengeHelpers" -import * as challengeRouting from "./candle/challengeRouting" import * as challengeService from "./candle/challengeService" import * as masteryService from "./candle/masteryService" import * as progressionService from "./candle/progressionService" @@ -71,6 +70,7 @@ import * as menuSystem from "./menus/menuSystem" import * as planning from "./menus/planning" import * as playnext from "./menus/playnext" import * as sniper from "./menus/sniper" +import * as stashpoints from "./menus/stashpoints" import * as multiplayerMenuData from "./multiplayer/multiplayerMenuData" import * as multiplayerService from "./multiplayer/multiplayerService" import * as multiplayerUtils from "./multiplayer/multiplayerUtils" @@ -151,10 +151,6 @@ export default { __esModule: true, ...challengeHelpers, }, - "@peacockproject/core/candle/challengeRouting": { - __esModule: true, - ...challengeRouting, - }, "@peacockproject/core/candle/challengeService": { __esModule: true, ...challengeService, @@ -230,6 +226,10 @@ export default { "@peacockproject/core/menus/planning": { __esModule: true, ...planning }, "@peacockproject/core/menus/playnext": { __esModule: true, ...playnext }, "@peacockproject/core/menus/sniper": { __esModule: true, ...sniper }, + "@peacockproject/core/menus/stashpoints": { + __esModule: true, + ...stashpoints, + }, "@peacockproject/core/multiplayer/multiplayerMenuData": { __esModule: true, ...multiplayerMenuData, diff --git a/components/menuData.ts b/components/menuData.ts index 6ae4da0a7..7b3be15ac 100644 --- a/components/menuData.ts +++ b/components/menuData.ts @@ -43,7 +43,6 @@ import type { GameVersion, HitsCategoryCategory, JwtData, - MissionManifest, PeacockLocationsData, PlayerProfileView, ProgressionData, @@ -56,7 +55,6 @@ import { complications, generateCompletionData, generateUserCentric, - getSubLocationByName, } from "./contracts/dataGen" import { log, LogLevel } from "./loggingInterop" import { @@ -82,6 +80,7 @@ import { createInventory, getUnlockableById } from "./inventory" import { json as jsonMiddleware } from "body-parser" import { hitsCategoryService } from "./contracts/hitsCategoryService" import { + ChallengeLocationQuery, DebriefingLeaderboardsQuery, GetCompletionDataForLocationQuery, GetDestinationQuery, @@ -90,15 +89,14 @@ import { MissionEndRequestQuery, PlanningQuery, SafehouseCategoryQuery, + SafehouseQuery, StashpointQuery, + StashpointQueryH2016, } from "./types/gameSchemas" import assert from "assert" -import { - challengeLocation, - ChallengeLocationQuery, -} from "./candle/challengeRouting" import { MissionEndResult } from "./types/score" import { getLeaderboardEntries } from "./contracts/leaderboards" +import { getLegacyStashData, getModernStashData } from "./menus/stashpoints" const menuDataRouter = Router() @@ -112,14 +110,30 @@ menuDataRouter.get( return } - const r = challengeLocation({ - query: req.query, - body: undefined as never, - gameVersion: req.gameVersion, - jwt: req.jwt, - }) + const location = getVersionedConfig( + "LocationsData", + req.gameVersion, + true, + ).children[req.query.locationId] + + const data = { + Name: location.DisplayNameLocKey, + Location: location, + Children: controller.challengeService.getChallengeDataForLocation( + req.query.locationId, + req.gameVersion, + req.jwt.unique_name, + ), + } - res.json(r) + res.json({ + template: getVersionedConfig( + "ChallengeLocationTemplate", + req.gameVersion, + false, + ), + data, + }) }, ) @@ -322,7 +336,32 @@ menuDataRouter.get("/Hub", (req: RequestWithJwt, res) => { }) menuDataRouter.get("/SafehouseCategory", (req: RequestWithJwt, res) => { - res.json(getSafehouseCategory(req.query, req.gameVersion, req.jwt)) + res.json({ + template: + req.gameVersion === "h1" + ? getConfig("LegacySafehouseTemplate", false) + : null, + data: getSafehouseCategory(req.query, req.gameVersion, req.jwt), + }) +}) + +menuDataRouter.get("/Safehouse", (req: RequestWithJwt, res) => { + const template = getConfig("LegacySafehouseTemplate", false) + + const newQuery: SafehouseCategoryQuery = { + type: req.query.type, + } + + res.json({ + template, + data: { + SafehouseData: getSafehouseCategory( + newQuery, + req.gameVersion, + req.jwt, + ), + }, + }) }) export function getSafehouseCategory( @@ -332,17 +371,11 @@ export function getSafehouseCategory( ) { const inventory = createInventory(jwt.unique_name, gameVersion) - const safehouseData = { - template: - gameVersion === "h1" - ? getConfig("LegacySafehouseTemplate", false) - : null, - data: { - Category: "_root", - SubCategories: [], - IsLeaf: false, - Data: null, - } as SafehouseCategory, + let safehouseData: SafehouseCategory = { + Category: "_root", + SubCategories: [], + IsLeaf: false, + Data: null, } for (const item of inventory) { @@ -375,7 +408,7 @@ export function getSafehouseCategory( continue // I don't want to put this in that elif statement } - let category = safehouseData.data.SubCategories.find( + let category = safehouseData.SubCategories.find( (cat) => cat.Category === item.Unlockable.Type, ) let subcategory @@ -387,7 +420,7 @@ export function getSafehouseCategory( IsLeaf: false, Data: null, } - safehouseData.data.SubCategories.push(category) + safehouseData.SubCategories.push(category) } subcategory = category.SubCategories.find( @@ -429,17 +462,17 @@ export function getSafehouseCategory( }) } - for (const [id, category] of safehouseData.data.SubCategories.entries()) { + for (const [id, category] of safehouseData.SubCategories.entries()) { if (category.SubCategories.length === 1) { // if category only has one subcategory - safehouseData.data.SubCategories[id] = category.SubCategories[0] // flatten it - safehouseData.data.SubCategories[id].Category = category.Category // but keep the top category's name + safehouseData.SubCategories[id] = category.SubCategories[0] // flatten it + safehouseData.SubCategories[id].Category = category.Category // but keep the top category's name } } - if (safehouseData.data.SubCategories.length === 1) { + if (safehouseData.SubCategories.length === 1) { // if root has only one subcategory - safehouseData.data = safehouseData.data.SubCategories[0] // flatten it + safehouseData = safehouseData.SubCategories[0] // flatten it } return safehouseData @@ -457,144 +490,69 @@ menuDataRouter.get("/report", (req: RequestWithJwt, res) => { }) }) +// /stashpoint?contractid=e5b6ccf4-1f29-4ec6-bfb8-2e9b78882c85&slotid=4&slotname=gear4&stashpoint=&allowlargeitems=true&allowcontainers=true +// /stashpoint?contractid=c1d015b4-be08-4e44-808e-ada0f387656f&slotid=3&slotname=disguise3&stashpoint=&allowlargeitems=true&allowcontainers=true +// /stashpoint?contractid=&slotid=3&slotname=disguise&stashpoint=&allowlargeitems=true&allowcontainers=false +// /stashpoint?contractid=5b5f8aa4-ecb4-4a0a-9aff-98aa1de43dcc&slotid=6&slotname=stashpoint6&stashpoint=28b03709-d1f0-4388-b207-f03611eafb64&allowlargeitems=true&allowcontainers=false menuDataRouter.get( "/stashpoint", - (req: RequestWithJwt, res) => { - // Note: this is handled differently for 2016 - // /stashpoint?contractid=e5b6ccf4-1f29-4ec6-bfb8-2e9b78882c85&slotid=4&slotname=gear4&stashpoint=&allowlargeitems=true&allowcontainers=true - // /stashpoint?contractid=c1d015b4-be08-4e44-808e-ada0f387656f&slotid=3&slotname=disguise3&stashpoint=&allowlargeitems=true&allowcontainers=true - // /stashpoint?contractid=&slotid=3&slotname=disguise&stashpoint=&allowlargeitems=true&allowcontainers=false - // /stashpoint?contractid=5b5f8aa4-ecb4-4a0a-9aff-98aa1de43dcc&slotid=6&slotname=stashpoint6&stashpoint=28b03709-d1f0-4388-b207-f03611eafb64&allowlargeitems=true&allowcontainers=false - const stashData: { - template: unknown - data?: { - SlotId?: string | number - LoadoutItemsData?: unknown - UserCentric?: UserCentricContract - ShowSlotName?: string | number - } - } = { - template: getVersionedConfig( - "StashpointTemplate", - req.gameVersion === "h1" ? "h1" : "h3", - false, - ), - } - - if ( - typeof req.query.slotname !== "string" || - !(req.query.slotid ?? undefined) - ) { - res.status(400).send("invalid slot data") - return + (req: RequestWithJwt, res) => { + function isValidModernQuery( + query: StashpointQuery | StashpointQueryH2016, + ): query is StashpointQuery { + return ( + query?.slotname !== undefined && + (query as StashpointQuery)?.slotid !== undefined + ) } - let contractData: MissionManifest | undefined = undefined + if (["h1", "scpc"].includes(req.gameVersion)) { + // H1 or SCPC + if (!uuidRegex.test(req.query.contractid)) { + res.status(400).send("contract id was not a uuid") + return + } - if (req.query.contractid) { - contractData = controller.resolveContract(req.query.contractid) - } + if (typeof req.query.slotname !== "string") { + res.status(400).send("invalid slot data") + return + } - const inventory = createInventory( - req.jwt.unique_name, - req.gameVersion, - getSubLocationByName( - contractData?.Metadata.Location, + const data = getLegacyStashData( + req.query, + req.jwt.unique_name, req.gameVersion, - ), - ) - - if (req.query.slotname.endsWith(req.query.slotid!.toString())) { - req.query.slotname = req.query.slotname.slice( - 0, - -req.query.slotid!.toString().length, - ) // weird - } - - stashData.data = { - SlotId: req.query.slotid, - LoadoutItemsData: { - SlotId: req.query.slotid, - Items: inventory - .filter((item) => { - if ( - req.query.slotname === "gear" && - contractData?.Peacock?.noGear === true - ) { - return false - } - - if ( - req.query.slotname === "concealedweapon" && - contractData?.Peacock?.noCarriedWeapon === true - ) { - return false - } + ) - if ( - item.Unlockable.Subtype === "disguise" && - req.gameVersion === "h3" - ) { - return false - } + if (!data) { + res.status(400).send("impossible to fulfill") + return + } - return ( - item.Unlockable.Properties.LoadoutSlot && // only display items - (!req.query.slotname || - ((uuidRegex.test(req.query.slotid as string) || // container - req.query.slotname === "stashpoint") && // stashpoint - item.Unlockable.Properties.LoadoutSlot !== - "disguise") || // container or stashpoint => display all items - item.Unlockable.Properties.LoadoutSlot === - req.query.slotname) && // else: display items for requested slot - (req.query.allowcontainers === "true" || - !item.Unlockable.Properties.IsContainer) && - (req.query.allowlargeitems === "true" || - item.Unlockable.Properties.LoadoutSlot !== - "carriedweapon") && - item.Unlockable.Type !== "challengemultiplier" && - !item.Unlockable.Properties.InclusionData - ) // not sure about this one - }) - .map((item) => ({ - Item: item, - ItemDetails: { - Capabilities: [], - StatList: item.Unlockable.Properties.Gameplay - ? Object.entries( - item.Unlockable.Properties.Gameplay, - ).map(([key, value]) => ({ - Name: key, - Ratio: value, - })) - : [], - PropertyTexts: [], - }, - SlotId: req.query.slotid, - SlotName: null, - })), - Page: 0, - HasMore: false, - HasMoreLeft: false, - HasMoreRight: false, - OptionalData: { - stashpoint: req.query.stashpoint || "", - AllowLargeItems: req.query.allowlargeitems, - AllowContainers: req.query.allowcontainers, // ?? true - }, - }, - ShowSlotName: req.query.slotname, - } + res.json({ + template: getConfig("LegacyStashpointTemplate", false), + data, + }) + } else { + // H2 or H3 + if (!isValidModernQuery(req.query)) { + res.status(400).send("invalid query") + return + } - if (contractData) { - stashData.data.UserCentric = generateUserCentric( - contractData, - req.jwt.unique_name, - req.gameVersion, - ) + res.json({ + template: getVersionedConfig( + "StashpointTemplate", + req.gameVersion === "h1" ? "h1" : "h3", + false, + ), + data: getModernStashData( + req.query, + req.jwt.unique_name, + req.gameVersion, + ), + }) } - - res.json(stashData) }, ) @@ -1724,14 +1682,12 @@ menuDataRouter.get( res.json({ template: masteryUnlockTemplate, - data: { - ...controller.masteryService.getMasteryDataForSubPackage( - parentLocation, - req.query.unlockableId, - req.gameVersion, - req.jwt.unique_name, - ), - }, + data: controller.masteryService.getMasteryDataForSubPackage( + parentLocation, + req.query.unlockableId, + req.gameVersion, + req.jwt.unique_name, + ), }) }, ) diff --git a/components/menus/stashpoints.ts b/components/menus/stashpoints.ts new file mode 100644 index 000000000..a11d6e762 --- /dev/null +++ b/components/menus/stashpoints.ts @@ -0,0 +1,346 @@ +/* + * 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 . + */ + +import { createInventory, getUnlockableById, InventoryItem } from "../inventory" +import type { + GameVersion, + MissionManifest, + UserCentricContract, +} from "../types/types" +import { + StashpointQuery, + StashpointQueryH2016, + StashpointSlotName, +} from "../types/gameSchemas" +import { getDefaultSuitFor, uuidRegex } from "../utils" +import { controller } from "../controller" +import { generateUserCentric, getSubLocationByName } from "../contracts/dataGen" +import { log, LogLevel } from "../loggingInterop" +import { getUserData } from "../databaseHandler" +import { getFlag } from "../flags" +import { loadouts } from "../loadouts" + +/** + * Algorithm to get the stashpoint items data for H2 and H3. + * + * @param inventory The user's inventory. + * @param query The input query for the stashpoint. + * @param gameVersion + * @param contractData The optional contract data. + */ +export function getModernStashItemsData( + inventory: InventoryItem[], + query: StashpointQuery, + gameVersion: GameVersion, + contractData: MissionManifest | undefined, +) { + return inventory + .filter((item) => { + if ( + (query.slotname === "gear" && + contractData?.Peacock?.noGear === true) || + (query.slotname === "concealedweapon" && + contractData?.Peacock?.noCarriedWeapon === true) + ) { + return false + } + + if ( + item.Unlockable.Subtype === "disguise" && + gameVersion === "h3" + ) { + return false + } + + return ( + item.Unlockable.Properties.LoadoutSlot && // only display items + (!query.slotname || + ((uuidRegex.test(query.slotid as string) || // container + query.slotname === "stashpoint") && // stashpoint + item.Unlockable.Properties.LoadoutSlot !== + "disguise") || // container or stashpoint => display all items + item.Unlockable.Properties.LoadoutSlot === + query.slotname) && // else: display items for requested slot + (query.allowcontainers === "true" || + !item.Unlockable.Properties.IsContainer) && + (query.allowlargeitems === "true" || + item.Unlockable.Properties.LoadoutSlot !== + "carriedweapon") && + item.Unlockable.Type !== "challengemultiplier" && + !item.Unlockable.Properties.InclusionData + ) // not sure about this one + }) + .map((item) => ({ + Item: item, + ItemDetails: { + Capabilities: [], + StatList: item.Unlockable.Properties.Gameplay + ? Object.entries(item.Unlockable.Properties.Gameplay).map( + ([key, value]) => ({ + Name: key, + Ratio: value, + }), + ) + : [], + PropertyTexts: [], + }, + SlotId: query.slotid, + SlotName: null, + })) +} + +export type ModernStashData = { + SlotId: string | number + LoadoutItemsData: unknown + UserCentric?: UserCentricContract + ShowSlotName: string | number +} + +/** + * Algorithm to get the stashpoint data for H2 and H3. + * + * @param query The stashpoint query. + * @param userId + * @param gameVersion + * @returns undefined if the query is invalid, or the stash data. + */ +export function getModernStashData( + query: StashpointQuery, + userId: string, + gameVersion: GameVersion, +): ModernStashData { + let contractData: MissionManifest | undefined = undefined + + if (query.contractid) { + contractData = controller.resolveContract(query.contractid) + } + + const inventory = createInventory( + userId, + gameVersion, + getSubLocationByName(contractData?.Metadata.Location, gameVersion), + ) + + if (query.slotname.endsWith(query.slotid!.toString())) { + query.slotname = query.slotname.slice( + 0, + -query.slotid!.toString().length, + ) // weird + } + + const stashData: ModernStashData = { + SlotId: query.slotid, + LoadoutItemsData: { + SlotId: query.slotid, + Items: getModernStashItemsData( + inventory, + query, + gameVersion, + contractData, + ), + Page: 0, + HasMore: false, + HasMoreLeft: false, + HasMoreRight: false, + OptionalData: { + stashpoint: query.stashpoint || "", + AllowLargeItems: query.allowlargeitems, + AllowContainers: query.allowcontainers, // ?? true + }, + }, + ShowSlotName: query.slotname, + } + + if (contractData) { + stashData.UserCentric = generateUserCentric( + contractData, + userId, + gameVersion, + ) + } + + return stashData +} + +/** + * Algorithm to get the stashpoint items data for H2016. + * + * @param inventory The user's inventory. + * @param slotname The slot name. + * @param query + * @param slotid The slot id. + */ +export function getLegacyStashItems( + inventory: InventoryItem[], + slotname: StashpointSlotName, + query: StashpointQueryH2016, + slotid: number, +) { + return inventory + .filter((item) => { + return ( + item.Unlockable.Properties.LoadoutSlot && // only display items + (item.Unlockable.Properties.LoadoutSlot === slotname || // display items for requested slot + (slotname === "stashpoint" && // else: if stashpoint + item.Unlockable.Properties.LoadoutSlot !== + "disguise")) && // => display all non-disguise items + (query.allowlargeitems === "true" || + item.Unlockable.Properties.LoadoutSlot !== + "carriedweapon") && + item.Unlockable.Type !== "challengemultipler" && + !item.Unlockable.Properties.InclusionData + ) // not sure about this one + }) + .map((item) => ({ + Item: item, + ItemDetails: { + Capabilities: [], + StatList: item.Unlockable.Properties.Gameplay + ? Object.entries(item.Unlockable.Properties.Gameplay).map( + ([key, value]) => ({ + Name: key, + Ratio: value, + }), + ) + : [], + PropertyTexts: [], + }, + SlotId: slotid.toString(), + SlotName: slotname, + })) +} + +const loadoutSlots: StashpointSlotName[] = [ + "carriedweapon", + "carrieditem", + "concealedweapon", + "disguise", + "gear", + "gear", + "stashpoint", +] + +/** + * Algorithm to get the stashpoint data for H2016. + * + * @param query The stashpoint query. + * @param userId + * @param gameVersion + */ +export function getLegacyStashData( + query: StashpointQueryH2016, + userId: string, + gameVersion: GameVersion, +) { + const contractData = controller.resolveContract(query.contractid) + + if (!contractData) { + return undefined + } + + if (!loadoutSlots.includes(query.slotname.slice(0, -1))) { + log( + LogLevel.ERROR, + `Unknown slotname in legacy stashpoint: ${query.slotname}`, + ) + return undefined + } + + const userProfile = getUserData(userId, gameVersion) + + const sublocation = getSubLocationByName( + contractData.Metadata.Location, + gameVersion, + ) + + const inventory = createInventory(userId, gameVersion, sublocation) + + const userCentricContract = generateUserCentric( + contractData, + userId, + gameVersion, + ) + + const defaultLoadout = { + 2: "FIREARMS_HERO_PISTOL_TACTICAL_001_SU_SKIN01", + 3: getDefaultSuitFor(sublocation), + 4: "TOKEN_FIBERWIRE", + 5: "PROP_TOOL_COIN", + } + + const getLoadoutItem = (id: number) => { + if (getFlag("loadoutSaving") === "LEGACY") { + const dl = userProfile.Extensions.defaultloadout + + if (!dl) { + return defaultLoadout[id] + } + + const forLocation = (userProfile.Extensions.defaultloadout || {})[ + sublocation?.Properties?.ParentLocation + ] + + return (forLocation || defaultLoadout)[id] + } else { + let dl = loadouts.getLoadoutFor("h1") + + if (!dl) { + dl = loadouts.createDefault("h1") + } + + const forLocation = dl.data[sublocation?.Properties?.ParentLocation] + + return (forLocation || defaultLoadout)[id] + } + } + + return { + ContractId: query.contractid, + // the game actually only needs the loadoutdata from the requested slotid, but this is what IOI servers do + LoadoutData: [...loadoutSlots.entries()].map(([slotid, slotname]) => ({ + SlotName: slotname, + SlotId: slotid.toString(), + Items: getLegacyStashItems(inventory, slotname, query, slotid), + Page: 0, + Recommended: getLoadoutItem(slotid) + ? { + item: getUnlockableById( + getLoadoutItem(slotid), + gameVersion, + ), + type: loadoutSlots[slotid], + owned: true, + } + : null, + HasMore: false, + HasMoreLeft: false, + HasMoreRight: false, + OptionalData: + slotid === 6 + ? { + stashpoint: query.stashpoint, + AllowLargeItems: + query.allowlargeitems || !query.stashpoint, + } + : {}, + })), + Contract: userCentricContract.Contract, + ShowSlotName: query.slotname, + UserCentric: userCentricContract, + } +} diff --git a/components/types/gameSchemas.ts b/components/types/gameSchemas.ts index 3aec74b87..d6eebe438 100644 --- a/components/types/gameSchemas.ts +++ b/components/types/gameSchemas.ts @@ -105,6 +105,14 @@ export type LoadSaveBody = Partial<{ contractId: string }> +/** + * Query params that `/profiles/page/Safehouse` gets. + * Roughly the same as {@link SafehouseCategoryQuery} but this route is only for H1. + */ +export type SafehouseQuery = { + type?: string +} + /** * Query params that `/profiles/page/SafehouseCategory` (used for Career > Inventory and possibly some of the H1 stuff) gets. */ @@ -137,3 +145,10 @@ export type DebriefingLeaderboardsQuery = { contractid: string difficulty?: string } + +/** + * Query params that `/profiles/page/ChallengeLocation` gets. + */ +export type ChallengeLocationQuery = { + locationId: string +} diff --git a/components/types/types.ts b/components/types/types.ts index 24315bf1c..912935729 100644 --- a/components/types/types.ts +++ b/components/types/types.ts @@ -1553,10 +1553,3 @@ export interface SMFLastDeploy { peacockPlugins?: string[] } } - -export interface StandardRequest { - query: Query - body: Body - gameVersion: GameVersion - jwt: JwtData -}