From a22da558a1632fe0cb7ab1a108669346178f28ca Mon Sep 17 00:00:00 2001 From: Reece Dunham Date: Wed, 22 Nov 2023 17:59:05 -0500 Subject: [PATCH] Reworking of the config system (squashed!) --- .idea/Peacock.iml | 1 + components/2016/legacyContractHandler.ts | 2 +- components/2016/legacyMenuData.ts | 2 +- components/2016/legacyProfileRouter.ts | 2 +- components/candle/challengeService.ts | 4 +- components/candle/masteryService.ts | 2 +- components/configManager.ts | 452 ++++++++++++++++++ components/configSwizzleManager.ts | 305 ------------ components/contracts/contractRouting.ts | 2 +- components/contracts/contractsModeRouting.ts | 2 +- components/contracts/dataGen.ts | 2 +- components/contracts/hitsCategoryService.ts | 2 +- components/controller.ts | 88 ++-- components/discordRp.ts | 2 +- components/eventHandler.ts | 2 +- components/evergreen.ts | 2 +- components/generatedPeacockRequireTable.ts | 8 +- components/hooksImpl.ts | 251 ---------- components/index.ts | 2 +- components/inventory.ts | 2 +- components/menuData.ts | 6 +- components/menus/campaigns.ts | 2 +- components/menus/destinations.ts | 2 +- components/menus/favoriteContracts.ts | 2 +- components/menus/menuSystem.ts | 4 +- components/menus/planning.ts | 2 +- components/menus/playnext.ts | 2 +- components/multiplayer/multiplayerMenuData.ts | 2 +- components/multiplayer/multiplayerService.ts | 2 +- components/oauthToken.ts | 4 +- components/playStyles.ts | 2 +- components/profileHandler.ts | 2 +- components/scoreHandler.ts | 2 +- components/smfSupport.ts | 11 +- components/tools.ts | 4 +- components/utils.ts | 2 +- components/webFeatures.ts | 2 +- packaging/build.mjs | 1 + tests/mocks/configSwizzleManager.ts | 35 -- tests/setup/globalDefines.ts | 41 ++ tests/src/oauthToken.test.ts | 10 +- tests/src/playStyles.test.ts | 7 +- tests/tsconfig.json | 7 +- 43 files changed, 599 insertions(+), 690 deletions(-) create mode 100644 components/configManager.ts delete mode 100644 components/configSwizzleManager.ts delete mode 100644 components/hooksImpl.ts delete mode 100644 tests/mocks/configSwizzleManager.ts diff --git a/.idea/Peacock.iml b/.idea/Peacock.iml index d4c88b150..0a94dc271 100644 --- a/.idea/Peacock.iml +++ b/.idea/Peacock.iml @@ -31,6 +31,7 @@ + diff --git a/components/2016/legacyContractHandler.ts b/components/2016/legacyContractHandler.ts index 4bca0e26c..d8f57c6fd 100644 --- a/components/2016/legacyContractHandler.ts +++ b/components/2016/legacyContractHandler.ts @@ -22,7 +22,7 @@ import { json as jsonMiddleware } from "body-parser" import { enqueueEvent, newSession } from "../eventHandler" import { _legacyBull, _theLastYardbirdScpc, controller } from "../controller" import { log, LogLevel } from "../loggingInterop" -import { getConfig } from "../configSwizzleManager" +import { getConfig } from "../configManager" import type { GameChanger, RequestWithJwt } from "../types/types" import { randomUUID } from "crypto" import { getFlag } from "../flags" diff --git a/components/2016/legacyMenuData.ts b/components/2016/legacyMenuData.ts index ae1a63540..bf2c8ca0e 100644 --- a/components/2016/legacyMenuData.ts +++ b/components/2016/legacyMenuData.ts @@ -18,7 +18,7 @@ import { Router } from "express" import { RequestWithJwt } from "../types/types" -import { getConfig } from "../configSwizzleManager" +import { getConfig } from "../configManager" import { getDefaultSuitFor, uuidRegex } from "../utils" import { json as jsonMiddleware } from "body-parser" import { controller } from "../controller" diff --git a/components/2016/legacyProfileRouter.ts b/components/2016/legacyProfileRouter.ts index 11dbbb78c..e449c7c25 100644 --- a/components/2016/legacyProfileRouter.ts +++ b/components/2016/legacyProfileRouter.ts @@ -22,7 +22,7 @@ import type { RequestWithJwt, } from "../types/types" import { log, LogLevel } from "../loggingInterop" -import { getConfig } from "../configSwizzleManager" +import { getConfig } from "../configManager" import { Router } from "express" import { controller } from "../controller" diff --git a/components/candle/challengeService.ts b/components/candle/challengeService.ts index bae93f879..7b4ccf019 100644 --- a/components/candle/challengeService.ts +++ b/components/candle/challengeService.ts @@ -58,11 +58,11 @@ import { mergeSavedChallengeGroups, } from "./challengeHelpers" import assert from "assert" -import { getVersionedConfig } from "../configSwizzleManager" -import { SyncHook } from "../hooksImpl" +import { getVersionedConfig } from "../configManager" import { getUserEscalationProgress } from "../contracts/escalations/escalationService" import { getUnlockableById } from "../inventory" +import { SyncHook } from "tapable" type ChallengeDefinitionLike = { Context?: Record diff --git a/components/candle/masteryService.ts b/components/candle/masteryService.ts index 451b57375..0a6801f46 100644 --- a/components/candle/masteryService.ts +++ b/components/candle/masteryService.ts @@ -21,7 +21,7 @@ import { getSubLocationByName, } from "../contracts/dataGen" import { log, LogLevel } from "../loggingInterop" -import { getConfig, getVersionedConfig } from "../configSwizzleManager" +import { getConfig, getVersionedConfig } from "../configManager" import { getUserData } from "../databaseHandler" import { MasteryData, diff --git a/components/configManager.ts b/components/configManager.ts new file mode 100644 index 000000000..8b05cb913 --- /dev/null +++ b/components/configManager.ts @@ -0,0 +1,452 @@ +/* + * 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 type { GameVersion } from "./types/types" +import { SyncWaterfallHook } from "tapable" +import { fastClone } from "./utils" + +class ConfigManager { + hooks: { + getConfig: SyncWaterfallHook<[unknown, ConfigKey, boolean]> + } = { + getConfig: new SyncWaterfallHook([ + "currentConfig", + "configName", + "clone", + ]), + } + + /** + * DO NOT MODIFY OUTSIDE OF TESTS!! + * The require function used to load configs. + */ + _require = require +} + +export const configManager = new ConfigManager() + +const h2LookupContractTemplate = () => + configManager._require("static/H2LookupContractTemplate.json") + +// noinspection JSUnusedGlobalSymbols +const configs = { + Roadmap() { + return configManager._require("static/Roadmap.json") + }, + StoreData() { + return configManager._require("static/StoreData.json") + }, + FilterData() { + return configManager._require("static/FilterData.json") + }, + LocationsData() { + return configManager._require("static/LocationsData.json") + }, + LeaderboardsViewTemplate() { + return configManager._require("static/LeaderboardsViewTemplate.json") + }, + LeaderboardEntriesTemplate() { + return configManager._require("static/LeaderboardEntriesTemplate.json") + }, + GameChangerProperties() { + return configManager._require("static/GameChangerProperties.json") + }, + allunlockables() { + return configManager._require("static/allunlockables.json") + }, + ServerVersionConfig() { + return configManager._require("static/ServerVersionConfig.json") + }, + OnlineConfig() { + return configManager._require("static/OnlineConfig.json") + }, + PrivacyPolicy() { + return configManager._require("static/PrivacyPolicy.json") + }, + UserDefault() { + return configManager._require("static/UserDefault.json") + }, + AgencyPickups() { + return configManager._require("static/AgencyPickups.json") + }, + Entrances() { + return configManager._require("static/Entrances.json") + }, + MissionEndReadyTemplate() { + return configManager._require("static/MissionEndReadyTemplate.json") + }, + MissionEndNotReadyTemplate() { + return configManager._require("static/MissionEndNotReadyTemplate.json") + }, + SelectAgencyPickupTemplate() { + return configManager._require("static/SelectAgencyPickupTemplate.json") + }, + SelectEntranceTemplate() { + return configManager._require("static/SelectEntranceTemplate.json") + }, + StashpointTemplate() { + return configManager._require("static/StashpointTemplate.json") + }, + LoadMenuTemplate() { + return configManager._require("static/LoadMenuTemplate.json") + }, + SaveMenuTemplate() { + return configManager._require("static/SaveMenuTemplate.json") + }, + Playstyles() { + return configManager._require("static/Playstyles.json") + }, + HubPageData() { + return configManager._require("static/HubPageData.json") + }, + DashboardCategoryEscalation() { + return configManager._require("static/DashboardCategoryEscalation.json") + }, + GlobalChallenges() { + return configManager._require("static/GlobalChallenges.json") + }, + ContractsTemplate() { + return configManager._require("static/ContractsTemplate.json") + }, + CreateContractPlanningTemplate() { + return configManager._require( + "static/CreateContractPlanningTemplate.json", + ) + }, + CreateContractReturnTemplate() { + return configManager._require( + "static/CreateContractReturnTemplate.json", + ) + }, + PlayerProfilePage() { + return configManager._require("static/PlayerProfileView.json") + }, + Legacyallunlockables() { + return configManager._require("static/Legacyallunlockables.json") + }, + LegacyGlobalChallenges() { + return configManager._require("static/LegacyGlobalChallenges.json") + }, + LegacySafehouseTemplate() { + return configManager._require("static/LegacySafehouseTemplate.json") + }, + LegacyHubTemplate() { + return configManager._require("static/LegacyHubTemplate.json") + }, + LegacyPlanningTemplate() { + return configManager._require("static/LegacyPlanningTemplate.json") + }, + LegacySelectAgencyPickupTemplate() { + return configManager._require( + "static/LegacySelectAgencyPickupTemplate.json", + ) + }, + LegacySelectEntranceTemplate() { + return configManager._require( + "static/LegacySelectEntranceTemplate.json", + ) + }, + LegacyStashpointTemplate() { + return configManager._require("static/LegacyStashpointTemplate.json") + }, + LegacyUserDefault() { + return configManager._require("static/LegacyUserDefault.json") + }, + LegacyFilterData() { + return configManager._require("static/LegacyFilterData.json") + }, + PlayNextTemplate() { + return configManager._require("static/PlayNextTemplate.json") + }, + LookupContractByIdTemplate() { + return configManager._require("static/LookupContractByIdTemplate.json") + }, + LookupContractFavoriteTemplate() { + return configManager._require( + "static/LookupContractFavoriteTemplate.json", + ) + }, + MissionStories() { + return configManager._require("static/MissionStories.json") + }, + DebriefingLeaderboardsTemplate() { + return configManager._require( + "static/DebriefingLeaderboardsTemplate.json", + ) + }, + LegacyHitsCategoryTemplate() { + return configManager._require("static/LegacyHitsCategoryTemplate.json") + }, + LegacyStoreData() { + return configManager._require("static/LegacyStoreData.json") + }, + LegacyDestinations() { + return configManager._require("static/LegacyDestinations.json") + }, + LegacyDestinationTemplate() { + return configManager._require("static/LegacyDestinationTemplate.json") + }, + LegacyLocationsData() { + return configManager._require("static/LegacyLocationsData.json") + }, + LegacySaveMenuTemplate() { + return configManager._require("static/LegacySaveMenuTemplate.json") + }, + LegacyLoadMenuTemplate() { + return configManager._require("static/LegacyLoadMenuTemplate.json") + }, + LegacyContractSearchResponseTemplate() { + return configManager._require( + "static/LegacyContractSearchResponseTemplate.json", + ) + }, + LegacyDebriefingChallengesTemplate() { + return configManager._require( + "static/LegacyDebriefingChallengesTemplate.json", + ) + }, + DebriefingChallengesTemplate() { + return configManager._require( + "static/DebriefingChallengesTemplate.json", + ) + }, + LegacyLookupContractByIdTemplate() { + return configManager._require( + "static/LegacyLookupContractByIdTemplate.json", + ) + }, + EiderDashboard() { + return configManager._require("static/EiderDashboard.json") + }, + FrankensteinHubTemplate() { + return configManager._require("static/FrankensteinHubTemplate.json") + }, + H2allunlockables() { + return configManager._require("static/H2allunlockables.json") + }, + H2DestinationsData() { + return configManager._require("static/H2DestinationsData.json") + }, + H2StoreData() { + return configManager._require("static/H2StoreData.json") + }, + H2ContractSearchResponseTemplate() { + return configManager._require( + "static/H2ContractSearchResponseTemplate.json", + ) + }, + H2LocationsData() { + return configManager._require("static/H2LocationsData.json") + }, + H2LookupContractByIdTemplate: h2LookupContractTemplate, + H2LookupContractFavoriteTemplate: h2LookupContractTemplate, + H2FilterData() { + return configManager._require("static/H2FilterData.json") + }, + H2CareerTemplate() { + return configManager._require("static/H2CareerTemplate.json") + }, + H2DashboardTemplate() { + return configManager._require("static/H2DashboardTemplate.json") + }, + H2SniperContentTemplate() { + return configManager._require("static/H2SniperContentTemplate.json") + }, + FrankensteinMmSpTemplate() { + return configManager._require("static/FrankensteinMmSpTemplate.json") + }, + FrankensteinMmMpTemplate() { + return configManager._require("static/FrankensteinMmMpTemplate.json") + }, + FrankensteinPlanningTemplate() { + return configManager._require( + "static/FrankensteinPlanningTemplate.json", + ) + }, + FrankensteinScoreOverviewTemplate() { + return configManager._require( + "static/FrankensteinScoreOverviewTemplate.json", + ) + }, + Videos() { + return configManager._require("static/Videos.json") + }, + ChallengeLocationTemplate() { + return configManager._require("static/ChallengeLocationTemplate.json") + }, + H2ChallengeLocationTemplate() { + return configManager._require("static/H2ChallengeLocationTemplate.json") + }, + LegacyChallengeLocationTemplate() { + return configManager._require( + "static/LegacyChallengeLocationTemplate.json", + ) + }, + ReportTemplate() { + return configManager._require("static/ReportTemplate.json") + }, + ContractSearchPageTemplate() { + return configManager._require("static/ContractSearchPageTemplate.json") + }, + ContractSearchPaginateTemplate() { + return configManager._require( + "static/ContractSearchPaginateTemplate.json", + ) + }, + ContractSearchResponseTemplate() { + return configManager._require( + "static/ContractSearchResponseTemplate.json", + ) + }, + MasteryUnlockablesTemplate() { + return configManager._require("static/MasteryUnlockablesTemplate.json") + }, + Scpcallunlockables() { + return configManager._require("static/Scpcallunlockables.json") + }, + ScpcLocationsData() { + return configManager._require("static/ScpcLocationsData.json") + }, + DiscordRichAssetsForBricks() { + return configManager._require("static/DiscordRichAssetsForBricks.json") + }, + EscalationCodenames() { + return configManager._require("static/EscalationCodenames.json") + }, + ScoreOverviewTemplate() { + return configManager._require("static/ScoreOverviewTemplate.json") + }, + PeacockGameChangerProperties() { + return configManager._require( + "static/PeacockGameChangerProperties.json", + ) + }, + MultiplayerPresets() { + return configManager._require("static/MultiplayerPresets.json") + }, + LobbySlimTemplate() { + return configManager._require("static/LobbySlimTemplate.json") + }, + MasteryDataForLocationTemplate() { + return configManager._require( + "static/MasteryDataForLocationTemplate.json", + ) + }, + LegacyMasteryLocationTemplate() { + return configManager._require( + "static/LegacyMasteryLocationTemplate.json", + ) + }, + DefaultCpdConfig() { + return configManager._require("static/DefaultCpdConfig.json") + }, + EvergreenGameChangerProperties() { + return configManager._require( + "static/EvergreenGameChangerProperties.json", + ) + }, + AreaMap() { + return configManager._require("static/AreaMap.json") + }, + ArcadePageTemplate() { + return configManager._require("static/ArcadePageTemplate.json") + }, + HitsCategoryElusiveTemplate() { + return configManager._require("static/HitsCategoryElusiveTemplate.json") + }, + HitsCategoryContractAttackTemplate() { + return configManager._require( + "static/HitsCategoryContractAttackTemplate.json", + ) + }, + MissionRewardsTemplate() { + return configManager._require("static/MissionRewardsTemplate.json") + }, + SniperUnlockables() { + return configManager._require("static/SniperUnlockables.json") + }, +} + +export type ConfigKey = keyof typeof configs + +export const configKeys: ConfigKey[] = Object.keys(configs) as ConfigKey[] + +/** + * Get a config file. + * Configs for H1 start with "Legacy", "H2" for HITMAN 2, and no prefix for HITMAN 3. + * + * @param config The name of the config file. + * @param clone If the value should be cloned (saves memory if false, but as a side effect, modifications will affect the actual config). + * @returns The config. + * @throws {Error} If the config file specified doesn't exist. + */ +export function getConfig(config: ConfigKey, clone: boolean): T { + if (!Object.hasOwn(configs, config)) { + throw new Error(`Tried to lookup config that does not exist: ${config}`) + } + + let c = configs[config]() + c = configManager.hooks.getConfig.call(c, config, clone) + + return clone ? fastClone(c) : c +} + +/** + * Get a config file intended for the specified game version. + * + * @param config The name of the config file. + * @param gameVersion The game's version ("h1", "h2", or "h3"). + * @param clone If the config should be cloned (saves memory if false, but as a side effect, modifications will affect the actual config). + * @returns The config. + * @see getConfig + * @throws {Error} If the config file specified doesn't exist. + */ +export function getVersionedConfig( + config: ConfigKey, + gameVersion: GameVersion, + clone: boolean, +): T { + let h1Prefix = "" + + if ( + // is this scpc, do we have a scpc config? + gameVersion === "scpc" && + Object.prototype.hasOwnProperty.call(configs, `Scpc${config}`) + ) { + h1Prefix = "Scpc" + } else { + // the above condition wasn't true + if (["scpc", "h1"].includes(gameVersion)) { + h1Prefix = "Legacy" + } + } + + // if this is H2, but we don't have a h2 specific config, fall back to h3 + if ( + gameVersion === "h2" && + !Object.prototype.hasOwnProperty.call(configs, `H2${config}`) + ) { + return getConfig(config, clone) + } + + return getConfig( + // @ts-expect-error No good way to explain this to TypeScript. + `${h1Prefix}${gameVersion === "h2" ? "H2" : ""}${config}`, + clone, + ) +} diff --git a/components/configSwizzleManager.ts b/components/configSwizzleManager.ts deleted file mode 100644 index de4fee149..000000000 --- a/components/configSwizzleManager.ts +++ /dev/null @@ -1,305 +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 . - */ - -/* eslint-disable @typescript-eslint/ban-ts-comment */ -// @ts-nocheck - -import Roadmap from "../static/Roadmap.json" -import StoreData from "../static/StoreData.json" -import FilterData from "../static/FilterData.json" -import LocationsData from "../static/LocationsData.json" -import GameChangerProperties from "../static/GameChangerProperties.json" -import allunlockables from "../static/allunlockables.json" -import ServerVersionConfig from "../static/ServerVersionConfig.json" -import OnlineConfig from "../static/OnlineConfig.json" -import PrivacyPolicy from "../static/PrivacyPolicy.json" -import UserDefault from "../static/UserDefault.json" -import AgencyPickups from "../static/AgencyPickups.json" -import Entrances from "../static/Entrances.json" -import LeaderboardEntriesTemplate from "../static/LeaderboardEntriesTemplate.json" -import LeaderboardsViewTemplate from "../static/LeaderboardsViewTemplate.json" -import MissionEndReadyTemplate from "../static/MissionEndReadyTemplate.json" -import MissionEndNotReadyTemplate from "../static/MissionEndNotReadyTemplate.json" -import SelectAgencyPickupTemplate from "../static/SelectAgencyPickupTemplate.json" -import SelectEntranceTemplate from "../static/SelectEntranceTemplate.json" -import StashpointTemplate from "../static/StashpointTemplate.json" -import LoadMenuTemplate from "../static/LoadMenuTemplate.json" -import SaveMenuTemplate from "../static/SaveMenuTemplate.json" -import Playstyles from "../static/Playstyles.json" -import HubPageData from "../static/HubPageData.json" -import DashboardCategoryEscalation from "../static/DashboardCategoryEscalation.json" -import GlobalChallenges from "../static/GlobalChallenges.json" -import ContractsTemplate from "../static/ContractsTemplate.json" -import CreateContractPlanningTemplate from "../static/CreateContractPlanningTemplate.json" -import CreateContractReturnTemplate from "../static/CreateContractReturnTemplate.json" -import PlayerProfilePage from "../static/PlayerProfileView.json" -import Legacyallunlockables from "../static/Legacyallunlockables.json" -import LegacyGlobalChallenges from "../static/LegacyGlobalChallenges.json" -import LegacySafehouseTemplate from "../static/LegacySafehouseTemplate.json" -import LegacyHubTemplate from "../static/LegacyHubTemplate.json" -import LegacyPlanningTemplate from "../static/LegacyPlanningTemplate.json" -import LegacySelectAgencyPickupTemplate from "../static/LegacySelectAgencyPickupTemplate.json" -import LegacySelectEntranceTemplate from "../static/LegacySelectEntranceTemplate.json" -import LegacyStashpointTemplate from "../static/LegacyStashpointTemplate.json" -import LegacyUserDefault from "../static/LegacyUserDefault.json" -import LegacyContractSearchResponseTemplate from "../static/LegacyContractSearchResponseTemplate.json" -import LegacyFilterData from "../static/LegacyFilterData.json" -import PlayNextTemplate from "../static/PlayNextTemplate.json" -import LookupContractByIdTemplate from "../static/LookupContractByIdTemplate.json" -import LookupContractFavoriteTemplate from "../static/LookupContractFavoriteTemplate.json" -import MissionStories from "../static/MissionStories.json" -import DebriefingLeaderboardsTemplate from "../static/DebriefingLeaderboardsTemplate.json" -import LegacyHitsCategoryTemplate from "../static/LegacyHitsCategoryTemplate.json" -import LegacyStoreData from "../static/LegacyStoreData.json" -import LegacyDestinations from "../static/LegacyDestinations.json" -import LegacyDestinationTemplate from "../static/LegacyDestinationTemplate.json" -import LegacyLocationsData from "../static/LegacyLocationsData.json" -import LegacySaveMenuTemplate from "../static/LegacySaveMenuTemplate.json" -import LegacyLoadMenuTemplate from "../static/LegacyLoadMenuTemplate.json" -import LegacyLookupContractByIdTemplate from "../static/LegacyLookupContractByIdTemplate.json" -import EiderDashboard from "../static/EiderDashboard.json" -import H2allunlockables from "../static/H2allunlockables.json" -import H2DestinationsData from "../static/H2DestinationsData.json" -import H2StoreData from "../static/H2StoreData.json" -import H2ContractSearchResponseTemplate from "../static/H2ContractSearchResponseTemplate.json" -import H2LocationsData from "../static/H2LocationsData.json" -import H2FilterData from "../static/H2FilterData.json" -import H2DashboardTemplate from "../static/H2DashboardTemplate.json" -import H2LookupContractTemplate from "../static/H2LookupContractTemplate.json" -import FrankensteinHubTemplate from "../static/FrankensteinHubTemplate.json" -import FrankensteinMmSpTemplate from "../static/FrankensteinMmSpTemplate.json" -import FrankensteinMmMpTemplate from "../static/FrankensteinMmMpTemplate.json" -import FrankensteinScoreOverviewTemplate from "../static/FrankensteinScoreOverviewTemplate.json" -import FrankensteinPlanningTemplate from "../static/FrankensteinPlanningTemplate.json" -import Videos from "../static/Videos.json" -import ChallengeLocationTemplate from "../static/ChallengeLocationTemplate.json" -import H2ChallengeLocationTemplate from "../static/H2ChallengeLocationTemplate.json" -import H2CareerTemplate from "../static/H2CareerTemplate.json" -import H2SniperContentTemplate from "../static/H2SniperContentTemplate.json" -import LegacyChallengeLocationTemplate from "../static/LegacyChallengeLocationTemplate.json" -import ReportTemplate from "../static/ReportTemplate.json" -import ContractSearchPageTemplate from "../static/ContractSearchPageTemplate.json" -import ContractSearchPaginateTemplate from "../static/ContractSearchPaginateTemplate.json" -import ContractSearchResponseTemplate from "../static/ContractSearchResponseTemplate.json" -import LegacyDebriefingChallengesTemplate from "../static/LegacyDebriefingChallengesTemplate.json" -import DebriefingChallengesTemplate from "../static/DebriefingChallengesTemplate.json" -import MasteryUnlockablesTemplate from "../static/MasteryUnlockablesTemplate.json" -import Scpcallunlockables from "../static/Scpcallunlockables.json" -import DiscordRichAssetsForBricks from "../static/DiscordRichAssetsForBricks.json" -import EscalationCodenames from "../static/EscalationCodenames.json" -import ScoreOverviewTemplate from "../static/ScoreOverviewTemplate.json" -import PeacockGameChangerProperties from "../static/PeacockGameChangerProperties.json" -import MultiplayerPresets from "../static/MultiplayerPresets.json" -import LobbySlimTemplate from "../static/LobbySlimTemplate.json" -import MasteryDataForLocationTemplate from "../static/MasteryDataForLocationTemplate.json" -import LegacyMasteryLocationTemplate from "../static/LegacyMasteryLocationTemplate.json" -import DefaultCpdConfig from "../static/DefaultCpdConfig.json" -import EvergreenGameChangerProperties from "../static/EvergreenGameChangerProperties.json" -import AreaMap from "../static/AreaMap.json" -import ArcadePageTemplate from "../static/ArcadePageTemplate.json" -import HitsCategoryElusiveTemplate from "../static/HitsCategoryElusiveTemplate.json" -import HitsCategoryContractAttackTemplate from "../static/HitsCategoryContractAttackTemplate.json" -import MissionRewardsTemplate from "../static/MissionRewardsTemplate.json" -import SniperUnlockables from "../static/SniperUnlockables.json" -import ScpcLocationsData from "../static/ScpcLocationsData.json" -import type { GameVersion } from "./types/types" -import { fastClone } from "./utils" - -/** - * All the configurations. Gets modified before being exported. - * - * @private - */ -const configs = { - Roadmap, - StoreData, - FilterData, - LocationsData, - LeaderboardsViewTemplate, - LeaderboardEntriesTemplate, - GameChangerProperties, - allunlockables, - ServerVersionConfig, - OnlineConfig, - PrivacyPolicy, - UserDefault, - AgencyPickups, - Entrances, - MissionEndReadyTemplate, - MissionEndNotReadyTemplate, - SelectAgencyPickupTemplate, - SelectEntranceTemplate, - StashpointTemplate, - LoadMenuTemplate, - SaveMenuTemplate, - Playstyles, - HubPageData, - DashboardCategoryEscalation, - GlobalChallenges, - ContractsTemplate, - CreateContractPlanningTemplate, - CreateContractReturnTemplate, - PlayerProfilePage, - Legacyallunlockables, - LegacyGlobalChallenges, - LegacySafehouseTemplate, - LegacyHubTemplate, - LegacyPlanningTemplate, - LegacySelectAgencyPickupTemplate, - LegacySelectEntranceTemplate, - LegacyStashpointTemplate, - LegacyUserDefault, - LegacyFilterData, - PlayNextTemplate, - LookupContractByIdTemplate, - LookupContractFavoriteTemplate, - MissionStories, - DebriefingLeaderboardsTemplate, - LegacyHitsCategoryTemplate, - LegacyStoreData, - LegacyDestinations, - LegacyDestinationTemplate, - LegacyLocationsData, - LegacySaveMenuTemplate, - LegacyLoadMenuTemplate, - LegacyContractSearchResponseTemplate, - LegacyDebriefingChallengesTemplate, - DebriefingChallengesTemplate, - LegacyLookupContractByIdTemplate, - EiderDashboard, - FrankensteinHubTemplate, - H2allunlockables, - H2DestinationsData, - H2StoreData, - H2ContractSearchResponseTemplate, - H2LocationsData, - H2LookupContractByIdTemplate: H2LookupContractTemplate, - H2LookupContractFavoriteTemplate: H2LookupContractTemplate, - H2FilterData, - H2CareerTemplate, - H2DashboardTemplate, - H2SniperContentTemplate, - FrankensteinMmSpTemplate, - FrankensteinMmMpTemplate, - FrankensteinPlanningTemplate, - FrankensteinScoreOverviewTemplate, - Videos, - ChallengeLocationTemplate, - H2ChallengeLocationTemplate, - LegacyChallengeLocationTemplate, - ReportTemplate, - ContractSearchPageTemplate, - ContractSearchPaginateTemplate, - ContractSearchResponseTemplate, - MasteryUnlockablesTemplate, - Scpcallunlockables, - ScpcLocationsData, - DiscordRichAssetsForBricks, - EscalationCodenames, - ScoreOverviewTemplate, - PeacockGameChangerProperties, - MultiplayerPresets, - LobbySlimTemplate, - MasteryDataForLocationTemplate, - LegacyMasteryLocationTemplate, - DefaultCpdConfig, - EvergreenGameChangerProperties, - AreaMap, - ArcadePageTemplate, - HitsCategoryElusiveTemplate, - HitsCategoryContractAttackTemplate, - MissionRewardsTemplate, - SniperUnlockables, -} - -Object.keys(configs).forEach((cfg) => { - // Parse the string into an object - configs[cfg] = JSON.parse(configs[cfg]) -}) - -export { configs } - -/** - * Get a config file. - * Configs for H1 start with "Legacy", "H2" for HITMAN 2, and no prefix for HITMAN 3. - * - * @param config The name of the config file. - * @param clone If the value should be cloned (saves memory if false, but as a side effect, modifications will affect the actual config). - * @returns The config. - * @throws {Error} If the config file specified doesn't exist. - */ -export function getConfig( - config: keyof typeof configs, - clone: boolean, -): T { - if (configs.hasOwnProperty.call(configs, config)) { - if (!clone) { - return configs[config] - } - - // properly create object clones - // this could be better, but this is the best temporary solution - return fastClone(configs[config]) - } - - throw new Error(`Tried to lookup config that does not exist: ${config}`) -} - -/** - * Get a config file intended for the specified game version. - * - * @param config The name of the config file. - * @param gameVersion The game's version ("h1", "h2", or "h3"). - * @param clone If the config should be cloned (saves memory if false, but as a side effect, modifications will affect the actual config). - * @returns The config. - * @see getConfig - * @throws {Error} If the config file specified doesn't exist. - */ -export function getVersionedConfig( - config: string, - gameVersion: GameVersion, - clone: boolean, -): T { - let h1Prefix = "" - - if ( - // is this scpc, do we have a scpc config? - gameVersion === "scpc" && - Object.prototype.hasOwnProperty.call(configs, `Scpc${config}`) - ) { - h1Prefix = "Scpc" - } else { - // the above condition wasn't true - if (["scpc", "h1"].includes(gameVersion)) { - h1Prefix = "Legacy" - } - } - - // if this is H2, but we don't have a h2 specific config, fall back to h3 - if ( - gameVersion === "h2" && - !Object.prototype.hasOwnProperty.call(configs, `H2${config}`) - ) { - return getConfig(config, clone) - } - - return getConfig( - `${h1Prefix}${gameVersion === "h2" ? "H2" : ""}${config}`, - clone, - ) -} diff --git a/components/contracts/contractRouting.ts b/components/contracts/contractRouting.ts index cfa48ae24..bfdee4aea 100644 --- a/components/contracts/contractRouting.ts +++ b/components/contracts/contractRouting.ts @@ -33,7 +33,7 @@ import { setupScoring, } from "../eventHandler" import { controller } from "../controller" -import { getConfig } from "../configSwizzleManager" +import { getConfig } from "../configManager" import type { CreateFromParamsBody, GameChanger, diff --git a/components/contracts/contractsModeRouting.ts b/components/contracts/contractsModeRouting.ts index 9e71d8b5f..8e6845519 100644 --- a/components/contracts/contractsModeRouting.ts +++ b/components/contracts/contractsModeRouting.ts @@ -22,7 +22,7 @@ import type { RequestWithJwt, } from "../types/types" import type { Response } from "express" -import { getConfig, getVersionedConfig } from "../configSwizzleManager" +import { getConfig, getVersionedConfig } from "../configManager" import { getUserData } from "../databaseHandler" import { generateUserCentric } from "./dataGen" import { controller, preserveContracts } from "../controller" diff --git a/components/contracts/dataGen.ts b/components/contracts/dataGen.ts index 265b52f80..88ae9c03e 100644 --- a/components/contracts/dataGen.ts +++ b/components/contracts/dataGen.ts @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { getConfig, getVersionedConfig } from "../configSwizzleManager" +import { getConfig, getVersionedConfig } from "../configManager" import type { CompletionData, GameChanger, diff --git a/components/contracts/hitsCategoryService.ts b/components/contracts/hitsCategoryService.ts index 549aef1d0..d22f01357 100644 --- a/components/contracts/hitsCategoryService.ts +++ b/components/contracts/hitsCategoryService.ts @@ -16,7 +16,6 @@ * along with this program. If not, see . */ -import { HookMap, SyncHook } from "../hooksImpl" import { ContractHistory, GameVersion, @@ -35,6 +34,7 @@ import { log, LogLevel } from "../loggingInterop" import { fastClone, getRemoteService } from "../utils" import { orderedETAs } from "./elusiveTargetArcades" import { missionsInLocations } from "./missionsInLocation" +import { HookMap, SyncHook } from "tapable" /** * The filters supported for HitsCategories. diff --git a/components/controller.ts b/components/controller.ts index 89a197d2f..5ab7b9e6a 100644 --- a/components/controller.ts +++ b/components/controller.ts @@ -41,8 +41,6 @@ import type { Unlockable, UserCentricContract, } from "./types/types" -import type * as configManagerType from "./configSwizzleManager" -import { configs, getConfig, getVersionedConfig } from "./configSwizzleManager" import { log, LogLevel } from "./loggingInterop" import * as axios from "axios" import { @@ -52,7 +50,6 @@ import { hitmapsUrl, versions, } from "./utils" -import { AsyncSeriesHook, SyncBailHook, SyncHook } from "./hooksImpl" import { parse } from "json5" import { userAuths } from "./officialServerAuth" // @ts-expect-error Ignore JSON import @@ -77,6 +74,8 @@ import generatedPeacockRequireTable from "./generatedPeacockRequireTable" import { escalationTypes } from "./contracts/escalations/escalationService" import { orderedETAs } from "./contracts/elusiveTargetArcades" import { SMFSupport } from "./smfSupport" +import { AsyncSeriesHook, SyncBailHook, SyncHook } from "tapable" +import { getVersionedConfig } from "./configManager" /** * An array of string arrays that contains the IDs of the featured contracts. @@ -307,8 +306,7 @@ function registerInternals(contracts: MissionManifest[]): void { export class Controller { public hooks: { serverStart: SyncHook<[]> - challengesLoaded: SyncHook<[]> - masteryDataLoaded: SyncHook<[]> + resourcesLoaded: SyncHook<[]> newEvent: SyncHook< [ /** event */ ClientToServerEvent, @@ -351,11 +349,7 @@ export class Controller { PlayNextGetCampaignsHookReturn | undefined > onMissionEnd: SyncHook<[/** session */ ContractSession]> - } - public configManager: typeof configManagerType = { - getConfig, - configs, - getVersionedConfig, + groupSearchDiscovery: SyncHook<[string[]]> } public missionsInLocations = missionsInLocations /** @@ -379,21 +373,26 @@ export class Controller { public locationsWithETA = new Set() public parentsWithETA = new Set() - /** - * The constructor. - */ - public constructor() { + constructor() { this.hooks = { serverStart: new SyncHook(), - challengesLoaded: new SyncHook(), - masteryDataLoaded: new SyncHook(), - newEvent: new SyncHook(), - newMetricsEvent: new SyncHook(), - getContractManifest: new SyncBailHook(), - contributeCampaigns: new SyncHook(), - getSearchResults: new AsyncSeriesHook(), - getNextCampaignMission: new SyncBailHook(), - onMissionEnd: new SyncHook(), + resourcesLoaded: new SyncHook(), + newEvent: new SyncHook(["event", "request", "session"]), + newMetricsEvent: new SyncHook(["event", "request"]), + getContractManifest: new SyncBailHook(["contractId"]), + contributeCampaigns: new SyncHook([ + "campaigns", + "genSingleMissionFunc", + "genSingleVideoFunc", + "gameVersion", + ]), + getSearchResults: new AsyncSeriesHook(["query", "contractIds"]), + getNextCampaignMission: new SyncBailHook([ + "contractId", + "gameVersion", + ]), + onMissionEnd: new SyncHook(["session"]), + groupSearchDiscovery: new SyncHook(), } } @@ -403,12 +402,6 @@ export class Controller { * @throws {Error} If all hope is lost. (In theory, this should never happen) */ async boot(pluginDevHost: boolean): Promise { - // this should never actually be hit, but it makes IntelliJ not - // complain that it's unused, so... - if (!this.configManager) { - throw new Error("All hope is lost.") - } - log( LogLevel.INFO, "Booting Peacock internal services - this may take a moment.", @@ -443,8 +436,7 @@ export class Controller { try { await this._loadResources() - this.hooks.challengesLoaded.call() - this.hooks.masteryDataLoaded.call() + this.hooks.resourcesLoaded.call() } catch (e) { log( LogLevel.ERROR, @@ -1027,10 +1019,12 @@ export class Controller { require: createPeacockRequire(pluginName), }) - let theExports + type Plugin = (controller: Controller) => Promise | void + type ESMPlugin = { default: Plugin; __esModule?: true } + + let theExports: ESMPlugin | Plugin try { - // eslint-disable-next-line prefer-const theExports = new Script(pluginContents, { filename: pluginPath, }).runInContext(context) @@ -1045,17 +1039,17 @@ export class Controller { } try { - let plugin = theExports - - if (theExports.__esModule) { - // the plugin thinks it's an ES module (incorrectly, as it's in - // a CommonJS environment, meaning the plugin was likely written - // as a module, and then compiled by a tool), so the actual - // function will likely be on the 'default' property - plugin = theExports.default ?? theExports - } - - await (plugin as (controller: Controller) => Promise)(this) + // if __esModule, the plugin thinks it's an ES module + // (yet it's in a CommonJS environment, meaning the plugin + // was likely written as a module, and then compiled by a tool), + // so the actual function will likely be on the 'default' property + const plugin: Plugin = (theExports).__esModule + ? // fake esm + (theExports).default ?? (theExports as Plugin) + : // commonjs + theExports + + await (plugin)(this) } catch (e) { log(LogLevel.ERROR, `Error while evaluating plugin ${pluginName}!`) log(LogLevel.ERROR, e) @@ -1087,9 +1081,13 @@ export class Controller { scanForGroups(): void { let groupCount = 0 + const groupsFromHook: string[] = [] + + this.hooks.groupSearchDiscovery.call(groupsFromHook) + allGroups: for (const contractId of new Set([ ...Object.keys(internalContracts), - ...this.hooks.getContractManifest.allTapNames, + ...groupsFromHook, ])) { const contract = this.resolveContract(contractId) diff --git a/components/discordRp.ts b/components/discordRp.ts index d31dea78b..78878137e 100644 --- a/components/discordRp.ts +++ b/components/discordRp.ts @@ -17,7 +17,7 @@ */ import { RPCClient } from "./discord/client" -import { getConfig } from "./configSwizzleManager" +import { getConfig } from "./configManager" import type { GameVersion, MissionType } from "./types/types" import { getFlag } from "./flags" import { log, LogLevel } from "./loggingInterop" diff --git a/components/eventHandler.ts b/components/eventHandler.ts index 98b52f260..051cff033 100644 --- a/components/eventHandler.ts +++ b/components/eventHandler.ts @@ -64,7 +64,7 @@ import { } from "./types/events" import picocolors from "picocolors" import { setCpd } from "./evergreen" -import { getConfig } from "./configSwizzleManager" +import { getConfig } from "./configManager" import { resetUserEscalationProgress } from "./contracts/escalations/escalationService" import { ManifestScoringDefinition, diff --git a/components/evergreen.ts b/components/evergreen.ts index d32d25cc4..90a0f2363 100644 --- a/components/evergreen.ts +++ b/components/evergreen.ts @@ -17,7 +17,7 @@ */ import { getUserData, writeUserData } from "./databaseHandler" -import { getConfig } from "./configSwizzleManager" +import { getConfig } from "./configManager" import { ContractProgressionData } from "./types/types" import { getFlag } from "./flags" import { EVERGREEN_LEVEL_INFO } from "./utils" diff --git a/components/generatedPeacockRequireTable.ts b/components/generatedPeacockRequireTable.ts index 49d49b927..2b6da76f0 100644 --- a/components/generatedPeacockRequireTable.ts +++ b/components/generatedPeacockRequireTable.ts @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import * as configSwizzleManager from "./configSwizzleManager" +import * as configManager from "./configManager" import * as controller from "./controller" import * as databaseHandler from "./databaseHandler" import * as discordRp from "./discordRp" @@ -24,7 +24,6 @@ import * as entitlementStrategies from "./entitlementStrategies" import * as eventHandler from "./eventHandler" import * as evergreen from "./evergreen" import * as flags from "./flags" -import * as hooksImpl from "./hooksImpl" import * as hotReloadService from "./hotReloadService" import * as inventory from "./inventory" import * as loadouts from "./loadouts" @@ -86,9 +85,9 @@ import * as types from "./types/types" import * as escalationService from "./contracts/escalations/escalationService" export default { - "@peacockproject/core/configSwizzleManager": { + "@peacockproject/core/configManager": { __esModule: true, - ...configSwizzleManager, + ...configManager, }, "@peacockproject/core/controller": { __esModule: true, ...controller }, "@peacockproject/core/databaseHandler": { @@ -103,7 +102,6 @@ export default { "@peacockproject/core/eventHandler": { __esModule: true, ...eventHandler }, "@peacockproject/core/evergreen": { __esModule: true, ...evergreen }, "@peacockproject/core/flags": { __esModule: true, ...flags }, - "@peacockproject/core/hooksImpl": { __esModule: true, ...hooksImpl }, "@peacockproject/core/hotReloadService": { __esModule: true, ...hotReloadService, diff --git a/components/hooksImpl.ts b/components/hooksImpl.ts deleted file mode 100644 index 644ade904..000000000 --- a/components/hooksImpl.ts +++ /dev/null @@ -1,251 +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 . - */ - -/** - * A HookMap is a helper class for a Map with Hooks. - * - * @example - * const myHookMap = new HookMap(key => new SyncHook()) - * - * @example - * hookMap.for("some-key").tap("MyPlugin", (arg) => { }) - * - * @example - * const hook = hookMap.for("some-key") - * hook.call("some value", 123456) - */ -export class HookMap { - private readonly _map: Map - - public constructor(private readonly _createFunc: (key: string) => Hook) { - this._map = new Map() - } - - /** - * Get a hook for the given key. - * - * @param key The hook to get. - * @returns The hook. - */ - public for(key: string): Hook { - if (this._map.has(key)) { - return this._map.get(key)! - } - - const hook = this._createFunc(key) - this._map.set(key, hook) - return hook - } -} - -/** - * The options for a hook. Will either be just the name (as a string), or an object containing the additional options. - */ -export type TapOptions = string | { name: string; context: boolean } - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type AsArray = T extends any[] ? T : [T] - -/** - * An internal interface containing the properties held by a single taps' container object. - */ -interface Tap { - name: string - func: (...args: AsArray) => R - enableContext: boolean -} - -/** - * The structure of an intercept. - * - * @see name - * @see call - * @see tap - */ -export interface Intercept { - /** - * The name of the intercept. - */ - name: string - - /** - * A function called just after the hook is called, and before all taps run. - * - * @param context The context object. Can be modified. - * @param params The parameters that the taps will get. Can be modified. - */ - call(context, ...params: AsArray): void - - /** - * A function called when the hook is tapped. Note that it will not be called when an interceptor is registered, since that doesn't count as a tap. - * - * @param name The name of the tap. - * @param func The tap's function. - */ - tap(name: string, func: (...args: AsArray) => Return): void -} - -/** - * The base for a hook, including {@link tap} and {@link intercept} functionality. - * - * @see SyncHook - * @see SyncBailHook - * @see AsyncSeriesHook - */ -export abstract class BaseImpl { - protected _intercepts: Intercept[] - protected _taps: Tap[] - - /** - * Register an interceptor. - * Interceptors can listen for certain events, and control things like context for them. - * - * @param intercept An object containing the intercept. - * @see Intercept - */ - public intercept(intercept: Intercept): void { - this._intercepts.push(intercept) - } - - /** - * Tap the hook. - * - * @param nameOrOptions A string containing the tap's name, or an object containing the tap's details. - * @param consumer The function that will be called when the hook is. - * @see TapOptions - */ - public tap( - nameOrOptions: TapOptions, - consumer: (...args: AsArray) => Return, - ): void { - const name = - typeof nameOrOptions === "string" - ? nameOrOptions - : nameOrOptions.name - const enableContext = - typeof nameOrOptions === "string" ? false : nameOrOptions.context - - for (const intercept of this._intercepts) { - if (intercept.tap) { - intercept.tap(name, consumer) - } - } - - this._taps.push({ - name, - func: consumer, - enableContext, - }) - } - - public get allTapNames(): string[] { - return this._taps.map((t) => t.name) - } -} - -/** - * A hook that runs each tap one-by-one. - */ -export class SyncHook extends BaseImpl { - public constructor() { - super() - this._taps = [] - this._intercepts = [] - } - - public call(...params: AsArray): void { - const context = {} - - for (const intercept of this._intercepts) { - if (intercept.call) { - intercept.call(context, ...params) - } - } - - for (const tap of this._taps) { - const args = tap.enableContext ? [context, ...params] : [...params] - - // @ts-expect-error TypeScript things. - tap.func(...args) - } - - return - } -} - -/** - * A hook that runs each tap one-by-one until one returns a result. - */ -export class SyncBailHook extends BaseImpl { - public constructor() { - super() - this._taps = [] - this._intercepts = [] - } - - public call(...params: AsArray): Return | null { - const context = {} - - for (const intercept of this._intercepts) { - if (intercept.call) { - intercept.call(context, ...params) - } - } - - for (const tap of this._taps) { - const args = tap.enableContext ? [context, ...params] : [...params] - - // @ts-expect-error TypeScript things. - const result = tap.func(...args) - - if (result) { - return result - } - } - - return null - } -} - -/** - * A hook that runs each tap, one-by-one, in an async context (each tap may be an async function). - */ -export class AsyncSeriesHook extends BaseImpl> { - public constructor() { - super() - this._taps = [] - this._intercepts = [] - } - - public async callAsync(...params: AsArray): Promise { - const context = {} - - for (const intercept of this._intercepts) { - if (intercept.call) { - await intercept.call(context, ...params) - } - } - - for (const tap of this._taps) { - const args = tap.enableContext ? [context, ...params] : [...params] - - // @ts-expect-error TypeScript things. - await tap.func(...args) - } - } -} diff --git a/components/index.ts b/components/index.ts index 72e26484c..86ab30331 100644 --- a/components/index.ts +++ b/components/index.ts @@ -40,7 +40,7 @@ import { PEACOCKVERSTRING, ServerVer, } from "./utils" -import { getConfig } from "./configSwizzleManager" +import { getConfig } from "./configManager" import { handleOauthToken } from "./oauthToken" import type { RequestWithJwt, diff --git a/components/inventory.ts b/components/inventory.ts index a7bc5ab10..ebe645e15 100644 --- a/components/inventory.ts +++ b/components/inventory.ts @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { getConfig, getVersionedConfig } from "./configSwizzleManager" +import { getConfig, getVersionedConfig } from "./configManager" import type { GameVersion, Unlockable, UserProfile } from "./types/types" import { brokenItems, diff --git a/components/menuData.ts b/components/menuData.ts index 85967b1fc..2679458f1 100644 --- a/components/menuData.ts +++ b/components/menuData.ts @@ -29,7 +29,7 @@ import { uuidRegex, } from "./utils" import { contractSessions, getSession } from "./eventHandler" -import { getConfig, getVersionedConfig } from "./configSwizzleManager" +import { ConfigKey, getConfig, getVersionedConfig } from "./configManager" import { contractIdToHitObject, controller } from "./controller" import { makeCampaigns } from "./menus/campaigns" import { @@ -1553,7 +1553,7 @@ menuDataRouter.post( async (req: RequestWithJwt<{ sorting?: unknown }, string[]>, res) => { const specialContracts: string[] = [] - await controller.hooks.getSearchResults.callAsync( + await controller.hooks.getSearchResults.promise( req.body, specialContracts, ) @@ -1729,7 +1729,7 @@ menuDataRouter.get("/contractcreation/create", (req: RequestWithJwt, res) => { }) const createLoadSaveMiddleware = - (menuTemplate: string) => + (menuTemplate: ConfigKey) => ( req: RequestWithJwt< { diff --git a/components/menus/campaigns.ts b/components/menus/campaigns.ts index 93eb2cfa0..41e8d4762 100644 --- a/components/menus/campaigns.ts +++ b/components/menus/campaigns.ts @@ -27,7 +27,7 @@ import type { StoryData, } from "../types/types" import { log, LogLevel } from "../loggingInterop" -import { getConfig } from "../configSwizzleManager" +import { getConfig } from "../configManager" import { fastClone } from "../utils" import assert from "assert" diff --git a/components/menus/destinations.ts b/components/menus/destinations.ts index c678e7322..5007f060d 100644 --- a/components/menus/destinations.ts +++ b/components/menus/destinations.ts @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { getConfig, getVersionedConfig } from "../configSwizzleManager" +import { getConfig, getVersionedConfig } from "../configManager" import type { CompletionData, GameLocationsData, diff --git a/components/menus/favoriteContracts.ts b/components/menus/favoriteContracts.ts index 828d4cfa6..8c723774e 100644 --- a/components/menus/favoriteContracts.ts +++ b/components/menus/favoriteContracts.ts @@ -25,7 +25,7 @@ import type { import { controller } from "../controller" import { generateUserCentric } from "../contracts/dataGen" import { getUserData, writeUserData } from "../databaseHandler" -import { getVersionedConfig } from "../configSwizzleManager" +import { getVersionedConfig } from "../configManager" import type { Response } from "express" import { getUnlockableById } from "../inventory" diff --git a/components/menus/menuSystem.ts b/components/menus/menuSystem.ts index 59a1143bb..41b045823 100644 --- a/components/menus/menuSystem.ts +++ b/components/menus/menuSystem.ts @@ -20,12 +20,12 @@ import { NextFunction, Response, Router } from "express" import serveStatic from "serve-static" import { join } from "path" import md5File from "md5-file" -import { getConfig } from "../configSwizzleManager" +import { getConfig } from "../configManager" import { readFile } from "atomically" import { GameVersion, RequestWithJwt } from "../types/types" import { log, LogLevel } from "../loggingInterop" import { imageFetchingMiddleware } from "./imageHandler" -import { SyncBailHook, SyncHook } from "../hooksImpl" +import { SyncBailHook, SyncHook } from "tapable" /** * Router triggered before {@link menuSystemRouter}. diff --git a/components/menus/planning.ts b/components/menus/planning.ts index e94ac80cb..51fbce67f 100644 --- a/components/menus/planning.ts +++ b/components/menus/planning.ts @@ -30,7 +30,7 @@ import { getSubLocationFromContract, mapObjectives, } from "../contracts/dataGen" -import { getConfig } from "../configSwizzleManager" +import { getConfig } from "../configManager" import { getUserData, writeUserData } from "../databaseHandler" import { getDefaultSuitFor, diff --git a/components/menus/playnext.ts b/components/menus/playnext.ts index 69bb11c77..02c03b065 100644 --- a/components/menus/playnext.ts +++ b/components/menus/playnext.ts @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { getConfig } from "../configSwizzleManager" +import { getConfig } from "../configManager" import { generateUserCentric } from "../contracts/dataGen" import { controller } from "../controller" import type { diff --git a/components/multiplayer/multiplayerMenuData.ts b/components/multiplayer/multiplayerMenuData.ts index 1d2402354..a5d9bb150 100644 --- a/components/multiplayer/multiplayerMenuData.ts +++ b/components/multiplayer/multiplayerMenuData.ts @@ -25,7 +25,7 @@ import { MultiplayerScore, } from "./multiplayerUtils" import { Router } from "express" -import { getConfig } from "../configSwizzleManager" +import { getConfig } from "../configManager" import { MultiplayerPreset } from "./multiplayerService" import { generateUserCentric } from "../contracts/dataGen" import { controller } from "../controller" diff --git a/components/multiplayer/multiplayerService.ts b/components/multiplayer/multiplayerService.ts index cf9e87cbe..cc81b5785 100644 --- a/components/multiplayer/multiplayerService.ts +++ b/components/multiplayer/multiplayerService.ts @@ -28,7 +28,7 @@ import { } from "../types/types" import { nilUuid } from "../utils" import { randomUUID } from "crypto" -import { getConfig } from "../configSwizzleManager" +import { getConfig } from "../configManager" import { generateUserCentric } from "../contracts/dataGen" import { controller } from "../controller" import { MatchOverC2SEvent } from "../types/events" diff --git a/components/oauthToken.ts b/components/oauthToken.ts index fa7d782f3..e05503793 100644 --- a/components/oauthToken.ts +++ b/components/oauthToken.ts @@ -20,7 +20,7 @@ import type { Response } from "express" import { decode, sign } from "jsonwebtoken" import { extractToken, uuidRegex } from "./utils" import type { GameVersion, RequestWithJwt, UserProfile } from "./types/types" -import { getVersionedConfig } from "./configSwizzleManager" +import { getVersionedConfig } from "./configManager" import { log, LogLevel } from "./loggingInterop" import { STEAM_NAMESPACE_2018, @@ -198,7 +198,7 @@ export async function handleOauthToken( log(LogLevel.DEBUG, "Unable to load profile information.") } - /* + /* Store user auth for all games except scpc */ if (!isFrankenstein) { diff --git a/components/playStyles.ts b/components/playStyles.ts index aa24dd6f8..4ee070bae 100644 --- a/components/playStyles.ts +++ b/components/playStyles.ts @@ -18,7 +18,7 @@ import { Playstyle } from "./types/scoring" import { RatingKill } from "./types/types" -import { getConfig } from "./configSwizzleManager" +import { getConfig } from "./configManager" /** * Checks the criteria of each possible play-style, ranking them by scoring. diff --git a/components/profileHandler.ts b/components/profileHandler.ts index 94715b21a..c89bc7e55 100644 --- a/components/profileHandler.ts +++ b/components/profileHandler.ts @@ -47,7 +47,7 @@ import { writeUserData, } from "./databaseHandler" import { randomUUID } from "crypto" -import { getVersionedConfig } from "./configSwizzleManager" +import { getVersionedConfig } from "./configManager" import { createInventory } from "./inventory" import { controller } from "./controller" import { loadouts } from "./loadouts" diff --git a/components/scoreHandler.ts b/components/scoreHandler.ts index 9d7c0e383..7800aa451 100644 --- a/components/scoreHandler.ts +++ b/components/scoreHandler.ts @@ -32,7 +32,7 @@ import { xpRequiredForLevel, } from "./utils" import { contractSessions, getCurrentState } from "./eventHandler" -import { getConfig } from "./configSwizzleManager" +import { getConfig } from "./configManager" import { _theLastYardbirdScpc, controller } from "./controller" import type { ContractHistory, diff --git a/components/smfSupport.ts b/components/smfSupport.ts index 9af76fd27..885fd3f30 100644 --- a/components/smfSupport.ts +++ b/components/smfSupport.ts @@ -25,6 +25,7 @@ import { basename, join } from "path" import { readFile } from "fs/promises" import { menuSystemDatabase } from "./menus/menuSystem" import { parse } from "json5" +import { configManager } from "./configManager" type LastServerSideData = SMFLastDeploy["lastServerSideStates"] @@ -138,8 +139,14 @@ export class SMFSupport { private handleUnlockables(lastServerSideData: LastServerSideData) { if (lastServerSideData?.unlockables) { - this.controller.configManager.configs["allunlockables"] = - lastServerSideData.unlockables.slice(1) + configManager.hooks.getConfig.tap( + "internal_SMFUnlockablesPlugin", + (currentConfig, configName) => { + return configName === "allunlockables" + ? lastServerSideData.unlockables.slice(1) + : currentConfig + }, + ) } } diff --git a/components/tools.ts b/components/tools.ts index f6f91db23..9868239ee 100644 --- a/components/tools.ts +++ b/components/tools.ts @@ -32,7 +32,7 @@ import { join, resolve as pathResolve } from "path" import picocolors from "picocolors" import { Filename, npath, PortablePath, ppath, xfs } from "@yarnpkg/fslib" import { makeEmptyArchive, ZipFS } from "@yarnpkg/libzip" -import { configs } from "./configSwizzleManager" +import { configKeys } from "./configManager" const IMAGE_PACK_REPO = "thepeacockproject/ImagePack" @@ -117,7 +117,7 @@ async function exportDebugInfo(): Promise { const data = { version: PEACOCKVERSTRING, ident: PEACOCKVER, - presentConfigs: Object.keys(configs), + presentConfigs: configKeys, chunkDigest: await md5File("chunk0.js"), patcherDigest: await md5File("PeacockPatcher.exe"), runtimeVersions: process.versions, diff --git a/components/utils.ts b/components/utils.ts index 7ecabadd9..707fd4f51 100644 --- a/components/utils.ts +++ b/components/utils.ts @@ -32,7 +32,7 @@ import axios, { AxiosError } from "axios" import { log, LogLevel } from "./loggingInterop" import { writeFileSync } from "fs" import { getFlag } from "./flags" -import { getConfig, getVersionedConfig } from "./configSwizzleManager" +import { getConfig, getVersionedConfig } from "./configManager" /** * True if the server is being run by the launcher, false otherwise. diff --git a/components/webFeatures.ts b/components/webFeatures.ts index 3ba910e64..ddb92c32c 100644 --- a/components/webFeatures.ts +++ b/components/webFeatures.ts @@ -17,7 +17,7 @@ */ import { Request, Response, Router } from "express" -import { getConfig } from "./configSwizzleManager" +import { getConfig } from "./configManager" import { readFileSync } from "atomically" import { GameVersion, UserProfile } from "./types/types" import { join } from "path" diff --git a/packaging/build.mjs b/packaging/build.mjs index 87eff56c5..b72438f8f 100644 --- a/packaging/build.mjs +++ b/packaging/build.mjs @@ -57,6 +57,7 @@ await e.build({ "node-gyp-build-optional-packages", "msgpackr-extract", "esbuild-wasm", + "static", ], define: { PEACOCK_DEV: "false", diff --git a/tests/mocks/configSwizzleManager.ts b/tests/mocks/configSwizzleManager.ts deleted file mode 100644 index 023956c04..000000000 --- a/tests/mocks/configSwizzleManager.ts +++ /dev/null @@ -1,35 +0,0 @@ -import * as configSwizzleManager from "../../components/configSwizzleManager" -import { readFileSync } from "fs" - -const originalFilePaths: Record = {} - -Object.keys(configSwizzleManager.configs).forEach((config: string) => { - originalFilePaths[config] = configSwizzleManager.configs[config] - - configSwizzleManager.configs[config] = undefined -}) - -export function loadConfig(config: string) { - if (!originalFilePaths[config]) { - return - } - - const contents = readFileSync(originalFilePaths[config], "utf-8") - - configSwizzleManager.configs[config] = JSON.parse(contents) -} - -export function setConfig(config: string, data: unknown) { - configSwizzleManager.configs[config] = data -} - -const getConfigOriginal = configSwizzleManager.getConfig -vi.spyOn(configSwizzleManager, "getConfig").mockImplementation( - (config: string, clone: boolean) => { - if (!configSwizzleManager.configs[config]) { - throw `Config '${config}' has not been loaded!` - } - - return getConfigOriginal(config, clone) - }, -) diff --git a/tests/setup/globalDefines.ts b/tests/setup/globalDefines.ts index c628c1152..1f76bae52 100644 --- a/tests/setup/globalDefines.ts +++ b/tests/setup/globalDefines.ts @@ -1,3 +1,24 @@ +/* + * 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 { readFileSync } from "fs" +import path from "path" + Object.assign(globalThis, { PEACOCK_DEV: false, HUMAN_VERSION: "test", @@ -7,4 +28,24 @@ Object.assign(globalThis, { process.env.TEST = "peacock" process.env.LOG_LEVEL_FILE = "none" +const configManager = await import("../../components/configManager.ts") + +const fakeRequireCache: Record = {} + +function loadConfig(configPath: string) { + if (fakeRequireCache[configPath]) { + return fakeRequireCache[configPath] + } + + const contents = readFileSync( + path.join(process.cwd(), "../", configPath), + "utf-8", + ) + fakeRequireCache[configPath] = JSON.parse(contents) + return fakeRequireCache[configPath] +} + +// @ts-expect-error This version of require doesn't need to implement real require things. +configManager.configManager._require = loadConfig + export {} diff --git a/tests/src/oauthToken.test.ts b/tests/src/oauthToken.test.ts index e725f4b6f..5d77fae9f 100644 --- a/tests/src/oauthToken.test.ts +++ b/tests/src/oauthToken.test.ts @@ -17,11 +17,11 @@ */ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { UserProfile } from "../../components/types/types" -import { handleOauthToken, JWT_SECRET } from "../../components/oauthToken" +import { UserProfile } from "../../components/types/types.ts" +import { handleOauthToken, JWT_SECRET } from "../../components/oauthToken.ts" import { sign, verify } from "jsonwebtoken" -import * as databaseHandler from "../../components/databaseHandler" -import * as platformEntitlements from "../../components/platformEntitlements" +import * as databaseHandler from "../../components/databaseHandler.ts" +import * as platformEntitlements from "../../components/platformEntitlements.ts" import axios from "axios" import { describe, expect, beforeEach, vi, it } from "vitest" @@ -31,7 +31,7 @@ import { mockRequestWithJwt, mockRequestWithValidJwt, mockResponse, -} from "../helpers/testHelpers" +} from "../helpers/testHelpers.ts" describe("oauthToken", () => { const pId = "00000000-0000-0000-0000-000000000047" diff --git a/tests/src/playStyles.test.ts b/tests/src/playStyles.test.ts index 994130a37..181347508 100644 --- a/tests/src/playStyles.test.ts +++ b/tests/src/playStyles.test.ts @@ -16,13 +16,10 @@ * along with this program. If not, see . */ -import { loadConfig } from "../mocks/configSwizzleManager" -import { ContractSession, RatingKill } from "../../components/types/types" -import { calculatePlaystyle } from "../../components/playStyles" +import { ContractSession, RatingKill } from "../../components/types/types.ts" +import { calculatePlaystyle } from "../../components/playStyles.ts" import { describe, expect, test } from "vitest" -loadConfig("Playstyles") - describe("calculatePlaystyle", () => { test("default", () => { const contractSession = { diff --git a/tests/tsconfig.json b/tests/tsconfig.json index 78b315171..f56853101 100644 --- a/tests/tsconfig.json +++ b/tests/tsconfig.json @@ -4,7 +4,12 @@ // Reset rootDir to default to make rootDirs take effect "rootDir": null, "rootDirs": ["../components", "."], - "types": ["vitest/globals"] + "types": ["vitest/globals"], + "module": "Node16", + "allowImportingTsExtensions": true, + "target": "ESNext", + "moduleResolution": "nodenext", + "moduleDetection": "force" }, "include": ["../components", "**/*.ts"], "exclude": []