mirror of
https://github.com/thepeacockproject/Peacock
synced 2024-11-22 22:12:45 +01:00
552 lines
24 KiB
TypeScript
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 }
|