1
mirror of https://github.com/thepeacockproject/Peacock synced 2024-11-22 22:12:45 +01:00
Peacock/components/menus/menuSystem.ts
2023-04-25 07:14:17 +01:00

552 lines
24 KiB
TypeScript

/*
* The Peacock Project - a HITMAN server replacement.
* Copyright (C) 2021-2023 The Peacock Project Team
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { NextFunction, Response, Router } from "express"
import serveStatic from "serve-static"
import { join } from "path"
import md5File from "md5-file"
import { getConfig } from "../configSwizzleManager"
import { readFile } from "atomically"
import { GameVersion, RequestWithJwt } from "../types/types"
import { log, LogLevel } from "../loggingInterop"
import { imageFetchingMiddleware } from "./imageHandler"
import { SyncBailHook, SyncHook } from "../hooksImpl"
/**
* Router triggered before {@link menuSystemRouter}.
*/
const menuSystemPreRouter = Router()
const menuSystemRouter = Router()
// /resources-8-11/
/**
* A class for managing the menu system's fetched JSON data.
*/
export class MenuSystemDatabase {
/**
* The hooks.
*/
hooks: {
/**
* A hook for getting a list of configurations which the game should
* fetch from the server.
*
* Params:
* - configs: The configurations list (mutable). These should be full paths,
* for instance, `/menusystem/data/testing.json`.
* - gameVersion: The game's version.
*/
getDatabaseDiff: SyncHook<
[/** configs */ string[], /** gameVersion */ GameVersion]
>
/**
* A hook for getting the requested configuration.
*
* Params:
* - configName: The requested file's name.
* - gameVersion: The game's version.
*
* Returns: The file as an object.
*/
getConfig: SyncBailHook<
[/** configName */ string, /** gameVersion */ GameVersion],
unknown
>
}
constructor() {
this.hooks = {
getDatabaseDiff: new SyncHook(),
getConfig: new SyncBailHook(),
}
this.hooks.getDatabaseDiff.tap(
"PeacockInternal",
(configs, gameVersion) => {
if (gameVersion === "h3") {
configs.push(
"menusystem/elements/settings/data/isnonvroptionvisible.json",
)
configs.push(
"images/unlockables/outfit_ef223b60-b53a-4c7b-b914-13c3310fc61a_0.jpg",
)
}
if (["h3", "h1"].includes(gameVersion)) {
configs.push("menusystem/pages/hub/hub_page.json")
}
configs.push("menusystem/data/ishitman3available.json")
configs.push("menusystem/pages/hub/modals/roadmap/modal.json")
configs.push(
"menusystem/pages/hub/data/isfullmenuavailable.json",
)
if (["h3", "h2"].includes(gameVersion)) {
configs.push(
"menusystem/pages/hub/dashboard/dashboard.json",
)
configs.push(
"menusystem/pages/hub/dashboard/category_escalation/result.json",
)
configs.push(
"menusystem/elements/contract/hitscategory_elusive.json",
)
// The following is to allow restart/replan/save/load on elusive contracts
// alongside removing the warning when starting one in H2/3 - AF
configs.push(
"menusystem/pages/pause/pausemenu/restart.json",
)
configs.push("menusystem/pages/pause/pausemenu/replan.json")
configs.push("menusystem/pages/pause/pausemenu/save.json")
configs.push("menusystem/pages/pause/pausemenu/load.json")
configs.push(
"menusystem/pages/planning/actions/actions_contextbutton_play.json",
)
}
if (gameVersion === "h2") {
configs.push("menusystem/data/ismultiplayeravailable.json")
configs.push(
"menusystem/pages/multiplayer/content/lobbyslim.json",
)
configs.push(
"menusystem/elements/contract/contractshitcategoryloading.json",
)
}
},
)
this.hooks.getConfig.tap("PeacockInternal", (name, gameVersion) => {
switch (name) {
case "/elements/settings/data/isnonvroptionvisible.json":
return {
$if: {
$condition: {
$and: ["$isingame", "$not $isineditor"],
},
$then: "$eq($vrmode,off)",
$else: true,
},
}
case "/elements/contract/hitscategory_elusive.json":
return getConfig("HitsCategoryElusiveTemplate", false)
case "/elements/contract/contractshitcategoryloading.json":
return {
controller: "group",
view: "menu3.containers.ScrollingListContainer",
layoutchildren: true,
id: "hitscategory_container",
nrows: 3,
ncols: 10,
pressable: false,
data: { direction: "horizontal" },
actions: {
activated: {
"load-async": {
path: {
"$if $eq ($.Category,Elusive_Target_Hits)":
{
$then: "menusystem/elements/contract/hitscategory_elusive.json",
$else: "menusystem/elements/contract/hitscategory.json",
},
},
from: {
url: "hitscategory",
args: {
page: 0,
type: "$.Category",
mode: "dataonly",
},
},
target: "hitscategory_container",
showloadingindicator: true,
blocksinput: false,
"post-load-action": [
{
"set-selected": {
target: "hitscategory_container",
},
},
],
},
},
},
children: [{ pressable: false, selectable: true }],
}
case "/data/ishitman3available.json":
return {
"$if $eq (0,0)": {
$then: "$isonline",
$else: false,
},
}
case "/pages/hub/modals/roadmap/modal.json":
return getConfig("Roadmap", false)
case "/pages/hub/hub_page.json":
return getConfig("HubPageData", false)
case "/pages/hub/data/isfullmenuavailable.json":
return {
"$if $not $isuser freeprologue": {
$then: true,
$else: {
$and: [
"$not $eq($platform,izumo)",
"$isonline",
],
},
},
}
case "/pages/hub/dashboard/dashboard.json":
if (gameVersion === "h3") {
return getConfig("EiderDashboard", false)
} else if (gameVersion === "h2") {
return getConfig("H2DashboardTemplate", false)
}
return undefined
case "/pages/hub/dashboard/category_escalation/result.json":
return getConfig("DashboardCategoryEscalation", false)
case "/data/ismultiplayeravailable.json":
return {
"$if $eq ($platform,stadia)": {
$then: false,
$else: true,
},
}
case "/pages/multiplayer/content/lobbyslim.json":
return getConfig("LobbySlimTemplate", false)
case "/pages/pause/pausemenu/restart.json":
return {
$if: {
$condition: {
$or: [
"$not $eq({$currentcontractcontext}.ContractType,evergreen)",
"$isallowedtorestart",
],
},
$then: {
$include: {
$path: "menusystem/pages/pause/pausemenu/restartnoconditions.json",
},
},
},
}
case "/pages/pause/pausemenu/replan.json":
return {
$if: {
$condition: {
$and: [
"$not $eq({$currentcontractcontext}.ContractLocation,LOCATION_ICA_FACILITY)",
"$not $eq({$currentcontractcontext}.ContractType,tutorial)",
"$not $eq({$currentcontractcontext}.ContractType,vsrace)",
"$not $eq({$currentcontractcontext}.ContractType,evergreen)",
],
},
$then: {
"$if $isallowedtorestart": {
$then: {
view: "menu3.basic.ListElementSmall",
pressable:
"$not $isnull {$currentcontractcontext}.Contract",
selectable:
"$not $isnull {$currentcontractcontext}.Contract",
data: {
showningame: "$isingame",
title: "$loc UI_MENU_PAGE_PAUSE_REPLAN",
icon: "planning",
},
actions: {
accept: {
"$if $eq ({$currentcontractcontext}.ContractType,placeholder)":
{
$then: {
$datacontext: {
in: "$.",
datavalues: {
AutoStart:
false,
},
do: {
$include: {
$path: "menusystem/pages/pause/pausemenu/replanplaceholder.json",
},
},
},
},
$else: {
"replan-level": {},
},
},
},
},
},
},
},
},
}
case "/pages/pause/pausemenu/save.json":
return {
$if: {
$condition: {
$and: [
"$not $eq({$currentcontractcontext}.ContractType,arcade)",
"$not $eq({$currentcontractcontext}.ContractType,evergreen)",
"$not $eq({$currentcontractcontext}.ContractType,sniper)",
"$not $eq({$currentcontractcontext}.ContractType,vsrace)",
],
},
$then: {
$datacontext: {
in: "$.",
datavalues: {
CanSave: "$cansave",
},
do: {
view: "menu3.basic.ListElementSmall",
selectable: "$.CanSave",
pressable: "$.CanSave",
data: {
showningame: "$isingame",
title: "$loc UI_MENU_PAGE_PAUSE_SAVE",
icon: {
"$if $.CanSave": {
$then: "save",
$else: "savedisabled",
},
},
greyelement: "$not $.CanSave",
},
actions: {
"$if $.CanSave": {
$then: {
accept: {
link: {
page: "savepage",
args: {
url: "save",
args: {
saveorload:
"save",
},
saveorload:
"save",
},
},
},
},
},
},
},
},
},
},
}
case "/pages/pause/pausemenu/load.json":
return {
$if: {
$condition: {
$and: [
"$not $eq({$currentcontractcontext}.ContractType,arcade)",
"$not $eq({$currentcontractcontext}.ContractType,evergreen)",
"$not $eq({$currentcontractcontext}.ContractType,sniper)",
"$not $eq({$currentcontractcontext}.ContractType,vsrace)",
],
},
$then: {
view: "menu3.basic.ListElementSmall",
data: {
showningame: "$isingame",
title: "$loc UI_MENU_PAGE_PAUSE_LOAD_GAME",
icon: "load",
},
actions: {
accept: {
link: {
page: "loadpage",
args: {
url: "load",
args: {
saveorload: "load",
},
saveorload: "load",
mainmenu: false,
reloadonchange: true,
},
},
},
},
},
},
}
// Following exists in the files of H3, but not H2. No need to put it in the diff. - AF
case "/pages/pause/pausemenu/restartnoconditions.json":
return {
view: "menu3.basic.ListElementSmall",
data: {
showningame: "$isingame",
title: {
"$if $eq ({$currentcontractcontext}.ContractType,vsrace)":
{
$then: "$loc UI_MENU_LOBBY_REMATCH",
$else: "$loc UI_MENU_PAGE_PAUSE_RESTART",
},
},
icon: "replay",
},
actions: {
accept: {
"$if $eq ({$currentcontractcontext}.ContractType,placeholder)":
{
$then: {
$datacontext: {
in: "$.",
datavalues: {
AutoStart: true,
},
do: {
$include: {
$path: "menusystem/pages/pause/pausemenu/replanplaceholder.json",
},
},
},
},
$else: {
"restart-level": {},
},
},
},
},
}
case "/pages/planning/actions/actions_contextbutton_play.json":
return {
$mergeobjects: [
{
accept: {
"start-contract": {
contract: "$.Contract",
difficulty:
"$.@parent.CurrentDifficulty",
objectives: "$.@parent.Objectives",
},
},
},
{
$include: {
$path: "menusystem/pages/planning/actions/actions_contextbutton_common.json",
},
},
],
}
default:
return undefined
}
})
}
/**
* @internal
*/
_getNamedConfig(configName: string, gameVersion: GameVersion): unknown {
return this.hooks.getConfig.call(configName, gameVersion)
}
/**
* Express middleware for fetching configurations.
*
* @param req The request.
* @param res The response.
* @param next The next function.
*/
static configMiddleware(
req: RequestWithJwt,
res: Response,
next: NextFunction,
): void {
const config = menuSystemDatabase._getNamedConfig(
req.path,
req.gameVersion,
)
if (config) {
res.json(config)
return
}
log(LogLevel.DEBUG, `Unable to resolve config ${req.path}, skipping...`)
next()
}
}
export const menuSystemDatabase = new MenuSystemDatabase()
menuSystemRouter.get(
"/dynamic_resources_pc_release_rpkg",
async (req: RequestWithJwt, res) => {
const dynamicResourceName = `dynamic_resources_${req.gameVersion}.rpkg`
const dynamicResourcePath = join(
PEACOCK_DEV ? process.cwd() : __dirname,
"resources",
dynamicResourceName,
)
log(
LogLevel.DEBUG,
`Serving dynamic resources from file ${dynamicResourceName}.`,
)
const hash = await md5File(dynamicResourcePath)
res.set("Content-Type", "application/octet-stream")
res.set("Content-MD5", Buffer.from(hash, "hex").toString("base64"))
res.send(await readFile(dynamicResourcePath))
},
)
menuSystemRouter.use("/menusystem/", MenuSystemDatabase.configMiddleware)
// Miranda Jamison's image path in the repository is escaped for some reason
menuSystemPreRouter.get(
"/images%5Cactors%5Celusive_goldendoublet_face.jpg",
(req, res, next) => {
req.url = "/images/actors/elusive_goldendoublet_face.jpg"
next("router")
},
)
// Sully Bowden is the same (come on IOI!)
menuSystemPreRouter.get(
"/images%5Cactors%5Celusive_redsnapper_face.jpg",
(req, res, next) => {
req.url = "/images/actors/elusive_redsnapper_face.jpg"
next("router")
},
)
menuSystemRouter.use(
"/images/",
serveStatic("images", { fallthrough: true }),
imageFetchingMiddleware,
)
export { menuSystemRouter, menuSystemPreRouter }